Merge branch 'duel' into 'master'

Duel

See merge request kart-krew-dev/ring-racers-internal!2456
This commit is contained in:
Oni VelocitOni 2025-05-25 22:26:49 +00:00
commit 65213e1363
23 changed files with 1145 additions and 119 deletions

View file

@ -783,6 +783,8 @@ consvar_t cv_timelimit = UnsavedNetVar("timelimit", "Default").min_max(1, 30*60,
consvar_t cv_votetime = UnsavedNetVar("votetime", "20").min_max(10, 3600);
consvar_t cv_dueltimelimit = UnsavedNetVar("dueltimelimit", "180").min_max(0, 3600);
consvar_t cv_duelscorelimit = UnsavedNetVar("duelscorelimit", "4").min_max(1, 9);
//
// Online cheats - synced in netgames.

View file

@ -2291,11 +2291,11 @@ void D_SetupVote(INT16 newgametype)
void D_ModifyClientVote(UINT8 player, SINT8 voted)
{
char buf[2];
char buf[3];
char *p = buf;
UINT8 sendPlayer = consoleplayer;
UINT8 sendPlayer = 0;
if (player == UINT8_MAX)
if (player >= MAXPLAYERS)
{
// Special game vote (map anger, duel)
if (!server)
@ -2304,16 +2304,16 @@ void D_ModifyClientVote(UINT8 player, SINT8 voted)
}
}
if (player == UINT8_MAX)
{
// special vote
WRITEUINT8(p, UINT8_MAX);
}
else
{
INT32 i = 0;
WRITEUINT8(p, player);
// Context value -- if context has changed, then discard vote update.
// This is to prevent votes being registered from different vote types.
// Currently used for Duel vs Normal votes.
WRITEUINT8(p, Y_VoteContext());
WRITEUINT8(p, player);
if (player <= MAXPLAYERS)
{
INT32 i;
for (i = 0; i <= splitscreen; i++)
{
if (g_localplayers[i] == player)
@ -5636,30 +5636,35 @@ static void Got_SetupVotecmd(const UINT8 **cp, INT32 playernum)
static void Got_ModifyVotecmd(const UINT8 **cp, INT32 playernum)
{
UINT8 context = READUINT8(*cp);
UINT8 targetID = READUINT8(*cp);
SINT8 vote = READSINT8(*cp);
if (targetID == UINT8_MAX)
if (context != Y_VoteContext())
{
if (playernum != serverplayer) // server-only special vote
// Silently discard. Server changed the
// vote type as we were sending our vote.
return;
}
if (targetID >= MAXPLAYERS)
{
// only the server is allowed to send these
if (playernum != serverplayer)
{
goto fail;
}
targetID = VOTE_SPECIAL;
}
else if (playeringame[targetID] == true && players[targetID].bot == true)
{
if (targetID >= MAXPLAYERS
|| playernum != serverplayer)
if (playernum != serverplayer)
{
goto fail;
}
}
else
{
if (targetID >= MAXPLAYERS
|| playernode[targetID] != playernode[playernum])
if (playernode[targetID] != playernode[playernum])
{
goto fail;
}

View file

@ -52,6 +52,7 @@ extern consvar_t cv_joyscale[MAXSPLITSCREENPLAYERS];
extern consvar_t cv_pointlimit;
extern consvar_t cv_timelimit;
extern consvar_t cv_dueltimelimit, cv_duelscorelimit;
extern consvar_t cv_numlaps;
extern UINT32 timelimitintics, extratimeintics, secretextratime;
extern UINT32 g_pointlimit;

View file

@ -957,6 +957,8 @@ struct player_t
INT32 cheatchecknum; // The number of the last cheatcheck you hit
INT32 checkpointId; // Players respawn here, objects/checkpoint.cpp
INT16 duelscore;
UINT8 team; // 0 == Spectator, 1 == Red, 2 == Blue
UINT8 checkskip; // Skipping checkpoints? Oh no no no

View file

@ -882,16 +882,24 @@ extern SINT8 spbplace;
extern boolean rainbowstartavailable;
extern tic_t linecrossed;
extern boolean inDuel;
extern UINT8 overtimecheckpoints;
extern tic_t bombflashtimer; // Used to avoid causing seizures if multiple mines explode close to you :)
extern boolean legitimateexit;
extern boolean comebackshowninfo;
#define VOTE_NUM_LEVELS (4)
#define VOTE_NOT_PICKED (-1)
#define VOTE_SPECIAL (MAXPLAYERS)
#define VOTE_TOTAL (MAXPLAYERS+1)
extern UINT16 g_voteLevels[4][2];
#define VOTE_TIMEOUT_LOSER (MAXPLAYERS+1) // not a real vote ID
#define VOTE_TIMEOUT_WINNER (MAXPLAYERS+2) // ditto
extern UINT16 g_voteLevels[VOTE_NUM_LEVELS][2];
extern SINT8 g_votes[VOTE_TOTAL];
extern SINT8 g_pickedVote;
extern boolean g_votes_striked[VOTE_NUM_LEVELS];
// ===========================
// Internal parameters, fixed.

View file

@ -297,9 +297,10 @@ boolean franticitems; // Frantic items currently enabled?
boolean g_teamplay;
// Voting system
UINT16 g_voteLevels[4][2]; // Levels that were rolled by the host
UINT16 g_voteLevels[VOTE_NUM_LEVELS][2]; // Levels that were rolled by the host
SINT8 g_votes[VOTE_TOTAL]; // Each player's vote
SINT8 g_pickedVote; // What vote the host rolls
boolean g_votes_striked[VOTE_NUM_LEVELS]; // Which levels were striked from votes?
// Server-sided, synched variables
tic_t wantedcalcdelay; // Time before it recalculates WANTED
@ -311,6 +312,7 @@ SINT8 spbplace; // SPB exists, give the person behind better items
boolean rainbowstartavailable; // Boolean, keeps track of if the rainbow start was gotten
tic_t linecrossed; // For Time Attack
boolean inDuel; // Boolean, keeps track of if it is a 1v1
UINT8 overtimecheckpoints; // Duel overtime speedups!
// Client-sided, unsynched variables (NEVER use in anything that needs to be synced with other players)
tic_t bombflashtimer = 0; // Cooldown before another FlashPal can be intialized by a bomb exploding near a displayplayer. Avoids seizures.
@ -3871,6 +3873,12 @@ tryAgain:
continue;
}
if (numPlayers == 2 && gametype == GT_RACE && ((mapheaderinfo[i]->levelflags & LF_SECTIONRACE) == LF_SECTIONRACE))
{
// Duel doesn't support sprints.
continue;
}
// Only care about restrictions if the host is a listen server.
if (!dedicated)
{

View file

@ -2067,6 +2067,11 @@ void K_UpdateBotGameplayVars(player_t *player)
if (cv_levelskull.value)
player->botvars.difficulty = MAXBOTDIFFICULTY;
if (K_InRaceDuel())
player->botvars.rival = true;
else if (grandprixinfo.gp != true)
player->botvars.rival = false;
player->botvars.rubberband = K_UpdateRubberband(player);
player->botvars.turnconfirm += player->cmd.bot.turnconfirm;

View file

@ -235,6 +235,13 @@ static patch_t *kp_team_underlay[2][2];
static patch_t *kp_team_minihead;
static patch_t *kp_team_you;
static patch_t *kp_duel_foe;
static patch_t *kp_duel_you;
static patch_t *kp_duel_sticker;
static patch_t *kp_duel_under;
static patch_t *kp_duel_over;
static patch_t *kp_duel_margin[6];
patch_t *kp_autoroulette;
patch_t *kp_autoring;
@ -1061,6 +1068,16 @@ void K_LoadKartHUDGraphics(void)
HU_UpdatePatch(&kp_team_underlay[1][1], "TEAM4UR");
HU_UpdatePatch(&kp_team_minihead, "TEAM4H");
HU_UpdatePatch(&kp_team_you, "TEAM_YOU");
HU_UpdatePatch(&kp_duel_foe, "DUEL_FOE");
HU_UpdatePatch(&kp_duel_sticker, "DUEL_S");
HU_UpdatePatch(&kp_duel_under, "DUEL_B");
HU_UpdatePatch(&kp_duel_over, "DUEL_B2");
HU_UpdatePatch(&kp_duel_you, "DUEL_YOU");
for (i = 0; i < 6; i++)
{
HU_UpdatePatch(&kp_duel_margin[i], "DUELMB0%d", i);
}
}
// For the item toggle menu
@ -2970,6 +2987,9 @@ static boolean K_drawKartPositionFaces(void)
if (!LUA_HudEnabled(hud_minirankings))
return false; // Don't proceed but still return true for free play above if HUD is disabled.
if (K_InRaceDuel())
return false;
switch (r_splitscreen)
{
case 0:
@ -3229,11 +3249,162 @@ INT32 K_GetTransFlagFromFixed(fixed_t value)
}
}
static tic_t duel_lastleveltime = 0;
static INT32 youheight = 0;
static void K_drawKartDuelScores(void)
{
if (!K_InRaceDuel())
return;
using srb2::Draw;
player_t *foe = K_DuelOpponent(stplyr);
INT32 basex = 0;
INT32 basey = 40;
INT32 flags = V_SNAPTOLEFT|V_HUDTRANS|V_SLIDEIN;
// score bars, here barheight is the size of bars at tied score
INT32 barx = 8;
INT32 bary = 61;
INT32 barheight = 48;
INT32 barwidth = 6;
// portraits
INT32 foex = 16;
INT32 foey = 21;
INT32 youx = 16;
INT32 youy = 85;
// scores
INT32 foescorex = 16;
INT32 foescorey = 38;
INT32 youscorex = 16;
INT32 youscorey = 69;
Draw::Font scorefont = Draw::Font::kThinTimer;
UINT8 ri = 6;
INT32 youfill = skincolors[stplyr->skincolor].ramp[ri];
INT32 foefill = skincolors[foe->skincolor].ramp[ri];
INT32 margin = std::min(overtimecheckpoints, (UINT8)5); // Absolutely what the fuck kind of cast.
V_DrawScaledPatch(basex, basey, flags, kp_duel_sticker);
INT32 scoredelta = stplyr->duelscore - foe->duelscore;
INT32 clutchscore = DUELWINNINGSCORE - 1; // we want the bar to be full when NEXT checkpoint wins...
INT32 savemargin = 3; // ...minus a little bit.
if (leveltime/(TICRATE/2) % 2)
savemargin += ((leveltime/2)%2);
if (clutchscore == 0)
clutchscore = 1; // Fuck it, just don't crash
INT32 targetyouheight = barheight*abs(clutchscore+scoredelta)/clutchscore;
if (targetyouheight == 0)
{
targetyouheight = savemargin;
}
else if (targetyouheight >= 2*barheight)
{
targetyouheight = 2*barheight - savemargin;
}
if (leveltime != duel_lastleveltime)
{
INT32 slide = std::max(1, abs(targetyouheight - youheight)/3);
if (targetyouheight > youheight)
youheight += slide;
else if (targetyouheight < youheight)
youheight -= slide;
}
duel_lastleveltime = leveltime;
INT32 foeheight = 2*barheight-youheight; // barheight is a single tied bar, so total height of the full gauge is 2x barheight
V_DrawFill(basex+barx, basey+bary-barheight, barwidth, foeheight, foefill|flags);
V_DrawFill(basex+barx, basey+bary-barheight+foeheight, barwidth, youheight, youfill|flags);
V_DrawScaledPatch(basex, basey, flags, kp_duel_under);
V_DrawScaledPatch(basex, basey-barheight+foeheight, flags, kp_duel_over);
V_DrawScaledPatch(basex, basey, flags, kp_duel_foe);
V_DrawScaledPatch(basex, basey, flags, kp_duel_you);
Draw foenum = Draw(basex+foescorex, basey+foescorey).flags(flags).font(scorefont).align(Draw::Align::kLeft);
Draw younum = Draw(basex+youscorex, basey+youscorey).flags(flags).font(scorefont).align(Draw::Align::kLeft);
if (abs(scoredelta) == clutchscore && ((leveltime % 2) || cv_reducevfx.value))
{
if (foe->duelscore > stplyr->duelscore)
foenum = foenum.colorize(SKINCOLOR_GOLD);
else
younum = younum.colorize(SKINCOLOR_GOLD);
}
foenum.text("{}", foe->duelscore);
younum.text("{}", stplyr->duelscore);
// minirankings shamelessly copypasted because i know that shit works already
// and SURELY we will never need to use this somewhere else, right?
UINT8 workingskin;
UINT8 *colormap;
INT32 xoff, yoff, flipflag, skinflags;
for (UINT8 draw = 0; draw < 2; draw++)
{
UINT8 drawme = draw ? (stplyr - players) : (foe - players);
UINT8 drawx = basex + (draw ? youx : foex);
UINT8 drawy = basey + (draw ? youy : foey);
if (!playeringame[drawme] || players[drawme].spectator)
continue;
if (!players[drawme].mo || P_MobjWasRemoved(players[drawme].mo))
continue;
skinflags = (demo.playback)
? demo.skinlist[demo.currentskinid[drawme]].flags
: skins[players[drawme].skin].flags;
// Flip SF_IRONMAN portraits, but only if they're transformed
if (skinflags & SF_IRONMAN
&& !(players[drawme].charflags & SF_IRONMAN) )
{
flipflag = V_FLIP|V_VFLIP; // blonic flip
xoff = yoff = 16;
} else
{
flipflag = 0;
xoff = yoff = 0;
}
if ((skin_t*)players[drawme].mo->skin)
workingskin = (skin_t*)players[drawme].mo->skin - skins;
else
workingskin = players[drawme].skin;
colormap = R_GetTranslationColormap(workingskin, static_cast<skincolornum_t>(players[drawme].mo->color), GTC_CACHE);
if (players[drawme].mo->colorized)
colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast<skincolornum_t>(players[drawme].mo->color), GTC_CACHE);
else
colormap = R_GetTranslationColormap(workingskin, static_cast<skincolornum_t>(players[drawme].mo->color), GTC_CACHE);
V_DrawMappedPatch(drawx+xoff, drawy+yoff, flags|flipflag, faceprefix[workingskin][FACE_RANK], colormap);
}
V_DrawScaledPatch(basex, basey, flags, kp_duel_margin[margin]);
}
static INT32 easedallyscore = 0;
static tic_t scorechangecooldown = 0;
// Mildly ugly. Don't want to export this to khud when it's so nicely handled here,
// but HUD hooks run at variable timing based on your actual framerate.
static tic_t lastleveltime = 0;
static tic_t teams_lastleveltime = 0;
static void K_drawKartTeamScores(void)
{
@ -3360,7 +3531,7 @@ static void K_drawKartTeamScores(void)
}
else
{
if (lastleveltime != leveltime) // Timing consistency
if (teams_lastleveltime != leveltime) // Timing consistency
{
INT32 delta = abs(easedallyscore - allyscore); // how wrong is display score?
@ -3387,7 +3558,7 @@ static void K_drawKartTeamScores(void)
enemyscore = totalscore - allyscore;
}
lastleveltime = leveltime;
teams_lastleveltime = leveltime;
fixed_t enemypercent = FixedDiv(enemyscore*FRACUNIT, totalscore*FRACUNIT);
// fixed_t allypercent = FixedDiv(allyscore*FRACUNIT, totalscore*FRACUNIT);
@ -3543,7 +3714,9 @@ static boolean K_drawKartLaps(void)
// I do not understand the way this system of offsets is laid out at all,
// so it's probably going to be pretty bad to maintain. Sorry.
if (numlaps != 1 && displayEXP != UINT16_MAX)
boolean drawinglaps = (numlaps != 1 && !K_InRaceDuel() && displayEXP != UINT16_MAX);
if (drawinglaps)
{
if (r_splitscreen > 1)
bump = 27;
@ -3551,7 +3724,7 @@ static boolean K_drawKartLaps(void)
bump = 40;
}
if (numlaps != 1)
if (drawinglaps)
{
if (r_splitscreen > 1)
{
@ -4862,7 +5035,7 @@ playertagtype_t K_WhichPlayerTag(player_t *p)
}
else if (p->bot)
{
if (p->botvars.rival == true || cv_levelskull.value)
if ((p->botvars.rival == true || cv_levelskull.value) && (!K_InRaceDuel()))
{
return PLAYERTAG_RIVAL;
}
@ -7251,9 +7424,14 @@ void K_drawKartHUD(void)
K_drawKartTeamScores();
}
if (K_InRaceDuel())
{
K_drawKartDuelScores();
}
if (LUA_HudEnabled(hud_gametypeinfo))
{
if (gametyperules & GTR_CIRCUIT)
if (gametyperules & GTR_CIRCUIT && !K_InRaceDuel())
{
K_drawKartLaps();
gametypeinfoshown = true;

View file

@ -119,6 +119,27 @@ boolean K_DuelItemAlwaysSpawns(mapthing_t *mt)
return !!(mt->thing_args[0]);
}
boolean K_InRaceDuel(void)
{
return (inDuel && (gametyperules & GTR_CIRCUIT) && !(mapheaderinfo[gamemap-1]->levelflags & LF_SECTIONRACE)) && !specialstageinfo.valid;
}
player_t *K_DuelOpponent(player_t *player)
{
if (!K_InRaceDuel())
return player; // ????
else
{
for (UINT8 i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] && !players[i].spectator && player - players != i)
return &players[i];
}
}
return player; // ????????????
}
static void K_SpawnDuelOnlyItems(void)
{
mapthing_t *mt = NULL;
@ -145,6 +166,7 @@ void K_TimerReset(void)
numbulbs = 1;
inDuel = rainbowstartavailable = false;
linecrossed = 0;
overtimecheckpoints = 0;
timelimitintics = extratimeintics = secretextratime = 0;
g_pointlimit = 0;
}
@ -273,6 +295,9 @@ void K_TimerInit(void)
introtime = (108) + 5; // 108 for rotation, + 5 for white fade
numbulbs += (numPlayers-2); // Extra POSITION!! time
}
if (K_InRaceDuel())
numlaps = 99;
}
}
@ -471,7 +496,10 @@ fixed_t K_GetKartGameSpeedScalar(SINT8 value)
if (cv_4thgear.value && !netgame && (!demo.playback || !demo.netgame) && !modeattacking)
value = 3;
return ((13 + (3*value)) << FRACBITS) / 16;
fixed_t base = ((13 + (3*value)) << FRACBITS) / 16;
fixed_t duel = overtimecheckpoints*(1<<FRACBITS)/16;
return base + duel;
}
// Array of states to pick the starting point of the animation, based on the actual time left for invincibility.
@ -4250,6 +4278,92 @@ void K_CheckpointCrossAward(player_t *player)
player->cangrabitems = 1;
K_AwardPlayerRings(player, (player->bot ? 20 : 10), true);
// Update Duel scoring.
if (K_InRaceDuel() && player->position == 1)
{
player->duelscore += 1;
if (leveltime > (tic_t)(TICRATE*DUELOVERTIME))
{
overtimecheckpoints++;
if (overtimecheckpoints > 1)
{
K_AddMessage(va("Margin Boost x%d!", overtimecheckpoints), true, false);
}
else
{
K_AddMessage("Margin Boost!", true, false);
g_darkness.start = leveltime;
g_darkness.end = INT32_MAX;
for (UINT8 i = 0; i < MAXSPLITSCREENPLAYERS; i++)
{
g_darkness.value[i] = FRACUNIT;
}
}
S_StartSound(NULL, sfx_gsha6);
}
player_t *opp = K_DuelOpponent(player);
boolean clutch = (player->duelscore - opp->duelscore == (DUELWINNINGSCORE-1));
boolean win = (player->duelscore - opp->duelscore == DUELWINNINGSCORE);
if (!win)
{
for (UINT8 i = 0; i < MAXSPLITSCREENPLAYERS; i++)
{
player_t *check = &players[displayplayers[i]];
if (check == player)
{
S_StartSound(NULL, sfx_mbs45);
if (clutch)
S_StartSoundAtVolume(NULL, sfx_s3k9c, 170);
}
else if (check == opp)
{
S_StartSound(NULL, sfx_mbs60);
if (clutch)
S_StartSoundAtVolume(NULL, sfx_kc4b, 150);
}
}
}
if (player->duelscore - opp->duelscore == DUELWINNINGSCORE)
{
opp->position = 2;
player->position = 1;
if (opp->distancetofinish - player->distancetofinish < 200) // Setting player.exiting changes distance reporting, check these first!
{
K_StartRoundWinCamera(
player->mo,
player->angleturn + ANGLE_180,
400*mapobjectscale,
6*TICRATE,
FRACUNIT/16
);
}
S_StartSound(NULL, sfx_s3k6a);
P_DoPlayerExit(player, 0);
P_DoAllPlayersExit(PF_NOCONTEST, 0);
}
else
{
// Doing this here because duel exit is a weird path, and we don't want to transform for endcam.
UINT32 skinflags = (demo.playback)
? demo.skinlist[demo.currentskinid[(player-players)]].flags
: skins[player->skin].flags;
if (skinflags & SF_IRONMAN)
{
SetRandomFakePlayerSkin(player, true, false);
}
}
}
}
boolean K_Overdrive(player_t *player)
@ -10996,7 +11110,7 @@ static void K_UpdateDistanceFromFinishLine(player_t *const player)
const mapheader_t *mapheader = mapheaderinfo[gamemap - 1];
if ((mapheader->levelflags & LF_SECTIONRACE) == 0U)
{
const UINT8 numfulllapsleft = ((UINT8)numlaps - player->laps) / mapheader->lapspersection;
UINT8 numfulllapsleft = ((UINT8)numlaps - player->laps) / mapheader->lapspersection;
player->distancetofinish += numfulllapsleft * K_GetCircuitLength();
}
}
@ -11044,6 +11158,7 @@ static void K_UpdatePlayerWaypoints(player_t *const player)
player->respawn.state == RESPAWNST_NONE && // Respawning should be a full reset.
old_currentwaypoint != NULL && // So should touching the first waypoint ever.
player->laps != 0 && // POSITION rooms may have unorthodox waypoints to guide bots.
player->exiting == 0 && // What the fuck? Why do duels antiskip the bot?
!(player->pflags & PF_TRUSTWAYPOINTS)) // Special exception.
{
extern consvar_t cv_debuglapcheat;
@ -12043,6 +12158,11 @@ void K_KartUpdatePosition(player_t *player)
realplayers++;
}
}
else if (K_InRaceDuel() && player->exiting)
{
// Positions directly set in K_CheckpointCrossAward, don't touch.
return;
}
else
{
for (i = 0; i < MAXPLAYERS; i++)

View file

@ -80,6 +80,9 @@ Make sure this matches the actual number of states
#define RINGVOLUMEREGEN 1
#define RINGTRANSPARENCYREGEN 3
#define DUELOVERTIME (cv_dueltimelimit.value)
#define DUELWINNINGSCORE (cv_duelscorelimit.value)
#define MIN_WAVEDASH_CHARGE ((11*TICRATE/16)*9)
#define MAXTOPACCEL (12*FRACUNIT)
@ -105,6 +108,8 @@ angle_t K_ReflectAngle(angle_t angle, angle_t against, fixed_t maxspeed, fixed_t
boolean K_IsDuelItem(mobjtype_t type);
boolean K_DuelItemAlwaysSpawns(mapthing_t *mt);
boolean K_InRaceDuel(void);
player_t *K_DuelOpponent(player_t *player);
void K_TimerReset(void);
void K_TimerInit(void);

View file

@ -19,6 +19,7 @@
#include "k_grandprix.h"
#include "k_profiles.h"
#include "k_serverstats.h"
#include "k_kart.h" // K_InRaceDuel
// Client-sided calculations done for Power Levels.
// This is done so that clients will never be able to hack someone else's score over the server.
@ -213,6 +214,10 @@ void K_UpdatePowerLevels(player_t *player, UINT8 lap, boolean forfeit)
CONS_Debug(DBG_PWRLV, "========\n");
yourPower = clientpowerlevels[playerNum][powerType];
if (K_InRaceDuel())
yourPower += clientPowerAdd[playerNum];
if (yourPower == 0)
{
// Guests don't record power level changes.
@ -225,6 +230,8 @@ void K_UpdatePowerLevels(player_t *player, UINT8 lap, boolean forfeit)
CONS_Debug(DBG_PWRLV, "%s's gametype score: %d\n", player_names[playerNum], yourScore);
CONS_Debug(DBG_PWRLV, "========\n");
boolean dueling = K_InRaceDuel();
for (i = 0; i < MAXPLAYERS; i++)
{
UINT16 theirScore = 0;
@ -254,6 +261,9 @@ void K_UpdatePowerLevels(player_t *player, UINT8 lap, boolean forfeit)
CONS_Debug(DBG_PWRLV, "%s VS %s:\n", player_names[playerNum], player_names[i]);
theirPower = clientpowerlevels[i][powerType];
if (K_InRaceDuel())
theirPower += clientPowerAdd[i];
if (theirPower == 0)
{
// No power level (splitscreen guests, bots)
@ -295,11 +305,13 @@ void K_UpdatePowerLevels(player_t *player, UINT8 lap, boolean forfeit)
}
}
if (exitBonus == false)
if (dueling)
{
INT16 prevInc = inc;
inc /= max(numlaps-1, 1);
// INT32 winnerscore = (yourScore > theirScore) ? player->duelscore : players[i].duelscore;
INT32 multiplier = 2;
inc *= multiplier;
if (inc == 0)
{
@ -313,7 +325,32 @@ void K_UpdatePowerLevels(player_t *player, UINT8 lap, boolean forfeit)
}
}
CONS_Debug(DBG_PWRLV, "Reduced (%d / %d = %d) because it's not the end of the race\n", prevInc, numlaps, inc);
// CONS_Printf("%s PWR UPDATE: %d\n", player_names[player - players], inc);
CONS_Debug(DBG_PWRLV, "DUELING: Boosted (%d * %d = %d)\n", prevInc, multiplier, inc);
}
else
{
if (exitBonus == false)
{
INT16 prevInc = inc;
inc /= max(numlaps-1, 1);
if (inc == 0)
{
if (prevInc > 0)
{
inc = 1;
}
else if (prevInc < 0)
{
inc = -1;
}
}
CONS_Debug(DBG_PWRLV, "Reduced (%d / %d = %d) because it's not the end of the race\n", prevInc, numlaps, inc);
}
}
CONS_Debug(DBG_PWRLV, "========\n");
@ -346,6 +383,10 @@ void K_UpdatePowerLevelsFinalize(player_t *player, boolean onForfeit)
INT16 lapsLeft = 0;
UINT8 i;
// No remaining laps in Duel.
if (K_InRaceDuel())
return;
lapsLeft = (numlaps - player->latestlap) + 1;
if (lapsLeft <= 0)
@ -394,7 +435,7 @@ INT16 K_FinalPowerIncrement(player_t *player, INT16 yourPower, INT16 baseInc)
if (inc <= 0)
{
if (player->position == 1 && numPlayers > 1)
if (player->position == 1 && numPlayers > 1 && !(K_InRaceDuel()))
{
// Won the whole match?
// Get at least one point.

View file

@ -46,6 +46,9 @@ boolean level_tally_t::UseBonuses(void)
return false;
}
if (K_InRaceDuel())
return false;
// No bonuses / ranking in FREE PLAY or Time Attack
return (grandprixinfo.gp == true || K_TimeAttackRules() == false);
}

View file

@ -109,6 +109,7 @@
// Give time for the animations to finish before finalizing the vote stages.
#define SELECT_DELAY_TIME (TICRATE*4)
#define PICK_DELAY_TIME (TICRATE/2)
#define STRIKE_DELAY_TIME (TICRATE/3)
#define MAP_ANGER_MAX (2)
@ -179,12 +180,21 @@ typedef struct
{
INT32 timer;
INT32 tic, endtic;
INT32 selectFinalize, pickFinalize;
INT32 selectFinalize, pickFinalize, strikeFinalize;
boolean notYetPicked;
boolean loaded;
SINT8 deferredLevel;
y_vote_player players[MAXSPLITSCREENPLAYERS];
y_vote_roulette roulette;
// If both of these players are valid,
// and they're the only players in the server,
// then we want stage striking!
player_t *strike_loser;
player_t *strike_winner;
boolean strike_turn;
boolean strike_time_out;
boolean stage_striking;
} y_vote_data;
// Voting level drawing
@ -209,6 +219,8 @@ typedef struct
patch_t *ruby_icon;
fixed_t ruby_height;
patch_t *strike_icon;
patch_t *bg_planet[PLANET_FRAMES];
patch_t *bg_checker;
patch_t *bg_levelText;
@ -230,13 +242,54 @@ typedef struct
static y_vote_data vote = {0};
static y_vote_draw vote_draw = {0};
static void Y_SetVoteTimer(void)
{
vote.timer = cv_votetime.value * TICRATE;
if (vote.stage_striking == true)
{
vote.timer /= 2;
}
}
static UINT8 Y_CountStriked(void)
{
INT32 i;
UINT8 num_striked = 0;
for (i = 0; i < VOTE_NUM_LEVELS; i++)
{
if (g_votes_striked[i] == true)
{
num_striked++;
}
}
return num_striked;
}
static boolean Y_VoteIDIsSpecial(const UINT8 playerId)
{
switch (playerId)
{
case VOTE_SPECIAL:
case VOTE_TIMEOUT_LOSER:
case VOTE_TIMEOUT_WINNER:
{
// Special vote spot, always allow
return true;
}
default:
{
return false;
}
}
}
boolean Y_PlayerIDCanVote(const UINT8 playerId)
{
player_t *player = NULL;
if (playerId == VOTE_SPECIAL)
if (Y_VoteIDIsSpecial(playerId) == true)
{
// Special vote spot, always allow
return true;
}
@ -245,7 +298,7 @@ boolean Y_PlayerIDCanVote(const UINT8 playerId)
return false;
}
player = &players[playerId];
const player_t *player = &players[playerId];
if (player->spectator == true)
{
return false;
@ -260,8 +313,48 @@ boolean Y_PlayerIDCanVote(const UINT8 playerId)
return true;
}
static boolean Y_IsPlayersTurn(const UINT8 playerId)
{
if (Y_VoteIDIsSpecial(playerId) == true)
{
return true;
}
if (vote.stage_striking == false)
{
// Not stage striking -- we can always vote.
return true;
}
// Is it our turn to strike a stage?
const player_t *player = &players[playerId];
if (vote.strike_turn == true)
{
return (player == vote.strike_winner);
}
else
{
return (player == vote.strike_loser);
}
}
static boolean Y_PlayerIDCanVoteRightNow(const UINT8 playerId)
{
if (Y_IsPlayersTurn(playerId) == false)
{
return false;
}
return Y_PlayerIDCanVote(playerId);
}
static boolean Y_PlayerCanSelect(const UINT8 localId)
{
if (localId > splitscreen)
{
return false;
}
const UINT8 p = g_localplayers[localId];
if (g_pickedVote != VOTE_NOT_PICKED)
@ -280,7 +373,7 @@ static boolean Y_PlayerCanSelect(const UINT8 localId)
return false;
}
return Y_PlayerIDCanVote(p);
return Y_PlayerIDCanVoteRightNow(p);
}
static void Y_SortPile(void)
@ -381,21 +474,73 @@ static void Y_SortPile(void)
}
}
void Y_SetPlayersVote(const UINT8 playerId, SINT8 newVote)
void Y_SetPlayersVote(const UINT8 inputPlayerId, SINT8 newVote)
{
y_vote_pile *const pile = &vote.roulette.pile[playerId];
y_vote_catcher *const catcher = &pile->catcher;
INT32 i;
if (gamestate != GS_VOTING)
{
return;
}
UINT8 playerId = inputPlayerId;
// Manually overwrite these players for timed out votes.
// Loser/winner is encoded in the vote ID to prevent race
// race conditions with real votes causing problems.
if (inputPlayerId == VOTE_TIMEOUT_LOSER)
{
playerId = (vote.strike_loser - players);
}
else if (inputPlayerId == VOTE_TIMEOUT_WINNER)
{
playerId = (vote.strike_winner - players);
}
if (newVote < 0 || newVote >= VOTE_NUM_LEVELS)
{
newVote = VOTE_NOT_PICKED;
}
if (playerId < MAXPLAYERS)
{
if (Y_PlayerIDCanVoteRightNow(playerId) == false)
{
// Not your turn, dude!
return;
}
if (vote.stage_striking == true)
{
if (newVote != VOTE_NOT_PICKED
&& g_votes_striked[newVote] == false
&& Y_CountStriked() < VOTE_NUM_LEVELS-1)
{
// Strike a stage, instead of voting.
g_votes_striked[newVote] = true;
// Change turn.
vote.strike_turn = !vote.strike_turn;
// Reset variables.
Y_SetVoteTimer();
for (i = 0; i <= splitscreen; i++)
{
vote.players[i].sentTimeOutVote = false;
vote.players[i].delay = NEWTICRATE/7;
}
vote.strike_time_out = false;
// TODO: striking animation
}
return;
}
}
y_vote_pile *const pile = &vote.roulette.pile[playerId];
y_vote_catcher *const catcher = &pile->catcher;
g_votes[playerId] = newVote;
Y_SortPile();
@ -416,12 +561,12 @@ void Y_SetPlayersVote(const UINT8 playerId, SINT8 newVote)
if (vote.timer == -1)
{
// Someone has voted, so start the timer now.
vote.timer = cv_votetime.value * TICRATE;
Y_SetVoteTimer();
}
#endif
}
static void Y_DrawVoteThumbnail(fixed_t center_x, fixed_t center_y, fixed_t width, INT32 flags, SINT8 v, boolean dim, SINT8 playerID)
static void Y_DrawVoteThumbnail(fixed_t center_x, fixed_t center_y, fixed_t width, INT32 flags, SINT8 v, boolean dim, SINT8 playerID, boolean from_selection)
{
const boolean encore = vote_draw.levels[v].encore;
const fixed_t height = (width * BASEVIDHEIGHT) / BASEVIDWIDTH;
@ -468,29 +613,64 @@ static void Y_DrawVoteThumbnail(fixed_t center_x, fixed_t center_y, fixed_t widt
V_AdjustXYWithSnap(&fx, &fy, flags, dupx, dupy);
boolean striked = false;
if (from_selection == true)
{
striked = g_votes_striked[v];
}
V_DrawFill(
fx - dupx, fy - dupy,
fw + (dupx << 1), fh + (dupy << 1),
0|flags|V_NOSCALESTART
);
K_DrawMapThumbnail(
x, y,
width, flags | ((encore == true) ? V_FLIP : 0),
g_voteLevels[v][0],
(dim == true ? R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_GREY, GTC_MENUCACHE) : NULL)
);
if (encore == true)
if (striked == true)
{
const fixed_t rubyScale = width / 72;
const fixed_t strikeScale = width / 32;
V_DrawFixedPatch(
center_x, center_y - FixedMul(vote_draw.ruby_height << 1, rubyScale),
rubyScale, flags,
vote_draw.ruby_icon,
center_x - (strikeScale * 25 / 2), center_y - (strikeScale * 22 / 2),
strikeScale, flags,
vote_draw.strike_icon,
NULL
);
}
else
{
K_DrawMapThumbnail(
x, y,
width, flags | ((encore == true) ? V_FLIP : 0),
g_voteLevels[v][0],
(dim == true ? R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_GREY, GTC_MENUCACHE) : NULL)
);
if (encore == true)
{
const fixed_t rubyScale = width / 72;
V_DrawFixedPatch(
center_x, center_y - FixedMul(vote_draw.ruby_height << 1, rubyScale),
rubyScale, flags,
vote_draw.ruby_icon,
NULL
);
}
if (vote.stage_striking == true
&& from_selection == true
&& dim == false)
{
if (Y_CountStriked() < VOTE_NUM_LEVELS-1)
{
const fixed_t strikeScale = width / 32;
V_DrawFixedPatch(
center_x - (strikeScale * 25 / 2), center_y - (strikeScale * 22 / 2),
strikeScale, flags /*| V_TRANSLUCENT*/,
vote_draw.strike_icon,
NULL
);
}
}
}
if (dim == true)
{
@ -506,7 +686,7 @@ static void Y_DrawVoteThumbnail(fixed_t center_x, fixed_t center_y, fixed_t widt
{
const INT32 whiteSq = 16 * dupx;
if (playerID < MAXPLAYERS)
if (playerID < MAXPLAYERS) // Player vote
{
UINT8 *playerMap = R_GetTranslationColormap(players[playerID].skin, players[playerID].skincolor, GTC_CACHE);
patch_t *playerPatch = faceprefix[players[playerID].skin][FACE_RANK];
@ -518,7 +698,7 @@ static void Y_DrawVoteThumbnail(fixed_t center_x, fixed_t center_y, fixed_t widt
playerPatch, playerMap
);
}
else
else if (vote.stage_striking == false) // Angry map
{
const fixed_t iconHeight = (14 << FRACBITS);
const fixed_t iconWidth = (iconHeight * 320) / 200;
@ -618,7 +798,7 @@ static void Y_DrawCatcher(y_vote_catcher *catcher)
baseX, catcher->y,
((catcher->small == true) ? PILE_WIDTH : SELECTION_WIDTH), 0,
catcher->level, false,
catcher->player
catcher->player, false
);
}
@ -813,7 +993,7 @@ static void Y_DrawVoteSelection(fixed_t offset)
continue;
}
if (g_votes[p] != VOTE_NOT_PICKED || Y_PlayerIDCanVote(p) == false)
if (g_votes[p] != VOTE_NOT_PICKED || Y_PlayerIDCanVoteRightNow(p) == false)
{
continue;
}
@ -881,12 +1061,41 @@ static void Y_DrawVoteSelection(fixed_t offset)
x, y - vote_draw.levels[i].hop,
SELECTION_WIDTH, 0,
i, (selected == false),
-1
-1, true
);
x += SELECTION_SPACING_W;
}
if (vote.stage_striking == true && Y_CountStriked() < VOTE_NUM_LEVELS-1)
{
UINT8 current_strike_player = (
(vote.strike_turn == true)
? (vote.strike_winner - players)
: (vote.strike_loser - players)
);
for (i = 0; i <= splitscreen; i++)
{
if (g_localplayers[i] == current_strike_player)
{
break;
}
}
if (i > splitscreen)
{
const char *wait_str = va("Waiting for %s...", player_names[current_strike_player]);
V_DrawThinString(
BASEVIDWIDTH / 2 - (V_ThinStringWidth(wait_str, 0) / 2),
180,
0,
wait_str
);
}
}
//
// Draw our catchers
//
@ -944,7 +1153,7 @@ static void Y_DrawVotePile(void)
PILE_WIDTH, 0,
g_votes[i],
(i != vote.roulette.anim || g_pickedVote == VOTE_NOT_PICKED),
i
i, false
);
}
@ -1060,8 +1269,30 @@ static void Y_VoteStops(SINT8 pick, SINT8 level)
}
}
static void Y_PlayerSendStrike(const UINT8 localPlayer)
{
y_vote_player *const player = &vote.players[localPlayer];
y_vote_catcher *const catcher = &player->catcher;
if (g_votes_striked[player->selection] == true)
{
// TODO: "Can't select" animation
return;
}
D_ModifyClientVote(g_localplayers[localPlayer], player->selection);
catcher->action = CATCHER_NA;
catcher->delay = 5;
}
static void Y_PlayerSendVote(const UINT8 localPlayer)
{
if (vote.stage_striking == true)
{
Y_PlayerSendStrike(localPlayer);
return;
}
y_vote_player *const player = &vote.players[localPlayer];
y_vote_catcher *const catcher = &player->catcher;
@ -1188,7 +1419,11 @@ static void Y_TickPlayerCatcher(const UINT8 localPlayer)
{
if (catcher->x == catcher->destX && catcher->y == catcher->destY)
{
D_ModifyClientVote(g_localplayers[localPlayer], vote.players[localPlayer].selection);
if (vote.stage_striking == false)
{
D_ModifyClientVote(g_localplayers[localPlayer], vote.players[localPlayer].selection);
}
catcher->action = CATCHER_NA;
catcher->delay = 5;
S_StopSoundByNum(sfx_kc37);
@ -1434,6 +1669,201 @@ static SINT8 Y_TryMapAngerVote(void)
return angryMaps[pick];
}
static void Y_ExitStageStrike(void)
{
INT32 i;
vote.stage_striking = false;
for (i = 0; i < VOTE_NUM_LEVELS; i++)
{
g_votes_striked[i] = false;
}
vote.strike_loser = NULL;
vote.strike_winner = NULL;
vote.strike_turn = false;
vote.strike_time_out = false;
Y_SetVoteTimer();
}
static boolean Y_CheckStageStrikeStatus(void)
{
INT32 i;
UINT8 num_voters = 0;
for (i = 0; i < MAXPLAYERS; i++)
{
if (Y_PlayerIDCanVote(i) == false)
{
continue;
}
num_voters++;
if (num_voters > 2)
{
break;
}
}
if (num_voters != 2)
{
// Someone joined or left. Stage striking is broken!
return false;
}
if (vote.strike_loser == NULL || Y_PlayerIDCanVote(vote.strike_loser - players) == false)
{
// Loser is invalidated!
return false;
}
if (vote.strike_winner == NULL || Y_PlayerIDCanVote(vote.strike_winner - players) == false)
{
// Winner is invalidated!
return false;
}
// Looks good, we can tick stage striking.
return true;
}
static void Y_TickVoteStageStrike(void)
{
INT32 i;
if (Y_CheckStageStrikeStatus() == false)
{
Y_ExitStageStrike();
return;
}
SINT8 the_only_level = VOTE_NOT_PICKED;
for (i = 0; i < VOTE_NUM_LEVELS; i++)
{
if (g_votes_striked[i] == true)
{
continue;
}
if (the_only_level != VOTE_NOT_PICKED)
{
// More than 1 valid level.
// Unset and stop iterating.
the_only_level = VOTE_NOT_PICKED;
break;
}
the_only_level = i;
}
if (the_only_level != VOTE_NOT_PICKED)
{
vote.timer = 0;
vote.strikeFinalize = STRIKE_DELAY_TIME;
if (vote.selectFinalize < SELECT_DELAY_TIME)
{
if (vote.selectFinalize == 0)
{
for (i = 0; i <= splitscreen; i++)
{
UINT8 p = g_localplayers[i];
if (p != (vote.strike_loser - players)
&& p != (vote.strike_winner - players))
{
continue;
}
y_vote_player *const player = &vote.players[i];
y_vote_catcher *const catcher = &player->catcher;
player->selection = the_only_level;
catcher->action = CATCHER_FG_LOWER;
catcher->x = catcher->destX = SELECTION_X + (SELECTION_SPACING_W * player->selection);
catcher->y = CATCHER_OFFSCREEN;
catcher->destY = SELECTION_Y - SELECTION_HOP;
catcher->spr = 0;
catcher->level = VOTE_NOT_PICKED;
S_StartSound(NULL, sfx_kc37);
}
}
vote.selectFinalize++;
}
if (vote.selectFinalize >= SELECT_DELAY_TIME)
{
if (vote.pickFinalize < PICK_DELAY_TIME)
{
vote.pickFinalize++;
}
else if (vote.endtic == -1)
{
vote.notYetPicked = false; /* don't pick vote twice */
if (server)
{
D_PickVote( the_only_level );
}
}
}
}
else
{
if (vote.timer == 0)
{
if (vote.strikeFinalize < STRIKE_DELAY_TIME)
{
vote.strikeFinalize++;
}
}
else
{
vote.strikeFinalize = 0;
}
if (vote.strikeFinalize >= STRIKE_DELAY_TIME)
{
// We didn't get their timeout strike net command.
// Maybe they hacked their exe, or connection was
// interrupted, or some other issue.
// Let's just strike a random stage for them.
if (server && vote.strike_time_out == false)
{
INT32 rng = M_RandomKey(VOTE_NUM_LEVELS);
for (i = 0; i < VOTE_NUM_LEVELS; i++)
{
if (g_votes_striked[i] == false)
{
break;
}
rng++;
if (rng >= VOTE_NUM_LEVELS)
{
rng = 0;
}
}
D_ModifyClientVote((vote.strike_turn == true) ? VOTE_TIMEOUT_WINNER : VOTE_TIMEOUT_LOSER, rng);
}
vote.strike_time_out = true;
}
else if (vote.timer > 0)
{
vote.timer--;
vote.selectFinalize = 0;
vote.pickFinalize = 0;
}
}
}
static void Y_TickVoteSelection(void)
{
boolean everyone_voted = true;/* the default condition */
@ -1463,6 +1893,22 @@ static void Y_TickVoteSelection(void)
// Time's up, send our vote ASAP.
if (vote.players[i].sentTimeOutVote == false)
{
// Move off of striked stages for the timeout vote.
INT32 j;
for (j = 0; j < VOTE_NUM_LEVELS; j++)
{
if (g_votes_striked[vote.players[i].selection] == false)
{
break;
}
vote.players[i].selection++;
if (vote.players[i].selection >= VOTE_NUM_LEVELS)
{
vote.players[i].selection = 0;
}
}
Y_PlayerSendVote(i);
vote.players[i].sentTimeOutVote = true;
vote.players[i].delay = NEWTICRATE/7;
@ -1514,12 +1960,27 @@ static void Y_TickVoteSelection(void)
continue;
}
if (players[i].bot == true && g_votes[i] == VOTE_NOT_PICKED)
if (server && players[i].bot == true && Y_PlayerIDCanVoteRightNow(i) == true && g_votes[i] == VOTE_NOT_PICKED)
{
if (( M_RandomFixed() % 100 ) == 0)
{
// bots vote randomly
D_ModifyClientVote(i, M_RandomKey(VOTE_NUM_LEVELS));
INT32 rng = M_RandomKey(VOTE_NUM_LEVELS);
for (i = 0; i < VOTE_NUM_LEVELS; i++)
{
if (g_votes_striked[i] == false)
{
break;
}
rng++;
if (rng >= VOTE_NUM_LEVELS)
{
rng = 0;
}
}
D_ModifyClientVote(i, rng);
}
}
@ -1529,6 +1990,13 @@ static void Y_TickVoteSelection(void)
}
}
if (vote.stage_striking == true)
{
// Use the same selection logic, otherwise use separate ending logic.
Y_TickVoteStageStrike();
return;
}
if (everyone_voted == true)
{
vote.timer = 0;
@ -1649,6 +2117,7 @@ static void Y_InitVoteDrawing(void)
INT32 i = 0, j = 0;
vote_draw.ruby_icon = W_CachePatchName("RUBYICON", PU_STATIC);
vote_draw.strike_icon = W_CachePatchName("K_NOBLNS", PU_STATIC);
for (i = 0; i < PLANET_FRAMES; i++)
{
@ -1736,6 +2205,103 @@ static void Y_InitVoteDrawing(void)
vote_draw.selectTransition = FRACUNIT;
}
static boolean Y_DetermineStageStrike(void)
{
player_t *a = NULL;
player_t *b = NULL;
UINT8 num_voters = 0;
INT32 i;
for (i = 0; i < MAXPLAYERS; i++)
{
if (Y_PlayerIDCanVote(i) == false)
{
continue;
}
num_voters++;
// Just set the pointers for now, sort them later.
if (a == NULL)
{
a = &players[i];
}
else if (b == NULL)
{
b = &players[i];
}
else
{
// Too many players
return false;
}
}
if (num_voters != 2 || a == NULL || b == NULL)
{
// Requires exactly 2 of them.
return false;
}
UINT32 score_a = 0;
UINT32 score_b = 0;
intertype_t scoring_type = Y_GetIntermissionType();
switch (scoring_type)
{
case int_time:
{
score_a = UINT32_MAX - a->realtime;
score_b = UINT32_MAX - b->realtime;
break;
}
case int_score:
{
score_a = a->score;
score_b = b->realtime;
break;
}
default:
{
// Invalid, exit now.
return false;
}
}
if (a->pflags & PF_NOCONTEST)
{
score_a = 0;
}
if (b->pflags & PF_NOCONTEST)
{
score_b = 0;
}
if (score_a == score_b)
{
// TODO: should be a coin flip, but how
// should the RNG for this be handled?
score_a++;
}
if (score_a > score_b)
{
vote.strike_loser = b;
vote.strike_winner = a;
}
else
{
vote.strike_loser = a;
vote.strike_winner = b;
}
vote.stage_striking = true;
return true;
}
void Y_StartVote(void)
{
INT32 i = 0;
@ -1749,12 +2315,6 @@ void Y_StartVote(void)
vote.tic = vote.endtic = -1;
#ifdef VOTE_TIME_WAIT_FOR_VOTE
vote.timer = -1; // Timer is not set until the first vote is added
#else
vote.timer = cv_votetime.value * TICRATE;
#endif
g_pickedVote = VOTE_NOT_PICKED;
vote.notYetPicked = true;
@ -1782,6 +2342,19 @@ void Y_StartVote(void)
catcher->player = i;
}
for (i = 0; i < VOTE_NUM_LEVELS; i++)
{
g_votes_striked[i] = false;
}
Y_DetermineStageStrike();
#ifdef VOTE_TIME_WAIT_FOR_VOTE
vote.timer = -1; // Timer is not set until the first vote is added
#else
Y_SetVoteTimer();
#endif
Y_InitVoteDrawing();
vote.loaded = true;
@ -1803,6 +2376,7 @@ static void Y_UnloadVoteData(void)
}
UNLOAD(vote_draw.ruby_icon);
UNLOAD(vote_draw.strike_icon);
for (i = 0; i < PLANET_FRAMES; i++)
{
@ -1941,4 +2515,25 @@ void Y_SetupVoteFinish(SINT8 pick, SINT8 level, SINT8 anger)
vote.timer = -1;
vote.selectFinalize = SELECT_DELAY_TIME;
vote.pickFinalize = PICK_DELAY_TIME;
vote.strikeFinalize = STRIKE_DELAY_TIME;
}
//
// Y_VoteContext
//
enum
{
VOTE_CTX_NORMAL = 0,
VOTE_CTX_DUEL,
};
UINT8 Y_VoteContext(void)
{
if (vote.stage_striking == true)
{
return VOTE_CTX_DUEL;
}
return VOTE_CTX_NORMAL;
}

View file

@ -19,9 +19,6 @@
extern "C" {
#endif
#define VOTE_NUM_LEVELS (4)
#define VOTE_NOT_PICKED (-1)
#define VOTE_MOD_ENCORE (0x01)
boolean Y_PlayerIDCanVote(const UINT8 playerId);
@ -32,6 +29,7 @@ void Y_VoteTicker(void);
void Y_StartVote(void);
void Y_EndVote(void);
void Y_SetupVoteFinish(SINT8 pick, SINT8 level, SINT8 anger);
UINT8 Y_VoteContext(void);
#ifdef __cplusplus
} // extern "C"

View file

@ -709,6 +709,8 @@ static int player_get(lua_State *L)
lua_pushinteger(L, plr->checkskip);
else if (fastcmp(field,"cheatchecknum"))
lua_pushinteger(L, plr->cheatchecknum);
else if (fastcmp(field,"duelscore"))
lua_pushinteger(L, plr->duelscore);
else if (fastcmp(field,"lastsidehit"))
lua_pushinteger(L, plr->lastsidehit);
else if (fastcmp(field,"lastlinehit"))
@ -1312,6 +1314,8 @@ static int player_set(lua_State *L)
plr->checkskip = (INT32)luaL_checkinteger(L, 3);
else if (fastcmp(field,"cheatchecknum"))
plr->cheatchecknum = (INT32)luaL_checkinteger(L, 3);
else if (fastcmp(field,"duelscore"))
plr->duelscore = (INT16)luaL_checkinteger(L, 3);
else if (fastcmp(field,"lastsidehit"))
plr->lastsidehit = (INT16)luaL_checkinteger(L, 3);
else if (fastcmp(field,"lastlinehit"))

View file

@ -1858,7 +1858,10 @@ boolean M_CheckCondition(condition_t *cn, player_t *player)
&& (gamespeed != KARTSPEED_EASY)
&& (player->tally.active == true)
&& (player->tally.totalExp > 0) // Only true if not Time Attack
&& (player->tally.exp >= player->tally.totalExp));
&& (
(player->tally.exp >= player->tally.totalExp)
|| (K_InRaceDuel() && player->duelscore == DUELWINNINGSCORE)
));
case UCRP_FINISHALLPRISONS:
return (battleprisons
&& !(player->pflags & PF_NOCONTEST)

View file

@ -52,6 +52,17 @@ menuitem_t OPTIONS_Gameplay[] =
NULL, {.cvar = &cv_kartbumpers}, 0, 0},
{IT_HEADER, "Duel...", NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Duel Time Limit", "How long it takes for Margin Boost to kick in (seconds).",
NULL, {.cvar = &cv_dueltimelimit}, 0, 0},
{IT_STRING | IT_CVAR, "Duel Score Limit", "How many points a player must be ahead to win a Duel.",
NULL, {.cvar = &cv_duelscorelimit}, 0, 0},
{IT_SPACE | IT_DYBIGSPACE, NULL, NULL,
NULL, {NULL}, 0, 0},

