Add "Lunatic" and "Maniac" modes

DUDE TOUHOU LMAO

Jokes aside:
* Lunatic = Master difficulty with modifications to make the races
  significantly more difficult:
  * Introduces a "lunaticmode" boolean to grandprixinfo;
    the demoversion has been upped to 0x0010 due to this
  * The bot modifier is, at MINIMUM, 2.0, making them aggressive as hell;
    Rival bots use a 2.5 modifier
  * Bump Spark is always off in this mode
  * RUNNERAUGMENT results have their distances significantly shortened;
    if a rival bot takes the lead, this distance is shortened even FURTHER
    so they don't frontrun against the human player endlessly
  * Alt. Invinc shows up earlier as a sort of "mercy" for human players;
    it would otherwise not show up until the race was effectively over
  * Maniac = Nightmare difficulty (Master at Expert speed)
    with Lunatic's changes
This commit is contained in:
yamamama 2025-12-28 18:03:06 -05:00
parent 271cec1907
commit 474a59ab7c
13 changed files with 157 additions and 18 deletions

View file

@ -97,6 +97,8 @@ CV_PossibleValue_t gpdifficulty_cons_t[] = {
{KARTSPEED_EXPERT, "Expert"},
{KARTGP_MASTER, "Master"},
{KARTGP_NIGHTMARE, "Nightmare"},
{KARTGP_LUNATIC, "Lunatic"},
{KARTGP_MANIAC, "Maniac"},
{0, NULL}
};

View file

@ -187,6 +187,8 @@ extern CV_PossibleValue_t CV_Natural[];
#define KARTSPEED_EXPERT 3
#define KARTGP_MASTER 4 // Not a speed setting, gives hard speed with maxed out bots
#define KARTGP_NIGHTMARE 5 // Not a speed setting, gives expert speed with maxed out bots
#define KARTGP_LUNATIC 6 // Not a speed setting; Master difficulty with some... modifications.
#define KARTGP_MANIAC 7 // Not a speed setting; Nightmare difficulty with some... modifications.
extern CV_PossibleValue_t kartspeed_cons_t[], gpdifficulty_cons_t[];
extern consvar_t cv_execversion;

View file

@ -1957,6 +1957,7 @@ void D_SRB2Main(void)
grandprixinfo.gamespeed = KARTSPEED_HARD;
grandprixinfo.encore = false;
grandprixinfo.masterbots = false;
grandprixinfo.lunaticmode = false;
grandprixinfo.gp = true;
grandprixinfo.roundnum = 0;
@ -2121,7 +2122,7 @@ void D_SRB2Main(void)
if (!gpdifficulty_cons_t[j].strvalue) // reached end of the list with no match
{
j = atoi(sskill); // assume they gave us a skill number, which is okay too
if (j >= KARTSPEED_EASY && j <= KARTGP_NIGHTMARE)
if (j >= KARTSPEED_EASY && j <= KARTGP_MANIAC)
newskill = (INT16)j;
}
@ -2137,6 +2138,18 @@ void D_SRB2Main(void)
grandprixinfo.masterbots = true;
newskill = KARTSPEED_EXPERT;
}
else if (newskill == KARTGP_LUNATIC)
{
grandprixinfo.masterbots = true;
grandprixinfo.lunaticmode = true;
newskill = KARTSPEED_HARD;
}
else if (newskill == KARTGP_MANIAC)
{
grandprixinfo.masterbots = true;
grandprixinfo.lunaticmode = true;
newskill = KARTSPEED_EXPERT;
}
grandprixinfo.gamespeed = newskill;
}
@ -2148,6 +2161,14 @@ void D_SRB2Main(void)
{
newskill = KARTSPEED_EXPERT;
}
else if (newskill == KARTGP_LUNATIC)
{
newskill = KARTSPEED_HARD;
}
else if (newskill == KARTGP_MANIAC)
{
newskill = KARTSPEED_EXPERT;
}
if (newskill != -1)
CV_SetValue(&cv_kartspeed, newskill);

View file

