diff --git a/src/d_player.h b/src/d_player.h index 56e52d849..4f848fd08 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -408,6 +408,7 @@ struct botvars_t // All entries above persist between rounds and must be recorded in demos fixed_t rubberband; // Bot rubberband value + UINT8 bumpslow; tic_t itemdelay; // Delay before using item at all tic_t itemconfirm; // When high enough, they will use their item diff --git a/src/deh_tables.c b/src/deh_tables.c index ae1abcb84..8369624ec 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -2028,6 +2028,15 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi "S_BUBBLESHIELDWAVE5", "S_BUBBLESHIELDWAVE6", + // Bubble Shield Visuals + "S_BUBA1", + "S_BUBB1", + "S_BUBB2", + "S_BUBC1", + "S_BUBC2", + "S_BUBD1", + "S_BUBE1", + // Flame Shield "S_FLAMESHIELD1", "S_FLAMESHIELD2", @@ -3616,6 +3625,7 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_LIGHTNINGSHIELD", // Shields "MT_BUBBLESHIELD", + "MT_BUBBLESHIELD_VISUAL", "MT_FLAMESHIELD", "MT_FLAMESHIELDUNDERLAY", "MT_FLAMESHIELDPAPER", diff --git a/src/info.c b/src/info.c index faed52546..1973faecc 100644 --- a/src/info.c +++ b/src/info.c @@ -376,6 +376,11 @@ char sprnames[NUMSPRITES + 1][5] = "TRNQ", // SPB Manta Ring loop "THNS", // Lightning Shield "BUBS", // Bubble Shield (not Bubs) + "BUBA", // Bubble Shield Outline + "BUBB", // Bubble Shield Top Wave + "BUBC", // Bubble Shield Bottom Wave + "BUBD", // Bubble Shield Reflection + "BUBE", // Bubble Shield Underline "BWVE", // Bubble Shield waves "FLMS", // Flame Shield "FLMD", // Flame Shield dash @@ -2564,6 +2569,15 @@ state_t states[NUMSTATES] = {SPR_BWVE, FF_FULLBRIGHT|4, 1, {NULL}, 0, 0, S_BUBBLESHIELDWAVE6}, // S_BUBBLESHIELDWAVE5 {SPR_BWVE, FF_FULLBRIGHT|5, 1, {NULL}, 0, 0, S_NULL}, // S_BUBBLESHIELDWAVE6 + // Bubble Shield Visuals + {SPR_BUBA, FF_FULLBRIGHT, 1, {NULL}, 0, 0, S_BUBA1}, // S_BUBA1 + {SPR_BUBB, FF_FULLBRIGHT|FF_ANIMATE, 36, {NULL}, 8, 4, S_BUBB1}, // S_BUBB1 + {SPR_NULL, 0, 5, {NULL}, 0, 0, S_BUBB1}, // S_BUBB2 + {SPR_BUBC, FF_FULLBRIGHT|FF_ANIMATE, 36, {NULL}, 8, 4, S_BUBC1}, // S_BUBC1 + {SPR_NULL, 0, 5, {NULL}, 0, 0, S_BUBC1}, // S_BUBC2 + {SPR_BUBD, FF_FULLBRIGHT, 1, {NULL}, 0, 0, S_BUBD1}, // S_BUBD1 + {SPR_BUBE, FF_FULLBRIGHT, 1, {NULL}, 0, 0, S_BUBE1}, // S_BUBE1 + {SPR_FLMS, FF_FULLBRIGHT, 2, {NULL}, 0, 0, S_FLAMESHIELD2}, // S_FLAMESHIELD1 {SPR_FLMS, FF_FULLBRIGHT|9, 2, {NULL}, 0, 0, S_FLAMESHIELD3}, // S_FLAMESHIELD2 {SPR_FLMS, FF_FULLBRIGHT|1, 2, {NULL}, 0, 0, S_FLAMESHIELD4}, // S_FLAMESHIELD3 @@ -15395,6 +15409,33 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = S_NULL // raisestate }, + { // MT_BUBBLESHIELD_VISUAL + -1, // doomednum + S_INVISIBLE, // spawnstate + 1000, // spawnhealth + S_NULL, // seestate + sfx_None, // seesound + 8, // 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 + 8, // speed + 28*FRACUNIT, // radius + 56*FRACUNIT, // height + 1, // display offset + 16, // mass + 0, // damage + sfx_None, // activesound + MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPTHING|MF_NOCLIPHEIGHT|MF_NOGRAVITY|MF_SCENERY|MF_DONTENCOREMAP, // flags + S_NULL // raisestate + }, + { // MT_FLAMESHIELD -1, // doomednum S_FLAMESHIELD1, // spawnstate diff --git a/src/info.h b/src/info.h index a525bfd50..9b26cad1c 100644 --- a/src/info.h +++ b/src/info.h @@ -915,6 +915,11 @@ typedef enum sprite SPR_TRNQ, // SPB Manta Ring loop SPR_THNS, // Thunder Shield SPR_BUBS, // Bubble Shield (not Bubs) + SPR_BUBA, // Bubble Shield Outline + SPR_BUBB, // Bubble Shield Top Wave + SPR_BUBC, // Bubble Shield Bottom Wave + SPR_BUBD, // Bubble Shield Reflection + SPR_BUBE, // Bubble Shield Underline SPR_BWVE, // Bubble Shield waves SPR_FLMS, // Flame Shield SPR_FLMD, // Flame Shield dash @@ -3059,6 +3064,15 @@ typedef enum state S_BUBBLESHIELDWAVE5, S_BUBBLESHIELDWAVE6, + // Bubble Shield Visuals + S_BUBA1, + S_BUBB1, + S_BUBB2, + S_BUBC1, + S_BUBC2, + S_BUBD1, + S_BUBE1, + // Flame Shield S_FLAMESHIELD1, S_FLAMESHIELD2, @@ -4674,6 +4688,7 @@ typedef enum mobj_type MT_LIGHTNINGSHIELD, // Shields MT_BUBBLESHIELD, + MT_BUBBLESHIELD_VISUAL, MT_FLAMESHIELD, MT_FLAMESHIELDUNDERLAY, MT_FLAMESHIELDPAPER, diff --git a/src/k_bot.cpp b/src/k_bot.cpp index c55766189..041a0be38 100644 --- a/src/k_bot.cpp +++ b/src/k_bot.cpp @@ -803,8 +803,13 @@ fixed_t K_UpdateRubberband(player_t *player) fixed_t dest = K_BotRubberband(player); fixed_t ret = player->botvars.rubberband; + UINT8 ease_soften = 8; + + if (player->botvars.bumpslow && dest > ret) + ease_soften *= 10; + // Ease into the new value. - ret += (dest - player->botvars.rubberband) / 8; + ret += (dest - player->botvars.rubberband) / ease_soften; return ret; } diff --git a/src/k_kart.c b/src/k_kart.c index 47b80f23d..fc0c41553 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -9305,6 +9305,9 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) player->flashing = K_GetKartFlashing(player); } + if (player->spinouttimer && player->respawn.state != RESPAWNST_NONE) + player->spinouttimer = 0; + if (player->spinouttimer) { if (((P_IsObjectOnGround(player->mo) @@ -9509,6 +9512,9 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) if (player->trickboost) player->trickboost--; + if (K_PlayerUsesBotMovement(players) && player->botvars.bumpslow && player->incontrol) + player->botvars.bumpslow--; + if (player->flamedash) { player->flamedash--; @@ -14126,6 +14132,8 @@ void K_MoveKartPlayer(player_t *player, boolean onground) P_SetTarget(&shield->target, player->mo); S_StartSound(player->mo, sfx_s3k3f); player->curshield = KSHIELD_BUBBLE; + + Obj_SpawnBubbleShieldVisuals(shield); } if (!HOLDING_ITEM && NO_HYUDORO) @@ -15554,6 +15562,15 @@ UINT32 K_GetNumGradingPoints(void) return numlaps * (1 + Obj_GetCheckpointCount()); } +void K_BotHitPenalty(player_t *player) +{ + if (K_PlayerUsesBotMovement(player)) + { + player->botvars.rubberband = max(player->botvars.rubberband/2, FRACUNIT/2); + player->botvars.bumpslow = TICRATE*2; + } +} + static boolean K_PickUp(player_t *player, mobj_t *picked) { SINT8 type = -1; diff --git a/src/k_kart.h b/src/k_kart.h index 8a6a0832c..4422253ff 100644 --- a/src/k_kart.h +++ b/src/k_kart.h @@ -317,6 +317,9 @@ UINT16 K_GetDisplayEXP(player_t *player); UINT32 K_GetNumGradingPoints(void); boolean K_LegacyRingboost(player_t *player); + +void K_BotHitPenalty(player_t *player); + boolean K_TryPickMeUp(mobj_t *m1, mobj_t *m2); #ifdef __cplusplus diff --git a/src/k_objects.h b/src/k_objects.h index 0d75c222a..ed0d62022 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -449,6 +449,10 @@ UINT8 K_HogChargeToHogCount(INT32 charge, UINT8 cap); void K_UpdateBallhogReticules(player_t *player, UINT8 num_hogs, boolean on_release); void K_DoBallhogAttack(player_t *player, UINT8 num_hogs); +/* Bubble Shield */ +void Obj_SpawnBubbleShieldVisuals(mobj_t *source); +boolean Obj_TickBubbleShieldVisual(mobj_t *mobj); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/objects/CMakeLists.txt b/src/objects/CMakeLists.txt index 35ee3ad6c..01c773b7d 100644 --- a/src/objects/CMakeLists.txt +++ b/src/objects/CMakeLists.txt @@ -60,6 +60,7 @@ target_sources(SRB2SDL2 PRIVATE pulley.cpp amps.c ballhog.cpp + bubble-shield.cpp flybot767.c ) diff --git a/src/objects/bubble-shield.cpp b/src/objects/bubble-shield.cpp new file mode 100644 index 000000000..89d60e129 --- /dev/null +++ b/src/objects/bubble-shield.cpp @@ -0,0 +1,136 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2025 by James Robert Roman +// 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. +//----------------------------------------------------------------------------- + +#include + +#include "objects.hpp" + +#include "../m_easing.h" +#include "../m_fixed.h" +#include "../tables.h" + +using namespace srb2::objects; + +namespace +{ + +struct Bubble : Mobj +{ + static constexpr fixed_t kBaseScale = 5*FRACUNIT/4; + static constexpr fixed_t kMaxScale = 5*FRACUNIT; + static constexpr fixed_t kScaleRange = kMaxScale - kBaseScale; + + void target() = delete; + Mobj* follow() const { return Mobj::target(); } + void follow(Mobj* n) { Mobj::target(n); } + + player_t* player() const { return follow()->player; } + + bool valid() const { return Mobj::valid(follow()) && player(); } +}; + +struct Visual : Mobj +{ + void target() = delete; + Bubble* bubble() const { return Mobj::target(); } + void bubble(Bubble* n) { Mobj::target(n); } + + void extravalue1() = delete; + Fixed prev_scale() const { return Mobj::extravalue1; } + void prev_scale(Fixed n) { Mobj::extravalue1 = n; } + + bool valid() const { return Mobj::valid(bubble()) && bubble()->valid(); } + + static void spawn + ( Bubble* bubble, + statenum_t state, + int flicker, + int offset) + { + if (!bubble->valid()) + return; + + Visual* x = Mobj::spawn(bubble->pos(), MT_BUBBLESHIELD_VISUAL); + //x->scale(5 * x->scale() / 4); + x->state(state); + x->spriteyoffset(22*FRACUNIT); + x->bubble(bubble); + x->linkdraw(bubble->follow(), offset); + + if (flicker) + x->renderflags |= RF_DONTDRAW; + } + + bool tick() + { + if (!valid()) + { + remove(); + return false; + } + + move_origin(bubble()->pos()); + + renderflags = ((renderflags ^ RF_DONTDRAW) & RF_DONTDRAW); + + // ATTENTION: this object relies on the original MT_BUBBLESHIELD object for scale + fixed_t f = Fixed {bubble()->scale() / bubble()->follow()->scale() - Bubble::kBaseScale} / Fixed {Bubble::kScaleRange}; + + scale(Easing_Linear(f, + bubble()->follow()->scale() * Fixed {Bubble::kBaseScale}, + bubble()->follow()->scale() * 4)); + + if (sprite != SPR_BUBB && + sprite != SPR_BUBC && + bubble()->player()->bubblecool && + f == 0) // base size + renderflags |= RF_DONTDRAW; + + if (scale() > prev_scale()) + spritescale({ 3*FRACUNIT/2, 3*FRACUNIT/4 }); + else if (scale() < prev_scale()) + spritescale({ 3*FRACUNIT/4, 3*FRACUNIT/2 }); + else + { + if (f == FRACUNIT) // max size + { + if (leveltime & 1) + spritescale({ 3*FRACUNIT/4, 3*FRACUNIT/2 }); + else + { + spritescale({ FRACUNIT, FRACUNIT }); + renderflags |= RF_ADD; + } + } + else + spritescale({ FRACUNIT, FRACUNIT }); + } + + prev_scale(scale()); + + return true; + } +}; + +}; // namespace + +void Obj_SpawnBubbleShieldVisuals(mobj_t *bubble) +{ + Visual::spawn(static_cast(bubble), S_BUBA1, 1, 2); + Visual::spawn(static_cast(bubble), S_BUBB1, 0, 1); + Visual::spawn(static_cast(bubble), S_BUBC1, 1, -1); + Visual::spawn(static_cast(bubble), S_BUBD1, 0, -2); + Visual::spawn(static_cast(bubble), S_BUBE1, 1, -3); +} + +boolean Obj_TickBubbleShieldVisual(mobj_t *mobj) +{ + return static_cast(mobj)->tick(); +} diff --git a/src/p_inter.c b/src/p_inter.c index fd30df42a..11f59d73a 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -2600,20 +2600,20 @@ static boolean P_KillPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, player->roundconditions.checkthisframe = true; } - if (gametyperules & (GTR_BUMPERS|GTR_CHECKPOINTS)) + if ((player->pitblame > -1) && (player->pitblame < MAXPLAYERS) + && (playeringame[player->pitblame]) && (!players[player->pitblame].spectator) + && (players[player->pitblame].mo) && (!P_MobjWasRemoved(players[player->pitblame].mo))) { - if ((player->pitblame > -1) && (player->pitblame < MAXPLAYERS) - && (playeringame[player->pitblame]) && (!players[player->pitblame].spectator) - && (players[player->pitblame].mo) && (!P_MobjWasRemoved(players[player->pitblame].mo))) - { + if (gametyperules & (GTR_BUMPERS|GTR_CHECKPOINTS)) P_DamageMobj(player->mo, players[player->pitblame].mo, players[player->pitblame].mo, 1, DMG_KARMA); - player->pitblame = -1; - } - else if (player->mo->health > 1 || K_Cooperative()) - { + else + K_SpawnAmps(&players[player->pitblame], 20, player->mo); + player->pitblame = -1; + } + else if (player->mo->health > 1 || K_Cooperative()) + { + if (gametyperules & (GTR_BUMPERS|GTR_CHECKPOINTS)) player->mo->health--; - } - } if (modeattacking & ATTACKING_SPB) @@ -3204,6 +3204,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da if (source && source != player->mo && source->player) { K_SpawnAmps(source->player, K_PvPAmpReward((type == DMG_WHUMBLE) ? 30 : 20, source->player, player), target); + K_BotHitPenalty(player); // Extend the invincibility if the hit was a direct hit. if (inflictor == source && source->player->invincibilitytimer && diff --git a/src/p_map.c b/src/p_map.c index 1289dc71f..68c487acf 100644 --- a/src/p_map.c +++ b/src/p_map.c @@ -4118,6 +4118,8 @@ static void P_BouncePlayerMove(mobj_t *mo, TryMoveResult_t *result) if (mo->player) mo->player->bumpUnstuck += 5; + K_BotHitPenalty(mo->player); + // Combo avoidance! if (mo->player && P_PlayerInPain(mo->player) && gametyperules & GTR_BUMPERS && mo->health == 1) { diff --git a/src/p_mobj.c b/src/p_mobj.c index 683163ee8..7e370eb1e 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -6658,6 +6658,14 @@ static void P_MobjSceneryThink(mobj_t *mobj) Obj_PulleyThink(mobj); return; } + case MT_BUBBLESHIELD_VISUAL: + { + if (!Obj_TickBubbleShieldVisual(mobj)) + { + return; + } + break; + } default: if (mobj->fuse) { // Scenery object fuse! Very basic! @@ -8534,6 +8542,12 @@ static boolean P_MobjRegularThink(mobj_t *mobj) return false; } + // FIXME: Old Bubble Shield code is still running. + // Some of it is visual, some gameplay. + // I left it alone and just tell it to go invisible~ + // See objects/bubble-shield.cpp + mobj->renderflags |= RF_DONTDRAW; + scale = (5*mobj->target->scale)>>2; curstate = ((mobj->tics == 1) ? (mobj->state->nextstate) : ((statenum_t)(mobj->state-states))); diff --git a/src/p_saveg.cpp b/src/p_saveg.cpp index 591613d6f..a4f5b269b 100644 --- a/src/p_saveg.cpp +++ b/src/p_saveg.cpp @@ -737,6 +737,7 @@ static void P_NetArchivePlayers(savebuffer_t *save) WRITEUINT8(save->p, players[i].botvars.diffincrease); WRITEUINT8(save->p, players[i].botvars.rival); WRITEFIXED(save->p, players[i].botvars.rubberband); + WRITEUINT8(save->p, players[i].botvars.bumpslow); WRITEUINT32(save->p, players[i].botvars.itemdelay); WRITEUINT32(save->p, players[i].botvars.itemconfirm); WRITESINT8(save->p, players[i].botvars.turnconfirm); @@ -1380,6 +1381,7 @@ static void P_NetUnArchivePlayers(savebuffer_t *save) players[i].botvars.diffincrease = READUINT8(save->p); players[i].botvars.rival = (boolean)READUINT8(save->p); players[i].botvars.rubberband = READFIXED(save->p); + players[i].botvars.bumpslow = READUINT8(save->p); players[i].botvars.itemdelay = READUINT32(save->p); players[i].botvars.itemconfirm = READUINT32(save->p); players[i].botvars.turnconfirm = READSINT8(save->p);