// 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_itemlist.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) consvar_t cv_fancyroulette = CVAR_INIT ("fancyroulette", "Off", CV_SAVE, CV_OnOff, NULL); consvar_t cv_fancyroulettespeed = CVAR_INIT ("fancyroulettespeed", "3", CV_SAVE, CV_Natural, NULL); 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 (fasticmp(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; return kartitems[itemtype].flags[K_IsKartItemAlternate(itemtype) ? 1 : 0]; } kartitemequip_e K_GetItemEquipStyle(kartitemtype_e itemtype) { if (itemtype >= numkartitems) return KITEMEQUIP_NONE; return kartitems[itemtype].equipstyle[K_IsKartItemAlternate(itemtype) ? 1 : 0]; } 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 K_GetItemFlags(type) & KIF_ANIMATED ? 2 : kartitems[type].graphics[tiny ? 1 : 0].numpatches + 1; } patch_t *K_GetCachedItemPatchEx(kartitemtype_e type, boolean tiny, UINT8 amount, SINT8 altmode) { kartitem_t *item = &kartitems[type >= numkartresults ? 0 : type]; boolean alt = altmode == -1 ? K_IsKartItemAlternate(type) : !!altmode; kartitemgraphics_t *graphics = &item->graphics[(tiny ? 1 : 0) + (alt ? 2 : 0)]; // fallback if alt graphics don't exist if (graphics->numpatches == 0 && altmode == -1) graphics = &item->graphics[tiny ? 1 : 0]; if (graphics->numpatches == 0) return missingpat; tic_t timer = G_GamestateUsesLevel() ? leveltime : finalecount; if (K_GetItemFlags(type) & 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 (K_GetItemFlags(itemType) & 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 } void K_SetItemOut(player_t *player, kartitemtype_e itemtype, itemflags_t flags) { player->itemflags |= flags; player->equippeditem = itemtype; } void K_UnsetItemOut(player_t *player) { player->itemflags &= ~(IF_ITEMOUT|IF_EGGMANOUT|IF_HOLDREADY); player->bananadrag = 0; player->equippeditem = KITEM_NONE; } 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; if (itemlistactive) K_AddItemRollToList((INT32)(player - players), type, 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 (K_GetItemFlags(player->itemtype) & 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 && K_GetItemFlags(mobj->threshold) & 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 (result->isalt == K_IsKartItemAlternate(result->type) && (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); } UINT16 roulette_size; INT32 K_GetRollingRouletteItem(player_t *player) { UINT8* translation = K_GetRollableItems(); return translation[(player->itemroulette / 3) % roulette_size]; } INT32 K_GetRollingRouletteItemOffset(player_t *player, INT16 offset, INT32 animspeed) { UINT8* translation = K_GetRollableItems(); INT32 icon = ((player->itemroulette + animspeed)/animspeed); return translation[((icon + offset + roulette_size) % roulette_size)]; } UINT8* K_GetRollableItems(void) { static UINT8 translation[MAXKARTRESULTS]; 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 (K_GetItemFlags(result->type) & 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; } odds_cached = gametype; } return translation; } // 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; INT32 roulettespeed = 3; // 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 (cv_fancyroulette.value) { roulettespeed = cv_fancyroulettespeed.value; } if ((player->itemroulette % roulettespeed) == 1 && P_IsDisplayPlayer(player)) { #define PLAYROULETTESND S_StartSound(NULL, sfx_itrol1 + ((player->itemroulette / roulettespeed) % 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, &K_ClusterFilter_NoFilter, 0, &(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); // Add the Sink roll to the list if (itemlistactive) K_AddItemRollToList((INT32)(player - players), KITEM_KITCHENSINK, 1); // 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); if (itemlistactive) { K_AddItemRollToList((INT32)(player - players), players[stealplayer].itemtype, players[stealplayer].itemamount); } 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_SetItemOut(player, KITEM_ROCKETSNEAKER, 0); 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, KITEM_BANANA, IF_ITEMOUT); 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 (K_IsKartItemAlternate(KITEM_EGGMAN)) { 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, KITEM_EGGMAN, IF_ITEMOUT); 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_EGGMINE_SHIELD); if (!mo) { player->itemamount = moloop; break; } mo->flags |= MF_NOCLIPTHING; mo->angle = newangle; 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 && (player->itemflags & IF_ITEMOUT)) // Egg Mine Thrown { mobj_t *mo; player->itemamount -= 1; if (player->throwdir == 1) { mo = K_ThrowKartItem(player, false, MT_EGGMINE, 1, 0); mo->health = 3; mo->whiteshadow = true; mo->lastlook = 1; } else { mo = K_ThrowKartItem(player, true, MT_EGGMINE, 4, 0); if (player->throwdir == 0) { mo->health = 2; mo->whiteshadow = true; } else { mo->health = 1; mo->whiteshadow = false; } mo->lastlook = 1; } K_UpdateHnextList(player, false); K_PlayAttackTaunt(player->mo); } } else { if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO) { mobj_t *mo; player->itemamount--; K_SetItemOut(player, KITEM_EGGMAN, 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, KITEM_ORBINAUT, IF_ITEMOUT); 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, KITEM_JAWZ, IF_ITEMOUT); 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, KITEM_MINE, IF_ITEMOUT); 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<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, KITEM_KITCHENSINK, IF_ITEMOUT); 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; } #undef PITY_SHRINKINCREASE_BASE #undef PITY_SHRINKINCREASE void K_SpawnEggMineBumpEffect(mobj_t *mo) { mobj_t *fx = P_SpawnMobj(mo->x, mo->y, mo->z, MT_BUMP); if (mo->eflags & MFE_VERTICALFLIP) { fx->eflags |= MFE_VERTICALFLIP; } else { fx->eflags |= ~MFE_VERTICALFLIP; } fx->scale = mo->scale; S_StartSound(mo, sfx_cdfm15); } void K_DoEggMineStrip(mobj_t *tmo, mobj_t *inflictor, mobj_t *source) { (void)source; if (inflictor && (inflictor->type == MT_LANDMINE || inflictor->type == MT_EGGMINE)) { if (tmo->player) { S_StartSound(tmo, sfx_bewar3); K_DropItems(tmo->player); tmo->player->itemtype = MAXKARTITEMS; // aka KITEM_SAD tmo->player->itemamount = 1; if (itemlistactive) K_AddItemRollToList((INT32)(tmo->player - players), MAXKARTITEMS, 1); tmo->player->itemblink = TICRATE; tmo->player->itemblinkmode = KITEMBLINK_MASHED; K_SetPlayerItemCooldown(tmo->player, TICRATE, false); } } }