diff --git a/src/d_player.h b/src/d_player.h index 039ec2390..31ee89f12 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -350,6 +350,14 @@ typedef enum khud_exp, khud_exptimer, + // Splits + khud_splittime, + khud_splitwin, + khud_splittimer, + khud_splitskin, + khud_splitcolor, + khud_splitlast, + NUMKARTHUD } karthudtype_t; diff --git a/src/doomstat.h b/src/doomstat.h index 9a8efea18..e585a5db3 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -884,7 +884,7 @@ extern boolean thwompsactive; extern UINT8 lastLowestLap; extern SINT8 spbplace; extern boolean rainbowstartavailable; -extern tic_t linecrossed; +extern tic_t attacktimingstarted; extern boolean inDuel; extern UINT8 overtimecheckpoints; diff --git a/src/g_demo.cpp b/src/g_demo.cpp index 17e095e29..39f714e3a 100644 --- a/src/g_demo.cpp +++ b/src/g_demo.cpp @@ -118,7 +118,7 @@ size_t copy_fixed_buf(void* p, const void* s, size_t n) static char demoname[MAX_WADPATH]; static savebuffer_t demobuf = {0}; -static UINT8 *demotime_p, *demoinfo_p; +static UINT8 *demotime_p, *demoinfo_p, *demoattack_p, *demosplits_p; static UINT16 demoflags; boolean demosynced = true; // console warning message @@ -1251,19 +1251,22 @@ void G_ConsGhostTic(INT32 playernum) void G_GhostTicker(void) { demoghost *g,*p; + for (g = ghosts, p = NULL; g; g = g->next) { UINT16 ziptic; UINT8 xziptic; + tic_t fastforward = 0; if (g->done) { continue; } - // Pause jhosts that cross until the timer starts. - if (g->linecrossed && leveltime < starttime && G_TimeAttackStart()) + if (g->attackstart != INT32_MAX && leveltime < starttime && leveltime >= g->attackstart && G_TimeAttackStart()) + { continue; + } readghosttic: #define follow g->mo->tracer @@ -1329,8 +1332,6 @@ fadeghost: g->p += g->sizes.skin_name + g->sizes.color_name; if (ziptic & DXD_WEAPONPREF) g->p++; // ditto - if (ziptic & DXD_START) - g->linecrossed = true; } else if (ziptic == DW_RNG) { @@ -1599,8 +1600,17 @@ skippedghosttic: I_Error("Ghost is not a record attack ghost GHOSTEND"); //@TODO lmao don't blow up like this // If the timer started, skip ahead until the ghost starts too. - if (starttime <= leveltime && !g->linecrossed && G_TimeAttackStart()) + if (!fastforward && attacktimingstarted && g->attackstart != INT32_MAX && leveltime < g->attackstart && G_TimeAttackStart()) + { + fastforward = g->attackstart - leveltime; + g->attackstart = INT32_MAX; + } + + if (fastforward) + { + fastforward--; goto readghosttic; + } p = g; #undef follow @@ -2056,6 +2066,17 @@ void G_BeginRecording(void) demoinfo_p = demobuf.p; WRITEUINT32(demobuf.p, 0); + // If special attack-start timing applies, we need to know where to skip the ghost to + demoattack_p = demobuf.p; + WRITEUINT32(demobuf.p, INT32_MAX); + + demosplits_p = demobuf.p; + for (i = 0; i < MAXSPLITS; i++) + { + WRITEUINT32(demobuf.p, INT32_MAX); + } + + // Save netvar data CV_SaveDemoVars(&demobuf.p); @@ -2222,6 +2243,70 @@ void srb2::write_current_demo_end_marker() *(UINT32 *)demoinfo_p = demobuf.p - demobuf.buffer; } +void G_SetDemoAttackTiming(tic_t time) +{ + if (demo.playback) + return; + + *(UINT32 *)demoattack_p = time; +} + +void G_SetDemoCheckpointTiming(player_t *player, tic_t time, UINT8 checkpoint) +{ + if (demo.playback) + return; + if (checkpoint >= MAXSPLITS || checkpoint < 0) + return; + + UINT32 *splits = (UINT32 *)demosplits_p; + splits[checkpoint] = time; + + demoghost *g; + tic_t lowest = INT32_MAX; + UINT32 lowestskin = ((skin_t*)player->mo->skin) - skins; + UINT32 lowestcolor = player->skincolor; + for (g = ghosts; g; g = g->next) + { + if (lowest > g->splits[checkpoint]) + { + lowest = g->splits[checkpoint]; + lowestskin = ((skin_t*)g->mo->skin)-skins; + lowestcolor = g->mo->color; + + } + } + + if (lowest != INT32_MAX) + { + player->karthud[khud_splittimer] = 3*TICRATE; + player->karthud[khud_splitskin] = lowestskin; + player->karthud[khud_splitcolor] = lowestcolor; + player->karthud[khud_splittime] = (INT32)time - (INT32)lowest; + + if (lowest < time) + { + player->karthud[khud_splitwin] = -2; // behind and losing + } + else + { + player->karthud[khud_splitwin] = 2; // ahead and gaining + } + + INT32 last = player->karthud[khud_splitlast]; + INT32 now = player->karthud[khud_splittime]; + + if (checkpoint != 0) + { + if (player->karthud[khud_splitwin] > 0 && now > last) + player->karthud[khud_splitwin] = 1; // ahead but losing + else if (player->karthud[khud_splitwin] < 0 && now < last) + player->karthud[khud_splitwin] = -1; // behind but gaining + } + + player->karthud[khud_splitlast] = player->karthud[khud_splittime]; + } +} + void G_SetDemoTime(UINT32 ptime, UINT32 plap) { if (!demo.recording || !demotime_p) @@ -2576,6 +2661,9 @@ void G_LoadDemoInfo(menudemo_t *pdemo, boolean allownonmultiplayer) } extrainfo_p = info.buffer + READUINT32(info.p); // The extra UINT32 read is for a blank 4 bytes? + info.p += 4; // attack start + for (i = 0; i < MAXSPLITS; i++) + info.p += 4; // splits // Pared down version of CV_LoadNetVars to find the kart speed pdemo->kartspeed = KARTSPEED_NORMAL; // Default to normal speed @@ -3075,6 +3163,11 @@ void G_DoPlayDemoEx(const char *defdemoname, lumpnum_t deflumpnum) } demobuf.p += 4; // Extrainfo location + demobuf.p += 4; // Attack start + for (i = 0; i < MAXSPLITS; i++) + { + demobuf.p += 4; // Splits + } // ...*map* not loaded? if (!gamemap || (gamemap > nummapheaders) || !mapheaderinfo[gamemap-1] || mapheaderinfo[gamemap-1]->lumpnum == LUMPERROR) @@ -3491,6 +3584,13 @@ void G_AddGhost(savebuffer_t *buffer, const char *defdemoname) } p += 4; // Extra data location reference + tic_t attackstart = READUINT32(p); + + UINT8 *splits = p; + for (i = 0; i < MAXSPLITS; i++) + { + p += 4; + } // net var data count = READUINT16(p); @@ -3582,6 +3682,9 @@ void G_AddGhost(savebuffer_t *buffer, const char *defdemoname) gh->numskins = worknumskins; gh->skinlist = skinlist; + gh->attackstart = attackstart; + std::memcpy(gh->splits, splits, sizeof(tic_t) * MAXSPLITS); + ghosts = gh; gh->version = ghostversion; @@ -3733,6 +3836,9 @@ staffbrief_t *G_GetStaffGhostBrief(UINT8 *buffer) } p += 4; // Extrainfo location marker + p += 4; // Attack start info + for (i = 0; i < MAXSPLITS; i++) + p += 4; // splits // Ehhhh don't need ghostversion here (?) so I'll reuse the var here ghostversion = READUINT16(p); diff --git a/src/g_demo.h b/src/g_demo.h index 4ce05c2a3..61a6a17fc 100644 --- a/src/g_demo.h +++ b/src/g_demo.h @@ -159,7 +159,6 @@ extern UINT8 demo_writerng; #define DXD_NAME 0x08 // name changed #define DXD_COLOR 0x10 // color changed #define DXD_FOLLOWER 0x20 // follower was changed -#define DXD_START 0x40 // Crossed the line in TA #define DXD_ADDPLAYER (DXD_JOINDATA|DXD_PLAYSTATE|DXD_COLOR|DXD_NAME|DXD_SKIN|DXD_FOLLOWER) @@ -169,6 +168,8 @@ extern UINT8 demo_writerng; #define DXD_PST_SPECTATING 0x02 #define DXD_PST_LEFT 0x03 +#define MAXSPLITS (32) + boolean G_CompatLevel(UINT16 level); // Record/playback tics @@ -203,7 +204,8 @@ struct demoghost { UINT8 fadein; UINT16 version; UINT8 numskins; - boolean linecrossed; + tic_t attackstart; + tic_t splits[MAXSPLITS]; boolean done; democharlist_t *skinlist; mobj_t oldmo, *mo; @@ -238,6 +240,9 @@ void G_DeferedPlayDemo(const char *demo); void G_SaveDemo(void); void G_ResetDemoRecording(void); +void G_SetDemoAttackTiming(tic_t time); +void G_SetDemoCheckpointTiming(player_t *player, tic_t time, UINT8 checkpoint); + boolean G_CheckDemoTitleEntry(void); typedef enum diff --git a/src/g_game.c b/src/g_game.c index 1b5b5208f..94b1fa68b 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -314,7 +314,7 @@ boolean thwompsactive; // Thwomps activate on lap 2 UINT8 lastLowestLap; // Last lowest lap, for activating race lap executors 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 +tic_t attacktimingstarted; // For Time Attack boolean inDuel; // Boolean, keeps track of if it is a 1v1 UINT8 overtimecheckpoints; // Duel overtime speedups! @@ -5133,7 +5133,7 @@ void G_EndGame(void) return; } - if (gametype == GT_TUTORIAL && M_GameAboutToStart() && restoreMenu == NULL) + if (gametype == GT_TUTORIAL && M_GameAboutToStart() && restoreMenu == NULL) { // Playground Hack F_StartIntro(); diff --git a/src/k_hud.cpp b/src/k_hud.cpp index 5ad635659..eab57e780 100644 --- a/src/k_hud.cpp +++ b/src/k_hud.cpp @@ -7760,9 +7760,75 @@ void K_drawKartHUD(void) using srb2::Draw; Draw::TextElement text = Draw::TextElement().parse(" Restart"); Draw(BASEVIDWIDTH - 19, 2) - .flags(flags | V_YELLOWMAP) + .flags(flags | V_ORANGEMAP) .align(Draw::Align::kRight) .text(text.string()); + + boolean debug_alwaysdraw = false; + + if ( + ( + !stplyr->karthud[khud_lapanimation] && + stplyr->karthud[khud_splittimer] && + (stplyr->karthud[khud_splittimer] > TICRATE/3 || stplyr->karthud[khud_splittimer]%2 || cv_reducevfx.value) + ) + || debug_alwaysdraw + ) + { + INT32 split = stplyr->karthud[khud_splittime]; + INT32 skin = stplyr->karthud[khud_splitskin]; + INT32 color = stplyr->karthud[khud_splitcolor]; + INT32 ahead = stplyr->karthud[khud_splitwin]; + + // debug + if (!stplyr->karthud[khud_splittimer]) + { + ahead = ((leveltime/17)%5) - 2; + split = leveltime; + skin = stplyr->skin; + color = stplyr->skincolor; + } + + split = std::abs(split); + + UINT8 *skincolor = R_GetTranslationColormap(skin, static_cast(color), GTC_CACHE); + + UINT8 textcolor = SKINCOLOR_WHITE; + switch (ahead) + { + case 2: + textcolor = SKINCOLOR_SAPPHIRE; // leading and gaining + break; + case 1: + textcolor = SKINCOLOR_PIGEON; // leading and losing + break; + case -1: + textcolor = SKINCOLOR_RUBY; // trailing and gaining + break; + case -2: + textcolor = SKINCOLOR_CRIMSON; // trailing and losing + break; + } + + + Draw row = Draw(BASEVIDWIDTH/2, BASEVIDHEIGHT/4).align(Draw::Align::kCenter) + .font(Draw::Font::kThinTimer).flags(V_30TRANS); + + std::string arrow = (ahead == 1 || ahead == -2) ? "(" : ")"; + + // vibes offset + row.x(-35).colormap(skincolor).patch(R_CanShowSkinInDemo(skin) ? faceprefix[skin][FACE_MINIMAP] : kp_unknownminimap); + + Draw::TextElement text = Draw::TextElement( + std::string(ahead >= 0 ? "-" : "+") + " " + "{:02}'{:02}\"{:02} " + arrow, + G_TicsToMinutes(split, true), + G_TicsToSeconds(split), + G_TicsToCentiseconds(split) + ); + + // vibes offset TWO + row.colormap(textcolor).colorize(textcolor).x(15).text(text); + } } else { diff --git a/src/k_kart.c b/src/k_kart.c index 216c182bd..96b1dcec0 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -195,7 +195,7 @@ void K_TimerReset(void) memset(&g_musicfade, 0, sizeof g_musicfade); numbulbs = 1; inDuel = rainbowstartavailable = false; - linecrossed = 0; + attacktimingstarted = 0; overtimecheckpoints = 0; timelimitintics = extratimeintics = secretextratime = 0; g_pointlimit = 0; @@ -348,6 +348,7 @@ void K_TimerInit(void) if (G_TimeAttackStart()) { starttime = TIMEATTACK_START; // Longest permitted start. No half-laps in reverse. + rainbowstartavailable = true; // (Changed on finish line cross later, don't worry.) } @@ -3112,6 +3113,9 @@ fixed_t K_PlayerTripwireSpeedThreshold(const player_t *player) if (specialstageinfo.valid) required_speed = 3 * K_GetKartSpeed(player, false, false) / 2; // 150% + if (modeattacking && !(gametyperules & GTR_CATCHER)) + required_speed = 4 * K_GetKartSpeed(player, false, false); + UINT32 distance = K_GetItemRouletteDistance(player, 8); if (gametype == GT_RACE && M_NotFreePlay() && !modeattacking) @@ -4432,6 +4436,11 @@ void K_CheckpointCrossAward(player_t *player) if (gametype != GT_RACE) return; + if (!demo.playback && G_TimeAttackStart()) + { + G_SetDemoCheckpointTiming(player, leveltime - starttime, player->gradingpointnum); + } + player->gradingfactor += K_GetGradingFactorAdjustment(player); player->gradingpointnum++; player->exp = K_GetEXP(player); @@ -9049,6 +9058,11 @@ void K_KartPlayerHUDUpdate(player_t *player) if (player->karthud[khud_trickcool]) player->karthud[khud_trickcool]--; + if (player->karthud[khud_splittimer] && !player->karthud[khud_lapanimation]) + { + player->karthud[khud_splittimer]--; + } + if (player->positiondelay) player->positiondelay--; @@ -9641,6 +9655,53 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) K_DropItems(player); } + if (G_TimeAttackStart() && !attacktimingstarted && player->speed && leveltime > introtime) + { + attacktimingstarted = leveltime; + /* + if (starttime > leveltime) // Overlong starts shouldn't reset time on cross + { + // Award some Amps for a fast start, to counterbalance Obvious Rainbow Driftboost + + tic_t starthaste = starttime - leveltime; // How much time we had left to cross + starthaste = TIMEATTACK_START - starthaste; // How much time we wasted before crossing + + tic_t leniency = TICRATE*4; // How long we can take to cross with no penalty to amp payout + + if (starthaste <= leniency) + starthaste = 0; + else + starthaste -= leniency; + + // fixed_t ampreward = Easing_OutQuart(starthaste*FRACUNIT/TIMEATTACK_START, 60*FRACUNIT, 0); + // K_SpawnAmps(player, ampreward/FRACUNIT, player->mo); + + UINT8 baseboost = 125; + + player->startboost = Easing_OutQuart(starthaste*FRACUNIT/TIMEATTACK_START, baseboost, 0); + + if (player->startboost == baseboost) + { + K_SpawnDriftBoostExplosion(player, 4); + K_SpawnDriftElectricSparks(player, SKINCOLOR_SILVER, false); + } + else + { + K_SpawnDriftBoostExplosion(player, 3); + // K_SpawnDriftElectricSparks(player, SKINCOLOR_SILVER, false); + } + + // And reset our time to 0. + starttime = leveltime; + } + */ + + starttime = leveltime; + G_SetDemoAttackTiming(leveltime); + + Music_Stop("position"); + } + if (player->transfer) { if (player->fastfall) @@ -13283,6 +13344,9 @@ static void K_KartSpindashDust(mobj_t *parent) ); flip = P_MobjFlip(dust); + if (G_TimeAttackStart() && leveltime < starttime) + dust->scale = 3 * dust->scale / 2; + dust->momx = FixedMul(hmomentum, FINECOSINE(ang >> ANGLETOFINESHIFT)); dust->momy = FixedMul(hmomentum, FINESINE(ang >> ANGLETOFINESHIFT)); dust->momz = vmomentum * flip; @@ -13345,6 +13409,12 @@ static void K_KartSpindash(player_t *player) { fixed_t thrust = FixedMul(player->mo->scale, min(player->spindash, MAXCHARGETIME)*FRACUNIT/5); + if (G_TimeAttackStart() && leveltime < starttime) + { + thrust *= 2; + // player->spindashspeed += FRACUNIT/2; + } + // Old behavior, before emergency zero-ring spindash /* if (gametyperules & GTR_CLOSERPLAYERS) @@ -13478,16 +13548,30 @@ static void K_KartSpindash(player_t *player) { UINT8 ringdropframes = 2 + (player->kartspeed + player->kartweight); boolean spawnOldEffect = true; + boolean normalsound = true; INT16 chargetime = MAXCHARGETIME - ++player->spindash; if (player->rings <= 0 && chargetime >= 0) // Desperation spindash { player->spindash++; + normalsound = false; if (!S_SoundPlaying(player->mo, sfx_kc38)) S_StartSound(player->mo, sfx_kc38); } + if (G_TimeAttackStart() && leveltime < starttime && chargetime >= 0) + { + if (player->spindash == 1) + { + S_ReducedVFXSound(player->mo, sfx_s3kab, player); + S_ReducedVFXSound(player->mo, sfx_s3k9c, player); + } + + normalsound = false; + player->spindash += 4; + } + if (player->spindash >= SPINDASHTHRUSTTIME) { K_KartSpindashDust(player->mo); @@ -13525,7 +13609,7 @@ static void K_KartSpindash(player_t *player) while ((soundcharge += ++add) < chargetime); - if (soundcharge == chargetime) + if (soundcharge == chargetime && normalsound) { if (spawnOldEffect == true) K_SpawnDashDustRelease(player); @@ -14010,7 +14094,9 @@ void K_MoveKartPlayer(player_t *player, boolean onground) { player->lastringboost = player->ringboost; UINT32 award = 5*player->ringboxaward + 10; - award = 23 * award / 20; // 115% Payout Increase + + if (!modeattacking) + award = 23 * award / 20; // 115% Payout Increase if (!K_ThunderDome()) award = 3 * award / 2; @@ -14053,28 +14139,29 @@ void K_MoveKartPlayer(player_t *player, boolean onground) // To try and help close this gap, we fudge Ring Box payouts to allow weaker characters // better access to things that make them go fast, without changing core handling. + UINT8 speed = player->kartspeed; UINT8 accel = 10-player->kartspeed; UINT8 weight = player->kartweight; // Relative stat power for bonus TA Ring Box awards. // AP 1, WP 2 = weight is worth twice what accel is. // 0 = stat not considered at all! - UINT8 accelPower = 0; - UINT8 weightPower = 4; + UINT8 accelPower = 1; + UINT8 weightPower = 6; UINT8 total = accelPower*accel + weightPower*weight; UINT8 maxtotal = accelPower*9 + weightPower*9; + UINT32 baseaward = award; + // Scale from base payout at 9/1 to max payout at 1/9. - award = Easing_InCubic(FRACUNIT*total/maxtotal, 13*award/10, 18*award/10); + award += Easing_Linear(FRACUNIT*total/maxtotal, 0, 11*baseaward/10); // And, because we don't have to give a damn about sandbagging, up the stakes the longer we progress! if (gametyperules & GTR_CIRCUIT) { - UINT8 maxgrade = 10; - UINT8 margin = min(player->gradingpointnum, maxgrade); - - award = Easing_Linear(FRACUNIT * margin / maxgrade, award, 2*award); + if (K_GetNumGradingPoints()) + award += Easing_Linear(FRACUNIT * player->gradingpointnum / K_GetNumGradingPoints(), 0, baseaward/2); } } else @@ -16475,6 +16562,9 @@ UINT16 K_GetEXP(player_t *player) UINT16 exp = FixedRescale(player->gradingfactor, factormin, factormax, Easing_Linear, targetminexp, targetmaxexp)>>FRACBITS; + if (modeattacking) + exp = 100 * player->gradingpointnum / numgradingpoints; + // CONS_Printf("Player %s numgradingpoints=%d gradingpoint=%d targetminexp=%d targetmaxexp=%d factor=%.2f factormin=%.2f factormax=%.2f exp=%d\n", // player_names[player - players], numgradingpoints, player->gradingpointnum, targetminexp, targetmaxexp, FIXED_TO_FLOAT(player->gradingfactor), FIXED_TO_FLOAT(factormin), FIXED_TO_FLOAT(factormax), exp); diff --git a/src/p_spec.c b/src/p_spec.c index 930b374b9..319cf4d19 100644 --- a/src/p_spec.c +++ b/src/p_spec.c @@ -2038,34 +2038,6 @@ static void K_HandleLapIncrement(player_t *player) K_UpdateAllPlayerPositions(); // P_DoPlayerExit calls this } - if (G_TimeAttackStart() && !linecrossed) - { - linecrossed = leveltime; - if (starttime > leveltime) // Overlong starts shouldn't reset time on cross - { - // Award some Amps for a fast start, to counterbalance Obvious Rainbow Driftboost - - tic_t starthaste = starttime - leveltime; // How much time we had left to cross - starthaste = TIMEATTACK_START - starthaste; // How much time we wasted before crossing - - tic_t leniency = TICRATE*2; // How long we can take to cross with no penalty to amp payout - - if (starthaste <= leniency) - starthaste = 0; - else - starthaste -= leniency; - - fixed_t ampreward = Easing_OutQuart(starthaste*FRACUNIT/TIMEATTACK_START, 100*FRACUNIT, 0); - K_SpawnAmps(player, ampreward/FRACUNIT, player->mo); - - // And reset our time to 0. - starttime = leveltime; - } - if (demo.recording) - demo_extradata[player-players] |= DXD_START; - Music_Stop("position"); - } - if (rainbowstartavailable == true && player->mo->hitlag == 0) { if (K_InRaceDuel()) @@ -2082,7 +2054,8 @@ static void K_HandleLapIncrement(player_t *player) K_SpawnDriftBoostExplosion(player, 4); K_SpawnDriftElectricSparks(player, SKINCOLOR_SILVER, false); - K_SpawnAmps(player, (K_InRaceDuel()) ? 20 : 35, player->mo); + if (!G_TimeAttackStart()) + K_SpawnAmps(player, (K_InRaceDuel()) ? 20 : 35, player->mo); if (g_teamplay) {