@ -3554,6 +3554,7 @@ static void Command_Map_f(void)
{
grandprixinfo.gamespeed = (cv_kartspeed.value == KARTSPEED_AUTO ? KARTSPEED_NORMAL : cv_kartspeed.value);
grandprixinfo.masterbots = false;
grandprixinfo.lunaticmode = false;
if (option_skill)
{
@ -3573,7 +3574,7 @@ static void Command_Map_f(void)
if (!gpdifficulty_cons_t[j].strvalue) // reached end of the list with no match
{
INT32 num = atoi(COM_Argv(option_skill + 1)); // assume they gave us a skill number, which is okay too
if (num >= KARTSPEED_EASY && num <= KARTGP_NIGHTMARE)
if (num >= KARTSPEED_EASY && num <= KARTGP_MANIAC)
newskill = (INT16)num;
}
@ -3589,6 +3590,18 @@ static void Command_Map_f(void)
grandprixinfo.gamespeed = KARTSPEED_EXPERT;
grandprixinfo.masterbots = true;
}
else if (newskill == KARTGP_LUNATIC)
{
grandprixinfo.gamespeed = KARTSPEED_HARD;
grandprixinfo.masterbots = true;
grandprixinfo.lunaticmode = true;
}
else if (newskill == KARTGP_MANIAC)
{
grandprixinfo.gamespeed = KARTSPEED_EXPERT;
grandprixinfo.masterbots = true;
grandprixinfo.lunaticmode = true;
}
else
{
grandprixinfo.gamespeed = newskill;
@ -8483,6 +8496,13 @@ static void KartBumpSpark_OnChange(void)
return;
}
if (grandprixinfo.lunaticmode)
{
CONS_Printf("No, no - lunatics don't need Bump Spark!\n");
bumpsparkactive = 0;
return;
}
if (leveltime < starttime)
{
CONS_Printf(M_GetText("Bump spark type has been changed to \"%s\".\n"), cv_kartbumpspark.string);

View file

@ -110,7 +110,7 @@ demoghost *ghosts = NULL;
// DEMO RECORDING
//
#define DEMOVERSION 0x000F
#define DEMOVERSION 0x0010
#define DEMOHEADER "\xF0" "BlanReplay" "\x0F"
#define DF_GHOST 0x01 // This demo contains ghost data too!
@ -268,6 +268,7 @@ typedef struct
struct // DF_GRANDPRIX
{
boolean masterbots;
boolean lunaticmode;
UINT8 gamespeed;
UINT8 eventmode;
} grandprix;
@ -704,6 +705,7 @@ static headerstatus_e G_ReadDemoHeader(UINT8 *dp, demoheader_t *header)
boolean rapreset = true; // + extended serverinfo length
boolean dubs = true; // Multiple voices
boolean availabilities = true; // Store player availabilities
boolean gplunatic = true; // Grand Prix: Lunatic mode
INT32 i;
// these may not be present in old demo formats, so initialize them
@ -745,6 +747,9 @@ static headerstatus_e G_ReadDemoHeader(UINT8 *dp, demoheader_t *header)
/* FALLTHRU */
case 0x000E:
availabilities = false;
/* FALLTHRU */
case 0x000F:
gplunatic = false;
break;
default: // too old, cannot support.
@ -754,7 +759,7 @@ static headerstatus_e G_ReadDemoHeader(UINT8 *dp, demoheader_t *header)
else if (!memcmp(startdp, "\xF0" "KartReplay" "\x0F", 12))
{
dubs = rapreset = raflag = false;
serverinfo = availabilities = false;
gplunatic = serverinfo = availabilities = false;
switch (header->demoversion)
{
case 0x0001: // SRB2Kart 1.0.x (only staff ghosts supported)
@ -929,6 +934,10 @@ skipfiles:
{
header->grandprix.gamespeed = READUINT8(dp);
header->grandprix.masterbots = READUINT8(dp) != 0;
if (gplunatic)
header->grandprix.lunaticmode = READUINT8(dp) != 0;
header->grandprix.eventmode = READUINT8(dp);
}
@ -2957,6 +2966,7 @@ void G_BeginRecording(void)
{
WRITEUINT8(demobuf.p, grandprixinfo.gamespeed);
WRITEUINT8(demobuf.p, grandprixinfo.masterbots == true);
WRITEUINT8(demobuf.p, grandprixinfo.lunaticmode == true);
WRITEUINT8(demobuf.p, grandprixinfo.eventmode);
}
@ -3800,6 +3810,7 @@ void G_DoPlayDemo(char *defdemoname)
grandprixinfo.gp = true;
grandprixinfo.gamespeed = header.grandprix.gamespeed;
grandprixinfo.masterbots = header.grandprix.masterbots;
grandprixinfo.lunaticmode = header.grandprix.lunaticmode;
grandprixinfo.eventmode = header.grandprix.eventmode;
}

View file

@ -578,6 +578,10 @@ fixed_t K_BotMapModifier(void)
// fuck it we ball
//return 5*FRACUNIT/10;
// ...with a bit of customization
if (grandprixinfo.lunaticmode == true)
return std::max(static_cast<fixed_t>(BASELUNATICSPEEDMOD), K_TrackModifierMax());
return K_TrackModifierMax();
#if 0
@ -689,13 +693,12 @@ fixed_t K_BotRubberband(const player_t *player)
);
// +/- x0.35
const fixed_t rubberStretchiness = FixedMul(
FixedDiv(
35 * FRACUNIT / 100,
K_GetKartGameSpeedScalar(gamespeed)
),
K_BotMapModifier()
);
const fixed_t rubberStretchiness =
FixedMul(FixedDiv(35 * FRACUNIT / 100, K_GetKartGameSpeedScalar(gamespeed)),
((grandprixinfo.lunaticmode == true) && (player->botvars.rival == true))
? std::max(BASELUNATICRIVALSPEEDMOD,
K_BotMapModifier()) // The rival is faster on Lunatic mode.
: K_BotMapModifier());
// Lv. 1: x0.4 min
// Lv. MAX: x0.85 min

View file

@ -57,6 +57,12 @@ extern consvar_t cv_botdrifting;
#define MAXDRIFTSKILL (FRACUNIT/2)
// Minimum bot complexity for Lunatic mode.
#define BASELUNATICSPEEDMOD (2 * FRACUNIT)
// 2.5
#define BASELUNATICRIVALSPEEDMOD (5 * FRACUNIT / 2)
typedef enum
{
DRIFTSTATE_AUTO,

View file

@ -32,6 +32,7 @@ extern struct grandprixinfo
UINT8 gamespeed; ///< Copy of gamespeed, just to make sure you can't cheat it with cvars
boolean encore; ///< Ditto, but for encore mode
boolean masterbots; ///< If true, all bots should be max difficulty (Master Mode)
boolean lunaticmode; ///< If true, make this GP especially difficult (Lunatic Mode)
boolean initalize; ///< If true, we need to initialize a new session.
boolean wonround; ///< If false, then we retry the map instead of going to the next.
UINT8 eventmode; ///< See GPEVENT_ constants

View file

@ -37,6 +37,7 @@
#include "k_kart.h"
#include "k_waypoint.h"
#include "k_director.h"
#include "k_grandprix.h"
#include "k_cluster.hpp"
#include "k_itemlist.hpp"
#include "k_items.h"
@ -621,6 +622,8 @@ void K_SetIndirectItemCooldown(tic_t cooldown)
}
}
#define LUNATIC_RUNNERAUG_CRUNCHER (29127) // FRACUNIT / 2.25
static INT32 GetItemOdds(kartroulette_t *roulette, kartresult_t *result, UINT8 *forceme)
{
INT32 newodds;
@ -791,8 +794,23 @@ static INT32 GetItemOdds(kartroulette_t *roulette, kartresult_t *result, UINT8 *
if ((flags & KRF_RUNNERAUGMENT) && (result->augcvar[aug_idx] != NULL))
{
// These odds get stronger as 1st's frontrun increases.
const INT32 distFromStart = max(secondToFirst - result->augcvar[aug_idx]->value, 0);
const INT32 distRange = (7 * result->augcvar[aug_idx]->value / 2) - result->augcvar[aug_idx]->value;
INT32 runner_distval = result->augcvar[aug_idx]->value;
if (grandprixinfo.lunaticmode == true)
{
// Lunatic Mode: Divide this distance by 2.25 for
// aggressive frontrun prevention.
runner_distval = FixedMul(runner_distval, LUNATIC_RUNNERAUG_CRUNCHER);
}
if (roulette->rival_frontrunner == true)
{
// The rival is frontrunning. Be *especially* vicious against them!
runner_distval = (runner_distval / 2);
}
const INT32 distFromStart = max(secondToFirst - runner_distval, 0);
const INT32 distRange = (7 * runner_distval / 2) - runner_distval;
const INT32 mulMax = 24;
INT32 multiplier = (distFromStart * mulMax) / distRange;
@ -861,6 +879,8 @@ static INT32 GetItemOdds(kartroulette_t *roulette, kartresult_t *result, UINT8 *
return newodds;
}
#undef LUNATIC_RUNNERAUG_CRUNCHER
void K_KartGetItemOdds(kartroulette_t *roulette, INT32 outodds[static MAXKARTRESULTS])
{
// Reset forceme
@ -883,6 +903,7 @@ UINT8 K_FindUseodds(const player_t *player, fixed_t mashed, UINT32 pdis, UINT8 b
UINT8 distlen = 0;
boolean oddsvalid[MAXODDS];
boolean rivalodds = false;
boolean rivalrunner = false;
// Unused now, oops :V
(void)bestbumper;
@ -893,6 +914,12 @@ UINT8 K_FindUseodds(const player_t *player, fixed_t mashed, UINT32 pdis, UINT8 b
rivalodds = true;
}
if (player->bot && player->botvars.rival && player->position <= 1)
{
// A rival is frontrunning!
rivalrunner = true;
}
INT32 itemodds[MAXKARTRESULTS];
kartroulette_t roulette = {
.pdis = pdis,
@ -904,6 +931,7 @@ UINT8 K_FindUseodds(const player_t *player, fixed_t mashed, UINT32 pdis, UINT8 b
.spbrush = spbrush,
.bot = player->bot,
.rival = rivalodds,
.rival_frontrunner = rivalrunner,
.inBottom = K_IsPlayerLosing(player),
};
@ -1466,7 +1494,8 @@ void K_KartItemRoulette(player_t *player, ticcmd_t *cmd)
.mashed = mashed,
.spbrush = spbrush,
.bot = player->bot,
.rival = player->bot && player->botvars.rival,
.rival = ((player->bot && player->botvars.rival) || (K_IsAltShrunk(player))),
.rival_frontrunner = ((player->bot && player->botvars.rival) && (player->position <= 1)),
.inBottom = K_IsPlayerLosing(player),
};
@ -1535,7 +1564,16 @@ void K_SetPlayerItemCooldown(player_t *player, tic_t timer, boolean force)
INT32 KO_AltInvinOdds(INT32 odds, const kartroulette_t *roulette, const kartresult_t *result, UINT8 *forceme)
{
(void)result;
odds = K_KartGetInvincibilityOdds(roulette->clusterDist);
UINT32 cdist = roulette->clusterDist;
if (grandprixinfo.lunaticmode)
{
// I'm tired, boss.
cdist = (9 * cdist / 5);
}
odds = K_KartGetInvincibilityOdds(cdist);
// Special case: if you're SERIOUSLY far behind before the cooldown finishes, ignore it and start forcing,
if (odds >= INVFORCEODDS)
@ -1565,12 +1603,26 @@ INT32 KO_SPBRaceOdds(INT32 odds, const kartroulette_t *roulette, const kartresul
odds = 0;
}
UINT32 raceforce_pdis = roulette->pdis;
if (grandprixinfo.lunaticmode == true)
{
// Multiply by 2.25
raceforce_pdis = FixedMul(raceforce_pdis, 9 * FRACUNIT / 2);
}
if (roulette->rival_frontrunner == true)
{
// Frontrunning rival? Multiply by 1.5
raceforce_pdis = FixedMul(raceforce_pdis, 3 * FRACUNIT / 2);
}
// No forced SPB in 1v1s, it has to be randomly rolled
if (roulette->pingame <= 2)
{
*forceme = 0;
}
else if (K_RaceForceSPB(roulette->playerpos, roulette->pdis)
else if (K_RaceForceSPB(roulette->playerpos, raceforce_pdis)
&& spbplace == -1 && K_GetKartResult("selfpropelledbomb")->cooldown == 0)
{
// Force SPB onto 2nd if they get too far behind.

View file

@ -155,6 +155,7 @@ struct kartroulette_t
boolean spbrush;
boolean bot;
boolean rival;
boolean rival_frontrunner; // Is a Rival bot frontrunning?
boolean inBottom;
// output: which results are being forced into a player's item slot for one reason or another. Higher value = higher priority.

View file

@ -2532,7 +2532,15 @@ UINT16 K_GetInvincibilityTime(player_t *player)
distmul = LEGACYALTINVINMUL;
}
fixed_t clustermul = K_InvincibilityEasing(FixedMul(player->distancefromcluster,distmul));
UINT32 cdist = player->distancefromcluster;
if (grandprixinfo.lunaticmode)
{
// I'm tired, boss.
cdist = (9 * cdist / 5);
}
fixed_t clustermul = K_InvincibilityEasing(FixedMul(cdist,distmul));
UINT16 invintics = FixedMul(BASEINVINTIME, clustermul);
return max(MININVINTIME, invintics);

View file

@ -6259,6 +6259,16 @@ INT32 MR_StartGrandPrix(INT32 choice)
grandprixinfo.gamespeed = KARTSPEED_EXPERT;
grandprixinfo.masterbots = true;
break;
case KARTGP_LUNATIC:
grandprixinfo.gamespeed = KARTSPEED_HARD;
grandprixinfo.lunaticmode = true;
grandprixinfo.masterbots = true;
break;
case KARTGP_MANIAC:
grandprixinfo.gamespeed = KARTSPEED_EXPERT;
grandprixinfo.lunaticmode = true;
grandprixinfo.masterbots = true;
break;
default:
CONS_Alert(CONS_WARNING, "Invalid GP difficulty\n");
grandprixinfo.gamespeed = KARTSPEED_NORMAL;

View file

@ -8230,7 +8230,9 @@ static void P_InitLevelSettings(boolean reloadinggamestate)
if (cv_itemlist.value)
itemlistactive = true;
bumpsparkactive = (UINT8)cv_kartbumpspark.value;
// Lunatics don't need Bump Spark!
if (grandprixinfo.lunaticmode == false)
bumpsparkactive = (UINT8)cv_kartbumpspark.value;
antibumptime = (tic_t)cv_kartantibump.value * TICRATE;