diff --git a/src/d_player.h b/src/d_player.h index 64547b348..0c2e0e19b 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -724,6 +724,8 @@ struct player_t UINT8 noEbrakeMagnet; // Briefly disable 2.2 responsive ebrake if you're bumped by another player. UINT8 tumbleBounces; UINT16 tumbleHeight; // In *mobjscaled* fracunits, or mfu, not raw fu + UINT16 stunned; // Number of tics during which rings cannot be picked up + UINT8 stunnedCombo; // Number of hits sustained while stunned, reduces consecutive stun penalties UINT8 justDI; // Turn-lockout timer to briefly prevent unintended turning after DI, resets when actionable or no input boolean flipDI; // Bananas flip the DI direction. Was a bug, but it made bananas much more interesting. diff --git a/src/deh_tables.c b/src/deh_tables.c index 44195a340..773b47b90 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -3081,6 +3081,9 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi "S_BADNIK_EXPLOSION_SHOCKWAVE2", "S_BADNIK_EXPLOSION1", "S_BADNIK_EXPLOSION2", + + // Flybot767 (stun) + "S_FLYBOT767", }; // RegEx to generate this from info.h: ^\tMT_([^,]+), --> \t"MT_\1", @@ -3973,6 +3976,8 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_PULLUPHOOK", "MT_AMPS", + + "MT_FLYBOT767", }; const char *const MOBJFLAG_LIST[] = { diff --git a/src/info.c b/src/info.c index 902575c82..faed52546 100644 --- a/src/info.c +++ b/src/info.c @@ -771,6 +771,9 @@ char sprnames[NUMSPRITES + 1][5] = "DIEM", // smoke "DIEN", // explosion + // Flybot767 (stun) + "STUN", + // Pulley "HCCH", "HCHK", @@ -3632,6 +3635,9 @@ state_t states[NUMSTATES] = {SPR_NULL, 0, 1, {A_PlaySound}, sfx_s3k3d, 1, S_BATTLEBUMPER_EXBLAST1}, // S_BADNIK_EXPLOSION_SHOCKWAVE2 {SPR_NULL, 0, 1, {NULL}, 0, 0, S_BADNIK_EXPLOSION2}, // S_BADNIK_EXPLOSION1 {SPR_WIPD, FF_FULLBRIGHT|FF_RANDOMANIM|FF_ANIMATE, 30, {NULL}, 9, 3, S_NULL}, // S_BADNIK_EXPLOSION2 + + // Flybot767 (stun) + {SPR_STUN, FF_FULLBRIGHT|FF_ANIMATE, -1, {NULL}, 4, 4, S_NULL}, // S_FLYBOT767 }; mobjinfo_t mobjinfo[NUMMOBJTYPES] = @@ -22320,6 +22326,32 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = MF_NOGRAVITY|MF_NOCLIPHEIGHT|MF_NOCLIP|MF_NOCLIPTHING, // flags S_NULL // raisestate }, + { // MT_FLYBOT767 + -1, // doomednum + S_FLYBOT767, // 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_pop, // deathsound + 4*FRACUNIT, // speed + 32*FRACUNIT, // radius + 15*FRACUNIT, // height + 0, // dispoffset + 0, // mass + 0, // damage + sfx_None, // activesound + MF_NOBLOCKMAP|MF_NOGRAVITY|MF_NOCLIPHEIGHT|MF_NOCLIP|MF_NOCLIPTHING, // flags + S_NULL // raisestate + }, }; diff --git a/src/info.h b/src/info.h index 0345073b7..a525bfd50 100644 --- a/src/info.h +++ b/src/info.h @@ -1306,6 +1306,9 @@ typedef enum sprite SPR_DIEM, // smoke SPR_DIEN, // explosion + // Flybot767 (stun) + SPR_STUN, + // Pulley SPR_HCCH, SPR_HCHK, @@ -4117,6 +4120,9 @@ typedef enum state S_BADNIK_EXPLOSION1, S_BADNIK_EXPLOSION2, + // Flybot767 (stun) + S_FLYBOT767, + S_FIRSTFREESLOT, S_LASTFREESLOT = S_FIRSTFREESLOT + NUMSTATEFREESLOTS - 1, NUMSTATES @@ -5032,6 +5038,8 @@ typedef enum mobj_type MT_AMPS, + MT_FLYBOT767, + MT_FIRSTFREESLOT, MT_LASTFREESLOT = MT_FIRSTFREESLOT + NUMMOBJFREESLOTS - 1, NUMMOBJTYPES diff --git a/src/k_kart.c b/src/k_kart.c index 5173fa3eb..0716ab7e5 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -9377,6 +9377,33 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) if (player->ringdelay) player->ringdelay--; + if ((player->stunned > 0) + && (player->respawn.state == RESPAWNST_NONE) + && !P_PlayerInPain(player) + && P_IsObjectOnGround(player->mo) + ) + { + // MEGA FUCKING HACK BECAUSE P_SAVEG MOBJS ARE FULL + // Would updating player_saveflags to 32 bits have any negative consequences? + // For now, player->stunned 16th bit is a flag to determine whether the flybots were spawned + + // timer counts down at triple speed while spindashing + player->stunned = (player->stunned & 0x8000) | max(0, (player->stunned & 0x7FFF) - (player->spindash ? 3 : 1)); + + // when timer reaches 0, reset the flag and stun combo counter + if ((player->stunned & 0x7FFF) == 0) + { + player->stunned = 0; + player->stunnedCombo = 0; + } + // otherwise if the flybots aren't spawned, spawn them now! + else if ((player->stunned & 0x8000) == 0) + { + player->stunned |= 0x8000; + Obj_SpawnFlybotsForPlayer(player); + } + } + if (player->trickpanel == TRICKSTATE_READY) { if (!player->throwdir && !cmd->turning) diff --git a/src/k_objects.h b/src/k_objects.h index c661e47d2..0d75c222a 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -435,6 +435,11 @@ boolean Obj_DestroyKart(mobj_t *kart); void Obj_DestroyedKartParticleThink(mobj_t *part); void Obj_DestroyedKartParticleLanding(mobj_t *part); +/* Flybot767 (stun) */ +void Obj_SpawnFlybotsForPlayer(player_t *player); +void Obj_FlybotThink(mobj_t *flybot); +void Obj_FlybotDeath(mobj_t *flybot); + /* Pulley */ void Obj_PulleyThink(mobj_t *root); void Obj_PulleyHookTouch(mobj_t *special, mobj_t *toucher); diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c index 62eca4611..f4a53be32 100644 --- a/src/lua_playerlib.c +++ b/src/lua_playerlib.c @@ -260,6 +260,10 @@ static int player_get(lua_State *L) lua_pushinteger(L, plr->tumbleBounces); else if (fastcmp(field,"tumbleheight")) lua_pushinteger(L, plr->tumbleHeight); + else if (fastcmp(field,"stunned")) + lua_pushinteger(L, plr->stunned); + else if (fastcmp(field,"stunnedcombo")) + lua_pushinteger(L, plr->stunnedCombo); else if (fastcmp(field,"justdi")) lua_pushinteger(L, plr->justDI); else if (fastcmp(field,"flipdi")) @@ -872,6 +876,10 @@ static int player_set(lua_State *L) plr->tumbleBounces = luaL_checkinteger(L, 3); else if (fastcmp(field,"tumbleheight")) plr->tumbleHeight = luaL_checkinteger(L, 3); + else if (fastcmp(field,"stunned")) + plr->stunned = luaL_checkinteger(L, 3); + else if (fastcmp(field,"stunnedcombo")) + plr->stunnedCombo = luaL_checkinteger(L, 3); else if (fastcmp(field,"justdi")) plr->justDI = luaL_checkinteger(L, 3); else if (fastcmp(field,"flipdi")) diff --git a/src/objects/CMakeLists.txt b/src/objects/CMakeLists.txt index 593d5b8f6..35ee3ad6c 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 + flybot767.c ) add_subdirectory(versus) diff --git a/src/objects/flybot767.c b/src/objects/flybot767.c new file mode 100644 index 000000000..790276387 --- /dev/null +++ b/src/objects/flybot767.c @@ -0,0 +1,142 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2025 by Lachlan "Lach" Wright +// 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 flybot767.c +/// \brief Flybot767 object code. + +#include "../p_local.h" +#include "../k_kart.h" +#include "../k_objects.h" +#include "../s_sound.h" +#include "../m_easing.h" + +#define FLYBOT_QUANTITY 2 +#define FLYBOT_VERTICAL_OFFSET (16 * FRACUNIT) +#define FLYBOT_BOB_AMPLITUDE (16 * FRACUNIT) +#define FLYBOT_BOB_FREQUENCY (ANG15) +#define FLYBOT_FADE_STARTTIME (2 * TICRATE) +#define FLYBOT_SCALE (17 * FRACUNIT / 20) +static const fixed_t PI = 355 * FRACUNIT / 113; + +static fixed_t SetFlybotZ(mobj_t *flybot) +{ + flybot->z = FixedMul(mapobjectscale, FLYBOT_VERTICAL_OFFSET) + FixedMul(mapobjectscale, P_ReturnThrustX(NULL, flybot->movedir, FLYBOT_BOB_AMPLITUDE)); + if (flybot->eflags & MFE_VERTICALFLIP) + { + flybot->z = -flybot->z - flybot->height; + } + else + { + flybot->z += flybot->target->height; + } + flybot->z += flybot->target->z; + return flybot->z; +} + +void Obj_SpawnFlybotsForPlayer(player_t *player) +{ + UINT8 i; + mobj_t *mo = player->mo; + fixed_t radius = mo->radius; + + for (i = 0; i < FLYBOT_QUANTITY; i++) + { + angle_t angle = mo->angle + ANGLE_90 + FixedAngle(i * 360 * FRACUNIT / FLYBOT_QUANTITY); + mobj_t *flybot = P_SpawnMobj( + mo->x + P_ReturnThrustX(NULL, angle, radius), + mo->y + P_ReturnThrustY(NULL, angle, radius), + mo->z, + MT_FLYBOT767 + ); + + P_InstaScale(flybot, flybot->old_scale = FixedMul(mapobjectscale, FLYBOT_SCALE)); + P_SetTarget(&flybot->target, mo); + flybot->eflags |= mo->eflags & MFE_VERTICALFLIP; + flybot->movedir = flybot->old_angle = flybot->angle = angle + ANGLE_90; + flybot->old_z = SetFlybotZ(flybot); + flybot->renderflags |= (i * RF_DONTDRAW); + } +} + +void Obj_FlybotThink(mobj_t *flybot) +{ + UINT16 stunned = UINT16_MAX; + angle_t deltaAngle, angle; + fixed_t radius, circumference; + fixed_t speed = FixedMul(mapobjectscale, flybot->info->speed); + mobj_t *mo = flybot->target; + + if (P_MobjWasRemoved(mo)) + { + P_KillMobj(flybot, NULL, NULL, 0); + return; + } + + if (mo->player) + { + if (((stunned = mo->player->stunned & 0x7FFF) == 0) || (mo->player->playerstate == PST_DEAD)) + { + P_KillMobj(flybot, NULL, NULL, 0); + return; + } + } + + flybot->frame = flybot->frame & ~FF_TRANSMASK; + if (stunned < FLYBOT_FADE_STARTTIME) + { + flybot->frame |= Easing_InCubic(FixedDiv(stunned, FLYBOT_FADE_STARTTIME), 7, 1) << FF_TRANSSHIFT; + } + + flybot->eflags = (flybot->eflags & ~MFE_VERTICALFLIP) | (mo->eflags & MFE_VERTICALFLIP); + flybot->movedir += FLYBOT_BOB_FREQUENCY; + flybot->renderflags ^= RF_DONTDRAW; + + radius = mo->radius; + circumference = 2 * FixedMul(PI, radius); + deltaAngle = FixedAngle(FixedMul(FixedDiv(speed, circumference), 360 * FRACUNIT)); + flybot->angle += deltaAngle; + angle = flybot->angle - ANGLE_90; + + P_MoveOrigin(flybot, + mo->x + P_ReturnThrustX(NULL, angle, radius), + mo->y + P_ReturnThrustY(NULL, angle, radius), + SetFlybotZ(flybot) + ); +} + +void Obj_FlybotDeath(mobj_t *flybot) +{ + UINT8 i; + angle_t angle = 0; + fixed_t hThrust = 4*mapobjectscale, vThrust = 4*mapobjectscale; + vector3_t mom = {0, 0, 0}; + mobj_t *mo = flybot->target; + + if (!P_MobjWasRemoved(mo)) + { + mom.x = mo->momx; + mom.y = mo->momy; + mom.z = mo->momz; + //S_StartSound(mo, flybot->info->deathsound); + } + + for (i = 0; i < 4; i++) + { + mo = P_SpawnMobjFromMobj(flybot, 0, 0, 0, MT_PARTICLE); + P_SetMobjState(mo, S_SPINDASHDUST); + mo->flags |= MF_NOSQUISH; + mo->renderflags |= RF_FULLBRIGHT; + mo->momx = mom.x; + mo->momy = mom.y; + mo->momz = mom.z + vThrust; + P_Thrust(mo, angle, hThrust); + vThrust *= -1; + angle += ANGLE_90; + } +} diff --git a/src/p_inter.c b/src/p_inter.c index b3d178254..02fe7393e 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -125,13 +125,20 @@ boolean P_CanPickupItem(player_t *player, UINT8 weapon) // 2: Eggbox // 3: Paperitem - if (weapon != 2 && player->instaWhipCharge) + if (weapon != PICKUP_EGGBOX && player->instaWhipCharge) return false; - if (weapon) + if (weapon == PICKUP_RINGORSPHERE) + { + if (player->stunned > 0) + { + return false; + } + } + else { // Item slot already taken up - if (weapon == 2) + if (weapon == PICKUP_EGGBOX) { // Invulnerable if (player->flashing > 0) @@ -153,11 +160,11 @@ boolean P_CanPickupItem(player_t *player, UINT8 weapon) // Item slot already taken up if (player->itemRoulette.active == true || player->ringboxdelay > 0 - || (weapon != 3 && player->itemamount) + || (weapon != PICKUP_PAPERITEM && player->itemamount) || (player->itemflags & IF_ITEMOUT)) return false; - if (weapon == 3 && K_GetShieldFromItem(player->itemtype) != KSHIELD_NONE) + if (weapon == PICKUP_PAPERITEM && K_GetShieldFromItem(player->itemtype) != KSHIELD_NONE) return false; // No stacking shields! } } @@ -411,7 +418,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) if (special->scale < special->destscale/2) return; - if (!P_CanPickupItem(player, 3) || (player->itemamount && player->itemtype != special->threshold)) + if (!P_CanPickupItem(player, PICKUP_PAPERITEM) || (player->itemamount && player->itemtype != special->threshold)) return; player->itemtype = special->threshold; @@ -433,7 +440,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) case MT_RANDOMITEM: { UINT8 cheesetype = (special->flags2 & MF2_BOSSDEAD) ? 2 : 1; // perma ring box - if (!P_CanPickupItem(player, 1)) + if (!P_CanPickupItem(player, PICKUP_ITEMBOX)) return; if (P_IsPickupCheesy(player, cheesetype)) return; @@ -473,7 +480,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) return; } case MT_SPHEREBOX: - if (!P_CanPickupItem(player, 0)) + if (!P_CanPickupItem(player, PICKUP_RINGORSPHERE)) return; special->momx = special->momy = special->momz = 0; @@ -499,7 +506,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) return; break; default: - if (!P_CanPickupItem(player, 1)) + if (!P_CanPickupItem(player, PICKUP_ITEMBOX)) return; if (P_IsPickupCheesy(player, 3)) return; @@ -558,7 +565,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) return; } case MT_EMERALD: - if (!P_CanPickupItem(player, 0) || P_PlayerInPain(player)) + if (!P_CanPickupItem(player, PICKUP_RINGORSPHERE) || P_PlayerInPain(player)) return; if (special->threshold > 0) @@ -617,7 +624,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) return; case MT_CDUFO: // SRB2kart - if (special->fuse || !P_CanPickupItem(player, 1)) + if (special->fuse || !P_CanPickupItem(player, PICKUP_ITEMBOX)) return; K_StartItemRoulette(player, false); @@ -693,7 +700,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) if (special->threshold > 0 || P_PlayerInPain(player) || player->spindash) // player->spindash: Otherwise, players can pick up rings that are thrown out of them from invinc spindash penalty return; - if (!(P_CanPickupItem(player, 0))) + if (!(P_CanPickupItem(player, PICKUP_RINGORSPHERE))) return; // Reached the cap, don't waste 'em! @@ -715,7 +722,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) return; case MT_BLUESPHERE: - if (!(P_CanPickupItem(player, 0))) + if (!(P_CanPickupItem(player, PICKUP_RINGORSPHERE))) return; P_GivePlayerSpheres(player, 1); @@ -2301,6 +2308,9 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget case MT_EMFAUCET_DRIP: Obj_EMZDripDeath(target); break; + case MT_FLYBOT767: + Obj_FlybotDeath(target); + break; default: break; } @@ -3001,6 +3011,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da UINT8 type = (damagetype & DMG_TYPEMASK); const boolean hardhit = (type == DMG_EXPLODE || type == DMG_KARMA || type == DMG_TUMBLE); // This damage type can do evil stuff like ALWAYS combo INT16 ringburst = 5; + UINT16 stunTics = 0; // Check if the player is allowed to be damaged! // If not, then spawn the instashield effect instead. @@ -3391,6 +3402,17 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da player->flipDI = true; } + // I'm wondering if weight 9 should have it for 70 tics, while weight 1 would have it for like 280 (basically x4) + // It may be worth designing it LIKE a value that could be changed for the future though, we may want different things to give different multipliers of stun later imo + stunTics = 2*TICRATE + (6*TICRATE * (9 - player->kartweight) / 8); + stunTics >>= player->stunnedCombo; // consecutive hits add half as much stun as the previous hit + + if (player->stunnedCombo < UINT8_MAX) + { + player->stunnedCombo++; + } + player->stunned = (player->stunned & 0x8000) | min(0x7FFF, (player->stunned & 0x7FFF) + stunTics); + K_DefensiveOverdrive(target->player); } } diff --git a/src/p_local.h b/src/p_local.h index ee269807f..00e86b453 100644 --- a/src/p_local.h +++ b/src/p_local.h @@ -557,6 +557,12 @@ void P_CheckTimeLimit(void); void P_CheckPointLimit(void); boolean P_CheckRacers(void); +// Pickup types +#define PICKUP_RINGORSPHERE 0 +#define PICKUP_ITEMBOX 1 +#define PICKUP_EGGBOX 2 +#define PICKUP_PAPERITEM 3 + boolean P_CanPickupItem(player_t *player, UINT8 weapon); boolean P_IsPickupCheesy(player_t *player, UINT8 type); void P_UpdateLastPickup(player_t *player, UINT8 type); diff --git a/src/p_mobj.c b/src/p_mobj.c index ee7a8f5e0..f1f84c444 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -10070,6 +10070,12 @@ static boolean P_MobjRegularThink(mobj_t *mobj) break; } + case MT_FLYBOT767: + { + Obj_FlybotThink(mobj); + break; + } + default: // check mobj against possible water content, before movement code P_MobjCheckWater(mobj); diff --git a/src/p_saveg.cpp b/src/p_saveg.cpp index 9f11fbb70..0b03a974f 100644 --- a/src/p_saveg.cpp +++ b/src/p_saveg.cpp @@ -455,6 +455,8 @@ static void P_NetArchivePlayers(savebuffer_t *save) WRITEUINT8(save->p, players[i].noEbrakeMagnet); WRITEUINT8(save->p, players[i].tumbleBounces); WRITEUINT16(save->p, players[i].tumbleHeight); + WRITEUINT16(save->p, players[i].stunned); + WRITEUINT8(save->p, players[i].stunnedCombo); WRITEUINT8(save->p, players[i].justDI); WRITEUINT8(save->p, players[i].flipDI); @@ -1093,6 +1095,8 @@ static void P_NetUnArchivePlayers(savebuffer_t *save) players[i].noEbrakeMagnet = READUINT8(save->p); players[i].tumbleBounces = READUINT8(save->p); players[i].tumbleHeight = READUINT16(save->p); + players[i].stunned = READUINT16(save->p); + players[i].stunnedCombo = READUINT8(save->p); players[i].justDI = READUINT8(save->p); players[i].flipDI = (boolean)READUINT8(save->p);