Dynamic Roulette: mega cleanup, mini fixes

This commit is contained in:
Antonio Martinez 2024-08-21 02:28:31 -07:00
parent d8fc1fed64
commit 5d19bfcb91
3 changed files with 142 additions and 158 deletions

View file

@ -5684,61 +5684,28 @@ static void K_drawDistributionDebugger(void)
itemroulette_t rouletteData = {0}; itemroulette_t rouletteData = {0};
const fixed_t scale = (FRACUNIT >> 1); const fixed_t scale = (FRACUNIT >> 1);
const fixed_t space = 24 * scale;
const fixed_t pad = 9 * scale; const fixed_t pad = 9 * scale;
fixed_t x = -pad; fixed_t x = -pad;
fixed_t y = -pad;
size_t i;
if (R_GetViewNumber() != 0) // only for p1 if (R_GetViewNumber() != 0) // only for p1
{ {
return; return;
} }
K_FillItemRouletteData(stplyr, &rouletteData, false); K_FillItemRouletteData(stplyr, &rouletteData, false, true);
return; if (cv_kartdebugdistribution.value <= 1)
return;
for (i = 0; i < rouletteData.itemListLen; i++) V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+10, V_SNAPTOTOP|V_SNAPTORIGHT, va("speed = %u", rouletteData.speed));
{
const kartitems_t item = static_cast<kartitems_t>(rouletteData.itemList[i]);
UINT8 amount = 1;
if (y > (BASEVIDHEIGHT << FRACBITS) - space - pad) V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+22, V_SNAPTOTOP|V_SNAPTORIGHT, va("baseDist = %u", rouletteData.baseDist));
{ V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+30, V_SNAPTOTOP|V_SNAPTORIGHT, va("dist = %u", rouletteData.dist));
x += space;
y = -pad;
}
V_DrawFixedPatch(x, y, scale, V_SNAPTOTOP, V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+42, V_SNAPTOTOP|V_SNAPTORIGHT, va("firstDist = %u", rouletteData.firstDist));
K_GetSmallStaticCachedItemPatch(item), NULL); V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+50, V_SNAPTOTOP|V_SNAPTORIGHT, va("secondDist = %u", rouletteData.secondDist));
V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+58, V_SNAPTOTOP|V_SNAPTORIGHT, va("secondToFirst = %u", rouletteData.secondToFirst));
// Display amount for multi-items
amount = K_ItemResultToAmount(item);
if (amount > 1)
{
V_DrawStringScaled(
x + (18 * scale),
y + (23 * scale),
scale, FRACUNIT, FRACUNIT,
V_SNAPTOTOP,
NULL, HU_FONT,
va("x%d", amount)
);
}
y += space;
}
V_DrawRightAlignedString(320 - (x >> FRACBITS), 10, V_SNAPTOTOP, va("speed = %u", rouletteData.speed));
V_DrawRightAlignedString(320 - (x >> FRACBITS), 22, V_SNAPTOTOP, va("baseDist = %u", rouletteData.baseDist));
V_DrawRightAlignedString(320 - (x >> FRACBITS), 30, V_SNAPTOTOP, va("dist = %u", rouletteData.dist));
V_DrawRightAlignedString(320 - (x >> FRACBITS), 42, V_SNAPTOTOP, va("firstDist = %u", rouletteData.firstDist));
V_DrawRightAlignedString(320 - (x >> FRACBITS), 50, V_SNAPTOTOP, va("secondDist = %u", rouletteData.secondDist));
V_DrawRightAlignedString(320 - (x >> FRACBITS), 58, V_SNAPTOTOP, va("secondToFirst = %u", rouletteData.secondToFirst));
#ifndef ITEM_LIST_SIZE #ifndef ITEM_LIST_SIZE
Z_Free(rouletteData.itemList); Z_Free(rouletteData.itemList);

View file

