blankart/src/k_items.c
GenericHeroGuy 9efc468979 Updated item toggles menu
Invinc is animated on the title screen, superring now gets locked out
Small odds code cleanup since the landmine's result is finally gone
2025-11-15 17:18:33 +01:00

2413 lines
66 KiB
C

// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 2018-2025 by Kart Krew.
// Copyright (C) 2025 by "yama".
// Copyright (C) 2025 Blankart Team.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file k_items.c
/// \brief Kart item systems.
// SRB2kart Roulette Code - Position Based
#include "doomdef.h"
#include "doomstat.h"
#include "d_netcmd.h"
#include "d_player.h"
#include "g_game.h"
#include "info.h"
#include "m_easing.h" // Invincibility gradienting
#include "m_fixed.h"
#include "m_random.h"
#include "p_local.h"
#include "p_mobj.h"
#include "p_setup.h"
#include "s_sound.h"
#include "typedef.h"
#include "deh_tables.h"
#include "lua_hook.h"
#include "z_zone.h"
#include "k_battle.h"
#include "k_boss.h"
#include "k_bot.h"
#include "k_kart.h"
#include "k_waypoint.h"
#include "k_director.h"
#include "k_cluster.hpp"
#include "k_items.h"
#include "k_collide.h"
kartitem_t kartitems[MAXKARTITEMS] = {0};
kartresult_t kartresults[MAXKARTRESULTS] = {0};
UINT8 numkartitems = 0;
UINT8 numkartresults = 0;
UINT8 oddstablemax[MAXODDSTABLES] = { MAXODDS, 2, 4 };
static CV_PossibleValue_t kartdebugitem_cons_t[MAXKARTITEMS] = {{0, "NONE"}, {0, NULL}};
consvar_t cv_kartdebugitem = CVAR_INIT ("kartdebugitem", "NONE", CV_NETVAR|CV_CHEAT, kartdebugitem_cons_t, NULL);
static CV_PossibleValue_t kartdebugamount_cons_t[] = {{1, "MIN"}, {255, "MAX"}, {0, NULL}};
consvar_t cv_kartdebugamount = CVAR_INIT ("kartdebugamount", "1", CV_NETVAR|CV_CHEAT, kartdebugamount_cons_t, NULL);
static CV_PossibleValue_t kartitemvariant_cons_t[] = {{0, "Legacy"}, {1, "Alternative"}, {0, NULL}};
#define NUMKARTODDS (MAXODDS*10)
// Base multiplication to ALL item odds to simulate fractional precision.
// Reduced from 4 to 1 due to 75-count probability.
#define BASEODDSMUL 1
// Multiplication to odds for Battle item odds specifically.
#define BATTLEODDSMUL 4
// Maximum probability.
#define REALMAXPROB 75
#define MAXPROBABILITY (REALMAXPROB * BASEODDSMUL)
#define REALMAXBATTLEPROB 10
#define MAXBATTLEPROBABILITY (REALMAXBATTLEPROB * BATTLEODDSMUL)
void K_RegisterItem(kartitemtype_e itemtype)
{
kartdebugitem_cons_t[itemtype].strvalue = DEH_KartItemName(itemtype);
kartdebugitem_cons_t[itemtype].value = itemtype;
kartdebugitem_cons_t[itemtype + 1].strvalue = NULL;
kartdebugitem_cons_t[itemtype + 1].value = 0;
}
kartresult_t *K_RegisterResult(const char *name, boolean alternate)
{
kartresult_t *result = &kartresults[numkartresults];
if (alternate)
{
// link to the normal result
kartresult_t *prev = K_GetKartResult(name);
if (prev == NULL)
I_Error("K_RegisterResult: Result %s doesn't exist, can't create alt result", name);
result->cvar = prev->cvar;
result->type = prev->type;
result->amount = prev->amount;
result->isalt = true;
// check if the kartitem needs to have its alt cvar registered
kartitem_t *item = &kartitems[result->type];
if (item->altcvar == NULL)
{
char *cvname = malloc(strlen(DEH_KartItemName(result->type))+1 + 8);
sprintf(cvname, "altitem_%s", DEH_KartItemName(result->type));
strlwr(cvname);
consvar_t *var = Z_Calloc(sizeof(consvar_t), PU_STATIC, &item->altcvar);
var->name = cvname;
var->defaultvalue = "Legacy";
var->flags = CV_NETVAR/*|CV_CALL*/;
var->PossibleValue = kartitemvariant_cons_t;
var->func = NULL;
CV_RegisterVar(var);
}
}
else
{
consvar_t *var = Z_Calloc(sizeof(consvar_t), PU_STATIC, &result->cvar);
char *cvname = Z_StrDup(name);
strlwr(cvname);
var->name = cvname;
var->defaultvalue = "On";
var->flags = CV_NETVAR|CV_CHEAT;
var->PossibleValue = CV_OnOff;
var->func = NULL;
CV_RegisterVar(var);
}
numkartresults++;
return result;
}
kartresult_t *K_GetKartResultAlt(const char *name, boolean alternate)
{
for (UINT8 i = 0; i < numkartresults; i++)
{
if (!stricmp(name, kartresults[i].cvar->name) && kartresults[i].isalt == alternate)
return &kartresults[i];
}
return NULL;
}
void K_SetupItemOdds(void)
{
// Update the item's variant based on the cvar's value.
for (kartitemtype_e i = 0; i < numkartitems; i++)
{
kartitem_t *item = &kartitems[i];
if (item->altcvar != NULL && item->altenabled != !!item->altcvar->value)
{
CONS_Printf("KITEM_%s: updating variant to %s\n", DEH_KartItemName(i), item->altcvar->string);
item->altenabled = !!item->altcvar->value;
}
}
for (UINT8 i = 0; i < numkartresults; i++)
kartresults[i].cooldown = 0;
}
boolean K_ItemResultEnabled(const kartresult_t *result)
{
if (result == NULL)
return false;
if (result->type == KITEM_SUPERRING && !K_RingsActive())
return false;
return result->cvar->value == 1;
}
boolean K_IsKartItemAlternate(kartitemtype_e itemtype)
{
return itemtype < numkartitems && kartitems[itemtype].altenabled;
}
kartitemflags_e K_GetItemFlags(kartitemtype_e itemtype)
{
if (itemtype >= numkartitems)
return 0;
// TODO: alt item flags
return kartitems[itemtype].flags;
}
UINT8 K_GetItemNumberDisplayMin(kartitemtype_e type, boolean tiny)
{
if (type == MAXKARTITEMS)
type = 0; // KITEM_SAD displays like normal
if (type >= numkartitems)
return 1; // actually broken items show x1 to let you know
return kartitems[type].flags & KIF_ANIMATED ? 2 : kartitems[type].graphics[tiny ? 1 : 0].numpatches + 1;
}
patch_t *K_GetCachedItemPatch(kartitemtype_e type, boolean tiny, UINT8 amount)
{
kartitem_t *item = &kartitems[type >= numkartresults ? 0 : type];
kartitemgraphics_t *graphics = &item->graphics[tiny ? 1 : 0];
if (graphics->numpatches == 0)
return missingpat;
tic_t timer = G_GamestateUsesLevel() ? leveltime : finalecount;
if (item->flags & KIF_ANIMATED)
return graphics->patches[(timer % (graphics->numpatches*3)) / 3];
else
return graphics->patches[CLAMP(amount - 1, 0, graphics->numpatches - 1)];
}
// sprite3 baybeeeee!!!!
// the renderer uses mobj->threshold to determine the item type
void K_UpdateMobjItemOverlay(mobj_t *part, kartitemtype_e itemType, UINT8 itemCount)
{
part->sprite = SPR_ITEM;
part->frame = FF_FULLBRIGHT|FF_PAPERSPRITE;
if (itemType >= numkartitems)
itemType = 0;
UINT8 numpatches = kartitems[itemType].graphics[0].numpatches;
if (kartitems[itemType].flags & KIF_ANIMATED)
part->frame |= (leveltime % (numpatches*3)) / 3;
else
part->frame |= CLAMP(itemCount - 1, 0, numpatches - 1);
}
/** \brief Are the odds in legacy distancing mode?
\return true if kartforcelegacyodds is enabled, or the map uses legacy WPs
*/
boolean K_LegacyOddsMode(void)
{
return (K_UsingLegacyCheckpoints() || (cv_kartforcelegacyodds.value));
}
static boolean K_InStartCooldown(void)
{
return (leveltime < (30*TICRATE)+starttime);
}
// Magic number distance for use with item roulette tiers
#define ACTIVEDISTVAR (K_LegacyOddsMode() ? DISTVAR_LEGACY : DISTVAR)
#define ACTIVESPBDIST (K_LegacyOddsMode() ? SPBDISTVAR_LEGACY : SPBDISTVAR)
#define SPBSTARTDIST (ACTIVESPBDIST) // Distance when SPB can start appearing
#define SPBFORCEDIST (7 * ACTIVESPBDIST / 2) // Distance when SPB is forced onto 2nd place (3.5x SPBDISTVAR)
#define ENDDIST (12*ACTIVEDISTVAR) // Distance when the game stops giving you bananas
static boolean K_RaceForceSPB(SINT8 playerpos, UINT32 pdis)
{
return ((gametyperules & GTR_CIRCUIT) && playerpos == 2 && pdis > (UINT32)SPBFORCEDIST);
}
// 1/21/2025: I hate tiptoeing around the integer limit.
// This is at a smaller scale.
static UINT32 K_Dist2D(INT32 x1, INT32 y1, INT32 x2, INT32 y2)
{
// d = √((x2 - x1)² + (y2 - y1)²)
INT32 xdiff, ydiff;
INT64 xprod, yprod;
xdiff = (x2 - x1);
ydiff = (y2 - y1);
xprod = ((INT64)xdiff * (INT64)xdiff);
yprod = ((INT64)ydiff * (INT64)ydiff);
return (UINT32)(IntSqrt64(xprod + yprod));
}
// Basic integer distancing, to quote myself:
// "Even if you did 256 units for 1 fracunit in distancing, it'd be a better result than trying to
// deal with overflows on a system that's already being pushed to the limit by needing 65536 units
// for precision. No seriously, I don't think anyone's losing sleep over "hmmmmm, 0.0000152 or
// 0.0039?????" when most 2D game engines only give a fuck about MAYBE 0.001"
/*static UINT32 K_IntDistance(fixed_t curx,
fixed_t cury,
fixed_t curz,
fixed_t destx,
fixed_t desty,
fixed_t destz)
{
return K_Dist2D(0,
curz / FRACUNIT,
K_Dist2D(curx / FRACUNIT, cury / FRACUNIT, destx / FRACUNIT, desty / FRACUNIT),
destz / FRACUNIT);
}*/
// This one uses map scaling instead, use in case of loss of depth on mobjscaled maps.
static UINT32 K_IntDistanceForMap(fixed_t curx,
fixed_t cury,
fixed_t curz,
fixed_t destx,
fixed_t desty,
fixed_t destz)
{
return K_Dist2D(0,
curz / mapobjectscale,
K_Dist2D(curx / mapobjectscale,
cury / mapobjectscale,
destx / mapobjectscale,
desty / mapobjectscale),
destz / mapobjectscale);
}
/** \brief Prevent overpowered rolls while under the effects of Alt. Shrink.
\param item the item to check
\return Don't double for this item?
*/
static boolean K_DontDoubleMyItems(kartitemtype_e type, UINT8 amount)
{
return type == KITEM_BALLHOG || type == KITEM_SPB
|| type == KITEM_INVINCIBILITY || type == KITEM_GROW
|| type == KITEM_BUBBLESHIELD || type == KITEM_FLAMESHIELD
|| type == KITEM_ROCKETSNEAKER || type == KITEM_SHRINK
|| type == KITEM_HYUDORO
|| (type == KITEM_BANANA && amount >= 4)
|| (type == KITEM_ORBINAUT && amount >= 3)
|| (type == KITEM_SNEAKER && amount >= 3)
|| (type == KITEM_JAWZ && amount >= 2)
|| type >= KITEM_FIRSTFREESLOT; // TODO: excludes custom items for now
}
static sfxenum_t resultfx[] = {
[KITEMBLINK_NORMAL] = sfx_itrolf,
[KITEMBLINK_MASHED] = sfx_itrolm,
[KITEMBLINK_KARMA] = sfx_itrolk,
[KITEMBLINK_DEBUG] = sfx_dbgsal,
};
// be careful with invalid item types here
void K_AwardPlayerItem(player_t *player, kartitemtype_e type, UINT8 amount, kartitemblink_e blink)
{
K_BotResetItemConfirm(player, true);
player->itemblink = TICRATE;
player->itemblinkmode = blink;
player->itemroulette = 0; // Since we're done, clear the roulette number
player->roulettetype = KROULETTETYPE_NORMAL; // This too
if (P_IsDisplayPlayer(player))
S_StartSound(NULL, resultfx[blink]);
player->itemtype = type;
if (K_IsAltShrunk(player) && !K_DontDoubleMyItems(type, amount) && blink != KITEMBLINK_DEBUG)
amount *= 2;
player->itemamount = amount;
}
static void K_AwardPlayerResult(player_t *player, kartresult_t *result, kartitemblink_e blink)
{
kartitemtype_e type = result != NULL ? result->type : MAXKARTITEMS;
UINT8 amount = result != NULL ? result->amount : 1;
K_AwardPlayerItem(player, type, amount, blink);
if (result == NULL)
return;
if (result->basecooldown > 0)
result->cooldown = result->basecooldown;
}
static fixed_t K_ItemOddsScale(UINT8 numPlayers, boolean spbrush)
{
// CEP: due to how baseplayer works, 17P+ lobbies will STILL have the disastrous odds of 0.22 prior, if not WORSE
// let's try adding another condition
const UINT8 basePlayer = 16; // The player count we design most of the game around.
const UINT8 vanillaMax = 17; // CEP: Maximum players in "vanilla" (non-30P) clients.
const UINT8 extPlayer = 24; // CEP: Cap for 17P+ so that odds don't get too muddled.
UINT8 playerCount = (spbrush ? 2 : numPlayers);
fixed_t playerScaling = 0;
// Then, it multiplies it further if the player count isn't equal to basePlayer.
// This is done to make low player count races more interesting and high player count rates more fair.
// (If you're in SPB mode and in 2nd place, it acts like it's a 1v1, so the catch-up game is not weakened.)
if (playerCount < basePlayer)
{
// Less than basePlayer: increase odds significantly.
// 2P: x1.4
playerScaling = (basePlayer - playerCount) * (FRACUNIT / 10);
}
else if (playerCount > basePlayer)
{
// More than basePlayer: reduce odds slightly.
// CEP: 17P+ adjustments
if (playerCount < vanillaMax)
{
// Less than vanillaMax: Use standard calculations.
// 16P: x0.6
playerScaling = (basePlayer - playerCount) * (FRACUNIT / 20);
}
else if (playerCount > vanillaMax)
{
// More than vanillaMax: Increase odds to fit with the increased playercount
// 24P: x0.6
// 30P: x0.45
playerScaling = (basePlayer - min(extPlayer, playerCount)) * (FRACUNIT / 40); // adding a cap here to be sure
}
}
return playerScaling;
}
UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers, boolean spbrush)
{
if (mapobjectscale != FRACUNIT)
{
// Bring back to normal scale.
distance = FixedDiv(distance * FRACUNIT, mapobjectscale) / FRACUNIT;
}
if (franticitems == true)
{
// Frantic items pretends everyone's farther apart, for crazier items.
distance = (15 * distance) / 14;
}
if (numPlayers > 0)
{
// Items get crazier with the fewer players that you have.
distance = FixedMul(
distance * FRACUNIT,
FRACUNIT + (K_ItemOddsScale(numPlayers, spbrush) / 2)
) / FRACUNIT;
}
return distance;
}
#define INVODDS 30
// Prevent integer overflows; don't let this go past 16383
#define INVINDESPERATION 4
#define MAXINVODDS ((MAXPROBABILITY * 2) * INVINDESPERATION)
// Odds value for Alt. Invin. to force itself on trailing players.
#define INVFORCEODDS (MAXINVODDS / 2)
#define FRAC_95pct (95 * FRACUNIT / 100)
static INT32 K_KartGetInvincibilityOdds(UINT32 dist)
{
UINT32 invindist = INVINDIST/2;
if (dist < invindist)
return 0;
INT32 finodds = 0;
fixed_t fac = (min(32000, (fixed_t)dist) * FRACUNIT) / (INVINDIST);
if (fac > FRACUNIT)
{
// Desperation! Climb exponentially until Invincibility is practically guaranteed.
fac = (((min(32000, (fixed_t)dist) * FRACUNIT) / (INVINDIST)) - FRACUNIT) >> 1;
finodds = Easing_InCubic(fac, INVODDS, MAXINVODDS);
}
else
{
if (fac <= FRAC_95pct)
{
// Invincibility is practically useless at lower distances.
// Only let it appear at or above 95%.
return 0;
}
// Basic linear climb to "reasonable" odds.
finodds = FixedMul(INVODDS, fac);
}
return min(MAXINVODDS, finodds);
}
#undef FRAC_95pct
// updates all result cooldown timers, and sets cooldowns for "unique" items
void K_UpdateItemCooldown(void)
{
INT32 i;
// tick down the cooldown timers
for (i = 0; i < numkartresults; i++)
if (kartresults[i].cooldown > 0)
kartresults[i].cooldown--;
// figure out which unique items need their cooldown set
boolean setcooldown[MAXKARTITEMS] = {0};
// start by checking each player's item slot (KIF_UNIQUESLOT)
for (i = 0; i < MAXPLAYERS; i++)
{
player_t *player = &players[i];
if (!playeringame[i] || player->spectator || player->exiting > 0)
continue;
if (gametyperules & GTR_BUMPERS && player->bumper == 0)
continue;
// special case for legacy shrink
if (player->growshrinktimer < 0 && !K_IsKartItemAlternate(KITEM_SHRINK))
setcooldown[KITEM_SHRINK] = true;
if (player->itemtype <= 0 || player->itemtype >= numkartitems || player->itemamount <= 0)
continue;
if (kartitems[player->itemtype].flags & KIF_UNIQUESLOT)
setcooldown[player->itemtype] = true;
}
// next, check for dropped items (KIF_UNIQUEDROP)
for (thinker_t *th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
{
mobj_t *mobj = (mobj_t *)th;
if (P_MobjWasRemoved(mobj))
continue;
switch (mobj->type)
{
case MT_FLOATINGITEM:
if (mobj->threshold > 0 && mobj->threshold < numkartitems && kartitems[mobj->threshold].flags & KIF_UNIQUEDROP)
setcooldown[mobj->threshold] = true;
break;
case MT_SPB: // SPBs in the field also count
setcooldown[KITEM_SPB] = true;
break;
default:
break;
}
}
// now it's FINALLY time to set cooldowns!
// unique item cooldowns only apply to race odds, unless it's an indirect item
for (i = 0; i < numkartresults; i++)
{
kartresult_t *result = &kartresults[i];
if ((gametyperules & GTR_RACEODDS || result->flags & KRF_INDIRECTITEM) && setcooldown[result->type])
result->cooldown = result->basecooldown;
}
// all indirect items share a single timer, whichever is higher
K_SetIndirectItemCooldown(K_GetIndirectItemCooldown());
}
tic_t K_GetIndirectItemCooldown(void)
{
tic_t maxcooldown = 0;
for (UINT8 i = 0; i < numkartresults; i++)
{
if (kartresults[i].flags & KRF_INDIRECTITEM)
maxcooldown = max(maxcooldown, kartresults[i].cooldown);
}
return maxcooldown;
}
void K_SetIndirectItemCooldown(tic_t cooldown)
{
for (UINT8 i = 0; i < numkartresults; i++)
{
if (kartresults[i].flags & KRF_INDIRECTITEM)
kartresults[i].cooldown = cooldown;
}
}
static INT32 GetItemOdds(kartroulette_t *roulette, kartresult_t *result, UINT8 *forceme)
{
INT32 newodds;
INT32 i;
SINT8 first = -1, second = -1;
UINT32 secondToFirst = UINT32_MAX;
UINT8 flags = result->flags;
if (result->cvar->value == 0 && !modeattacking)
return 0;
if (kartitems[result->type].altenabled != result->isalt)
return 0; // wrong alt item result
/*
if (roulette->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;
}
}
*/
INT32 oddsmul = BASEODDSMUL;
// Item type used for actual odds retrieval and cooldown assignments.
UINT8 oddstable;
if (gametyperules & GTR_BATTLEODDS)
oddstable = ODDS_BATTLE;
else if (gametyperules & GTR_RACEODDS)
oddstable = ODDS_RACE;
else
oddstable = ODDS_SPECIAL;
I_Assert(roulette->pos < oddstablemax[oddstable]); // DO NOT allow positions past the bounds of the table
if (gametyperules & GTR_BATTLEODDS)
oddsmul = BATTLEODDSMUL;
// TODO: braaap (make a separate table for the current level!)
newodds = result->odds[oddstable][roulette->pos];
// Blow up the odds with a multiplier.
newodds *= oddsmul;
INT32 shieldtype = K_GetShieldFromItem(result->type);
roulette->pexiting = 0;
roulette->pingame = 0;
roulette->firstDist = UINT32_MAX;
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator)
continue;
if (!(gametyperules & GTR_BUMPERS) || players[i].bumper)
roulette->pingame++;
if (players[i].exiting)
roulette->pexiting++;
if (shieldtype != KSHIELD_NONE && ((shieldtype == K_GetShieldFromItem(players[i].itemtype))
|| (shieldtype == K_GetShieldFromPlayer(&players[i]))))
{
// Don't allow more than one of each shield type at a time
return 0;
}
if (players[i].mo && gametype == GT_RACE)
{
if (players[i].position == 1 && first == -1)
first = i;
if (players[i].position == 2 && second == -1)
second = i;
}
}
if (first != -1 && second != -1) // calculate 2nd's distance from 1st, for SPB
{
if (!K_LegacyOddsMode())
{
roulette->firstDist = players[first].distancetofinish;
if (mapobjectscale != FRACUNIT)
{
roulette->firstDist = FixedDiv(roulette->firstDist * FRACUNIT, mapobjectscale) / FRACUNIT;
}
secondToFirst = K_ScaleItemDistance(
players[second].distancetofinish - players[first].distancetofinish,
roulette->pingame, roulette->spbrush
);
}
else
{
secondToFirst = P_AproxDistance(P_AproxDistance(
players[first].mo->x/4 - players[second].mo->x/4,
players[first].mo->y/4 - players[second].mo->y/4),
players[first].mo->z/4 - players[second].mo->z/4);
// Scale it to prevent overflow issues.
secondToFirst = (secondToFirst / FRACUNIT)*2;
secondToFirst = K_ScaleItemDistance(
secondToFirst,
roulette->pingame, roulette->spbrush
);
}
}
// TODO: convert into uniqueodds functions (what happened to "already shrunk"?)
if (result->type == KITEM_SHRINK)
{
if (!K_IsKartItemAlternate(KITEM_SHRINK))
{
if (roulette->pingame-1 <= roulette->pexiting)
newodds = 0;
}
else
{
if (roulette->rival)
{
// Rival bot or already shrunk. DON'T roll another.
newodds = 0;
}
}
}
if (result->unique_odds[oddstable])
{
// This item has unique odds!
//CONS_Printf("Unique odds found. Assigning uoddsfunc...");
useoddsfunc_f *uoddsfunc = result->unique_odds[oddstable];
//CONS_Printf("Running...");
newodds = uoddsfunc(newodds, roulette, result, forceme);
//CONS_Printf("OK!\n");
}
// In very small matches, remove the stupid bottom half item limiter
if (roulette->pingame < 6)
{
flags &= ~KRF_NOTFORBOTTOM;
}
if (newodds == 0)
{
// Nothing else we want to do with odds matters at this point :p
return newodds;
}
if (flags & KRF_RUNNERAUGMENT)
{
// These odds get stronger as 1st's frontrun increases.
const INT32 distFromStart = max(secondToFirst - SPBSTARTDIST, 0);
const INT32 distRange = SPBFORCEDIST - SPBSTARTDIST;
const INT32 mulMax = 24;
INT32 multiplier = (distFromStart * mulMax) / distRange;
if (multiplier < 0)
multiplier = 0;
if (multiplier > mulMax)
multiplier = mulMax;
newodds *= multiplier;
}
if (result->cooldown > 0)
{
// This item is on cooldown; don't let it get rolled.
newodds = 0;
}
else if (flags & KRF_COOLDOWNONSTART && K_InStartCooldown())
{
// This item should not appear at the beginning of a race. (Usually really powerful crowd-breaking items)
newodds = 0;
}
else if (!K_LegacyOddsMode() && flags & KRF_NOTNEAREND && roulette->ourDist < (UINT32)ENDDIST)
{
// This item should not appear at the end of a race. (Usually trap items that lose their effectiveness)
newodds = 0;
}
else if (flags & KRF_NOTFORBOTTOM && roulette->inBottom && leveltime >= (30*TICRATE)+starttime)
{
// This item should not appear for losing players. (Usually items that feel less effective at these positions)
newodds = 0;
}
else if (flags & KRF_HIDEFROMSPB && (spbplace != -1))
{
// This item doesn't appear if an SPB is chasing a player.
newodds = 0;
}
else if (flags & KRF_POWERITEM)
{
// This item is a "power item". This activates "frantic item" toggle related functionality.
fixed_t fracOdds = newodds * FRACUNIT;
if (franticitems == true)
{
// First, power items multiply their odds by 2 if frantic items are on; easy-peasy.
fracOdds *= 2;
}
if (roulette->rival == true)
{
// The Rival bot gets frantic-like items, also :p
fracOdds *= 2;
}
fracOdds = FixedMul(fracOdds, FRACUNIT + K_ItemOddsScale(roulette->pingame, roulette->spbrush));
if (roulette->mashed > 0)
{
// Lastly, it *divides* it based on your mashed value, so that power items are less likely when you mash.
fracOdds = FixedDiv(fracOdds, FRACUNIT + roulette->mashed);
}
newodds = fracOdds / FRACUNIT;
}
return newodds;
}
void K_KartGetItemOdds(kartroulette_t *roulette, INT32 outodds[static MAXKARTRESULTS])
{
// Reset forceme
memset(roulette->forceme, 0, sizeof(roulette->forceme));
for (UINT8 i = 0; i < numkartresults; i++)
{
outodds[i] = GetItemOdds(roulette, &kartresults[i], &roulette->forceme[i]);
}
};
//{ SRB2kart Roulette Code - Distance Based, yes waypoints
UINT8 K_FindUseodds(const player_t *player, fixed_t mashed, UINT32 pdis, UINT8 bestbumper, boolean spbrush)
{
fixed_t oddsfac = max(FRACUNIT, (MAXODDS * FRACUNIT) / 8);
UINT8 i, j;
UINT8 useodds = 0;
UINT8 disttable[(MAXODDS - 1) * 2];
UINT8 distlen = 0;
boolean oddsvalid[MAXODDS];
boolean rivalodds = false;
// Unused now, oops :V
(void)bestbumper;
if ((player->bot && player->botvars.rival) || K_IsAltShrunk(player))
{
// Rival bots and players using Alt. Shrink get crazier items.
rivalodds = true;
}
INT32 itemodds[MAXKARTRESULTS];
kartroulette_t roulette = {
.pdis = pdis,
.playerpos = player->position,
//.pos = i,
.ourDist = player->distancetofinish,
.clusterDist = player->distancefromcluster,
.mashed = mashed,
.spbrush = spbrush,
.bot = player->bot,
.rival = rivalodds,
.inBottom = K_IsPlayerLosing(player),
};
for (i = 0; i < MAXODDS; i++)
{
boolean available = false;
if ((gametyperules & GTR_BATTLEODDS) && i > 1)
{
oddsvalid[i] = false;
break;
}
roulette.pos = i;
K_KartGetItemOdds(&roulette, itemodds);
for (j = 0; j < numkartresults; j++)
{
if (itemodds[j] > 0)
{
available = true;
break;
}
}
oddsvalid[i] = available;
}
#define SETUPDISTTABLE(odds, num) \
if (oddsvalid[odds]) \
for (i = num; i; --i) \
{ \
disttable[distlen++] = odds; \
distlen = min(sizeof(disttable) - 1, distlen); \
}
if (gametyperules & GTR_BATTLEODDS) // Battle Mode
{
if (player->roulettetype == KROULETTETYPE_KARMA && oddsvalid[1] == true)
{
// 1 is the extreme odds of player-controlled "Karma" items
useodds = 1;
}
else
{
useodds = 0;
if (oddsvalid[0] == false && oddsvalid[1] == true)
{
// try to use karma odds as a fallback
useodds = 1;
}
}
}
else if (gametyperules & GTR_RACEODDS)
{
INT32 tablediv = FixedMul(2, oddsfac);
INT32 jj;
// "Why j instead of i"?
// Using i causes an infinite loop due to SETUPDISTTABLE. Yep.
for (j = 0; j < MAXODDS; j++)
{
if (j == (MAXODDS - 1))
{
// Attempt to replicate vanilla behavior; Useodds 8 is set up like this.
SETUPDISTTABLE(j,1);
}
else
{
jj = max(1, ((j - 3) / tablediv) + 1);
if (jj < 1)
{
jj = 1;
}
//CONS_Printf("SETUPDISTTABLE(%d, %d)\n", j, jj);
SETUPDISTTABLE(j,jj);
}
}
/*SETUPDISTTABLE(0,1);
SETUPDISTTABLE(1,1);
SETUPDISTTABLE(2,1);
SETUPDISTTABLE(3,2);
SETUPDISTTABLE(4,2);
SETUPDISTTABLE(5,3);
SETUPDISTTABLE(6,3);
SETUPDISTTABLE(7,1);*/
const INT32 usedistvar = FixedDiv(ACTIVEDISTVAR, oddsfac);
if (pdis == 0)
useodds = disttable[0];
else if (pdis > (UINT32)ACTIVEDISTVAR * ((12 * distlen) / sizeof(disttable)))
useodds = disttable[max(0, distlen-1)];
else
{
for (i = 1; i < (sizeof(disttable) - 1); i++)
{
INT32 distcalc = min(distlen-1, (i * distlen) / sizeof(disttable));
if (pdis <= (UINT32)usedistvar * distcalc)
{
useodds = disttable[distcalc];
break;
}
}
}
}
#undef SETUPDISTTABLE
return min(MAXODDS - 1, useodds);
}
INT32 K_GetRollingRouletteItem(player_t *player)
{
static UINT8 translation[MAXKARTRESULTS];
static UINT16 roulette_size;
static INT16 odds_cached = -1;
// Race odds have more columns than Battle
const UINT8 EMPTYODDS[MAXODDS] = {0}; // or not
if (odds_cached != gametype)
{
UINT8 oddstable = 0;
kartitemtype_e seen[MAXKARTITEMS];
size_t numseen = 0, j;
roulette_size = 0;
if (gametyperules & GTR_BATTLEODDS)
oddstable = ODDS_BATTLE;
else
oddstable = ODDS_RACE;
for (UINT8 i = 0; i < numkartresults; i++)
{
kartresult_t *result = &kartresults[i];
if (kartitems[result->type].flags & KIF_HIDEFROMROULETTE)
continue;
for (j = 0; j < numseen; j++)
if (seen[j] == result->type)
break;
if (j == numseen && memcmp(result->odds[oddstable], EMPTYODDS, sizeof(EMPTYODDS)))
translation[roulette_size++] = seen[numseen++] = result->type;
}
roulette_size *= 3;
odds_cached = gametype;
}
return translation[(player->itemroulette % roulette_size) / 3];
}
// Legacy odds are fickle and finicky, so we exaggerate distances
// to simulate parity with pathfind odds.
#define LEGACYODDSEXAGGERATE (2*FRACUNIT/3)
/** \brief Gets the distance between a player and a position by chaining all other players
* between them and the target position.
\param player player object
\param startPos first position to trace to the player from
\return (UINT32) pdis
*/
UINT32 K_GetCongaLineDistance(const player_t *player, UINT8 startPos)
{
SINT8 sortedPlayers[MAXPLAYERS];
UINT8 sortLength = 0;
UINT32 pdis = 0;
INT32 i;
memset(sortedPlayers, -1, sizeof(sortedPlayers));
if (player->mo != NULL && P_MobjWasRemoved(player->mo) == false)
{
// Sort all of the players ahead of you.
// Then tally up their distances in a conga line.
// This will create a much more consistent item
// distance algorithm than the "spider web" thing
// that it was doing before.
// Add yourself to the list.
// You'll always be the end of the list,
// so we can also calculate the length here.
sortedPlayers[ player->position - 1 ] = player - players;
sortLength = player->position;
// Will only need to do this if there's goint to be
// more than yourself in the list.
if (sortLength > 1)
{
SINT8 firstIndex = -1;
SINT8 secondIndex = -1;
INT32 startFrom = INT32_MAX;
// Add all of the other players.
for (i = 0; i < MAXPLAYERS; i++)
{
INT32 pos = INT32_MAX;
if (!playeringame[i] || players[i].spectator)
{
continue;
}
if (players[i].mo == NULL || P_MobjWasRemoved(players[i].mo) == true)
{
continue;
}
pos = players[i].position;
if ((pos <= 0) || (pos > MAXPLAYERS) || (pos < startPos))
{
// Invalid position.
continue;
}
if (pos >= player->position)
{
// Tied / behind us.
// Also handles ourselves, obviously.
continue;
}
// Ties are done with port priority, if there are any.
if (sortedPlayers[ pos - 1 ] == -1)
{
sortedPlayers[ pos - 1 ] = i;
}
}
// The chance of this list having gaps is improbable,
// but not impossible. So we need to spend some extra time
// to prevent the gaps from mattering.
for (i = 0; i < sortLength-1; i++)
{
if (sortedPlayers[i] >= 0 && sortedPlayers[i] < MAXPLAYERS)
{
// First valid index in the list found.
firstIndex = sortedPlayers[i];
// Start the next loop after this player.
startFrom = i + 1;
break;
}
}
if (firstIndex >= 0 && firstIndex < MAXPLAYERS
&& startFrom < sortLength)
{
// First index is valid, so we can
// start comparing the players.
player_t *firstPlayer = NULL;
player_t *secondPlayer = NULL;
for (i = startFrom; i < sortLength; i++)
{
if (sortedPlayers[i] >= 0 && sortedPlayers[i] < MAXPLAYERS)
{
secondIndex = sortedPlayers[i];
firstPlayer = &players[firstIndex];
secondPlayer = &players[secondIndex];
// Add the distance to the player behind you.
// At a (relative to map) integer scale using basic distancing
// arithmetic; more accurate and less concern for overflows.
// TODO: Overflow-safe addition (caps at UINT32_MAX).
pdis += K_IntDistanceForMap(
secondPlayer->mo->x,
secondPlayer->mo->y,
secondPlayer->mo->z,
firstPlayer->mo->x,
firstPlayer->mo->y,
firstPlayer->mo->z);
// Advance to next index.
firstIndex = secondIndex;
}
}
}
}
}
return pdis;
}
UINT32 K_CalculateInitalPDIS(const player_t *player, UINT8 pingame)
{
UINT8 i;
UINT32 pdis = 0;
(void)pingame;
if (!K_LegacyOddsMode())
{
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;
}
}
}
else
{
pdis = K_GetCongaLineDistance(player, 1);
// Exaggerate odds; don't you love the legacy system? :Trollic:
pdis = FixedMul(pdis, LEGACYODDSEXAGGERATE);
}
return pdis;
}
#undef LEGACYODDSEXAGGERATE
UINT32 K_CalculatePDIS(const player_t *player, UINT8 numPlayers, boolean *spbrush)
{
UINT32 pdis = 0;
pdis = K_CalculateInitalPDIS(player, numPlayers);
if (cv_kartspbrush.value && spbplace != -1 && player->position == spbplace+1)
{
// SPB Rush Mode: It's 2nd place's job to catch-up items and make 1st place's job hell
pdis = (3 * pdis) / 2;
*spbrush = true;
}
pdis = K_ScaleItemDistance(pdis, numPlayers, *spbrush);
if (player->bot && player->botvars.rival)
{
// Rival has better odds :)
pdis = (15 * pdis) / 14;
}
// Can almost barely overflow this calc, fudge to prevent this.
if (pdis > 30000)
pdis = 30000;
return pdis;
}
static boolean K_BattleForceSPB(player_t *player)
{
boolean battlecond = ((gametyperules & GTR_WANTED) && (gametyperules & GTR_WANTEDSPB) && (mostwanted != -1) && (!K_IsPlayerMostWanted(player)));
return battlecond;
}
void K_KartItemRoulette(player_t *player, ticcmd_t *cmd)
{
INT32 i;
UINT8 pingame = 0;
UINT8 roulettestop;
UINT32 pdis = 0;
UINT8 useodds = 0;
INT32 spawnchance[MAXKARTRESULTS];
INT64 totalspawnchance = 0; // 75-scale numbers are going to get BIG. This is for paranoia's sake.
UINT8 bestbumper = 0;
fixed_t mashed = 0;
boolean dontforcespb = false;
boolean spbrush = false;
// This makes the roulette cycle through items - if this is 0, you shouldn't be here.
if (!player->itemroulette)
return;
player->itemroulette++;
// Gotta check how many players are active at this moment.
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator)
continue;
pingame++;
if (players[i].exiting)
dontforcespb = true;
if (players[i].bumper > bestbumper)
bestbumper = players[i].bumper;
}
// No forced SPB in 1v1s, it has to be randomly rolled
if (pingame <= 2)
dontforcespb = true;
// This makes the roulette produce the random noises.
if ((player->itemroulette % 3) == 1 && P_IsDisplayPlayer(player))
{
#define PLAYROULETTESND S_StartSound(NULL, sfx_itrol1 + ((player->itemroulette / 3) % 8))
for (i = 0; i <= r_splitscreen; i++)
{
if (player == &players[displayplayers[i]] && players[displayplayers[i]].itemroulette)
PLAYROULETTESND;
}
#undef PLAYROULETTESND
}
roulettestop = TICRATE + (3*(pingame - player->position));
// If the roulette finishes or the player presses BT_ATTACK, stop the roulette and calculate the item.
// I'm returning via the exact opposite, however, to forgo having another bracket embed. Same result either way, I think.
// Finally, if you get past this check, now you can actually start calculating what item you get.
if ((cmd->buttons & BT_ATTACK) && (player->itemroulette >= roulettestop)
&& (K_RingsActive() || (modeattacking == ATTACKING_NONE))
&& !(player->itemflags & (IF_ITEMOUT|IF_EGGMANOUT|IF_USERINGS))
&& !(player->itemusecooldown))
{
// Mashing reduces your chances for the good items
mashed = FixedDiv((player->itemroulette)*FRACUNIT, ((TICRATE*3)+roulettestop)*FRACUNIT) - FRACUNIT;
}
else if (!(player->itemroulette >= (TICRATE*3)))
return;
pdis = K_CalculatePDIS(player, pingame, &spbrush);
// SPECIAL CASE No. 1:
// Fake Eggman items
if (player->roulettetype == KROULETTETYPE_EGGMAN)
{
player->eggmanexplode = 4*TICRATE;
player->itemroulette = 0;
player->roulettetype = KROULETTETYPE_NORMAL;
if (P_IsDisplayPlayer(player))
S_StartSound(NULL, sfx_itrole);
return;
}
// SPECIAL CASE No. 2:
// Give a debug item instead if specified
if (cv_kartdebugitem.value != 0 && !modeattacking)
{
K_AwardPlayerItem(player, cv_kartdebugitem.value, cv_kartdebugamount.value, KITEMBLINK_DEBUG);
return;
}
// SPECIAL CASE No. 3:
// This Gametype never specified an odds type. Roll something random please!
if (!(gametyperules & GTR_RACEODDS) && !(gametyperules & GTR_BATTLEODDS))
{
UINT8 itemroll = P_RandomRange(0, numkartresults - 1);
K_AwardPlayerResult(player, &kartresults[itemroll], KITEMBLINK_NORMAL);
return;
}
// SPECIAL CASE No. 4:
// Record Attack / alone mashing behavior
if ((modeattacking || pingame == 1)
&& ((gametyperules & GTR_RACEODDS)
|| ((gametyperules & GTR_BATTLEODDS) && (itembreaker || bossinfo.boss))))
{
if ((gametyperules & GTR_RACEODDS))
{
if (mashed && (K_ItemResultEnabled(K_GetKartResult("superring")) || (modeattacking && K_RingsActive()))) // ANY mashed value? You get rings.
{
K_AwardPlayerResult(player, K_GetKartResult("superring"), KITEMBLINK_MASHED);
}
else
{
if (modeattacking || K_ItemResultEnabled(K_GetKartResult("sneaker"))) // Waited patiently? You get a sneaker!
K_AwardPlayerResult(player, K_GetKartResult("sneaker"), KITEMBLINK_NORMAL);
else // Default to sad if nothing's enabled...
K_AwardPlayerResult(player, NULL, KITEMBLINK_NORMAL);
}
}
else if (gametyperules & GTR_BATTLEODDS)
{
if (mashed && (bossinfo.boss || K_ItemResultEnabled(K_GetKartResult("banana"))) && !itembreaker) // ANY mashed value? You get a banana.
K_AwardPlayerResult(player, K_GetKartResult("banana"), KITEMBLINK_MASHED);
else if (bossinfo.boss)
K_AwardPlayerResult(player, K_GetKartResult("orbinaut"), KITEMBLINK_NORMAL);
else if (itembreaker)
K_AwardPlayerResult(player, K_GetKartResult("sneaker"), KITEMBLINK_MASHED);
}
return;
}
// SPECIAL CASE No. 5:
// Being in ring debt occasionally forces Super Ring on you if you mashed
if (K_ItemResultEnabled(K_GetKartResult("superring")) && mashed && player->rings < 0)
{
INT32 debtamount = min(abs(player->ringmin), abs(player->rings));
if (P_RandomChance((debtamount*FRACUNIT)/abs(player->ringmin)))
{
K_AwardPlayerResult(player, K_GetKartResult("superring"), KITEMBLINK_MASHED);
return;
}
}
// SPECIAL CASE No. 6:
// In battle, an SPB is forced onto players to target the "most wanted" player
if (K_BattleForceSPB(player)
&& spbplace == -1 && K_GetKartResult("spb")->cooldown == 0 && !dontforcespb
&& K_ItemResultEnabled(K_GetKartResult("spb")))
{
K_AwardPlayerResult(player, K_GetKartResult("spb"), KITEMBLINK_KARMA);
return;
}
// NOW that we're done with most of those specialized cases, we can move onto the REAL item roulette tables.
// Initializes existing spawnchance values
memset(spawnchance, 0, sizeof(spawnchance));
// Split into another function for a debug function below
useodds = K_FindUseodds(player, mashed, pdis, bestbumper, spbrush);
kartroulette_t roulette = {
.pdis = pdis,
.playerpos = player->position,
.pos = useodds,
.ourDist = player->distancetofinish,
.clusterDist = player->distancefromcluster,
.mashed = mashed,
.spbrush = spbrush,
.bot = player->bot,
.rival = player->bot && player->botvars.rival,
.inBottom = K_IsPlayerLosing(player),
};
K_KartGetItemOdds(&roulette, spawnchance);
// SPECIAL CASE No. 7:
// Item forcing; the item with the highest "forceme" priority is the one given.
{
kartresult_t *forceresult;
UINT8 bestforce = 0;
for (i = 0; i < numkartresults; i++)
{
if (roulette.forceme[i] && roulette.forceme[i] > bestforce)
{
bestforce = roulette.forceme[i];
forceresult = &kartresults[i];
}
}
if ((bestforce) && (forceresult))
{
K_AwardPlayerResult(player, forceresult, KITEMBLINK_KARMA);
return;
}
}
for (i = 0; i < numkartresults; i++)
{
totalspawnchance += spawnchance[i];
spawnchance[i] = totalspawnchance;
}
// Award the player whatever power is rolled
kartresult_t *result = NULL;
if (totalspawnchance > 0)
{
totalspawnchance = P_RandomKey(totalspawnchance);
for (i = 0; i < numkartresults && spawnchance[i] <= totalspawnchance; i++);
result = &kartresults[i];
}
K_AwardPlayerResult(player, result, player->roulettetype == KROULETTETYPE_KARMA ? KITEMBLINK_KARMA : mashed ? KITEMBLINK_MASHED : KITEMBLINK_NORMAL);
}
void K_StartRoulette(player_t *player, kartroulettetype_e roulettetype)
{
if (roulettetype == KROULETTETYPE_EGGMAN)
K_DropItems(player);
player->itemroulette = 1;
player->roulettetype = roulettetype;
}
void K_SetPlayerItemCooldown(player_t *player, tic_t timer, boolean force)
{
if ((timer >= player->itemusecooldown) || force)
{
player->itemusecooldown = timer;
player->itemusecooldownmax = max(timer, player->itemusecooldownmax);
}
}
// Unique odds functions, for REAL this time
// Alt. Invin. odds
INT32 KO_AltInvinOdds(INT32 odds, const kartroulette_t *roulette, const kartresult_t *result, UINT8 *forceme)
{
(void)result;
odds = K_KartGetInvincibilityOdds(roulette->clusterDist);
// Special case: if you're SERIOUSLY far behind before the cooldown finishes, ignore it and start forcing,
if (odds >= INVFORCEODDS)
{
*forceme = 3; // Take priority over SPBs
}
else if (K_InStartCooldown())
{
odds = 0;
}
odds *= BASEODDSMUL;
return odds;
}
// SPB odds
INT32 KO_SPBRaceOdds(INT32 odds, const kartroulette_t *roulette, const kartresult_t *result, UINT8 *forceme)
{
(void)result;
if (!K_LegacyOddsMode() && roulette->firstDist < (UINT32)ENDDIST) // No SPB near the end of the race
{
odds = 0;
}
else if (K_LegacyOddsMode() && roulette->pexiting > 0)
{
odds = 0;
}
// No forced SPB in 1v1s, it has to be randomly rolled
if (roulette->pingame <= 2)
{
*forceme = 0;
}
else if (K_RaceForceSPB(roulette->playerpos, roulette->pdis)
&& spbplace == -1 && K_GetKartResult("spb")->cooldown == 0)
{
// Force SPB onto 2nd if they get too far behind.
*forceme = 2;
}
return odds;
}
#define ALTSHRINK_EPSILON (320 * FRACUNIT)
#define NEIGHBOR_IFRAMES (TICRATE / 2)
#define BASE_IFRAMES (2 * TICRATE)
static void K_DoGrowShrink(player_t *player, boolean shrinking)
{
player->mo->scalespeed = mapobjectscale/TICRATE;
player->mo->destscale = FixedMul(mapobjectscale, (shrinking) ? SHRINK_SCALE : GROW_SCALE);
if (K_PlayerShrinkCheat(player) == true)
{
player->mo->destscale = FixedMul(player->mo->destscale, SHRINK_SCALE);
}
if ((shrinking) && (ALTSHRINKTIME > 0))
{
player->growshrinktimer = ALTSHRINKTIME * -TICRATE;
}
else
{
player->growshrinktimer = (gametyperules & GTR_CLOSERPLAYERS ? 8 : 12) * ((shrinking) ? -TICRATE : TICRATE);
}
if (shrinking)
{
// Find neighbors
INT32 n = K_CountNeighboringPlayers(player, ALTSHRINK_EPSILON, &(vector3_t){});
// For every neighbor, add some iframes for a clean breakaway.
UINT32 iframes = BASE_IFRAMES + n * NEIGHBOR_IFRAMES;
player->flashing = iframes > UINT16_MAX ? UINT16_MAX : iframes;
}
if (player->invincibilitytimer > 0)
{
; // invincibility has priority in P_RestoreMusic, no point in starting here
}
else if (P_IsLocalPlayer(player) == true)
{
S_ChangeMusicSpecial((shrinking) ? "kshrnk" : "kgrow");
}
else //used to be "if (P_IsDisplayPlayer(player) == false)"
{
if (!shrinking)
{
S_StartSound(player->mo, (cv_kartinvinsfx.value ? sfx_alarmg : sfx_kgrow));
}
}
P_RestoreMusic(player);
}
static void K_DoShrink(player_t *user)
{
INT32 i;
mobj_t *mobj, *next;
S_StartSound(user->mo, sfx_kc46); // Sound the BANG!
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator || !players[i].mo)
continue;
if (&players[i] == user)
continue;
if (players[i].position < user->position)
{
//P_FlashPal(&players[i], PAL_NUKE, 10);
// Grow should get taken away.
if (players[i].growshrinktimer > 0)
K_RemoveGrowShrink(&players[i]);
else
{
// Start shrinking!
K_DropItems(&players[i]);
players[i].growshrinktimer = -(15*TICRATE);
if (players[i].mo && !P_MobjWasRemoved(players[i].mo))
{
players[i].mo->scalespeed = mapobjectscale/TICRATE;
players[i].mo->destscale = FixedMul(mapobjectscale, SHRINK_SCALE);
if (K_PlayerShrinkCheat(&players[i]) == true)
{
players[i].mo->destscale = FixedMul(players[i].mo->destscale, SHRINK_SCALE);
}
S_StartSound(players[i].mo, sfx_kc59);
}
}
}
}
// kill everything in the kitem list while we're at it:
for (mobj = kitemcap; mobj; mobj = next)
{
next = mobj->itnext;
// check if the item is being held by a player behind us before removing it.
// check if the item is a "shield" first, bc i'm p sure thrown items keep the player that threw em as target anyway
if (mobj->type == MT_BANANA_SHIELD || mobj->type == MT_JAWZ_SHIELD ||
mobj->type == MT_SSMINE_SHIELD || mobj->type == MT_EGGMANITEM_SHIELD ||
mobj->type == MT_SINK_SHIELD || mobj->type == MT_ORBINAUT_SHIELD)
{
if (mobj->target && mobj->target->player)
{
if (mobj->target->player->position > user->position)
continue; // this guy's behind us, don't take his stuff away!
}
}
mobj->destscale = 0;
mobj->flags &= ~(MF_SOLID|MF_SHOOTABLE|MF_SPECIAL);
mobj->flags |= MF_NOCLIPTHING; // Just for safety
if (mobj->type == MT_SPB)
spbplace = -1;
}
}
static void K_DoHyudoroSteal(player_t *player)
{
INT32 i, numplayers = 0;
INT32 playerswappable[MAXPLAYERS];
INT32 stealplayer = -1; // The player that's getting stolen from
INT32 prandom = 0;
boolean sink = P_RandomChance(FRACUNIT/64);
boolean force_sink = false;
INT32 hyu = hyudorotime;
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] && players[i].mo && players[i].mo->health > 0 && players[i].playerstate == PST_LIVE
&& player != &players[i] && !players[i].exiting && !players[i].spectator // Player in-game
// Can steal from this player
&& (gametype == GT_RACE //&& players[i].position < player->position)
|| ((gametyperules & GTR_BUMPERS) && players[i].bumper > 0))
// Has an item
&& (players[i].itemtype
&& players[i].itemamount
&& !(players[i].itemflags & IF_ITEMOUT)
&& !players[i].itemblink))
{
playerswappable[numplayers] = i;
numplayers++;
}
}
prandom = P_RandomFixed();
S_StartSound(player->mo, sfx_s3k92);
if (numplayers == 1) // With just 2 players, we just need to set the other player to be the one to steal from
{
stealplayer = playerswappable[numplayers-1];
}
else if (numplayers > 1) // We need to choose between the available candidates for the 2nd player
{
stealplayer = playerswappable[prandom%(numplayers-1)];
}
force_sink = LUA_HookKartHyudoro(player, &stealplayer, sink);
if (force_sink || (sink && numplayers > 0 && K_ItemResultEnabled(K_GetKartResult("kitchensink")))) // BEHOLD THE KITCHEN SINK
{
player->hyudorotimer = hyu;
player->stealingtimer = stealtime;
player->itemtype = KITEM_KITCHENSINK;
player->itemamount = 1;
K_UnsetItemOut(player);
// Woah this could be big, lets get the inside scoop...
K_DirectorForceSwitch(player - players, 1);
return;
}
else if ((gametype == GT_RACE && player->position == 1) || numplayers == 0) // No-one can be stolen from? Oh well...
{
player->hyudorotimer = hyu;
player->stealingtimer = stealtime;
return;
}
if (stealplayer > -1) // Now here's where we do the stealing, has to be done here because we still know the player we're stealing from
{
player->hyudorotimer = hyu;
player->stealingtimer = stealtime;
players[stealplayer].stolentimer = stealtime;
player->itemtype = players[stealplayer].itemtype;
player->itemamount = players[stealplayer].itemamount;
K_UnsetItemOut(player);
players[stealplayer].itemtype = KITEM_NONE;
players[stealplayer].itemamount = 0;
K_UnsetItemOut(&players[stealplayer]);
if (P_IsDisplayPlayer(&players[stealplayer]) && !r_splitscreen)
S_StartSound(NULL, sfx_s3k92);
}
}
#define THUNDERRADIUS 320
// Rough size of the outer-rim sprites, after scaling.
// (The hitbox is already pretty strict due to only 1 active frame,
// we don't need to have it disjointedly small too...)
#define THUNDERSPRITE 80
void K_DoThunderShield(player_t *player)
{
mobj_t *mo;
int i = 0;
fixed_t sx;
fixed_t sy;
angle_t an;
S_StartSound(player->mo, sfx_zio3);
K_ThunderShieldAttack(player->mo, (THUNDERRADIUS + THUNDERSPRITE) * FRACUNIT);
// spawn vertical bolt
mo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_THOK);
P_SetTarget(&mo->target, player->mo);
P_SetMobjState(mo, S_LZIO11);
mo->color = SKINCOLOR_TEAL;
mo->scale = player->mo->scale*3 + (player->mo->scale/2);
mo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_THOK);
P_SetTarget(&mo->target, player->mo);
P_SetMobjState(mo, S_LZIO21);
mo->color = SKINCOLOR_CYAN;
mo->scale = player->mo->scale*3 + (player->mo->scale/2);
// spawn horizontal bolts;
for (i=0; i<7; i++)
{
mo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_THOK);
mo->angle = P_RandomRange(0, 359)*ANG1;
mo->fuse = P_RandomRange(20, 50);
P_SetTarget(&mo->target, player->mo);
P_SetMobjState(mo, S_KLIT1);
}
// spawn the radius thing:
an = ANGLE_22h;
for (i=0; i<15; i++)
{
sx = player->mo->x + FixedMul((player->mo->scale*THUNDERRADIUS), FINECOSINE((an*i)>>ANGLETOFINESHIFT));
sy = player->mo->y + FixedMul((player->mo->scale*THUNDERRADIUS), FINESINE((an*i)>>ANGLETOFINESHIFT));
mo = P_SpawnMobj(sx, sy, player->mo->z, MT_THOK);
mo->angle = an*i;
mo->extravalue1 = THUNDERRADIUS; // Used to know whether we should teleport by radius or something.
mo->scale = player->mo->scale*3;
P_SetTarget(&mo->target, player->mo);
P_SetMobjState(mo, S_KSPARK1);
}
}
#undef THUNDERRADIUS
#undef THUNDERSPRITE
#define MAXSHARDCOUNT 40
#define SHARDROT (360 * FRACUNIT / 40)
void K_BreakBubbleShield(player_t* player)
{
if (K_GetShieldFromPlayer(player) != KSHIELD_BUBBLE)
return;
const INT32 flip = P_MobjFlip(player->mo);
const fixed_t scalediff = player->shieldtracer->scale - mapobjectscale;
const fixed_t shieldrad = player->shieldtracer->radius;
for (INT32 i = 0; i < MAXSHARDCOUNT; i++)
{
vector2_t mul_vec = { FRACUNIT, 0 };
fixed_t randang = SHARDROT * (i + 1);
fixed_t randzang = P_RandomRange(10, 120) * FRACUNIT;
fixed_t move_magnitude =
FixedMul((P_RandomRange(4, 16) * FRACUNIT), mapobjectscale + (scalediff / 4));
FV2_Rotate(&mul_vec, randzang);
// Do shitty initial 3D rotations around the shield's radius.
mobj_t *shard = P_SpawnMobj(
player->shieldtracer->x +
FixedMul(FixedMul(mul_vec.x, shieldrad), FCOS(FixedAngle(randang))),
player->shieldtracer->y +
FixedMul(FixedMul(mul_vec.x, shieldrad), FSIN(FixedAngle(randang))),
player->shieldtracer->z + ((player->shieldtracer->height + scalediff) / 4)
+ FixedMul(mul_vec.y * flip, shieldrad),
MT_BUBBLESHIELD_DEBRIS);
if (!P_MobjWasRemoved(shard))
{
//CONS_Printf(M_GetText("randzang: %d, randang: %d\n"), randzang / FRACUNIT, randang / FRACUNIT);
mul_vec.x = FixedMul(move_magnitude, mul_vec.x);
mul_vec.y = FixedMul(move_magnitude, mul_vec.y * flip);
vector3_t mom = {
.x = FixedMul(mul_vec.x, FCOS(FixedAngle(randang))),
.y = FixedMul(mul_vec.x, FSIN(FixedAngle(randang))),
.z = mul_vec.y,
};
shard->momx = mom.x + player->mo->momx;
shard->momy = mom.y + player->mo->momy;
shard->momz = mom.z + player->mo->momz;
shard->fuse = 3*TICRATE;
shard->extravalue1 = i % 6; // 16% of shards play the death sound
}
}
S_StartSound(player->mo, sfx_kc41);
}
#undef MAXSHARDCOUNT
#undef SHARDROT
static void K_ThrowLandMine(player_t *player)
{
mobj_t *landMine;
mobj_t *throwmo;
landMine = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z + player->mo->height/2, MT_LANDMINE);
K_FlipFromObject(landMine, player->mo);
landMine->threshold = 10;
if (landMine->info->seesound)
S_StartSound(player->mo, landMine->info->seesound);
P_SetTarget(&landMine->target, player->mo);
P_SetScale(landMine, player->mo->scale);
landMine->destscale = player->mo->destscale;
landMine->angle = player->mo->angle;
landMine->momz = (30 * mapobjectscale * P_MobjFlip(player->mo)) + player->mo->momz;
landMine->color = player->skincolor;
throwmo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z + player->mo->height/2, MT_FIREDITEM);
P_SetTarget(&throwmo->target, player->mo);
// Ditto:
if (player->mo->eflags & MFE_VERTICALFLIP)
{
throwmo->z -= player->mo->height;
throwmo->eflags |= MFE_VERTICALFLIP;
}
throwmo->movecount = 0; // above player
}
void K_PlayerItemThink(player_t *player, boolean onground)
{
const UINT16 buttons = K_GetKartButtons(player);
const boolean ATTACK_IS_DOWN = ((buttons & BT_ATTACK) && !(player->oldcmd.buttons & BT_ATTACK));
const boolean HOLDING_ITEM = (player->itemflags & (IF_ITEMOUT|IF_EGGMANOUT));
const boolean NO_HYUDORO = (player->stealingtimer == 0 && player->stolentimer == 0);
boolean force = false;
if (LUA_HookPlayerItem(player, player->itemtype, HOLDING_ITEM, &force))
return;
switch (player->itemtype)
{
case KITEM_SNEAKER:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && (onground || force) && NO_HYUDORO)
{
K_DoSneaker(player, SNEAKERTYPE_SNEAKER);
K_PlayBoostTaunt(player->mo);
player->itemamount--;
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_ROCKETSNEAKER:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && (onground || force) && NO_HYUDORO
&& player->rocketsneakertimer == 0)
{
INT32 moloop;
mobj_t *mo = NULL;
mobj_t *prev = player->mo;
K_PlayBoostTaunt(player->mo);
S_StartSound(player->mo, sfx_s3k3a);
//K_DoSneaker(player, SNEAKERTYPE_ROCKETSNEAKER);
player->rocketsneakertimer = (itemtime*3);
player->itemamount--;
K_UpdateHnextList(player, true);
for (moloop = 0; moloop < 2; moloop++)
{
mo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_ROCKETSNEAKER);
K_MatchGenericExtraFlags(mo, player->mo);
mo->flags |= MF_NOCLIPTHING;
mo->angle = player->mo->angle;
mo->threshold = 10;
mo->movecount = moloop%2;
mo->movedir = mo->lastlook = moloop+1;
P_SetTarget(&mo->target, player->mo);
P_SetTarget(&mo->hprev, prev);
P_SetTarget(&prev->hnext, mo);
prev = mo;
}
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_INVINCIBILITY:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO) // Doesn't hold your item slot hostage in Legacy, so you're free to waste it if you have multiple
{
K_DoInvincibility(player, K_GetInvincibilityTime(player));
K_PlayPowerGloatSound(player->mo);
player->itemamount--;
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_BANANA:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
INT32 moloop;
mobj_t *mo;
mobj_t *prev = player->mo;
//K_PlayAttackTaunt(player->mo);
K_SetItemOut(player);
S_StartSound(player->mo, sfx_s254);
for (moloop = 0; moloop < player->itemamount; moloop++)
{
mo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_BANANA_SHIELD);
if (!mo)
{
player->itemamount = moloop;
break;
}
mo->flags |= MF_NOCLIPTHING;
mo->threshold = 10;
mo->movecount = player->itemamount;
mo->movedir = moloop+1;
P_SetTarget(&mo->target, player->mo);
P_SetTarget(&mo->hprev, prev);
P_SetTarget(&prev->hnext, mo);
prev = mo;
}
K_BotResetItemConfirm(player, false);
}
else if (ATTACK_IS_DOWN && (player->itemflags & IF_ITEMOUT)) // Banana x3 thrown
{
player->itemamount--;
mobj_t *mo = K_ThrowKartItem(player, false, MT_BANANA, -1, 0);
mo->color = player->skincolor;
K_PlayAttackTaunt(player->mo);
K_UpdateHnextList(player, false);
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_EGGMAN:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
mobj_t *mo;
player->itemamount--;
player->itemflags |= IF_EGGMANOUT;
S_StartSound(player->mo, sfx_s254);
mo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_EGGMANITEM_SHIELD);
if (mo)
{
K_FlipFromObject(mo, player->mo);
mo->flags |= MF_NOCLIPTHING;
mo->threshold = 10;
mo->movecount = 1;
mo->movedir = 1;
P_SetTarget(&mo->target, player->mo);
P_SetTarget(&player->mo->hnext, mo);
}
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_ORBINAUT:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
angle_t newangle;
INT32 moloop;
mobj_t *mo = NULL;
mobj_t *prev = player->mo;
//K_PlayAttackTaunt(player->mo);
K_SetItemOut(player);
S_StartSound(player->mo, sfx_s3k3a);
for (moloop = 0; moloop < player->itemamount; moloop++)
{
newangle = (player->mo->angle + ANGLE_157h) + FixedAngle(((360 / player->itemamount) * moloop) << FRACBITS) + ANGLE_90;
mo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_ORBINAUT_SHIELD);
if (!mo)
{
player->itemamount = moloop;
break;
}
mo->flags |= MF_NOCLIPTHING;
mo->angle = newangle;
mo->threshold = 10;
mo->movecount = player->itemamount;
mo->movedir = mo->lastlook = moloop+1;
mo->color = player->skincolor;
P_SetTarget(&mo->target, player->mo);
P_SetTarget(&mo->hprev, prev);
P_SetTarget(&prev->hnext, mo);
prev = mo;
}
K_BotResetItemConfirm(player, false);
}
else if (ATTACK_IS_DOWN && (player->itemflags & IF_ITEMOUT)) // Orbinaut x3 thrown
{
player->itemamount--;
K_ThrowKartItem(player, true, MT_ORBINAUT, 1, 0);
K_PlayAttackTaunt(player->mo);
K_UpdateHnextList(player, false);
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_JAWZ:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
angle_t newangle;
INT32 moloop;
mobj_t *mo = NULL;
mobj_t *prev = player->mo;
//K_PlayAttackTaunt(player->mo);
K_SetItemOut(player);
S_StartSound(player->mo, sfx_s3k3a);
for (moloop = 0; moloop < player->itemamount; moloop++)
{
newangle = (player->mo->angle + ANGLE_157h) + FixedAngle(((360 / player->itemamount) * moloop) << FRACBITS) + ANGLE_90;
mo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_JAWZ_SHIELD);
if (!mo)
{
player->itemamount = moloop;
break;
}
mo->flags |= MF_NOCLIPTHING;
mo->angle = newangle;
mo->threshold = 10;
mo->movecount = player->itemamount;
mo->movedir = mo->lastlook = moloop+1;
P_SetTarget(&mo->target, player->mo);
P_SetTarget(&mo->hprev, prev);
P_SetTarget(&prev->hnext, mo);
prev = mo;
}
K_BotResetItemConfirm(player, false);
}
else if (ATTACK_IS_DOWN && HOLDING_ITEM && (player->itemflags & IF_ITEMOUT)) // Jawz thrown
{
player->itemamount--;
if (player->throwdir == 1 || player->throwdir == 0)
K_ThrowKartItem(player, true, MT_JAWZ, 1, 0);
else if (player->throwdir == -1) // Throwing backward gives you a dud that doesn't home in
K_ThrowKartItem(player, true, MT_JAWZ_DUD, -1, 0);
K_PlayAttackTaunt(player->mo);
K_UpdateHnextList(player, false);
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_MINE:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
mobj_t *mo;
K_SetItemOut(player);
S_StartSound(player->mo, sfx_s254);
mo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_SSMINE_SHIELD);
if (mo)
{
mo->flags |= MF_NOCLIPTHING;
mo->threshold = 10;
mo->movecount = 1;
mo->movedir = 1;
P_SetTarget(&mo->target, player->mo);
P_SetTarget(&player->mo->hnext, mo);
}
K_BotResetItemConfirm(player, false);
}
else if (ATTACK_IS_DOWN && (player->itemflags & IF_ITEMOUT))
{
player->itemamount--;
K_ThrowKartItem(player, false, MT_SSMINE, 1, 1);
K_PlayAttackTaunt(player->mo);
player->itemflags &= ~IF_ITEMOUT;
K_UpdateHnextList(player, true);
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_LANDMINE:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
player->itemamount--;
K_ThrowLandMine(player);
K_PlayAttackTaunt(player->mo);
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_BALLHOG:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
player->itemamount--;
K_ThrowKartItem(player, true, MT_BALLHOG, 1, 0);
K_PlayAttackTaunt(player->mo);
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_SPB:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
player->itemamount--;
K_ThrowKartItem(player, true, MT_SPB, 1, 0);
K_PlayAttackTaunt(player->mo);
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_GROW:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
player->itemamount--;
if (player->growshrinktimer < 0 && !K_IsKartItemAlternate(KITEM_SHRINK))
{
// If you're shrunk, then "grow" will just make you normal again...
K_RemoveGrowShrink(player);
}
else
{
// ...in Legacy mode.
// Alt. Shrink's a powerup, so Grow overrides!
if (K_IsAltShrunk(player))
{
player->growshrinktimer = 0; // Paranoia
}
K_PlayPowerGloatSound(player->mo);
K_DoGrowShrink(player, false);
S_StartSound(player->mo, sfx_kc5a);
}
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_SHRINK:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
if (K_IsKartItemAlternate(KITEM_SHRINK))
{
K_DoGrowShrink(player, true);
S_StartSound(player->mo, sfx_kc46);
}
else
{
K_DoShrink(player);
}
player->itemamount--;
K_PlayPowerGloatSound(player->mo);
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_THUNDERSHIELD:
if (K_GetShieldFromPlayer(player) != KSHIELD_THUNDER)
{
mobj_t *shield = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_THUNDERSHIELD);
P_SetScale(shield, (shield->destscale = (5*shield->destscale)>>2));
P_SetTarget(&shield->target, player->mo);
P_SetTarget(&player->shieldtracer, shield);
S_StartSound(player->mo, sfx_s3k41);
}
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
K_DoThunderShield(player);
if (player->itemamount > 0)
{
// Why is this a conditional?
// Thunder shield: the only item that allows you to
// activate a mine while you're out of its radius,
// the SAME tic it sets your itemamount to 0
// ...:dumbestass:
player->itemamount--;
K_PlayAttackTaunt(player->mo);
K_BotResetItemConfirm(player, false);
}
}
break;
case KITEM_BUBBLESHIELD:
if (K_GetShieldFromPlayer(player) != KSHIELD_BUBBLE)
{
mobj_t *shield = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_BUBBLESHIELD);
P_SetScale(shield, (shield->destscale = (5*shield->destscale)>>2));
P_SetTarget(&shield->target, player->mo);
P_SetTarget(&player->shieldtracer, shield);
S_StartSound(player->mo, sfx_s3k3f);
if (player->bubblehealth <= 0 || player->bubblehealth > MAXBUBBLEHEALTH)
player->bubblehealth = MAXBUBBLEHEALTH;
}
if (!HOLDING_ITEM && NO_HYUDORO)
{
if ((buttons & BT_ATTACK && player->itemflags & IF_HOLDREADY)
|| (player->bubbleblowup > 0 && player->bubblecool <= bubbletime/2)) // auto
{
if (player->bubblecool == 0)
S_StartSound(player->mo, sfx_s3k75);
if (player->bubblecool < bubbletime && player->bubblehealth > 0)
{
player->bubbleblowup += 3;
player->bubblehealth--;
}
else if (player->bubblecool >= bubbletime)
player->bubbleblowup++; // overcharge bonus
if (++player->bubblecool >= bubbletime + TICRATE/2)
{
// If you overcharge the Bubble Shield at
// any point, it pops and gives you a boost.
K_PopPlayerShield(player);
if (onground)
{
// If you're on the ground, you're going to
// immediately feel the effects of this boost.
// Play a sound to let everyone know!
S_StartSoundAtVolume(player->mo, sfx_cdfm57, 170);
K_PlayBoostTaunt(player->mo);
}
// experiment: don't boost, just give invulnerability
if (cv_kartbubble_boost_allow.value)
{
player->bubbleboost = 7 * sneakertime / 10;
}
player->flashing = 4*TICRATE/7;
}
}
else
{
if (player->bubblecool > bubbletime)
player->bubblecool = bubbletime;
if (player->bubbleblowup > 0)
{
player->bubbleblowup--;
if (player->bubbleblowup == 0)
K_BotResetItemConfirm(player, false);
if (player->bubblecool < bubbletime)
player->bubblecool++;
}
if (player->bubbleblowup == 0 && player->bubblecool > 0)
{
player->bubblecool--;
if (player->bubblecool == 0 && player->bubblehealth <= 0)
K_PopPlayerShield(player);
}
if (buttons & BT_ATTACK || player->bubblecool > 0)
{
player->itemflags &= ~IF_HOLDREADY;
}
else
{
player->itemflags |= IF_HOLDREADY;
}
}
}
break;
case KITEM_FLAMESHIELD:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO && K_GetShieldFromPlayer(player) != KSHIELD_FLAME)
{
player->itemamount--;
player->flametimer = (itemtime*3);
mobj_t *shield = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_FLAMESHIELD);
P_SetScale(shield, (shield->destscale = (5*shield->destscale)>>2));
P_SetTarget(&shield->target, player->mo);
P_SetTarget(&player->shieldtracer, shield);
S_StartSound(player->mo, sfx_s3k3e);
}
break;
case KITEM_HYUDORO:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
player->itemamount--;
K_DoHyudoroSteal(player); // yes. yes they do.
K_PlayAttackTaunt(player->mo);
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_POGOSPRING:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && (onground || force) && NO_HYUDORO && player->pogospring == 0)
{
player->itemamount--;
K_PlayBoostTaunt(player->mo);
K_DoPogoSpring(player->mo, 32<<FRACBITS, 2);
player->pogospring = 1;
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_SUPERRING:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
player->itemamount--;
K_AwardScaledPlayerRings(player, ASR_SUPERRING);
K_BotResetItemConfirm(player, false);
}
break;
case KITEM_KITCHENSINK:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
mobj_t *mo;
K_SetItemOut(player);
S_StartSound(player->mo, sfx_s254);
mo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_SINK_SHIELD);
if (mo)
{
mo->flags |= MF_NOCLIPTHING;
mo->threshold = 10;
mo->movecount = 1;
mo->movedir = 1;
P_SetTarget(&mo->target, player->mo);
P_SetTarget(&player->mo->hnext, mo);
}
K_BotResetItemConfirm(player, false);
// Woah this could be big, lets get the inside scoop...
K_DirectorForceSwitch(player - players, 1);
}
else if (ATTACK_IS_DOWN && HOLDING_ITEM && (player->itemflags & IF_ITEMOUT)) // Sink thrown
{
player->itemamount--;
K_ThrowKartItem(player, false, MT_SINK, 1, 2);
K_PlayAttackTaunt(player->mo);
player->itemflags &= ~IF_ITEMOUT;
K_UpdateHnextList(player, true);
K_BotResetItemConfirm(player, false);
}
break;
case MAXKARTITEMS: // aka KITEM_SAD
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO
&& !player->sadtimer)
{
player->sadtimer = stealtime;
player->itemamount--;
K_BotResetItemConfirm(player, false);
}
break;
default:
break;
}
}
INT16 K_GetShrinkTime(const player_t *player)
{
return player->growshrinktimer * -1;
}
boolean K_IsAltShrunk(const player_t *player)
{
return player->growshrinktimer < 0 && K_IsKartItemAlternate(KITEM_SHRINK);
}
#define PITY_SHRINKINCREASE_BASE (3)
#define PITY_SHRINKINCREASE (TICRATE / 2)
void K_AltShrinkPityIncrease(player_t *player)
{
// Increase your shrink timer by a little bit for every player you run into.
INT32 shrinktime = K_GetShrinkTime(player);
fixed_t dimin = FRACUNIT;
dimin = max(0, 5 - player->altshrinktimeshit) * FRACUNIT / 5;
shrinktime = min(INT16_MAX, shrinktime + PITY_SHRINKINCREASE_BASE + FixedMul(PITY_SHRINKINCREASE, dimin));
player->growshrinktimer = ((INT16)shrinktime) * -1;
}