View file

@ -9110,7 +9110,10 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
&& (gamespeed != KARTSPEED_EASY)
&& (newplayer->tally.active == true)
&& (newplayer->tally.totalExp > 0) // Only true if not Time Attack
&& (newplayer->tally.exp >= newplayer->tally.totalExp)
&& (
(newplayer->tally.exp >= newplayer->tally.totalExp) ||
(K_InRaceDuel() && newplayer->duelscore == DUELWINNINGSCORE)
)
)
{
UINT8 pnum = (newplayer-players);

View file

@ -291,6 +291,7 @@ static void P_NetArchivePlayers(savebuffer_t *save)
WRITEUINT32(save->p, players[i].exp);
WRITEINT32(save->p, players[i].gradingfactor);
WRITEUINT16(save->p, players[i].gradingpointnum);
WRITEINT16(save->p, players[i].duelscore);
WRITEINT32(save->p, players[i].cheatchecknum);
WRITEINT32(save->p, players[i].checkpointId);
@ -983,6 +984,7 @@ static void P_NetUnArchivePlayers(savebuffer_t *save)
players[i].exp = READUINT32(save->p);
players[i].gradingfactor = READINT32(save->p);
players[i].gradingpointnum = READUINT16(save->p);
players[i].duelscore = READINT16(save->p);
players[i].cheatchecknum = READINT32(save->p);
players[i].checkpointId = READINT32(save->p);
@ -6757,6 +6759,7 @@ static void P_NetArchiveMisc(savebuffer_t *save, boolean resending)
{
WRITEUINT16(save->p, g_voteLevels[i][0]);
WRITEUINT16(save->p, g_voteLevels[i][1]);
WRITEUINT8(save->p, g_votes_striked[i]);
}
for (i = 0; i < VOTE_TOTAL; i++)
@ -6834,6 +6837,7 @@ static void P_NetArchiveMisc(savebuffer_t *save, boolean resending)
WRITESINT8(save->p, spbplace);
WRITEUINT8(save->p, rainbowstartavailable);
WRITEUINT8(save->p, inDuel);
WRITEUINT8(save->p, overtimecheckpoints);
WRITEUINT32(save->p, introtime);
WRITEUINT32(save->p, starttime);
@ -7143,6 +7147,7 @@ static boolean P_NetUnArchiveMisc(savebuffer_t *save, boolean reloading)
{
g_voteLevels[i][0] = READUINT16(save->p);
g_voteLevels[i][1] = READUINT16(save->p);
g_votes_striked[i] = (boolean)READUINT8(save->p);
}
for (i = 0; i < VOTE_TOTAL; i++)
@ -7216,6 +7221,7 @@ static boolean P_NetUnArchiveMisc(savebuffer_t *save, boolean reloading)
spbplace = READSINT8(save->p);
rainbowstartavailable = (boolean)READUINT8(save->p);
inDuel = (boolean)READUINT8(save->p);
overtimecheckpoints = (boolean)READUINT8(save->p);
introtime = READUINT32(save->p);
starttime = READUINT32(save->p);

View file

@ -53,6 +53,7 @@
#include "m_easing.h"
#include "music.h"
#include "k_battle.h" // battleprisons
#include "k_endcam.h" // K_EndCameraIsFreezing()
// Not sure if this is necessary, but it was in w_wad.c, so I'm putting it here too -Shadow Hog
#include <errno.h>
@ -1995,7 +1996,7 @@ static void K_HandleLapIncrement(player_t *player)
}
// finished race exit setup
if (player->laps > numlaps)
if (player->laps > numlaps && !K_InRaceDuel())
{
pflags_t applyflags = 0;
if (specialstageinfo.valid == true)
@ -2021,7 +2022,8 @@ static void K_HandleLapIncrement(player_t *player)
: skins[player->skin].flags;
if (skinflags & SF_IRONMAN)
{
SetRandomFakePlayerSkin(player, true, false);
if ((player->laps == 1 && lapisfresh) || !K_InRaceDuel()) // We'll do this in K_CheckpointCrossAward if necessary.
SetRandomFakePlayerSkin(player, true, false);
}
// Always trust waypoints entering the first lap.
@ -2064,24 +2066,32 @@ static void K_HandleLapIncrement(player_t *player)
if (rainbowstartavailable == true && player->mo->hitlag == 0)
{
S_StartSound(player->mo, sfx_s23c);
player->startboost = 125;
K_SpawnDriftBoostExplosion(player, 4);
K_SpawnDriftElectricSparks(player, SKINCOLOR_SILVER, false);
K_SpawnAmps(player, 35, player->mo);
if (g_teamplay)
if (K_InRaceDuel())
{
for (UINT8 j = 0; i < MAXPLAYERS; i++)
K_SpawnDriftElectricSparks(player, player->skincolor, false);
K_SpawnAmps(player, 20, player->mo);
}
else
{
S_StartSound(player->mo, sfx_s23c);
player->startboost = 125;
K_SpawnDriftBoostExplosion(player, 4);
K_SpawnDriftElectricSparks(player, SKINCOLOR_SILVER, false);
K_SpawnAmps(player, (K_InRaceDuel()) ? 20 : 35, player->mo);
if (g_teamplay)
{
if (!playeringame[j] || players[j].spectator || !players[j].mo || P_MobjWasRemoved(players[j].mo))
continue;
if (!G_SameTeam(player, &players[j]))
continue;
if (player == &players[j])
continue;
K_SpawnAmps(&players[j], 10, player->mo);
for (UINT8 j = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[j] || players[j].spectator || !players[j].mo || P_MobjWasRemoved(players[j].mo))
continue;
if (!G_SameTeam(player, &players[j]))
continue;
if (player == &players[j])
continue;
K_SpawnAmps(&players[j], 10, player->mo);
}
}
}
@ -2105,7 +2115,9 @@ static void K_HandleLapIncrement(player_t *player)
}
else if (P_IsDisplayPlayer(player))
{
if (numlaps > 1 && player->laps == numlaps) // final lap
if (K_InRaceDuel())
S_StartSound(NULL, sfx_s221);
else if (numlaps > 1 && player->laps == numlaps) // final lap
S_StartSound(NULL, sfx_s3k68);
else if ((player->laps > 1) && (player->laps < numlaps)) // non-final lap
S_StartSound(NULL, sfx_s221);
@ -2118,7 +2130,7 @@ static void K_HandleLapIncrement(player_t *player)
}
else
{
if ((player->laps > numlaps) && (player->position == 1))
if ((player->laps > numlaps) && (player->position == 1) && (!K_InRaceDuel()))
{
// opponent finished
S_StartSound(NULL, sfx_s253);
@ -4752,7 +4764,7 @@ void P_SetupSignExit(player_t *player, boolean tie)
return;
// SRB2Kart: FINALLY, add in an alternative if no place is found
if (player->mo && !P_MobjWasRemoved(player->mo))
if (player->mo && !P_MobjWasRemoved(player->mo) && !K_EndCameraIsFreezing())
{
thing = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->floorz, MT_SIGN);
thing->angle = bestAngle;

View file

@ -1389,7 +1389,7 @@ void P_DoPlayerExit(player_t *player, pflags_t flags)
void P_DoAllPlayersExit(pflags_t flags, boolean trygivelife)
{
UINT8 i;
const boolean dofinishsound = (musiccountdown == 0);
const boolean dofinishsound = (musiccountdown == 0) && (!K_InRaceDuel());
if (grandprixinfo.gp == false
|| grandprixinfo.eventmode == GPEVENT_SPECIAL

View file

@ -2211,6 +2211,35 @@ boolean Y_ShouldDoIntermission(void)
return true;
}
//
// Y_GetIntermissionType
//
// Returns the intermission type from the current gametype.
//
intertype_t Y_GetIntermissionType(void)
{
intertype_t ret = static_cast<intertype_t>(gametypes[gametype]->intermission);
if (ret == int_scoreortimeattack)
{
UINT8 i = 0, nump = 0;
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator)
{
continue;
}
nump++;
}
ret = (nump < 2 ? int_time : int_score);
}
return ret;
}
//
// Y_DetermineIntermissionType
//
@ -2225,21 +2254,7 @@ void Y_DetermineIntermissionType(void)
return;
}
// set initially
intertype = static_cast<intertype_t>(gametypes[gametype]->intermission);
// special cases
if (intertype == int_scoreortimeattack)
{
UINT8 i = 0, nump = 0;
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator)
continue;
nump++;
}
intertype = (nump < 2 ? int_time : int_score);
}
intertype = Y_GetIntermissionType();
}
static UINT8 Y_PlayersBestPossiblePosition(player_t *const player)

View file

@ -62,9 +62,6 @@ void Y_StartIntermission(void);
void Y_MidIntermission(void);
void Y_EndIntermission(void);
boolean Y_ShouldDoIntermission(void);
void Y_DetermineIntermissionType(void);
void Y_PlayIntermissionMusic(void);
boolean Y_IntermissionPlayerLock(void);
@ -79,6 +76,10 @@ typedef enum
extern intertype_t intertype;
boolean Y_ShouldDoIntermission(void);
intertype_t Y_GetIntermissionType(void);
void Y_DetermineIntermissionType(void);
#ifdef __cplusplus
} // extern "C"
#endif