RingRacers/src/p_inter.c
2025-10-15 22:10:08 -05:00

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