diff --git a/src/cvars.cpp b/src/cvars.cpp index 048613394..48435238f 100644 --- a/src/cvars.cpp +++ b/src/cvars.cpp @@ -819,7 +819,7 @@ consvar_t cv_fuzz = OnlineCheat("fuzz", "Off").on_off().description("Human playe consvar_t cv_kartdebugamount = OnlineCheat("debugitemamount", "1").min_max(1, 255).description("If debugitem, give multiple copies of an item"); consvar_t cv_kartdebugbots = OnlineCheat("debugbots", "Off").on_off().description("Bot AI debugger"); -consvar_t cv_kartdebugdistribution = OnlineCheat("debugitemodds", "Off").on_off().description("Show items that the roulette can roll"); +consvar_t cv_kartdebugdistribution = OnlineCheat("debugitemodds", "0").min_max(0, 2).description("Show items that the roulette can roll"); consvar_t cv_kartdebughuddrop = OnlineCheat("debugitemdrop", "Off").on_off().description("Players drop paper items when damaged in any way"); consvar_t cv_kartdebugbotwhip = OnlineCheat("debugbotwhip", "Off").on_off().description("Disable bot ring and item pickups"); diff --git a/src/d_clisrv.c b/src/d_clisrv.c index c3c54ea99..c4adc7476 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -6805,6 +6805,22 @@ INT32 D_NumPlayers(void) return num; } +/** Returns the number of players racing, not spectating and includes bots + * \return Number of players. Can be zero if we're running a ::dedicated + * server. + */ +INT32 D_NumPlayersInRace(void) +{ + INT32 numPlayers = 0; + INT32 i; + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] && !players[i].spectator) + numPlayers++; + } + return numPlayers; +} + /** Return whether a player is a real person (not a CPU) and not spectating. */ boolean D_IsPlayerHumanAndGaming (INT32 player_number) diff --git a/src/d_clisrv.h b/src/d_clisrv.h index 64a75744e..3b6889fcb 100644 --- a/src/d_clisrv.h +++ b/src/d_clisrv.h @@ -657,6 +657,7 @@ extern UINT8 playernode[MAXPLAYERS]; extern UINT8 playerconsole[MAXPLAYERS]; INT32 D_NumPlayers(void); +INT32 D_NumPlayersInRace(void); boolean D_IsPlayerHumanAndGaming(INT32 player_number); void D_ResetTiccmds(void); diff --git a/src/d_player.h b/src/d_player.h index b2a513339..214722a5c 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -495,9 +495,8 @@ struct itemroulette_t SINT8 *itemList; #endif - UINT8 useOdds; UINT8 playing, exiting; - UINT32 dist, baseDist; + UINT32 preexpdist, dist, baseDist; UINT32 firstDist, secondDist; UINT32 secondToFirst; @@ -914,6 +913,7 @@ struct player_t UINT8 laps; // Number of laps (optional) UINT8 latestlap; UINT32 lapPoints; // Points given from laps + INT32 exp; INT32 cheatchecknum; // The number of the last cheatcheck you hit INT32 checkpointId; // Players respawn here, objects/checkpoint.cpp diff --git a/src/g_game.c b/src/g_game.c index 1628e6d36..4ba14b585 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -2131,6 +2131,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) UINT8 laps; UINT8 latestlap; UINT32 lapPoints; + INT32 exp; UINT16 skincolor; INT32 skin; UINT8 availabilities[MAXAVAILABILITY]; @@ -2319,6 +2320,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) laps = 0; latestlap = 0; lapPoints = 0; + exp = FRACUNIT; roundscore = 0; exiting = 0; khudfinish = 0; @@ -2356,6 +2358,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) laps = players[player].laps; latestlap = players[player].latestlap; lapPoints = players[player].lapPoints; + exp = players[player].exp; roundscore = players[player].roundscore; @@ -2470,6 +2473,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) p->laps = laps; p->latestlap = latestlap; p->lapPoints = lapPoints; + p->exp = exp; p->totalring = totalring; for (i = 0; i < LAP__MAX; i++) @@ -5248,7 +5252,7 @@ void G_InitNew(UINT8 pencoremode, INT32 map, boolean resetplayer, boolean skippr players[i].score = 0; } - if (resetplayer || map != gamemap) + if (resetplayer || !(gametyperules & GTR_CHECKPOINTS && map == gamemap)) { players[i].checkpointId = 0; } diff --git a/src/k_hud.cpp b/src/k_hud.cpp index c68bdd955..398f4cca3 100644 --- a/src/k_hud.cpp +++ b/src/k_hud.cpp @@ -1061,7 +1061,7 @@ static patch_t *K_GetCachedItemPatch(INT32 item, UINT8 offset) return NULL; } -static patch_t *K_GetSmallStaticCachedItemPatch(kartitems_t item) +patch_t *K_GetSmallStaticCachedItemPatch(kartitems_t item) { UINT8 offset; @@ -5736,60 +5736,28 @@ static void K_drawDistributionDebugger(void) itemroulette_t rouletteData = {0}; const fixed_t scale = (FRACUNIT >> 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 (R_GetViewNumber() != 0) // only for p1 { return; } - K_FillItemRouletteData(stplyr, &rouletteData, false); + K_FillItemRouletteData(stplyr, &rouletteData, false, true); - for (i = 0; i < rouletteData.itemListLen; i++) - { - const kartitems_t item = static_cast(rouletteData.itemList[i]); - UINT8 amount = 1; + if (cv_kartdebugdistribution.value <= 1) + return; - if (y > (BASEVIDHEIGHT << FRACBITS) - space - pad) - { - x += space; - y = -pad; - } + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+10, V_SNAPTOTOP|V_SNAPTORIGHT, va("speed = %u", rouletteData.speed)); - V_DrawFixedPatch(x, y, scale, V_SNAPTOTOP, - K_GetSmallStaticCachedItemPatch(item), NULL); + 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)); - // 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_DrawString((x >> FRACBITS) + 20, 2, V_SNAPTOTOP, va("useOdds[%u]", rouletteData.useOdds)); - V_DrawString((x >> FRACBITS) + 20, 10, V_SNAPTOTOP, va("speed = %u", rouletteData.speed)); - - V_DrawString((x >> FRACBITS) + 20, 22, V_SNAPTOTOP, va("baseDist = %u", rouletteData.baseDist)); - V_DrawString((x >> FRACBITS) + 20, 30, V_SNAPTOTOP, va("dist = %u", rouletteData.dist)); - - V_DrawString((x >> FRACBITS) + 20, 42, V_SNAPTOTOP, va("firstDist = %u", rouletteData.firstDist)); - V_DrawString((x >> FRACBITS) + 20, 50, V_SNAPTOTOP, va("secondDist = %u", rouletteData.secondDist)); - V_DrawString((x >> FRACBITS) + 20, 58, V_SNAPTOTOP, va("secondToFirst = %u", rouletteData.secondToFirst)); + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+42, V_SNAPTOTOP|V_SNAPTORIGHT, va("firstDist = %u", rouletteData.firstDist)); + 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)); #ifndef ITEM_LIST_SIZE Z_Free(rouletteData.itemList); @@ -6096,7 +6064,7 @@ void K_ClearPersistentMessages() } // Return value can be used for "paired" splitscreen messages, true = was displayed -void K_AddMessageForPlayer(player_t *player, const char *msg, boolean interrupt, boolean persist) +void K_AddMessageForPlayer(const player_t *player, const char *msg, boolean interrupt, boolean persist) { if (!player) return; @@ -6564,6 +6532,10 @@ void K_drawKartHUD(void) if (cv_kartdebugdistribution.value) K_drawDistributionDebugger(); + // temp debug + V_DrawSmallString(8, 2, V_SNAPTOTOP, va("Exp/Dist mult: %.2f", FixedToFloat(stplyr->exp))); + // V_DrawSmallString(8, 4, V_SNAPTOTOP, va("Exp/Dist mult: %.2f", FixedToFloat(stplyr->exp))); + if (cv_kartdebugnodes.value) { UINT8 p; diff --git a/src/k_hud.h b/src/k_hud.h index 319601d00..698b72653 100644 --- a/src/k_hud.h +++ b/src/k_hud.h @@ -108,11 +108,13 @@ extern patch_t *kp_facenum[MAXPLAYERS+1]; extern patch_t *kp_unknownminimap; void K_AddMessage(const char *msg, boolean interrupt, boolean persist); -void K_AddMessageForPlayer(player_t *player, const char *msg, boolean interrupt, boolean persist); +void K_AddMessageForPlayer(const player_t *player, const char *msg, boolean interrupt, boolean persist); void K_ClearPersistentMessages(void); void K_ClearPersistentMessageForPlayer(player_t *player); void K_TickMessages(void); +patch_t *K_GetSmallStaticCachedItemPatch(kartitems_t item); + typedef enum { PLAYERTAG_NONE, diff --git a/src/k_kart.c b/src/k_kart.c index f4eabed79..0f5ca15b6 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -4007,8 +4007,10 @@ void K_SpawnAmps(player_t *player, UINT8 amps, mobj_t *impact) if (gametyperules & GTR_SPHERES) return; - // Give that Sonic guy some help. - UINT16 scaledamps = min(amps, amps * (10 + player->kartspeed - player->kartweight) / 10); + UINT16 scaledamps = min(amps, amps * (10 + (9-player->kartspeed) - (9-player->kartweight)) / 10); + + if (player->position <= 1) + scaledamps /= 2; for (int i = 0; i < (scaledamps/2); i++) { @@ -4052,6 +4054,13 @@ void K_AwardPlayerAmps(player_t *player, UINT8 amps) if (player->rings <= 0 && player->ampspending == 0) { + // Auto Overdrive! + // If this is a fresh OD, give 'em some extra juice to make up for lack of flexibility. + if (!player->overdrive && player->mo && !P_MobjWasRemoved(player->mo)) + { + S_StartSound(player->mo, sfx_gshac); + player->amps *= 2; + } K_Overdrive(player); } } @@ -4091,7 +4100,7 @@ boolean K_Overdrive(player_t *player) S_StartSound(player->mo, sfx_cdfm35); S_StartSound(player->mo, sfx_cdfm13); - player->overdrive += (player->amps)*6; + player->overdrive += (player->amps)*5; player->overshield += (player->amps)*2; player->overdrivepower = FRACUNIT; @@ -4112,7 +4121,7 @@ boolean K_DefensiveOverdrive(player_t *player) S_StartSound(player->mo, sfx_cdfm35); S_StartSound(player->mo, sfx_cdfm13); - player->overdrive += (player->amps)*4; + player->overdrive += (player->amps)*3; player->overshield += (player->amps)*2 + TICRATE*2; player->overdrivepower = FRACUNIT; @@ -7424,7 +7433,7 @@ SINT8 K_GetTotallyRandomResult(UINT8 useodds) // Avoid calling K_FillItemRouletteData since that // function resets PR_ITEM_ROULETTE. spawnchance[i] = ( - totalspawnchance += K_KartGetItemOdds(NULL, NULL, useodds, i) + totalspawnchance += K_KartGetBattleOdds(NULL, useodds, i) ); } @@ -12712,6 +12721,7 @@ void K_MoveKartPlayer(player_t *player, boolean onground) else { UINT32 behind = K_GetItemRouletteDistance(player, player->itemRoulette.playing); + behind = FixedMul(behind, max(player->exp, FRACUNIT/2)); UINT32 behindMulti = behind / 500; behindMulti = min(behindMulti, 60); award = award * (behindMulti + 10) / 10; @@ -12982,6 +12992,7 @@ void K_MoveKartPlayer(player_t *player, boolean onground) else player->rocketsneakertimer -= 3*TICRATE; player->botvars.itemconfirm = 2*TICRATE; + player->overshield += TICRATE/2; // TEMP prototype } } else if (player->itemamount == 0) @@ -12997,6 +13008,8 @@ void K_MoveKartPlayer(player_t *player, boolean onground) { K_DoSneaker(player, 1); K_PlayBoostTaunt(player->mo); + player->overshield += TICRATE/2; // TEMP prototype + player->sneakertimer += TICRATE; // TEMP prototype player->itemamount--; player->botvars.itemconfirm = 0; } @@ -14780,4 +14793,43 @@ boolean K_PlayerCanUseItem(player_t *player) return (player->mo->health > 0 && !player->spectator && !P_PlayerInPain(player) && !mapreset && leveltime > introtime); } +fixed_t K_GetExpAdjustment(player_t *player) +{ + fixed_t exp_power = 3*FRACUNIT/100; // adjust to change overall xp volatility + fixed_t exp_stablerate = 3*FRACUNIT/10; // how low is your placement before losing XP? 4*FRACUNIT/10 = top 40% of race will gain + fixed_t result = 0; + + INT32 live_players = 0; + + for (INT32 i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i] || players[i].spectator || player == players+i) + continue; + + live_players++; + } + + if (live_players < 8) + { + exp_power += (8 - live_players) * exp_power/4; + } + + // Increase XP for each player you're beating... + for (INT32 i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i] || players[i].spectator || player == players+i) + continue; + + if (player->position < players[i].position) + result += exp_power; + } + + // ...then take all of the XP you could possibly have earned, + // and lose it proportional to the stable rate. If you're below + // the stable threshold, this results in you losing XP. + result -= FixedMul(exp_power, FixedMul(live_players*FRACUNIT, FRACUNIT - exp_stablerate)); + + return result; +} + //} diff --git a/src/k_kart.h b/src/k_kart.h index 298296d11..0f6c14c5f 100644 --- a/src/k_kart.h +++ b/src/k_kart.h @@ -288,6 +288,8 @@ boolean K_ThunderDome(void); boolean K_PlayerCanUseItem(player_t *player); +fixed_t K_GetExpAdjustment(player_t *player); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/k_objects.h b/src/k_objects.h index 9a2d314bc..d0fd010fc 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -263,6 +263,9 @@ mobj_t *Obj_FindCheckpoint(INT32 id); boolean Obj_GetCheckpointRespawnPosition(const mobj_t *checkpoint, vector3_t *return_pos); angle_t Obj_GetCheckpointRespawnAngle(const mobj_t *checkpoint); void Obj_ActivateCheckpointInstantly(mobj_t* mobj); +UINT32 Obj_GetCheckpointCount(); +void Obj_ClearCheckpoints(); +void Obj_DeactivateCheckpoints(); /* Rideroid / Rideroid Node */ void Obj_RideroidThink(mobj_t *mo); diff --git a/src/k_rank.cpp b/src/k_rank.cpp index af7f12bf1..aa6322f5c 100644 --- a/src/k_rank.cpp +++ b/src/k_rank.cpp @@ -26,6 +26,7 @@ #include "byteptr.h" #include "k_race.h" #include "command.h" +#include "k_objects.h" // I was ALMOST tempted to start tearing apart all // of the map loading code and turning it into C++ @@ -510,7 +511,8 @@ void gpRank_t::Update(void) } lvl->time = UINT32_MAX; - lvl->totalLapPoints = K_RaceLapCount(gamemap - 1) * 2; + + lvl->totalLapPoints = ( K_RaceLapCount(gamemap - 1) + Obj_GetCheckpointCount() )* 2; lvl->totalPrisons = maptargets; UINT8 i; diff --git a/src/k_roulette.c b/src/k_roulette.c index 1a44f0b69..d0ef0d2c6 100644 --- a/src/k_roulette.c +++ b/src/k_roulette.c @@ -51,6 +51,8 @@ #include "k_objects.h" #include "k_grandprix.h" #include "k_specialstage.h" +#include "k_hud.h" // distribution debugger +#include "m_easing.h" // Magic number distance for use with item roulette tiers #define DISTVAR (2048) @@ -75,41 +77,113 @@ #define ROULETTE_SPEED_TIMEATTACK (9) #define ROULETTE_SPEED_VERSUS_SLOWEST (12) -static UINT8 K_KartItemOddsRace[NUMKARTRESULTS-1][8] = +static UINT32 K_DynamicItemOddsRace[NUMKARTRESULTS-1][2] = { - { 0, 0, 2, 3, 4, 0, 0, 0 }, // Sneaker - { 0, 0, 0, 0, 0, 3, 4, 5 }, // Rocket Sneaker - { 0, 0, 0, 0, 2, 5, 5, 7 }, // Invincibility - { 2, 3, 1, 0, 0, 0, 0, 0 }, // Banana - { 1, 2, 0, 0, 0, 0, 0, 0 }, // Eggman Monitor - { 5, 5, 2, 2, 0, 0, 0, 0 }, // Orbinaut - { 0, 4, 2, 1, 0, 0, 0, 0 }, // Jawz - { 0, 3, 3, 2, 0, 0, 0, 0 }, // Mine - { 3, 0, 0, 0, 0, 0, 0, 0 }, // Land Mine - { 0, 0, 2, 2, 0, 0, 0, 0 }, // Ballhog - { 0, 0, 0, 0, 0, 2, 4, 0 }, // Self-Propelled Bomb - { 0, 0, 0, 0, 2, 5, 0, 0 }, // Grow - { 0, 0, 0, 0, 0, 2, 4, 2 }, // Shrink - { 1, 0, 0, 0, 0, 0, 0, 0 }, // Lightning Shield - { 0, 1, 2, 1, 0, 0, 0, 0 }, // Bubble Shield - { 0, 0, 0, 0, 0, 1, 3, 5 }, // Flame Shield - { 3, 0, 0, 0, 0, 0, 0, 0 }, // Hyudoro - { 0, 0, 0, 0, 0, 0, 0, 0 }, // Pogo Spring - { 2, 1, 1, 0, 0, 0, 0, 0 }, // Super Ring - { 0, 0, 0, 0, 0, 0, 0, 0 }, // Kitchen Sink - { 3, 0, 0, 0, 0, 0, 0, 0 }, // Drop Target - { 0, 0, 0, 1, 2, 2, 0, 0 }, // Garden Top - { 0, 0, 0, 0, 0, 0, 0, 0 }, // Gachabom - { 0, 0, 2, 3, 3, 1, 0, 0 }, // Sneaker x2 - { 0, 0, 0, 0, 4, 4, 4, 0 }, // Sneaker x3 - { 0, 1, 1, 0, 0, 0, 0, 0 }, // Banana x3 - { 0, 0, 1, 0, 0, 0, 0, 0 }, // Orbinaut x3 - { 0, 0, 0, 2, 0, 0, 0, 0 }, // Orbinaut x4 - { 0, 0, 1, 2, 1, 0, 0, 0 }, // Jawz x2 - { 0, 0, 0, 0, 0, 0, 0, 0 } // Gachabom x3 + // distance, duplication tolerance + {22, 14}, // sneaker + {63, 12}, // rocketsneaker + {60, 19}, // invincibility + {8, 4}, // banana + {3, 1}, // eggmark + {11, 4}, // orbinaut + {16, 4}, // jawz + {19, 4}, // mine + {1, 3}, // landmine + {25, 3}, // ballhog + {58, 6}, // selfpropelledbomb + {55, 7}, // grow + {70, 8}, // shrink + {1, 1}, // lightningshield + {25, 4}, // bubbleshield + {66, 9}, // flameshield + {1, 3}, // hyudoro + {0, 0}, // pogospring + {7, 4}, // superring + {0, 0}, // kitchensink + {1, 3}, // droptarget + {43, 5}, // gardentop + {0, 0}, // gachabom + {30, 14}, // dualsneaker + {42, 14}, // triplesneaker + {25, 2}, // triplebanana + {25, 1}, // tripleorbinaut + {35, 2}, // quadorbinaut + {30, 4}, // dualjawz + {0, 0}, // triplegachabom }; -static UINT8 K_KartItemOddsBattle[NUMKARTRESULTS-1][2] = +static UINT32 K_DynamicItemOddsBattle[NUMKARTRESULTS-1][2] = +{ + // distance, duplication tolerance + {20, 1}, // sneaker + {0, 0}, // rocketsneaker + {20, 1}, // invincibility + {0, 0}, // banana + {0, 0}, // eggmark + {10, 2}, // orbinaut + {12, 4}, // jawz + {13, 3}, // mine + {0, 0}, // landmine + {13, 3}, // ballhog + {0, 0}, // selfpropelledbomb + {15, 2}, // grow + {0, 0}, // shrink + {0, 0}, // lightningshield + {10, 1}, // bubbleshield + {0, 0}, // flameshield + {0, 0}, // hyudoro + {0, 0}, // pogospring + {0, 0}, // superring + {0, 0}, // kitchensink + {0, 0}, // droptarget + {0, 0}, // gardentop + {10, 5}, // gachabom + {0, 0}, // dualsneaker + {20, 1}, // triplesneaker + {0, 0}, // triplebanana + {10, 2}, // tripleorbinaut + {13, 3}, // quadorbinaut + {13, 3}, // dualjawz + {10, 2}, // triplegachabom +}; + +static UINT32 K_DynamicItemOddsSpecial[NUMKARTRESULTS-1][2] = +{ + // distance, duplication tolerance + {15, 2}, // sneaker + {0, 0}, // rocketsneaker + {0, 0}, // invincibility + {0, 0}, // banana + {0, 0}, // eggmark + {20, 3}, // orbinaut + {15, 2}, // jawz + {0, 0}, // mine + {0, 0}, // landmine + {0, 0}, // ballhog + {70, 1}, // selfpropelledbomb + {0, 0}, // grow + {0, 0}, // shrink + {0, 0}, // lightningshield + {0, 0}, // bubbleshield + {0, 0}, // flameshield + {0, 0}, // hyudoro + {0, 0}, // pogospring + {0, 0}, // superring + {0, 0}, // kitchensink + {0, 0}, // droptarget + {0, 0}, // gardentop + {0, 0}, // gachabom + {35, 2}, // dualsneaker + {0, 0}, // triplesneaker + {0, 0}, // triplebanana + {35, 2}, // tripleorbinaut + {0, 0}, // quadorbinaut + {35, 2}, // dualjawz + {0, 0}, // triplegachabom +}; + + +static UINT8 K_KartLegacyBattleOdds[NUMKARTRESULTS-1][2] = { { 0, 1 }, // Sneaker { 0, 0 }, // Rocket Sneaker @@ -143,40 +217,6 @@ static UINT8 K_KartItemOddsBattle[NUMKARTRESULTS-1][2] = { 2, 0 } // Gachabom x3 }; -static UINT8 K_KartItemOddsSpecial[NUMKARTRESULTS-1][4] = -{ - { 1, 1, 0, 0 }, // Sneaker - { 0, 0, 0, 0 }, // Rocket Sneaker - { 0, 0, 0, 0 }, // Invincibility - { 0, 0, 0, 0 }, // Banana - { 0, 0, 0, 0 }, // Eggman Monitor - { 1, 1, 1, 0 }, // Orbinaut - { 1, 1, 0, 0 }, // Jawz - { 0, 0, 0, 0 }, // Mine - { 0, 0, 0, 0 }, // Land Mine - { 0, 0, 0, 0 }, // Ballhog - { 0, 0, 0, 1 }, // Self-Propelled Bomb - { 0, 0, 0, 0 }, // Grow - { 0, 0, 0, 0 }, // Shrink - { 0, 0, 0, 0 }, // Lightning Shield - { 0, 0, 0, 0 }, // Bubble Shield - { 0, 0, 0, 0 }, // Flame Shield - { 0, 0, 0, 0 }, // Hyudoro - { 0, 0, 0, 0 }, // Pogo Spring - { 0, 0, 0, 0 }, // Super Ring - { 0, 0, 0, 0 }, // Kitchen Sink - { 0, 0, 0, 0 }, // Drop Target - { 0, 0, 0, 0 }, // Garden Top - { 0, 0, 0, 0 }, // Gachabom - { 0, 0, 1, 1 }, // Sneaker x2 - { 0, 0, 0, 0 }, // Sneaker x3 - { 0, 0, 0, 0 }, // Banana x3 - { 0, 0, 1, 1 }, // Orbinaut x3 - { 0, 0, 0, 0 }, // Orbinaut x4 - { 0, 0, 1, 1 }, // Jawz x2 - { 0, 0, 0, 0 } // Gachabom x3 -}; - static kartitems_t K_KartItemReelSpecialEnd[] = { KITEM_SUPERRING, @@ -423,13 +463,14 @@ static UINT32 K_UndoMapScaling(UINT32 distance) as well as Frantic Items. Input Arguments:- + player - The player to get the distance of. distance - Original distance. numPlayers - Number of players in the game. Return:- New distance after scaling. --------------------------------------------------*/ -static UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers) +static UINT32 K_ScaleItemDistance(const player_t* player, UINT32 distance, UINT8 numPlayers) { if (franticitems == true) { @@ -443,6 +484,9 @@ static UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers) FRACUNIT + (K_ItemOddsScale(numPlayers) / 2) ); + // Distance is reduced based on the player's exp + // distance = FixedMul(distance, player->exp); + return distance; } @@ -509,7 +553,7 @@ UINT32 K_GetItemRouletteDistance(const player_t *player, UINT8 numPlayers) } pdis = K_UndoMapScaling(pdis); - pdis = K_ScaleItemDistance(pdis, numPlayers); + pdis = K_ScaleItemDistance(player, pdis, numPlayers); if (player->bot && (player->botvars.rival || cv_levelskull.value)) { @@ -578,9 +622,9 @@ static boolean K_DenyAutoRouletteOdds(kartitems_t item) } /*-------------------------------------------------- - static fixed_t K_AdjustSPBOdds(const itemroulette_t *roulette, UINT8 position) + static fixed_t K_PercentSPBOdds(const itemroulette_t *roulette, UINT8 position) - Adjust odds of SPB according to distances of first and + Provide odds of SPB according to distances of first and second place players. Input Arguments:- @@ -592,7 +636,7 @@ static boolean K_DenyAutoRouletteOdds(kartitems_t item) Return:- New item odds. --------------------------------------------------*/ -static fixed_t K_AdjustSPBOdds(const itemroulette_t *roulette, UINT8 position) +static fixed_t K_PercentSPBOdds(const itemroulette_t *roulette, UINT8 position) { I_Assert(roulette != NULL); @@ -605,7 +649,6 @@ static fixed_t K_AdjustSPBOdds(const itemroulette_t *roulette, UINT8 position) { 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) @@ -618,382 +661,32 @@ static fixed_t K_AdjustSPBOdds(const itemroulette_t *roulette, UINT8 position) multiplier = FRACUNIT; } - return FixedMul(maxOdds, multiplier); + return multiplier; } } -typedef struct { - boolean powerItem; - boolean cooldownOnStart; - boolean notNearEnd; - - // gameplay state - boolean rival; // player is a bot Rival -} itemconditions_t; /*-------------------------------------------------- - static fixed_t K_AdjustItemOddsToConditions(fixed_t newOdds, const itemconditions_t *conditions, const itemroulette_t *roulette) - - Adjust item odds to certain group conditions. - - Input Arguments:- - newOdds - The item odds to adjust. - conditions - The conditions state. - roulette - The roulette data that we intend to - insert this item into. - - Return:- - New item odds. ---------------------------------------------------*/ -static fixed_t K_AdjustItemOddsToConditions(fixed_t newOdds, const itemconditions_t *conditions, const itemroulette_t *roulette) -{ - // None if this applies outside of Race modes (for now?) - if ((gametyperules & GTR_CIRCUIT) == 0) - { - return newOdds; - } - - if ((conditions->cooldownOnStart == true) && (leveltime < (30*TICRATE) + starttime)) - { - // This item should not appear at the beginning of a race. (Usually really powerful crowd-breaking items) - newOdds = 0; - } - else if ((conditions->notNearEnd == true) && (roulette != NULL && roulette->baseDist < ENDDIST)) - { - // This item should not appear at the end of a race. (Usually trap items that lose their effectiveness) - newOdds = 0; - } - else if (conditions->powerItem == true) - { - // This item is a "power item". This activates "frantic item" toggle related functionality. - if (franticitems == true) - { - // First, power items multiply their odds by 2 if frantic items are on; easy-peasy. - newOdds *= 2; - } - - if (conditions->rival == true) - { - // The Rival bot gets frantic-like items, also :p - newOdds *= 2; - } - - if (roulette != NULL) - { - newOdds = FixedMul(newOdds, FRACUNIT + K_ItemOddsScale(roulette->playing)); - } - } - - return newOdds; -} - -/*-------------------------------------------------- - INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette, UINT8 pos, kartitems_t item) + INT32 K_KartGetBattleOdds(const player_t *player, 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) + +INT32 K_KartGetBattleOdds(const player_t *player, UINT8 pos, kartitems_t item) { - boolean bot = false; - UINT8 position = 0; - - itemconditions_t conditions = { - .powerItem = false, - .cooldownOnStart = false, - .notNearEnd = false, - .rival = false, - }; - fixed_t newOdds = 0; I_Assert(item > KITEM_NONE); // too many off by one scenarioes. I_Assert(item < NUMKARTRESULTS); - if (player != NULL) - { - bot = player->bot; - conditions.rival = (bot == true && (player->botvars.rival || cv_levelskull.value)); - position = player->position; - } - - if (K_ItemEnabled(item) == false) - { - return 0; - } - - if (K_GetItemCooldown(item) > 0) - { - // Cooldown is still running, don't give another. - return 0; - } - - /* - if (bot) - { - // TODO: Item use on bots should all be passed-in functions. - // Instead of manually inserting these, it should return 0 - // for any items without an item use function supplied - - switch (item) - { - case KITEM_SNEAKER: - break; - default: - return 0; - } - } - */ - (void)bot; - - if (K_DenyShieldOdds(item)) - { - return 0; - } - - if (roulette && roulette->autoroulette == true) - { - if (K_DenyAutoRouletteOdds(item)) - { - return 0; - } - } - - if (gametype == GT_BATTLE) - { - I_Assert(pos < 2); // DO NOT allow positions past the bounds of the table - newOdds = K_KartItemOddsBattle[item-1][pos]; - } - else if (specialstageinfo.valid == true) - { - I_Assert(pos < 4); // Ditto - newOdds = K_KartItemOddsSpecial[item-1][pos]; - } - else - { - I_Assert(pos < 8); // Ditto - newOdds = K_KartItemOddsRace[item-1][pos]; - } + I_Assert(pos < 2); // DO NOT allow positions past the bounds of the table + newOdds = K_KartLegacyBattleOdds[item-1][pos]; newOdds <<= FRACBITS; - switch (item) - { - case KITEM_BANANA: - case KITEM_EGGMAN: - case KITEM_SUPERRING: - { - conditions.notNearEnd = true; - break; - } - - case KITEM_ROCKETSNEAKER: - case KITEM_JAWZ: - case KITEM_LANDMINE: - case KITEM_DROPTARGET: - case KITEM_BALLHOG: - case KRITEM_TRIPLESNEAKER: - case KRITEM_TRIPLEORBINAUT: - case KRITEM_QUADORBINAUT: - case KRITEM_DUALJAWZ: - { - conditions.powerItem = true; - break; - } - - case KITEM_HYUDORO: - case KRITEM_TRIPLEBANANA: - { - conditions.powerItem = true; - conditions.notNearEnd = true; - break; - } - - case KITEM_INVINCIBILITY: - case KITEM_MINE: - case KITEM_GROW: - case KITEM_BUBBLESHIELD: - { - conditions.cooldownOnStart = true; - conditions.powerItem = true; - break; - } - - case KITEM_FLAMESHIELD: - case KITEM_GARDENTOP: - { - conditions.cooldownOnStart = true; - conditions.powerItem = true; - conditions.notNearEnd = true; - break; - } - - case KITEM_SPB: - { - conditions.cooldownOnStart = true; - conditions.notNearEnd = true; - - if (roulette != NULL && - (gametyperules & GTR_CIRCUIT) && - specialstageinfo.valid == false) - { - newOdds = K_AdjustSPBOdds(roulette, position); - } - break; - } - - case KITEM_SHRINK: - { - conditions.cooldownOnStart = true; - conditions.powerItem = true; - conditions.notNearEnd = true; - - if (roulette != NULL && - (gametyperules & GTR_CIRCUIT) && - roulette->playing - 1 <= roulette->exiting) - { - return 0; - } - break; - } - - case KITEM_LIGHTNINGSHIELD: - { - conditions.cooldownOnStart = true; - conditions.powerItem = true; - - if ((gametyperules & GTR_CIRCUIT) && spbplace != -1) - { - return 0; - } - break; - } - - default: - { - break; - } - } - - if (newOdds == 0) - { - // Nothing else we want to do with odds matters at this point :p - return newOdds; - } - - newOdds = FixedInt(FixedRound(K_AdjustItemOddsToConditions(newOdds, &conditions, roulette))); return newOdds; } -/*-------------------------------------------------- - static UINT8 K_FindUseodds(const player_t *player, itemroulette_t *const roulette) - - Gets which item bracket the player is in. - This can be adjusted depending on which - items being turned off. - - Input Arguments:- - player - The player the roulette is for. - roulette - The item roulette data. - - Return:- - The item bracket the player is in, as an - index to the array. ---------------------------------------------------*/ -static UINT8 K_FindUseodds(const player_t *player, itemroulette_t *const roulette) -{ - UINT8 i; - UINT8 useOdds = 0; - UINT8 distTable[14]; - UINT8 distLen = 0; - UINT8 totalSize = 0; - boolean oddsValid[8]; - - for (i = 0; i < 8; i++) - { - UINT8 j; - - if (gametype == GT_BATTLE && i > 1) - { - oddsValid[i] = false; - continue; - } - else if (specialstageinfo.valid == true && i > 3) - { - oddsValid[i] = false; - continue; - } - - for (j = 1; j < NUMKARTRESULTS; j++) - { - if (K_KartGetItemOdds(player, roulette, i, j) > 0) - { - break; - } - } - - oddsValid[i] = (j < NUMKARTRESULTS); - } - -#define SETUPDISTTABLE(odds, num) \ - totalSize += num; \ - if (oddsValid[odds]) \ - for (i = num; i; --i) \ - distTable[distLen++] = odds; - - if (gametype == GT_BATTLE) // Battle Mode - { - useOdds = 0; - } - else - { - if (specialstageinfo.valid == true) // Special Stages - { - SETUPDISTTABLE(0,2); - SETUPDISTTABLE(1,2); - SETUPDISTTABLE(2,3); - SETUPDISTTABLE(3,1); - } - else - { - SETUPDISTTABLE(0,1); - SETUPDISTTABLE(1,1); - SETUPDISTTABLE(2,1); - SETUPDISTTABLE(3,2); - SETUPDISTTABLE(4,2); - SETUPDISTTABLE(5,3); - SETUPDISTTABLE(6,3); - SETUPDISTTABLE(7,1); - } - - for (i = 0; i < totalSize; i++) - { - fixed_t pos = 0; - fixed_t dist = 0; - UINT8 index = 0; - - if (i == totalSize-1) - { - useOdds = distTable[distLen - 1]; - break; - } - - pos = ((i << FRACBITS) * distLen) / totalSize; - dist = FixedMul(DISTVAR << FRACBITS, pos) >> FRACBITS; - index = FixedInt(FixedRound(pos)); - - if (roulette->dist <= (unsigned)dist) - { - useOdds = distTable[index]; - break; - } - } - } - -#undef SETUPDISTTABLE - - return useOdds; -} - /*-------------------------------------------------- static boolean K_ForcedSPB(const player_t *player, itemroulette_t *const roulette) @@ -1090,7 +783,6 @@ static void K_InitRoulette(itemroulette_t *const roulette) 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; @@ -1145,12 +837,14 @@ static void K_InitRoulette(itemroulette_t *const roulette) roulette->firstDist = K_UndoMapScaling(K_GetSpecialUFODistance()); } + + // Calculate 2nd's distance from 1st, for SPB if (roulette->firstDist != UINT32_MAX && roulette->secondDist != UINT32_MAX && roulette->secondDist > roulette->firstDist) { roulette->secondToFirst = roulette->secondDist - roulette->firstDist; - roulette->secondToFirst = K_ScaleItemDistance(roulette->secondToFirst, 16 - roulette->playing); // Reversed scaling + roulette->secondToFirst = K_ScaleItemDistance(&players[i], roulette->secondToFirst, 16 - roulette->playing); // Reversed scaling } } @@ -1310,12 +1004,226 @@ static void K_CalculateRouletteSpeed(itemroulette_t *const roulette) 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) +{ + switch (item) + { + 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: + case KITEM_HYUDORO: + case KRITEM_TRIPLEBANANA: + case KITEM_FLAMESHIELD: + case KITEM_GARDENTOP: + case KITEM_SHRINK: + case KITEM_LIGHTNINGSHIELD: + return true; + default: + return false; + } +} + +static boolean K_IsItemFirstOnly(kartitems_t item) +{ + switch (item) + { + case KITEM_LANDMINE: + case KITEM_LIGHTNINGSHIELD: + case KITEM_HYUDORO: + case KITEM_DROPTARGET: + return true; + default: + return false; + } +} + +static boolean K_IsItemFirstPermitted(kartitems_t item) +{ + if (K_IsItemFirstOnly(item)) + return true; + + switch (item) + { + case KITEM_BANANA: + case KITEM_EGGMAN: + case KITEM_ORBINAUT: + case KITEM_SUPERRING: + return true; + default: + return false; + } +} + +// Maybe for later... +#if 0 +static boolean K_IsItemSpeed(kartitems_t item) +{ + switch (item) + { + case KITEM_SNEAKER: + case KRITEM_DUALSNEAKER: + case KRITEM_TRIPLESNEAKER: + case KITEM_FLAMESHIELD: + case KITEM_ROCKETSNEAKER: + return true; + default: + return false; + } +} +#endif + +static boolean K_IsItemUselessAlone(kartitems_t item) +{ + switch (item) + { + case KITEM_JAWZ: + case KRITEM_DUALJAWZ: + case KITEM_LIGHTNINGSHIELD: + case KITEM_ORBINAUT: + case KRITEM_TRIPLEORBINAUT: + case KRITEM_QUADORBINAUT: + case KITEM_BALLHOG: + case KITEM_BUBBLESHIELD: + return true; + default: + return false; + } +} + +static boolean K_IsItemSpeed(kartitems_t item) +{ + switch (item) + { + case KITEM_ROCKETSNEAKER: + case KITEM_GROW: + case KITEM_INVINCIBILITY: + case KITEM_SNEAKER: + case KRITEM_DUALSNEAKER: + case KRITEM_TRIPLESNEAKER: + case KITEM_FLAMESHIELD: + case KITEM_SHRINK: + return true; + default: + return false; + } +} + +// Which items are disallowed for this player's specific placement? +static boolean K_ShouldPlayerAllowItem(kartitems_t item, const player_t *player) +{ + if (!(gametyperules & GTR_CIRCUIT)) + return true; + if (specialstageinfo.valid == true) + return true; + + if (player->position == 1) + return K_IsItemFirstPermitted(item); + 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))) + return false; + return !K_IsItemFirstOnly(item); + } +} + +// Which items are disallowed because it's the wrong time for them? +static boolean K_TimingPermitsItem(kartitems_t item, const itemroulette_t *roulette) +{ + if (!(gametyperules & GTR_CIRCUIT)) + return true; + if (specialstageinfo.valid == true) + return true; + + boolean notNearEnd = false; + boolean cooldownOnStart = false; + + switch (item) + { + case KITEM_BANANA: + case KITEM_EGGMAN: + case KITEM_SUPERRING: + { + notNearEnd = true; + break; + } + + case KITEM_HYUDORO: + case KRITEM_TRIPLEBANANA: + { + notNearEnd = true; + break; + } + + case KITEM_INVINCIBILITY: + case KITEM_MINE: + case KITEM_GROW: + case KITEM_BUBBLESHIELD: + { + cooldownOnStart = true; + break; + } + + case KITEM_FLAMESHIELD: + case KITEM_GARDENTOP: + { + cooldownOnStart = true; + notNearEnd = true; + break; + } + + case KITEM_SPB: + { + // In Race, we reintroduce and reenable this item to counter breakaway frontruns. + // No need to roll it if that's not the case. + return false; + break; + } + + case KITEM_SHRINK: + { + cooldownOnStart = true; + notNearEnd = true; + break; + } + + case KITEM_LIGHTNINGSHIELD: + { + cooldownOnStart = true; + if ((gametyperules & GTR_CIRCUIT) && spbplace != -1) + { + return false; + } + break; + } + + default: + break; + } + + if (cooldownOnStart && (leveltime < ((30*TICRATE) + starttime))) + return false; + if (notNearEnd && (roulette != NULL && roulette->baseDist < ENDDIST)) + return false; + + 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. --------------------------------------------------*/ -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 totalSpawnChance = 0; @@ -1324,7 +1232,7 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet UINT8 numItems = 0; kartitems_t singleItem = KITEM_SAD; - size_t i; + size_t i, j; K_InitRoulette(roulette); @@ -1413,7 +1321,7 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet // every item in the game! // Create the same item reel given the same inputs. - P_SetRandSeed(PR_ITEM_ROULETTE, ITEM_REEL_SEED); + // P_SetRandSeed(PR_ITEM_ROULETTE, ITEM_REEL_SEED); for (i = 1; i < NUMKARTRESULTS; i++) { @@ -1483,28 +1391,307 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet // 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); + roulette->preexpdist = K_GetItemRouletteDistance(player, roulette->playing); + roulette->dist = roulette->preexpdist; + + if (gametyperules & GTR_CIRCUIT) + roulette->dist = FixedMul(roulette->preexpdist, max(player->exp, FRACUNIT/2)); + + // =============================================================================== + // Dynamic Roulette. Oh boy! + // Alright, here's the broad plan: + // 1: Determine what items are permissible + // 2: Determine the permitted item that's most appropriate for our distance from leader + // 3: Pick that item, then penalize it so it's less likely to be repicked + // 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 + + fixed_t largegamescaler = roulette->playing * 6 + 100; // Spread out item odds in large games for a less insane experience. + UINT32 targetpower = 100 * roulette->dist / largegamescaler; // fill roulette with items around this value! + + 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 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 + boolean permit[NUMKARTRESULTS]; // is this item allowed? + + boolean rival = (player->bot && (player->botvars.rival || cv_levelskull.value)); + boolean filterweakitems = true; // strip unusually weak items from reel? + UINT8 reelsize = 15; // How many items to attempt to add in prepass? + UINT32 humanscaler = 250; // Scaler that converts "useodds" style distances in odds tables to raw distances. Affects general item distance scale. + + // == 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++) + { + if (!K_TimingPermitsItem(i, roulette)) + permit[i] = false; + else if (!K_ShouldPlayerAllowItem(i, player)) + permit[i] = false; + else if (K_GetItemCooldown(i)) + permit[i] = false; + else if (!K_ItemEnabled(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; + } + + // == ODDS TIME + // Set up the right item odds for the gametype we're in. + + UINT32 maxpower = 0; // Clamp target power to the lowest item that exists, or some of the math gets hard to reason about. for (i = 1; i < NUMKARTRESULTS; i++) { - spawnChance[i] = ( - totalSpawnChance += K_KartGetItemOdds(player, roulette, roulette->useOdds, i) - ); + // NOTE: Battle odds are underspecified, we don't invoke roulettes in this mode! + if (gametyperules & GTR_BUMPERS) + { + powers[i] = humanscaler * K_DynamicItemOddsBattle[i-1][0]; + dupetolerance[i] = K_DynamicItemOddsBattle[i-1][1]; + filterweakitems = false; + } + else if (specialstageinfo.valid == true) + { + powers[i] = humanscaler * K_DynamicItemOddsSpecial[i-1][0]; + dupetolerance[i] = K_DynamicItemOddsSpecial[i-1][1]; + reelsize = 8; // Smaller roulette in Special because there are much fewer standard items. + filterweakitems = false; + } + else + { + powers[i] = humanscaler * K_DynamicItemOddsRace[i-1][0]; + dupetolerance[i] = K_DynamicItemOddsRace[i-1][1]; + } + + maxpower = max(maxpower, powers[i]); } - if (totalSpawnChance == 0) + targetpower = min(maxpower, targetpower); // Make sure that we don't fall out of the bottom of the odds table. + + // == 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++) { - // 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. + if (powers[i] == 0) + { + permit[i] = false; + } + } + + // == 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++) + { + candidates[i] = 0; + deltas[i] = min(targetpower - powers[i], powers[i] - targetpower); + } + + // == LONELINESS DETECTION + // 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? + // This will set the "loneliness" percentage to be used later. + UINT32 lonelinessThreshold = 3*DISTVAR; // How far away can we be before items are considered useless? + UINT32 toAttacker = lonelinessThreshold; // Distance to the player trying to kill us. + UINT32 toDefender = lonelinessThreshold; // Distance to the player we are trying to kill. + fixed_t loneliness = 0; + + if (player->position > 1) // Loneliness is expected when frontrunnning, don't influence their item table. + { + if ((gametyperules & GTR_CIRCUIT) && specialstageinfo.valid == false) + { + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] == false || players[i].spectator == true || players[i].exiting) + continue; + + if (players[i].position == player->position + 1) + toAttacker = K_UndoMapScaling(players[i].distancetofinish - player->distancetofinish); + + if (players[i].position == player->position - 1) + toDefender = K_UndoMapScaling(player->distancetofinish - players[i].distancetofinish); + } + } + + // Your relationship to each closest player counts for half, but will be eased later. + // If you're far from an attacker but close to a defender, that Ballhog is still useful! + loneliness += min(FRACUNIT/2, FRACUNIT * toAttacker / lonelinessThreshold / 2); + loneliness += min(FRACUNIT/2, FRACUNIT * toDefender / lonelinessThreshold / 2); + + // Give interaction items a nudge against initial selection if you're lonely.. + for (i = 1; i < NUMKARTRESULTS; i++) + { + if (K_IsItemUselessAlone(i)) + { + deltas[i] = Easing_InCubic(loneliness, deltas[i], deltas[i] + (2*DISTVAR)); + } + } + } + + // == INTRODUCE TRYHARD-EATING PREDATOR + // If the frontrunner's making a major breakaway, "break the rules" + // 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++) + { + UINT32 lowestdelta = INT32_MAX; + size_t bestitem = 0; + + // Each rep, get the legal item with the lowest delta... + for (j = 1; j < NUMKARTRESULTS; j++) + { + if (!permit[j]) + continue; + + if (lowestdelta > deltas[j]) + { + bestitem = j; + lowestdelta = deltas[j]; + } + } + + // 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) + break; + + // 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]; + + // 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) + deltapenalty = 3 * deltapenalty / 4; + if (K_IsItemPower(bestitem) && franticitems) + 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 (K_IsItemUselessAlone(bestitem)) + deltapenalty = Easing_InCubic(loneliness, deltapenalty, 3*deltapenalty); + + // Draw complex odds debugger. This one breaks down all the calcs in order. + if (cv_kartdebugdistribution.value > 1) + { + UINT16 BASE_X = 18; + UINT16 BASE_Y = 5+12*i; + INT32 FLAGS = V_SNAPTOTOP|V_SNAPTOLEFT; + 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 + 20, BASE_Y, FLAGS, va("%d", dupetolerance[bestitem])); + V_DrawFixedPatch(BASE_X*FRACUNIT, (BASE_Y-7)*FRACUNIT, (FRACUNIT >> 1), FLAGS, K_GetSmallStaticCachedItemPatch(bestitem), NULL); + UINT8 amount = K_ItemResultToAmount(bestitem); + if (amount > 1) + V_DrawThinString(BASE_X, BASE_Y, FLAGS, va("x%d", amount)); + } + + // Add the selected item to our list of candidates and update its working delta. + candidates[bestitem]++; + deltas[bestitem] += deltapenalty; + + // Then update our ongoing average of the reel's power. + totalreelpower += powers[bestitem]; + added++; + } + + // No items?! + if (added == 0) + { + // Guess we're making circles now. + // Just do something that doesn't crash. K_AddItemToReel(player, roulette, singleItem); return; } - // Create the same item reel given the same inputs. - P_SetRandSeed(PR_ITEM_ROULETTE, ITEM_REEL_SEED); + // Frontrunner roulette is precise, no need to filter it. + if (player->position <= 1) + filterweakitems = false; + UINT8 debugcount = 0; // For the "simple" odds debugger. + UINT32 meanreelpower = totalreelpower/max(added, 1); // Average power for the "moth filter". + + // == 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++) + { + // If an item is far too week for this reel, reject it. + // 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 + // feels exciting, low-rolling feels punishing! + 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]) + { + UINT16 BASE_X = 280; + UINT16 BASE_Y = 5+12*debugcount; + INT32 FLAGS = V_SNAPTOTOP|V_SNAPTORIGHT; + 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+24, FLAGS, va("%d", toDefender)); + V_DrawThinString(BASE_X - 12, 5+36, FLAGS, va("%d", loneliness)); + 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); + UINT8 amount = K_ItemResultToAmount(i); + if (amount > 1) + V_DrawThinString(BASE_X, BASE_Y, FLAGS, va("x%d", amount)); + if (reject) + V_DrawThinString(BASE_X, BASE_Y, FLAGS|V_60TRANS, va("WEAK")); + debugcount++; + } + + // Okay, apply the rejection now. + if (reject) + candidates[i] = 0; + + // Bump totalSpawnChance, write that rolling counter, and move on. + spawnChance[i] = ( + totalSpawnChance += candidates[i] + ); + } + + 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! + + // == 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) { rngRoll = P_RandomKey(PR_ITEM_ROULETTE, totalSpawnChance); @@ -1517,7 +1704,6 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet for (; i < NUMKARTRESULTS; i++) { - // Be sure to fix the remaining items' odds too. if (spawnChance[i] > 0) { spawnChance[i]--; @@ -1538,7 +1724,7 @@ void K_StartItemRoulette(player_t *const player, boolean ringbox) itemroulette_t *const roulette = &player->itemRoulette; size_t i; - K_FillItemRouletteData(player, roulette, ringbox); + K_FillItemRouletteData(player, roulette, ringbox, false); if (roulette->autoroulette) roulette->index = P_RandomRange(PR_AUTOROULETTE, 0, roulette->itemListLen - 1); diff --git a/src/k_roulette.h b/src/k_roulette.h index 058562cb6..03a4adb22 100644 --- a/src/k_roulette.h +++ b/src/k_roulette.h @@ -78,17 +78,14 @@ botItemPriority_e K_GetBotItemPriority(kartitems_t result); /*-------------------------------------------------- - INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette, UINT8 pos, kartitems_t item); + INT32 K_KartGetBattleOdds(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). + Gets legacy item priority. + Currently used only for Battle monitors/spawners. 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. @@ -97,11 +94,11 @@ botItemPriority_e K_GetBotItemPriority(kartitems_t result); into the roulette. --------------------------------------------------*/ -INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette, UINT8 pos, kartitems_t item); +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 initially created. This function needs to be @@ -113,12 +110,13 @@ INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette, Can be NULL for generic use. roulette - The roulette data struct to fill out. 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:- 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); /*-------------------------------------------------- diff --git a/src/k_tally.cpp b/src/k_tally.cpp index ee9b0373d..d2783accb 100644 --- a/src/k_tally.cpp +++ b/src/k_tally.cpp @@ -36,6 +36,7 @@ #include "r_fps.h" #include "g_party.h" #include "g_input.h" +#include "k_objects.h" boolean level_tally_t::UseBonuses(void) { @@ -343,7 +344,7 @@ void level_tally_t::Init(player_t *player) if ((gametypes[gt]->rules & GTR_CIRCUIT) == GTR_CIRCUIT) { laps = player->lapPoints; - totalLaps = numlaps; + totalLaps = numlaps + numlaps * Obj_GetCheckpointCount(); if (inDuel == false) { diff --git a/src/mobj_list.hpp b/src/mobj_list.hpp index ec277adf5..bf46e7e38 100644 --- a/src/mobj_list.hpp +++ b/src/mobj_list.hpp @@ -37,6 +37,7 @@ struct MobjList { ptr->next(front()); front(ptr); + count_++; } void erase(T* node) @@ -45,6 +46,7 @@ struct MobjList { front(node->next()); node->next(nullptr); + count_--; return; } @@ -69,6 +71,7 @@ struct MobjList { prev->next(node->next()); node->next(nullptr); + count_--; break; } } @@ -77,9 +80,12 @@ struct MobjList auto begin() const { return view().begin(); } auto end() const { return view().end(); } + auto count() { return count_; } + private: void front(T* ptr) { Mobj::ManagedPtr {Head} = ptr; } auto view() const { return MobjListView(front(), [](T* node) { return node->next(); }); } + UINT32 count_ = 0; }; }; // namespace srb2 diff --git a/src/objects/amps.c b/src/objects/amps.c index dab90aba2..89d86344f 100644 --- a/src/objects/amps.c +++ b/src/objects/amps.c @@ -55,7 +55,7 @@ void Obj_AmpsThink (mobj_t *amps) amps->extravalue2++; - speed += amps->extravalue1 * amps->scale/2; + speed += amps->extravalue2 * amps->scale/2; fakez = mo->z + (vert * amps->extravalue1 / AMP_ARCTIME); damper = 1; diff --git a/src/objects/checkpoint.cpp b/src/objects/checkpoint.cpp index 7269e5b79..9b62c9d79 100644 --- a/src/objects/checkpoint.cpp +++ b/src/objects/checkpoint.cpp @@ -17,6 +17,7 @@ #include "../doomdef.h" #include "../doomtype.h" #include "../info.h" +#include "../g_game.h" #include "../k_color.h" #include "../k_kart.h" #include "../k_objects.h" @@ -34,9 +35,17 @@ #include "../sounds.h" #include "../tables.h" +using std::vector; +using std::pair; +using std::min; +using std::max; +using std::clamp; + extern mobj_t* svg_checkpoints; #define checkpoint_id(o) ((o)->thing_args[0]) +#define checkpoint_linetag(o) ((o)->thing_args[1]) +#define checkpoint_extralength(o) ((o)->thing_args[2]) #define checkpoint_other(o) ((o)->target) #define checkpoint_orb(o) ((o)->tracer) #define checkpoint_arm(o) ((o)->hnext) @@ -51,12 +60,14 @@ namespace struct LineOnDemand : line_t { + LineOnDemand(const line_t* line) {} + LineOnDemand(fixed_t x1, fixed_t y1, fixed_t x2, fixed_t y2) : line_t { .v1 = &v1_data_, .dx = x2 - x1, .dy = y2 - y1, - .bbox = {std::max(y1, y2), std::min(y1, y2), std::min(x1, x2), std::max(x1, x2)}, + .bbox = {max(y1, y2), min(y1, y2), min(x1, x2), max(x1, x2)}, }, v1_data_ {.x = x1, .y = y1} { @@ -76,6 +87,12 @@ struct LineOnDemand : line_t bbox[BOXLEFT] <= other.bbox[BOXRIGHT] && bbox[BOXRIGHT] >= other.bbox[BOXLEFT]; } + bool overlaps(const line_t& other) const + { + return bbox[BOXTOP] >= other.bbox[BOXBOTTOM] && bbox[BOXBOTTOM] <= other.bbox[BOXTOP] && + bbox[BOXLEFT] <= other.bbox[BOXRIGHT] && bbox[BOXRIGHT] >= other.bbox[BOXLEFT]; + } + private: vertex_t v1_data_; }; @@ -170,6 +187,30 @@ struct Checkpoint : mobj_t deactivate(); } + // will not work properly after a player enters intoa new lap + INT32 players_passed() + { + INT32 pcount = 0; + for (INT32 i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] && !players[i].spectator && players[i].checkpointId >= id()) + pcount++; + } + return pcount; + } + + boolean top_half_has_passed() + { + INT32 pcount = 0; + INT32 winningpos = 1; + + INT32 nump = D_NumPlayersInRace(); + winningpos = nump / 2; + winningpos += nump % 2; + + return players_passed() >= winningpos; + } + void animate() { orient(); @@ -181,10 +222,11 @@ struct Checkpoint : mobj_t if (!clip_var()) { - speed(speed() - FixedDiv(speed() / 50, std::max(speed_multiplier(), 1))); + speed(speed() - FixedDiv(speed() / 50, max(speed_multiplier(), 1))); } } - else if (!activated()) + + if (!top_half_has_passed()) { sparkle_between(0); } @@ -193,7 +235,7 @@ struct Checkpoint : mobj_t void twirl(angle_t dir, fixed_t multiplier) { var(0); - speed_multiplier(std::clamp(multiplier, kMinSpeedMultiplier, kMaxSpeedMultiplier)); + speed_multiplier(clamp(multiplier, kMinSpeedMultiplier, kMaxSpeedMultiplier)); speed(FixedDiv(kBaseSpeed, speed_multiplier())); reverse(AngleDeltaSigned(angle_to_other(), dir) > 0); @@ -266,7 +308,7 @@ private: kMinPivotDelay ); - return to_angle(FixedDiv(std::max(var(), pos) - pos, FRACUNIT - pos)) / 4; + return to_angle(FixedDiv(max(var(), pos) - pos, FRACUNIT - pos)) / 4; } void orient() @@ -304,7 +346,7 @@ private: } } - void spawn_sparkle(const vector3_t& pos, fixed_t xy_momentum, fixed_t z_momentum, angle_t dir) + void spawn_sparkle(const vector3_t& pos, fixed_t xy_momentum, fixed_t z_momentum, angle_t dir, skincolornum_t color = SKINCOLOR_ULTRAMARINE) { auto rng = [=](int units) { return P_RandomRange(PR_DECORATION, -(units) * scale, +(units) * scale); }; @@ -324,10 +366,10 @@ private: if (xy_momentum) { P_Thrust(p, dir, xy_momentum); - p->momz = P_RandomKey(PR_DECORATION, std::max(z_momentum, 1)); + p->momz = P_RandomKey(PR_DECORATION, max(z_momentum, 1)); p->destscale = 0; p->scalespeed = p->scale / 35; - p->color = SKINCOLOR_ULTRAMARINE; + p->color = color; p->fuse = 0; // Something lags at the start of the level. The @@ -342,7 +384,7 @@ private: } else { - p->color = K_RainbowColor(leveltime); + p->color = color; p->fuse = 2; } } @@ -369,7 +411,8 @@ private: {x + FixedMul(ofs, FCOS(a)), y + FixedMul(ofs, FSIN(a)), z + (kSparkleZ * scale)}, momentum, momentum / 2, - dir + dir, + activated() ? SKINCOLOR_GREEN : SKINCOLOR_ULTRAMARINE ); } } @@ -402,14 +445,93 @@ struct CheckpointManager auto begin() { return list_.begin(); } auto end() { return list_.end(); } - auto find(INT32 id) { return std::find_if(begin(), end(), [id](Checkpoint* chk) { return chk->id() == id; }); } + auto find_checkpoint(INT32 id) { + auto it = find_if(list_.begin(), list_.end(), [id](auto pair) { return pair.first->id() == id; }); + if (it != list_.end()) + { + return it->first; + } + return static_cast(nullptr); + } - void push_front(Checkpoint* chk) { list_.push_front(chk); } + // auto find_pair(Checkpoint* chk) { + // pair> retpair; + // auto it = find_if(list_.begin(), list_.end(), [chk](auto pair) { return pair.first == chk; }); + // if (it != list_.end()) + // { + // retpair = *it; + // return retpair; + // } + // return static_cast>>(nullptr); + // } - void erase(Checkpoint* chk) { list_.erase(chk); } + void remove_checkpoint(mobj_t* end) + { + auto chk = static_cast(end); + auto it = find_if(list_.begin(), list_.end(), [&](auto pair) { return pair.first == chk; }); + if (it != list_.end()) + { + list_.erase(it); + } + } + + void link_checkpoint(mobj_t* end) + { + auto chk = static_cast(end); + auto id = chk->id(); + if (chk->spawnpoint && id == 0) + { + auto msg = fmt::format( + "Checkpoint thing (index #{}, thing type {}) has an invalid ID! ID must not be 0.\n", + chk->spawnpoint - mapthings, + chk->spawnpoint->type + ); + CONS_Alert(CONS_WARNING, "%s", msg.c_str()); + return; + } + + if (auto other = find_checkpoint(id)) + { + if (chk->spawnpoint && other->spawnpoint && chk->spawnpoint->angle != other->spawnpoint->angle) + { + auto msg = fmt::format( + "Checkpoints things with ID {} (index #{} and #{}, thing type {}) do not have matching angles.\n", + chk->id(), + chk->spawnpoint - mapthings, + other->spawnpoint - mapthings, + chk->spawnpoint->type + ); + CONS_Alert(CONS_WARNING, "%s", msg.c_str()); + return; + } + other->other(chk); + chk->other(other); + } + else // Checkpoint isn't in the list, find any associated tagged lines and make the pair + { + vector checklines; + if (checkpoint_linetag(chk)) + { + INT32 li; + INT32 tag = checkpoint_linetag(chk); + TAG_ITER_LINES(tag, li) + { + line_t* line = lines + li; + checklines.push_back(line); + } + } + list_.emplace_back(chk, move(checklines)); + } + + chk->gingerbread(); + } + + void clear() { list_.clear(); } + + auto count() { return list_.size(); } private: - srb2::MobjList list_; + vector>> list_; }; CheckpointManager g_checkpoints; @@ -418,54 +540,15 @@ CheckpointManager g_checkpoints; void Obj_LinkCheckpoint(mobj_t* end) { - auto chk = static_cast(end); - - if (chk->spawnpoint && chk->id() == 0) - { - auto msg = fmt::format( - "Checkpoint thing (index #{}, thing type {}) has an invalid ID! ID must not be 0.\n", - chk->spawnpoint - mapthings, - chk->spawnpoint->type - ); - CONS_Alert(CONS_WARNING, "%s", msg.c_str()); - return; - } - - if (auto it = g_checkpoints.find(chk->id()); it != g_checkpoints.end()) - { - Checkpoint* other = *it; - - if (chk->spawnpoint && other->spawnpoint && chk->spawnpoint->angle != other->spawnpoint->angle) - { - auto msg = fmt::format( - "Checkpoints things with ID {} (index #{} and #{}, thing type {}) do not have matching angles.\n", - chk->id(), - chk->spawnpoint - mapthings, - other->spawnpoint - mapthings, - chk->spawnpoint->type - ); - CONS_Alert(CONS_WARNING, "%s", msg.c_str()); - return; - } - - other->other(chk); - chk->other(other); - } - else - { - g_checkpoints.push_front(chk); - } - - chk->gingerbread(); + g_checkpoints.link_checkpoint(end); } void Obj_UnlinkCheckpoint(mobj_t* end) { auto chk = static_cast(end); - - g_checkpoints.erase(chk); - + g_checkpoints.remove_checkpoint(end); P_RemoveMobj(chk->orb()); + P_RemoveMobj(chk->arm()); } void Obj_CheckpointThink(mobj_t* end) @@ -480,39 +563,64 @@ void Obj_CheckpointThink(mobj_t* end) chk->animate(); } -void Obj_CrossCheckpoints(player_t* player, fixed_t old_x, fixed_t old_y) +void __attribute__((optimize("O0"))) Obj_CrossCheckpoints(player_t* player, fixed_t old_x, fixed_t old_y) { LineOnDemand ray(old_x, old_y, player->mo->x, player->mo->y, player->mo->radius); - auto it = std::find_if( + auto it = find_if( g_checkpoints.begin(), g_checkpoints.end(), - [&](const Checkpoint* chk) + [&](auto chkpair) { + Checkpoint* chk = chkpair.first; if (!chk->valid()) { return false; } - LineOnDemand gate = chk->crossing_line(); + LineOnDemand* gate; + + if (chkpair.second.empty()) + { + LineOnDemand dyngate = chk->crossing_line(); + if (!ray.overlaps(dyngate)) + return false; + gate = &dyngate; + } + else + { + auto it = find_if( + chkpair.second.begin(), + chkpair.second.end(), + [&](const line_t* line) + { + return ray.overlaps(*line); + } + ); + + if (it == chkpair.second.end()) + { + return false; + } + + line_t* line = *it; + gate = static_cast(line); + } // Check if the bounding boxes of the two lines // overlap. This relies on the player movement not // being so large that it creates an oversized box, // but thankfully that doesn't seem to happen, under // normal circumstances. - if (!ray.overlaps(gate)) - { - return false; - } - INT32 side = P_PointOnLineSide(player->mo->x, player->mo->y, &gate); - INT32 oldside = P_PointOnLineSide(old_x, old_y, &gate); + INT32 side = P_PointOnLineSide(player->mo->x, player->mo->y, gate); + INT32 oldside = P_PointOnLineSide(old_x, old_y, gate); if (side == oldside) { // Did not cross. return false; + } return true; @@ -524,41 +632,58 @@ void Obj_CrossCheckpoints(player_t* player, fixed_t old_x, fixed_t old_y) return; } - Checkpoint* chk = *it; + Checkpoint* chk = it->first; - if (chk->activated()) + if (player->checkpointId == chk->id()) { return; } - for (Checkpoint* chk : g_checkpoints) + if (player->position <= 1) { - if (chk->valid()) - { - // Swing down any previously passed checkpoints. - // TODO: this could look weird in multiplayer if - // other players cross different checkpoints. - chk->untwirl(); - chk->other()->untwirl(); - } + angle_t direction = R_PointToAngle2(old_x, old_y, player->mo->x, player->mo->y); + fixed_t speed_multiplier = FixedDiv(player->speed, K_GetKartSpeed(player, false, false)); + chk->twirl(direction, speed_multiplier); + chk->other()->twirl(direction, speed_multiplier); } - angle_t direction = R_PointToAngle2(old_x, old_y, player->mo->x, player->mo->y); - fixed_t speed_multiplier = FixedDiv(player->speed, K_GetKartSpeed(player, false, false)); - - chk->twirl(direction, speed_multiplier); - chk->other()->twirl(direction, speed_multiplier); + if (gametyperules & GTR_CHECKPOINTS) + { + for (auto chkpair : g_checkpoints) + { + Checkpoint* chk = chkpair.first; + if (chk->valid()) + { + chk->untwirl(); + chk->other()->untwirl(); + } + } + } S_StartSound(player->mo, sfx_s3k63); player->checkpointId = chk->id(); + + if (D_NumPlayersInRace() > 1 && !K_IsPlayerLosing(player)) + { + if (player->position == 1) + { + player->lapPoints += 2; + } + else + { + player->lapPoints += 1; + } + } + + player->exp += K_GetExpAdjustment(player); + + K_UpdatePowerLevels(player, player->laps, false); } -mobj_t *Obj_FindCheckpoint(INT32 id) +mobj_t* Obj_FindCheckpoint(INT32 id) { - auto it = g_checkpoints.find(id); - - return it != g_checkpoints.end() ? *it : nullptr; + return g_checkpoints.find_checkpoint(id); } boolean Obj_GetCheckpointRespawnPosition(const mobj_t* mobj, vector3_t* return_pos) @@ -593,3 +718,27 @@ void Obj_ActivateCheckpointInstantly(mobj_t* mobj) chk->other()->activate(); } } + +// Returns a count of checkpoint gates, not objects +UINT32 Obj_GetCheckpointCount() +{ + return g_checkpoints.count(); +} + +void Obj_ClearCheckpoints() +{ + g_checkpoints.clear(); +} + +void Obj_DeactivateCheckpoints() +{ + for (auto chkpair : g_checkpoints) + { + Checkpoint* chk = chkpair.first; + if (chk->valid()) + { + chk->untwirl(); + chk->other()->untwirl(); + } + } +} \ No newline at end of file diff --git a/src/p_inter.c b/src/p_inter.c index 5a95788d5..52a0da4f5 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -3092,11 +3092,6 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da { K_DoPowerClash(target, inflictor); - if (inflictor->type != MT_PLAYER) - { - K_SpawnAmps(player, 5, inflictor); - } - if (inflictor->type == MT_SUPER_FLICKY) { Obj_BlockSuperFlicky(inflictor); @@ -3565,4 +3560,6 @@ void P_PlayerRingBurst(player_t *player, INT32 num_rings) { P_FlingBurst(player, fa, MT_DEBTSPIKE, 0, 3 * FRACUNIT / 2, i++); } + + K_DefensiveOverdrive(player); } diff --git a/src/p_mobj.c b/src/p_mobj.c index cf9b73a10..0a6d5c9fa 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -12572,8 +12572,6 @@ static boolean P_AllowMobjSpawn(mapthing_t* mthing, mobjtype_t i) return false; break; case MT_CHECKPOINT_END: - if (!(gametyperules & GTR_CHECKPOINTS)) - return false; break; default: break; diff --git a/src/p_saveg.c b/src/p_saveg.c index 8ca5c3d09..c30c741f2 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -281,6 +281,7 @@ static void P_NetArchivePlayers(savebuffer_t *save) WRITEUINT8(save->p, players[i].laps); WRITEUINT8(save->p, players[i].latestlap); WRITEUINT32(save->p, players[i].lapPoints); + WRITEINT32(save->p, players[i].exp); WRITEINT32(save->p, players[i].cheatchecknum); WRITEINT32(save->p, players[i].checkpointId); @@ -743,7 +744,7 @@ static void P_NetArchivePlayers(savebuffer_t *save) } #endif - WRITEUINT8(save->p, players[i].itemRoulette.useOdds); + WRITEUINT32(save->p, players[i].itemRoulette.preexpdist); WRITEUINT32(save->p, players[i].itemRoulette.dist); WRITEUINT32(save->p, players[i].itemRoulette.index); WRITEUINT8(save->p, players[i].itemRoulette.sound); @@ -937,6 +938,7 @@ static void P_NetUnArchivePlayers(savebuffer_t *save) players[i].laps = READUINT8(save->p); // Number of laps (optional) players[i].latestlap = READUINT8(save->p); players[i].lapPoints = READUINT32(save->p); + players[i].exp = READINT32(save->p); players[i].cheatchecknum = READINT32(save->p); players[i].checkpointId = READINT32(save->p); @@ -1366,7 +1368,7 @@ static void P_NetUnArchivePlayers(savebuffer_t *save) } #endif - players[i].itemRoulette.useOdds = READUINT8(save->p); + players[i].itemRoulette.preexpdist = READUINT32(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); diff --git a/src/p_setup.cpp b/src/p_setup.cpp index 0b6422ee1..e719c755a 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -118,6 +118,7 @@ #include "k_hud.h" // K_ClearPersistentMessages #include "k_endcam.h" #include "k_credits.h" +#include "k_objects.h" // Replay names have time #if !defined (UNDER_CE) @@ -8586,6 +8587,8 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) LUA_InvalidateLevel(); + Obj_ClearCheckpoints(); + for (ss = sectors; sectors+numsectors != ss; ss++) { Z_Free(ss->attached); diff --git a/src/p_spec.c b/src/p_spec.c index db21a7cb1..dd05ebd07 100644 --- a/src/p_spec.c +++ b/src/p_spec.c @@ -2117,6 +2117,14 @@ static void K_HandleLapIncrement(player_t *player) player->lapPoints++; } } + + player->exp += K_GetExpAdjustment(player); + + if (player->position == 1 && !(gametyperules & GTR_CHECKPOINTS)) + { + Obj_DeactivateCheckpoints(); + } + player->checkpointId = 0; } // Set up lap animation vars