mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2025-10-30 08:01:28 +00:00
4612 lines
119 KiB
C
4612 lines
119 KiB
C
// DR. ROBOTNIK'S RING RACERS
|
|
//-----------------------------------------------------------------------------
|
|
// Copyright (C) 2025 by Kart Krew.
|
|
// Copyright (C) 2020 by Sonic Team Junior.
|
|
// Copyright (C) 2000 by DooM Legacy Team.
|
|
// Copyright (C) 1996 by id Software, Inc.
|
|
//
|
|
// 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 p_inter.c
|
|
/// \brief Handling interactions (i.e., collisions)
|
|
|
|
#include "doomdef.h"
|
|
#include "i_system.h"
|
|
#include "am_map.h"
|
|
#include "g_game.h"
|
|
#include "m_random.h"
|
|
#include "p_local.h"
|
|
#include "s_sound.h"
|
|
#include "r_main.h"
|
|
#include "st_stuff.h"
|
|
#include "hu_stuff.h"
|
|
#include "lua_hook.h"
|
|
#include "m_cond.h" // unlockables, emblems, etc
|
|
#include "p_setup.h"
|
|
#include "m_cheat.h" // objectplace
|
|
#include "m_misc.h"
|
|
#include "v_video.h" // video flags for CEchos
|
|
#include "f_finale.h"
|
|
|
|
// SRB2kart
|
|
#include "k_kart.h"
|
|
#include "k_battle.h"
|
|
#include "k_specialstage.h"
|
|
#include "k_pwrlv.h"
|
|
#include "k_profiles.h"
|
|
#include "k_grandprix.h"
|
|
#include "k_respawn.h"
|
|
#include "p_spec.h"
|
|
#include "k_objects.h"
|
|
#include "k_roulette.h"
|
|
#include "k_boss.h"
|
|
#include "k_hitlag.h"
|
|
#include "acs/interface.h"
|
|
#include "k_powerup.h"
|
|
#include "k_collide.h"
|
|
#include "m_easing.h"
|
|
#include "k_hud.h" // K_AddMessage
|
|
|
|
void P_ForceFeed(const player_t *player, INT32 attack, INT32 fade, tic_t duration, INT32 period)
|
|
{
|
|
BasicFF_t Basicfeed;
|
|
if (!player)
|
|
return;
|
|
Basicfeed.Duration = (UINT32)(duration * (100L/TICRATE));
|
|
Basicfeed.ForceX = Basicfeed.ForceY = 1;
|
|
Basicfeed.Gain = 25000;
|
|
Basicfeed.Magnitude = period*10;
|
|
Basicfeed.player = player;
|
|
/// \todo test FFB
|
|
P_RampConstant(&Basicfeed, attack, fade);
|
|
}
|
|
|
|
void P_ForceConstant(const BasicFF_t *FFInfo)
|
|
{
|
|
JoyFF_t ConstantQuake;
|
|
if (!FFInfo || !FFInfo->player)
|
|
return;
|
|
ConstantQuake.ForceX = FFInfo->ForceX;
|
|
ConstantQuake.ForceY = FFInfo->ForceY;
|
|
ConstantQuake.Duration = FFInfo->Duration;
|
|
ConstantQuake.Gain = FFInfo->Gain;
|
|
ConstantQuake.Magnitude = FFInfo->Magnitude;
|
|
if (FFInfo->player == &players[consoleplayer])
|
|
I_Tactile(ConstantForce, &ConstantQuake);
|
|
else if (splitscreen && FFInfo->player == &players[g_localplayers[1]])
|
|
I_Tactile2(ConstantForce, &ConstantQuake);
|
|
else if (splitscreen > 1 && FFInfo->player == &players[g_localplayers[2]])
|
|
I_Tactile3(ConstantForce, &ConstantQuake);
|
|
else if (splitscreen > 2 && FFInfo->player == &players[g_localplayers[3]])
|
|
I_Tactile4(ConstantForce, &ConstantQuake);
|
|
}
|
|
void P_RampConstant(const BasicFF_t *FFInfo, INT32 Start, INT32 End)
|
|
{
|
|
JoyFF_t RampQuake;
|
|
if (!FFInfo || !FFInfo->player)
|
|
return;
|
|
RampQuake.ForceX = FFInfo->ForceX;
|
|
RampQuake.ForceY = FFInfo->ForceY;
|
|
RampQuake.Duration = FFInfo->Duration;
|
|
RampQuake.Gain = FFInfo->Gain;
|
|
RampQuake.Magnitude = FFInfo->Magnitude;
|
|
RampQuake.Start = Start;
|
|
RampQuake.End = End;
|
|
if (FFInfo->player == &players[consoleplayer])
|
|
I_Tactile(ConstantForce, &RampQuake);
|
|
else if (splitscreen && FFInfo->player == &players[g_localplayers[1]])
|
|
I_Tactile2(ConstantForce, &RampQuake);
|
|
else if (splitscreen > 1 && FFInfo->player == &players[g_localplayers[2]])
|
|
I_Tactile3(ConstantForce, &RampQuake);
|
|
else if (splitscreen > 2 && FFInfo->player == &players[g_localplayers[3]])
|
|
I_Tactile4(ConstantForce, &RampQuake);
|
|
}
|
|
|
|
|
|
//
|
|
// GET STUFF
|
|
//
|
|
|
|
|
|
//
|
|
// P_CanPickupItem
|
|
//
|
|
// Returns true if the player is in a state where they can pick up items.
|
|
//
|
|
boolean P_CanPickupItem(player_t *player, UINT8 weapon)
|
|
{
|
|
if (player->exiting || mapreset || (player->pflags & PF_ELIMINATED) || player->itemRoulette.reserved)
|
|
return false;
|
|
|
|
// See p_local.h for pickup types
|
|
|
|
if (weapon != PICKUP_EGGBOX && player->instaWhipCharge)
|
|
return false;
|
|
|
|
if (weapon == PICKUP_ITEMBOX && !player->cangrabitems)
|
|
return false;
|
|
|
|
if (weapon == PICKUP_RINGORSPHERE)
|
|
{
|
|
// No picking up rings while SPB is targetting you
|
|
if (player->pflags & PF_RINGLOCK)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// No picking up rings while stunned
|
|
if (player->stunned > 0)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Item slot already taken up
|
|
if (weapon == PICKUP_EGGBOX)
|
|
{
|
|
// Invulnerable
|
|
if (player->flashing > 0)
|
|
return false;
|
|
|
|
// Already have fake
|
|
if ((player->itemRoulette.active && player->itemRoulette.eggman) == true
|
|
|| player->eggmanexplode)
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// Item-specific timer going off
|
|
if (player->stealingtimer
|
|
|| player->rocketsneakertimer
|
|
|| player->eggmanexplode)
|
|
return false;
|
|
|
|
// Item slot already taken up
|
|
if (player->itemRoulette.active == true
|
|
|| player->ringboxdelay > 0
|
|
|| (weapon != PICKUP_PAPERITEM && player->itemamount)
|
|
|| (player->itemflags & IF_ITEMOUT))
|
|
return false;
|
|
|
|
if (weapon == PICKUP_PAPERITEM && K_GetShieldFromItem(player->itemtype) != KSHIELD_NONE)
|
|
return false; // No stacking shields!
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Allow players to pick up only one pickup from each set of pickups.
|
|
// Anticheese pickup types are different than-P_CanPickupItem weapon, because that system is
|
|
// already slightly scary without introducing special cases for different types of the same pickup.
|
|
// See p_local.h for cheese types.
|
|
boolean P_IsPickupCheesy(player_t *player, UINT8 type)
|
|
{
|
|
extern consvar_t cv_debugcheese;
|
|
|
|
if (cv_debugcheese.value)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (gametyperules & GTR_CATCHER)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (player->lastpickupdistance && player->lastpickuptype == type)
|
|
{
|
|
UINT32 distancedelta = min(player->distancetofinish - player->lastpickupdistance, player->lastpickupdistance - player->distancetofinish);
|
|
if (distancedelta < 2500)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void P_UpdateLastPickup(player_t *player, UINT8 type)
|
|
{
|
|
player->lastpickuptype = type;
|
|
player->lastpickupdistance = player->distancetofinish;
|
|
}
|
|
|
|
boolean P_CanPickupEmblem(player_t *player, INT32 emblemID)
|
|
{
|
|
if (emblemID < 0 || emblemID >= MAXEMBLEMS)
|
|
{
|
|
// Invalid emblem ID, can't pickup.
|
|
return false;
|
|
}
|
|
|
|
if (demo.playback)
|
|
{
|
|
// Never collect emblems in replays.
|
|
return false;
|
|
}
|
|
|
|
if (player != NULL)
|
|
{
|
|
if (player->bot)
|
|
{
|
|
// Your nefarious opponent puppy can't grab these for you.
|
|
return false;
|
|
}
|
|
|
|
if (player->exiting)
|
|
{
|
|
// Yeah but YOU didn't actually do it now did you
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
boolean P_EmblemWasCollected(INT32 emblemID)
|
|
{
|
|
if (emblemID < 0 || emblemID >= numemblems
|
|
|| emblemlocations[emblemID].type == ET_NONE)
|
|
{
|
|
// Invalid emblem ID, can't pickup.
|
|
return true;
|
|
}
|
|
|
|
return gamedata->collected[emblemID];
|
|
}
|
|
|
|
static void P_ItemPop(mobj_t *actor)
|
|
{
|
|
/*
|
|
INT32 locvar1 = var1;
|
|
|
|
if (LUA_CallAction(A_ITEMPOP, actor))
|
|
return;
|
|
|
|
if (!(actor->target && actor->target->player))
|
|
{
|
|
if (cht_debug && !(actor->target && actor->target->player))
|
|
CONS_Printf("ERROR: Powerup has no target!\n");
|
|
return;
|
|
}
|
|
*/
|
|
|
|
Obj_SpawnItemDebrisEffects(actor, actor->target);
|
|
|
|
if (!specialstageinfo.valid
|
|
&& (gametyperules & GTR_SPHERES) != GTR_SPHERES)
|
|
{
|
|
// Doesn't apply to Special
|
|
P_SetMobjState(actor, S_RINGBOX1);
|
|
}
|
|
|
|
actor->extravalue1 = 0;
|
|
|
|
// de-solidify
|
|
// Do not set item boxes intangible, those are handled in fusethink for item pickup leniency
|
|
// Sphere boxes still need to be set intangible here though
|
|
if (actor->type != MT_RANDOMITEM)
|
|
actor->flags |= MF_NOCLIPTHING;
|
|
|
|
// RF_DONTDRAW will flicker as the object's fuse gets
|
|
// closer to running out (see P_FuseThink)
|
|
actor->renderflags |= RF_DONTDRAW|RF_TRANS50;
|
|
actor->color = SKINCOLOR_GREY;
|
|
actor->colorized = true;
|
|
|
|
/*
|
|
if (locvar1 == 1)
|
|
{
|
|
P_GivePlayerSpheres(actor->target->player, actor->extravalue1);
|
|
}
|
|
else if (locvar1 == 0)
|
|
{
|
|
if (actor->extravalue1 >= TICRATE)
|
|
K_StartItemRoulette(actor->target->player, false);
|
|
else
|
|
K_StartItemRoulette(actor->target->player, true);
|
|
}
|
|
*/
|
|
|
|
// Here at mapload in battle?
|
|
if (gametype != GT_TUTORIAL && !(gametyperules & GTR_CIRCUIT) && (actor->flags2 & MF2_BOSSFLEE))
|
|
{
|
|
numgotboxes++;
|
|
|
|
// do not flicker back in just yet, handled by
|
|
// P_RespawnBattleBoxes eventually
|
|
P_SetMobjState(actor, S_INVISIBLE);
|
|
}
|
|
}
|
|
|
|
/** Takes action based on a ::MF_SPECIAL thing touched by a player.
|
|
* Actually, this just checks a few things (heights, toucher->player, no
|
|
* objectplace, no dead or disappearing things)
|
|
*
|
|
* The special thing may be collected and disappear, or a sound may play, or
|
|
* both.
|
|
*
|
|
* \param special The special thing.
|
|
* \param toucher The player's mobj.
|
|
* \param heightcheck Whether or not to make sure the player and the object
|
|
* are actually touching.
|
|
*/
|
|
void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
|
|
{
|
|
player_t *player;
|
|
|
|
if (objectplacing)
|
|
return;
|
|
|
|
I_Assert(special != NULL);
|
|
I_Assert(toucher != NULL);
|
|
|
|
// Dead thing touching.
|
|
// Can happen with a sliding player corpse.
|
|
if (toucher->health <= 0)
|
|
return;
|
|
if (special->health <= 0)
|
|
return;
|
|
|
|
if (heightcheck)
|
|
{
|
|
fixed_t toucher_bottom = toucher->z;
|
|
fixed_t special_bottom = special->z;
|
|
|
|
if (toucher->flags & MF_PICKUPFROMBELOW)
|
|
toucher_bottom -= toucher->height;
|
|
|
|
if (special->flags & MF_PICKUPFROMBELOW)
|
|
special_bottom -= special->height;
|
|
|
|
if (toucher->momz < 0) {
|
|
if (toucher_bottom + toucher->momz > special->z + special->height)
|
|
return;
|
|
} else if (toucher_bottom > special->z + special->height)
|
|
return;
|
|
if (toucher->momz > 0) {
|
|
if (toucher->z + toucher->height + toucher->momz < special_bottom)
|
|
return;
|
|
} else if (toucher->z + toucher->height < special_bottom)
|
|
return;
|
|
}
|
|
|
|
player = toucher->player;
|
|
I_Assert(player != NULL); // Only players can touch stuff!
|
|
|
|
if (player->spectator)
|
|
return;
|
|
|
|
// Ignore multihits in "ouchie" mode
|
|
if (special->flags & (MF_ENEMY|MF_BOSS) && special->flags2 & MF2_FRET)
|
|
return;
|
|
|
|
if (LUA_HookTouchSpecial(special, toucher) || P_MobjWasRemoved(special))
|
|
return;
|
|
|
|
if ((special->flags & (MF_ENEMY|MF_BOSS)) && !(special->flags & MF_MISSILE))
|
|
{
|
|
////////////////////////////////////////////////////////
|
|
/////ENEMIES & BOSSES!!/////////////////////////////////
|
|
////////////////////////////////////////////////////////
|
|
|
|
if (special->type == MT_BLENDEYE_MAIN)
|
|
{
|
|
if (!VS_BlendEye_Touched(special, toucher))
|
|
return;
|
|
}
|
|
|
|
P_DamageMobj(toucher, special, special, 1, DMG_NORMAL);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// We now identify by object type, not sprite! Tails 04-11-2001
|
|
switch (special->type)
|
|
{
|
|
case MT_FLOATINGITEM: // SRB2Kart
|
|
if (special->extravalue1 > 0 && toucher != special->tracer)
|
|
{
|
|
if (special->tracer && !P_MobjWasRemoved(special->tracer) && special->tracer->player)
|
|
{
|
|
if (!G_SameTeam(special->tracer->player, player))
|
|
{
|
|
player->pflags |= PF_CASTSHADOW;
|
|
return;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (special->threshold >= FIRSTPOWERUP)
|
|
{
|
|
if (P_PlayerInPain(player))
|
|
return;
|
|
|
|
K_GivePowerUp(player, special->threshold, special->movecount);
|
|
}
|
|
else
|
|
{
|
|
// Avoid being picked up immediately
|
|
if (special->scale < special->destscale/2)
|
|
return;
|
|
|
|
if (!P_CanPickupItem(player, PICKUP_PAPERITEM))
|
|
return;
|
|
|
|
if (special->threshold == KDROP_STONESHOETRAP)
|
|
{
|
|
if (K_TryPickMeUp(special, toucher, false))
|
|
return;
|
|
|
|
if (!P_MobjWasRemoved(player->stoneShoe))
|
|
{
|
|
player->pflags |= PF_CASTSHADOW;
|
|
return;
|
|
}
|
|
|
|
P_SetTarget(&player->stoneShoe, Obj_SpawnStoneShoe(special->extravalue2, toucher));
|
|
K_AddHitLag(toucher, 8, false);
|
|
|
|
player_t *owner = Obj_StoneShoeOwnerPlayer(special);
|
|
if (owner)
|
|
{
|
|
K_SpawnAmps(player, K_PvPAmpReward(20, owner, player), toucher);
|
|
K_SpawnAmps(owner, K_PvPAmpReward(20, owner, player), toucher);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (player->itemamount && player->itemtype != special->threshold)
|
|
return;
|
|
|
|
player->itemtype = special->threshold;
|
|
if ((UINT16)(player->itemamount) + special->movecount > 255)
|
|
K_SetPlayerItemAmount(player, 255);
|
|
else
|
|
K_AdjustPlayerItemAmount(player, special->movecount);
|
|
}
|
|
}
|
|
|
|
S_StartSound(special, special->info->deathsound);
|
|
|
|
P_SetTarget(&special->tracer, toucher);
|
|
special->flags2 |= MF2_NIGHTSPULL;
|
|
special->destscale = mapobjectscale>>4;
|
|
special->scalespeed <<= 1;
|
|
|
|
special->flags &= ~MF_SPECIAL;
|
|
return;
|
|
case MT_RANDOMITEM: {
|
|
UINT8 cheesetype = (special->flags2 & MF2_BOSSDEAD) ? CHEESE_RINGBOX : CHEESE_ITEMBOX; // perma ring box
|
|
|
|
if (!P_CanPickupItem(player, PICKUP_ITEMBOX))
|
|
return;
|
|
if (P_IsPickupCheesy(player, cheesetype))
|
|
return;
|
|
|
|
special->momx = special->momy = special->momz = 0;
|
|
P_SetTarget(&special->target, toucher);
|
|
P_UpdateLastPickup(player, cheesetype);
|
|
// P_KillMobj(special, toucher, toucher, DMG_NORMAL);
|
|
|
|
statenum_t specialstate = special->state - states;
|
|
|
|
if (special->fuse) // This box is respawning, but was broken very recently (see P_FuseThink)
|
|
{
|
|
// What was this box broken as?
|
|
if (!K_ThunderDome() && special->cusval && !(special->flags2 & MF2_BOSSDEAD))
|
|
K_StartItemRoulette(player, false);
|
|
else
|
|
K_StartItemRoulette(player, true);
|
|
}
|
|
else if (specialstate >= S_RANDOMITEM1 && specialstate <= S_RANDOMITEM12)
|
|
{
|
|
K_StartItemRoulette(player, false);
|
|
special->cusval = 1; // Lenient pickup should be ITEM
|
|
}
|
|
else
|
|
{
|
|
K_StartItemRoulette(player, true);
|
|
special->cusval = 0; // Lenient pickup should be RING
|
|
}
|
|
|
|
P_ItemPop(special);
|
|
|
|
if (!special->fuse)
|
|
special->fuse = TICRATE;
|
|
return;
|
|
}
|
|
case MT_SPHEREBOX:
|
|
if (!P_CanPickupItem(player, PICKUP_RINGORSPHERE))
|
|
return;
|
|
|
|
special->momx = special->momy = special->momz = 0;
|
|
P_SetTarget(&special->target, toucher);
|
|
// P_KillMobj(special, toucher, toucher, DMG_NORMAL);
|
|
P_ItemPop(special);
|
|
P_GivePlayerSpheres(player, special->extravalue2);
|
|
return;
|
|
case MT_ITEMCAPSULE:
|
|
if (special->scale < special->extravalue1) // don't break it while it's respawning
|
|
return;
|
|
|
|
switch (special->threshold)
|
|
{
|
|
case KITEM_SPB:
|
|
if (K_IsSPBInGame()) // don't spawn a second SPB
|
|
return;
|
|
break;
|
|
case KCAPSULE_RING:
|
|
if (!P_CanPickupItem(player, PICKUP_RINGORSPHERE)) // no cheaty rings
|
|
return;
|
|
break;
|
|
default:
|
|
if (!P_CanPickupItem(player, PICKUP_ITEMCAPSULE))
|
|
return;
|
|
if (P_IsPickupCheesy(player, CHEESE_ITEMCAPSULE))
|
|
return;
|
|
break;
|
|
}
|
|
|
|
// Ring Capsules shouldn't affect pickup cheese, they're just used as condensed ground-ring placements.
|
|
if (special->threshold != KCAPSULE_RING)
|
|
P_UpdateLastPickup(player, 3);
|
|
|
|
S_StartSound(toucher, special->info->deathsound);
|
|
P_KillMobj(special, toucher, toucher, DMG_NORMAL);
|
|
return;
|
|
case MT_KARMAHITBOX:
|
|
if (!special->target->player)
|
|
return;
|
|
if (player == special->target->player)
|
|
return;
|
|
if (special->target->player->exiting || player->exiting)
|
|
return;
|
|
|
|
if (P_PlayerInPain(special->target->player))
|
|
return;
|
|
|
|
if (special->target->player->karmadelay > 0)
|
|
return;
|
|
|
|
{
|
|
mobj_t *boom;
|
|
|
|
if (P_DamageMobj(toucher, special, special->target, 1, DMG_KARMA) == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
boom = P_SpawnMobj(special->target->x, special->target->y, special->target->z, MT_BOOMEXPLODE);
|
|
|
|
boom->scale = special->target->scale;
|
|
boom->destscale = special->target->scale;
|
|
boom->momz = 5*FRACUNIT;
|
|
|
|
if (special->target->color)
|
|
boom->color = special->target->color;
|
|
else
|
|
boom->color = SKINCOLOR_KETCHUP;
|
|
|
|
S_StartSound(boom, special->info->attacksound);
|
|
|
|
special->target->player->karthud[khud_yougotem] = 2*TICRATE;
|
|
special->target->player->karmadelay = comebacktime;
|
|
}
|
|
return;
|
|
case MT_DUELBOMB:
|
|
{
|
|
Obj_DuelBombTouch(special, toucher);
|
|
return;
|
|
}
|
|
case MT_EMERALD:
|
|
if (!P_CanPickupItem(player, PICKUP_RINGORSPHERE) || P_PlayerInPain(player))
|
|
return;
|
|
|
|
if (special->threshold > 0)
|
|
return;
|
|
|
|
if (toucher->hitlag > 0)
|
|
return;
|
|
|
|
// Emerald will now orbit the player
|
|
|
|
{
|
|
const tic_t orbit = 2*TICRATE;
|
|
Obj_BeginEmeraldOrbit(special, toucher, toucher->radius, orbit, orbit * 20);
|
|
Obj_SetEmeraldAwardee(special, toucher);
|
|
}
|
|
|
|
// You have 6 emeralds and you touch the 7th: win instantly!
|
|
if (ALLCHAOSEMERALDS((player->emeralds | special->extravalue1)))
|
|
{
|
|
player->emeralds |= special->extravalue1;
|
|
K_CheckEmeralds(player);
|
|
}
|
|
|
|
return;
|
|
case MT_SPECIAL_UFO:
|
|
if (Obj_UFOEmeraldCollect(special, toucher) == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
break;
|
|
/*
|
|
case MT_EERIEFOG:
|
|
special->frame &= ~FF_TRANS80;
|
|
special->frame |= FF_TRANS90;
|
|
return;
|
|
*/
|
|
|
|
case MT_SPECIALSTAGEBOMB:
|
|
// only attempt to damage the player if they're not invincible
|
|
if (!(player->invincibilitytimer > 0
|
|
|| K_IsBigger(toucher, special) == true
|
|
|| K_PlayerGuard(player) == true
|
|
|| player->hyudorotimer > 0))
|
|
{
|
|
if (P_DamageMobj(toucher, special, special, 1, DMG_STUMBLE))
|
|
{
|
|
P_SetMobjState(special, special->info->painstate);
|
|
special->eflags |= MFE_DAMAGEHITLAG;
|
|
return;
|
|
}
|
|
}
|
|
// if we didn't damage the player, just explode
|
|
P_SetMobjState(special, special->info->painstate);
|
|
P_SetMobjState(special, special->info->raisestate); // immediately explode visually
|
|
return;
|
|
|
|
case MT_CDUFO: // SRB2kart
|
|
if (special->fuse || !P_CanPickupItem(player, PICKUP_ITEMBOX))
|
|
return;
|
|
|
|
K_StartItemRoulette(player, false);
|
|
|
|
// Karma fireworks
|
|
/*for (i = 0; i < 5; i++)
|
|
{
|
|
mobj_t *firework = P_SpawnMobj(special->x, special->y, special->z, MT_KARMAFIREWORK);
|
|
firework->momx = toucher->momx;
|
|
firework->momy = toucher->momy;
|
|
firework->momz = toucher->momz;
|
|
P_Thrust(firework, FixedAngle((72*i)<<FRACBITS), P_RandomRange(PR_ITEM_DEBRIS, 1,8)*special->scale);
|
|
P_SetObjectMomZ(firework, P_RandomRange(PR_ITEM_DEBRIS, 1,8)*special->scale, false);
|
|
firework->color = toucher->color;
|
|
}*/
|
|
|
|
K_SetHitLagForObjects(special, toucher, toucher, 2, true);
|
|
|
|
break;
|
|
|
|
case MT_BALLOON: // SRB2kart
|
|
P_SetObjectMomZ(toucher, 20<<FRACBITS, false);
|
|
break;
|
|
|
|
case MT_BUBBLESHIELDTRAP:
|
|
if ((special->target == toucher || special->target == toucher->target) && (special->threshold > 0))
|
|
return;
|
|
|
|
if (special->tracer && !P_MobjWasRemoved(special->tracer))
|
|
return;
|
|
|
|
if (special->health <= 0 || toucher->health <= 0)
|
|
return;
|
|
|
|
if (!player->mo || player->spectator)
|
|
return;
|
|
|
|
if (K_TryPickMeUp(special, toucher, false))
|
|
return;
|
|
|
|
if (special->target && !P_MobjWasRemoved(special->target) && toucher->player && (toucher->player != (special->target->player))) // Last condition here is so you can't get your own amps
|
|
{
|
|
K_SpawnAmps(special->target->player, K_PvPAmpReward(20, special->target->player, toucher->player), toucher);
|
|
}
|
|
|
|
// attach to player!
|
|
P_SetTarget(&special->tracer, toucher);
|
|
toucher->flags |= MF_NOGRAVITY;
|
|
toucher->momz = (8*toucher->scale) * P_MobjFlip(toucher);
|
|
toucher->player->carry = CR_TRAPBUBBLE;
|
|
P_SetTarget(&toucher->tracer, special); //use tracer to acces the object
|
|
|
|
// Snap to the unfortunate player and quit moving laterally, or we can end up quite far away
|
|
special->momx = 0;
|
|
special->momy = 0;
|
|
special->x = toucher->x;
|
|
special->y = toucher->y;
|
|
special->z = toucher->z;
|
|
|
|
S_StartSound(toucher, sfx_s1b2);
|
|
return;
|
|
|
|
case MT_HYUDORO:
|
|
Obj_HyudoroCollide(special, toucher);
|
|
return;
|
|
|
|
case MT_RING:
|
|
case MT_FLINGRING:
|
|
if (special->extravalue1)
|
|
return;
|
|
|
|
// No picking up rings while SPB is targetting you
|
|
if (player->pflags & PF_RINGLOCK)
|
|
return;
|
|
|
|
// Prepping instawhip? Don't ruin it by collecting rings
|
|
if (player->instaWhipCharge)
|
|
return;
|
|
|
|
if (player->baildrop || player->bailcharge || player->defenseLockout > PUNISHWINDOW)
|
|
return;
|
|
|
|
// Don't immediately pick up spilled rings
|
|
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, PICKUP_RINGORSPHERE)))
|
|
return;
|
|
|
|
// Reached the cap, don't waste 'em!
|
|
if (RINGTOTAL(player) >= 20)
|
|
return;
|
|
|
|
special->momx = special->momy = special->momz = 0;
|
|
|
|
special->extravalue1 = 1; // Ring collect animation timer
|
|
special->angle = R_PointToAngle2(toucher->x, toucher->y, special->x, special->y); // animation angle
|
|
P_SetTarget(&special->target, toucher); // toucher for thinker
|
|
|
|
// For MT_FLINGRING - don't delete yourself mid-pickup.
|
|
special->renderflags &= ~RF_DONTDRAW;
|
|
special->fuse = 0;
|
|
|
|
player->pickuprings++;
|
|
|
|
return;
|
|
|
|
case MT_BLUESPHERE:
|
|
if (!(P_CanPickupItem(player, PICKUP_RINGORSPHERE)))
|
|
return;
|
|
|
|
P_GivePlayerSpheres(player, 1);
|
|
break;
|
|
|
|
// Secret emblem thingy
|
|
case MT_EMBLEM:
|
|
{
|
|
if (!P_CanPickupEmblem(player, special->health - 1))
|
|
return;
|
|
|
|
if (!P_IsPartyPlayer(player))
|
|
{
|
|
// Must be party.
|
|
return;
|
|
}
|
|
|
|
if (!gamedata->collected[special->health-1])
|
|
{
|
|
gamedata->collected[special->health-1] = true;
|
|
if (!M_UpdateUnlockablesAndExtraEmblems(true, true))
|
|
S_StartSound(NULL, sfx_ncitem);
|
|
gamedata->deferredsave = true;
|
|
}
|
|
|
|
// Don't delete the object, just fade it.
|
|
return;
|
|
}
|
|
|
|
case MT_SPRAYCAN:
|
|
{
|
|
if (demo.playback)
|
|
{
|
|
// Never collect emblems in replays.
|
|
return;
|
|
}
|
|
|
|
if (player->bot)
|
|
{
|
|
// Your nefarious opponent puppy can't grab these for you.
|
|
return;
|
|
}
|
|
|
|
if (player->exiting)
|
|
{
|
|
// Yeah but YOU didn't actually do it now did you
|
|
return;
|
|
}
|
|
|
|
if (!P_IsPartyPlayer(player))
|
|
{
|
|
// Must be party.
|
|
return;
|
|
}
|
|
|
|
// See also P_SprayCanInit
|
|
UINT16 can_id = mapheaderinfo[gamemap-1]->records.spraycan;
|
|
|
|
if (can_id < gamedata->numspraycans || can_id == MCAN_BONUS)
|
|
{
|
|
// Assigned to this level, has been grabbed
|
|
return;
|
|
}
|
|
|
|
if (
|
|
(gamemap-1 >= basenummapheaders)
|
|
|| (gamedata->gotspraycans >= gamedata->numspraycans)
|
|
)
|
|
{
|
|
// Custom course OR we ran out of assignables.
|
|
|
|
if (special->threshold != 0)
|
|
return;
|
|
|
|
can_id = MCAN_BONUS;
|
|
}
|
|
else
|
|
{
|
|
// Unassigned, get the next grabbable colour
|
|
can_id = gamedata->gotspraycans;
|
|
|
|
// Multiple cans in one map?
|
|
if (special->threshold != 0)
|
|
{
|
|
UINT16 ref_id = can_id + (special->threshold & UINT8_MAX);
|
|
if (ref_id >= gamedata->numspraycans)
|
|
return;
|
|
|
|
// Swap this specific can to the head of the list.
|
|
UINT16 swapcol = gamedata->spraycans[ref_id].col;
|
|
|
|
gamedata->spraycans[ref_id].col =
|
|
gamedata->spraycans[can_id].col;
|
|
skincolors[gamedata->spraycans[ref_id].col].cache_spraycan = ref_id;
|
|
|
|
gamedata->spraycans[can_id].col = swapcol;
|
|
skincolors[swapcol].cache_spraycan = can_id;
|
|
}
|
|
|
|
gamedata->spraycans[can_id].map = gamemap-1;
|
|
|
|
if (gamedata->gotspraycans == 0
|
|
&& gametype == GT_TUTORIAL
|
|
&& cv_ttlprofilen.value > 0
|
|
&& cv_ttlprofilen.value < PR_GetNumProfiles())
|
|
{
|
|
profile_t *p = PR_GetProfile(cv_ttlprofilen.value);
|
|
if (p->color == SKINCOLOR_NONE)
|
|
{
|
|
// Apply your favourite colour to the profile!
|
|
p->color = gamedata->spraycans[can_id].col;
|
|
}
|
|
}
|
|
|
|
gamedata->gotspraycans++;
|
|
}
|
|
|
|
mapheaderinfo[gamemap-1]->records.spraycan = can_id;
|
|
|
|
if (!M_UpdateUnlockablesAndExtraEmblems(true, true))
|
|
S_StartSound(NULL, sfx_ncitem);
|
|
gamedata->deferredsave = true;
|
|
|
|
{
|
|
mobj_t *canmo = NULL;
|
|
mobj_t *next = NULL;
|
|
|
|
for (canmo = trackercap; canmo; canmo = next)
|
|
{
|
|
next = canmo->itnext;
|
|
|
|
if (canmo->type != MT_SPRAYCAN)
|
|
continue;
|
|
|
|
// Don't delete the object(s), just fade it.
|
|
if (netgame || canmo == special)
|
|
{
|
|
P_SprayCanInit(canmo);
|
|
continue;
|
|
}
|
|
|
|
// Get ready to get rid of these
|
|
canmo->renderflags |= (tr_trans50 << RF_TRANSSHIFT);
|
|
canmo->destscale = 0;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
case MT_PRISONEGGDROP:
|
|
{
|
|
if (demo.playback)
|
|
{
|
|
// Never collect emblems in replays.
|
|
return;
|
|
}
|
|
|
|
if (player->bot)
|
|
{
|
|
// Your nefarious opponent puppy can't grab these for you.
|
|
return;
|
|
}
|
|
|
|
if (!P_IsPartyPlayer(player))
|
|
{
|
|
// Must be party.
|
|
return;
|
|
}
|
|
|
|
if (special->hitlag || special->scale < mapobjectscale/2)
|
|
{
|
|
// Don't get during the initial activation
|
|
return;
|
|
}
|
|
|
|
if (special->extravalue1)
|
|
{
|
|
// Don't get during destruction
|
|
return;
|
|
}
|
|
|
|
if (special->scale > mapobjectscale)
|
|
{
|
|
// Short window so you can't pick it up instantly
|
|
return;
|
|
}
|
|
|
|
if (
|
|
grandprixinfo.gp == true // Bonus Round
|
|
&& netgame == false // game design + makes it easier to implement
|
|
&& gamedata->thisprisoneggpickup_cached != NULL
|
|
)
|
|
{
|
|
gamedata->thisprisoneggpickupgrabbed = true;
|
|
if (gamedata->prisoneggstothispickup < GDINIT_PRISONSTOPRIZE)
|
|
{
|
|
// Just in case it's set absurdly low for testing.
|
|
gamedata->prisoneggstothispickup = GDINIT_PRISONSTOPRIZE;
|
|
}
|
|
|
|
if (!M_UpdateUnlockablesAndExtraEmblems(true, true))
|
|
S_StartSound(NULL, sfx_ncitem);
|
|
gamedata->deferredsave = true;
|
|
}
|
|
|
|
statenum_t teststate = (special->state-states);
|
|
|
|
if (teststate == S_PRISONEGGDROP_CD)
|
|
{
|
|
if (P_IsObjectOnGround(special))
|
|
{
|
|
special->momz = P_MobjFlip(special) * 2 * mapobjectscale;
|
|
special->flags = (special->flags & ~MF_SPECIAL) | (MF_NOGRAVITY|MF_NOCLIPHEIGHT);
|
|
}
|
|
|
|
special->extravalue1 = 1;
|
|
|
|
special->renderflags = (special->renderflags & ~RF_BRIGHTMASK) | (RF_ADD | RF_FULLBRIGHT);
|
|
|
|
return;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case MT_LSZ_BUNGEE:
|
|
Obj_BungeeSpecial(special, player);
|
|
return;
|
|
|
|
case MT_CHEATCHECK:
|
|
P_TouchCheatcheck(special, player, special->thing_args[1]);
|
|
return;
|
|
|
|
case MT_BIGTUMBLEWEED:
|
|
case MT_LITTLETUMBLEWEED:
|
|
if (toucher->momx || toucher->momy)
|
|
{
|
|
special->momx = toucher->momx;
|
|
special->momy = toucher->momy;
|
|
special->momz = P_AproxDistance(toucher->momx, toucher->momy)/4;
|
|
|
|
if (toucher->momz > 0)
|
|
special->momz += toucher->momz/8;
|
|
|
|
P_SetMobjState(special, special->info->seestate);
|
|
}
|
|
return;
|
|
|
|
case MT_WATERDROP:
|
|
if (special->state == &states[special->info->spawnstate])
|
|
{
|
|
special->z = toucher->z+toucher->height-FixedMul(8*FRACUNIT, special->scale);
|
|
special->momz = 0;
|
|
special->flags |= MF_NOGRAVITY;
|
|
P_SetMobjState (special, special->info->deathstate);
|
|
S_StartSound (special, special->info->deathsound+(P_RandomKey(PR_DECORATION, special->info->mass)));
|
|
}
|
|
return;
|
|
|
|
case MT_LOOPENDPOINT:
|
|
Obj_LoopEndpointCollide(special, toucher);
|
|
return;
|
|
|
|
case MT_RINGSHOOTER:
|
|
if (player->freeRingShooterCooldown)
|
|
player->pflags |= PF_CASTSHADOW; // you can't use this right now!
|
|
else
|
|
Obj_PlayerUsedRingShooter(special, player);
|
|
return;
|
|
|
|
case MT_SUPER_FLICKY:
|
|
Obj_SuperFlickyPlayerCollide(special, toucher);
|
|
return;
|
|
|
|
case MT_DASHRING:
|
|
case MT_RAINBOWDASHRING:
|
|
Obj_DashRingTouch(special, player);
|
|
return;
|
|
|
|
case MT_ADVENTUREAIRBOOSTER_HITBOX:
|
|
Obj_AdventureAirBoosterHitboxTouch(special, player);
|
|
return;
|
|
|
|
case MT_DLZ_ROCKET:
|
|
Obj_DLZRocketSpecial(special, player);
|
|
return;
|
|
|
|
case MT_AHZ_CLOUD:
|
|
case MT_AGZ_CLOUD:
|
|
case MT_SSZ_CLOUD:
|
|
Obj_CloudTouched(special, toucher);
|
|
return;
|
|
|
|
case MT_AGZ_BULB:
|
|
Obj_BulbTouched(special, toucher);
|
|
return;
|
|
|
|
case MT_BALLSWITCH_BALL:
|
|
{
|
|
Obj_BallSwitchTouched(special, toucher);
|
|
return;
|
|
}
|
|
|
|
case MT_BLENDEYE_PUYO:
|
|
{
|
|
if (!VS_PuyoTouched(special, toucher))
|
|
return;
|
|
break;
|
|
}
|
|
|
|
case MT_GGZICEDUST:
|
|
{
|
|
Obj_IceDustCollide(special, toucher);
|
|
return;
|
|
}
|
|
|
|
case MT_IVOBALL:
|
|
case MT_AIRIVOBALL:
|
|
{
|
|
Obj_IvoBallTouch(special, toucher);
|
|
return;
|
|
}
|
|
case MT_PATROLIVOBALL:
|
|
{
|
|
Obj_PatrolIvoBallTouch(special, toucher);
|
|
return;
|
|
}
|
|
|
|
case MT_SA2_CRATE:
|
|
case MT_ICECAPBLOCK:
|
|
{
|
|
Obj_TryCrateTouch(special, toucher);
|
|
return;
|
|
}
|
|
|
|
case MT_BETA_PARTICLE_PHYSICAL:
|
|
{
|
|
Obj_FuelCanisterTouch(special, toucher);
|
|
break;
|
|
}
|
|
|
|
case MT_BETA_PARTICLE_EXPLOSION:
|
|
{
|
|
Obj_FuelCanisterExplosionTouch(special, toucher);
|
|
return;
|
|
}
|
|
|
|
case MT_AZROCKS:
|
|
case MT_EMROCKS:
|
|
{
|
|
Obj_TouchRocks(special, toucher);
|
|
return;
|
|
}
|
|
|
|
case MT_TRICKBALLOON_RED:
|
|
case MT_TRICKBALLOON_YELLOW:
|
|
Obj_TrickBalloonTouchSpecial(special, toucher);
|
|
return;
|
|
|
|
case MT_PULLUPHOOK:
|
|
Obj_PulleyHookTouch(special, toucher);
|
|
return;
|
|
|
|
case MT_STONESHOE_CHAIN:
|
|
Obj_CollideStoneShoe(toucher, special);
|
|
return;
|
|
|
|
case MT_TOXOMISTER_POLE:
|
|
Obj_ToxomisterPoleCollide(special, toucher);
|
|
return;
|
|
|
|
case MT_TOXOMISTER_CLOUD:
|
|
Obj_ToxomisterCloudCollide(special, toucher);
|
|
return;
|
|
|
|
case MT_ANCIENTGEAR:
|
|
Obj_AncientGearTouch(special, toucher);
|
|
return;
|
|
|
|
case MT_MHPOLE:
|
|
Obj_MushroomHillPoleTouch(special, toucher);
|
|
return;
|
|
|
|
default: // SOC or script pickup
|
|
P_SetTarget(&special->target, toucher);
|
|
break;
|
|
}
|
|
}
|
|
|
|
S_StartSound(toucher, special->info->deathsound); // was NULL, but changed to player so you could hear others pick up rings
|
|
P_KillMobj(special, NULL, toucher, DMG_NORMAL);
|
|
special->shadowscale = 0;
|
|
}
|
|
|
|
/** Saves a player's level progress at a Cheat Check
|
|
*
|
|
* \param post The Cheat Check to trigger
|
|
* \param player The player that should receive the cheatcheck
|
|
* \param snaptopost If true, the respawn point will use the cheatcheck's position, otherwise player x/y and star post z
|
|
*/
|
|
void P_TouchCheatcheck(mobj_t *post, player_t *player, boolean snaptopost)
|
|
{
|
|
mobj_t *toucher = player->mo;
|
|
|
|
(void)snaptopost;
|
|
|
|
// Player must have touched all previous cheatchecks
|
|
if (post->health - player->cheatchecknum > 1)
|
|
{
|
|
if (!player->checkskip)
|
|
S_StartSound(toucher, sfx_lose);
|
|
player->checkskip = 3;
|
|
return;
|
|
}
|
|
|
|
// With the parameter + angle setup, we can go up to 1365 star posts. Who needs that many?
|
|
if (post->health > 1365)
|
|
{
|
|
CONS_Debug(DBG_GAMELOGIC, "Bad Cheatcheck Number!\n");
|
|
return;
|
|
}
|
|
|
|
if (player->cheatchecknum >= post->health)
|
|
return; // Already hit this post
|
|
|
|
player->cheatchecknum = post->health;
|
|
}
|
|
|
|
void P_TrackRoundConditionTargetDamage(targetdamaging_t targetdamaging)
|
|
{
|
|
UINT8 i;
|
|
for (i = 0; i <= splitscreen; i++)
|
|
{
|
|
if (!playeringame[g_localplayers[i]])
|
|
continue;
|
|
if (players[g_localplayers[i]].spectator)
|
|
continue;
|
|
players[g_localplayers[i]].roundconditions.targetdamaging |= targetdamaging;
|
|
/* -- the following isn't needed because we can just check for targetdamaging == UFOD_GACHABOM
|
|
if (targetdamaging != UFOD_GACHABOM)
|
|
players[g_localplayers[i]].roundconditions.gachabom_miser = 0xFF;
|
|
*/
|
|
}
|
|
}
|
|
|
|
static void P_AddBrokenPrison(mobj_t *target, mobj_t *inflictor, mobj_t *source)
|
|
{
|
|
if (!battleprisons)
|
|
return;
|
|
|
|
// Check to see if everyone's out.
|
|
{
|
|
UINT8 i = 0;
|
|
|
|
for (; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator || players[i].exiting)
|
|
continue;
|
|
break;
|
|
}
|
|
|
|
if (i == MAXPLAYERS)
|
|
{
|
|
// Nobody can claim credit for this just-too-late hit!
|
|
P_DoAllPlayersExit(0, false); // softlock prevention
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If you CAN recieve points, get them!
|
|
if ((gametyperules & GTR_POINTLIMIT)
|
|
&& (source && !P_MobjWasRemoved(source) && source->player))
|
|
{
|
|
K_GivePointsToPlayer(source->player, NULL, 1);
|
|
}
|
|
|
|
targetdamaging_t targetdamaging = UFOD_GENERIC;
|
|
if (!inflictor || P_MobjWasRemoved(inflictor) == true)
|
|
;
|
|
else switch (inflictor->type)
|
|
{
|
|
case MT_GACHABOM:
|
|
targetdamaging = UFOD_GACHABOM;
|
|
break;
|
|
case MT_ORBINAUT:
|
|
case MT_ORBINAUT_SHIELD:
|
|
targetdamaging = UFOD_ORBINAUT;
|
|
break;
|
|
case MT_BANANA:
|
|
targetdamaging = UFOD_BANANA;
|
|
break;
|
|
case MT_INSTAWHIP:
|
|
targetdamaging = UFOD_WHIP;
|
|
break;
|
|
// This is only accessible for MT_CDUFO's touch!
|
|
case MT_PLAYER:
|
|
targetdamaging = UFOD_BOOST;
|
|
break;
|
|
// The following can't be accessed in standard play...
|
|
// but the cost of tracking them here is trivial :D
|
|
case MT_JAWZ:
|
|
case MT_JAWZ_SHIELD:
|
|
targetdamaging = UFOD_JAWZ;
|
|
break;
|
|
case MT_SPB:
|
|
targetdamaging = UFOD_SPB;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
P_TrackRoundConditionTargetDamage(targetdamaging);
|
|
|
|
if (gamedata->prisoneggstothispickup)
|
|
{
|
|
gamedata->prisoneggstothispickup--;
|
|
}
|
|
|
|
// Standard progression.
|
|
if (++numtargets >= maptargets)
|
|
{
|
|
// Yipue!
|
|
|
|
P_DoAllPlayersExit(0, true);
|
|
}
|
|
else
|
|
{
|
|
S_StartSound(NULL, sfx_s221);
|
|
|
|
// Time limit recovery
|
|
if (timelimitintics)
|
|
{
|
|
UINT16 bonustime = 10*TICRATE;
|
|
INT16 clamptime = 0; // Don't allow reserve time past this value (by much)...
|
|
INT16 mintime = 5*TICRATE; // But give SOME reward for every hit. (This value used for Normal)
|
|
|
|
if (grandprixinfo.gp)
|
|
{
|
|
if (grandprixinfo.masterbots)
|
|
{
|
|
clamptime = 10*TICRATE;
|
|
mintime = 1*TICRATE;
|
|
}
|
|
else if (grandprixinfo.gamespeed == KARTSPEED_HARD)
|
|
{
|
|
clamptime = 15*TICRATE;
|
|
mintime = 2*TICRATE;
|
|
}
|
|
else if (grandprixinfo.gamespeed == KARTSPEED_NORMAL)
|
|
{
|
|
clamptime = 20*TICRATE;
|
|
mintime = 5*TICRATE;
|
|
}
|
|
else if (grandprixinfo.gamespeed == KARTSPEED_EASY)
|
|
{
|
|
// "I think Easy Mode should be about Trying Not To Kill Your Self" -VelocitOni
|
|
clamptime = 45*TICRATE;
|
|
mintime = 20*TICRATE;
|
|
}
|
|
}
|
|
|
|
UINT16 effectivetime = timelimitintics + extratimeintics - leveltime + starttime;
|
|
|
|
if (clamptime) // Lower bonus if you have more reserve, keep it tense.
|
|
{
|
|
bonustime = Easing_InOutSine(min(FRACUNIT, (effectivetime) * FRACUNIT / clamptime), bonustime, mintime);
|
|
|
|
// Quicker rolloff if you're stacking time substantially past clamptime
|
|
if ((effectivetime + bonustime) > clamptime)
|
|
bonustime = Easing_InSine(min(FRACUNIT, (effectivetime + bonustime - clamptime) * FRACUNIT / clamptime), bonustime, 1);
|
|
}
|
|
|
|
extratimeintics += bonustime;
|
|
secretextratime = TICRATE/2;
|
|
}
|
|
|
|
// Everything below dependent on our coords
|
|
if (!target || P_MobjWasRemoved(target))
|
|
return;
|
|
|
|
// Prison Egg challenge drops (CDs, etc)
|
|
#ifdef DEVELOP
|
|
extern consvar_t cv_debugprisoncd;
|
|
#endif
|
|
if ((
|
|
grandprixinfo.gp == true // Bonus Round
|
|
&& demo.playback == false // Not playback
|
|
&& netgame == false // game design + makes it easier to implement
|
|
&& gamedata->thisprisoneggpickup_cached != NULL
|
|
&& gamedata->prisoneggstothispickup == 0
|
|
&& gamedata->thisprisoneggpickupgrabbed == false
|
|
)
|
|
#ifdef DEVELOP
|
|
|| (cv_debugprisoncd.value && gamedata->thisprisoneggpickup_cached != NULL)
|
|
#endif
|
|
)
|
|
{
|
|
// Will be 0 for the next level
|
|
gamedata->prisoneggstothispickup = (maptargets - numtargets);
|
|
|
|
mobj_t *secretpickup = P_SpawnMobj(
|
|
target->x, target->y,
|
|
target->z + target->height/2,
|
|
MT_PRISONEGGDROP
|
|
);
|
|
|
|
if (secretpickup)
|
|
{
|
|
secretpickup->hitlag = target->hitlag;
|
|
|
|
secretpickup->z -= secretpickup->height/2;
|
|
|
|
P_SetScale(secretpickup, 3*secretpickup->scale);
|
|
secretpickup->scalespeed = (secretpickup->scale - secretpickup->destscale) / TICRATE;
|
|
|
|
// flags are NOT from the target - just in case it's just been placed on the ceiling as a gimmick
|
|
secretpickup->flags2 |= (source->flags2 & MF2_OBJECTFLIP);
|
|
secretpickup->eflags |= (source->eflags & MFE_VERTICALFLIP);
|
|
|
|
// Okay these have to use M_Random because replays...
|
|
// The spawning of these won't be recorded back!
|
|
const fixed_t dist = R_PointToDist2(target->x, target->y, source->x, source->y);
|
|
const fixed_t maxDist = 640 * mapobjectscale;
|
|
const fixed_t launchmomentum = Easing_Linear(
|
|
FixedDiv(min(dist, maxDist), maxDist),
|
|
5 * mapobjectscale,
|
|
20 * mapobjectscale
|
|
);
|
|
secretpickup->momz = P_MobjFlip(target) * launchmomentum; // THIS one uses target!
|
|
|
|
mobj_t *flare = P_SpawnMobj(
|
|
target->x, target->y,
|
|
target->z + target->height/2,
|
|
MT_SPARK
|
|
);
|
|
|
|
if (flare)
|
|
{
|
|
// Will flicker in place until secretpickup exits hitlag.
|
|
flare->colorized = true;
|
|
flare->renderflags |= RF_ALWAYSONTOP;
|
|
P_InstaScale(flare, 4 * flare->scale);
|
|
P_SetTarget(&secretpickup->target, flare);
|
|
P_SetMobjStateNF(flare, S_PRISONEGGDROP_FLAREA1);
|
|
}
|
|
|
|
// Darken the level for roughly how long it takes until the last sound effect stops playing.
|
|
g_darkness.start = leveltime;
|
|
g_darkness.end = leveltime + target->hitlag + TICRATE + DARKNESS_FADE_TIME;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Checks if the level timer is over the timelimit and the round should end,
|
|
* unless you are in overtime. In which case leveltime may stretch out beyond
|
|
* timelimitintics and overtime's status will be checked here each tick.
|
|
*
|
|
* \sa cv_timelimit, P_CheckPointLimit, P_UpdateSpecials
|
|
*/
|
|
void P_CheckTimeLimit(void)
|
|
{
|
|
if (exitcountdown)
|
|
return;
|
|
|
|
if (!timelimitintics)
|
|
return;
|
|
|
|
if (leveltime < starttime)
|
|
{
|
|
if (secretextratime)
|
|
secretextratime--;
|
|
return;
|
|
}
|
|
|
|
if (leveltime < (timelimitintics + starttime))
|
|
{
|
|
if (secretextratime)
|
|
{
|
|
secretextratime--;
|
|
timelimitintics++;
|
|
}
|
|
else if (extratimeintics)
|
|
{
|
|
timelimitintics++;
|
|
if (leveltime & 1)
|
|
;
|
|
else
|
|
{
|
|
if (extratimeintics > 20)
|
|
{
|
|
extratimeintics -= 20;
|
|
timelimitintics += 20;
|
|
}
|
|
else
|
|
{
|
|
timelimitintics += extratimeintics;
|
|
extratimeintics = 0;
|
|
}
|
|
S_StartSound(NULL, sfx_ptally);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (timelimitintics + starttime - leveltime <= 3*TICRATE)
|
|
{
|
|
if (((timelimitintics + starttime - leveltime) % TICRATE) == 0)
|
|
S_StartSound(NULL, sfx_s3ka7);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (gameaction == ga_completed)
|
|
return;
|
|
|
|
if ((grandprixinfo.gp == false) && (cv_overtime.value) && (gametyperules & GTR_OVERTIME))
|
|
{
|
|
#ifndef TESTOVERTIMEINFREEPLAY
|
|
UINT8 i;
|
|
boolean foundone = false; // Overtime is used for closing off down to a specific item.
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator)
|
|
continue;
|
|
|
|
if (foundone)
|
|
{
|
|
#endif
|
|
// Initiate the kill zone
|
|
if (!battleovertime.enabled)
|
|
{
|
|
thinker_t *th;
|
|
mobj_t *center = NULL;
|
|
|
|
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
|
|
{
|
|
mobj_t *thismo;
|
|
|
|
if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
|
|
continue;
|
|
|
|
thismo = (mobj_t *)th;
|
|
|
|
if (thismo->type == MT_OVERTIME_CENTER)
|
|
{
|
|
center = thismo;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (center == NULL || P_MobjWasRemoved(center))
|
|
{
|
|
CONS_Alert(CONS_WARNING, "No center point for overtime!\n");
|
|
|
|
battleovertime.x = 0;
|
|
battleovertime.y = 0;
|
|
battleovertime.z = 0;
|
|
}
|
|
else
|
|
{
|
|
battleovertime.x = center->x;
|
|
battleovertime.y = center->y;
|
|
battleovertime.z = center->z;
|
|
}
|
|
|
|
// Get largest radius from center point to minimap edges
|
|
|
|
fixed_t r = 0;
|
|
fixed_t n;
|
|
#define corner(px, py) ((n = FixedHypot(battleovertime.x - (px), battleovertime.y - (py))), r = max(r, n))
|
|
corner(minimapinfo.min_x * FRACUNIT, minimapinfo.min_y * FRACUNIT);
|
|
corner(minimapinfo.min_x * FRACUNIT, minimapinfo.max_y * FRACUNIT);
|
|
corner(minimapinfo.max_x * FRACUNIT, minimapinfo.min_y * FRACUNIT);
|
|
corner(minimapinfo.max_x * FRACUNIT, minimapinfo.max_y * FRACUNIT);
|
|
#undef corner
|
|
|
|
battleovertime.initial_radius = min(
|
|
max(r, 4096 * mapobjectscale),
|
|
// Prevent overflow in K_RunBattleOvertime
|
|
FixedDiv(INT32_MAX, M_PI_FIXED) / 2
|
|
);
|
|
|
|
battleovertime.radius = battleovertime.initial_radius;
|
|
|
|
battleovertime.enabled = 1;
|
|
|
|
S_StartSound(NULL, sfx_kc47);
|
|
}
|
|
|
|
return;
|
|
#ifndef TESTOVERTIMEINFREEPLAY
|
|
}
|
|
else
|
|
foundone = true;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
P_DoAllPlayersExit(0, false);
|
|
}
|
|
|
|
/** Checks if a player's score is over the pointlimit and the round should end.
|
|
*
|
|
* \sa cv_pointlimit, P_CheckTimeLimit, P_UpdateSpecials
|
|
*/
|
|
void P_CheckPointLimit(void)
|
|
{
|
|
INT32 i;
|
|
|
|
if (exitcountdown)
|
|
return;
|
|
|
|
if (!K_CanChangeRules(true))
|
|
return;
|
|
|
|
if (!g_pointlimit)
|
|
return;
|
|
|
|
if (!(gametyperules & GTR_POINTLIMIT))
|
|
return;
|
|
|
|
if (battleprisons)
|
|
return;
|
|
|
|
// This will be handled by P_KillPlayer
|
|
if (gametyperules & GTR_BUMPERS)
|
|
return;
|
|
|
|
// pointlimit is nonzero, check if it's been reached by this player
|
|
if (G_GametypeHasTeams() == true)
|
|
{
|
|
for (i = 0; i < TEAM__MAX; i++)
|
|
{
|
|
if (g_pointlimit <= g_teamscores[i])
|
|
{
|
|
P_DoAllPlayersExit(0, false);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator)
|
|
continue;
|
|
|
|
if (g_pointlimit <= players[i].roundscore)
|
|
{
|
|
P_DoAllPlayersExit(0, false);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Checks whether or not to end a race netgame.
|
|
boolean P_CheckRacers(void)
|
|
{
|
|
const boolean griefed = (spectateGriefed > 0);
|
|
|
|
boolean eliminateLast = (!K_CanChangeRules(true) || (cv_karteliminatelast.value != 0));
|
|
|
|
if (grandprixinfo.gp && grandprixinfo.gamespeed == KARTSPEED_EASY)
|
|
eliminateLast = false;
|
|
|
|
boolean allHumansDone = true;
|
|
//boolean allBotsDone = true;
|
|
|
|
UINT8 numPlaying = 0;
|
|
UINT8 numExiting = 0;
|
|
UINT8 numHumans = 0;
|
|
UINT8 numBots = 0;
|
|
|
|
UINT8 i;
|
|
|
|
// Check if all the players in the race have finished. If so, end the level.
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator || (players[i].lives <= 0 && !players[i].exiting))
|
|
{
|
|
// Y'all aren't even playing
|
|
continue;
|
|
}
|
|
|
|
numPlaying++;
|
|
|
|
if (players[i].bot)
|
|
{
|
|
numBots++;
|
|
}
|
|
else
|
|
{
|
|
numHumans++;
|
|
}
|
|
|
|
if (players[i].exiting || (players[i].pflags & PF_NOCONTEST))
|
|
{
|
|
numExiting++;
|
|
}
|
|
else
|
|
{
|
|
if (players[i].bot)
|
|
{
|
|
//allBotsDone = false;
|
|
}
|
|
else
|
|
{
|
|
allHumansDone = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (numPlaying <= 1 || specialstageinfo.valid == true)
|
|
{
|
|
// Never do this without enough players.
|
|
eliminateLast = false;
|
|
}
|
|
else
|
|
{
|
|
if (griefed == true && numHumans > 0)
|
|
{
|
|
// Don't do this if someone spectated
|
|
eliminateLast = false;
|
|
}
|
|
#ifndef DEVELOP
|
|
else if (grandprixinfo.gp == true)
|
|
{
|
|
// Always do this in GP
|
|
eliminateLast = true;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if (eliminateLast == true && (numExiting >= numPlaying-1))
|
|
{
|
|
// Everyone's done playing but one guy apparently.
|
|
// Just kill everyone who is still playing.
|
|
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator || players[i].lives <= 0)
|
|
{
|
|
// Y'all aren't even playing
|
|
continue;
|
|
}
|
|
|
|
if (players[i].exiting || (players[i].pflags & PF_NOCONTEST))
|
|
{
|
|
// You're done, you're free to go.
|
|
continue;
|
|
}
|
|
|
|
P_DoTimeOver(&players[i]);
|
|
}
|
|
|
|
// Everyone should be done playing at this point now.
|
|
racecountdown = 0;
|
|
return true;
|
|
}
|
|
|
|
if (numHumans > 0 && allHumansDone == true)
|
|
{
|
|
// There might be bots that are still going,
|
|
// but all of the humans are done, so we can exit now.
|
|
racecountdown = 0;
|
|
return true;
|
|
}
|
|
|
|
// SO, we're not done playing.
|
|
// Let's see if it's time to start the death counter!
|
|
|
|
if (racecountdown == 0 && K_Cooperative() == false)
|
|
{
|
|
// If the winners are all done, then start the death timer.
|
|
UINT8 winningPos = max(1, numPlaying / 2);
|
|
|
|
if (numPlaying % 2) // Any remainder? Then round up.
|
|
{
|
|
winningPos++;
|
|
}
|
|
|
|
if (numExiting >= winningPos)
|
|
{
|
|
tic_t countdown = 30*TICRATE; // 30 seconds left to finish, get going!
|
|
|
|
if (K_CanChangeRules(true) == true)
|
|
{
|
|
// Custom timer
|
|
countdown = cv_countdowntime.value * TICRATE;
|
|
}
|
|
|
|
racecountdown = countdown + 1;
|
|
}
|
|
}
|
|
|
|
// We're still playing, but no one else is,
|
|
// so we need to reset spectator griefing.
|
|
if (numPlaying <= 1)
|
|
{
|
|
spectateGriefed = 0;
|
|
}
|
|
|
|
// We are still having fun and playing the game :)
|
|
return false;
|
|
}
|
|
|
|
void P_UpdateRemovedOrbital(mobj_t *target, mobj_t *inflictor, mobj_t *source)
|
|
{
|
|
// SRB2kart
|
|
// I wish I knew a better way to do this
|
|
if (!P_MobjWasRemoved(target->target) && target->target->player && !P_MobjWasRemoved(target->target->player->mo))
|
|
{
|
|
if ((target->target->player->itemflags & IF_EGGMANOUT) && target->type == MT_EGGMANITEM_SHIELD)
|
|
target->target->player->itemflags &= ~IF_EGGMANOUT;
|
|
|
|
if (target->target->player->itemflags & IF_ITEMOUT)
|
|
{
|
|
if ((target->type == MT_BANANA_SHIELD && target->target->player->itemtype == KITEM_BANANA) // trail items
|
|
|| (target->type == MT_SSMINE_SHIELD && target->target->player->itemtype == KITEM_MINE)
|
|
|| (target->type == MT_DROPTARGET_SHIELD && target->target->player->itemtype == KITEM_DROPTARGET)
|
|
|| (target->type == MT_SINK_SHIELD && target->target->player->itemtype == KITEM_KITCHENSINK))
|
|
{
|
|
if (target->movedir != 0 && target->movedir < (UINT16)target->target->player->itemamount)
|
|
{
|
|
if (target->target->hnext && !P_MobjWasRemoved(target->target->hnext))
|
|
K_KillBananaChain(target->target->hnext, inflictor, source);
|
|
|
|
K_SetPlayerItemAmount(target->target->player, 0);
|
|
}
|
|
else if (target->target->player->itemamount)
|
|
K_AdjustPlayerItemAmount(target->target->player, -1);
|
|
}
|
|
else if ((target->type == MT_ORBINAUT_SHIELD && target->target->player->itemtype == KITEM_ORBINAUT) // orbit items
|
|
|| (target->type == MT_JAWZ_SHIELD && target->target->player->itemtype == KITEM_JAWZ))
|
|
{
|
|
if (target->target->player->itemamount)
|
|
K_AdjustPlayerItemAmount(target->target->player, -1);
|
|
if (target->lastlook != 0)
|
|
{
|
|
K_RepairOrbitChain(target);
|
|
}
|
|
}
|
|
|
|
if (!target->target->player->itemamount)
|
|
target->target->player->itemflags &= ~IF_ITEMOUT;
|
|
|
|
if (target->target->hnext == target)
|
|
P_SetTarget(&target->target->hnext, NULL);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Kills an object.
|
|
*
|
|
* \param target The victim.
|
|
* \param inflictor The attack weapon. May be NULL (environmental damage).
|
|
* \param source The attacker. May be NULL.
|
|
* \param damagetype The type of damage dealt that killed the target. If bit 7 (0x80) was set, this was an instant-death.
|
|
* \todo Cleanup, refactor, split up.
|
|
* \sa P_DamageMobj
|
|
*/
|
|
void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damagetype)
|
|
{
|
|
if (target->flags & (MF_ENEMY|MF_BOSS))
|
|
target->momx = target->momy = target->momz = 0;
|
|
|
|
// SRB2kart
|
|
if (target->type != MT_PLAYER
|
|
&& !(target->type == MT_ORBINAUT || target->type == MT_ORBINAUT_SHIELD
|
|
|| target->type == MT_JAWZ || target->type == MT_JAWZ_SHIELD
|
|
|| target->type == MT_BANANA || target->type == MT_BANANA_SHIELD
|
|
|| target->type == MT_DROPTARGET || target->type == MT_DROPTARGET_SHIELD
|
|
|| target->type == MT_EGGMANITEM || target->type == MT_EGGMANITEM_SHIELD
|
|
|| target->type == MT_BALLHOG || target->type == MT_SPB
|
|
|| target->type == MT_GACHABOM || target->type == MT_KART_LEFTOVER)) // kart dead items
|
|
target->flags |= MF_NOGRAVITY; // Don't drop Tails 03-08-2000
|
|
else
|
|
target->flags &= ~MF_NOGRAVITY; // lose it if you for whatever reason have it, I'm looking at you shields
|
|
//
|
|
|
|
if (target->flags2 & MF2_NIGHTSPULL)
|
|
{
|
|
P_SetTarget(&target->tracer, NULL);
|
|
target->movefactor = 0; // reset NightsItemChase timer
|
|
}
|
|
|
|
// dead target is no more shootable
|
|
target->flags &= ~(MF_SHOOTABLE|MF_FLOAT|MF_SPECIAL);
|
|
target->flags2 &= ~(MF2_SKULLFLY|MF2_NIGHTSPULL);
|
|
target->health = 0; // This makes it easy to check if something's dead elsewhere.
|
|
|
|
if (target->type != MT_BATTLEBUMPER && target->type != MT_PLAYER)
|
|
{
|
|
target->shadowscale = 0;
|
|
}
|
|
|
|
if (LUA_HookMobjDeath(target, inflictor, source, damagetype) || P_MobjWasRemoved(target))
|
|
return;
|
|
|
|
P_ActivateThingSpecial(target, source);
|
|
|
|
//K_SetHitLagForObjects(target, inflictor, source, MAXHITLAGTICS, true);
|
|
|
|
P_UpdateRemovedOrbital(target, inflictor, source);
|
|
// Above block does not clean up rocket sneakers when a player dies, so we need to do it here target->target is null when using rocket sneakers
|
|
if (target->player)
|
|
K_DropRocketSneaker(target->player);
|
|
|
|
// Let EVERYONE know what happened to a player! 01-29-2002 Tails
|
|
if (target->player && !target->player->spectator)
|
|
{
|
|
target->renderflags &= ~RF_DONTDRAW;
|
|
}
|
|
|
|
// if killed by a player
|
|
if (source && source->player)
|
|
{
|
|
if (target->type == MT_RANDOMITEM)
|
|
{
|
|
P_SetTarget(&target->target, source);
|
|
|
|
if (!(gametyperules & GTR_CIRCUIT))
|
|
{
|
|
target->fuse = 2;
|
|
}
|
|
else
|
|
{
|
|
target->fuse = 2*TICRATE + 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if a player avatar dies...
|
|
if (target->player)
|
|
{
|
|
UINT8 i;
|
|
|
|
target->flags &= ~(MF_SOLID|MF_SHOOTABLE); // does not block
|
|
P_UnsetThingPosition(target);
|
|
target->flags |= MF_NOBLOCKMAP|MF_NOCLIPTHING|MF_NOGRAVITY;
|
|
P_SetThingPosition(target);
|
|
target->standingslope = NULL;
|
|
target->terrain = NULL;
|
|
target->pmomz = 0;
|
|
|
|
target->player->playerstate = PST_DEAD;
|
|
|
|
// respawn from where you died
|
|
target->player->respawn.pointx = target->x;
|
|
target->player->respawn.pointy = target->y;
|
|
target->player->respawn.pointz = target->z;
|
|
|
|
if (target->player == &players[consoleplayer])
|
|
{
|
|
// don't die in auto map,
|
|
// switch view prior to dying
|
|
if (automapactive)
|
|
AM_Stop();
|
|
}
|
|
|
|
//added : 22-02-98: recenter view for next life...
|
|
for (i = 0; i <= r_splitscreen; i++)
|
|
{
|
|
if (target->player == &players[displayplayers[i]])
|
|
{
|
|
localaiming[i] = 0;
|
|
}
|
|
}
|
|
|
|
if (target->player->spectator == false)
|
|
{
|
|
UINT32 skinflags = (demo.playback)
|
|
? demo.skinlist[demo.currentskinid[(target->player-players)]].flags
|
|
: skins[target->player->skin]->flags;
|
|
|
|
if (skinflags & SF_IRONMAN)
|
|
{
|
|
target->skin = skins[target->player->skin];
|
|
target->player->charflags = skinflags;
|
|
K_SpawnMagicianParticles(target, 5);
|
|
S_StartSound(target, sfx_slip);
|
|
}
|
|
|
|
target->renderflags &= ~RF_DONTDRAW;
|
|
}
|
|
|
|
K_DropEmeraldsFromPlayer(target->player, target->player->emeralds);
|
|
|
|
target->player->carry = CR_NONE;
|
|
|
|
K_KartResetPlayerColor(target->player);
|
|
|
|
P_ResetPlayer(target->player);
|
|
|
|
#define PlayerPointerRemove(field) \
|
|
if (P_MobjWasRemoved(field) == false) \
|
|
{ \
|
|
P_RemoveMobj(field); \
|
|
P_SetTarget(&field, NULL); \
|
|
}
|
|
|
|
PlayerPointerRemove(target->player->stumbleIndicator);
|
|
PlayerPointerRemove(target->player->wavedashIndicator);
|
|
PlayerPointerRemove(target->player->trickIndicator);
|
|
|
|
#undef PlayerPointerRemove
|
|
|
|
if (gametyperules & GTR_BUMPERS)
|
|
{
|
|
if (battleovertime.enabled >= 10*TICRATE) // Overtime Barrier is armed
|
|
{
|
|
target->player->pflags |= PF_ELIMINATED;
|
|
if (target->player->darkness_end < leveltime)
|
|
{
|
|
target->player->darkness_start = leveltime;
|
|
}
|
|
target->player->darkness_end = INFTICS;
|
|
}
|
|
|
|
K_CheckBumpers();
|
|
|
|
P_AddPlayerScore(target->player, -2);
|
|
}
|
|
|
|
target->player->trickpanel = TRICKSTATE_NONE;
|
|
|
|
ACS_RunPlayerDeathScript(target->player);
|
|
}
|
|
|
|
if (source && target && target->player && source->player && (target->player != source->player))
|
|
P_PlayVictorySound(source); // Killer laughs at you. LAUGHS! BWAHAHAHA!
|
|
|
|
// Other death animation effects
|
|
switch(target->type)
|
|
{
|
|
case MT_BLASTEXECUTOR:
|
|
if (target->spawnpoint)
|
|
P_LinedefExecute(target->spawnpoint->angle, (source ? source : inflictor), target->subsector->sector);
|
|
break;
|
|
|
|
case MT_EGGTRAP:
|
|
// Time for birdies! Yaaaaaaaay!
|
|
target->fuse = TICRATE;
|
|
break;
|
|
|
|
case MT_PLAYER:
|
|
if (damagetype != DMG_SPECTATOR)
|
|
{
|
|
fixed_t flingSpeed = FixedHypot(target->momx, target->momy);
|
|
angle_t flingAngle;
|
|
|
|
target->fuse = TICRATE*3; // timer before mobj disappears from view (even if not an actual player)
|
|
target->momx = target->momy = target->momz = 0;
|
|
|
|
Obj_SpawnDestroyedKart(target);
|
|
|
|
if (source && !P_MobjWasRemoved(source))
|
|
{
|
|
flingAngle = R_PointToAngle2(
|
|
source->x - source->momx, source->y - source->momy,
|
|
target->x, target->y
|
|
);
|
|
}
|
|
else
|
|
{
|
|
flingAngle = target->angle;
|
|
|
|
if (P_RandomByte(PR_ITEM_RINGS) & 1)
|
|
{
|
|
flingAngle -= ANGLE_45/2;
|
|
}
|
|
else
|
|
{
|
|
flingAngle += ANGLE_45/2;
|
|
}
|
|
}
|
|
|
|
// On -20 ring deaths, you're guaranteed to be hitting the ground from Tumble,
|
|
// so make sure that this draws at the correct angle.
|
|
target->rollangle = 0;
|
|
|
|
target->player->instaWhipCharge = 0;
|
|
|
|
fixed_t inflictorSpeed = 0;
|
|
if (!P_MobjWasRemoved(inflictor))
|
|
{
|
|
inflictorSpeed = FixedHypot(inflictor->momx, inflictor->momy);
|
|
if (inflictorSpeed > flingSpeed)
|
|
{
|
|
flingSpeed = inflictorSpeed;
|
|
}
|
|
}
|
|
|
|
boolean battle = (gametyperules & (GTR_BUMPERS | GTR_BOSS)) == GTR_BUMPERS;
|
|
P_InstaThrust(target, flingAngle, max(flingSpeed, 6 * target->scale) / (battle ? 1 : 3));
|
|
P_SetObjectMomZ(target, battle ? 20*FRACUNIT : 18*FRACUNIT, false);
|
|
}
|
|
|
|
// Prisons Free Play: don't eliminate P1 for
|
|
// spectating. Because in Free Play, this player
|
|
// can enter the game again, and these flags would
|
|
// make them intangible.
|
|
if (!(gametyperules & GTR_CHECKPOINTS) && K_Cooperative() && !target->player->spectator)
|
|
{
|
|
target->player->pflags |= PF_ELIMINATED;
|
|
|
|
if (!target->player->exiting)
|
|
{
|
|
target->player->pflags |= PF_NOCONTEST;
|
|
K_InitPlayerTally(target->player);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MT_KART_LEFTOVER:
|
|
if (!P_MobjWasRemoved(inflictor))
|
|
{
|
|
K_KartSolidBounce(target, inflictor);
|
|
target->momz = 20 * inflictor->scale * P_MobjFlip(inflictor);
|
|
}
|
|
target->z += P_MobjFlip(target);
|
|
target->tics = 175;
|
|
return;
|
|
|
|
// SRB2Kart:
|
|
|
|
case MT_ITEMCAPSULE:
|
|
{
|
|
UINT8 i;
|
|
mobj_t *attacker = inflictor ? inflictor : source;
|
|
mobj_t *part = target->hnext;
|
|
angle_t angle = FixedAngle(360*P_RandomFixed(PR_ITEM_DEBRIS));
|
|
INT16 spacing = (target->radius >> 1) / target->scale;
|
|
|
|
// set respawn fuse
|
|
if (damagetype == DMG_INSTAKILL)
|
|
; // Don't respawn (external)
|
|
else if (gametype == GT_TUTORIAL)
|
|
target->fuse = 5*TICRATE;
|
|
else if (K_CapsuleTimeAttackRules() == true)
|
|
; // Don't respawn (internal)
|
|
else if (target->threshold == KCAPSULE_RING)
|
|
target->fuse = 20*TICRATE;
|
|
else
|
|
target->fuse = 40*TICRATE;
|
|
|
|
// burst effects
|
|
for (i = 0; i < 2; i++)
|
|
{
|
|
mobj_t *blast = P_SpawnMobjFromMobj(target, 0, 0, target->info->height >> 1, MT_BATTLEBUMPER_BLAST);
|
|
blast->angle = angle + i*ANGLE_90;
|
|
P_SetScale(blast, 2*blast->scale/3);
|
|
blast->destscale = 2*blast->scale;
|
|
}
|
|
|
|
// dust effects
|
|
for (i = 0; i < 10; i++)
|
|
{
|
|
fixed_t rand_x;
|
|
fixed_t rand_y;
|
|
fixed_t rand_z;
|
|
|
|
// note: determinate random argument eval order
|
|
rand_z = P_RandomRange(PR_ITEM_DEBRIS, 0, 4*spacing);
|
|
rand_y = P_RandomRange(PR_ITEM_DEBRIS, -spacing, spacing);
|
|
rand_x = P_RandomRange(PR_ITEM_DEBRIS, -spacing, spacing);
|
|
mobj_t *puff = P_SpawnMobjFromMobj(
|
|
target,
|
|
rand_x * FRACUNIT,
|
|
rand_y * FRACUNIT,
|
|
rand_z * FRACUNIT,
|
|
MT_SPINDASHDUST
|
|
);
|
|
|
|
P_SetScale(puff, (puff->destscale *= 2));
|
|
puff->momz = puff->scale * P_MobjFlip(puff);
|
|
|
|
P_Thrust(puff, R_PointToAngle2(target->x, target->y, puff->x, puff->y), 3*puff->scale);
|
|
if (attacker)
|
|
{
|
|
puff->momx += attacker->momx;
|
|
puff->momy += attacker->momy;
|
|
puff->momz += attacker->momz;
|
|
}
|
|
}
|
|
|
|
// remove inside item
|
|
if (target->tracer && !P_MobjWasRemoved(target->tracer))
|
|
P_RemoveMobj(target->tracer);
|
|
|
|
// bust capsule caps
|
|
while (part && !P_MobjWasRemoved(part))
|
|
{
|
|
P_InstaThrust(part, part->angle + ANGLE_90, 6 * part->target->scale);
|
|
P_SetObjectMomZ(part, 6 * FRACUNIT, false);
|
|
part->fuse = TICRATE/2;
|
|
part->flags &= ~MF_NOGRAVITY;
|
|
|
|
if (attacker)
|
|
{
|
|
part->momx += attacker->momx;
|
|
part->momy += attacker->momy;
|
|
part->momz += attacker->momz;
|
|
}
|
|
part = part->hnext;
|
|
}
|
|
|
|
// give the player an item!
|
|
if (source && source->player)
|
|
{
|
|
player_t *player = source->player;
|
|
|
|
// MF2_STRONGBOX: always put the item right in the hotbar!
|
|
if (!(target->flags2 & MF2_STRONGBOX))
|
|
{
|
|
// special behavior for ring capsules
|
|
if (target->threshold == KCAPSULE_RING)
|
|
{
|
|
K_AwardPlayerRings(player, 5 * target->movecount, true);
|
|
break;
|
|
}
|
|
|
|
// special behavior for SPB capsules
|
|
if (target->threshold == KITEM_SPB)
|
|
{
|
|
K_ThrowKartItem(player, true, MT_SPB, 1, 0, 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (target->threshold < 1 || target->threshold >= NUMKARTITEMS) // bruh moment prevention
|
|
{
|
|
player->itemtype = KITEM_SAD;
|
|
K_SetPlayerItemAmount(player, 1);
|
|
}
|
|
else
|
|
{
|
|
player->itemtype = target->threshold;
|
|
if (K_GetShieldFromItem(player->itemtype) != KSHIELD_NONE) // never give more than 1 shield
|
|
K_SetPlayerItemAmount(player, 1);
|
|
else
|
|
K_SetPlayerItemAmount(player, max(1, target->movecount));
|
|
}
|
|
player->karthud[khud_itemblink] = TICRATE;
|
|
player->karthud[khud_itemblinkmode] = 0;
|
|
K_StopRoulette(&player->itemRoulette);
|
|
if (P_IsDisplayPlayer(player))
|
|
S_StartSound(NULL, sfx_itrolf);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case MT_BATTLECAPSULE:
|
|
{
|
|
mobj_t *cur;
|
|
angle_t dir = 0;
|
|
|
|
target->fuse = 16;
|
|
target->flags |= MF_NOCLIP|MF_NOCLIPTHING;
|
|
|
|
if (inflictor)
|
|
{
|
|
dir = R_PointToAngle2(inflictor->x, inflictor->y, target->x, target->y);
|
|
P_Thrust(target, dir, P_AproxDistance(inflictor->momx, inflictor->momy)/12);
|
|
}
|
|
else if (source)
|
|
dir = R_PointToAngle2(source->x, source->y, target->x, target->y);
|
|
|
|
target->momz += 8 * target->scale * P_MobjFlip(target);
|
|
target->flags &= ~MF_NOGRAVITY;
|
|
|
|
cur = target->hnext;
|
|
|
|
while (cur && !P_MobjWasRemoved(cur))
|
|
{
|
|
cur->momx = target->momx;
|
|
cur->momy = target->momy;
|
|
cur->momz = target->momz;
|
|
|
|
// Shoot every piece outward
|
|
if (!(cur->x == target->x && cur->y == target->y))
|
|
{
|
|
P_Thrust(cur,
|
|
R_PointToAngle2(target->x, target->y, cur->x, cur->y),
|
|
R_PointToDist2(target->x, target->y, cur->x, cur->y) / 12
|
|
);
|
|
}
|
|
|
|
cur->flags &= ~MF_NOGRAVITY;
|
|
cur->tics = TICRATE;
|
|
cur->frame &= ~FF_ANIMATE; // Stop animating the propellers
|
|
|
|
cur->hitlag = target->hitlag;
|
|
cur->eflags |= MFE_DAMAGEHITLAG;
|
|
|
|
cur = cur->hnext;
|
|
}
|
|
|
|
// Spawn three Followers (if possible)
|
|
if (mapheaderinfo[gamemap-1]->numFollowers)
|
|
{
|
|
dir = FixedAngle(P_RandomKey(PR_RANDOMAUDIENCE, 360)*FRACUNIT);
|
|
|
|
const fixed_t launchmomentum = 7 * mapobjectscale;
|
|
const fixed_t jaggedness = 4;
|
|
angle_t launchangle;
|
|
UINT8 i;
|
|
for (i = 0; i < 6; i++, dir += ANG60)
|
|
{
|
|
cur = P_SpawnMobj(
|
|
target->x, target->y,
|
|
target->z + target->height/2,
|
|
MT_RANDOMAUDIENCE
|
|
);
|
|
|
|
// We check if you have some horrible Lua
|
|
if (P_MobjWasRemoved(cur))
|
|
break;
|
|
|
|
Obj_AudienceInit(cur, NULL, -1);
|
|
|
|
// We check again if the list is invalid
|
|
if (P_MobjWasRemoved(cur))
|
|
break;
|
|
|
|
cur->hitlag = target->hitlag;
|
|
|
|
cur->destscale /= 2;
|
|
P_SetScale(cur, cur->destscale/TICRATE);
|
|
cur->scalespeed = cur->destscale/TICRATE;
|
|
cur->z -= cur->height/2;
|
|
|
|
if (source && !P_MobjWasRemoved(source))
|
|
{
|
|
// flags are NOT from the target - just in case it's just been placed on the ceiling as a gimmick
|
|
cur->flags2 |= (source->flags2 & MF2_OBJECTFLIP);
|
|
cur->eflags |= (source->eflags & MFE_VERTICALFLIP);
|
|
}
|
|
else
|
|
{
|
|
// Welp, nothing to be done here
|
|
cur->flags2 |= (target->flags2 & MF2_OBJECTFLIP);
|
|
cur->eflags |= (target->eflags & MFE_VERTICALFLIP);
|
|
}
|
|
|
|
|
|
launchangle = FixedAngle(
|
|
(
|
|
(
|
|
P_RandomRange(PR_RANDOMAUDIENCE, 12/jaggedness, 24/jaggedness) * jaggedness
|
|
) + (i & 1)*16
|
|
) * FRACUNIT
|
|
);
|
|
|
|
cur->momz = P_MobjFlip(target) // THIS one uses target!
|
|
* P_ReturnThrustY(cur, launchangle, launchmomentum);
|
|
|
|
cur->angle = dir;
|
|
|
|
P_InstaThrust(
|
|
cur, cur->angle,
|
|
P_ReturnThrustX(cur, launchangle, launchmomentum)
|
|
);
|
|
|
|
cur->fuse = (3*TICRATE)/2;
|
|
cur->flags |= MF_NOCLIPHEIGHT;
|
|
}
|
|
}
|
|
|
|
S_StartSound(target, sfx_mbs60);
|
|
|
|
P_AddBrokenPrison(target, inflictor, source);
|
|
}
|
|
break;
|
|
|
|
case MT_CDUFO:
|
|
S_StartSound(inflictor, sfx_mbs60);
|
|
|
|
target->momz = -(3*mapobjectscale)/2;
|
|
target->fuse = 2*TICRATE;
|
|
|
|
P_AddBrokenPrison(target, inflictor, source);
|
|
break;
|
|
|
|
case MT_BATTLEBUMPER:
|
|
{
|
|
mobj_t *owner = target->target;
|
|
mobj_t *overlay;
|
|
|
|
S_StartSound(target, sfx_kc52);
|
|
target->flags &= ~MF_NOGRAVITY;
|
|
|
|
target->destscale = (3 * target->destscale) / 2;
|
|
target->scalespeed = FRACUNIT/100;
|
|
|
|
if (owner && !P_MobjWasRemoved(owner))
|
|
{
|
|
P_Thrust(target, R_PointToAngle2(owner->x, owner->y, target->x, target->y), 4 * target->scale);
|
|
}
|
|
|
|
target->momz += (18 * target->scale) * P_MobjFlip(target);
|
|
target->fuse = 8;
|
|
|
|
overlay = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_OVERLAY);
|
|
|
|
P_SetTarget(&target->tracer, overlay);
|
|
P_SetTarget(&overlay->target, target);
|
|
|
|
overlay->color = target->color;
|
|
P_SetMobjState(overlay, S_INVISIBLE);
|
|
}
|
|
break;
|
|
|
|
case MT_DROPTARGET:
|
|
case MT_DROPTARGET_SHIELD:
|
|
target->fuse = 1;
|
|
break;
|
|
|
|
case MT_BANANA:
|
|
case MT_BANANA_SHIELD:
|
|
{
|
|
const UINT8 numParticles = 8;
|
|
const angle_t diff = ANGLE_MAX / numParticles;
|
|
UINT8 i;
|
|
|
|
for (i = 0; i < numParticles; i++)
|
|
{
|
|
mobj_t *spark = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_BANANA_SPARK);
|
|
spark->angle = (diff * i) - (diff / 2);
|
|
|
|
if (inflictor != NULL && P_MobjWasRemoved(inflictor) == false)
|
|
{
|
|
spark->angle += K_MomentumAngle(inflictor);
|
|
spark->momx += inflictor->momx / 2;
|
|
spark->momy += inflictor->momy / 2;
|
|
spark->momz += inflictor->momz / 2;
|
|
}
|
|
|
|
P_SetObjectMomZ(spark, (12 + P_RandomRange(PR_DECORATION, -4, 4)) * FRACUNIT, true);
|
|
P_Thrust(spark, spark->angle, (12 + P_RandomRange(PR_DECORATION, -4, 4)) * spark->scale);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case MT_MONITOR:
|
|
Obj_MonitorOnDeath(target, source);
|
|
break;
|
|
case MT_BATTLEUFO:
|
|
Obj_BattleUFODeath(target, inflictor);
|
|
break;
|
|
case MT_BLENDEYE_MAIN:
|
|
VS_BlendEye_Death(target);
|
|
break;
|
|
case MT_BLENDEYE_GLASS:
|
|
VS_BlendEye_Glass_Death(target);
|
|
break;
|
|
case MT_BLENDEYE_PUYO:
|
|
VS_PuyoDeath(target);
|
|
break;
|
|
case MT_EMFAUCET_DRIP:
|
|
Obj_EMZDripDeath(target);
|
|
break;
|
|
case MT_FLYBOT767:
|
|
Obj_FlybotDeath(target);
|
|
break;
|
|
case MT_ANCIENTGEAR:
|
|
Obj_AncientGearDeath(target, source);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if ((target->type == MT_JAWZ || target->type == MT_JAWZ_SHIELD) && !(target->flags2 & MF2_AMBUSH))
|
|
{
|
|
target->z += P_MobjFlip(target)*20*target->scale;
|
|
}
|
|
|
|
// kill tracer
|
|
if (target->type == MT_FROGGER)
|
|
{
|
|
if (target->tracer && !P_MobjWasRemoved(target->tracer))
|
|
P_KillMobj(target->tracer, inflictor, source, DMG_NORMAL);
|
|
}
|
|
|
|
if (target->type == MT_FROGGER || target->type == MT_ROBRA_HEAD || target->type == MT_BLUEROBRA_HEAD) // clean hnext list
|
|
{
|
|
mobj_t *cur = target->hnext;
|
|
while (cur && !P_MobjWasRemoved(cur))
|
|
{
|
|
P_KillMobj(cur, inflictor, source, DMG_NORMAL);
|
|
cur = cur->hnext;
|
|
}
|
|
}
|
|
|
|
// Final state setting - do something instead of P_SetMobjState;
|
|
if (target->type == MT_SPIKE && target->info->deathstate != S_NULL)
|
|
{
|
|
const angle_t ang = ((inflictor) ? inflictor->angle : 0) + ANGLE_90;
|
|
const fixed_t scale = target->scale;
|
|
const fixed_t xoffs = P_ReturnThrustX(target, ang, 8*scale), yoffs = P_ReturnThrustY(target, ang, 8*scale);
|
|
const UINT16 flip = (target->eflags & MFE_VERTICALFLIP);
|
|
mobj_t *chunk;
|
|
fixed_t momz;
|
|
|
|
S_StartSound(target, target->info->deathsound);
|
|
|
|
if (target->info->xdeathstate != S_NULL)
|
|
{
|
|
momz = 6*scale;
|
|
if (flip)
|
|
momz *= -1;
|
|
#define makechunk(angtweak, xmov, ymov) \
|
|
chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_SPIKE);\
|
|
P_SetMobjState(chunk, target->info->xdeathstate);\
|
|
chunk->health = 0;\
|
|
chunk->angle = angtweak;\
|
|
P_UnsetThingPosition(chunk);\
|
|
chunk->flags = MF_NOCLIP;\
|
|
chunk->x += xmov;\
|
|
chunk->y += ymov;\
|
|
P_SetThingPosition(chunk);\
|
|
P_InstaThrust(chunk,chunk->angle, 4*scale);\
|
|
chunk->momz = momz
|
|
|
|
makechunk(ang + ANGLE_180, -xoffs, -yoffs);
|
|
makechunk(ang, xoffs, yoffs);
|
|
|
|
#undef makechunk
|
|
}
|
|
|
|
momz = 7*scale;
|
|
if (flip)
|
|
momz *= -1;
|
|
|
|
chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_SPIKE);
|
|
P_SetMobjState(chunk, target->info->deathstate);
|
|
chunk->health = 0;
|
|
chunk->angle = ang + ANGLE_180;
|
|
P_UnsetThingPosition(chunk);
|
|
chunk->flags = MF_NOCLIP;
|
|
chunk->x -= xoffs;
|
|
chunk->y -= yoffs;
|
|
if (flip)
|
|
chunk->z -= 12*scale;
|
|
else
|
|
chunk->z += 12*scale;
|
|
P_SetThingPosition(chunk);
|
|
P_InstaThrust(chunk, chunk->angle, 2*scale);
|
|
chunk->momz = momz;
|
|
|
|
P_SetMobjState(target, target->info->deathstate);
|
|
target->health = 0;
|
|
target->angle = ang;
|
|
P_UnsetThingPosition(target);
|
|
target->flags = MF_NOCLIP;
|
|
target->x += xoffs;
|
|
target->y += yoffs;
|
|
target->z = chunk->z;
|
|
P_SetThingPosition(target);
|
|
P_InstaThrust(target, target->angle, 2*scale);
|
|
target->momz = momz;
|
|
}
|
|
else if (target->type == MT_WALLSPIKE && target->info->deathstate != S_NULL)
|
|
{
|
|
const angle_t ang = (/*(inflictor) ? inflictor->angle : */target->angle) + ANGLE_90;
|
|
const fixed_t scale = target->scale;
|
|
const fixed_t xoffs = P_ReturnThrustX(target, ang, 8*scale), yoffs = P_ReturnThrustY(target, ang, 8*scale), forwardxoffs = P_ReturnThrustX(target, target->angle, 7*scale), forwardyoffs = P_ReturnThrustY(target, target->angle, 7*scale);
|
|
const UINT16 flip = (target->eflags & MFE_VERTICALFLIP);
|
|
mobj_t *chunk;
|
|
boolean sprflip;
|
|
|
|
S_StartSound(target, target->info->deathsound);
|
|
if (!P_MobjWasRemoved(target->tracer))
|
|
P_RemoveMobj(target->tracer);
|
|
|
|
if (target->info->xdeathstate != S_NULL)
|
|
{
|
|
sprflip = P_RandomChance(PR_DECORATION, FRACUNIT/2);
|
|
|
|
#define makechunk(angtweak, xmov, ymov) \
|
|
chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_WALLSPIKE);\
|
|
P_SetMobjState(chunk, target->info->xdeathstate);\
|
|
chunk->health = 0;\
|
|
chunk->angle = target->angle;\
|
|
P_UnsetThingPosition(chunk);\
|
|
chunk->flags = MF_NOCLIP;\
|
|
chunk->x += xmov - forwardxoffs;\
|
|
chunk->y += ymov - forwardyoffs;\
|
|
P_SetThingPosition(chunk);\
|
|
P_InstaThrust(chunk, angtweak, 4*scale);\
|
|
chunk->momz = P_RandomRange(PR_DECORATION, 5, 7)*scale;\
|
|
if (flip)\
|
|
chunk->momz *= -1;\
|
|
if (sprflip)\
|
|
chunk->frame |= FF_VERTICALFLIP
|
|
|
|
makechunk(ang + ANGLE_180, -xoffs, -yoffs);
|
|
sprflip = !sprflip;
|
|
makechunk(ang, xoffs, yoffs);
|
|
|
|
#undef makechunk
|
|
}
|
|
|
|
sprflip = P_RandomChance(PR_DECORATION, FRACUNIT/2);
|
|
|
|
chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_WALLSPIKE);
|
|
|
|
P_SetMobjState(chunk, target->info->deathstate);
|
|
chunk->health = 0;
|
|
chunk->angle = target->angle;
|
|
P_UnsetThingPosition(chunk);
|
|
chunk->flags = MF_NOCLIP;
|
|
chunk->x += forwardxoffs - xoffs;
|
|
chunk->y += forwardyoffs - yoffs;
|
|
P_SetThingPosition(chunk);
|
|
P_InstaThrust(chunk, ang + ANGLE_180, 2*scale);
|
|
chunk->momz = P_RandomRange(PR_DECORATION, 5, 7)*scale;
|
|
if (flip)
|
|
chunk->momz *= -1;
|
|
if (sprflip)
|
|
chunk->frame |= FF_VERTICALFLIP;
|
|
|
|
P_SetMobjState(target, target->info->deathstate);
|
|
target->health = 0;
|
|
P_UnsetThingPosition(target);
|
|
target->flags = MF_NOCLIP;
|
|
target->x += forwardxoffs + xoffs;
|
|
target->y += forwardyoffs + yoffs;
|
|
P_SetThingPosition(target);
|
|
P_InstaThrust(target, ang, 2*scale);
|
|
target->momz = P_RandomRange(PR_DECORATION, 5, 7)*scale;
|
|
if (flip)
|
|
target->momz *= -1;
|
|
if (!sprflip)
|
|
target->frame |= FF_VERTICALFLIP;
|
|
}
|
|
else if (target->type == MT_BLENDEYE_GENERATOR && !P_MobjWasRemoved(inflictor))
|
|
{
|
|
mobj_t *refobj = (inflictor->type == MT_INSTAWHIP) ? source : inflictor;
|
|
angle_t impactangle = R_PointToAngle2(target->x, target->y, refobj->x - refobj->momx, refobj->y - refobj->momy) - (target->angle + ANGLE_90);
|
|
|
|
if (P_MobjWasRemoved(target->tracer) == false)
|
|
{
|
|
target->tracer->flags2 &= ~MF2_FRET;
|
|
target->tracer->flags |= MF_SHOOTABLE;
|
|
P_DamageMobj(target->tracer, inflictor, source, 1, DMG_NORMAL);
|
|
target->tracer->flags &= ~MF_SHOOTABLE;
|
|
}
|
|
|
|
P_SetMobjState(
|
|
target,
|
|
((impactangle < ANGLE_180)
|
|
? target->info->deathstate
|
|
: target->info->xdeathstate
|
|
)
|
|
);
|
|
}
|
|
else if (target->player)
|
|
{
|
|
P_SetPlayerMobjState(target, target->info->deathstate);
|
|
}
|
|
else
|
|
#ifdef DEBUG_NULL_DEATHSTATE
|
|
P_SetMobjState(target, S_NULL);
|
|
#else
|
|
P_SetMobjState(target, target->info->deathstate);
|
|
#endif
|
|
|
|
/** \note For player, the above is redundant because of P_SetMobjState (target, S_PLAY_DIE1)
|
|
in P_DamageMobj()
|
|
Graue 12-22-2003 */
|
|
}
|
|
|
|
static boolean P_PlayerHitsPlayer(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
|
|
{
|
|
(void)inflictor;
|
|
(void)damage;
|
|
|
|
// SRB2Kart: We want to hurt ourselves, so it's now DMG_CANTHURTSELF
|
|
if (damagetype & DMG_CANTHURTSELF)
|
|
{
|
|
// You can't kill yourself, idiot...
|
|
if (source == target)
|
|
return false;
|
|
|
|
#if 0
|
|
// Don't hurt your team, either!
|
|
if (G_SameTeam(source->player, target->player) == true)
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static boolean P_KillPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 type)
|
|
{
|
|
(void)inflictor;
|
|
(void)source;
|
|
|
|
const boolean beforeexit = !(player->exiting || (player->pflags & PF_NOCONTEST));
|
|
|
|
if (type == DMG_SPECTATOR && (G_GametypeHasTeams() || G_GametypeHasSpectators()))
|
|
{
|
|
P_SetPlayerSpectator(player-players);
|
|
}
|
|
else
|
|
{
|
|
// DMG_TIMEOVER: player explosion
|
|
if (player->respawn.state != RESPAWNST_NONE && type != DMG_TIMEOVER)
|
|
{
|
|
K_DoInstashield(player);
|
|
return false;
|
|
}
|
|
|
|
if (player->exiting == false && specialstageinfo.valid == true)
|
|
{
|
|
if (type == DMG_DEATHPIT)
|
|
{
|
|
HU_DoTitlecardCEcho(player, "FALL OUT!", false);
|
|
}
|
|
|
|
// This must be done before the condition to set
|
|
// destscale = 1, so any special stage death
|
|
// shrinks the player to a speck.
|
|
P_DoPlayerExit(player, PF_NOCONTEST);
|
|
}
|
|
|
|
if (player->exiting && type == DMG_DEATHPIT)
|
|
{
|
|
// If the player already finished the race, and
|
|
// they fall into a death pit afterward, their
|
|
// body shrinks into nothingness.
|
|
player->mo->destscale = 1;
|
|
player->mo->flags |= MF_NOCLIPTHING;
|
|
player->tumbleBounces = 0;
|
|
|
|
return false;
|
|
}
|
|
|
|
if (modeattacking & ATTACKING_SPB)
|
|
{
|
|
// Death in SPB Attack is an instant loss.
|
|
P_DoPlayerExit(player, PF_NOCONTEST);
|
|
}
|
|
}
|
|
|
|
switch (type)
|
|
{
|
|
case DMG_DEATHPIT:
|
|
// Fell off the stage
|
|
if (player->roundconditions.fell_off == false
|
|
&& beforeexit == true)
|
|
{
|
|
player->roundconditions.fell_off = true;
|
|
player->roundconditions.checkthisframe = true;
|
|
}
|
|
|
|
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);
|
|
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)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (player->mo->health <= 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Quick respawn; does not kill
|
|
return K_DoIngameRespawn(player), false;
|
|
|
|
case DMG_SPECTATOR:
|
|
// disappearifies, but still gotta put items back in play
|
|
break;
|
|
|
|
case DMG_TIMEOVER:
|
|
player->pflags |= PF_ELIMINATED;
|
|
//FALLTHRU
|
|
default:
|
|
// Everything else REALLY kills
|
|
if (leveltime < starttime)
|
|
{
|
|
K_DoFault(player);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void AddTimesHit(player_t *player)
|
|
{
|
|
const INT32 oldtimeshit = player->timeshit;
|
|
|
|
player->timeshit++;
|
|
|
|
// overflow prevention
|
|
if (player->timeshit < oldtimeshit)
|
|
{
|
|
player->timeshit = oldtimeshit;
|
|
}
|
|
}
|
|
|
|
static void AddNullHitlag(player_t *player, tic_t oldHitlag)
|
|
{
|
|
if (player == NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Hitlag from what would normally be damage but the
|
|
// player was invulnerable.
|
|
//
|
|
// If we're constantly getting hit the same number of
|
|
// times, we're probably standing on a damage floor.
|
|
//
|
|
// Checking if we're hit more than before ensures that:
|
|
//
|
|
// 1) repeating damage doesn't count
|
|
// 2) new damage sources still count
|
|
|
|
if (player->timeshit <= player->timeshitprev || player->hyudorotimer > 0)
|
|
{
|
|
player->nullHitlag += (player->mo->hitlag - oldHitlag);
|
|
}
|
|
}
|
|
|
|
static boolean P_FlashingException(const player_t *player, const mobj_t *inflictor)
|
|
{
|
|
if (!inflictor)
|
|
{
|
|
// Sector damage always behaves the same.
|
|
return false;
|
|
}
|
|
|
|
if (inflictor->type == MT_SSMINE)
|
|
{
|
|
// Mine's first hit is DMG_EXPLODE.
|
|
// Afterward, it leaves a spinout hitbox which remains for a short period.
|
|
// If the spinout hitbox ignored flashing tics, you would be combod every tic and die instantly.
|
|
// DMG_EXPLODE already ignores flashing tics (correct behavior).
|
|
return false;
|
|
}
|
|
|
|
if (inflictor->type == MT_SPB)
|
|
{
|
|
// The SPB does not die on impact with players other than its intended target.
|
|
// Ignoring flashing tics would cause an endless combo on anyone who gets in way of the SPB.
|
|
// Upon hitting its target, DMG_EXPLODE will be used (which ignores flashing tics).
|
|
return false;
|
|
}
|
|
|
|
if (!P_IsKartItem(inflictor->type) && inflictor->type != MT_PLAYER)
|
|
{
|
|
// Exception only applies to player items.
|
|
// Also applies to players because of PvP collision.
|
|
// Lightning Shield also uses the player object as inflictor.
|
|
return false;
|
|
}
|
|
|
|
if (!P_PlayerInPain(player))
|
|
{
|
|
// Flashing tics is sometimes used in a way unrelated to damage.
|
|
// E.g. picking up a power-up gives you flashing tics.
|
|
// Respect this usage of flashing tics.
|
|
return false;
|
|
}
|
|
|
|
// Flashing tics are ignored.
|
|
return true;
|
|
}
|
|
|
|
// P_DamageMobj for 0x0010 compat.
|
|
// I know this sucks ass, but this function is legitimately too complicated to add more behavior switches.
|
|
static boolean P_DamageMobjCompat(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
|
|
{
|
|
player_t *player;
|
|
player_t *playerInflictor;
|
|
boolean force = false;
|
|
boolean spbpop = false;
|
|
boolean downgraded = false;
|
|
|
|
INT32 laglength = 6;
|
|
|
|
if (objectplacing)
|
|
return false;
|
|
|
|
if (target->health <= 0)
|
|
return false;
|
|
|
|
// Spectator handling
|
|
if (damagetype != DMG_SPECTATOR && target->player && target->player->spectator)
|
|
return false;
|
|
|
|
// source is checked without a removal guard in so many places that it's genuinely less work to do it here.
|
|
if (source && P_MobjWasRemoved(source))
|
|
source = NULL;
|
|
|
|
if (source && source->player && source->player->spectator)
|
|
return false;
|
|
|
|
if (((damagetype & DMG_TYPEMASK) == DMG_STING)
|
|
|| ((inflictor && !P_MobjWasRemoved(inflictor)) && inflictor->type == MT_BANANA && inflictor->health <= 1))
|
|
{
|
|
laglength = 2;
|
|
}
|
|
else if (target->type == MT_DROPTARGET || target->type == MT_DROPTARGET_SHIELD)
|
|
{
|
|
laglength = 0; // handled elsewhere
|
|
}
|
|
|
|
switch (target->type)
|
|
{
|
|
case MT_MONITOR:
|
|
damage = Obj_MonitorGetDamage(target, inflictor, damagetype);
|
|
Obj_MonitorOnDamage(target, inflictor, damage);
|
|
break;
|
|
case MT_CDUFO:
|
|
// Make it possible to pick them up during race
|
|
if (inflictor->type == MT_ORBINAUT_SHIELD || inflictor->type == MT_JAWZ_SHIELD)
|
|
return false;
|
|
break;
|
|
|
|
case MT_SPB:
|
|
spbpop = (damagetype & DMG_TYPEMASK) == DMG_VOLTAGE;
|
|
if (spbpop && source && source->player
|
|
&& source->player->roundconditions.spb_neuter == false)
|
|
{
|
|
source->player->roundconditions.spb_neuter = true;
|
|
source->player->roundconditions.checkthisframe = true;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Everything above here can't be forced.
|
|
{
|
|
UINT8 shouldForce = LUA_HookShouldDamage(target, inflictor, source, damage, damagetype);
|
|
if (P_MobjWasRemoved(target))
|
|
return (shouldForce == 1); // mobj was removed
|
|
if (shouldForce == 1)
|
|
force = true;
|
|
else if (shouldForce == 2)
|
|
return false;
|
|
}
|
|
|
|
switch (target->type)
|
|
{
|
|
case MT_BALLSWITCH_BALL:
|
|
Obj_BallSwitchDamaged(target, inflictor, source);
|
|
return false;
|
|
|
|
case MT_SA2_CRATE:
|
|
case MT_ICECAPBLOCK:
|
|
return Obj_TryCrateDamage(target, inflictor);
|
|
|
|
case MT_KART_LEFTOVER:
|
|
// intangible (do not let instawhip shred damage)
|
|
if (Obj_DestroyKart(target))
|
|
return false;
|
|
|
|
P_SetObjectMomZ(target, 12*FRACUNIT, false);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (!force)
|
|
{
|
|
if (!spbpop)
|
|
{
|
|
if (!(target->flags & MF_SHOOTABLE))
|
|
return false; // shouldn't happen...
|
|
}
|
|
}
|
|
|
|
if (target->flags2 & MF2_SKULLFLY)
|
|
target->momx = target->momy = target->momz = 0;
|
|
|
|
if (target->flags & (MF_ENEMY|MF_BOSS))
|
|
{
|
|
if (!force && target->flags2 & MF2_FRET) // Currently flashing from being hit
|
|
return false;
|
|
|
|
if (LUA_HookMobjDamage(target, inflictor, source, damage, damagetype) || P_MobjWasRemoved(target))
|
|
return true;
|
|
|
|
if (target->health > 1)
|
|
target->flags2 |= MF2_FRET;
|
|
}
|
|
|
|
player = target->player;
|
|
playerInflictor = inflictor ? inflictor->player : NULL;
|
|
|
|
if (playerInflictor)
|
|
{
|
|
AddTimesHit(playerInflictor);
|
|
}
|
|
|
|
if (player) // Player is the target
|
|
{
|
|
AddTimesHit(player);
|
|
|
|
if (player->pflags & PF_GODMODE)
|
|
return false;
|
|
|
|
if (!force)
|
|
{
|
|
// Player hits another player
|
|
if (source && source->player)
|
|
{
|
|
if (!P_PlayerHitsPlayer(target, inflictor, source, damage, damagetype))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (source && source->player)
|
|
{
|
|
if (source->player->roundconditions.hit_midair == false
|
|
&& source != target
|
|
&& inflictor
|
|
&& K_IsMissileOrKartItem(inflictor)
|
|
&& target->player->airtime > TICRATE/2
|
|
&& source->player->airtime > TICRATE/2)
|
|
{
|
|
source->player->roundconditions.hit_midair = true;
|
|
source->player->roundconditions.checkthisframe = true;
|
|
}
|
|
|
|
if (source->player->roundconditions.hit_drafter_lookback == false
|
|
&& source != target
|
|
&& target->player->lastdraft == (source->player - players)
|
|
&& (K_GetKartButtons(source->player) & BT_LOOKBACK) == BT_LOOKBACK
|
|
/*&& (AngleDelta(K_MomentumAngle(source), R_PointToAngle2(source->x, source->y, target->x, target->y)) > ANGLE_90)*/)
|
|
{
|
|
source->player->roundconditions.hit_drafter_lookback = true;
|
|
source->player->roundconditions.checkthisframe = true;
|
|
}
|
|
|
|
if (source->player->roundconditions.giant_foe_shrunken_orbi == false
|
|
&& source != target
|
|
&& player->growshrinktimer > 0
|
|
&& !P_MobjWasRemoved(inflictor)
|
|
&& inflictor->type == MT_ORBINAUT
|
|
&& inflictor->scale < FixedMul((FRACUNIT + SHRINK_SCALE), mapobjectscale * 2)) // halfway between base scale and shrink scale, a little bit of leeway
|
|
{
|
|
source->player->roundconditions.giant_foe_shrunken_orbi = true;
|
|
source->player->roundconditions.checkthisframe = true;
|
|
}
|
|
|
|
if (source == target
|
|
&& !P_MobjWasRemoved(inflictor)
|
|
&& inflictor->type == MT_SPBEXPLOSION
|
|
&& inflictor->threshold == KITEM_EGGMAN
|
|
&& !P_MobjWasRemoved(inflictor->tracer)
|
|
&& inflictor->tracer != source
|
|
&& inflictor->tracer->player
|
|
&& inflictor->tracer->player->roundconditions.returntosender_mark == false)
|
|
{
|
|
inflictor->tracer->player->roundconditions.returntosender_mark = true;
|
|
inflictor->tracer->player->roundconditions.checkthisframe = true;
|
|
}
|
|
}
|
|
else if (!(inflictor && inflictor->player)
|
|
&& !(player->exiting || player->laps > numlaps)
|
|
&& damagetype != DMG_DEATHPIT)
|
|
{
|
|
// laps will never increment outside of GTR_CIRCUIT, so this is still fine
|
|
const UINT8 requiredbit = 1<<(player->laps & 7);
|
|
|
|
if (!(player->roundconditions.hittrackhazard[player->laps/8] & requiredbit))
|
|
{
|
|
player->roundconditions.hittrackhazard[player->laps/8] |= requiredbit;
|
|
player->roundconditions.checkthisframe = true;
|
|
}
|
|
}
|
|
|
|
// Instant-Death
|
|
if ((damagetype & DMG_DEATHMASK))
|
|
{
|
|
if (!P_KillPlayer(player, inflictor, source, damagetype))
|
|
return false;
|
|
}
|
|
else if (LUA_HookMobjDamage(target, inflictor, source, damage, damagetype))
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
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;
|
|
|
|
// Check if the player is allowed to be damaged!
|
|
// If not, then spawn the instashield effect instead.
|
|
if (!force)
|
|
{
|
|
boolean invincible = true;
|
|
boolean clash = false;
|
|
sfxenum_t sfx = sfx_None;
|
|
|
|
if (!(gametyperules & GTR_BUMPERS))
|
|
{
|
|
if (damagetype & DMG_STEAL)
|
|
{
|
|
// Gametype does not have bumpers, steal damage is intended to not do anything
|
|
// (No instashield is intentional)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (player->invincibilitytimer > 0)
|
|
{
|
|
sfx = sfx_invind;
|
|
}
|
|
else if (K_IsBigger(target, inflictor) == true &&
|
|
// SPB bypasses grow (K_IsBigger handles NULL check)
|
|
(type != DMG_EXPLODE || inflictor->type != MT_SPBEXPLOSION || !inflictor->movefactor))
|
|
{
|
|
sfx = sfx_grownd;
|
|
}
|
|
else if (K_PlayerGuard(player))
|
|
{
|
|
sfx = sfx_s3k3a;
|
|
clash = true;
|
|
}
|
|
else if (player->overshield &&
|
|
(type != DMG_EXPLODE || inflictor->type != MT_SPBEXPLOSION || !inflictor->movefactor))
|
|
{
|
|
clash = true;
|
|
}
|
|
else if (player->hyudorotimer > 0)
|
|
;
|
|
else
|
|
{
|
|
invincible = false;
|
|
}
|
|
|
|
// Hack for instawhip-guard counter, lets invincible players lose to guard
|
|
if (inflictor == target)
|
|
{
|
|
invincible = false;
|
|
}
|
|
|
|
if (player->pflags2 & PF2_ALWAYSDAMAGED)
|
|
{
|
|
invincible = false;
|
|
clash = false;
|
|
}
|
|
|
|
// TODO: doing this from P_DamageMobj limits punting to objects that damage the player.
|
|
// And it may be kind of yucky.
|
|
// But this is easier than accounting for every condition in PIT_CheckThing!
|
|
if (inflictor && K_PuntCollide(inflictor, target))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (invincible && type != DMG_WHUMBLE)
|
|
{
|
|
const INT32 oldHitlag = target->hitlag;
|
|
const INT32 oldHitlagInflictor = inflictor ? inflictor->hitlag : 0;
|
|
|
|
// Damage during hitlag should be a no-op
|
|
// for invincibility states because there
|
|
// are no flashing tics. If the damage is
|
|
// from a constant source, a deadlock
|
|
// would occur.
|
|
|
|
if (target->eflags & MFE_PAUSED)
|
|
{
|
|
player->timeshit--; // doesn't count
|
|
|
|
if (playerInflictor)
|
|
{
|
|
playerInflictor->timeshit--;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
laglength = max(laglength / 2, 1);
|
|
K_SetHitLagForObjects(target, inflictor, source, laglength, false);
|
|
|
|
AddNullHitlag(player, oldHitlag);
|
|
AddNullHitlag(playerInflictor, oldHitlagInflictor);
|
|
|
|
if (player->timeshit > player->timeshitprev)
|
|
{
|
|
S_StartSound(target, sfx);
|
|
}
|
|
|
|
if (clash)
|
|
{
|
|
player->spheres = max(player->spheres - 5, 0);
|
|
|
|
if (inflictor)
|
|
{
|
|
K_DoPowerClash(target, inflictor);
|
|
|
|
if (inflictor->type == MT_SUPER_FLICKY)
|
|
{
|
|
Obj_BlockSuperFlicky(inflictor);
|
|
}
|
|
}
|
|
else if (source)
|
|
K_DoPowerClash(target, source);
|
|
}
|
|
|
|
// Full invulnerability
|
|
K_DoInstashield(player);
|
|
return false;
|
|
}
|
|
{
|
|
// Check if we should allow wombo combos (hard hits by default, inverted by the presence of DMG_WOMBO).
|
|
boolean allowcombo = ((hardhit || (type == DMG_STUMBLE || type == DMG_WHUMBLE)) == !(damagetype & DMG_WOMBO));
|
|
|
|
// Tumble/stumble is a special case.
|
|
if (type == DMG_TUMBLE)
|
|
{
|
|
// don't allow constant combo
|
|
if (player->tumbleBounces == 1 && (P_MobjFlip(target)*target->momz > 0))
|
|
allowcombo = false;
|
|
}
|
|
else if (type == DMG_STUMBLE || type == DMG_WHUMBLE)
|
|
{
|
|
// don't allow constant combo
|
|
if (player->tumbleBounces == TUMBLEBOUNCES-1 && (P_MobjFlip(target)*target->momz > 0))
|
|
{
|
|
if (type == DMG_STUMBLE)
|
|
return false; // No-sell strings of stumble
|
|
|
|
allowcombo = false;
|
|
}
|
|
}
|
|
|
|
if (inflictor && !P_MobjWasRemoved(inflictor) && inflictor->momx == 0 && inflictor->momy == 0 && inflictor->momz == 0)
|
|
{
|
|
// Probably a map hazard.
|
|
allowcombo = false;
|
|
}
|
|
|
|
if (allowcombo == false && (target->eflags & MFE_PAUSED))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// DMG_EXPLODE excluded from flashtic checks to prevent dodging eggbox/SPB with weak spinout
|
|
if ((target->hitlag == 0 || allowcombo == false) &&
|
|
player->flashing > 0 &&
|
|
type != DMG_EXPLODE &&
|
|
type != DMG_STUMBLE &&
|
|
type != DMG_WHUMBLE &&
|
|
P_FlashingException(player, inflictor) == false)
|
|
{
|
|
// Post-hit invincibility
|
|
K_DoInstashield(player);
|
|
return false;
|
|
}
|
|
else if (target->flags2 & MF2_ALREADYHIT) // do not deal extra damage in the same tic
|
|
{
|
|
K_SetHitLagForObjects(target, inflictor, source, laglength, true);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (gametyperules & GTR_BUMPERS)
|
|
{
|
|
if (damagetype & DMG_STEAL)
|
|
{
|
|
// Steals 2 bumpers
|
|
damage = 2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Do not die from damage outside of bumpers health system
|
|
damage = 0;
|
|
}
|
|
|
|
boolean softenTumble = false;
|
|
|
|
// Sting and stumble shouldn't be rewarding Battle hits.
|
|
if (type == DMG_STING || type == DMG_STUMBLE)
|
|
{
|
|
damage = 0;
|
|
|
|
if (source && source != player->mo && source->player)
|
|
{
|
|
if (!P_PlayerInPain(player) && (player->defenseLockout || player->instaWhipCharge))
|
|
{
|
|
K_SpawnAmps(source->player, 20, target);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We successfully damaged them! Give 'em some bumpers!
|
|
|
|
if (source && source != player->mo && source->player)
|
|
{
|
|
// Stone Shoe handles amps on its own, but this is also a good place to set soften tumble for it
|
|
if (inflictor->type == MT_STONESHOE || inflictor->type == MT_STONESHOE_CHAIN)
|
|
softenTumble = true;
|
|
else
|
|
K_SpawnAmps(source->player, K_PvPAmpReward((type == DMG_WHUMBLE) ? 30 : 20, source->player, player), target);
|
|
|
|
|
|
K_BotHitPenalty(player);
|
|
|
|
if (G_SameTeam(source->player, player))
|
|
{
|
|
if (type != DMG_EXPLODE)
|
|
{
|
|
type = DMG_STUMBLE;
|
|
downgraded = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (UINT8 i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator || !players[i].mo || P_MobjWasRemoved(players[i].mo))
|
|
continue;
|
|
if (!G_SameTeam(source->player, &players[i]))
|
|
continue;
|
|
if (source->player == &players[i])
|
|
continue;
|
|
K_SpawnAmps(&players[i], FixedInt(FixedMul(5, K_TeamComebackMultiplier(player))), target);
|
|
}
|
|
}
|
|
|
|
|
|
// Extend the invincibility if the hit was a direct hit.
|
|
if (inflictor == source && source->player->invincibilitytimer &&
|
|
!K_PowerUpRemaining(source->player, POWERUP_SMONITOR))
|
|
{
|
|
tic_t kinvextend;
|
|
|
|
softenTumble = true;
|
|
|
|
if (gametyperules & GTR_CLOSERPLAYERS)
|
|
kinvextend = 2*TICRATE;
|
|
else
|
|
kinvextend = 3*TICRATE;
|
|
|
|
// Reduce the value of subsequent invinc extensions
|
|
kinvextend = kinvextend / (1 + source->player->invincibilityextensions); // 50%, 33%, 25%[...]
|
|
kinvextend = max(kinvextend, TICRATE);
|
|
|
|
source->player->invincibilityextensions++;
|
|
|
|
source->player->invincibilitytimer += kinvextend;
|
|
|
|
if (P_IsDisplayPlayer(source->player))
|
|
S_StartSound(NULL, sfx_gsha7);
|
|
}
|
|
|
|
// if the inflictor is a landmine, its reactiontime will be non-zero if it is still moving
|
|
if (inflictor->type == MT_LANDMINE && inflictor->reactiontime > 0)
|
|
{
|
|
// reduce tumble severity to account for getting beaned point blank sometimes
|
|
softenTumble = true;
|
|
// make it more consistent with set landmines
|
|
inflictor->momx = 0;
|
|
inflictor->momy = 0;
|
|
}
|
|
|
|
K_TryHurtSoundExchange(target, source);
|
|
|
|
if (K_Cooperative() == false)
|
|
{
|
|
K_BattleAwardHit(source->player, player, inflictor, damage);
|
|
}
|
|
|
|
if (K_Bumpers(source->player) < K_StartingBumperCount() || (damagetype & DMG_STEAL))
|
|
{
|
|
K_TakeBumpersFromPlayer(source->player, player, damage);
|
|
}
|
|
|
|
if (damagetype & DMG_STEAL)
|
|
{
|
|
// Give them ALL of your emeralds instantly :)
|
|
source->player->emeralds |= player->emeralds;
|
|
player->emeralds = 0;
|
|
K_CheckEmeralds(source->player);
|
|
}
|
|
}
|
|
|
|
if (!(damagetype & DMG_STEAL))
|
|
{
|
|
// Drop all of your emeralds
|
|
K_DropEmeraldsFromPlayer(player, player->emeralds);
|
|
}
|
|
}
|
|
|
|
if (source && source != player->mo && source->player)
|
|
{
|
|
if (damagetype != DMG_DEATHPIT)
|
|
{
|
|
player->pitblame = source->player - players;
|
|
}
|
|
}
|
|
|
|
player->sneakertimer = player->numsneakers = 0;
|
|
player->panelsneakertimer = player->numpanelsneakers = 0;
|
|
player->weaksneakertimer = player->numweaksneakers = 0;
|
|
player->driftboost = player->strongdriftboost = 0;
|
|
player->gateBoost = 0;
|
|
player->fastfall = 0;
|
|
player->ringboost = 0;
|
|
player->glanceDir = 0;
|
|
player->preventfailsafe = TICRATE*3;
|
|
player->pflags &= ~PF_GAINAX;
|
|
Obj_EndBungee(player);
|
|
K_BumperInflate(target->player);
|
|
|
|
UINT32 hurtskinflags = (demo.playback)
|
|
? demo.skinlist[demo.currentskinid[(player-players)]].flags
|
|
: skins[player->skin]->flags;
|
|
if (hurtskinflags & SF_IRONMAN)
|
|
{
|
|
if (gametyperules & GTR_BUMPERS)
|
|
SetRandomFakePlayerSkin(player, false, true);
|
|
}
|
|
|
|
// Explosions are explicit combo setups.
|
|
if (damagetype & DMG_EXPLODE)
|
|
player->bumperinflate = 0;
|
|
|
|
if (player->spectator == false && !(player->charflags & SF_IRONMAN))
|
|
{
|
|
UINT32 skinflags = (demo.playback)
|
|
? demo.skinlist[demo.currentskinid[(player-players)]].flags
|
|
: skins[player->skin]->flags;
|
|
|
|
if (skinflags & SF_IRONMAN)
|
|
{
|
|
player->mo->skin = skins[player->skin];
|
|
player->charflags = skinflags;
|
|
K_SpawnMagicianParticles(player->mo, 5);
|
|
}
|
|
}
|
|
|
|
if (player->rings <= -20)
|
|
{
|
|
player->markedfordeath = true;
|
|
damagetype = DMG_TUMBLE;
|
|
type = DMG_TUMBLE;
|
|
P_StartQuakeFromMobj(5, 44 * player->mo->scale, 2560 * player->mo->scale, player->mo);
|
|
//P_KillPlayer(player, inflictor, source, damagetype);
|
|
}
|
|
|
|
// Death save! On your last hit, no matter what, demote to weakest damage type for one last escape chance.
|
|
if (player->mo->health == 2 && damage && gametyperules & GTR_BUMPERS)
|
|
{
|
|
K_AddMessageForPlayer(player, "\x8DLast Chance!", false, false);
|
|
S_StartSound(target, sfx_gshc7);
|
|
player->flashing = TICRATE;
|
|
type = DMG_STUMBLE;
|
|
}
|
|
|
|
if (inflictor && !P_MobjWasRemoved(inflictor) && P_IsKartItem(inflictor->type) && inflictor->cvmem
|
|
&& inflictor->type != MT_BANANA) // Are there other designed trap items that can be deployed and dropped? If you add one, list it here!
|
|
{
|
|
type = DMG_STUMBLE;
|
|
downgraded = true;
|
|
player->ringburst += 5; // IT'S THE DAMAGE STUMBLE HACK AGAIN AAAAAAAAHHHHHHHHHHH
|
|
K_PopPlayerShield(player);
|
|
}
|
|
|
|
if (!(gametyperules & GTR_SPHERES) && player->tripwireLeniency && !P_PlayerInPain(player))
|
|
{
|
|
switch (type)
|
|
{
|
|
case DMG_EXPLODE:
|
|
type = DMG_TUMBLE;
|
|
downgraded = true;
|
|
softenTumble = true;
|
|
break;
|
|
case DMG_TUMBLE:
|
|
softenTumble = true;
|
|
break;
|
|
case DMG_NORMAL:
|
|
case DMG_WIPEOUT:
|
|
downgraded = true;
|
|
type = DMG_STUMBLE;
|
|
player->ringburst += 5; // THERE IS SIMPLY NO HOPE AT THIS POINT
|
|
K_PopPlayerShield(player);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (type)
|
|
{
|
|
case DMG_STING:
|
|
K_DebtStingPlayer(player, source);
|
|
K_KartPainEnergyFling(player);
|
|
ringburst = 0;
|
|
break;
|
|
case DMG_STUMBLE:
|
|
case DMG_WHUMBLE:
|
|
K_StumblePlayer(player);
|
|
ringburst = 5;
|
|
break;
|
|
case DMG_TUMBLE:
|
|
K_TumblePlayer(player, inflictor, source, softenTumble);
|
|
ringburst = 10;
|
|
break;
|
|
case DMG_EXPLODE:
|
|
case DMG_KARMA:
|
|
ringburst = K_ExplodePlayer(player, inflictor, source);
|
|
break;
|
|
case DMG_WIPEOUT:
|
|
K_SpinPlayer(player, inflictor, source, KSPIN_WIPEOUT);
|
|
K_KartPainEnergyFling(player);
|
|
break;
|
|
case DMG_VOLTAGE:
|
|
case DMG_NORMAL:
|
|
default:
|
|
K_SpinPlayer(player, inflictor, source, KSPIN_SPINOUT);
|
|
break;
|
|
}
|
|
|
|
// Have a shield? You get hit, but don't lose your rings!
|
|
if (player->curshield != KSHIELD_NONE)
|
|
{
|
|
ringburst = 0;
|
|
}
|
|
|
|
player->ringburst += ringburst;
|
|
|
|
K_PopPlayerShield(player);
|
|
|
|
if ((type != DMG_STUMBLE && type != DMG_WHUMBLE) || (type == DMG_STUMBLE && downgraded))
|
|
{
|
|
if (type != DMG_STING)
|
|
player->flashing = K_GetKartFlashing(player);
|
|
player->instashield = 15;
|
|
}
|
|
|
|
K_PlayPainSound(target, source);
|
|
|
|
if (gametyperules & GTR_BUMPERS)
|
|
player->spheres = min(player->spheres + 10, 40);
|
|
|
|
if ((hardhit == true && !softenTumble) || cv_kartdebughuddrop.value)
|
|
{
|
|
K_DropItems(player);
|
|
}
|
|
else
|
|
{
|
|
K_DropHnextList(player);
|
|
}
|
|
|
|
if (inflictor && !P_MobjWasRemoved(inflictor) && inflictor->type == MT_BANANA)
|
|
{
|
|
player->flipDI = true;
|
|
}
|
|
|
|
// Apply stun!
|
|
if (type != DMG_STING)
|
|
{
|
|
K_ApplyStun(player, inflictor, source, damage, damagetype);
|
|
}
|
|
|
|
K_DefensiveOverdrive(target->player);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (target->type == MT_SPECIAL_UFO)
|
|
{
|
|
return Obj_SpecialUFODamage(target, inflictor, source, damagetype);
|
|
}
|
|
else if (target->type == MT_BLENDEYE_MAIN)
|
|
{
|
|
VS_BlendEye_Damage(target, inflictor, source, damage);
|
|
}
|
|
|
|
if (damagetype & DMG_STEAL)
|
|
{
|
|
// Not a player, steal damage is intended to not do anything
|
|
return false;
|
|
}
|
|
|
|
if ((target->flags & MF_BOSS) == MF_BOSS)
|
|
{
|
|
targetdamaging_t targetdamaging = UFOD_GENERIC;
|
|
if (P_MobjWasRemoved(inflictor) == true)
|
|
;
|
|
else switch (inflictor->type)
|
|
{
|
|
case MT_GACHABOM:
|
|
targetdamaging = UFOD_GACHABOM;
|
|
break;
|
|
case MT_ORBINAUT:
|
|
case MT_ORBINAUT_SHIELD:
|
|
targetdamaging = UFOD_ORBINAUT;
|
|
break;
|
|
case MT_BANANA:
|
|
targetdamaging = UFOD_BANANA;
|
|
break;
|
|
case MT_INSTAWHIP:
|
|
inflictor->extravalue2 = 1; // Disable whip collision
|
|
targetdamaging = UFOD_WHIP;
|
|
break;
|
|
case MT_PLAYER:
|
|
targetdamaging = UFOD_BOOST;
|
|
break;
|
|
case MT_JAWZ:
|
|
case MT_JAWZ_SHIELD:
|
|
targetdamaging = UFOD_JAWZ;
|
|
break;
|
|
case MT_SPB:
|
|
targetdamaging = UFOD_SPB;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
P_TrackRoundConditionTargetDamage(targetdamaging);
|
|
}
|
|
}
|
|
|
|
// do the damage
|
|
if (damagetype & DMG_DEATHMASK)
|
|
target->health = 0;
|
|
else
|
|
target->health -= damage;
|
|
|
|
if (source && source->player && target)
|
|
G_GhostAddHit((INT32) (source->player - players), target);
|
|
|
|
// Insta-Whip (DMG_WHUMBLE): do not reduce hitlag because
|
|
// this can leave room for double-damage.
|
|
if ((damagetype & DMG_TYPEMASK) != DMG_WHUMBLE && (gametyperules & GTR_BUMPERS) && !battleprisons)
|
|
laglength /= 2;
|
|
|
|
if (!(target->player && (damagetype & DMG_DEATHMASK)))
|
|
K_SetHitLagForObjects(target, inflictor, source, laglength, true);
|
|
|
|
target->flags2 |= MF2_ALREADYHIT;
|
|
|
|
if (target->health <= 0)
|
|
{
|
|
P_KillMobj(target, inflictor, source, damagetype);
|
|
return true;
|
|
}
|
|
|
|
//K_SetHitLagForObjects(target, inflictor, source, laglength, true);
|
|
|
|
if (!player)
|
|
{
|
|
P_SetMobjState(target, target->info->painstate);
|
|
|
|
if (!P_MobjWasRemoved(target))
|
|
{
|
|
// if not intent on another player,
|
|
// chase after this one
|
|
P_SetTarget(&target->target, source);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/** Damages an object, which may or may not be a player.
|
|
* For melee attacks, source and inflictor are the same.
|
|
*
|
|
* \param target The object being damaged.
|
|
* \param inflictor The thing that caused the damage: creature, missile,
|
|
* gargoyle, and so forth. Can be NULL in the case of
|
|
* environmental damage, such as slime or crushing.
|
|
* \param source The creature or person responsible. For example, if a
|
|
* player is hit by a ring, the player who shot it. In some
|
|
* cases, the target will go after this object after
|
|
* receiving damage. This can be NULL.
|
|
* \param damage Amount of damage to be dealt.
|
|
* \param damagetype Type of damage to be dealt. If bit 7 (0x80) is set, this is an instant-kill.
|
|
* \return True if the target sustained damage, otherwise false.
|
|
* \todo Clean up this mess, split into multiple functions.
|
|
* \sa P_KillMobj
|
|
*/
|
|
boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
|
|
{
|
|
if (G_CompatLevel(0x0010))
|
|
return P_DamageMobjCompat(target, inflictor, source, damage, damagetype);
|
|
|
|
player_t *player;
|
|
player_t *playerInflictor;
|
|
boolean force = false;
|
|
boolean spbpop = false;
|
|
ATTRUNUSED boolean downgraded = false;
|
|
boolean truewhumble = false; // Invincibility-ignoring DMG_WHUMBLE from the Insta-Whip itself.
|
|
|
|
INT32 laglength = 6;
|
|
|
|
if (objectplacing)
|
|
return false;
|
|
|
|
if (target->health <= 0)
|
|
return false;
|
|
|
|
// Spectator handling
|
|
if (damagetype != DMG_SPECTATOR && target->player && target->player->spectator)
|
|
return false;
|
|
|
|
// source is checked without a removal guard in so many places that it's genuinely less work to do it here.
|
|
if (source && P_MobjWasRemoved(source))
|
|
source = NULL;
|
|
|
|
if (source && source->player && source->player->spectator)
|
|
return false;
|
|
|
|
if (((damagetype & DMG_TYPEMASK) == DMG_STING)
|
|
|| ((inflictor && !P_MobjWasRemoved(inflictor)) && inflictor->type == MT_BANANA && inflictor->health <= 1))
|
|
{
|
|
laglength = 2;
|
|
}
|
|
else if (target->type == MT_DROPTARGET || target->type == MT_DROPTARGET_SHIELD)
|
|
{
|
|
laglength = 0; // handled elsewhere
|
|
}
|
|
|
|
switch (target->type)
|
|
{
|
|
case MT_MONITOR:
|
|
damage = Obj_MonitorGetDamage(target, inflictor, damagetype);
|
|
Obj_MonitorOnDamage(target, inflictor, damage);
|
|
break;
|
|
case MT_CDUFO:
|
|
// Make it possible to pick them up during race
|
|
if (inflictor->type == MT_ORBINAUT_SHIELD || inflictor->type == MT_JAWZ_SHIELD)
|
|
return false;
|
|
break;
|
|
|
|
case MT_SPB:
|
|
spbpop = (damagetype & DMG_TYPEMASK) == DMG_VOLTAGE;
|
|
if (spbpop && source && source->player
|
|
&& source->player->roundconditions.spb_neuter == false)
|
|
{
|
|
source->player->roundconditions.spb_neuter = true;
|
|
source->player->roundconditions.checkthisframe = true;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Everything above here can't be forced.
|
|
{
|
|
UINT8 shouldForce = LUA_HookShouldDamage(target, inflictor, source, damage, damagetype);
|
|
if (P_MobjWasRemoved(target))
|
|
return (shouldForce == 1); // mobj was removed
|
|
if (shouldForce == 1)
|
|
force = true;
|
|
else if (shouldForce == 2)
|
|
return false;
|
|
}
|
|
|
|
switch (target->type)
|
|
{
|
|
case MT_BALLSWITCH_BALL:
|
|
Obj_BallSwitchDamaged(target, inflictor, source);
|
|
return false;
|
|
|
|
case MT_SA2_CRATE:
|
|
case MT_ICECAPBLOCK:
|
|
return Obj_TryCrateDamage(target, inflictor);
|
|
|
|
case MT_KART_LEFTOVER:
|
|
// intangible (do not let instawhip shred damage)
|
|
if (Obj_DestroyKart(target))
|
|
return false;
|
|
|
|
P_SetObjectMomZ(target, 12*FRACUNIT, false);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (!force)
|
|
{
|
|
if (!spbpop)
|
|
{
|
|
if (!(target->flags & MF_SHOOTABLE))
|
|
return false; // shouldn't happen...
|
|
}
|
|
}
|
|
|
|
if (target->flags2 & MF2_SKULLFLY)
|
|
target->momx = target->momy = target->momz = 0;
|
|
|
|
if (target->flags & (MF_ENEMY|MF_BOSS))
|
|
{
|
|
if (!force && target->flags2 & MF2_FRET) // Currently flashing from being hit
|
|
return false;
|
|
|
|
if (LUA_HookMobjDamage(target, inflictor, source, damage, damagetype) || P_MobjWasRemoved(target))
|
|
return true;
|
|
|
|
if (target->health > 1)
|
|
target->flags2 |= MF2_FRET;
|
|
}
|
|
|
|
player = target->player;
|
|
playerInflictor = inflictor ? inflictor->player : NULL;
|
|
|
|
if (playerInflictor)
|
|
{
|
|
AddTimesHit(playerInflictor);
|
|
}
|
|
|
|
if (player) // Player is the target
|
|
{
|
|
AddTimesHit(player);
|
|
|
|
if (player->pflags & PF_GODMODE)
|
|
return false;
|
|
|
|
if (!force)
|
|
{
|
|
// Player hits another player
|
|
if (source && source->player)
|
|
{
|
|
if (!P_PlayerHitsPlayer(target, inflictor, source, damage, damagetype))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (source && source->player)
|
|
{
|
|
if (source->player->roundconditions.hit_midair == false
|
|
&& source != target
|
|
&& inflictor
|
|
&& K_IsMissileOrKartItem(inflictor)
|
|
&& target->player->airtime > TICRATE/2
|
|
&& source->player->airtime > TICRATE/2)
|
|
{
|
|
source->player->roundconditions.hit_midair = true;
|
|
source->player->roundconditions.checkthisframe = true;
|
|
}
|
|
|
|
if (source->player->roundconditions.hit_drafter_lookback == false
|
|
&& source != target
|
|
&& target->player->lastdraft == (source->player - players)
|
|
&& (K_GetKartButtons(source->player) & BT_LOOKBACK) == BT_LOOKBACK
|
|
/*&& (AngleDelta(K_MomentumAngle(source), R_PointToAngle2(source->x, source->y, target->x, target->y)) > ANGLE_90)*/)
|
|
{
|
|
source->player->roundconditions.hit_drafter_lookback = true;
|
|
source->player->roundconditions.checkthisframe = true;
|
|
}
|
|
|
|
if (source->player->roundconditions.giant_foe_shrunken_orbi == false
|
|
&& source != target
|
|
&& player->growshrinktimer > 0
|
|
&& !P_MobjWasRemoved(inflictor)
|
|
&& inflictor->type == MT_ORBINAUT
|
|
&& inflictor->scale < FixedMul((FRACUNIT + SHRINK_SCALE), mapobjectscale * 2)) // halfway between base scale and shrink scale, a little bit of leeway
|
|
{
|
|
source->player->roundconditions.giant_foe_shrunken_orbi = true;
|
|
source->player->roundconditions.checkthisframe = true;
|
|
}
|
|
|
|
if (source == target
|
|
&& !P_MobjWasRemoved(inflictor)
|
|
&& inflictor->type == MT_SPBEXPLOSION
|
|
&& inflictor->threshold == KITEM_EGGMAN
|
|
&& !P_MobjWasRemoved(inflictor->tracer)
|
|
&& inflictor->tracer != source
|
|
&& inflictor->tracer->player
|
|
&& inflictor->tracer->player->roundconditions.returntosender_mark == false)
|
|
{
|
|
inflictor->tracer->player->roundconditions.returntosender_mark = true;
|
|
inflictor->tracer->player->roundconditions.checkthisframe = true;
|
|
}
|
|
}
|
|
else if (!(inflictor && inflictor->player)
|
|
&& !(player->exiting || player->laps > numlaps)
|
|
&& damagetype != DMG_DEATHPIT)
|
|
{
|
|
// laps will never increment outside of GTR_CIRCUIT, so this is still fine
|
|
const UINT8 requiredbit = 1<<(player->laps & 7);
|
|
|
|
if (!(player->roundconditions.hittrackhazard[player->laps/8] & requiredbit))
|
|
{
|
|
player->roundconditions.hittrackhazard[player->laps/8] |= requiredbit;
|
|
player->roundconditions.checkthisframe = true;
|
|
}
|
|
}
|
|
|
|
// Instant-Death
|
|
if ((damagetype & DMG_DEATHMASK))
|
|
{
|
|
if (!P_KillPlayer(player, inflictor, source, damagetype))
|
|
return false;
|
|
}
|
|
else if (LUA_HookMobjDamage(target, inflictor, source, damage, damagetype))
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
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;
|
|
|
|
if (inflictor && !P_MobjWasRemoved(inflictor) && inflictor->type == MT_INSTAWHIP && type == DMG_WHUMBLE)
|
|
truewhumble = true;
|
|
|
|
// Check if the player is allowed to be damaged!
|
|
// If not, then spawn the instashield effect instead.
|
|
if (!force)
|
|
{
|
|
boolean invincible = true;
|
|
boolean clash = true; // This effect is cool and reads well, why not
|
|
sfxenum_t sfx = sfx_None;
|
|
|
|
if (!(gametyperules & GTR_BUMPERS))
|
|
{
|
|
if (damagetype & DMG_STEAL)
|
|
{
|
|
// Gametype does not have bumpers, steal damage is intended to not do anything
|
|
// (No instashield is intentional)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (player->invincibilitytimer > 0)
|
|
{
|
|
sfx = sfx_invind;
|
|
}
|
|
else if (K_IsBigger(target, inflictor) == true &&
|
|
// SPB bypasses grow (K_IsBigger handles NULL check)
|
|
(type != DMG_EXPLODE || inflictor->type != MT_SPBEXPLOSION || !inflictor->movefactor))
|
|
{
|
|
sfx = sfx_grownd;
|
|
}
|
|
else if (K_PlayerGuard(player))
|
|
{
|
|
sfx = sfx_s3k3a;
|
|
}
|
|
else if (player->overshield &&
|
|
(type != DMG_EXPLODE || inflictor->type != MT_SPBEXPLOSION || !inflictor->movefactor))
|
|
{
|
|
;
|
|
}
|
|
else if (player->lightningcharge &&
|
|
(type != DMG_EXPLODE || inflictor->type != MT_SPBEXPLOSION || !inflictor->movefactor))
|
|
{
|
|
;
|
|
sfx = sfx_s3k45;
|
|
}
|
|
else if (player->hyudorotimer > 0)
|
|
{
|
|
clash = false;
|
|
}
|
|
else
|
|
{
|
|
invincible = false;
|
|
}
|
|
|
|
// Hack for instawhip-guard counter, lets invincible players lose to guard
|
|
if (inflictor == target)
|
|
{
|
|
invincible = false;
|
|
}
|
|
|
|
if (player->pflags2 & PF2_ALWAYSDAMAGED)
|
|
{
|
|
invincible = false;
|
|
clash = false;
|
|
}
|
|
|
|
// TODO: doing this from P_DamageMobj limits punting to objects that damage the player.
|
|
// And it may be kind of yucky.
|
|
// But this is easier than accounting for every condition in PIT_CheckThing!
|
|
if (inflictor && K_PuntCollide(inflictor, target))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (invincible && !truewhumble)
|
|
{
|
|
const INT32 oldHitlag = target->hitlag;
|
|
const INT32 oldHitlagInflictor = inflictor ? inflictor->hitlag : 0;
|
|
|
|
// Damage during hitlag should be a no-op
|
|
// for invincibility states because there
|
|
// are no flashing tics. If the damage is
|
|
// from a constant source, a deadlock
|
|
// would occur.
|
|
|
|
if (target->eflags & MFE_PAUSED)
|
|
{
|
|
player->timeshit--; // doesn't count
|
|
|
|
if (playerInflictor)
|
|
{
|
|
playerInflictor->timeshit--;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!clash) // Currently a no-op, damage floor hitlag kinda sucked ass
|
|
{
|
|
laglength = max(laglength / 2, 1);
|
|
K_SetHitLagForObjects(target, inflictor, source, laglength, false);
|
|
|
|
AddNullHitlag(player, oldHitlag);
|
|
AddNullHitlag(playerInflictor, oldHitlagInflictor);
|
|
}
|
|
|
|
if (player->timeshit > player->timeshitprev)
|
|
{
|
|
S_StartSound(target, sfx);
|
|
}
|
|
|
|
if (clash)
|
|
{
|
|
player->spheres = max(player->spheres - 5, 0);
|
|
|
|
if (inflictor)
|
|
{
|
|
K_DoPowerClash(target, inflictor);
|
|
|
|
if (player->lightningcharge)
|
|
{
|
|
K_SpawnDriftElectricSparks(player, SKINCOLOR_PURPLE, true);
|
|
}
|
|
|
|
if (inflictor->type == MT_SUPER_FLICKY)
|
|
{
|
|
Obj_BlockSuperFlicky(inflictor);
|
|
}
|
|
|
|
S_StartSound(target, sfx);
|
|
}
|
|
else if (source)
|
|
{
|
|
K_DoPowerClash(target, source);
|
|
S_StartSound(target, sfx);
|
|
}
|
|
|
|
}
|
|
|
|
// Full invulnerability
|
|
K_DoInstashield(player);
|
|
return false;
|
|
}
|
|
{
|
|
// Check if we should allow wombo combos (hard hits by default, inverted by the presence of DMG_WOMBO).
|
|
boolean allowcombo = ((hardhit || (type == DMG_STUMBLE || type == DMG_WHUMBLE)) == !(damagetype & DMG_WOMBO));
|
|
|
|
// Tumble/stumble is a special case.
|
|
if (type == DMG_TUMBLE)
|
|
{
|
|
// don't allow constant combo
|
|
if (player->tumbleBounces == 1 && (P_MobjFlip(target)*target->momz > 0))
|
|
allowcombo = false;
|
|
}
|
|
else if (type == DMG_STUMBLE || type == DMG_WHUMBLE)
|
|
{
|
|
// don't allow constant combo
|
|
if (player->tumbleBounces == TUMBLEBOUNCES-1 && (P_MobjFlip(target)*target->momz > 0))
|
|
{
|
|
if (type == DMG_STUMBLE)
|
|
return false; // No-sell strings of stumble
|
|
|
|
allowcombo = false;
|
|
}
|
|
}
|
|
|
|
if (inflictor && !P_MobjWasRemoved(inflictor) && inflictor->momx == 0 && inflictor->momy == 0 && inflictor->momz == 0 && inflictor->type != MT_SPBEXPLOSION)
|
|
{
|
|
// Probably a map hazard.
|
|
allowcombo = false;
|
|
}
|
|
|
|
if (allowcombo == false && (target->eflags & MFE_PAUSED))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// DMG_EXPLODE excluded from flashtic checks to prevent dodging eggbox/SPB with weak spinout
|
|
if ((target->hitlag == 0 || allowcombo == false) &&
|
|
player->flashing > 0 &&
|
|
type != DMG_EXPLODE &&
|
|
type != DMG_STUMBLE &&
|
|
type != DMG_WHUMBLE &&
|
|
P_FlashingException(player, inflictor) == false)
|
|
{
|
|
// Post-hit invincibility
|
|
K_DoInstashield(player);
|
|
return false;
|
|
}
|
|
else if (target->flags2 & MF2_ALREADYHIT) // do not deal extra damage in the same tic
|
|
{
|
|
K_SetHitLagForObjects(target, inflictor, source, laglength, true);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (gametyperules & GTR_BUMPERS)
|
|
{
|
|
if (damagetype & DMG_STEAL)
|
|
{
|
|
// Steals 2 bumpers
|
|
damage = 2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Do not die from damage outside of bumpers health system
|
|
damage = 0;
|
|
}
|
|
|
|
boolean softenTumble = false;
|
|
|
|
// Sting and stumble shouldn't be rewarding Battle hits.
|
|
if (type == DMG_STING || type == DMG_STUMBLE)
|
|
{
|
|
damage = 0;
|
|
|
|
if (source && source != player->mo && source->player)
|
|
{
|
|
if (!P_PlayerInPain(player) && (player->defenseLockout || player->instaWhipCharge))
|
|
{
|
|
K_SpawnAmps(source->player, 20, target);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We successfully damaged them! Give 'em some bumpers!
|
|
|
|
if (source && source != player->mo && source->player)
|
|
{
|
|
// Stone Shoe handles amps on its own, but this is also a good place to set soften tumble for it
|
|
if (inflictor->type == MT_STONESHOE || inflictor->type == MT_STONESHOE_CHAIN)
|
|
softenTumble = true;
|
|
else
|
|
K_SpawnAmps(source->player, K_PvPAmpReward((truewhumble) ? 30 : 20, source->player, player), target);
|
|
|
|
|
|
K_BotHitPenalty(player);
|
|
|
|
if (G_SameTeam(source->player, player))
|
|
{
|
|
if (type != DMG_EXPLODE)
|
|
{
|
|
type = DMG_STUMBLE;
|
|
downgraded = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (UINT8 i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator || !players[i].mo || P_MobjWasRemoved(players[i].mo))
|
|
continue;
|
|
if (!G_SameTeam(source->player, &players[i]))
|
|
continue;
|
|
if (source->player == &players[i])
|
|
continue;
|
|
K_SpawnAmps(&players[i], FixedInt(FixedMul(5, K_TeamComebackMultiplier(player))), target);
|
|
}
|
|
}
|
|
|
|
|
|
// Extend the invincibility if the hit was a direct hit.
|
|
if (inflictor == source && source->player->invincibilitytimer &&
|
|
!K_PowerUpRemaining(source->player, POWERUP_SMONITOR))
|
|
{
|
|
tic_t kinvextend;
|
|
|
|
softenTumble = true;
|
|
|
|
if (gametyperules & GTR_CLOSERPLAYERS)
|
|
kinvextend = 2*TICRATE;
|
|
else
|
|
kinvextend = 3*TICRATE;
|
|
|
|
// Reduce the value of subsequent invinc extensions
|
|
kinvextend = kinvextend / (1 + source->player->invincibilityextensions); // 50%, 33%, 25%[...]
|
|
kinvextend = max(kinvextend, TICRATE);
|
|
|
|
source->player->invincibilityextensions++;
|
|
|
|
source->player->invincibilitytimer += kinvextend;
|
|
|
|
if (P_IsDisplayPlayer(source->player))
|
|
S_StartSound(NULL, sfx_gsha7);
|
|
}
|
|
|
|
// if the inflictor is a landmine, its reactiontime will be non-zero if it is still moving
|
|
if (inflictor->type == MT_LANDMINE && inflictor->reactiontime > 0)
|
|
{
|
|
// reduce tumble severity to account for getting beaned point blank sometimes
|
|
softenTumble = true;
|
|
// make it more consistent with set landmines
|
|
inflictor->momx = 0;
|
|
inflictor->momy = 0;
|
|
}
|
|
|
|
K_TryHurtSoundExchange(target, source);
|
|
|
|
if (K_Cooperative() == false)
|
|
{
|
|
K_BattleAwardHit(source->player, player, inflictor, damage);
|
|
}
|
|
|
|
if (K_Bumpers(source->player) < K_StartingBumperCount() || (damagetype & DMG_STEAL))
|
|
{
|
|
K_TakeBumpersFromPlayer(source->player, player, damage);
|
|
}
|
|
|
|
if (damagetype & DMG_STEAL)
|
|
{
|
|
// Give them ALL of your emeralds instantly :)
|
|
source->player->emeralds |= player->emeralds;
|
|
player->emeralds = 0;
|
|
K_CheckEmeralds(source->player);
|
|
}
|
|
}
|
|
|
|
if (!(damagetype & DMG_STEAL))
|
|
{
|
|
// Drop all of your emeralds
|
|
K_DropEmeraldsFromPlayer(player, player->emeralds);
|
|
}
|
|
}
|
|
|
|
if (source && source != player->mo && source->player)
|
|
{
|
|
if (damagetype != DMG_DEATHPIT)
|
|
{
|
|
player->pitblame = source->player - players;
|
|
}
|
|
}
|
|
|
|
player->sneakertimer = player->numsneakers = 0;
|
|
player->panelsneakertimer = player->numpanelsneakers = 0;
|
|
player->weaksneakertimer = player->numweaksneakers = 0;
|
|
player->driftboost = player->strongdriftboost = 0;
|
|
player->gateBoost = 0;
|
|
player->fastfall = 0;
|
|
player->ringboost = 0;
|
|
player->glanceDir = 0;
|
|
player->preventfailsafe = TICRATE*3;
|
|
player->pflags &= ~PF_GAINAX;
|
|
Obj_EndBungee(player);
|
|
K_BumperInflate(target->player);
|
|
|
|
UINT32 hurtskinflags = (demo.playback)
|
|
? demo.skinlist[demo.currentskinid[(player-players)]].flags
|
|
: skins[player->skin]->flags;
|
|
if (hurtskinflags & SF_IRONMAN)
|
|
{
|
|
if (gametyperules & GTR_BUMPERS)
|
|
SetRandomFakePlayerSkin(player, false, true);
|
|
}
|
|
|
|
// Explosions are explicit combo setups.
|
|
if (damagetype & DMG_EXPLODE)
|
|
player->bumperinflate = 0;
|
|
|
|
if (player->spectator == false && !(player->charflags & SF_IRONMAN))
|
|
{
|
|
UINT32 skinflags = (demo.playback)
|
|
? demo.skinlist[demo.currentskinid[(player-players)]].flags
|
|
: skins[player->skin]->flags;
|
|
|
|
if (skinflags & SF_IRONMAN)
|
|
{
|
|
player->mo->skin = skins[player->skin];
|
|
player->charflags = skinflags;
|
|
K_SpawnMagicianParticles(player->mo, 5);
|
|
}
|
|
}
|
|
|
|
if (player->rings <= -20)
|
|
{
|
|
player->markedfordeath = true;
|
|
damagetype = DMG_TUMBLE;
|
|
type = DMG_TUMBLE;
|
|
P_StartQuakeFromMobj(5, 44 * player->mo->scale, 2560 * player->mo->scale, player->mo);
|
|
//P_KillPlayer(player, inflictor, source, damagetype);
|
|
}
|
|
|
|
// Death save! On your last hit, no matter what, demote to weakest damage type for one last escape chance.
|
|
if (player->mo->health == 2 && damage && gametyperules & GTR_BUMPERS)
|
|
{
|
|
K_AddMessageForPlayer(player, "\x8DLast Chance!", false, false);
|
|
S_StartSound(target, sfx_gshc7);
|
|
player->flashing = TICRATE;
|
|
type = DMG_STUMBLE;
|
|
downgraded = true;
|
|
}
|
|
|
|
// Downgrade backthrown items that are not dedicated traps.
|
|
if (inflictor && !P_MobjWasRemoved(inflictor) && P_IsKartItem(inflictor->type) && inflictor->cvmem
|
|
&& inflictor->type != MT_BANANA)
|
|
{
|
|
type = DMG_WHUMBLE;
|
|
downgraded = true;
|
|
}
|
|
|
|
// Downgrade orbital items.
|
|
if (inflictor && !P_MobjWasRemoved(inflictor) && (inflictor->type == MT_ORBINAUT_SHIELD || inflictor->type == MT_JAWZ_SHIELD))
|
|
{
|
|
type = DMG_WHUMBLE;
|
|
downgraded = true;
|
|
}
|
|
|
|
if (!(gametyperules & GTR_SPHERES) && player->tripwireLeniency && !P_PlayerInPain(player))
|
|
{
|
|
switch (type)
|
|
{
|
|
case DMG_EXPLODE:
|
|
type = DMG_TUMBLE;
|
|
downgraded = true;
|
|
softenTumble = true;
|
|
break;
|
|
case DMG_TUMBLE:
|
|
softenTumble = true;
|
|
break;
|
|
case DMG_NORMAL:
|
|
case DMG_WIPEOUT:
|
|
downgraded = true;
|
|
type = DMG_WHUMBLE;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (type)
|
|
{
|
|
case DMG_STING:
|
|
K_DebtStingPlayer(player, source);
|
|
K_KartPainEnergyFling(player);
|
|
ringburst = 0;
|
|
break;
|
|
case DMG_STUMBLE:
|
|
case DMG_WHUMBLE:
|
|
K_StumblePlayer(player);
|
|
ringburst = (type == DMG_WHUMBLE) ? 5 : 0;
|
|
break;
|
|
case DMG_TUMBLE:
|
|
K_TumblePlayer(player, inflictor, source, softenTumble);
|
|
ringburst = 10;
|
|
break;
|
|
case DMG_EXPLODE:
|
|
case DMG_KARMA:
|
|
ringburst = K_ExplodePlayer(player, inflictor, source);
|
|
break;
|
|
case DMG_WIPEOUT:
|
|
K_SpinPlayer(player, inflictor, source, KSPIN_WIPEOUT);
|
|
K_KartPainEnergyFling(player);
|
|
break;
|
|
case DMG_VOLTAGE:
|
|
case DMG_NORMAL:
|
|
default:
|
|
K_SpinPlayer(player, inflictor, source, KSPIN_SPINOUT);
|
|
break;
|
|
}
|
|
|
|
// Have a shield? You get hit, but don't lose your rings!
|
|
if (player->curshield != KSHIELD_NONE)
|
|
{
|
|
ringburst = 0;
|
|
}
|
|
|
|
player->ringburst += ringburst;
|
|
|
|
if (type != DMG_STUMBLE)
|
|
{
|
|
if (type != DMG_STING)
|
|
player->flashing = K_GetKartFlashing(player);
|
|
|
|
K_PopPlayerShield(player);
|
|
player->instashield = 15;
|
|
K_PlayPainSound(target, source);
|
|
player->ringboost = 0;
|
|
}
|
|
|
|
if (gametyperules & GTR_BUMPERS)
|
|
player->spheres = min(player->spheres + 10, 40);
|
|
|
|
if ((hardhit == true && !softenTumble) || cv_kartdebughuddrop.value)
|
|
{
|
|
K_DropItems(player);
|
|
}
|
|
else
|
|
{
|
|
K_DropHnextList(player);
|
|
}
|
|
|
|
if (inflictor && !P_MobjWasRemoved(inflictor) && inflictor->type == MT_BANANA)
|
|
{
|
|
player->flipDI = true;
|
|
}
|
|
|
|
// Apply stun!
|
|
if (type != DMG_STING)
|
|
{
|
|
K_ApplyStun(player, inflictor, source, damage, damagetype);
|
|
}
|
|
|
|
K_DefensiveOverdrive(target->player);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (target->type == MT_SPECIAL_UFO)
|
|
{
|
|
return Obj_SpecialUFODamage(target, inflictor, source, damagetype);
|
|
}
|
|
else if (target->type == MT_BLENDEYE_MAIN)
|
|
{
|
|
VS_BlendEye_Damage(target, inflictor, source, damage);
|
|
}
|
|
|
|
if (damagetype & DMG_STEAL)
|
|
{
|
|
// Not a player, steal damage is intended to not do anything
|
|
return false;
|
|
}
|
|
|
|
if ((target->flags & MF_BOSS) == MF_BOSS)
|
|
{
|
|
targetdamaging_t targetdamaging = UFOD_GENERIC;
|
|
if (P_MobjWasRemoved(inflictor) == true)
|
|
;
|
|
else switch (inflictor->type)
|
|
{
|
|
case MT_GACHABOM:
|
|
targetdamaging = UFOD_GACHABOM;
|
|
break;
|
|
case MT_ORBINAUT:
|
|
case MT_ORBINAUT_SHIELD:
|
|
targetdamaging = UFOD_ORBINAUT;
|
|
break;
|
|
case MT_BANANA:
|
|
targetdamaging = UFOD_BANANA;
|
|
break;
|
|
case MT_INSTAWHIP:
|
|
inflictor->extravalue2 = 1; // Disable whip collision
|
|
targetdamaging = UFOD_WHIP;
|
|
break;
|
|
case MT_PLAYER:
|
|
targetdamaging = UFOD_BOOST;
|
|
break;
|
|
case MT_JAWZ:
|
|
case MT_JAWZ_SHIELD:
|
|
targetdamaging = UFOD_JAWZ;
|
|
break;
|
|
case MT_SPB:
|
|
targetdamaging = UFOD_SPB;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
P_TrackRoundConditionTargetDamage(targetdamaging);
|
|
}
|
|
}
|
|
|
|
// do the damage
|
|
if (damagetype & DMG_DEATHMASK)
|
|
target->health = 0;
|
|
else
|
|
target->health -= damage;
|
|
|
|
if (source && source->player && target)
|
|
G_GhostAddHit((INT32) (source->player - players), target);
|
|
|
|
// Insta-Whip (DMG_WHUMBLE): do not reduce hitlag because
|
|
// this can leave room for double-damage.
|
|
if (truewhumble && (gametyperules & GTR_BUMPERS) && !battleprisons)
|
|
laglength /= 2;
|
|
|
|
if (target->type == MT_PLAYER && inflictor && !P_MobjWasRemoved(inflictor)
|
|
&& inflictor->type == MT_PLAYER && K_PlayerCanPunt(inflictor->player))
|
|
laglength = max(laglength / 2, 2);
|
|
|
|
if (!(target->player && (damagetype & DMG_DEATHMASK)))
|
|
K_SetHitLagForObjects(target, inflictor, source, laglength, true);
|
|
|
|
target->flags2 |= MF2_ALREADYHIT;
|
|
|
|
if (target->health <= 0)
|
|
{
|
|
P_KillMobj(target, inflictor, source, damagetype);
|
|
return true;
|
|
}
|
|
|
|
//K_SetHitLagForObjects(target, inflictor, source, laglength, true);
|
|
|
|
if (!player)
|
|
{
|
|
P_SetMobjState(target, target->info->painstate);
|
|
|
|
if (!P_MobjWasRemoved(target))
|
|
{
|
|
// if not intent on another player,
|
|
// chase after this one
|
|
P_SetTarget(&target->target, source);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#define RING_LAYER_SIDE_SIZE (3)
|
|
#define RING_LAYER_SIZE (RING_LAYER_SIDE_SIZE * 2)
|
|
|
|
void P_FlingBurst
|
|
( player_t *player,
|
|
angle_t fa,
|
|
mobjtype_t objType,
|
|
tic_t objFuse,
|
|
fixed_t objScale,
|
|
INT32 i,
|
|
fixed_t dampen)
|
|
{
|
|
mobj_t *mo = P_SpawnMobjFromMobj(player->mo, 0, 0, 0, objType);
|
|
P_SetTarget(&mo->target, player->mo);
|
|
|
|
mo->threshold = 10; // not useful for spikes
|
|
mo->fuse = objFuse;
|
|
|
|
// We want everything from P_SpawnMobjFromMobj except scale.
|
|
objScale = FixedMul(objScale, FixedDiv(mapobjectscale, player->mo->scale));
|
|
|
|
if (objScale != FRACUNIT)
|
|
{
|
|
P_SetScale(mo, FixedMul(objScale, mo->scale));
|
|
mo->destscale = mo->scale;
|
|
}
|
|
|
|
if (i & 1)
|
|
{
|
|
fa += ANGLE_180;
|
|
}
|
|
|
|
// Pitch offset changes every other ring
|
|
angle_t offset = ANGLE_90 / (RING_LAYER_SIDE_SIZE + 2);
|
|
angle_t fp = offset + (((i / 2) % RING_LAYER_SIDE_SIZE) * (offset * 3 >> 1));
|
|
|
|
const UINT8 layer = i / RING_LAYER_SIZE;
|
|
fixed_t thrust = (13 * mo->scale) + (7 * mo->scale * layer);
|
|
thrust = FixedDiv(thrust, dampen);
|
|
mo->momx = (player->mo->momx / 2) + FixedMul(FixedMul(thrust, FINECOSINE(fp >> ANGLETOFINESHIFT)), FINECOSINE(fa >> ANGLETOFINESHIFT));
|
|
mo->momy = (player->mo->momy / 2) + FixedMul(FixedMul(thrust, FINECOSINE(fp >> ANGLETOFINESHIFT)), FINESINE(fa >> ANGLETOFINESHIFT));
|
|
mo->momz = (player->mo->momz / 2) + (FixedMul(thrust, FINESINE(fp >> ANGLETOFINESHIFT)) * P_MobjFlip(mo));
|
|
}
|
|
|
|
/** Spills an injured player's rings.
|
|
*
|
|
* \param player The player who is losing rings.
|
|
* \param num_rings Number of rings lost. A maximum of 20 rings will be
|
|
* spawned.
|
|
* \sa P_PlayerFlagBurst
|
|
*/
|
|
void P_PlayerRingBurst(player_t *player, INT32 num_rings)
|
|
{
|
|
INT32 spill_total, num_fling_rings;
|
|
INT32 i;
|
|
angle_t fa;
|
|
|
|
// Rings shouldn't be in Battle!
|
|
if (gametyperules & GTR_SPHERES)
|
|
return;
|
|
|
|
// Better safe than sorry.
|
|
if (!player)
|
|
return;
|
|
|
|
// Have a shield? You get hit, but don't lose your rings!
|
|
if (player->curshield != KSHIELD_NONE)
|
|
return;
|
|
|
|
// 20 is the maximum number of rings that can be taken from you at once - half the span of your counter
|
|
if (num_rings > 20)
|
|
num_rings = 20;
|
|
else if (num_rings <= 0)
|
|
return;
|
|
|
|
spill_total = -P_GivePlayerRings(player, -num_rings);
|
|
num_fling_rings = spill_total + min(0, player->rings);
|
|
|
|
// determine first angle
|
|
fa = player->mo->angle + ((P_RandomByte(PR_ITEM_RINGS) & 1) ? -ANGLE_90 : ANGLE_90);
|
|
|
|
for (i = 0; i < num_fling_rings; i++)
|
|
{
|
|
P_FlingBurst(player, fa, MT_FLINGRING, 60*TICRATE, FRACUNIT, i, FRACUNIT);
|
|
}
|
|
|
|
while (i < spill_total)
|
|
{
|
|
P_FlingBurst(player, fa, MT_DEBTSPIKE, 0, 3 * FRACUNIT / 2, i++, FRACUNIT);
|
|
}
|
|
|
|
K_DefensiveOverdrive(player);
|
|
}
|