diff --git a/src/cvars.cpp b/src/cvars.cpp index 8315d9fd7..2b30a208d 100644 --- a/src/cvars.cpp +++ b/src/cvars.cpp @@ -800,6 +800,7 @@ consvar_t cv_battleufotest = OnlineCheat("battleufotest", "Off").on_off().descri #ifdef DEVELOP consvar_t cv_botcontrol = OnlineCheat("botcontrol", "On").on_off().description("Toggle bot AI movement"); + consvar_t cv_takeover = OnlineCheat("takeover", "Off").on_off().description("Human players use bot movement"); #endif extern CV_PossibleValue_t capsuletest_cons_t[]; diff --git a/src/d_player.h b/src/d_player.h index 1e7d1f8b5..e92d79038 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -339,6 +339,11 @@ typedef enum // Tricks khud_trickcool, + // Exp + khud_oldexp, + khud_exp, + khud_exptimer, + NUMKARTHUD } karthudtype_t; diff --git a/src/deh_tables.c b/src/deh_tables.c index b8ba3ebea..c6d40380a 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -2686,6 +2686,7 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi "S_EGOORB", "S_AMPS", + "S_EXP", "S_WATERTRAIL1", "S_WATERTRAIL2", @@ -4009,6 +4010,7 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_PULLUPHOOK", "MT_AMPS", + "MT_EXP", "MT_FLYBOT767", diff --git a/src/info.c b/src/info.c index 559aeab94..8ac7b4eba 100644 --- a/src/info.c +++ b/src/info.c @@ -600,6 +600,8 @@ char sprnames[NUMSPRITES + 1][5] = "AMPC", "AMPD", + "EXPC", + "SOR_", "WTRL", // Water Trail @@ -3246,6 +3248,7 @@ state_t states[NUMSTATES] = {SPR_EGOO, 0, 1, {NULL}, 0, 0, S_NULL}, // S_EGOORB {SPR_AMPA, FF_FULLBRIGHT|FF_ANIMATE, -1, {NULL}, 41, 1, S_NULL}, // S_AMPS + {SPR_EXPC, FF_FULLBRIGHT, -1, {NULL}, 0, 0, S_NULL}, // S_EXP // Water Trail {SPR_WTRL, FF_PAPERSPRITE , 2, {NULL}, 0, 0, S_NULL}, // S_WATERTRAIL1 @@ -22482,7 +22485,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = S_NULL // raisestate }, { // MT_AMPS - 3444, // doomednum + -1, // doomednum S_AMPS, // spawnstate 1000, // spawnhealth S_NULL, // seestate @@ -22507,6 +22510,32 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = MF_NOGRAVITY|MF_NOCLIPHEIGHT|MF_NOCLIP|MF_NOCLIPTHING, // flags S_NULL // raisestate }, + { // MT_EXP + -1, // doomednum + S_EXP, // spawnstate + 1000, // spawnhealth + S_NULL, // seestate + sfx_None, // seesound + 0, // reactiontime + sfx_None, // attacksound + S_NULL, // painstate + 0, // painchance + sfx_None, // painsound + S_NULL, // meleestate + S_NULL, // missilestate + S_NULL, // deathstate + S_NULL, // xdeathstate + sfx_None, // deathsound + 1, // speed + 32*FRACUNIT, // radius + 32*FRACUNIT, // height + 0, // dispoffset + 0, // mass + 0, // damage + sfx_None, // activesound + MF_NOGRAVITY|MF_NOCLIPHEIGHT|MF_NOCLIP|MF_NOCLIPTHING, // flags + S_NULL // raisestate + }, { // MT_FLYBOT767 -1, // doomednum S_FLYBOT767, // spawnstate diff --git a/src/info.h b/src/info.h index 737daf64b..e6df879b5 100644 --- a/src/info.h +++ b/src/info.h @@ -1141,6 +1141,8 @@ typedef enum sprite SPR_AMPC, SPR_AMPD, + SPR_EXPC, + SPR_SOR_, SPR_WTRL, // Water Trail @@ -3742,6 +3744,7 @@ typedef enum state S_EGOORB, S_AMPS, + S_EXP, S_WATERTRAIL1, S_WATERTRAIL2, @@ -5091,6 +5094,7 @@ typedef enum mobj_type MT_PULLUPHOOK, MT_AMPS, + MT_EXP, MT_FLYBOT767, diff --git a/src/k_bot.cpp b/src/k_bot.cpp index d9d91a134..f553d4856 100644 --- a/src/k_bot.cpp +++ b/src/k_bot.cpp @@ -432,6 +432,11 @@ boolean K_PlayerUsesBotMovement(const player_t *player) if (player->bot) return true; +#ifdef DEVELOP + if (cv_takeover.value) + return true; +#endif + return false; } diff --git a/src/k_bot.h b/src/k_bot.h index 8a4cfe893..6044bbd92 100644 --- a/src/k_bot.h +++ b/src/k_bot.h @@ -24,6 +24,7 @@ extern "C" { #ifdef DEVELOP extern consvar_t cv_botcontrol; + extern consvar_t cv_takeover; #endif // Maximum value of botvars.difficulty diff --git a/src/k_hud.cpp b/src/k_hud.cpp index c30f4b918..4a3c66ac4 100644 --- a/src/k_hud.cpp +++ b/src/k_hud.cpp @@ -3921,7 +3921,7 @@ static boolean K_drawKartLaps(void) INT32 bump = 0; boolean drewsticker = false; - UINT16 displayEXP = stplyr->exp; + UINT16 displayEXP = stplyr->karthud[khud_exp]; // Jesus Christ. // I do not understand the way this system of offsets is laid out at all, @@ -3999,6 +3999,10 @@ static boolean K_drawKartLaps(void) } } + boolean dance = (stplyr->exp > (UINT32)stplyr->karthud[khud_exp]); + INT32 danceflag = dance ? V_STRINGDANCE : 0; + UINT16 dancecolor = dance ? SKINCOLOR_AQUAMARINE : 0; + // EXP if (displayEXP == UINT16_MAX) { @@ -4048,9 +4052,15 @@ static boolean K_drawKartLaps(void) V_DrawMappedPatch(fr, fy, transflag|V_SLIDEIN|splitflags, kp_exp[1], colormap); // EXP - V_DrawScaledPatch(fr+11, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[displayEXP/100]); - V_DrawScaledPatch(fr+15, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[displayEXP/10%10]); - V_DrawScaledPatch(fr+19, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[displayEXP%10]); + + using srb2::Draw; + + Draw row = Draw(fr+11, fy).flags(V_HUDTRANS|V_SLIDEIN|splitflags|danceflag).font(Draw::Font::kPing).colorize(dancecolor); + row.text("{:03}", displayEXP); + + //V_DrawScaledPatch(fr+11, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[displayEXP/100]); + //V_DrawScaledPatch(fr+15, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[displayEXP/10%10]); + //V_DrawScaledPatch(fr+19, fy, V_HUDTRANS|V_SLIDEIN|splitflags, fontv[PINGNUM_FONT].font[displayEXP%10]); } else { @@ -4065,7 +4075,7 @@ static boolean K_drawKartLaps(void) V_DrawMappedPatch(LAPS_X+bump, LAPS_Y, transflag|V_SLIDEIN|splitflags, kp_exp[0], colormap); using srb2::Draw; - Draw row = Draw(LAPS_X+23+bump, LAPS_Y+3).flags(V_HUDTRANS|V_SLIDEIN|splitflags).font(Draw::Font::kThinTimer); + Draw row = Draw(LAPS_X+23+bump, LAPS_Y+3).flags(V_HUDTRANS|V_SLIDEIN|splitflags|danceflag).font(Draw::Font::kThinTimer).colorize(dancecolor); row.text("{:03}", displayEXP); } diff --git a/src/k_kart.c b/src/k_kart.c index a8793558c..25e375001 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -4229,6 +4229,25 @@ void K_SpawnAmps(player_t *player, UINT8 amps, mobj_t *impact) } } +void K_SpawnEXP(player_t *player, UINT8 exp, mobj_t *impact) +{ + if (exp == 0) + return; + + for (int i = 0; i < exp; i++) + { + mobj_t *pickup = P_SpawnMobj(impact->x, impact->y, impact->z, MT_EXP); + pickup->momx = impact->momx; + pickup->momy = impact->momy; + pickup->momz = impact->momz; + pickup->momx += P_RandomRange(PR_ITEM_DEBRIS, -20*mapobjectscale, 20*mapobjectscale); + pickup->momy += P_RandomRange(PR_ITEM_DEBRIS, -20*mapobjectscale, 20*mapobjectscale); + pickup->momz += P_RandomRange(PR_ITEM_DEBRIS, -20*mapobjectscale, 20*mapobjectscale); + // pickup->color = player->skincolor; + P_SetTarget(&pickup->target, player->mo); + } +} + void K_AwardPlayerAmps(player_t *player, UINT8 amps) { UINT16 getamped = player->amps + amps; @@ -8879,6 +8898,31 @@ void K_KartPlayerHUDUpdate(player_t *player) if (player->positiondelay) player->positiondelay--; + if (player->exp != (UINT32)player->karthud[khud_oldexp]) + { + if (player->exp <= (UINT32)player->karthud[khud_oldexp]) + { + player->karthud[khud_oldexp] = 0; + player->karthud[khud_exp] = 0; + player->karthud[khud_exptimer] = 0; + } + else + { + INT32 delta = player->exp - player->karthud[khud_exp]; + INT32 speed = max(1, 10-delta); + + player->karthud[khud_exptimer]++; + + if (player->karthud[khud_exptimer] >= speed) + { + player->karthud[khud_exp]++; + player->karthud[khud_exptimer] = 0; + if (player->exp == (UINT32)player->karthud[khud_exp]) + player->karthud[khud_oldexp] = player->exp; + } + } + } + if (!(player->pflags & PF_FAULT || player->pflags & PF_VOID)) player->karthud[khud_fault] = 0; else if (player->karthud[khud_fault] > 0 && player->karthud[khud_fault] <= 2*TICRATE) diff --git a/src/k_kart.h b/src/k_kart.h index 738513358..1b2804e93 100644 --- a/src/k_kart.h +++ b/src/k_kart.h @@ -159,6 +159,7 @@ angle_t K_MomentumAngleReal(const mobj_t *mo); #define K_MomentumAngle(mo) K_MomentumAngleEx(mo, 6 * mo->scale) boolean K_PvPAmpReward(UINT32 award, player_t *attacker, player_t *defender); void K_SpawnAmps(player_t *player, UINT8 amps, mobj_t *impact); +void K_SpawnEXP(player_t *player, UINT8 exp, mobj_t *impact); void K_AwardPlayerAmps(player_t *player, UINT8 amps); void K_CheckpointCrossAward(player_t *player); void K_AwardPlayerRings(player_t *player, UINT16 rings, boolean overload); diff --git a/src/k_objects.h b/src/k_objects.h index b1b9b8a93..f5c36da12 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -146,6 +146,8 @@ void Obj_AmpBurstThink(mobj_t *amp); void Obj_AmpsThink(mobj_t *amps); +void Obj_ExpThink(mobj_t *exp); + void Obj_ChargeAuraThink(mobj_t *aura); void Obj_ChargeFallThink(mobj_t *charge); void Obj_ChargeReleaseThink(mobj_t *release); diff --git a/src/k_race.c b/src/k_race.c index b41e55021..cbdc28e77 100644 --- a/src/k_race.c +++ b/src/k_race.c @@ -468,3 +468,18 @@ UINT8 K_RaceLapCount(INT16 mapNum) return cv_numlaps.value; } + +void K_SpawnFinishEXP(player_t *player, UINT16 exp) +{ + if (finishBeamLine != NULL) + { + mobj_t *p1 = P_SpawnMobj(finishBeamLine->v1->x, finishBeamLine->v1->y, player->mo->z, MT_THOK); + mobj_t *p2 = P_SpawnMobj(finishBeamLine->v2->x, finishBeamLine->v2->y, player->mo->z, MT_THOK); + K_SpawnEXP(player, exp, p1); + K_SpawnEXP(player, exp, p2); + } + else + { + K_SpawnEXP(player, exp*2, player->mo); + } +} diff --git a/src/k_race.h b/src/k_race.h index 542acacb0..c010e45eb 100644 --- a/src/k_race.h +++ b/src/k_race.h @@ -85,6 +85,8 @@ void K_RunFinishLineBeam(void); UINT8 K_RaceLapCount(INT16 mapNum); +void K_SpawnFinishEXP(player_t *player, UINT16 exp); + #ifdef __cplusplus } // extern "C" diff --git a/src/objects/CMakeLists.txt b/src/objects/CMakeLists.txt index f050047ca..8d339d8f4 100644 --- a/src/objects/CMakeLists.txt +++ b/src/objects/CMakeLists.txt @@ -65,6 +65,7 @@ target_sources(SRB2SDL2 PRIVATE lightning-shield.cpp flame-shield.cpp stone-shoe.cpp + exp.c ) add_subdirectory(versus) diff --git a/src/objects/checkpoint.cpp b/src/objects/checkpoint.cpp index 81b546400..0d2883a4f 100644 --- a/src/objects/checkpoint.cpp +++ b/src/objects/checkpoint.cpp @@ -688,8 +688,17 @@ void Obj_CrossCheckpoints(player_t* player, fixed_t old_x, fixed_t old_y) player->checkpointId = chk->id(); + UINT16 oldexp = player->exp; + K_CheckpointCrossAward(player); + if (player->exp > oldexp) + { + UINT16 expdiff = (player->exp - oldexp); + K_SpawnEXP(player, expdiff, chk); + K_SpawnEXP(player, expdiff, chk->other()); + } + K_UpdatePowerLevels(player, player->laps, false); } diff --git a/src/objects/exp.c b/src/objects/exp.c new file mode 100644 index 000000000..f0e1d4f3f --- /dev/null +++ b/src/objects/exp.c @@ -0,0 +1,143 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2025 by AJ "Tyron" Martinez. +// Copyright (C) 2025 by Kart Krew +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file amps.c +/// \brief EXP VFX code. + +#include "../doomdef.h" +#include "../info.h" +#include "../k_objects.h" +#include "../p_local.h" +#include "../k_kart.h" +#include "../k_powerup.h" +#include "../m_random.h" +#include "../r_main.h" +#include "../m_easing.h" +#include "../s_sound.h" +#include "../sounds.h" + +#define EXP_ARCTIME (8) +#define EXP_ORBIT (100) + +static void ghostme(mobj_t *exp, player_t *player) +{ + if (exp->cusval%2) + return; + + mobj_t *ghost = P_SpawnGhostMobj(exp); + ghost->colorized = true; + ghost->color = player->skincolor; + ghost->renderflags |= RF_ADD; + ghost->fuse = 2; +} + +void Obj_ExpThink (mobj_t *exp) +{ + if (P_MobjWasRemoved(exp->target) + || exp->target->health == 0 + || exp->target->destscale <= 1 // sealed star fall out + || !exp->target->player) + { + P_RemoveMobj(exp); + } + else + { + mobj_t *mo = exp->target; + player_t *player = mo->player; + + fixed_t dist, fakez; + angle_t hang, vang; + + dist = P_AproxDistance(P_AproxDistance(exp->x - mo->x, exp->y - mo->y), exp->z - mo->z); + + exp->renderflags |= RF_DONTDRAW; + exp->renderflags &= ~K_GetPlayerDontDrawFlag(player); + + // K_MatchGenericExtraFlags(exp, mo); + + exp->cusval++; + + // bullshit copypaste orbit behavior + if (exp->threshold) + { + fixed_t orbit = (4*mo->scale) * (16 - exp->extravalue1); + + P_SetScale(exp, (exp->destscale = mapobjectscale - ((mapobjectscale/28) * exp->extravalue1))); + exp->z = exp->target->z; + P_MoveOrigin(exp, + mo->x + FixedMul(orbit, FINECOSINE(exp->angle >> ANGLETOFINESHIFT)), + mo->y + FixedMul(orbit, FINESINE(exp->angle >> ANGLETOFINESHIFT)), + exp->z + mo->scale * 24 * P_MobjFlip(exp)); + + exp->momx = 0; + exp->momy = 0; + exp->momz = 0; + + ghostme(exp, player); + + exp->angle += ANG30; + exp->extravalue1++; + + if (exp->extravalue1 >= 16) + P_RemoveMobj(exp); + + return; + } + + exp->angle += ANGLE_45/2; + + UINT8 damper = 3; + + fixed_t vert = dist/3; + fixed_t speed = 60*exp->scale; + + if (exp->extravalue2) // Mode: going down, aim at the player and speed up / dampen stray movement + { + if (exp->extravalue1) + exp->extravalue1--; + + exp->extravalue2++; + + speed += exp->extravalue2 * exp->scale/2; + + fakez = mo->z + (vert * exp->extravalue1 / EXP_ARCTIME); + damper = 1; + } + else // Mode: going up, aim above the player + { + exp->extravalue1++; + if (exp->extravalue1 >= EXP_ARCTIME) + exp->extravalue2 = 1; + + fakez = mo->z + vert; + } + + if (mo->flags & MFE_VERTICALFLIP) + fakez -= mo->height/2; + else + fakez += mo->height/2; + + hang = R_PointToAngle2(exp->x, exp->y, mo->x, mo->y); + vang = R_PointToAngle2(exp->z, 0, fakez, dist); + + exp->momx -= exp->momx>>(damper), exp->momy -= exp->momy>>(damper), exp->momz -= exp->momz>>(damper); + exp->momx += FixedMul(FINESINE(vang>>ANGLETOFINESHIFT), FixedMul(FINECOSINE(hang>>ANGLETOFINESHIFT), speed)); + exp->momy += FixedMul(FINESINE(vang>>ANGLETOFINESHIFT), FixedMul(FINESINE(hang>>ANGLETOFINESHIFT), speed)); + exp->momz += FixedMul(FINECOSINE(vang>>ANGLETOFINESHIFT), speed); + + ghostme(exp, player); + + if (dist < (EXP_ORBIT * exp->scale) && exp->extravalue2) + { + exp->threshold = TICRATE; + exp->extravalue1 = 0; + exp->extravalue2 = 0; + } + } +} \ No newline at end of file diff --git a/src/p_mobj.c b/src/p_mobj.c index a4011d24d..da3e08bc6 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -8908,6 +8908,11 @@ static boolean P_MobjRegularThink(mobj_t *mobj) Obj_AmpsThink(mobj); break; } + case MT_EXP: + { + Obj_ExpThink(mobj); + break; + } case MT_BLOCKRING: { Obj_BlockRingThink(mobj); diff --git a/src/p_spec.c b/src/p_spec.c index eb30cc265..805052097 100644 --- a/src/p_spec.c +++ b/src/p_spec.c @@ -54,6 +54,7 @@ #include "music.h" #include "k_battle.h" // battleprisons #include "k_endcam.h" // K_EndCameraIsFreezing() +#include "k_race.h" // K_SpawnFinishEXP // Not sure if this is necessary, but it was in w_wad.c, so I'm putting it here too -Shadow Hog #include @@ -2153,8 +2154,14 @@ static void K_HandleLapIncrement(player_t *player) // Update power levels for this lap. K_UpdatePowerLevels(player, player->laps, false); + UINT16 oldexp = player->exp; K_CheckpointCrossAward(player); + if (player->exp > oldexp) + { + K_SpawnFinishEXP(player, player->exp - oldexp); + } + if (player->position == 1 && !(gametyperules & GTR_CHECKPOINTS)) { Obj_DeactivateCheckpoints();