@ -1004,6 +1004,9 @@ static void K_CalculateRouletteSpeed(itemroulette_t *const roulette)
roulette->tics = roulette->speed = ROULETTE_SPEED_FASTEST + FixedMul(ROULETTE_SPEED_SLOWEST - ROULETTE_SPEED_FASTEST, total); roulette->tics = roulette->speed = ROULETTE_SPEED_FASTEST + FixedMul(ROULETTE_SPEED_SLOWEST - ROULETTE_SPEED_FASTEST, total);
} }
// Honestly, the "power item" class is kind of a vestigial concept,
// but we'll faithfully port it over since it's not hurting anything so far
// (and it's at least ostensibly a Rival balancing mechanism, wheee).
static boolean K_IsItemPower(kartitems_t item) static boolean K_IsItemPower(kartitems_t item)
{ {
switch (item) switch (item)
@ -1060,7 +1063,8 @@ static boolean K_IsItemFirstPermitted(kartitems_t item)
} }
} }
// Maybe for later...
#if 0
static boolean K_IsItemSpeed(kartitems_t item) static boolean K_IsItemSpeed(kartitems_t item)
{ {
switch (item) switch (item)
@ -1075,6 +1079,7 @@ static boolean K_IsItemSpeed(kartitems_t item)
return false; return false;
} }
} }
#endif
static boolean K_IsItemUselessAlone(kartitems_t item) static boolean K_IsItemUselessAlone(kartitems_t item)
{ {
@ -1094,7 +1099,7 @@ static boolean K_IsItemUselessAlone(kartitems_t item)
} }
} }
// Which items are disallowed for THIS player? // Which items are disallowed for this player's specific placement?
static boolean K_ShouldPlayerAllowItem(kartitems_t item, const player_t *player) static boolean K_ShouldPlayerAllowItem(kartitems_t item, const player_t *player)
{ {
if (!(gametyperules & GTR_CIRCUIT)) if (!(gametyperules & GTR_CIRCUIT))
@ -1106,14 +1111,15 @@ static boolean K_ShouldPlayerAllowItem(kartitems_t item, const player_t *player)
return K_IsItemFirstPermitted(item); return K_IsItemFirstPermitted(item);
else else
{ {
// A little inelegant: filter the most chaotic items from courses with early sets and tight layouts.
if (K_IsItemPower(item) && (leveltime < ((15*TICRATE) + starttime))) if (K_IsItemPower(item) && (leveltime < ((15*TICRATE) + starttime)))
return false; return false;
return !K_IsItemFirstOnly(item); return !K_IsItemFirstOnly(item);
} }
} }
// Which items are disallowed for ALL players? // Which items are disallowed because it's the wrong time for them?
static boolean K_ShouldAllowItem(kartitems_t item, const itemroulette_t *roulette) static boolean K_TimingPermitsItem(kartitems_t item, const itemroulette_t *roulette)
{ {
if (!(gametyperules & GTR_CIRCUIT)) if (!(gametyperules & GTR_CIRCUIT))
return true; return true;
@ -1159,10 +1165,9 @@ static boolean K_ShouldAllowItem(kartitems_t item, const itemroulette_t *roulett
case KITEM_SPB: case KITEM_SPB:
{ {
cooldownOnStart = true; // In Race, we reintroduce and reenable this item to counter breakaway frontruns.
notNearEnd = true; // No need to roll it if that's not the case.
// TODO forcing, just disable for now return false;
return false;
break; break;
} }
@ -1191,26 +1196,16 @@ static boolean K_ShouldAllowItem(kartitems_t item, const itemroulette_t *roulett
return false; return false;
if (notNearEnd && (roulette != NULL && roulette->baseDist < ENDDIST)) if (notNearEnd && (roulette != NULL && roulette->baseDist < ENDDIST))
return false; return false;
if (K_DenyShieldOdds(item))
return false;
if (roulette && roulette->autoroulette == true)
{
if (K_DenyAutoRouletteOdds(item))
{
return false;
}
}
return true; return true;
} }
/*-------------------------------------------------- /*--------------------------------------------------
void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox) void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox, boolean dryrun)
See header file for description. See header file for description.
--------------------------------------------------*/ --------------------------------------------------*/
void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox) void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox, boolean dryrun)
{ {
UINT32 spawnChance[NUMKARTRESULTS] = {0}; UINT32 spawnChance[NUMKARTRESULTS] = {0};
UINT32 totalSpawnChance = 0; UINT32 totalSpawnChance = 0;
@ -1384,39 +1379,53 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet
if (gametyperules & GTR_CIRCUIT) if (gametyperules & GTR_CIRCUIT)
roulette->dist = FixedMul(roulette->preexpdist, max(player->exp, FRACUNIT/2)); roulette->dist = FixedMul(roulette->preexpdist, max(player->exp, FRACUNIT/2));
// ===============================================================================
// Dynamic Roulette. Oh boy! // Dynamic Roulette. Oh boy!
// Alright, here's the broad plan:
// STAGE 1: Determine what items are permissible // 1: Determine what items are permissible
// STAGE 2: Determine the item that's most appropriate for our distance from leader // 2: Determine the permitted item that's most appropriate for our distance from leader
// STAGE 3: Pick that item, then penalize it // 3: Pick that item, then penalize it so it's less likely to be repicked
// STAGE 4: Repeat 3 until the reel is full, then cram everything in // 4: Repeat 3 until we've picked enough stuff
// 5: Skim any items that are much weaker than the reel's average out of the roulette
// 6: Cram it all in
UINT32 targetpower = roulette->dist; // fill roulette with items around this value! UINT32 targetpower = roulette->dist; // fill roulette with items around this value!
UINT32 powers[NUMKARTRESULTS]; // how strong is each item? UINT32 powers[NUMKARTRESULTS]; // how strong is each item? think of this as a "target distance" for this item to spawn at
UINT32 deltas[NUMKARTRESULTS]; // how different is that strength from target? UINT32 deltas[NUMKARTRESULTS]; // how different is that strength from target?
UINT32 candidates[NUMKARTRESULTS]; // how many of this item should we try to insert? UINT32 candidates[NUMKARTRESULTS]; // how many of this item should we try to insert?
UINT32 dupetolerance[NUMKARTRESULTS]; // how willing are we to select this item after already selecting it? higher values = lower dupe penalty UINT32 dupetolerance[NUMKARTRESULTS]; // how willing are we to select this item after already selecting it? higher values = lower dupe penalty
boolean permit[NUMKARTRESULTS]; // is this item allowed? boolean permit[NUMKARTRESULTS]; // is this item allowed?
boolean rival = (player->bot && (player->botvars.rival || cv_levelskull.value)); boolean rival = (player->bot && (player->botvars.rival || cv_levelskull.value));
boolean mothfilter = true; // strip unusually weak items from reel? boolean filterweakitems = true; // strip unusually weak items from reel?
UINT8 reelsize = 15; // How many items to attempt to add in prepass? UINT8 reelsize = 15; // How many items to attempt to add in prepass?
UINT32 humanscaler = 250 + (roulette->playing * 15); // Scaler that converts "useodds" style distances in odds tables to raw distances. UINT32 humanscaler = 250 + (roulette->playing * 15); // Scaler that converts "useodds" style distances in odds tables to raw distances.
// Cache which items are permissible // == ARE THESE ITEMS ALLOWED?
// We have a fuckton of rules about when items are allowed to show up,
// like limiting trap items at the end of the race, limiting strong
// items at the start of the race... Dynamic stuff, not always trivial.
// We're about to do a bunch of work with items, so let's cache them all.
for (i = 1; i < NUMKARTRESULTS; i++) for (i = 1; i < NUMKARTRESULTS; i++)
{ {
permit[i] = K_ShouldAllowItem(i, roulette); if (!K_TimingPermitsItem(i, roulette))
permit[i] = false;
// CONS_Printf("%s permit prepass %d\n", cv_items[i-1].name, permit[i]); else if (!K_ShouldPlayerAllowItem(i, player))
permit[i] = false;
if (permit[i]) else if (K_GetItemCooldown(i))
permit[i] = K_ShouldPlayerAllowItem(i, player); permit[i] = false;
else if (!K_ItemEnabled(i))
// CONS_Printf("%s permit postpass %d\n", cv_items[i-1].name, permit[i]); permit[i] = false;
else if (K_DenyShieldOdds(i))
permit[i] = false;
else if (roulette && roulette->autoroulette == true && K_DenyAutoRouletteOdds(i))
permit[i] = false;
else
permit[i] = true;
} }
// temp - i have no fucking clue how pointers work i am so sorry // == ODDS TIME
// Set up the right item odds for the gametype we're in.
for (i = 1; i < NUMKARTRESULTS; i++) for (i = 1; i < NUMKARTRESULTS; i++)
{ {
// NOTE: Battle odds are underspecified, we don't invoke roulettes in this mode! // NOTE: Battle odds are underspecified, we don't invoke roulettes in this mode!
@ -1424,14 +1433,14 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet
{ {
powers[i] = humanscaler * K_DynamicItemOddsBattle[i-1][0]; powers[i] = humanscaler * K_DynamicItemOddsBattle[i-1][0];
dupetolerance[i] = K_DynamicItemOddsBattle[i-1][1]; dupetolerance[i] = K_DynamicItemOddsBattle[i-1][1];
mothfilter = false; filterweakitems = false;
} }
else if (specialstageinfo.valid == true) else if (specialstageinfo.valid == true)
{ {
powers[i] = humanscaler * K_DynamicItemOddsSpecial[i-1][0]; powers[i] = humanscaler * K_DynamicItemOddsSpecial[i-1][0];
dupetolerance[i] = K_DynamicItemOddsSpecial[i-1][1]; dupetolerance[i] = K_DynamicItemOddsSpecial[i-1][1];
reelsize = 8; reelsize = 8; // Smaller roulette in Special because there are much fewer standard items.
mothfilter = false; filterweakitems = false;
} }
else else
{ {
@ -1440,29 +1449,34 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet
} }
} }
// null stuff that doesn't have odds // == GTFO WEIRD ITEMS
// If something is set to distance 0 in its odds table, that means the item
// is completely ineligible for the gametype we're in, and should never be selected.
for (i = 1; i < NUMKARTRESULTS; i++) for (i = 1; i < NUMKARTRESULTS; i++)
{ {
if (powers[i] == 0) if (powers[i] == 0)
{ {
// CONS_Printf("%s nulled\n", cv_items[i-1].name);
permit[i] = false; permit[i] = false;
} }
} }
// Starting deltas // == REEL CANDIDATE PREP
// Dynamic Roulette works by comparing an item's "ideal" distance to our current distance from 1st.
// It'll pick the most suitable item, do some math, then move on to the next most suitable item.
// Calculate starting deltas and clear out the "candidates" array that stores what we pick.
for (i = 1; i < NUMKARTRESULTS; i++) for (i = 1; i < NUMKARTRESULTS; i++)
{ {
candidates[i] = 0; candidates[i] = 0;
deltas[i] = min(targetpower - powers[i], powers[i] - targetpower); deltas[i] = min(targetpower - powers[i], powers[i] - targetpower);
// CONS_Printf("starting delta for %s is %d\n", cv_items[i-1].name, deltas[i]);
} }
// == LONELINESS DETECTION
// A lot of items suck if no players are nearby to interact with them. // A lot of items suck if no players are nearby to interact with them.
// Should we bias towards items that get us back to the action? // Should we bias towards items that get us back to the action?
// This will set the "lonely" flag to be used later.
UINT32 lonelinessThreshold = 3*DISTVAR/2; UINT32 lonelinessThreshold = 3*DISTVAR/2;
UINT32 toAttacker = lonelinessThreshold; UINT32 toAttacker = lonelinessThreshold; // Distance to the player trying to kill us.
UINT32 toDefender = lonelinessThreshold; UINT32 toDefender = lonelinessThreshold; // Distance to the player we are trying to kill.
boolean lonely = false; boolean lonely = false;
if ((gametyperules & GTR_CIRCUIT) && specialstageinfo.valid == false) if ((gametyperules & GTR_CIRCUIT) && specialstageinfo.valid == false)
@ -1483,32 +1497,39 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet
if (toAttacker >= lonelinessThreshold && toDefender >= lonelinessThreshold && player->position > 1) if (toAttacker >= lonelinessThreshold && toDefender >= lonelinessThreshold && player->position > 1)
lonely = true; lonely = true;
// let's start finding items to list // == INTRODUCE TRYHARD-EATING PREDATOR
UINT8 added = 0; // If the frontrunner's making a major breakaway, "break the rules"
UINT32 totalreelpower = 0; // and insert the SPB into the roulette. This doesn't have to be
// incredibly forceful; there's a truly forced special case above.
fixed_t spb_odds = K_PercentSPBOdds(roulette, player->position);
if ((gametyperules & GTR_CIRCUIT)
&& specialstageinfo.valid == false
&& (spb_odds > 0) & (spbplace == -1)
&& (roulette->preexpdist >= powers[KITEM_SPB])) // SPECIAL CASE: Check raw distance instead of EXP-influenced target distance.
{
// When reenabling the SPB, we also adjust its delta to ensure that it has good odds of showing up.
// Players who are _seriously_ struggling are more likely to see Invinc or Rockets, since those items
// have a lower target distance, so we nudge the SPB towards them.
permit[KITEM_SPB] = true;
deltas[KITEM_SPB] = Easing_Linear(spb_odds, deltas[KITEM_SPB], 0);
}
// == ITEM SELECTION
// All the prep work's done: let's pick out a sampler platter of items until we fill the reel.
UINT8 added = 0; // How many items added so far?
UINT32 totalreelpower = 0; // How much total item power in the reel? Used for an average later.
for (i = 0; i < reelsize; i++) for (i = 0; i < reelsize; i++)
{ {
UINT32 lowestdelta = INT32_MAX; UINT32 lowestdelta = INT32_MAX;
size_t bestitem = 0; size_t bestitem = 0;
// CONS_Printf("LOOP %d\n", i); // Each rep, get the legal item with the lowest delta...
// check each kartitem to see which is the best fit,
// based on what's closest to our target power
// (but ignore items that aren't allowed now)
for (j = 1; j < NUMKARTRESULTS; j++) for (j = 1; j < NUMKARTRESULTS; j++)
{ {
// CONS_Printf("precheck %s, perm %d CD %d\n", cv_items[j-1].name, permit[j], K_GetItemCooldown(j));
if (!permit[j]) if (!permit[j])
continue; continue;
if (K_GetItemCooldown(j))
continue;
if (!K_ItemEnabled(j))
continue;
// CONS_Printf("checking %s, delta %d\n", cv_items[j-1].name, deltas[j]);
if (lowestdelta > deltas[j]) if (lowestdelta > deltas[j])
{ {
@ -1517,117 +1538,115 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet
} }
} }
// couldn't find an item? goodbye lol // Couldn't find any eligible items at all? GTFO.
// (This should never trigger, but you never know with the item switch menu.)
if (bestitem == 0) if (bestitem == 0)
break; break;
// UINT32 deltapenalty = (DISTVAR*4)^(candidates[bestitem])/dupetolerance[bestitem]; // Impose a penalty to this item's delta, to bias against selecting it again.
// This is naively slashed by an item's "duplicate tolerance":
// lower tolerance means that an item is less likely to be reselected (it's "rarer").
UINT32 deltapenalty = 4*DISTVAR*(1+candidates[bestitem])/dupetolerance[bestitem]; UINT32 deltapenalty = 4*DISTVAR*(1+candidates[bestitem])/dupetolerance[bestitem];
// Power items get better odds in frantic, or if you're the rival.
// (For the rival, this is way more likely to matter at lower skills, where they're
// worse at selecting their item—but it always matters in frantic gameplay.)
if (K_IsItemPower(bestitem) && rival) if (K_IsItemPower(bestitem) && rival)
deltapenalty = 3 * deltapenalty / 4; deltapenalty = 3 * deltapenalty / 4;
if (K_IsItemPower(bestitem) && franticitems) if (K_IsItemPower(bestitem) && franticitems)
deltapenalty = 3 * deltapenalty / 4; deltapenalty = 3 * deltapenalty / 4;
// Conversely, if we're lonely, try not to reselect an item that wouldn't be useful to us
// without any players to use it on.
if (lonely && K_IsItemUselessAlone(bestitem)) if (lonely && K_IsItemUselessAlone(bestitem))
deltapenalty *= 2; deltapenalty *= 2;
// Draw complex odds debugger. This one breaks down all the calcs in order.
if (cv_kartdebugdistribution.value > 1) if (cv_kartdebugdistribution.value > 1)
{ {
UINT16 BASE_X = 18; UINT16 BASE_X = 18;
UINT16 BASE_Y = 5+12*i; UINT16 BASE_Y = 5+12*i;
INT32 FLAGS = V_SNAPTOTOP|V_SNAPTOLEFT; INT32 FLAGS = V_SNAPTOTOP|V_SNAPTOLEFT;
// V_DrawThinString(BASE_X + 100, BASE_Y, FLAGS, va("%s", cv_items[lowestindex-1].name));
V_DrawThinString(BASE_X + 35, BASE_Y, FLAGS, va("P%d", powers[bestitem]/humanscaler)); V_DrawThinString(BASE_X + 35, BASE_Y, FLAGS, va("P%d", powers[bestitem]/humanscaler));
V_DrawThinString(BASE_X + 65, BASE_Y, FLAGS, va("D%d", deltas[bestitem]/humanscaler)); V_DrawThinString(BASE_X + 65, BASE_Y, FLAGS, va("D%d", deltas[bestitem]/humanscaler));
V_DrawThinString(BASE_X + 20, BASE_Y, FLAGS, va("%d", dupetolerance[bestitem])); V_DrawThinString(BASE_X + 20, BASE_Y, FLAGS, va("%d", dupetolerance[bestitem]));
//V_DrawThinString(BASE_X + 70, BASE_Y, FLAGS, va("+%d", deltapenalty));
V_DrawFixedPatch(BASE_X*FRACUNIT, (BASE_Y-7)*FRACUNIT, (FRACUNIT >> 1), FLAGS, K_GetSmallStaticCachedItemPatch(bestitem), NULL); V_DrawFixedPatch(BASE_X*FRACUNIT, (BASE_Y-7)*FRACUNIT, (FRACUNIT >> 1), FLAGS, K_GetSmallStaticCachedItemPatch(bestitem), NULL);
UINT8 amount = K_ItemResultToAmount(bestitem); UINT8 amount = K_ItemResultToAmount(bestitem);
if (amount > 1) if (amount > 1)
{
V_DrawThinString(BASE_X, BASE_Y, FLAGS, va("x%d", amount)); V_DrawThinString(BASE_X, BASE_Y, FLAGS, va("x%d", amount));
}
} }
// otherwise, prep it to be added and give it a duplicaton penalty, // Add the selected item to our list of candidates and update its working delta.
// so that a different item is more likely to be inserted next
candidates[bestitem]++; candidates[bestitem]++;
deltas[bestitem] += deltapenalty; deltas[bestitem] += deltapenalty;
// Then update our ongoing average of the reel's power.
totalreelpower += powers[bestitem]; totalreelpower += powers[bestitem];
added++; added++;
// CONS_Printf("added %s with candidates %d\n", cv_items[lowestindex-1].name, candidates[lowestindex]);
} }
// Introduce SPB to race if there's a major frontrun breakway if (added == 0)
fixed_t spb_odds = K_PercentSPBOdds(roulette, player->position);
if ((gametyperules & GTR_CIRCUIT)
&& specialstageinfo.valid == false
&& (spb_odds > 0) & (spbplace == -1))
{ {
permit[KITEM_SPB] = true; // Guess we're making circles now.
deltas[KITEM_SPB] = Easing_Linear(spb_odds, 3000, 0); // Just do something that doesn't crash.
K_AddItemToReel(player, roulette, singleItem);
return;
} }
UINT8 debugcount = 0; UINT8 debugcount = 0; // For the "simple" odds debugger.
UINT32 meanreelpower = totalreelpower/max(added, 1); UINT32 meanreelpower = totalreelpower/max(added, 1); // Average power for the "moth filter".
// set up the list indices used to random-shuffle the ro ulette // == PREP FOR ADDING TO THE ROULETTE REEL
// Sal's prior work for this is rock-solid.
// This fills the spawnChance array with a rolling count of items,
// so that we can loop upward through it until we hit our random index.
for (i = 1; i < NUMKARTRESULTS; i++) for (i = 1; i < NUMKARTRESULTS; i++)
{ {
// filter items vastly too weak for this reel // If an item is far too week for this reel, reject it.
boolean reject = (powers[i] + DISTVAR < meanreelpower); // This can happen in regions of the odds with a lot of items that
// don't really like to be duplicated. Favor the player; high-rolling
if (!mothfilter) // feels exciting, low-rolling feels punishing!
reject = false; boolean reject = (filterweakitems) && (powers[i] + DISTVAR < meanreelpower);
// Before we actually apply that rejection, draw the simple odds debugger.
// This one is just to watch the distribution for vibes as you drive around.
if (cv_kartdebugdistribution.value && candidates[i]) if (cv_kartdebugdistribution.value && candidates[i])
{ {
UINT16 BASE_X = 280; UINT16 BASE_X = 280;
UINT16 BASE_Y = 5+12*debugcount; UINT16 BASE_Y = 5+12*debugcount;
INT32 FLAGS = V_SNAPTOTOP|V_SNAPTORIGHT; INT32 FLAGS = V_SNAPTOTOP|V_SNAPTORIGHT;
V_DrawThinString(BASE_X - 12, 5, FLAGS, va("%d", targetpower/humanscaler)); V_DrawThinString(BASE_X - 12, 5, FLAGS, va("%d", targetpower/humanscaler));
V_DrawThinString(BASE_X - 12, 5+12, FLAGS, va("%d", toAttacker)); V_DrawThinString(BASE_X - 12, 5+12, FLAGS, va("%d", toAttacker));
V_DrawThinString(BASE_X - 12, 5+24, FLAGS, va("%d", toDefender)); V_DrawThinString(BASE_X - 12, 5+24, FLAGS, va("%d", toDefender));
if (lonely) if (lonely)
V_DrawThinString(BASE_X - 12, 5+36, FLAGS, va(":(")); V_DrawThinString(BASE_X - 12, 5+36, FLAGS, va(":("));
for(UINT8 k = 0; k < candidates[i]; k++) for(UINT8 k = 0; k < candidates[i]; k++)
V_DrawFixedPatch((BASE_X + 3*k)*FRACUNIT, (BASE_Y-7)*FRACUNIT, (FRACUNIT >> 1), FLAGS, K_GetSmallStaticCachedItemPatch(i), NULL); V_DrawFixedPatch((BASE_X + 3*k)*FRACUNIT, (BASE_Y-7)*FRACUNIT, (FRACUNIT >> 1), FLAGS, K_GetSmallStaticCachedItemPatch(i), NULL);
UINT8 amount = K_ItemResultToAmount(i); UINT8 amount = K_ItemResultToAmount(i);
if (amount > 1) if (amount > 1)
{
V_DrawThinString(BASE_X, BASE_Y, FLAGS, va("x%d", amount)); V_DrawThinString(BASE_X, BASE_Y, FLAGS, va("x%d", amount));
}
if (reject) if (reject)
V_DrawThinString(BASE_X, BASE_Y, FLAGS|V_60TRANS, va("WEAK")); V_DrawThinString(BASE_X, BASE_Y, FLAGS|V_60TRANS, va("WEAK"));
debugcount++; debugcount++;
} }
if (!reject) // Okay, apply the rejection now.
{ if (reject)
spawnChance[i] = ( candidates[i] = 0;
totalSpawnChance += candidates[i]
); // Bump totalSpawnChance, write that rolling counter, and move on.
} spawnChance[i] = (
totalSpawnChance += candidates[i]
);
} }
if (totalSpawnChance == 0) if (dryrun) // We're being called from the debugger on a view conditional!
{ return; // This is net unsafe if we do things with side effects. GTFO!
// why did this fucking happen LOL
// don't crash
K_AddItemToReel(player, roulette, singleItem);
return;
}
// and insert all of our candidates into the roulette in a random order // == FINALLY ADD THIS SHIT TO THE REEL
// Super simple: generate a random index,
// count up until we hit that index,
// insert that item and decrement everything after.
while (totalSpawnChance > 0) while (totalSpawnChance > 0)
{ {
rngRoll = P_RandomKey(PR_ITEM_ROULETTE, totalSpawnChance); rngRoll = P_RandomKey(PR_ITEM_ROULETTE, totalSpawnChance);
@ -1636,13 +1655,10 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet
continue; continue;
} }
// CONS_Printf("adding %s, tsp %d\n", cv_items[i-1].name, totalSpawnChance);
K_AddItemToReel(player, roulette, i); K_AddItemToReel(player, roulette, i);
for (; i < NUMKARTRESULTS; i++) for (; i < NUMKARTRESULTS; i++)
{ {
// Be sure to fix the remaining items' odds too.
if (spawnChance[i] > 0) if (spawnChance[i] > 0)
{ {
spawnChance[i]--; spawnChance[i]--;
@ -1663,7 +1679,7 @@ void K_StartItemRoulette(player_t *const player, boolean ringbox)
itemroulette_t *const roulette = &player->itemRoulette; itemroulette_t *const roulette = &player->itemRoulette;
size_t i; size_t i;
K_FillItemRouletteData(player, roulette, ringbox); K_FillItemRouletteData(player, roulette, ringbox, false);
if (roulette->autoroulette) if (roulette->autoroulette)
roulette->index = P_RandomRange(PR_AUTOROULETTE, 0, roulette->itemListLen - 1); roulette->index = P_RandomRange(PR_AUTOROULETTE, 0, roulette->itemListLen - 1);

View file

@ -98,7 +98,7 @@ INT32 K_KartGetBattleOdds(const player_t *player, UINT8 pos, kartitems_t item);
/*-------------------------------------------------- /*--------------------------------------------------
void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox); void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox, boolean dryrun);
Fills out the item roulette struct when it is Fills out the item roulette struct when it is
initially created. This function needs to be initially created. This function needs to be
@ -110,12 +110,13 @@ INT32 K_KartGetBattleOdds(const player_t *player, UINT8 pos, kartitems_t item);
Can be NULL for generic use. Can be NULL for generic use.
roulette - The roulette data struct to fill out. roulette - The roulette data struct to fill out.
ringbox - Is this roulette fill triggered by a just-respawned Ring Box? ringbox - Is this roulette fill triggered by a just-respawned Ring Box?
dryrun - Are we calling this from the distribution debugger? Don't call RNG or write roulette data!
Return:- Return:-
N/A N/A
--------------------------------------------------*/ --------------------------------------------------*/
void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox); void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox, boolean dryrun);
/*-------------------------------------------------- /*--------------------------------------------------