diff --git a/src/Sourcefile b/src/Sourcefile index f2aa652b1..4da925828 100644 --- a/src/Sourcefile +++ b/src/Sourcefile @@ -125,3 +125,4 @@ k_director.c k_follower.c k_profiles.c k_specialstage.c +k_roulette.c diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 086c4cbfa..9820eeed3 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -360,35 +360,36 @@ consvar_t cv_joyscale[MAXSPLITSCREENPLAYERS] = { //Alam: Dummy for save #endif // SRB2kart -consvar_t cv_sneaker = CVAR_INIT ("sneaker", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_rocketsneaker = CVAR_INIT ("rocketsneaker", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_invincibility = CVAR_INIT ("invincibility", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_banana = CVAR_INIT ("banana", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_eggmanmonitor = CVAR_INIT ("eggmanmonitor", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_orbinaut = CVAR_INIT ("orbinaut", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_jawz = CVAR_INIT ("jawz", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_mine = CVAR_INIT ("mine", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_landmine = CVAR_INIT ("landmine", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_ballhog = CVAR_INIT ("ballhog", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_selfpropelledbomb = CVAR_INIT ("selfpropelledbomb", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_grow = CVAR_INIT ("grow", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_shrink = CVAR_INIT ("shrink", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_lightningshield = CVAR_INIT ("lightningshield", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_bubbleshield = CVAR_INIT ("bubbleshield", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_flameshield = CVAR_INIT ("flameshield", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_hyudoro = CVAR_INIT ("hyudoro", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_pogospring = CVAR_INIT ("pogospring", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_superring = CVAR_INIT ("superring", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_kitchensink = CVAR_INIT ("kitchensink", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_droptarget = CVAR_INIT ("droptarget", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_gardentop = CVAR_INIT ("gardentop", "On", CV_NETVAR, CV_OnOff, NULL); - -consvar_t cv_dualsneaker = CVAR_INIT ("dualsneaker", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_triplesneaker = CVAR_INIT ("triplesneaker", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_triplebanana = CVAR_INIT ("triplebanana", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_tripleorbinaut = CVAR_INIT ("tripleorbinaut", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_quadorbinaut = CVAR_INIT ("quadorbinaut", "On", CV_NETVAR, CV_OnOff, NULL); -consvar_t cv_dualjawz = CVAR_INIT ("dualjawz", "On", CV_NETVAR, CV_OnOff, NULL); +consvar_t cv_items[NUMKARTRESULTS-1] = { + CVAR_INIT ("sneaker", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("rocketsneaker", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("invincibility", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("banana", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("eggmanmonitor", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("orbinaut", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("jawz", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("mine", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("landmine", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("ballhog", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("selfpropelledbomb", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("grow", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("shrink", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("lightningshield", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("bubbleshield", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("flameshield", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("hyudoro", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("pogospring", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("superring", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("kitchensink", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("droptarget", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("gardentop", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("dualsneaker", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("triplesneaker", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("triplebanana", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("tripleorbinaut", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("quadorbinaut", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("dualjawz", "On", CV_NETVAR, CV_OnOff, NULL) +}; consvar_t cv_kartspeed = CVAR_INIT ("gamespeed", "Auto", CV_NETVAR|CV_CALL|CV_NOINIT, kartspeed_cons_t, KartSpeed_OnChange); static CV_PossibleValue_t kartbumpers_cons_t[] = {{1, "MIN"}, {12, "MAX"}, {0, NULL}}; @@ -5659,7 +5660,7 @@ static void Got_Cheat(UINT8 **cp, INT32 playernum) K_StripItems(player); // Cancel roulette if rolling - player->itemroulette = 0; + player->itemRoulette.active = false; player->itemtype = item; player->itemamount = amt; diff --git a/src/d_netcmd.h b/src/d_netcmd.h index 3f4d00645..e0b969ffd 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -16,6 +16,7 @@ #define __D_NETCMD__ #include "command.h" +#include "d_player.h" // console vars extern consvar_t cv_playername[MAXSPLITSCREENPLAYERS]; @@ -72,37 +73,7 @@ extern consvar_t cv_pause; extern consvar_t cv_restrictskinchange, cv_allowteamchange, cv_maxplayers, cv_respawntime; // SRB2kart items -extern consvar_t - cv_sneaker, - cv_rocketsneaker, - cv_invincibility, - cv_banana, - cv_eggmanmonitor, - cv_orbinaut, - cv_jawz, - cv_mine, - cv_landmine, - cv_ballhog, - cv_selfpropelledbomb, - cv_grow, - cv_shrink, - cv_lightningshield, - cv_bubbleshield, - cv_flameshield, - cv_hyudoro, - cv_pogospring, - cv_superring, - cv_kitchensink, - cv_droptarget, - cv_gardentop; - -extern consvar_t - cv_dualsneaker, - cv_triplesneaker, - cv_triplebanana, - cv_tripleorbinaut, - cv_quadorbinaut, - cv_dualjawz; +extern consvar_t cv_items[NUMKARTRESULTS-1]; extern consvar_t cv_kartspeed; extern consvar_t cv_kartbumpers; diff --git a/src/d_player.h b/src/d_player.h index a65f0235b..81a80d641 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -225,6 +225,7 @@ typedef enum // Item box khud_itemblink, // Item flashing after roulette, serves as a mashing indicator khud_itemblinkmode, // Type of flashing: 0 = white (normal), 1 = red (mashing), 2 = rainbow (enhanced items) + khud_rouletteoffset,// Roulette stop height // Rings khud_ringframe, // Ring spin frame @@ -329,6 +330,41 @@ struct skybox_t { mobj_t * centerpoint; }; +// player_t struct for item roulette variables + +// Doing this the right way is causing problems. +// so FINE, it's a static length now. +#define ITEM_LIST_SIZE (NUMKARTRESULTS << 3) + +struct itemroulette_t +{ + boolean active; + +#ifdef ITEM_LIST_SIZE + size_t itemListLen; + SINT8 itemList[ITEM_LIST_SIZE]; +#else + size_t itemListCap; + size_t itemListLen; + SINT8 *itemList; +#endif + + UINT8 useOdds; + UINT8 playing, exiting; + UINT32 dist, baseDist; + UINT32 firstDist, secondDist; + UINT32 secondToFirst; + + size_t index; + UINT8 sound; + + tic_t speed; + tic_t tics; + tic_t elapsed; + + boolean eggman; +}; + // ======================================================================== // PLAYER STRUCTURE // ======================================================================== @@ -477,8 +513,7 @@ struct player_t UINT8 tripwirePass; // see tripwirepass_t UINT16 tripwireLeniency; // When reaching a state that lets you go thru tripwire, you get an extra second leniency after it ends to still go through it. - UINT16 itemroulette; // Used for the roulette when deciding what item to give you (was "pw_kartitem") - UINT8 roulettetype; // Used for the roulette, for deciding type (0 = normal, 1 = better, 2 = eggman mark) + itemroulette_t itemRoulette; // Item roulette data // Item held stuff SINT8 itemtype; // KITEM_ constant for item number diff --git a/src/g_game.c b/src/g_game.c index 5cde2d270..ab4b6bc9c 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -2260,11 +2260,10 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) SINT8 xtralife; // SRB2kart + itemroulette_t itemRoulette; respawnvars_t respawn; INT32 itemtype; INT32 itemamount; - INT32 itemroulette; - INT32 roulettetype; INT32 growshrinktimer; INT32 bumper; boolean songcredit = false; @@ -2323,10 +2322,13 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) pflags = (players[player].pflags & (PF_WANTSTOJOIN|PF_KICKSTARTACCEL|PF_SHRINKME|PF_SHRINKACTIVE)); // SRB2kart + memcpy(&itemRoulette, &players[player].itemRoulette, sizeof (itemRoulette)); + memcpy(&respawn, &players[player].respawn, sizeof (respawn)); + if (betweenmaps || leveltime < introtime) { - itemroulette = 0; - roulettetype = 0; + itemRoulette.active = false; + itemtype = 0; itemamount = 0; growshrinktimer = 0; @@ -2348,9 +2350,6 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) } else { - itemroulette = (players[player].itemroulette > 0 ? 1 : 0); - roulettetype = players[player].roulettetype; - if (players[player].pflags & PF_ITEMOUT) { itemtype = 0; @@ -2406,8 +2405,6 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) P_SetTarget(&players[player].follower, NULL); } - memcpy(&respawn, &players[player].respawn, sizeof (respawn)); - p = &players[player]; memset(p, 0, sizeof (*p)); @@ -2453,8 +2450,6 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) p->xtralife = xtralife; // SRB2kart - p->itemroulette = itemroulette; - p->roulettetype = roulettetype; p->itemtype = itemtype; p->itemamount = itemamount; p->growshrinktimer = growshrinktimer; @@ -2470,6 +2465,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) p->botvars.rubberband = FRACUNIT; p->botvars.controller = UINT16_MAX; + memcpy(&p->itemRoulette, &itemRoulette, sizeof (p->itemRoulette)); memcpy(&p->respawn, &respawn, sizeof (p->respawn)); if (follower) @@ -2481,7 +2477,6 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) //p->follower = NULL; // respawn a new one with you, it looks better. // ^ Not necessary anyway since it will be respawned regardless considering it doesn't exist anymore. - p->playerstate = PST_LIVE; p->panim = PA_STILL; // standing animation diff --git a/src/k_botitem.c b/src/k_botitem.c index 6c8b9ece4..fe0837c84 100644 --- a/src/k_botitem.c +++ b/src/k_botitem.c @@ -26,6 +26,7 @@ #include "d_ticcmd.h" #include "m_random.h" #include "r_things.h" // numskins +#include "k_roulette.h" /*-------------------------------------------------- static inline boolean K_ItemButtonWasDown(player_t *player) @@ -739,7 +740,7 @@ static void K_BotItemEggman(player_t *player, ticcmd_t *cmd) tryLookback = true; } - if (stealth > 1 || player->itemroulette > 0) + if (stealth > 1 || player->itemRoulette.active == true) { player->botvars.itemconfirm += player->botvars.difficulty * 4; throwdir = -1; @@ -1393,27 +1394,15 @@ static void K_BotItemRings(player_t *player, ticcmd_t *cmd) --------------------------------------------------*/ static void K_BotItemRouletteMash(player_t *player, ticcmd_t *cmd) { - boolean mash = false; - if (K_ItemButtonWasDown(player) == true) { return; } - if (player->rings < 0 && cv_superring.value) - { - // Uh oh, we need a loan! - // It'll be better in the long run for bots to lose an item set for 10 free rings. - mash = true; - } + // TODO: Would be nice to implement smarter behavior + // for selecting items. - // TODO: Mash based on how far behind you are, when items are - // almost garantueed to be in your favor. - - if (mash == true) - { - cmd->buttons |= BT_ATTACK; - } + cmd->buttons |= BT_ATTACK; } /*-------------------------------------------------- @@ -1441,7 +1430,7 @@ void K_BotItemUsage(player_t *player, ticcmd_t *cmd, INT16 turnamt) return; } - if (player->itemroulette) + if (player->itemRoulette.active == true) { // Mashing behaviors K_BotItemRouletteMash(player, cmd); diff --git a/src/k_collide.c b/src/k_collide.c index 11ac0b745..867efba03 100644 --- a/src/k_collide.c +++ b/src/k_collide.c @@ -12,6 +12,7 @@ #include "doomdef.h" // Sink snipe print #include "g_game.h" // Sink snipe print #include "k_objects.h" +#include "k_roulette.h" angle_t K_GetCollideAngle(mobj_t *t1, mobj_t *t2) { @@ -158,10 +159,7 @@ boolean K_EggItemCollide(mobj_t *t1, mobj_t *t2) } else { - K_DropItems(t2->player); //K_StripItems(t2->player); - //K_StripOther(t2->player); - t2->player->itemroulette = 1; - t2->player->roulettetype = 2; + K_StartEggmanRoulette(t2->player); } if (t2->player->flamedash && t2->player->itemtype == KITEM_FLAMESHIELD) diff --git a/src/k_hud.c b/src/k_hud.c index 9822f1706..7c1d94e42 100644 --- a/src/k_hud.c +++ b/src/k_hud.c @@ -36,6 +36,7 @@ #include "r_things.h" #include "r_fps.h" #include "m_random.h" +#include "k_roulette.h" //{ Patch Definitions static patch_t *kp_nodraw; @@ -1022,63 +1023,83 @@ static void K_drawKartItem(void) // Why write V_DrawScaledPatch calls over and over when they're all the same? // Set to 'no item' just in case. const UINT8 offset = ((r_splitscreen > 1) ? 1 : 0); - patch_t *localpatch = kp_nodraw; + patch_t *localpatch[3] = { kp_nodraw }; patch_t *localbg = ((offset) ? kp_itembg[2] : kp_itembg[0]); patch_t *localinv = ((offset) ? kp_invincibility[((leveltime % (6*3)) / 3) + 7] : kp_invincibility[(leveltime % (7*3)) / 3]); INT32 fx = 0, fy = 0, fflags = 0; // final coords for hud and flags... const INT32 numberdisplaymin = ((!offset && stplyr->itemtype == KITEM_ORBINAUT) ? 5 : 2); INT32 itembar = 0; INT32 maxl = 0; // itembar's normal highest value - const INT32 barlength = (r_splitscreen > 1 ? 12 : 26); - UINT16 localcolor = SKINCOLOR_NONE; - SINT8 colormode = TC_RAINBOW; - UINT8 *colmap = NULL; + const INT32 barlength = (offset ? 12 : 26); + UINT16 localcolor[3] = { stplyr->skincolor }; + SINT8 colormode[3] = { TC_RAINBOW }; boolean flipamount = false; // Used for 3P/4P splitscreen to flip item amount stuff - if (stplyr->itemroulette) + fixed_t rouletteOffset = 0; + fixed_t rouletteSpace = ROULETTE_SPACING; + vector2_t rouletteCrop = {7, 7}; + INT32 i; + + if (stplyr->itemRoulette.itemListLen > 0) { - const INT32 item = K_GetRollingRouletteItem(stplyr); - - if (stplyr->skincolor) - localcolor = stplyr->skincolor; - - switch (item) + // Init with item roulette stuff. + for (i = 0; i < 3; i++) { - case KITEM_INVINCIBILITY: - localpatch = localinv; - break; + const SINT8 indexOfs = i-1; + const size_t index = (stplyr->itemRoulette.index + indexOfs) % stplyr->itemRoulette.itemListLen; - case KITEM_ORBINAUT: - localpatch = kp_orbinaut[3 + offset]; - break; + const SINT8 result = stplyr->itemRoulette.itemList[index]; + const SINT8 item = K_ItemResultToType(result); + const UINT8 amt = K_ItemResultToAmount(result); - default: - localpatch = K_GetCachedItemPatch(item, offset); + switch (item) + { + case KITEM_INVINCIBILITY: + localpatch[i] = localinv; + break; + + case KITEM_ORBINAUT: + localpatch[i] = kp_orbinaut[(offset ? 4 : min(amt-1, 3))]; + break; + + default: + localpatch[i] = K_GetCachedItemPatch(item, offset); + break; + } } } + + if (stplyr->itemRoulette.active == true) + { + rouletteOffset = K_GetRouletteOffset(&stplyr->itemRoulette, rendertimefrac); + } else { // I'm doing this a little weird and drawing mostly in reverse order // The only actual reason is to make sneakers line up this way in the code below // This shouldn't have any actual baring over how it functions // Hyudoro is first, because we're drawing it on top of the player's current item + + localcolor[1] = SKINCOLOR_NONE; + rouletteOffset = stplyr->karthud[khud_rouletteoffset]; + if (stplyr->stealingtimer < 0) { if (leveltime & 2) - localpatch = kp_hyudoro[offset]; + localpatch[1] = kp_hyudoro[offset]; else - localpatch = kp_nodraw; + localpatch[1] = kp_nodraw; } else if ((stplyr->stealingtimer > 0) && (leveltime & 2)) { - localpatch = kp_hyudoro[offset]; + localpatch[1] = kp_hyudoro[offset]; } else if (stplyr->eggmanexplode > 1) { if (leveltime & 1) - localpatch = kp_eggman[offset]; + localpatch[1] = kp_eggman[offset]; else - localpatch = kp_nodraw; + localpatch[1] = kp_nodraw; } else if (stplyr->ballhogcharge > 0) { @@ -1086,9 +1107,9 @@ static void K_drawKartItem(void) maxl = (((stplyr->itemamount-1) * BALLHOGINCREMENT) + 1); if (leveltime & 1) - localpatch = kp_ballhog[offset]; + localpatch[1] = kp_ballhog[offset]; else - localpatch = kp_nodraw; + localpatch[1] = kp_nodraw; } else if (stplyr->rocketsneakertimer > 1) { @@ -1096,31 +1117,31 @@ static void K_drawKartItem(void) maxl = (itemtime*3) - barlength; if (leveltime & 1) - localpatch = kp_rocketsneaker[offset]; + localpatch[1] = kp_rocketsneaker[offset]; else - localpatch = kp_nodraw; + localpatch[1] = kp_nodraw; } else if (stplyr->sadtimer > 0) { if (leveltime & 2) - localpatch = kp_sadface[offset]; + localpatch[1] = kp_sadface[offset]; else - localpatch = kp_nodraw; + localpatch[1] = kp_nodraw; } else { if (stplyr->itemamount <= 0) return; - switch(stplyr->itemtype) + switch (stplyr->itemtype) { case KITEM_INVINCIBILITY: - localpatch = localinv; + localpatch[1] = localinv; localbg = kp_itembg[offset+1]; break; case KITEM_ORBINAUT: - localpatch = kp_orbinaut[(offset ? 4 : min(stplyr->itemamount-1, 3))]; + localpatch[1] = kp_orbinaut[(offset ? 4 : min(stplyr->itemamount-1, 3))]; break; case KITEM_SPB: @@ -1131,44 +1152,45 @@ static void K_drawKartItem(void) /*FALLTHRU*/ default: - localpatch = K_GetCachedItemPatch(stplyr->itemtype, offset); + localpatch[1] = K_GetCachedItemPatch(stplyr->itemtype, offset); - if (localpatch == NULL) - localpatch = kp_nodraw; // diagnose underflows + if (localpatch[1] == NULL) + localpatch[1] = kp_nodraw; // diagnose underflows break; } if ((stplyr->pflags & PF_ITEMOUT) && !(leveltime & 1)) - localpatch = kp_nodraw; + localpatch[1] = kp_nodraw; } if (stplyr->karthud[khud_itemblink] && (leveltime & 1)) { - colormode = TC_BLINK; + colormode[1] = TC_BLINK; switch (stplyr->karthud[khud_itemblinkmode]) { case 2: - localcolor = K_RainbowColor(leveltime); + localcolor[1] = K_RainbowColor(leveltime); break; case 1: - localcolor = SKINCOLOR_RED; + localcolor[1] = SKINCOLOR_RED; break; default: - localcolor = SKINCOLOR_WHITE; + localcolor[1] = SKINCOLOR_WHITE; break; } } + else + { + // Hide the other items. + // Effectively lets the other roulette items + // show flicker away after you select. + localpatch[0] = localpatch[2] = kp_nodraw; + } } // pain and suffering defined below - if (r_splitscreen < 2) // don't change shit for THIS splitscreen. - { - fx = ITEM_X; - fy = ITEM_Y; - fflags = V_SNAPTOTOP|V_SNAPTOLEFT|V_SPLITSCREEN; - } - else // now we're having a fun game. + if (offset) { if (stplyr == &players[displayplayers[0]] || stplyr == &players[displayplayers[2]]) // If we are P1 or P3... { @@ -1183,35 +1205,91 @@ static void K_drawKartItem(void) fflags = V_SNAPTORIGHT|V_SNAPTOTOP|V_SPLITSCREEN; flipamount = true; } - } - if (localcolor != SKINCOLOR_NONE) - colmap = R_GetTranslationColormap(colormode, localcolor, GTC_CACHE); + rouletteSpace = ROULETTE_SPACING_SPLITSCREEN; + rouletteOffset = FixedMul(rouletteOffset, FixedDiv(ROULETTE_SPACING_SPLITSCREEN, ROULETTE_SPACING)); + rouletteCrop.x = 16; + rouletteCrop.y = 15; + } + else + { + fx = ITEM_X; + fy = ITEM_Y; + fflags = V_SNAPTOTOP|V_SNAPTOLEFT|V_SPLITSCREEN; + } V_DrawScaledPatch(fx, fy, V_HUDTRANS|V_SLIDEIN|fflags, localbg); - //V_SetClipRect((fx + 10) << FRACBITS, (fy + 10) << FRACBITS, 30 << FRACBITS, 30 << FRACBITS, V_HUDTRANS|V_SLIDEIN|fflags); + // Need to draw these in a particular order, for sorting. + V_SetClipRect( + (fx + rouletteCrop.x) << FRACBITS, (fy + rouletteCrop.y) << FRACBITS, + rouletteSpace, rouletteSpace, + V_SLIDEIN|fflags + ); - // Then, the numbers: - if (stplyr->itemamount >= numberdisplaymin && !stplyr->itemroulette) + V_DrawFixedPatch( + fx<itemRoulette.active == true) { - V_DrawScaledPatch(fx + (flipamount ? 48 : 0), fy, V_HUDTRANS|V_SLIDEIN|fflags|(flipamount ? V_FLIP : 0), kp_itemmulsticker[offset]); // flip this graphic for p2 and p4 in split and shift it. - V_DrawFixedPatch(fx<itemamount)); - else - V_DrawString(fx+24, fy+31, V_ALLOWLOWERCASE|V_HUDTRANS|V_SLIDEIN|fflags, va("x%d", stplyr->itemamount)); - else - { - V_DrawScaledPatch(fy+28, fy+41, V_HUDTRANS|V_SLIDEIN|fflags, kp_itemx); - V_DrawKartString(fx+38, fy+36, V_HUDTRANS|V_SLIDEIN|fflags, va("%d", stplyr->itemamount)); - } + // Draw the item underneath the box. + V_DrawFixedPatch( + fx<itemamount >= numberdisplaymin && stplyr->itemRoulette.active == false) + { + // Then, the numbers: + V_DrawScaledPatch( + fx + (flipamount ? 48 : 0), fy, + V_HUDTRANS|V_SLIDEIN|fflags|(flipamount ? V_FLIP : 0), + kp_itemmulsticker[offset] + ); // flip this graphic for p2 and p4 in split and shift it. + + V_DrawFixedPatch( + fx<itemamount)); + else + V_DrawString(fx+24, fy+31, V_ALLOWLOWERCASE|V_HUDTRANS|V_SLIDEIN|fflags, va("x%d", stplyr->itemamount)); + } + else + { + V_DrawScaledPatch(fy+28, fy+41, V_HUDTRANS|V_SLIDEIN|fflags, kp_itemx); + V_DrawKartString(fx+38, fy+36, V_HUDTRANS|V_SLIDEIN|fflags, va("%d", stplyr->itemamount)); + } + } + else + { + V_DrawFixedPatch( + fx<> 1); + const fixed_t space = 24 * scale; + const fixed_t pad = 9 * scale; + + fixed_t x = -pad; + fixed_t y = -pad; + size_t i; if (stplyr != &players[displayplayers[0]]) // only for p1 - return; - - if (K_ForcedSPB(stplyr) == true) { - V_DrawScaledPatch(x, y, V_SNAPTOTOP, items[KITEM_SPB]); - V_DrawThinString(x+11, y+31, V_ALLOWLOWERCASE|V_SNAPTOTOP, "EX"); return; } - // The only code duplication from the Kart, just to avoid the actual item function from calculating pingame twice - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i] || players[i].spectator) - continue; - pingame++; - if (players[i].bumpers > bestbumper) - bestbumper = players[i].bumpers; - } + K_FillItemRouletteData(stplyr, &rouletteData); - // lovely double loop...... - for (i = 0; i < MAXPLAYERS; i++) + for (i = 0; i < rouletteData.itemListLen; i++) { - if (playeringame[i] && !players[i].spectator - && players[i].position == 1) + const kartitems_t item = rouletteData.itemList[i]; + UINT8 amount = 1; + + if (y > (BASEVIDHEIGHT << FRACBITS) - space - pad) { - // This player is first! Yay! - pdis = stplyr->distancetofinish - players[i].distancetofinish; - break; + x += space; + y = -pad; } - } - pdis = K_ScaleItemDistance(pdis, pingame); - - if (stplyr->bot && stplyr->botvars.rival) - { - // Rival has better odds :) - pdis = (15 * pdis) / 14; - } - - useodds = K_FindUseodds(stplyr, 0, pdis, bestbumper); - - for (i = 1; i < NUMKARTRESULTS; i++) - { - INT32 itemodds = K_KartGetItemOdds( - useodds, i, - stplyr->distancetofinish, - 0, - stplyr->bot, (stplyr->bot && stplyr->botvars.rival) - ); - INT32 amount = 1; - - if (itemodds <= 0) - continue; - - V_DrawScaledPatch(x, y, V_SNAPTOTOP, items[i]); - V_DrawThinString(x+11, y+31, V_SNAPTOTOP, va("%d", itemodds)); + V_DrawFixedPatch(x, y, scale, V_SNAPTOTOP, patches[item], NULL); // Display amount for multi-items - amount = K_ItemResultToAmount(i); + amount = K_ItemResultToAmount(item); if (amount > 1) { - V_DrawString(x+24, y+31, V_ALLOWLOWERCASE|V_SNAPTOTOP, va("x%d", amount)); + V_DrawStringScaled( + x + (18 * scale), + y + (23 * scale), + scale, FRACUNIT, FRACUNIT, + V_ALLOWLOWERCASE|V_SNAPTOTOP, + NULL, HU_FONT, + va("x%d", amount) + ); } - x += 32; - if (x >= 297) - { - x = -9; - y += 32; - } + y += space; } - V_DrawString(0, 0, V_SNAPTOTOP, va("USEODDS %d", useodds)); + V_DrawString((x >> FRACBITS) + 20, 2, V_ALLOWLOWERCASE|V_SNAPTOTOP, va("useOdds[%u]", rouletteData.useOdds)); + V_DrawString((x >> FRACBITS) + 20, 10, V_ALLOWLOWERCASE|V_SNAPTOTOP, va("speed = %u", rouletteData.speed)); + + V_DrawString((x >> FRACBITS) + 20, 22, V_ALLOWLOWERCASE|V_SNAPTOTOP, va("baseDist = %u", rouletteData.baseDist)); + V_DrawString((x >> FRACBITS) + 20, 30, V_ALLOWLOWERCASE|V_SNAPTOTOP, va("dist = %u", rouletteData.dist)); + + V_DrawString((x >> FRACBITS) + 20, 42, V_ALLOWLOWERCASE|V_SNAPTOTOP, va("firstDist = %u", rouletteData.firstDist)); + V_DrawString((x >> FRACBITS) + 20, 50, V_ALLOWLOWERCASE|V_SNAPTOTOP, va("secondDist = %u", rouletteData.secondDist)); + V_DrawString((x >> FRACBITS) + 20, 58, V_ALLOWLOWERCASE|V_SNAPTOTOP, va("secondToFirst = %u", rouletteData.secondToFirst)); + +#ifndef ITEM_LIST_SIZE + Z_Free(rouletteData.itemList); +#endif } static void K_DrawWaypointDebugger(void) diff --git a/src/k_kart.c b/src/k_kart.c index 12598f831..92fb5dbc5 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -42,6 +42,7 @@ #include "k_objects.h" #include "k_grandprix.h" #include "k_specialstage.h" +#include "k_roulette.h" // SOME IMPORTANT VARIABLES DEFINED IN DOOMDEF.H: // gamespeed is cc (0 for easy, 1 for normal, 2 for hard) @@ -295,35 +296,12 @@ angle_t K_ReflectAngle(angle_t yourangle, angle_t theirangle, fixed_t yourspeed, void K_RegisterKartStuff(void) { - CV_RegisterVar(&cv_sneaker); - CV_RegisterVar(&cv_rocketsneaker); - CV_RegisterVar(&cv_invincibility); - CV_RegisterVar(&cv_banana); - CV_RegisterVar(&cv_eggmanmonitor); - CV_RegisterVar(&cv_orbinaut); - CV_RegisterVar(&cv_jawz); - CV_RegisterVar(&cv_mine); - CV_RegisterVar(&cv_landmine); - CV_RegisterVar(&cv_ballhog); - CV_RegisterVar(&cv_selfpropelledbomb); - CV_RegisterVar(&cv_grow); - CV_RegisterVar(&cv_shrink); - CV_RegisterVar(&cv_lightningshield); - CV_RegisterVar(&cv_bubbleshield); - CV_RegisterVar(&cv_flameshield); - CV_RegisterVar(&cv_hyudoro); - CV_RegisterVar(&cv_pogospring); - CV_RegisterVar(&cv_superring); - CV_RegisterVar(&cv_kitchensink); - CV_RegisterVar(&cv_droptarget); - CV_RegisterVar(&cv_gardentop); + INT32 i; - CV_RegisterVar(&cv_dualsneaker); - CV_RegisterVar(&cv_triplesneaker); - CV_RegisterVar(&cv_triplebanana); - CV_RegisterVar(&cv_tripleorbinaut); - CV_RegisterVar(&cv_quadorbinaut); - CV_RegisterVar(&cv_dualjawz); + for (i = 0; i < NUMKARTRESULTS-1; i++) + { + CV_RegisterVar(&cv_items[i]); + } CV_RegisterVar(&cv_kartspeed); CV_RegisterVar(&cv_kartbumpers); @@ -397,150 +375,6 @@ fixed_t K_GetKartGameSpeedScalar(SINT8 value) return ((13 + (3*value)) << FRACBITS) / 16; } -//{ SRB2kart Roulette Code - Position Based - -consvar_t *KartItemCVars[NUMKARTRESULTS-1] = -{ - &cv_sneaker, - &cv_rocketsneaker, - &cv_invincibility, - &cv_banana, - &cv_eggmanmonitor, - &cv_orbinaut, - &cv_jawz, - &cv_mine, - &cv_landmine, - &cv_ballhog, - &cv_selfpropelledbomb, - &cv_grow, - &cv_shrink, - &cv_lightningshield, - &cv_bubbleshield, - &cv_flameshield, - &cv_hyudoro, - &cv_pogospring, - &cv_superring, - &cv_kitchensink, - &cv_droptarget, - &cv_gardentop, - &cv_dualsneaker, - &cv_triplesneaker, - &cv_triplebanana, - &cv_tripleorbinaut, - &cv_quadorbinaut, - &cv_dualjawz -}; - -#define NUMKARTODDS 80 - -// Less ugly 2D arrays -static UINT8 K_KartItemOddsRace[NUMKARTRESULTS-1][8] = -{ - //B C D E F G H I - { 0, 0, 2, 3, 4, 0, 0, 0 }, // Sneaker - { 0, 0, 0, 0, 0, 3, 4, 5 }, // Rocket Sneaker - { 0, 0, 0, 0, 2, 5, 5, 7 }, // Invincibility - { 2, 3, 1, 0, 0, 0, 0, 0 }, // Banana - { 1, 2, 0, 0, 0, 0, 0, 0 }, // Eggman Monitor - { 5, 5, 2, 2, 0, 0, 0, 0 }, // Orbinaut - { 0, 4, 2, 1, 0, 0, 0, 0 }, // Jawz - { 0, 3, 3, 2, 0, 0, 0, 0 }, // Mine - { 3, 0, 0, 0, 0, 0, 0, 0 }, // Land Mine - { 0, 0, 2, 2, 0, 0, 0, 0 }, // Ballhog - { 0, 0, 0, 0, 0, 2, 4, 0 }, // Self-Propelled Bomb - { 0, 0, 0, 0, 2, 5, 0, 0 }, // Grow - { 0, 0, 0, 0, 0, 2, 4, 2 }, // Shrink - { 1, 0, 0, 0, 0, 0, 0, 0 }, // Lightning Shield - { 0, 1, 2, 1, 0, 0, 0, 0 }, // Bubble Shield - { 0, 0, 0, 0, 0, 1, 3, 5 }, // Flame Shield - { 3, 0, 0, 0, 0, 0, 0, 0 }, // Hyudoro - { 0, 0, 0, 0, 0, 0, 0, 0 }, // Pogo Spring - { 2, 1, 1, 0, 0, 0, 0, 0 }, // Super Ring - { 0, 0, 0, 0, 0, 0, 0, 0 }, // Kitchen Sink - { 3, 0, 0, 0, 0, 0, 0, 0 }, // Drop Target - { 0, 0, 0, 3, 5, 0, 0, 0 }, // Garden Top - { 0, 0, 2, 2, 2, 0, 0, 0 }, // Sneaker x2 - { 0, 0, 0, 0, 4, 4, 4, 0 }, // Sneaker x3 - { 0, 1, 1, 0, 0, 0, 0, 0 }, // Banana x3 - { 0, 0, 1, 0, 0, 0, 0, 0 }, // Orbinaut x3 - { 0, 0, 0, 2, 0, 0, 0, 0 }, // Orbinaut x4 - { 0, 0, 1, 2, 1, 0, 0, 0 } // Jawz x2 -}; - -static UINT8 K_KartItemOddsBattle[NUMKARTRESULTS][2] = -{ - //K L - { 2, 1 }, // Sneaker - { 0, 0 }, // Rocket Sneaker - { 4, 1 }, // Invincibility - { 0, 0 }, // Banana - { 1, 0 }, // Eggman Monitor - { 8, 0 }, // Orbinaut - { 8, 1 }, // Jawz - { 6, 1 }, // Mine - { 2, 0 }, // Land Mine - { 2, 1 }, // Ballhog - { 0, 0 }, // Self-Propelled Bomb - { 2, 1 }, // Grow - { 0, 0 }, // Shrink - { 4, 0 }, // Lightning Shield - { 1, 0 }, // Bubble Shield - { 1, 0 }, // Flame Shield - { 2, 0 }, // Hyudoro - { 3, 0 }, // Pogo Spring - { 0, 0 }, // Super Ring - { 0, 0 }, // Kitchen Sink - { 2, 0 }, // Drop Target - { 4, 0 }, // Garden Top - { 0, 0 }, // Sneaker x2 - { 0, 1 }, // Sneaker x3 - { 0, 0 }, // Banana x3 - { 2, 0 }, // Orbinaut x3 - { 1, 1 }, // Orbinaut x4 - { 5, 1 } // Jawz x2 -}; - -// TODO: add back when this gets used -#if 0 -static UINT8 K_KartItemOddsSpecial[NUMKARTRESULTS-1][4] = -{ - //M N O P - { 1, 1, 0, 0 }, // Sneaker - { 0, 0, 0, 0 }, // Rocket Sneaker - { 0, 0, 0, 0 }, // Invincibility - { 0, 0, 0, 0 }, // Banana - { 0, 0, 0, 0 }, // Eggman Monitor - { 1, 1, 0, 0 }, // Orbinaut - { 1, 1, 0, 0 }, // Jawz - { 0, 0, 0, 0 }, // Mine - { 0, 0, 0, 0 }, // Land Mine - { 0, 0, 0, 0 }, // Ballhog - { 0, 0, 0, 1 }, // Self-Propelled Bomb - { 0, 0, 0, 0 }, // Grow - { 0, 0, 0, 0 }, // Shrink - { 0, 0, 0, 0 }, // Lightning Shield - { 0, 0, 0, 0 }, // Bubble Shield - { 0, 0, 0, 0 }, // Flame Shield - { 0, 0, 0, 0 }, // Hyudoro - { 0, 0, 0, 0 }, // Pogo Spring - { 0, 0, 0, 0 }, // Super Ring - { 0, 0, 0, 0 }, // Kitchen Sink - { 0, 0, 0, 0 }, // Drop Target - { 0, 0, 0, 0 }, // Garden Top - { 0, 1, 1, 0 }, // Sneaker x2 - { 0, 0, 1, 1 }, // Sneaker x3 - { 0, 0, 0, 0 }, // Banana x3 - { 0, 1, 1, 0 }, // Orbinaut x3 - { 0, 0, 1, 1 }, // Orbinaut x4 - { 0, 0, 1, 1 } // Jawz x2 -}; -#endif - -#define DISTVAR (2048) // Magic number distance for use with item roulette tiers -#define SPBSTARTDIST (6*DISTVAR) // Distance when SPB can start appearing -#define SPBFORCEDIST (12*DISTVAR) // Distance when SPB is forced onto the next person who rolls an item -#define ENDDIST (12*DISTVAR) // Distance when the game stops giving you bananas - // Array of states to pick the starting point of the animation, based on the actual time left for invincibility. static INT32 K_SparkleTrailStartStates[KART_NUMINVSPARKLESANIM][2] = { {S_KARTINVULN12, S_KARTINVULNB12}, @@ -669,813 +503,6 @@ void K_RunItemCooldowns(void) } } - -/** \brief Item Roulette for Kart - - \param player player - \param getitem what item we're looking for - - \return void -*/ -static void K_KartGetItemResult(player_t *player, SINT8 getitem) -{ - if (getitem == KITEM_SPB || getitem == KITEM_SHRINK) - { - K_SetItemCooldown(getitem, 20*TICRATE); - } - - player->botvars.itemdelay = TICRATE; - player->botvars.itemconfirm = 0; - - player->itemtype = K_ItemResultToType(getitem); - player->itemamount = K_ItemResultToAmount(getitem); -} - -fixed_t K_ItemOddsScale(UINT8 playerCount) -{ - const UINT8 basePlayer = 8; // The player count we design most of the game around. - fixed_t playerScaling = 0; - - if (playerCount < 2) - { - // Cap to 1v1 scaling - playerCount = 2; - } - - // Then, it multiplies it further if the player count isn't equal to basePlayer. - // This is done to make low player count races more interesting and high player count rates more fair. - if (playerCount < basePlayer) - { - // Less than basePlayer: increase odds significantly. - // 2P: x2.5 - playerScaling = (basePlayer - playerCount) * (FRACUNIT / 4); - } - else if (playerCount > basePlayer) - { - // More than basePlayer: reduce odds slightly. - // 16P: x0.75 - playerScaling = (basePlayer - playerCount) * (FRACUNIT / 32); - } - - return playerScaling; -} - -UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers) -{ - if (mapobjectscale != FRACUNIT) - { - // Bring back to normal scale. - distance = FixedDiv(distance, mapobjectscale); - } - - if (franticitems == true) - { - // Frantic items pretends everyone's farther apart, for crazier items. - distance = (15 * distance) / 14; - } - - // Items get crazier with the fewer players that you have. - distance = FixedMul( - distance, - FRACUNIT + (K_ItemOddsScale(numPlayers) / 2) - ); - - return distance; -} - -/** \brief Item Roulette for Kart - - \param player player object passed from P_KartPlayerThink - - \return void -*/ - -INT32 K_KartGetItemOdds( - UINT8 pos, SINT8 item, - UINT32 ourDist, - fixed_t mashed, - boolean bot, boolean rival) -{ - INT32 newodds; - INT32 i; - - UINT8 pingame = 0, pexiting = 0; - - player_t *first = NULL; - player_t *second = NULL; - - UINT32 firstDist = UINT32_MAX; - UINT32 secondDist = UINT32_MAX; - UINT32 secondToFirst = 0; - boolean isFirst = false; - - boolean powerItem = false; - boolean cooldownOnStart = false; - boolean notNearEnd = false; - - INT32 shieldtype = KSHIELD_NONE; - - I_Assert(item > KITEM_NONE); // too many off by one scenarioes. - I_Assert(KartItemCVars[NUMKARTRESULTS-2] != NULL); // Make sure this exists - - if (!KartItemCVars[item-1]->value && !modeattacking) - { - return 0; - } - - if (K_GetItemCooldown(item) > 0) - { - // Cooldown is still running, don't give another. - return 0; - } - - /* - if (bot) - { - // TODO: Item use on bots should all be passed-in functions. - // Instead of manually inserting these, it should return 0 - // for any items without an item use function supplied - - switch (item) - { - case KITEM_SNEAKER: - break; - default: - return 0; - } - } - */ - (void)bot; - - if (gametype == GT_BATTLE) - { - I_Assert(pos < 2); // DO NOT allow positions past the bounds of the table - newodds = K_KartItemOddsBattle[item-1][pos]; - } - else - { - I_Assert(pos < 8); // Ditto - newodds = K_KartItemOddsRace[item-1][pos]; - } - - // Base multiplication to ALL item odds to simulate fractional precision - newodds *= 4; - - shieldtype = K_GetShieldFromItem(item); - - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i] || players[i].spectator) - continue; - - if (!(gametyperules & GTR_BUMPERS) || players[i].bumpers) - pingame++; - - if (players[i].exiting) - pexiting++; - - switch (shieldtype) - { - case KSHIELD_NONE: - /* Marble Garden Top is not REALLY - a Sonic 3 shield */ - case KSHIELD_TOP: - break; - - default: - if (shieldtype == K_GetShieldFromItem(players[i].itemtype)) - { - // Don't allow more than one of each shield type at a time - return 0; - } - } - - if (players[i].position == 1) - { - first = &players[i]; - } - - if (players[i].position == 2) - { - second = &players[i]; - } - } - - if (first != NULL) // calculate 2nd's distance from 1st, for SPB - { - firstDist = first->distancetofinish; - isFirst = (ourDist <= firstDist); - } - - if (second != NULL) - { - secondDist = second->distancetofinish; - } - - if (first != NULL && second != NULL) - { - secondToFirst = secondDist - firstDist; - secondToFirst = K_ScaleItemDistance(secondToFirst, 16 - pingame); // Reversed scaling, so 16P is like 1v1, and 1v1 is like 16P - } - - switch (item) - { - case KITEM_BANANA: - case KITEM_EGGMAN: - case KITEM_SUPERRING: - notNearEnd = true; - break; - - case KITEM_ROCKETSNEAKER: - case KITEM_JAWZ: - case KITEM_LANDMINE: - case KITEM_DROPTARGET: - case KITEM_BALLHOG: - case KITEM_HYUDORO: - case KRITEM_TRIPLESNEAKER: - case KRITEM_TRIPLEORBINAUT: - case KRITEM_QUADORBINAUT: - case KRITEM_DUALJAWZ: - powerItem = true; - break; - - case KRITEM_TRIPLEBANANA: - powerItem = true; - notNearEnd = true; - break; - - case KITEM_INVINCIBILITY: - case KITEM_MINE: - case KITEM_GROW: - case KITEM_BUBBLESHIELD: - case KITEM_FLAMESHIELD: - cooldownOnStart = true; - powerItem = true; - break; - - case KITEM_SPB: - cooldownOnStart = true; - notNearEnd = true; - - if (firstDist < ENDDIST*2 // No SPB when 1st is almost done - || isFirst == true) // No SPB for 1st ever - { - newodds = 0; - } - else - { - const UINT32 dist = max(0, ((signed)secondToFirst) - SPBSTARTDIST); - const UINT32 distRange = SPBFORCEDIST - SPBSTARTDIST; - const UINT8 maxOdds = 20; - fixed_t multiplier = (dist * FRACUNIT) / distRange; - - if (multiplier < 0) - { - multiplier = 0; - } - - if (multiplier > FRACUNIT) - { - multiplier = FRACUNIT; - } - - newodds = FixedMul(maxOdds * 4, multiplier); - } - break; - - case KITEM_SHRINK: - cooldownOnStart = true; - powerItem = true; - notNearEnd = true; - - if (pingame-1 <= pexiting) - newodds = 0; - break; - - case KITEM_LIGHTNINGSHIELD: - cooldownOnStart = true; - powerItem = true; - - if (spbplace != -1) - newodds = 0; - break; - - default: - break; - } - - if (newodds == 0) - { - // Nothing else we want to do with odds matters at this point :p - return newodds; - } - - - if ((cooldownOnStart == true) && (leveltime < (30*TICRATE)+starttime)) - { - // This item should not appear at the beginning of a race. (Usually really powerful crowd-breaking items) - newodds = 0; - } - else if ((notNearEnd == true) && (ourDist < ENDDIST)) - { - // This item should not appear at the end of a race. (Usually trap items that lose their effectiveness) - newodds = 0; - } - else if (powerItem == true) - { - // 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 (rival == true) - { - // The Rival bot gets frantic-like items, also :p - fracOdds *= 2; - } - - fracOdds = FixedMul(fracOdds, FRACUNIT + K_ItemOddsScale(pingame)); - - if (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 + mashed); - } - - newodds = fracOdds / FRACUNIT; - } - - return newodds; -} - -//{ SRB2kart Roulette Code - Distance Based, yes waypoints - -UINT8 K_FindUseodds(player_t *player, fixed_t mashed, UINT32 pdis, UINT8 bestbumper) -{ - UINT8 i; - UINT8 useodds = 0; - UINT8 disttable[14]; - UINT8 distlen = 0; - boolean oddsvalid[8]; - - // Unused now, oops :V - (void)bestbumper; - - for (i = 0; i < 8; i++) - { - UINT8 j; - boolean available = false; - - if (gametype == GT_BATTLE && i > 1) - { - oddsvalid[i] = false; - break; - } - - for (j = 1; j < NUMKARTRESULTS; j++) - { - if (K_KartGetItemOdds( - i, j, - player->distancetofinish, - mashed, - player->bot, (player->bot && player->botvars.rival) - ) > 0) - { - available = true; - break; - } - } - - oddsvalid[i] = available; - } - -#define SETUPDISTTABLE(odds, num) \ - if (oddsvalid[odds]) \ - for (i = num; i; --i) \ - disttable[distlen++] = odds; - - if (gametype == GT_BATTLE) // Battle Mode - { - if (player->roulettetype == 1 && 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 - { - SETUPDISTTABLE(0,1); - SETUPDISTTABLE(1,1); - SETUPDISTTABLE(2,1); - SETUPDISTTABLE(3,2); - SETUPDISTTABLE(4,2); - SETUPDISTTABLE(5,3); - SETUPDISTTABLE(6,3); - SETUPDISTTABLE(7,1); - - if (pdis == 0) - useodds = disttable[0]; - else if (pdis > DISTVAR * ((12 * distlen) / 14)) - useodds = disttable[distlen-1]; - else - { - for (i = 1; i < 13; i++) - { - if (pdis <= DISTVAR * ((i * distlen) / 14)) - { - useodds = disttable[((i * distlen) / 14)]; - break; - } - } - } - } - -#undef SETUPDISTTABLE - - return useodds; -} - -INT32 K_GetRollingRouletteItem(player_t *player) -{ - static UINT8 translation[NUMKARTITEMS-1]; - static UINT16 roulette_size; - - static INT16 odds_cached = -1; - - // Race odds have more columns than Battle - const UINT8 EMPTYODDS[sizeof K_KartItemOddsRace[0]] = {0}; - - if (odds_cached != gametype) - { - UINT8 *odds_row; - size_t odds_row_size; - - UINT8 i; - - roulette_size = 0; - - if (gametype == GT_BATTLE) - { - odds_row = K_KartItemOddsBattle[0]; - odds_row_size = sizeof K_KartItemOddsBattle[0]; - } - else - { - odds_row = K_KartItemOddsRace[0]; - odds_row_size = sizeof K_KartItemOddsRace[0]; - } - - for (i = 1; i < NUMKARTITEMS; ++i) - { - if (memcmp(odds_row, EMPTYODDS, odds_row_size)) - { - translation[roulette_size] = i; - roulette_size++; - } - - odds_row += odds_row_size; - } - - roulette_size *= 3; - odds_cached = gametype; - } - - return translation[(player->itemroulette % roulette_size) / 3]; -} - -boolean K_ForcedSPB(player_t *player) -{ - player_t *first = NULL; - player_t *second = NULL; - UINT32 secondToFirst = UINT32_MAX; - UINT8 pingame = 0; - UINT8 i; - - if (!cv_selfpropelledbomb.value) - { - return false; - } - - if (!(gametyperules & GTR_CIRCUIT)) - { - return false; - } - - if (player->position <= 1) - { - return false; - } - - if (spbplace != -1) - { - return false; - } - - if (itemCooldowns[KITEM_SPB - 1] > 0) - { - return false; - } - - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i] || players[i].spectator) - { - continue; - } - - if (players[i].exiting) - { - return false; - } - - pingame++; - - if (players[i].position == 1) - { - first = &players[i]; - } - - if (players[i].position == 2) - { - second = &players[i]; - } - } - -#if 0 - if (pingame <= 2) - { - return false; - } -#endif - - if (first != NULL && second != NULL) - { - secondToFirst = second->distancetofinish - first->distancetofinish; - secondToFirst = K_ScaleItemDistance(secondToFirst, 16 - pingame); - } - - return (secondToFirst >= SPBFORCEDIST); -} - -static void K_KartItemRoulette(player_t *player, ticcmd_t *cmd) -{ - INT32 i; - UINT8 pingame = 0; - UINT8 roulettestop; - UINT32 pdis = 0; - UINT8 useodds = 0; - INT32 spawnchance[NUMKARTRESULTS]; - INT32 totalspawnchance = 0; - UINT8 bestbumper = 0; - fixed_t mashed = 0; - - // 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].bumpers > bestbumper) - bestbumper = players[i].bumpers; - } - - // This makes the roulette produce the random noises. - if ((player->itemroulette % 3) == 1 && P_IsDisplayPlayer(player) && !demo.freecam) - { -#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) - && !(player->pflags & (PF_ITEMOUT|PF_EGGMANOUT|PF_USERINGS))) - { - // 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; - - for (i = 0; i < MAXPLAYERS; i++) - { - if (playeringame[i] && !players[i].spectator - && players[i].position == 1) - { - // This player is first! Yay! - - if (player->distancetofinish <= players[i].distancetofinish) - { - // Guess you're in first / tied for first? - pdis = 0; - } - else - { - // Subtract 1st's distance from your distance, to get your distance from 1st! - pdis = player->distancetofinish - players[i].distancetofinish; - } - break; - } - } - - pdis = K_ScaleItemDistance(pdis, pingame); - - if (player->bot && player->botvars.rival) - { - // Rival has better odds :) - pdis = (15 * pdis) / 14; - } - - // SPECIAL CASE No. 1: - // Fake Eggman items - if (player->roulettetype == 2) - { - player->eggmanexplode = 4*TICRATE; - //player->karthud[khud_itemblink] = TICRATE; - //player->karthud[khud_itemblinkmode] = 1; - player->itemroulette = 0; - player->roulettetype = 0; - if (P_IsDisplayPlayer(player) && !demo.freecam) - S_StartSound(NULL, sfx_itrole); - return; - } - - // SPECIAL CASE No. 2: - // Give a debug item instead if specified - if (cv_kartdebugitem.value != 0 && !modeattacking) - { - K_KartGetItemResult(player, cv_kartdebugitem.value); - player->itemamount = cv_kartdebugamount.value; - player->karthud[khud_itemblink] = TICRATE; - player->karthud[khud_itemblinkmode] = 2; - player->itemroulette = 0; - player->roulettetype = 0; - if (P_IsDisplayPlayer(player) && !demo.freecam) - S_StartSound(NULL, sfx_dbgsal); - return; - } - - // SPECIAL CASE No. 3: - // Record Attack / alone mashing behavior - if (modeattacking || pingame == 1) - { - if (gametype == GT_RACE) - { - if (mashed && (modeattacking || cv_superring.value)) // ANY mashed value? You get rings. - { - K_KartGetItemResult(player, KITEM_SUPERRING); - player->karthud[khud_itemblinkmode] = 1; - if (P_IsDisplayPlayer(player)) - S_StartSound(NULL, sfx_itrolm); - } - else - { - if (modeattacking || cv_sneaker.value) // Waited patiently? You get a sneaker! - K_KartGetItemResult(player, KITEM_SNEAKER); - else // Default to sad if nothing's enabled... - K_KartGetItemResult(player, KITEM_SAD); - player->karthud[khud_itemblinkmode] = 0; - if (P_IsDisplayPlayer(player)) - S_StartSound(NULL, sfx_itrolf); - } - } - else if (gametype == GT_BATTLE) - { - if (mashed && (modeattacking || bossinfo.boss || cv_banana.value)) // ANY mashed value? You get a banana. - { - K_KartGetItemResult(player, KITEM_BANANA); - player->karthud[khud_itemblinkmode] = 1; - if (P_IsDisplayPlayer(player)) - S_StartSound(NULL, sfx_itrolm); - } - else if (bossinfo.boss) - { - K_KartGetItemResult(player, KITEM_ORBINAUT); - player->karthud[khud_itemblinkmode] = 0; - if (P_IsDisplayPlayer(player)) - S_StartSound(NULL, sfx_itrolf); - } - else - { - if (modeattacking || cv_tripleorbinaut.value) // Waited patiently? You get Orbinaut x3! - K_KartGetItemResult(player, KRITEM_TRIPLEORBINAUT); - else // Default to sad if nothing's enabled... - K_KartGetItemResult(player, KITEM_SAD); - player->karthud[khud_itemblinkmode] = 0; - if (P_IsDisplayPlayer(player)) - S_StartSound(NULL, sfx_itrolf); - } - } - - player->karthud[khud_itemblink] = TICRATE; - player->itemroulette = 0; - player->roulettetype = 0; - return; - } - - // SPECIAL CASE No. 4: - // Being in ring debt occasionally forces Super Ring on you if you mashed - if (!(gametyperules & GTR_SPHERES) && mashed && player->rings < 0 && cv_superring.value) - { - INT32 debtamount = min(20, abs(player->rings)); - if (P_RandomChance(PR_ITEM_ROULETTE, (debtamount*FRACUNIT)/20)) - { - K_KartGetItemResult(player, KITEM_SUPERRING); - player->karthud[khud_itemblink] = TICRATE; - player->karthud[khud_itemblinkmode] = 1; - player->itemroulette = 0; - player->roulettetype = 0; - if (P_IsDisplayPlayer(player)) - S_StartSound(NULL, sfx_itrolm); - return; - } - } - - // SPECIAL CASE No. 5: - // Force SPB if 2nd is way too far behind - if (K_ForcedSPB(player) == true) - { - K_KartGetItemResult(player, KITEM_SPB); - player->karthud[khud_itemblink] = TICRATE; - player->karthud[khud_itemblinkmode] = 2; - player->itemroulette = 0; - player->roulettetype = 0; - if (P_IsDisplayPlayer(player)) - S_StartSound(NULL, sfx_itrolk); - return; - } - - // NOW that we're done with all of those specialized cases, we can move onto the REAL item roulette tables. - // Initializes existing spawnchance values - for (i = 0; i < NUMKARTRESULTS; i++) - spawnchance[i] = 0; - - // Split into another function for a debug function below - useodds = K_FindUseodds(player, mashed, pdis, bestbumper); - - for (i = 1; i < NUMKARTRESULTS; i++) - { - spawnchance[i] = (totalspawnchance += K_KartGetItemOdds( - useodds, i, - player->distancetofinish, - mashed, - player->bot, (player->bot && player->botvars.rival)) - ); - } - - // Award the player whatever power is rolled - if (totalspawnchance > 0) - { - totalspawnchance = P_RandomKey(PR_ITEM_ROULETTE, totalspawnchance); - for (i = 0; i < NUMKARTRESULTS && spawnchance[i] <= totalspawnchance; i++); - - K_KartGetItemResult(player, i); - } - else - { - player->itemtype = KITEM_SAD; - player->itemamount = 1; - } - - if (P_IsDisplayPlayer(player) && !demo.freecam) - S_StartSound(NULL, ((player->roulettetype == 1) ? sfx_itrolk : (mashed ? sfx_itrolm : sfx_itrolf))); - - player->karthud[khud_itemblink] = TICRATE; - player->karthud[khud_itemblinkmode] = ((player->roulettetype == 1) ? 2 : (mashed ? 1 : 0)); - - player->itemroulette = 0; // Since we're done, clear the roulette number - player->roulettetype = 0; // This too -} - //} //{ SRB2kart p_user.c Stuff @@ -7033,6 +6060,7 @@ mobj_t *K_CreatePaperItem(fixed_t x, fixed_t y, fixed_t z, angle_t angle, SINT8 if (type == 0) { + itemroulette_t rouletteData = {0}; UINT8 useodds = 0; INT32 spawnchance[NUMKARTRESULTS]; INT32 totalspawnchance = 0; @@ -7042,13 +6070,12 @@ mobj_t *K_CreatePaperItem(fixed_t x, fixed_t y, fixed_t z, angle_t angle, SINT8 useodds = amount; + K_FillItemRouletteData(NULL, &rouletteData); + for (i = 1; i < NUMKARTRESULTS; i++) { - spawnchance[i] = (totalspawnchance += K_KartGetItemOdds( - useodds, i, - UINT32_MAX, - 0, - false, false) + spawnchance[i] = ( + totalspawnchance += K_KartGetItemOdds(NULL, &rouletteData, useodds, i) ); } @@ -7921,6 +6948,20 @@ void K_KartPlayerHUDUpdate(player_t *player) player->karthud[khud_itemblink] = 0; } + if (player->karthud[khud_rouletteoffset] != 0) + { + if (abs(player->karthud[khud_rouletteoffset]) < (FRACUNIT >> 1)) + { + // Snap to 0, since the change is getting very small. + player->karthud[khud_rouletteoffset] = 0; + } + else + { + // Lerp to 0. + player->karthud[khud_rouletteoffset] = FixedMul(player->karthud[khud_rouletteoffset], FRACUNIT*3/4); + } + } + if (!(gametyperules & GTR_SPHERES)) { if (player->mo && player->mo->hitlag <= 0) @@ -10056,10 +9097,9 @@ void K_StripItems(player_t *player) player->itemamount = 0; player->pflags &= ~(PF_ITEMOUT|PF_EGGMANOUT); - if (!player->itemroulette || player->roulettetype != 2) + if (player->itemRoulette.eggman == false) { - player->itemroulette = 0; - player->roulettetype = 0; + player->itemRoulette.active = false; } player->hyudorotimer = 0; @@ -10075,8 +9115,7 @@ void K_StripItems(player_t *player) void K_StripOther(player_t *player) { - player->itemroulette = 0; - player->roulettetype = 0; + player->itemRoulette.active = false; player->invincibilitytimer = 0; if (player->growshrinktimer) @@ -10096,7 +9135,7 @@ void K_StripOther(player_t *player) static INT32 K_FlameShieldMax(player_t *player) { UINT32 disttofinish = 0; - UINT32 distv = DISTVAR; + UINT32 distv = 2048; UINT8 numplayers = 0; UINT8 i; @@ -10768,7 +9807,7 @@ void K_MoveKartPlayer(player_t *player, boolean onground) if (player->itemtype == KITEM_NONE && NO_HYUDORO && !(HOLDING_ITEM || player->itemamount - || player->itemroulette + || player->itemRoulette.active == true || player->rocketsneakertimer || player->eggmanexplode)) player->pflags |= PF_USERINGS; @@ -11470,8 +10509,7 @@ void K_MoveKartPlayer(player_t *player, boolean onground) if (spbplace == -1 || player->position != spbplace) player->pflags &= ~PF_RINGLOCK; // reset ring lock - if (player->itemtype == KITEM_SPB - || player->itemtype == KITEM_SHRINK) + if (K_ItemSingularity(player->itemtype) == true) { K_SetItemCooldown(player->itemtype, 20*TICRATE); } diff --git a/src/k_kart.h b/src/k_kart.h index 6827c77bc..8831fe0a4 100644 --- a/src/k_kart.h +++ b/src/k_kart.h @@ -50,14 +50,6 @@ void K_ReduceVFX(mobj_t *mo, player_t *owner); boolean K_IsPlayerLosing(player_t *player); fixed_t K_GetKartGameSpeedScalar(SINT8 value); -extern consvar_t *KartItemCVars[NUMKARTRESULTS-1]; - -UINT8 K_FindUseodds(player_t *player, fixed_t mashed, UINT32 pdis, UINT8 bestbumper); -fixed_t K_ItemOddsScale(UINT8 numPlayers); -UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers); -INT32 K_KartGetItemOdds(UINT8 pos, SINT8 item, UINT32 ourDist, fixed_t mashed, boolean bot, boolean rival); -INT32 K_GetRollingRouletteItem(player_t *player); -boolean K_ForcedSPB(player_t *player); INT32 K_GetShieldFromItem(INT32 item); SINT8 K_ItemResultToType(SINT8 getitem); UINT8 K_ItemResultToAmount(SINT8 getitem); diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 8bfec4716..71f8b4242 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -3455,7 +3455,7 @@ void M_DrawItemToggles(void) continue; } - cv = KartItemCVars[currentMenu->menuitems[thisitem].mvar1-1]; + cv = &cv_items[currentMenu->menuitems[thisitem].mvar1-1]; translucent = (cv->value ? 0 : V_TRANSLUCENT); drawnum = K_ItemResultToAmount(currentMenu->menuitems[thisitem].mvar1); @@ -3502,7 +3502,7 @@ void M_DrawItemToggles(void) } else { - cv = KartItemCVars[currentMenu->menuitems[itemOn].mvar1-1]; + cv = &cv_items[currentMenu->menuitems[itemOn].mvar1-1]; translucent = (cv->value ? 0 : V_TRANSLUCENT); drawnum = K_ItemResultToAmount(currentMenu->menuitems[itemOn].mvar1); diff --git a/src/k_menufunc.c b/src/k_menufunc.c index ab43217ff..384905f71 100644 --- a/src/k_menufunc.c +++ b/src/k_menufunc.c @@ -5632,12 +5632,12 @@ void M_HandleItemToggles(INT32 choice) else if (currentMenu->menuitems[itemOn].mvar1 == 0) { - INT32 v = cv_sneaker.value; + INT32 v = cv_items[0].value; S_StartSound(NULL, sfx_s1b4); for (i = 0; i < NUMKARTRESULTS-1; i++) { - if (KartItemCVars[i]->value == v) - CV_AddValue(KartItemCVars[i], 1); + if (cv_items[i].value == v) + CV_AddValue(&cv_items[i], 1); } } else @@ -5650,7 +5650,7 @@ void M_HandleItemToggles(INT32 choice) { S_StartSound(NULL, sfx_s1ba); } - CV_AddValue(KartItemCVars[currentMenu->menuitems[itemOn].mvar1-1], 1); + CV_AddValue(&cv_items[currentMenu->menuitems[itemOn].mvar1-1], 1); } } diff --git a/src/k_roulette.c b/src/k_roulette.c new file mode 100644 index 000000000..ec15f4e9b --- /dev/null +++ b/src/k_roulette.c @@ -0,0 +1,1332 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2022 by Kart Krew +// Copyright (C) 2022 by Sally "TehRealSalt" Cochenour +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file k_roulette.c +/// \brief Item roulette code. + +#include "k_roulette.h" + +#include "d_player.h" +#include "doomdef.h" +#include "hu_stuff.h" +#include "g_game.h" +#include "m_random.h" +#include "p_local.h" +#include "p_slopes.h" +#include "p_setup.h" +#include "r_draw.h" +#include "r_local.h" +#include "r_things.h" +#include "s_sound.h" +#include "st_stuff.h" +#include "v_video.h" +#include "z_zone.h" +#include "m_misc.h" +#include "m_cond.h" +#include "f_finale.h" +#include "lua_hud.h" // For Lua hud checks +#include "lua_hook.h" // For MobjDamage and ShouldDamage +#include "m_cheat.h" // objectplacing +#include "p_spec.h" + +#include "k_kart.h" +#include "k_battle.h" +#include "k_boss.h" +#include "k_pwrlv.h" +#include "k_color.h" +#include "k_respawn.h" +#include "k_waypoint.h" +#include "k_bot.h" +#include "k_hud.h" +#include "k_terrain.h" +#include "k_director.h" +#include "k_collide.h" +#include "k_follower.h" +#include "k_objects.h" +#include "k_grandprix.h" +#include "k_specialstage.h" + +// Magic number distance for use with item roulette tiers +#define DISTVAR (2048) + +// Distance when SPB can start appearing +#define SPBSTARTDIST (8*DISTVAR) + +// Distance when SPB is forced onto the next person who rolls an item +#define SPBFORCEDIST (16*DISTVAR) + +// Distance when the game stops giving you bananas +#define ENDDIST (14*DISTVAR) + +// Consistent seed used for item reels +#define ITEM_REEL_SEED (0x22D5FAA8) + +#define FRANTIC_ITEM_SCALE (FRACUNIT*6/5) + +#define ROULETTE_SPEED_SLOWEST (20) +#define ROULETTE_SPEED_FASTEST (2) +#define ROULETTE_SPEED_DIST (150*DISTVAR) +#define ROULETTE_SPEED_TIMEATTACK (9) + +static UINT8 K_KartItemOddsRace[NUMKARTRESULTS-1][8] = +{ + { 0, 0, 2, 3, 4, 0, 0, 0 }, // Sneaker + { 0, 0, 0, 0, 0, 3, 4, 5 }, // Rocket Sneaker + { 0, 0, 0, 0, 2, 5, 5, 7 }, // Invincibility + { 2, 3, 1, 0, 0, 0, 0, 0 }, // Banana + { 1, 2, 0, 0, 0, 0, 0, 0 }, // Eggman Monitor + { 5, 5, 2, 2, 0, 0, 0, 0 }, // Orbinaut + { 0, 4, 2, 1, 0, 0, 0, 0 }, // Jawz + { 0, 3, 3, 2, 0, 0, 0, 0 }, // Mine + { 3, 0, 0, 0, 0, 0, 0, 0 }, // Land Mine + { 0, 0, 2, 2, 0, 0, 0, 0 }, // Ballhog + { 0, 0, 0, 0, 0, 2, 4, 0 }, // Self-Propelled Bomb + { 0, 0, 0, 0, 2, 5, 0, 0 }, // Grow + { 0, 0, 0, 0, 0, 2, 4, 2 }, // Shrink + { 1, 0, 0, 0, 0, 0, 0, 0 }, // Lightning Shield + { 0, 1, 2, 1, 0, 0, 0, 0 }, // Bubble Shield + { 0, 0, 0, 0, 0, 1, 3, 5 }, // Flame Shield + { 3, 0, 0, 0, 0, 0, 0, 0 }, // Hyudoro + { 0, 0, 0, 0, 0, 0, 0, 0 }, // Pogo Spring + { 2, 1, 1, 0, 0, 0, 0, 0 }, // Super Ring + { 0, 0, 0, 0, 0, 0, 0, 0 }, // Kitchen Sink + { 3, 0, 0, 0, 0, 0, 0, 0 }, // Drop Target + { 0, 0, 0, 3, 5, 0, 0, 0 }, // Garden Top + { 0, 0, 2, 2, 2, 0, 0, 0 }, // Sneaker x2 + { 0, 0, 0, 0, 4, 4, 4, 0 }, // Sneaker x3 + { 0, 1, 1, 0, 0, 0, 0, 0 }, // Banana x3 + { 0, 0, 1, 0, 0, 0, 0, 0 }, // Orbinaut x3 + { 0, 0, 0, 2, 0, 0, 0, 0 }, // Orbinaut x4 + { 0, 0, 1, 2, 1, 0, 0, 0 } // Jawz x2 +}; + +static UINT8 K_KartItemOddsBattle[NUMKARTRESULTS-1][2] = +{ + //K L + { 2, 1 }, // Sneaker + { 0, 0 }, // Rocket Sneaker + { 4, 1 }, // Invincibility + { 0, 0 }, // Banana + { 1, 0 }, // Eggman Monitor + { 8, 0 }, // Orbinaut + { 8, 1 }, // Jawz + { 6, 1 }, // Mine + { 2, 0 }, // Land Mine + { 2, 1 }, // Ballhog + { 0, 0 }, // Self-Propelled Bomb + { 2, 1 }, // Grow + { 0, 0 }, // Shrink + { 4, 0 }, // Lightning Shield + { 1, 0 }, // Bubble Shield + { 1, 0 }, // Flame Shield + { 2, 0 }, // Hyudoro + { 3, 0 }, // Pogo Spring + { 0, 0 }, // Super Ring + { 0, 0 }, // Kitchen Sink + { 2, 0 }, // Drop Target + { 4, 0 }, // Garden Top + { 0, 0 }, // Sneaker x2 + { 0, 1 }, // Sneaker x3 + { 0, 0 }, // Banana x3 + { 2, 0 }, // Orbinaut x3 + { 1, 1 }, // Orbinaut x4 + { 5, 1 } // Jawz x2 +}; + +static kartitems_t K_KartItemReelTimeAttack[] = +{ + KITEM_SNEAKER, + KITEM_SUPERRING, + KITEM_NONE +}; + +static kartitems_t K_KartItemReelBreakTheCapsules[] = +{ + KRITEM_TRIPLEORBINAUT, + KITEM_BANANA, + KITEM_NONE +}; + +static kartitems_t K_KartItemReelBoss[] = +{ + KITEM_ORBINAUT, + KITEM_BANANA, + KITEM_NONE +}; + +/*-------------------------------------------------- + boolean K_ItemEnabled(kartitems_t item) + + See header file for description. +--------------------------------------------------*/ +boolean K_ItemEnabled(kartitems_t item) +{ + if (item < 1 || item >= NUMKARTRESULTS) + { + // Not a real item. + return false; + } + + if (K_CanChangeRules(true) == false) + { + // Force all items to be enabled. + return true; + } + + // Allow the user preference. + return cv_items[item - 1].value; +} + +/*-------------------------------------------------- + boolean K_ItemSingularity(kartitems_t item) + + See header file for description. +--------------------------------------------------*/ +boolean K_ItemSingularity(kartitems_t item) +{ + switch (item) + { + case KITEM_SPB: + case KITEM_SHRINK: + { + return true; + } + default: + { + return false; + } + } +} + +/*-------------------------------------------------- + static fixed_t K_ItemOddsScale(UINT8 playerCount) + + A multiplier for odds and distances to scale + them with the player count. + + Input Arguments:- + playerCount - Number of players in the game. + + Return:- + Fixed point number, to multiply odds or + distances by. +--------------------------------------------------*/ +static fixed_t K_ItemOddsScale(UINT8 playerCount) +{ + const UINT8 basePlayer = 8; // The player count we design most of the game around. + fixed_t playerScaling = 0; + + if (playerCount < 2) + { + // Cap to 1v1 scaling + playerCount = 2; + } + + // Then, it multiplies it further if the player count isn't equal to basePlayer. + // This is done to make low player count races more interesting and high player count rates more fair. + if (playerCount < basePlayer) + { + // Less than basePlayer: increase odds significantly. + // 2P: x2.5 + playerScaling = (basePlayer - playerCount) * (FRACUNIT / 4); + } + else if (playerCount > basePlayer) + { + // More than basePlayer: reduce odds slightly. + // 16P: x0.75 + playerScaling = (basePlayer - playerCount) * (FRACUNIT / 32); + } + + return playerScaling; +} + +/*-------------------------------------------------- + static UINT32 K_UndoMapScaling(UINT32 distance) + + Takes a raw map distance and adjusts it to + be in x1 scale. + + Input Arguments:- + distance - Original distance. + + Return:- + Distance unscaled by mapobjectscale. +--------------------------------------------------*/ +static UINT32 K_UndoMapScaling(UINT32 distance) +{ + if (mapobjectscale != FRACUNIT) + { + // Bring back to normal scale. + return FixedDiv(distance, mapobjectscale); + } + + return distance; +} + +/*-------------------------------------------------- + static UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers) + + Adjust item distance for lobby-size scaling + as well as Frantic Items. + + Input Arguments:- + distance - Original distance. + numPlayers - Number of players in the game. + + Return:- + New distance after scaling. +--------------------------------------------------*/ +static UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers) +{ + if (franticitems == true) + { + // Frantic items pretends everyone's farther apart, for crazier items. + distance = FixedMul(distance, FRANTIC_ITEM_SCALE); + } + + // Items get crazier with the fewer players that you have. + distance = FixedMul( + distance, + FRACUNIT + (K_ItemOddsScale(numPlayers) / 2) + ); + + return distance; +} + +/*-------------------------------------------------- + static UINT32 K_GetItemRouletteDistance(const player_t *player, UINT8 numPlayers) + + Gets a player's distance used for the item + roulette, including all scaling factors. + + Input Arguments:- + player - The player to get the distance of. + numPlayers - Number of players in the game. + + Return:- + The player's finalized item distance. +--------------------------------------------------*/ +static UINT32 K_GetItemRouletteDistance(const player_t *player, UINT8 numPlayers) +{ + UINT32 pdis = 0; + + if (player == NULL) + { + return 0; + } + +#if 0 + if (specialStage.active == true) + { + UINT32 ufoDis = K_GetSpecialUFODistance(); + + if (player->distancetofinish <= ufoDis) + { + // You're ahead of the UFO. + pdis = 0; + } + else + { + // Subtract the UFO's distance from your distance! + pdis = player->distancetofinish - ufoDis; + } + } + else +#endif + { + UINT8 i; + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] && !players[i].spectator + && players[i].position == 1) + { + // This player is first! Yay! + + if (player->distancetofinish <= players[i].distancetofinish) + { + // Guess you're in first / tied for first? + pdis = 0; + } + else + { + // Subtract 1st's distance from your distance, to get your distance from 1st! + pdis = player->distancetofinish - players[i].distancetofinish; + } + break; + } + } + } + + pdis = K_UndoMapScaling(pdis); + pdis = K_ScaleItemDistance(pdis, numPlayers); + + if (player->bot && player->botvars.rival) + { + // Rival has better odds :) + pdis = FixedMul(pdis, FRANTIC_ITEM_SCALE); + } + + return pdis; +} + +/*-------------------------------------------------- + INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette, UINT8 pos, kartitems_t item) + + See header file for description. +--------------------------------------------------*/ +INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette, UINT8 pos, kartitems_t item) +{ + boolean bot = false; + boolean rival = false; + UINT8 position = 0; + + INT32 shieldType = KSHIELD_NONE; + + boolean powerItem = false; + boolean cooldownOnStart = false; + boolean notNearEnd = false; + + fixed_t newOdds = 0; + size_t i; + + I_Assert(roulette != NULL); + + I_Assert(item > KITEM_NONE); // too many off by one scenarioes. + I_Assert(item < NUMKARTRESULTS); + + if (player != NULL) + { + bot = player->bot; + rival = (bot == true && player->botvars.rival == true); + position = player->position; + } + + if (K_ItemEnabled(item) == false) + { + return 0; + } + + if (K_GetItemCooldown(item) > 0) + { + // Cooldown is still running, don't give another. + return 0; + } + + /* + if (bot) + { + // TODO: Item use on bots should all be passed-in functions. + // Instead of manually inserting these, it should return 0 + // for any items without an item use function supplied + + switch (item) + { + case KITEM_SNEAKER: + break; + default: + return 0; + } + } + */ + (void)bot; + + shieldType = K_GetShieldFromItem(item); + switch (shieldType) + { + case KSHIELD_NONE: + /* Marble Garden Top is not REALLY + a Sonic 3 shield */ + case KSHIELD_TOP: + { + break; + } + + default: + { + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] == false || players[i].spectator == true) + { + continue; + } + + if (shieldType == K_GetShieldFromItem(players[i].itemtype)) + { + // Don't allow more than one of each shield type at a time + return 0; + } + } + } + } + + if (gametype == GT_BATTLE) + { + I_Assert(pos < 2); // DO NOT allow positions past the bounds of the table + newOdds = K_KartItemOddsBattle[item-1][pos]; + } + else + { + I_Assert(pos < 8); // Ditto + newOdds = K_KartItemOddsRace[item-1][pos]; + } + + newOdds <<= FRACBITS; + + switch (item) + { + case KITEM_BANANA: + case KITEM_EGGMAN: + case KITEM_SUPERRING: + { + notNearEnd = true; + break; + } + + case KITEM_ROCKETSNEAKER: + case KITEM_JAWZ: + case KITEM_LANDMINE: + case KITEM_DROPTARGET: + case KITEM_BALLHOG: + case KRITEM_TRIPLESNEAKER: + case KRITEM_TRIPLEORBINAUT: + case KRITEM_QUADORBINAUT: + case KRITEM_DUALJAWZ: + { + powerItem = true; + break; + } + + case KITEM_HYUDORO: + case KRITEM_TRIPLEBANANA: + { + powerItem = true; + notNearEnd = true; + break; + } + + case KITEM_INVINCIBILITY: + case KITEM_MINE: + case KITEM_GROW: + case KITEM_BUBBLESHIELD: + case KITEM_FLAMESHIELD: + { + cooldownOnStart = true; + powerItem = true; + break; + } + + case KITEM_SPB: + { + cooldownOnStart = true; + notNearEnd = true; + + if ((gametyperules & GTR_CIRCUIT) == 0) + { + // Needs to be a race. + return 0; + } + + if (roulette->firstDist < ENDDIST*2 // No SPB when 1st is almost done + || position == 1) // No SPB for 1st ever + { + return 0; + } + else + { + const UINT32 dist = max(0, ((signed)roulette->secondToFirst) - SPBSTARTDIST); + const UINT32 distRange = SPBFORCEDIST - SPBSTARTDIST; + const fixed_t maxOdds = 20 << FRACBITS; + fixed_t multiplier = FixedDiv(dist, distRange); + + if (multiplier < 0) + { + multiplier = 0; + } + + if (multiplier > FRACUNIT) + { + multiplier = FRACUNIT; + } + + newOdds = FixedMul(maxOdds, multiplier); + } + break; + } + + case KITEM_SHRINK: + { + cooldownOnStart = true; + powerItem = true; + notNearEnd = true; + + if (roulette->playing - 1 <= roulette->exiting) + { + return 0; + } + break; + } + + case KITEM_LIGHTNINGSHIELD: + { + cooldownOnStart = true; + powerItem = true; + + if (spbplace != -1) + { + return 0; + } + break; + } + + default: + { + break; + } + } + + if (newOdds == 0) + { + // Nothing else we want to do with odds matters at this point :p + return newOdds; + } + + if ((cooldownOnStart == true) && (leveltime < (30*TICRATE)+starttime)) + { + // This item should not appear at the beginning of a race. (Usually really powerful crowd-breaking items) + newOdds = 0; + } + else if ((notNearEnd == true) && (roulette->baseDist < ENDDIST)) + { + // This item should not appear at the end of a race. (Usually trap items that lose their effectiveness) + newOdds = 0; + } + else if (powerItem == true) + { + // This item is a "power item". This activates "frantic item" toggle related functionality. + if (franticitems == true) + { + // First, power items multiply their odds by 2 if frantic items are on; easy-peasy. + newOdds *= 2; + } + + if (rival == true) + { + // The Rival bot gets frantic-like items, also :p + newOdds *= 2; + } + + newOdds = FixedMul(newOdds, FRACUNIT + K_ItemOddsScale(roulette->playing)); + } + + newOdds = FixedInt(FixedRound(newOdds)); + return newOdds; +} + +/*-------------------------------------------------- + static UINT8 K_FindUseodds(const player_t *player, itemroulette_t *const roulette) + + Gets which item bracket the player is in. + This can be adjusted depending on which + items being turned off. + + Input Arguments:- + player - The player the roulette is for. + roulette - The item roulette data. + + Return:- + The item bracket the player is in, as an + index to the array. +--------------------------------------------------*/ +static UINT8 K_FindUseodds(const player_t *player, itemroulette_t *const roulette) +{ + UINT8 i; + UINT8 useOdds = 0; + UINT8 distTable[14]; + UINT8 distLen = 0; + UINT8 totalSize = 0; + boolean oddsValid[8]; + + for (i = 0; i < 8; i++) + { + UINT8 j; + + if (gametype == GT_BATTLE && i > 1) + { + oddsValid[i] = false; + continue; + } + + for (j = 1; j < NUMKARTRESULTS; j++) + { + if (K_KartGetItemOdds(player, roulette, i, j) > 0) + { + break; + } + } + + oddsValid[i] = (j < NUMKARTRESULTS); + } + +#define SETUPDISTTABLE(odds, num) \ + totalSize += num; \ + if (oddsValid[odds]) \ + for (i = num; i; --i) \ + distTable[distLen++] = odds; + + if (gametype == GT_BATTLE) // Battle Mode + { + useOdds = 0; + } + else + { + SETUPDISTTABLE(0,1); + SETUPDISTTABLE(1,1); + SETUPDISTTABLE(2,1); + SETUPDISTTABLE(3,2); + SETUPDISTTABLE(4,2); + SETUPDISTTABLE(5,3); + SETUPDISTTABLE(6,3); + SETUPDISTTABLE(7,1); + + for (i = 0; i < totalSize; i++) + { + fixed_t pos = 0; + fixed_t dist = 0; + UINT8 index = 0; + + if (i == totalSize-1) + { + useOdds = distTable[distLen - 1]; + break; + } + + pos = ((i << FRACBITS) * distLen) / totalSize; + dist = FixedMul(DISTVAR << FRACBITS, pos) >> FRACBITS; + index = FixedInt(FixedRound(pos)); + + if (roulette->dist <= (unsigned)dist) + { + useOdds = distTable[index]; + break; + } + } + } + +#undef SETUPDISTTABLE + + return useOdds; +} + +/*-------------------------------------------------- + static boolean K_ForcedSPB(const player_t *player, itemroulette_t *const roulette) + + Determines special conditions where we want + to forcefully give the player an SPB. + + Input Arguments:- + player - The player the roulette is for. + roulette - The item roulette data. + + Return:- + true if we want to give the player a forced SPB, + otherwise false. +--------------------------------------------------*/ +static boolean K_ForcedSPB(const player_t *player, itemroulette_t *const roulette) +{ + if (K_ItemEnabled(KITEM_SPB) == false) + { + return false; + } + + if (!(gametyperules & GTR_CIRCUIT)) + { + return false; + } + + if (player == NULL) + { + return false; + } + + if (player->position <= 1) + { + return false; + } + + if (spbplace != -1) + { + return false; + } + + if (itemCooldowns[KITEM_SPB - 1] > 0) + { + return false; + } + +#if 0 + if (roulette->playing <= 2) + { + return false; + } +#endif + + return (roulette->secondToFirst >= SPBFORCEDIST); +} + +/*-------------------------------------------------- + static void K_InitRoulette(itemroulette_t *const roulette) + + Initializes the data for a new item roulette. + + Input Arguments:- + roulette - The item roulette data to initialize. + + Return:- + N/A +--------------------------------------------------*/ +static void K_InitRoulette(itemroulette_t *const roulette) +{ + size_t i; + +#ifndef ITEM_LIST_SIZE + if (roulette->itemList == NULL) + { + roulette->itemListCap = 8; + roulette->itemList = Z_Calloc( + sizeof(SINT8) * roulette->itemListCap, + PU_STATIC, + &roulette->itemList + ); + + if (roulette->itemList == NULL) + { + I_Error("Not enough memory for item roulette list\n"); + } + } +#endif + + roulette->itemListLen = 0; + roulette->index = 0; + + roulette->useOdds = UINT8_MAX; + roulette->baseDist = roulette->dist = 0; + roulette->playing = roulette->exiting = 0; + roulette->firstDist = roulette->secondDist = UINT32_MAX; + roulette->secondToFirst = 0; + + roulette->elapsed = 0; + roulette->tics = roulette->speed = ROULETTE_SPEED_FASTEST; // Some default speed + + roulette->active = true; + roulette->eggman = false; + + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] == false || players[i].spectator == true) + { + continue; + } + + roulette->playing++; + + if (players[i].exiting) + { + roulette->exiting++; + } + + if (players[i].position == 1) + { + roulette->firstDist = K_UndoMapScaling(players[i].distancetofinish); + } + + if (players[i].position == 2) + { + roulette->secondDist = K_UndoMapScaling(players[i].distancetofinish); + } + } + + // Calculate 2nd's distance from 1st, for SPB + if (roulette->firstDist != UINT32_MAX && roulette->secondDist != UINT32_MAX) + { + roulette->secondToFirst = roulette->secondDist - roulette->firstDist; + roulette->secondToFirst = K_ScaleItemDistance(roulette->secondToFirst, 16 - roulette->playing); // Reversed scaling + } +} + +/*-------------------------------------------------- + static void K_PushToRouletteItemList(itemroulette_t *const roulette, kartitems_t item) + + Pushes a new item to the end of the item + roulette's item list. + + Input Arguments:- + roulette - The item roulette data to modify. + item - The item to push to the list. + + Return:- + N/A +--------------------------------------------------*/ +static void K_PushToRouletteItemList(itemroulette_t *const roulette, kartitems_t item) +{ +#ifdef ITEM_LIST_SIZE + if (roulette->itemListLen >= ITEM_LIST_SIZE) + { + I_Error("Out of space for item reel! Go and make ITEM_LIST_SIZE bigger I guess?\n"); + return; + } +#else + I_Assert(roulette->itemList != NULL); + + if (roulette->itemListLen >= roulette->itemListCap) + { + roulette->itemListCap *= 2; + roulette->itemList = Z_Realloc( + roulette->itemList, + sizeof(SINT8) * roulette->itemListCap, + PU_STATIC, + &roulette->itemList + ); + + if (roulette->itemList == NULL) + { + I_Error("Not enough memory for item roulette list\n"); + } + } +#endif + + roulette->itemList[ roulette->itemListLen ] = item; + roulette->itemListLen++; +} + +/*-------------------------------------------------- + static void K_AddItemToReel(const player_t *player, itemroulette_t *const roulette, kartitems_t item) + + Adds an item to a player's item reel. Unlike + pushing directly with K_PushToRouletteItemList, + this function handles special behaviors (like + padding with extra Super Rings). + + Input Arguments:- + player - The player to add to the item roulette. + This is valid to be NULL. + roulette - The player's item roulette data. + item - The item to push to the list. + + Return:- + N/A +--------------------------------------------------*/ +static void K_AddItemToReel(const player_t *player, itemroulette_t *const roulette, kartitems_t item) +{ + K_PushToRouletteItemList(roulette, item); + + if (player == NULL) + { + return; + } + + // If we're in ring debt, pad out the reel with + // a BUNCH of Super Rings. + if (K_ItemEnabled(KITEM_SUPERRING) == true + && player->rings <= 0 + && (gametyperules & GTR_SPHERES) == 0) + { + K_PushToRouletteItemList(roulette, KITEM_SUPERRING); + } +} + +/*-------------------------------------------------- + static void K_CalculateRouletteSpeed(itemroulette_t *const roulette) + + Determines the speed for the item roulette, + adjusted for progress in the race and front + running. + + Input Arguments:- + roulette - The item roulette data to modify. + + Return:- + N/A +--------------------------------------------------*/ +static void K_CalculateRouletteSpeed(itemroulette_t *const roulette) +{ + fixed_t frontRun = 0; + fixed_t progress = 0; + fixed_t total = 0; + + if (modeattacking || roulette->playing <= 1) + { + // Time Attack rules; use a consistent speed. + roulette->tics = roulette->speed = ROULETTE_SPEED_TIMEATTACK; + return; + } + + if (roulette->baseDist > ENDDIST) + { + // Being farther in the course makes your roulette faster. + progress = min(FRACUNIT, FixedDiv(roulette->baseDist - ENDDIST, ROULETTE_SPEED_DIST)); + } + + if (roulette->baseDist > roulette->firstDist) + { + // Frontrunning makes your roulette faster. + frontRun = min(FRACUNIT, FixedDiv(roulette->baseDist - roulette->firstDist, ENDDIST)); + } + + // Combine our two factors together. + total = min(FRACUNIT, (frontRun / 2) + (progress / 2)); + + if (leveltime < starttime + 20*TICRATE) + { + // Don't impact as much at the start. + // This makes it so that everyone gets to enjoy the lowest speed at the start. + if (leveltime < starttime) + { + total = FRACUNIT; + } + else + { + const fixed_t lerp = FixedDiv(leveltime - starttime, 20*TICRATE); + total = FRACUNIT + FixedMul(lerp, total - FRACUNIT); + } + } + + roulette->tics = roulette->speed = ROULETTE_SPEED_FASTEST + FixedMul(ROULETTE_SPEED_SLOWEST - ROULETTE_SPEED_FASTEST, total); +} + +/*-------------------------------------------------- + void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette) + + See header file for description. +--------------------------------------------------*/ +void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette) +{ + UINT32 spawnChance[NUMKARTRESULTS] = {0}; + UINT32 totalSpawnChance = 0; + size_t rngRoll = 0; + + UINT8 numItems = 0; + kartitems_t singleItem = KITEM_SAD; + + size_t i; + + K_InitRoulette(roulette); + + if (player != NULL) + { + roulette->baseDist = K_UndoMapScaling(player->distancetofinish); + K_CalculateRouletteSpeed(roulette); + } + + // SPECIAL CASE No. 1: + // Give only the debug item if specified + if (cv_kartdebugitem.value != KITEM_NONE) + { + K_PushToRouletteItemList(roulette, cv_kartdebugitem.value); + return; + } + + // SPECIAL CASE No. 2: + // Use a special, pre-determined item reel for Time Attack / Free Play + if (bossinfo.boss == true) + { + for (i = 0; K_KartItemReelBoss[i] != KITEM_NONE; i++) + { + K_PushToRouletteItemList(roulette, K_KartItemReelBoss[i]); + } + + return; + } + else if (modeattacking || roulette->playing <= 1) + { + switch (gametype) + { + case GT_RACE: + default: + { + for (i = 0; K_KartItemReelTimeAttack[i] != KITEM_NONE; i++) + { + K_PushToRouletteItemList(roulette, K_KartItemReelTimeAttack[i]); + } + break; + } + case GT_BATTLE: + { + for (i = 0; K_KartItemReelBreakTheCapsules[i] != KITEM_NONE; i++) + { + K_PushToRouletteItemList(roulette, K_KartItemReelBreakTheCapsules[i]); + } + break; + } + } + + return; + } + + // SPECIAL CASE No. 3: + // Only give the SPB if conditions are right + if (K_ForcedSPB(player, roulette) == true) + { + K_AddItemToReel(player, roulette, KITEM_SPB); + return; + } + + // SPECIAL CASE No. 4: + // If only one item is enabled, always use it + for (i = 1; i < NUMKARTRESULTS; i++) + { + if (K_ItemEnabled(i) == true) + { + numItems++; + if (numItems > 1) + { + break; + } + + singleItem = i; + } + } + + if (numItems < 2) + { + // singleItem = KITEM_SAD by default, + // so it will be used when all items are turned off. + K_AddItemToReel(player, roulette, singleItem); + return; + } + + // Special cases are all handled, we can now + // actually calculate actual item reels. + roulette->dist = K_GetItemRouletteDistance(player, roulette->playing); + roulette->useOdds = K_FindUseodds(player, roulette); + + for (i = 1; i < NUMKARTRESULTS; i++) + { + spawnChance[i] = ( + totalSpawnChance += K_KartGetItemOdds(player, roulette, roulette->useOdds, i) + ); + } + + if (totalSpawnChance == 0) + { + // This shouldn't happen, but if it does, early exit. + // Maybe can happen if you enable multiple items for + // another gametype, so we give the singleItem as a fallback. + K_AddItemToReel(player, roulette, singleItem); + return; + } + + // Create the same item reel given the same inputs. + P_SetRandSeed(PR_ITEM_ROULETTE, ITEM_REEL_SEED); + + while (totalSpawnChance > 0) + { + rngRoll = P_RandomKey(PR_ITEM_ROULETTE, totalSpawnChance); + for (i = 1; i < NUMKARTRESULTS && spawnChance[i] <= rngRoll; i++) + { + continue; + } + + K_AddItemToReel(player, roulette, i); + + for (; i < NUMKARTRESULTS; i++) + { + // Be sure to fix the remaining items' odds too. + if (spawnChance[i] > 0) + { + spawnChance[i]--; + } + } + + totalSpawnChance--; + } +} + +/*-------------------------------------------------- + void K_StartItemRoulette(player_t *const player) + + See header file for description. +--------------------------------------------------*/ +void K_StartItemRoulette(player_t *const player) +{ + itemroulette_t *const roulette = &player->itemRoulette; + size_t i; + + K_FillItemRouletteData(player, roulette); + + // Make the bots select their item after a little while. + // One of the few instances of bot RNG, would be nice to remove it. + player->botvars.itemdelay = P_RandomRange(PR_UNDEFINED, TICRATE, TICRATE*3); + + // Prevent further duplicates of items that + // are intended to only have one out at a time. + for (i = 0; i < roulette->itemListLen; i++) + { + kartitems_t item = roulette->itemList[i]; + if (K_ItemSingularity(item) == true) + { + K_SetItemCooldown(item, TICRATE<<4); + } + } +} + +/*-------------------------------------------------- + void K_StartEggmanRoulette(player_t *const player) + + See header file for description. +--------------------------------------------------*/ +void K_StartEggmanRoulette(player_t *const player) +{ + itemroulette_t *const roulette = &player->itemRoulette; + K_StartItemRoulette(player); + roulette->eggman = true; +} + +/*-------------------------------------------------- + fixed_t K_GetRouletteOffset(itemroulette_t *const roulette, fixed_t renderDelta) + + See header file for description. +--------------------------------------------------*/ +fixed_t K_GetRouletteOffset(itemroulette_t *const roulette, fixed_t renderDelta) +{ + const fixed_t curTic = (roulette->tics << FRACBITS) - renderDelta; + const fixed_t midTic = roulette->speed * (FRACUNIT >> 1); + + return FixedMul(FixedDiv(midTic - curTic, ((roulette->speed + 1) << FRACBITS)), ROULETTE_SPACING); +} + +/*-------------------------------------------------- + static void K_KartGetItemResult(player_t *const player, kartitems_t getitem) + + Initializes a player's item to what was + received from the roulette. + + Input Arguments:- + player - The player receiving the item. + getitem - The item to give to the player. + + Return:- + N/A +--------------------------------------------------*/ +static void K_KartGetItemResult(player_t *const player, kartitems_t getitem) +{ + if (K_ItemSingularity(getitem) == true) + { + K_SetItemCooldown(getitem, 20*TICRATE); + } + + player->botvars.itemdelay = TICRATE; + player->botvars.itemconfirm = 0; + + player->itemtype = K_ItemResultToType(getitem); + player->itemamount = K_ItemResultToAmount(getitem); +} + +/*-------------------------------------------------- + void K_KartItemRoulette(player_t *const player, ticcmd_t *const cmd) + + See header file for description. +--------------------------------------------------*/ +void K_KartItemRoulette(player_t *const player, ticcmd_t *const cmd) +{ + itemroulette_t *const roulette = &player->itemRoulette; + boolean confirmItem = false; + + // This makes the roulette cycle through items. + // If this isn't active, you shouldn't be here. + if (roulette->active == false) + { + return; + } + + if (roulette->itemListLen == 0 +#ifndef ITEM_LIST_SIZE + || roulette->itemList == NULL +#endif + ) + { + // Invalid roulette setup. + // Escape before we run into issues. + roulette->active = false; + return; + } + + if (roulette->elapsed > TICRATE>>1) // Prevent accidental immediate item confirm + { + if (roulette->elapsed > TICRATE<<4) + { + // Waited way too long, forcefully confirm the item. + confirmItem = true; + } + else + { + // We can stop our item when we choose. + confirmItem = !!(cmd->buttons & BT_ATTACK); + } + } + + // If the roulette finishes or the player presses BT_ATTACK, stop the roulette and calculate the item. + // I'm returning via the exact opposite, however, to forgo having another bracket embed. Same result either way, I think. + // Finally, if you get past this check, now you can actually start calculating what item you get. + if (confirmItem == true && (player->pflags & (PF_ITEMOUT|PF_EGGMANOUT|PF_USERINGS)) == 0) + { + if (roulette->eggman == true) + { + // FATASS JUMPSCARE instead of your actual item + player->eggmanexplode = 4*TICRATE; + + //player->karthud[khud_itemblink] = TICRATE; + //player->karthud[khud_itemblinkmode] = 1; + //player->karthud[khud_rouletteoffset] = K_GetRouletteOffset(roulette, FRACUNIT); + + if (P_IsDisplayPlayer(player) && !demo.freecam) + { + S_StartSound(NULL, sfx_itrole); + } + } + else + { + kartitems_t finalItem = roulette->itemList[ roulette->index ]; + + K_KartGetItemResult(player, finalItem); + + player->karthud[khud_itemblink] = TICRATE; + player->karthud[khud_itemblinkmode] = 0; + player->karthud[khud_rouletteoffset] = K_GetRouletteOffset(roulette, FRACUNIT); + + if (P_IsDisplayPlayer(player) && !demo.freecam) + { + S_StartSound(NULL, sfx_itrolf); + } + } + + // We're done, disable the roulette + roulette->active = false; + return; + } + + roulette->elapsed++; + + if (roulette->tics == 0) + { + roulette->index = (roulette->index + 1) % roulette->itemListLen; + roulette->tics = roulette->speed; + + // This makes the roulette produce the random noises. + roulette->sound = (roulette->sound + 1) % 8; + + if (P_IsDisplayPlayer(player) && !demo.freecam) + { + S_StartSound(NULL, sfx_itrol1 + roulette->sound); + } + } + else + { + roulette->tics--; + } +} diff --git a/src/k_roulette.h b/src/k_roulette.h new file mode 100644 index 000000000..0cef89f7a --- /dev/null +++ b/src/k_roulette.h @@ -0,0 +1,169 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2022 by Kart Krew +// Copyright (C) 2022 by Sally "TehRealSalt" Cochenour +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file k_roulette.h +/// \brief Item roulette code. + +#ifndef __K_ROULETTE_H__ +#define __K_ROULETTE_H__ + +#include "doomtype.h" +#include "d_player.h" + +#define ROULETTE_SPACING (36 << FRACBITS) +#define ROULETTE_SPACING_SPLITSCREEN (16 << FRACBITS) + +/*-------------------------------------------------- + boolean K_ItemEnabled(kartitems_t item); + + Determines whenever or not an item should + be enabled. Accounts for situations where + rules should not be able to be changed. + + Input Arguments:- + item - The item to check. + + Return:- + true if the item is enabled, otherwise false. +--------------------------------------------------*/ + +boolean K_ItemEnabled(kartitems_t item); + + +/*-------------------------------------------------- + boolean K_ItemSingularity(kartitems_t item); + + Determines whenever or not this item should + be using special cases to prevent more than + one existing at a time. + + Input Arguments:- + item - The item to check. + + Return:- + true to use the special rules, otherwise false. +--------------------------------------------------*/ + +boolean K_ItemSingularity(kartitems_t item); + + +/*-------------------------------------------------- + INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette, UINT8 pos, kartitems_t item); + + Gets the frequency an item should show up in + an item bracket, and adjusted for special + factors (such as Frantic Items). + + Input Arguments:- + player - The player we intend to give the item to later. + Can be NULL for generic use. + roulette - The roulette data that we intend to + insert this item into. + pos - The item bracket we are in. + item - The item to give. + + Return:- + The number of items we want to insert + into the roulette. +--------------------------------------------------*/ + +INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette, UINT8 pos, kartitems_t item); + + +/*-------------------------------------------------- + void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette); + + Fills out the item roulette struct when it is + initially created. This function needs to be + HUD-safe for the item debugger, so the player + cannot be modified at this stage. + + Input Arguments:- + player - The player this roulette data is for. + Can be NULL for generic use. + roulette - The roulette data struct to fill out. + + Return:- + N/A +--------------------------------------------------*/ + +void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette); + + +/*-------------------------------------------------- + void K_StartItemRoulette(player_t *const player); + + Starts the item roulette sequence for a player. + This stage can only be used by gameplay, thus + this handles gameplay modifications as well. + + Input Arguments:- + player - The player to start the item roulette for. + + Return:- + N/A +--------------------------------------------------*/ + +void K_StartItemRoulette(player_t *const player); + + +/*-------------------------------------------------- + void K_StartEggmanRoulette(player_t *const player); + + Starts the Eggman Mark roulette sequence for + a player. Looks identical to a regular item + roulette, but gives you the Eggman explosion + countdown instead when confirming it. + + Input Arguments:- + player - The player to start the Eggman roulette for. + + Return:- + N/A +--------------------------------------------------*/ + +void K_StartEggmanRoulette(player_t *const player); + + +/*-------------------------------------------------- + fixed_t K_GetRouletteOffset(itemroulette_t *const roulette, fixed_t renderDelta); + + Gets the Y offset, for use in the roulette HUD. + A separate function since it is used both by the + HUD itself, as well as when confirming an item. + + Input Arguments:- + roulette - The roulette we are drawing for. + renderDelta - Fractional tic delta, when used for HUD. + + Return:- + The Y offset when drawing the item. +--------------------------------------------------*/ + +fixed_t K_GetRouletteOffset(itemroulette_t *const roulette, fixed_t renderDelta); + + +/*-------------------------------------------------- + void K_KartItemRoulette(player_t *const player, ticcmd_t *cmd); + + Handles ticking a player's item roulette, + and player input for stopping it. + + Input Arguments:- + player - The player to run the item roulette for. + cmd - The player's controls. + + Return:- + N/A +--------------------------------------------------*/ + +void K_KartItemRoulette(player_t *const player, ticcmd_t *cmd); + + +#endif // __K_ROULETTE_H__ diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c index 2a3b9e46a..b69449ba2 100644 --- a/src/lua_playerlib.c +++ b/src/lua_playerlib.c @@ -304,10 +304,10 @@ static int player_get(lua_State *L) lua_pushinteger(L, plr->tripwirePass); else if (fastcmp(field,"tripwireLeniency")) lua_pushinteger(L, plr->tripwireLeniency); + /* else if (fastcmp(field,"itemroulette")) lua_pushinteger(L, plr->itemroulette); - else if (fastcmp(field,"roulettetype")) - lua_pushinteger(L, plr->roulettetype); + */ else if (fastcmp(field,"itemtype")) lua_pushinteger(L, plr->itemtype); else if (fastcmp(field,"itemamount")) @@ -680,10 +680,10 @@ static int player_set(lua_State *L) plr->tripwirePass = luaL_checkinteger(L, 3); else if (fastcmp(field,"tripwireLeniency")) plr->tripwireLeniency = luaL_checkinteger(L, 3); + /* else if (fastcmp(field,"itemroulette")) plr->itemroulette = luaL_checkinteger(L, 3); - else if (fastcmp(field,"roulettetype")) - plr->roulettetype = luaL_checkinteger(L, 3); + */ else if (fastcmp(field,"itemtype")) plr->itemtype = luaL_checkinteger(L, 3); else if (fastcmp(field,"itemamount")) diff --git a/src/p_enemy.c b/src/p_enemy.c index 61762cce8..2e1ed8557 100644 --- a/src/p_enemy.c +++ b/src/p_enemy.c @@ -34,6 +34,7 @@ #include "k_respawn.h" #include "k_collide.h" #include "k_objects.h" +#include "k_roulette.h" #ifdef HW3SOUND #include "hardware/hw3sound.h" @@ -13043,9 +13044,13 @@ void A_ItemPop(mobj_t *actor) Obj_SpawnItemDebrisEffects(actor, actor->target); if (locvar1 == 1) + { P_GivePlayerSpheres(actor->target->player, actor->extravalue1); + } else if (locvar1 == 0) - actor->target->player->itemroulette = 1; + { + K_StartItemRoulette(actor->target->player); + } // Here at mapload in battle? if ((gametyperules & GTR_BUMPERS) && (actor->flags2 & MF2_BOSSNOTRAP)) diff --git a/src/p_inter.c b/src/p_inter.c index 76757f193..6400b9d35 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -38,6 +38,7 @@ #include "k_respawn.h" #include "p_spec.h" #include "k_objects.h" +#include "k_roulette.h" // CTF player names #define CTFTEAMCODE(pl) pl->ctfteam ? (pl->ctfteam == 1 ? "\x85" : "\x84") : "" @@ -130,7 +131,7 @@ boolean P_CanPickupItem(player_t *player, UINT8 weapon) return false; // Already have fake - if (player->roulettetype == 2 + if (player->itemRoulette.eggman == true || player->eggmanexplode) return false; } @@ -143,7 +144,7 @@ boolean P_CanPickupItem(player_t *player, UINT8 weapon) return false; // Item slot already taken up - if (player->itemroulette + if (player->itemRoulette.active == true || (weapon != 3 && player->itemamount) || (player->pflags & PF_ITEMOUT)) return false; @@ -411,8 +412,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) if (special->fuse || !P_CanPickupItem(player, 1) || ((gametyperules & GTR_BUMPERS) && player->bumpers <= 0)) return; - player->itemroulette = 1; - player->roulettetype = 1; + K_StartItemRoulette(player); // Karma fireworks for (i = 0; i < 5; i++) @@ -1449,8 +1449,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget } player->karthud[khud_itemblink] = TICRATE; player->karthud[khud_itemblinkmode] = 0; - player->itemroulette = 0; - player->roulettetype = 0; + player->itemRoulette.active = false; if (P_IsDisplayPlayer(player)) S_StartSound(NULL, sfx_itrolf); } diff --git a/src/p_mobj.c b/src/p_mobj.c index ce526659d..efddf4fb8 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -6128,7 +6128,7 @@ static void P_MobjSceneryThink(mobj_t *mobj) break; // see also K_drawKartItem in k_hud.c - case MT_PLAYERARROW: + case MT_PLAYERARROW: // FIXME: Delete this object, attach to name tags instead. if (mobj->target && mobj->target->health && mobj->target->player && !mobj->target->player->spectator && mobj->target->health && mobj->target->player->playerstate != PST_DEAD @@ -6182,7 +6182,7 @@ static void P_MobjSceneryThink(mobj_t *mobj) } // Do this in an easy way - if (mobj->target->player->itemroulette) + if (mobj->target->player->itemRoulette.active) { mobj->tracer->color = mobj->target->player->skincolor; mobj->tracer->colorized = true; @@ -6198,11 +6198,11 @@ static void P_MobjSceneryThink(mobj_t *mobj) const INT32 numberdisplaymin = ((mobj->target->player->itemtype == KITEM_ORBINAUT) ? 5 : 2); // Set it to use the correct states for its condition - if (mobj->target->player->itemroulette) + if (mobj->target->player->itemRoulette.active) { P_SetMobjState(mobj, S_PLAYERARROW_BOX); mobj->tracer->sprite = SPR_ITEM; - mobj->tracer->frame = K_GetRollingRouletteItem(mobj->target->player) | FF_FULLBRIGHT; + mobj->tracer->frame = 1 | FF_FULLBRIGHT; mobj->tracer->renderflags &= ~RF_DONTDRAW; } else if (mobj->target->player->stealingtimer < 0) diff --git a/src/p_saveg.c b/src/p_saveg.c index 73d4a7d1f..3ce18aa5f 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -96,7 +96,7 @@ static void P_NetArchivePlayers(void) { INT32 i, j; UINT16 flags; -// size_t q; + size_t q; WRITEUINT32(save_p, ARCHIVEBLOCK_PLAYERS); @@ -310,9 +310,6 @@ static void P_NetArchivePlayers(void) WRITEUINT8(save_p, players[i].tripwirePass); WRITEUINT16(save_p, players[i].tripwireLeniency); - WRITEUINT16(save_p, players[i].itemroulette); - WRITEUINT8(save_p, players[i].roulettetype); - WRITESINT8(save_p, players[i].itemtype); WRITEUINT8(save_p, players[i].itemamount); WRITESINT8(save_p, players[i].throwdir); @@ -410,6 +407,50 @@ static void P_NetArchivePlayers(void) WRITEUINT32(save_p, players[i].botvars.itemconfirm); WRITESINT8(save_p, players[i].botvars.turnconfirm); WRITEUINT32(save_p, players[i].botvars.spindashconfirm); + + // itemroulette_t + WRITEUINT8(save_p, players[i].itemRoulette.active); + +#ifdef ITEM_LIST_SIZE + WRITEUINT32(save_p, players[i].itemRoulette.itemListLen); + + for (q = 0; q < ITEM_LIST_SIZE; q++) + { + if (q >= players[i].itemRoulette.itemListLen) + { + WRITESINT8(save_p, KITEM_NONE); + } + else + { + WRITESINT8(save_p, players[i].itemRoulette.itemList[q]); + } + } +#else + if (players[i].itemRoulette.itemList == NULL) + { + WRITEUINT32(save_p, 0); + WRITEUINT32(save_p, 0); + } + else + { + WRITEUINT32(save_p, players[i].itemRoulette.itemListCap); + WRITEUINT32(save_p, players[i].itemRoulette.itemListLen); + + for (q = 0; q < players[i].itemRoulette.itemListLen; q++) + { + WRITESINT8(save_p, players[i].itemRoulette.itemList[q]); + } + } +#endif + + WRITEUINT8(save_p, players[i].itemRoulette.useOdds); + WRITEUINT32(save_p, players[i].itemRoulette.dist); + WRITEUINT32(save_p, players[i].itemRoulette.index); + WRITEUINT8(save_p, players[i].itemRoulette.sound); + WRITEUINT32(save_p, players[i].itemRoulette.speed); + WRITEUINT32(save_p, players[i].itemRoulette.tics); + WRITEUINT32(save_p, players[i].itemRoulette.elapsed); + WRITEUINT8(save_p, players[i].itemRoulette.eggman); } } @@ -417,6 +458,7 @@ static void P_NetUnArchivePlayers(void) { INT32 i, j; UINT16 flags; + size_t q; if (READUINT32(save_p) != ARCHIVEBLOCK_PLAYERS) I_Error("Bad $$$.sav at archive block Players"); @@ -612,9 +654,6 @@ static void P_NetUnArchivePlayers(void) players[i].tripwirePass = READUINT8(save_p); players[i].tripwireLeniency = READUINT16(save_p); - players[i].itemroulette = READUINT16(save_p); - players[i].roulettetype = READUINT8(save_p); - players[i].itemtype = READSINT8(save_p); players[i].itemamount = READUINT8(save_p); players[i].throwdir = READSINT8(save_p); @@ -713,6 +752,61 @@ static void P_NetUnArchivePlayers(void) players[i].botvars.turnconfirm = READSINT8(save_p); players[i].botvars.spindashconfirm = READUINT32(save_p); + // itemroulette_t + players[i].itemRoulette.active = (boolean)READUINT8(save_p); + +#ifdef ITEM_LIST_SIZE + players[i].itemRoulette.itemListLen = (size_t)READUINT32(save_p); + + for (q = 0; q < ITEM_LIST_SIZE; q++) + { + players[i].itemRoulette.itemList[q] = READSINT8(save_p); + } +#else + players[i].itemRoulette.itemListCap = (size_t)READUINT32(save_p); + players[i].itemRoulette.itemListLen = (size_t)READUINT32(save_p); + + if (players[i].itemRoulette.itemListCap > 0) + { + if (players[i].itemRoulette.itemList == NULL) + { + players[i].itemRoulette.itemList = Z_Calloc( + sizeof(SINT8) * players[i].itemRoulette.itemListCap, + PU_STATIC, + &players[i].itemRoulette.itemList + ); + } + else + { + players[i].itemRoulette.itemList = Z_Realloc( + players[i].itemRoulette.itemList, + sizeof(SINT8) * players[i].itemRoulette.itemListCap, + PU_STATIC, + &players[i].itemRoulette.itemList + ); + } + + if (players[i].itemRoulette.itemList == NULL) + { + I_Error("Not enough memory for item roulette list\n"); + } + + for (q = 0; q < players[i].itemRoulette.itemListLen; q++) + { + players[i].itemRoulette.itemList[q] = READSINT8(save_p); + } + } +#endif + + players[i].itemRoulette.useOdds = READUINT8(save_p); + players[i].itemRoulette.dist = READUINT32(save_p); + players[i].itemRoulette.index = (size_t)READUINT32(save_p); + players[i].itemRoulette.sound = READUINT8(save_p); + players[i].itemRoulette.speed = (tic_t)READUINT32(save_p); + players[i].itemRoulette.tics = (tic_t)READUINT32(save_p); + players[i].itemRoulette.elapsed = (tic_t)READUINT32(save_p); + players[i].itemRoulette.eggman = (boolean)READUINT8(save_p); + //players[i].viewheight = P_GetPlayerViewHeight(players[i]); // scale cannot be factored in at this point } } diff --git a/src/typedef.h b/src/typedef.h index 5fb5ec146..0be1884bc 100644 --- a/src/typedef.h +++ b/src/typedef.h @@ -37,6 +37,7 @@ TYPEDEF (discordRequest_t); TYPEDEF (respawnvars_t); TYPEDEF (botvars_t); TYPEDEF (skybox_t); +TYPEDEF (itemroulette_t); TYPEDEF (player_t); // d_clisrv.h