Add stun mechanic & Flybot767

This commit is contained in:
Lach 2025-05-14 01:39:42 +10:00
parent a323365ce2
commit 22817ac60b
13 changed files with 281 additions and 13 deletions

View file

@ -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.

View file

@ -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[] = {

View file

@ -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
},
};

View file

@ -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

View file

@ -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)

View file

@ -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);

View file

@ -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"))

View file

@ -60,6 +60,7 @@ target_sources(SRB2SDL2 PRIVATE
pulley.cpp
amps.c
ballhog.cpp
flybot767.c
)
add_subdirectory(versus)

142
src/objects/flybot767.c Normal file
View file

@ -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;
}
}

View file

@ -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;
}
// <VelocitOni> I'm wondering if weight 9 should have it for 70 tics, while weight 1 would have it for like 280 (basically x4)
// <VelocitOni> 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);
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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);