mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2025-10-30 08:01:28 +00:00
1493 lines
37 KiB
C++
1493 lines
37 KiB
C++
// DR. ROBOTNIK'S RING RACERS
|
|
//-----------------------------------------------------------------------------
|
|
// Copyright (C) 2025 by Kart Krew.
|
|
//
|
|
// This program is free software distributed under the
|
|
// terms of the GNU General Public License, version 2.
|
|
// See the 'LICENSE' file for more details.
|
|
//-----------------------------------------------------------------------------
|
|
/// \file k_collide.cpp
|
|
/// \brief SRB2Kart item collision hooks
|
|
|
|
#include <algorithm>
|
|
|
|
#include "k_collide.h"
|
|
#include "doomtype.h"
|
|
#include "p_mobj.h"
|
|
#include "k_kart.h"
|
|
#include "p_local.h"
|
|
#include "s_sound.h"
|
|
#include "r_main.h" // R_PointToAngle2, R_PointToDist2
|
|
#include "hu_stuff.h" // Sink snipe print
|
|
#include "doomdef.h" // Sink snipe print
|
|
#include "g_game.h" // Sink snipe print
|
|
#include "k_objects.h"
|
|
#include "k_roulette.h"
|
|
#include "k_podium.h"
|
|
#include "k_powerup.h"
|
|
#include "k_hitlag.h"
|
|
#include "m_random.h"
|
|
#include "k_hud.h" // K_AddMessage
|
|
#include "m_easing.h"
|
|
#include "r_skins.h"
|
|
|
|
angle_t K_GetCollideAngle(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
fixed_t momux, momuy;
|
|
angle_t test;
|
|
|
|
if (!(t1->flags & MF_PAPERCOLLISION))
|
|
{
|
|
return R_PointToAngle2(t1->x, t1->y, t2->x, t2->y)+ANGLE_90;
|
|
}
|
|
|
|
test = R_PointToAngle2(0, 0, t2->momx, t2->momy) + ANGLE_90 - t1->angle;
|
|
if (test > ANGLE_180)
|
|
test = t1->angle + ANGLE_180;
|
|
else
|
|
test = t1->angle;
|
|
|
|
// intentional way around - sine...
|
|
momuy = P_AproxDistance(t2->momx, t2->momy);
|
|
momux = t2->momx - P_ReturnThrustY(t2, test, 2*momuy);
|
|
momuy = t2->momy - P_ReturnThrustX(t2, test, 2*momuy);
|
|
|
|
return R_PointToAngle2(0, 0, momux, momuy);
|
|
}
|
|
|
|
extern "C" consvar_t cv_debugpickmeup;
|
|
|
|
boolean K_BananaBallhogCollide(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
boolean damageitem = false;
|
|
|
|
if (((t1->target == t2) || (!(t2->flags & (MF_ENEMY|MF_BOSS)) && (t1->target == t2->target))) && (t1->threshold > 0 || (t2->type != MT_PLAYER && t2->threshold > 0)))
|
|
return true;
|
|
|
|
if (t1->health <= 0 || t2->health <= 0)
|
|
return true;
|
|
|
|
if (((t1->type == MT_BANANA_SHIELD) && (t2->type == MT_BANANA_SHIELD))
|
|
&& (t1->target == t2->target)) // Don't hit each other if you have the same target
|
|
return true;
|
|
|
|
if (t1->type == MT_BALLHOG && t2->type == MT_BALLHOG)
|
|
return true; // Ballhogs don't collide with eachother
|
|
|
|
if (t1->type == MT_BALLHOGBOOM && t2->type == MT_BALLHOGBOOM)
|
|
return true; // Ballhogs don't collide with eachother
|
|
|
|
if (t1->type == MT_BALLHOGBOOM && t2->type == MT_PLAYER && t1->target == t2 && !cv_debugpickmeup.value)
|
|
return true; // Allied hog explosion, not snatchable but shouldn't damage
|
|
|
|
if (K_TryPickMeUp(t1, t2, false))
|
|
return true;
|
|
|
|
if (t2->player)
|
|
{
|
|
if (t2->player->flashing > 0 && t2->hitlag == 0)
|
|
return true;
|
|
|
|
// Banana snipe!
|
|
if (t1->type == MT_BANANA && t1->health > 1)
|
|
S_StartSound(t2, sfx_bsnipe);
|
|
|
|
if (t1->type != MT_BALLHOGBOOM) // ballhog booms linger and expire after their anim is done
|
|
{
|
|
damageitem = true;
|
|
}
|
|
|
|
if (t2->player->flamedash && t2->player->itemtype == KITEM_FLAMESHIELD)
|
|
{
|
|
// Melt item
|
|
S_StartSound(t2, sfx_s3k43);
|
|
}
|
|
else if (K_IsRidingFloatingTop(t2->player))
|
|
{
|
|
// Float over silly banana
|
|
damageitem = false;
|
|
}
|
|
else
|
|
{
|
|
P_DamageMobj(t2, t1, t1->target, 1, DMG_NORMAL|DMG_WOMBO);
|
|
}
|
|
}
|
|
else if (t2->type == MT_BANANA || t2->type == MT_BANANA_SHIELD
|
|
|| t2->type == MT_ORBINAUT || t2->type == MT_ORBINAUT_SHIELD
|
|
|| t2->type == MT_JAWZ || t2->type == MT_JAWZ_SHIELD
|
|
|| t2->type == MT_BALLHOG || t2->type == MT_GACHABOM)
|
|
{
|
|
// Other Item Damage
|
|
angle_t bounceangle = K_GetCollideAngle(t1, t2);
|
|
|
|
S_StartSound(t2, t2->info->deathsound);
|
|
P_KillMobj(t2, t1, t1, DMG_NORMAL);
|
|
|
|
P_SetObjectMomZ(t2, 24*FRACUNIT, false);
|
|
P_InstaThrust(t2, bounceangle, 16*FRACUNIT);
|
|
|
|
P_SpawnMobj(t2->x/2 + t1->x/2, t2->y/2 + t1->y/2, t2->z/2 + t1->z/2, MT_ITEMCLASH);
|
|
|
|
damageitem = true;
|
|
}
|
|
else if (t2->type == MT_SSMINE_SHIELD || t2->type == MT_SSMINE || t2->type == MT_LANDMINE)
|
|
{
|
|
damageitem = true;
|
|
// Bomb death
|
|
P_KillMobj(t2, t1, t1, DMG_NORMAL);
|
|
}
|
|
else if (t2->flags & MF_SHOOTABLE)
|
|
{
|
|
// Shootable damage
|
|
P_DamageMobj(t2, t1, t1->target, 1, DMG_NORMAL);
|
|
damageitem = true;
|
|
}
|
|
|
|
if (damageitem && P_MobjWasRemoved(t1) == false)
|
|
{
|
|
angle_t bounceangle;
|
|
|
|
if (P_MobjWasRemoved(t2) == false)
|
|
{
|
|
bounceangle = K_GetCollideAngle(t2, t1);
|
|
}
|
|
else
|
|
{
|
|
bounceangle = K_MomentumAngle(t1) + ANGLE_90;
|
|
t2 = NULL; // handles the arguments to P_KillMobj
|
|
}
|
|
|
|
// This Item Damage
|
|
S_StartSound(t1, t1->info->deathsound);
|
|
P_KillMobj(t1, t2, t2, DMG_NORMAL);
|
|
|
|
P_SetObjectMomZ(t1, 24*FRACUNIT, false);
|
|
|
|
P_InstaThrust(t1, bounceangle, 16*FRACUNIT);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
boolean K_EggItemCollide(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
// Push fakes out of other item boxes
|
|
if (t2->type == MT_RANDOMITEM || t2->type == MT_EGGMANITEM)
|
|
{
|
|
P_InstaThrust(t1, R_PointToAngle2(t2->x, t2->y, t1->x, t1->y), t2->radius/4);
|
|
return true;
|
|
}
|
|
|
|
if (t2->player)
|
|
{
|
|
if ((t1->target == t2 || t1->target == t2->target) && (t1->threshold > 0))
|
|
return true;
|
|
|
|
if (t1->health <= 0 || t2->health <= 0)
|
|
return true;
|
|
|
|
if (K_TryPickMeUp(t1, t2, false))
|
|
return true;
|
|
|
|
if (!P_CanPickupItem(t2->player, PICKUP_EGGBOX))
|
|
return true;
|
|
|
|
K_DropItems(t2->player);
|
|
K_StartEggmanRoulette(t2->player);
|
|
|
|
if (t2->player->flamedash && t2->player->itemtype == KITEM_FLAMESHIELD)
|
|
{
|
|
// Melt item
|
|
S_StartSound(t2, sfx_s3k43);
|
|
P_KillMobj(t1, t2, t2, DMG_NORMAL);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
Obj_SpawnItemDebrisEffects(t1, t2);
|
|
|
|
#if 0
|
|
// Eggbox snipe!
|
|
if (t1->type == MT_EGGMANITEM && t1->health > 1)
|
|
S_StartSound(t2, sfx_bsnipe);
|
|
#endif
|
|
|
|
if (t1->target && t1->target->player)
|
|
{
|
|
t2->player->eggmanblame = t1->target->player - players;
|
|
|
|
if (t1->target->hnext == t1)
|
|
{
|
|
P_SetTarget(&t1->target->hnext, NULL);
|
|
t1->target->player->itemflags &= ~IF_EGGMANOUT;
|
|
}
|
|
}
|
|
|
|
P_RemoveMobj(t1);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static mobj_t *grenade;
|
|
static fixed_t explodedist;
|
|
static boolean explodespin;
|
|
static INT32 minehitlag;
|
|
|
|
static inline boolean PIT_SSMineChecks(mobj_t *thing)
|
|
{
|
|
if (thing == grenade) // Don't explode yourself! Endless loop!
|
|
return true;
|
|
|
|
if (thing->health <= 0)
|
|
return true;
|
|
|
|
if (!(thing->flags & MF_SHOOTABLE) || (thing->flags & MF_SCENERY))
|
|
return true;
|
|
|
|
if (thing->player && (thing->player->spectator || thing->player->hyudorotimer > 0))
|
|
return true;
|
|
|
|
if (P_AproxDistance(P_AproxDistance(thing->x - grenade->x, thing->y - grenade->y), thing->z - grenade->z) > explodedist)
|
|
return true; // Too far away
|
|
|
|
if (P_CheckSight(grenade, thing) == false)
|
|
return true; // Not in sight
|
|
|
|
return false;
|
|
}
|
|
|
|
extern "C" consvar_t cv_debugpickmeup;
|
|
|
|
static inline BlockItReturn_t PIT_SSMineSearch(mobj_t *thing)
|
|
{
|
|
if (grenade == NULL || P_MobjWasRemoved(grenade))
|
|
return BMIT_ABORT; // There's the possibility these can chain react onto themselves after they've already died if there are enough all in one spot
|
|
|
|
if (grenade->flags2 & MF2_DEBRIS) // don't explode twice
|
|
return BMIT_ABORT;
|
|
|
|
switch (thing->type)
|
|
{
|
|
case MT_PLAYER: // Don't explode for anything but an actual player.
|
|
case MT_SPECIAL_UFO: // Also UFO catcher
|
|
break;
|
|
|
|
default:
|
|
return BMIT_CONTINUE;
|
|
}
|
|
|
|
if (!cv_debugpickmeup.value)
|
|
{
|
|
if (grenade->target && !P_MobjWasRemoved(grenade->target))
|
|
{
|
|
if (thing == grenade->target) // Don't blow up at your owner instantly.
|
|
return BMIT_CONTINUE;
|
|
|
|
if (grenade->target->player && thing->player && G_SameTeam(grenade->target->player, thing->player))
|
|
return BMIT_CONTINUE;
|
|
}
|
|
}
|
|
|
|
if (PIT_SSMineChecks(thing) == true)
|
|
return BMIT_CONTINUE;
|
|
|
|
// Explode!
|
|
P_SetMobjState(grenade, grenade->info->deathstate);
|
|
return BMIT_ABORT;
|
|
}
|
|
|
|
void K_DoMineSearch(mobj_t *actor, fixed_t size)
|
|
{
|
|
INT32 bx, by, xl, xh, yl, yh;
|
|
|
|
explodedist = FixedMul(size, actor->scale);
|
|
grenade = actor;
|
|
|
|
yh = (unsigned)(actor->y + (explodedist + MAXRADIUS) - bmaporgy)>>MAPBLOCKSHIFT;
|
|
yl = (unsigned)(actor->y - (explodedist + MAXRADIUS) - bmaporgy)>>MAPBLOCKSHIFT;
|
|
xh = (unsigned)(actor->x + (explodedist + MAXRADIUS) - bmaporgx)>>MAPBLOCKSHIFT;
|
|
xl = (unsigned)(actor->x - (explodedist + MAXRADIUS) - bmaporgx)>>MAPBLOCKSHIFT;
|
|
|
|
BMBOUNDFIX (xl, xh, yl, yh);
|
|
|
|
for (by = yl; by <= yh; by++)
|
|
for (bx = xl; bx <= xh; bx++)
|
|
P_BlockThingsIterator(bx, by, PIT_SSMineSearch);
|
|
}
|
|
|
|
static inline BlockItReturn_t PIT_SSMineExplode(mobj_t *thing)
|
|
{
|
|
const INT32 oldhitlag = thing->hitlag;
|
|
INT32 lagadded;
|
|
|
|
if (grenade == NULL || P_MobjWasRemoved(grenade))
|
|
return BMIT_ABORT; // There's the possibility these can chain react onto themselves after they've already died if there are enough all in one spot
|
|
|
|
#if 0
|
|
if (grenade->flags2 & MF2_DEBRIS) // don't explode twice
|
|
return BMIT_ABORT;
|
|
#endif
|
|
|
|
if (PIT_SSMineChecks(thing) == true)
|
|
return BMIT_CONTINUE;
|
|
|
|
// Don't do Big Boy Damage to the UFO Catcher with
|
|
// lingering spinout damage
|
|
if (thing->type == MT_SPECIAL_UFO && explodespin)
|
|
{
|
|
return BMIT_CONTINUE;
|
|
}
|
|
|
|
P_DamageMobj(thing, grenade, grenade->target, 1, (explodespin ? DMG_NORMAL : DMG_EXPLODE));
|
|
|
|
lagadded = (thing->hitlag - oldhitlag);
|
|
|
|
if (lagadded > minehitlag)
|
|
{
|
|
minehitlag = lagadded;
|
|
}
|
|
|
|
return BMIT_CONTINUE;
|
|
}
|
|
|
|
tic_t K_MineExplodeAttack(mobj_t *actor, fixed_t size, boolean spin)
|
|
{
|
|
INT32 bx, by, xl, xh, yl, yh;
|
|
|
|
explodespin = spin;
|
|
explodedist = FixedMul(size, actor->scale);
|
|
grenade = actor;
|
|
minehitlag = 0;
|
|
|
|
// Use blockmap to check for nearby shootables
|
|
yh = (unsigned)(actor->y + explodedist - bmaporgy)>>MAPBLOCKSHIFT;
|
|
yl = (unsigned)(actor->y - explodedist - bmaporgy)>>MAPBLOCKSHIFT;
|
|
xh = (unsigned)(actor->x + explodedist - bmaporgx)>>MAPBLOCKSHIFT;
|
|
xl = (unsigned)(actor->x - explodedist - bmaporgx)>>MAPBLOCKSHIFT;
|
|
|
|
BMBOUNDFIX (xl, xh, yl, yh);
|
|
|
|
for (by = yl; by <= yh; by++)
|
|
for (bx = xl; bx <= xh; bx++)
|
|
P_BlockThingsIterator(bx, by, PIT_SSMineExplode);
|
|
|
|
// Set this flag to ensure that the inital action won't be triggered twice.
|
|
actor->flags2 |= MF2_DEBRIS;
|
|
|
|
if (minehitlag == 0)
|
|
{
|
|
minehitlag = actor->hitlag;
|
|
}
|
|
|
|
// Set this flag to ensure the hitbox timer doesn't get extended with every player hit
|
|
actor->flags |= MF_NOHITLAGFORME;
|
|
actor->hitlag = 0; // same deal
|
|
|
|
if (!spin)
|
|
{
|
|
return minehitlag;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
boolean K_MineCollide(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
if (((t1->target == t2) || (!(t2->flags & (MF_ENEMY|MF_BOSS)) && (t1->target == t2->target))) && (t1->threshold > 0 || (t2->type != MT_PLAYER && t2->threshold > 0)))
|
|
return true;
|
|
|
|
if (t1->health <= 0 || t2->health <= 0)
|
|
return true;
|
|
|
|
if (t2->player)
|
|
{
|
|
if (t2->player->flashing > 0 && t2->hitlag == 0)
|
|
return true;
|
|
|
|
if (K_TryPickMeUp(t1, t2, false))
|
|
return true;
|
|
|
|
// Bomb punting
|
|
if ((t1->state >= &states[S_SSMINE1] && t1->state <= &states[S_SSMINE4])
|
|
|| (t1->state >= &states[S_SSMINE_DEPLOY8] && t1->state <= &states[S_SSMINE_EXPLODE2]))
|
|
{
|
|
P_KillMobj(t1, t2, t2, DMG_NORMAL);
|
|
}
|
|
else
|
|
{
|
|
K_PuntMine(t1, t2);
|
|
}
|
|
}
|
|
else if (t2->type == MT_ORBINAUT || t2->type == MT_JAWZ
|
|
|| t2->type == MT_ORBINAUT_SHIELD || t2->type == MT_JAWZ_SHIELD
|
|
|| t2->type == MT_GACHABOM)
|
|
{
|
|
// Bomb death
|
|
angle_t bounceangle = K_GetCollideAngle(t1, t2);
|
|
|
|
P_KillMobj(t1, t2, t2, DMG_NORMAL);
|
|
|
|
// Other Item Damage
|
|
S_StartSound(t2, t2->info->deathsound);
|
|
P_KillMobj(t2, t1, t1, DMG_NORMAL);
|
|
|
|
P_SetObjectMomZ(t2, 24*FRACUNIT, false);
|
|
P_InstaThrust(t2, bounceangle, 16*FRACUNIT);
|
|
}
|
|
else if (t2->flags & MF_SHOOTABLE)
|
|
{
|
|
// Bomb death
|
|
P_KillMobj(t1, t2, t2, DMG_NORMAL);
|
|
// Shootable damage
|
|
P_DamageMobj(t2, t1, t1->target, 1, DMG_NORMAL);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
boolean K_LandMineCollide(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
if (((t1->target == t2) || (!(t2->flags & (MF_ENEMY|MF_BOSS)) && (t1->target == t2->target))) && (t1->threshold > 0 || (t2->type != MT_PLAYER && t2->threshold > 0)))
|
|
return true;
|
|
|
|
if (t1->health <= 0 || t2->health <= 0)
|
|
return true;
|
|
|
|
if (K_TryPickMeUp(t1, t2, false))
|
|
return true;
|
|
|
|
if (t2->player)
|
|
{
|
|
const INT32 oldhitlag = t2->hitlag;
|
|
|
|
if (t2->player->flashing)
|
|
return true;
|
|
|
|
// Banana snipe!
|
|
if (t1->health > 1)
|
|
{
|
|
if (t1->target
|
|
&& t1->target->player
|
|
&& t2->player != t1->target->player)
|
|
{
|
|
t1->target->player->roundconditions.landmine_dunk = true;
|
|
t1->target->player->roundconditions.checkthisframe = true;
|
|
}
|
|
|
|
S_StartSound(t2, sfx_bsnipe);
|
|
}
|
|
|
|
if (t2->player->flamedash && t2->player->itemtype == KITEM_FLAMESHIELD)
|
|
{
|
|
// Melt item
|
|
S_StartSound(t2, sfx_s3k43);
|
|
K_SetHitLagForObjects(t2, t1, t1->target, 3, false);
|
|
}
|
|
else
|
|
{
|
|
// Player Damage
|
|
P_DamageMobj(t2, t1, t1->target, 1, DMG_TUMBLE);
|
|
}
|
|
|
|
t1->reactiontime = (t2->hitlag - oldhitlag);
|
|
P_KillMobj(t1, t2, t2, DMG_NORMAL);
|
|
}
|
|
else if (t2->type == MT_BANANA || t2->type == MT_BANANA_SHIELD
|
|
|| t2->type == MT_ORBINAUT || t2->type == MT_ORBINAUT_SHIELD
|
|
|| t2->type == MT_JAWZ || t2->type == MT_JAWZ_SHIELD
|
|
|| t2->type == MT_BALLHOG || t2->type == MT_GACHABOM)
|
|
{
|
|
// Other Item Damage
|
|
angle_t bounceangle = K_GetCollideAngle(t1, t2);
|
|
|
|
if (t2->eflags & MFE_VERTICALFLIP)
|
|
t2->z -= t2->height;
|
|
else
|
|
t2->z += t2->height;
|
|
|
|
P_SpawnMobj(t2->x/2 + t1->x/2, t2->y/2 + t1->y/2, t2->z/2 + t1->z/2, MT_ITEMCLASH);
|
|
|
|
S_StartSound(t2, t2->info->deathsound);
|
|
P_KillMobj(t2, t1, t1, DMG_NORMAL);
|
|
|
|
if (P_MobjWasRemoved(t2))
|
|
{
|
|
t2 = NULL; // handles the arguments to P_KillMobj
|
|
}
|
|
else
|
|
{
|
|
P_SetObjectMomZ(t2, 24*FRACUNIT, false);
|
|
P_InstaThrust(t2, bounceangle, 16*FRACUNIT);
|
|
|
|
t1->reactiontime = t2->hitlag;
|
|
}
|
|
P_KillMobj(t1, t2, t2, DMG_NORMAL);
|
|
}
|
|
else if (t2->type == MT_SSMINE_SHIELD || t2->type == MT_SSMINE || t2->type == MT_LANDMINE)
|
|
{
|
|
P_KillMobj(t1, t2, t2, DMG_NORMAL);
|
|
// Bomb death
|
|
P_KillMobj(t2, t1, t1, DMG_NORMAL);
|
|
}
|
|
else if (t2->flags & MF_SHOOTABLE)
|
|
{
|
|
// Shootable damage
|
|
P_DamageMobj(t2, t1, t1->target, 1, DMG_NORMAL);
|
|
|
|
if (P_MobjWasRemoved(t2))
|
|
{
|
|
t2 = NULL; // handles the arguments to P_KillMobj
|
|
}
|
|
else
|
|
{
|
|
t1->reactiontime = t2->hitlag;
|
|
}
|
|
|
|
P_KillMobj(t1, t2, t2, DMG_NORMAL);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
boolean K_DropTargetCollide(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
mobj_t *draggeddroptarget = (t1->type == MT_DROPTARGET_SHIELD) ? t1->target : NULL;
|
|
UINT8 strength;
|
|
|
|
if (((t1->target == t2) || (t1->target == t2->target)) && ((t1->threshold > 0 && t2->type == MT_PLAYER) || (t2->type != MT_PLAYER && t2->threshold > 0)))
|
|
return true;
|
|
|
|
if (t1->health <= 0 || t2->health <= 0)
|
|
return true;
|
|
|
|
if (t2->player && (t2->player->hyudorotimer || t2->player->justbumped))
|
|
return true;
|
|
|
|
if (K_TryPickMeUp(t1, t2, false))
|
|
return true;
|
|
|
|
if (draggeddroptarget && P_MobjWasRemoved(draggeddroptarget))
|
|
draggeddroptarget = NULL; // Beware order-of-execution on crushers, I guess?!
|
|
|
|
if (t1->health > 3) // forward thrown
|
|
{
|
|
strength = 0;
|
|
}
|
|
else if (t1->reactiontime == 0 || draggeddroptarget)
|
|
{
|
|
strength = 80;
|
|
}
|
|
else
|
|
{
|
|
strength = 140;
|
|
}
|
|
|
|
// Intensify bumps if already spinning...
|
|
P_Thrust(t1, R_PointToAngle2(t1->x, t1->y, t2->x, t2->y), strength * t1->scale);
|
|
|
|
if (draggeddroptarget)
|
|
{
|
|
// "Pass through" the shock of the impact, part 1.
|
|
t1->momx = t1->target->momx;
|
|
t1->momy = t1->target->momy;
|
|
t1->momz = t1->target->momz;
|
|
}
|
|
|
|
fixed_t bumppower = FRACUNIT;
|
|
if (t2->player)
|
|
{
|
|
fixed_t speeddampen = FixedDiv(t2->player->speed, 2*K_GetKartSpeed(t2->player, false, false));
|
|
bumppower = Easing_InQuad(
|
|
std::min(speeddampen, FRACUNIT),
|
|
FRACUNIT,
|
|
3*FRACUNIT/4
|
|
);
|
|
if (t2->player->tripwireLeniency || t2->player->tripwirePass != TRIPWIRE_NONE)
|
|
bumppower = FRACUNIT/2;
|
|
}
|
|
|
|
if (t2->type == MT_INSTAWHIP)
|
|
bumppower = 0;
|
|
|
|
{
|
|
angle_t t2angle = R_PointToAngle2(t2->momx, t2->momy, 0, 0);
|
|
angle_t t2deflect;
|
|
fixed_t t1speed, t2speed;
|
|
|
|
if (t2->type == MT_INSTAWHIP && t2->target && !P_MobjWasRemoved(t2->target))
|
|
{
|
|
t2angle = R_PointToAngle2(t2->target->momx, t2->target->momy, 0, 0);
|
|
t2speed = FixedHypot(t2->target->momx, t2->target->momy);
|
|
P_InstaThrust(t1, ANGLE_180 + R_PointToAngle2(t1->x, t1->y, t2->x, t2->y), 100*t2->target->scale + t2speed);
|
|
}
|
|
else
|
|
{
|
|
K_KartBouncing(t1, t2);
|
|
t2speed = FixedHypot(t2->momx, t2->momy);
|
|
}
|
|
|
|
t1speed = FixedHypot(t1->momx, t1->momy);
|
|
|
|
t2deflect = t2angle - R_PointToAngle2(0, 0, t2->momx, t2->momy);
|
|
if (t2deflect > ANGLE_180)
|
|
t2deflect = InvAngle(t2deflect);
|
|
if (t2deflect < ANG10)
|
|
P_InstaThrust(t2, t2angle, FixedMul(t2speed, bumppower));
|
|
|
|
t1->angle = t1->old_angle = R_PointToAngle2(0, 0, t1->momx, t1->momy);
|
|
|
|
t1->reactiontime = (7 * (t1speed + t2speed)) / (4 * t1->scale);
|
|
if (t1->reactiontime < 10)
|
|
t1->reactiontime = 10;
|
|
t1->threshold = 10;
|
|
}
|
|
|
|
t1->renderflags &= ~RF_FULLDARK; // brightest on the bump
|
|
|
|
if (draggeddroptarget)
|
|
{
|
|
// "Pass through" the shock of the impact, part 2.
|
|
draggeddroptarget->momx = t1->momx;
|
|
draggeddroptarget->momy = t1->momy;
|
|
draggeddroptarget->momz = t1->momz;
|
|
|
|
// Have the drop target travel between them.
|
|
t1->momx = (t1->momx + t2->momx)/2;
|
|
t1->momy = (t1->momy + t2->momy)/2;
|
|
t1->momz = (t1->momz + t2->momz)/2;
|
|
|
|
K_AddHitLag(t1->target, 6, false);
|
|
}
|
|
|
|
K_AddHitLag(t1, 6, true);
|
|
K_AddHitLag(t2, 6, false);
|
|
|
|
if (t2->type == MT_INSTAWHIP && t2->target && !P_MobjWasRemoved(t2->target))
|
|
K_AddHitLag(t2->target, 6, false);
|
|
|
|
{
|
|
mobj_t *ghost = P_SpawnGhostMobj(t1);
|
|
UINT8 i;
|
|
|
|
P_SetScale(ghost, 3*ghost->destscale/2);
|
|
ghost->destscale = 15*ghost->destscale/2;
|
|
ghost->fuse = 10;
|
|
ghost->scalespeed = (ghost->destscale - ghost->scale)/ghost->fuse;
|
|
|
|
for (i = 0; i < 2; i++)
|
|
{
|
|
mobj_t *blast = P_SpawnMobjFromMobj(t1, 0, 0, FixedDiv(t1->height, t1->scale), MT_BATTLEBUMPER_BLAST);
|
|
P_SetScale(blast, 5*blast->scale/2);
|
|
|
|
blast->angle = R_PointToAngle2(0, 0, t1->momx, t1->momy) + ANGLE_45;
|
|
if (i & 1)
|
|
{
|
|
blast->angle += ANGLE_90;
|
|
}
|
|
|
|
blast->destscale *= 10;
|
|
}
|
|
}
|
|
|
|
t1->flags |= MF_SHOOTABLE;
|
|
// The following sets t1->target to t2, so draggeddroptarget keeps it persisting...
|
|
P_DamageMobj(t1, t2, (t2->target ? t2->target : t2), 1, DMG_NORMAL);
|
|
|
|
switch (t1->health)
|
|
{
|
|
case 3:
|
|
t1->color = SKINCOLOR_LIME;
|
|
break;
|
|
|
|
case 2:
|
|
t1->color = SKINCOLOR_GOLD;
|
|
break;
|
|
|
|
case 1:
|
|
t1->color = SKINCOLOR_CRIMSON;
|
|
break;
|
|
}
|
|
|
|
t1->flags &= ~MF_SHOOTABLE;
|
|
|
|
t1->spritexscale = 3*FRACUNIT;
|
|
t1->spriteyscale = 3*FRACUNIT/2;
|
|
|
|
if (!t2->player)
|
|
{
|
|
t2->angle += ANGLE_180;
|
|
if (t2->type == MT_JAWZ)
|
|
P_SetTarget(&t2->tracer, t2->target); // Back to the source!
|
|
|
|
// Reflected item becomes owned by the DT owner, so it becomes dangerous the the thrower
|
|
if (t1->target && !P_MobjWasRemoved(t1->target))
|
|
P_SetTarget(&t2->target, t1->target);
|
|
|
|
t2->threshold = 10;
|
|
}
|
|
|
|
if (t1->reactiontime > 1000) {
|
|
S_StartSound(t2, sfx_kdtrg3);
|
|
} else if (t1->reactiontime > 500) {
|
|
S_StartSound(t2, sfx_kdtrg2);
|
|
} else {
|
|
S_StartSound(t2, sfx_kdtrg1);
|
|
}
|
|
|
|
if (t1->tracer && t1->tracer->player && t2->player && t2->player != t1->tracer->player)
|
|
{
|
|
K_SpawnAmps(t1->tracer->player, K_PvPAmpReward(20, t1->tracer->player, t2->player), t1);
|
|
}
|
|
|
|
if (draggeddroptarget && !P_MobjWasRemoved(draggeddroptarget) && draggeddroptarget->player)
|
|
{
|
|
// The following removes t1, be warned
|
|
// (its newly assigned properties are moved across)
|
|
K_DropHnextList(draggeddroptarget->player);
|
|
// Do NOT modify or reference t1 after this line
|
|
// I mean it! Do not even absentmindedly try it
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static mobj_t *lightningSource;
|
|
static fixed_t lightningDist;
|
|
|
|
static inline BlockItReturn_t PIT_LightningShieldAttack(mobj_t *thing)
|
|
{
|
|
if (lightningSource == NULL || P_MobjWasRemoved(lightningSource))
|
|
{
|
|
// Invalid?
|
|
return BMIT_ABORT;
|
|
}
|
|
|
|
if (thing == NULL || P_MobjWasRemoved(thing))
|
|
{
|
|
// Invalid?
|
|
return BMIT_ABORT;
|
|
}
|
|
|
|
if (thing == lightningSource)
|
|
{
|
|
// Don't explode yourself!!
|
|
return BMIT_CONTINUE;
|
|
}
|
|
|
|
if (thing->health <= 0)
|
|
{
|
|
// Dead
|
|
return BMIT_CONTINUE;
|
|
}
|
|
|
|
if (thing->type != MT_SPB)
|
|
{
|
|
if (!(thing->flags & MF_SHOOTABLE) || (thing->flags & MF_SCENERY))
|
|
{
|
|
// Not shootable
|
|
return BMIT_CONTINUE;
|
|
}
|
|
}
|
|
|
|
if (thing->player && thing->player->spectator)
|
|
{
|
|
// Spectator
|
|
return BMIT_CONTINUE;
|
|
}
|
|
|
|
if (P_AproxDistance(thing->x - lightningSource->x, thing->y - lightningSource->y) > lightningDist + thing->radius)
|
|
{
|
|
// Too far away
|
|
return BMIT_CONTINUE;
|
|
}
|
|
|
|
// see if it went over / under
|
|
if (lightningSource->z - lightningDist > thing->z + thing->height)
|
|
return BMIT_CONTINUE; // overhead
|
|
if (lightningSource->z + lightningSource->height + lightningDist < thing->z)
|
|
return BMIT_CONTINUE; // underneath
|
|
|
|
#if 0
|
|
if (P_CheckSight(lightningSource, thing) == false)
|
|
{
|
|
// Not in sight
|
|
return BMIT_CONTINUE;
|
|
}
|
|
#endif
|
|
|
|
P_DamageMobj(thing, lightningSource, lightningSource, 1, DMG_VOLTAGE|DMG_CANTHURTSELF|DMG_WOMBO);
|
|
return BMIT_CONTINUE;
|
|
}
|
|
|
|
void K_LightningShieldAttack(mobj_t *actor, fixed_t size)
|
|
{
|
|
INT32 bx, by, xl, xh, yl, yh;
|
|
|
|
lightningDist = FixedMul(size, actor->scale);
|
|
lightningSource = actor;
|
|
|
|
// Use blockmap to check for nearby shootables
|
|
yh = (unsigned)(actor->y + lightningDist - bmaporgy)>>MAPBLOCKSHIFT;
|
|
yl = (unsigned)(actor->y - lightningDist - bmaporgy)>>MAPBLOCKSHIFT;
|
|
xh = (unsigned)(actor->x + lightningDist - bmaporgx)>>MAPBLOCKSHIFT;
|
|
xl = (unsigned)(actor->x - lightningDist - bmaporgx)>>MAPBLOCKSHIFT;
|
|
|
|
BMBOUNDFIX (xl, xh, yl, yh);
|
|
|
|
for (by = yl; by <= yh; by++)
|
|
for (bx = xl; bx <= xh; bx++)
|
|
P_BlockThingsIterator(bx, by, PIT_LightningShieldAttack);
|
|
}
|
|
|
|
boolean K_BubbleShieldCanReflect(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
return (t2->type == MT_ORBINAUT || t2->type == MT_JAWZ || t2->type == MT_GACHABOM
|
|
|| t2->type == MT_BANANA || t2->type == MT_EGGMANITEM || t2->type == MT_BALLHOG
|
|
|| t2->type == MT_SSMINE || t2->type == MT_LANDMINE || t2->type == MT_SINK
|
|
|| t2->type == MT_GARDENTOP
|
|
|| t2->type == MT_DROPTARGET
|
|
|| t2->type == MT_KART_LEFTOVER
|
|
|| (t2->type == MT_PLAYER && t1->target != t2));
|
|
}
|
|
|
|
boolean K_BubbleShieldReflect(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
mobj_t *owner = t1->player ? t1 : t1->target;
|
|
|
|
if (t2->target != owner || !t2->threshold || t2->type == MT_DROPTARGET)
|
|
{
|
|
if (t1->player && K_PlayerGuard(t1->player))
|
|
{
|
|
K_KartSolidBounce(t1, t2);
|
|
K_DoPowerClash(t1, t2);
|
|
}
|
|
if (!t2->momx && !t2->momy)
|
|
{
|
|
t2->momz += (24*t2->scale) * P_MobjFlip(t2);
|
|
}
|
|
else
|
|
{
|
|
t2->momx = -6*t2->momx;
|
|
t2->momy = -6*t2->momy;
|
|
t2->momz = -6*t2->momz;
|
|
t2->angle += ANGLE_180;
|
|
}
|
|
if (t2->type == MT_JAWZ)
|
|
P_SetTarget(&t2->tracer, t2->target); // Back to the source!
|
|
P_SetTarget(&t2->target, owner); // Let the source reflect it back again!
|
|
t2->threshold = 10;
|
|
S_StartSound(t1, sfx_s3k44);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
boolean K_BubbleShieldCollide(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
if (t2->type == MT_PLAYER)
|
|
{
|
|
// Counter desyncs
|
|
/*mobj_t *oldthing = thing;
|
|
mobj_t *oldg_tm.thing = g_tm.thing;
|
|
|
|
P_Thrust(g_tm.thing, R_PointToAngle2(thing->x, thing->y, g_tm.thing->x, g_tm.thing->y), 4*thing->scale);
|
|
|
|
thing = oldthing;
|
|
P_SetTarget(&g_tm.thing, oldg_tm.thing);*/
|
|
|
|
boolean hit = false;
|
|
|
|
if (K_KartBouncing(t2, t1->target) == true)
|
|
{
|
|
if (t2->player && t1->target && t1->target->player)
|
|
{
|
|
hit = K_PvPTouchDamage(t2, t1->target);
|
|
}
|
|
|
|
// Don't play from t1 else it gets cut out... for some reason.
|
|
S_StartSound(t2, sfx_s3k44);
|
|
}
|
|
|
|
if (hit && (gametyperules & GTR_BUMPERS))
|
|
{
|
|
K_PopBubbleShield(t1->target->player);
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (K_BubbleShieldCanReflect(t1, t2))
|
|
{
|
|
return K_BubbleShieldReflect(t1, t2);
|
|
}
|
|
|
|
if (t2->flags & MF_SHOOTABLE)
|
|
{
|
|
P_DamageMobj(t2, t1, t1->target, 1, DMG_NORMAL);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
boolean K_InstaWhipCollide(mobj_t *shield, mobj_t *victim)
|
|
{
|
|
int victimHitlag = 10;
|
|
int attackerHitlag = 4;
|
|
|
|
// EV1 is used to indicate that we should no longer hit monitors.
|
|
// EV2 indicates we should no longer hit anything.
|
|
if (shield->extravalue2)
|
|
return false;
|
|
|
|
mobj_t *attacker = shield->target;
|
|
|
|
if (!attacker || P_MobjWasRemoved(attacker) || !attacker->player)
|
|
return false; // How did we even get here?
|
|
|
|
player_t *attackerPlayer = attacker->player;
|
|
|
|
if (victim->player)
|
|
{
|
|
player_t *victimPlayer = victim->player;
|
|
|
|
if (victim == attacker)
|
|
return false;
|
|
|
|
// If both players have a whip, hits are order-of-execution dependent and that sucks.
|
|
// Player expectation is a clash here.
|
|
if (victimPlayer->whip && !P_MobjWasRemoved(victimPlayer->whip))
|
|
{
|
|
if (victim->hitlag != 0)
|
|
return false;
|
|
|
|
victimPlayer->whip->extravalue2 = 1;
|
|
shield->extravalue2 = 1;
|
|
|
|
K_DoPowerClash(victim, attacker);
|
|
|
|
victim->renderflags &= ~RF_DONTDRAW;
|
|
attacker->renderflags &= ~RF_DONTDRAW;
|
|
|
|
angle_t thrangle = R_PointToAngle2(attacker->x, attacker->y, victim->x, victim->y);
|
|
P_Thrust(victim, thrangle, mapobjectscale*28);
|
|
P_Thrust(attacker, ANGLE_180 + thrangle, mapobjectscale*28);
|
|
|
|
return false;
|
|
}
|
|
|
|
if (P_PlayerInPain(victimPlayer) ? victim->hitlag == 0 : victimPlayer->flashing == 0)
|
|
{
|
|
// Instawhip _always_ loses to guard.
|
|
if (K_PlayerGuard(victimPlayer))
|
|
//if (true)
|
|
{
|
|
victimHitlag = 3*victimHitlag;
|
|
|
|
if (P_PlayerInPain(attackerPlayer))
|
|
return false; // never punish shield more than once
|
|
|
|
angle_t thrangle = R_PointToAngle2(victim->x, victim->y, shield->x, shield->y);
|
|
attacker->momx = attacker->momy = 0;
|
|
P_Thrust(attacker, thrangle, mapobjectscale*7);
|
|
|
|
// target is inflictor: hack to let invincible players lose to guard
|
|
P_DamageMobj(attacker, attacker, victim, 1, DMG_TUMBLE);
|
|
|
|
// A little extra juice, so successful reads are usually positive or zero on spheres.
|
|
victimPlayer->spheres = std::min(victimPlayer->spheres + 10, 40);
|
|
|
|
shield->renderflags &= ~RF_DONTDRAW;
|
|
shield->flags |= MF_NOCLIPTHING;
|
|
|
|
// Attacker should be free to all reasonable followups.
|
|
attacker->renderflags &= ~RF_DONTDRAW;
|
|
attackerPlayer->spindashboost = 0;
|
|
attackerPlayer->sneakertimer = 0;
|
|
attackerPlayer->panelsneakertimer = 0;
|
|
attackerPlayer->weaksneakertimer = 0;
|
|
attackerPlayer->instaWhipCharge = 0;
|
|
attackerPlayer->flashing = 0;
|
|
|
|
K_AddMessageForPlayer(victimPlayer, "Whip Reflected!", false, false);
|
|
K_AddMessageForPlayer(attackerPlayer, "COUNTERED!!", false, false);
|
|
|
|
// Localized broly for a local event.
|
|
if (mobj_t *broly = Obj_SpawnBrolyKi(victim, victimHitlag/2))
|
|
{
|
|
broly->extravalue2 = 16*mapobjectscale;
|
|
}
|
|
|
|
P_PlayVictorySound(victim);
|
|
|
|
P_DamageMobj(attacker, attacker, victim, 1, DMG_TUMBLE);
|
|
|
|
S_StartSound(victim, sfx_mbv92);
|
|
K_AddHitLag(attacker, victimHitlag, true);
|
|
K_AddHitLag(victim, attackerHitlag, false);
|
|
|
|
K_DoPowerClash(shield, victim); // REJECTED
|
|
|
|
shield->extravalue2 = 1;
|
|
|
|
return true;
|
|
}
|
|
|
|
// if you're here, you're getting hit
|
|
P_DamageMobj(victim, shield, attacker, 1, DMG_WHUMBLE);
|
|
|
|
K_DropPowerUps(victimPlayer);
|
|
|
|
angle_t thrangle = ANGLE_180 + R_PointToAngle2(victim->x, victim->y, shield->x, shield->y);
|
|
P_Thrust(victim, thrangle, mapobjectscale*40);
|
|
|
|
K_AddHitLag(victim, victimHitlag, true);
|
|
K_AddHitLag(attacker, attackerHitlag, false);
|
|
shield->hitlag = attacker->hitlag;
|
|
|
|
if (attackerPlayer->roundconditions.whip_hyuu == false
|
|
&& attackerPlayer->hyudorotimer > 0)
|
|
{
|
|
attackerPlayer->roundconditions.whip_hyuu = true;
|
|
attackerPlayer->roundconditions.checkthisframe = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
else if (victim->type == MT_SUPER_FLICKY)
|
|
{
|
|
if (Obj_IsSuperFlickyWhippable(victim, attacker))
|
|
{
|
|
K_AddHitLag(victim, victimHitlag, true);
|
|
K_AddHitLag(attacker, attackerHitlag, false);
|
|
shield->hitlag = attacker->hitlag;
|
|
|
|
Obj_WhipSuperFlicky(victim);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
else if (victim->type == MT_DROPTARGET || victim->type == MT_DROPTARGET_SHIELD)
|
|
{
|
|
if (K_TryPickMeUp(attacker, victim, true))
|
|
{
|
|
shield->hitlag = attacker->hitlag; // players hitlag is handled in K_TryPickMeUp, and we need to set for the shield too
|
|
}
|
|
else
|
|
{
|
|
K_DropTargetCollide(victim, shield);
|
|
}
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if (victim->flags & MF_SHOOTABLE)
|
|
{
|
|
// Monitor hack. We can hit monitors once per instawhip, no multihit shredding!
|
|
// Damage values in Obj_MonitorGetDamage.
|
|
// Apply to UFO also -- steelt 29062023
|
|
if (victim->type == MT_MONITOR || victim->type == MT_BATTLEUFO || victim->type == MT_BALLSWITCH_BALL)
|
|
{
|
|
if (shield->extravalue1 == 1)
|
|
return false;
|
|
shield->extravalue1 = 1;
|
|
}
|
|
|
|
if (K_TryPickMeUp(attacker, victim, true))
|
|
{
|
|
shield->hitlag = attacker->hitlag; // players hitlag is handled in K_TryPickMeUp, and we need to set for the shield too
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
P_DamageMobj(victim, shield, attacker, 1, DMG_NORMAL);
|
|
K_AddHitLag(attacker, attackerHitlag, false);
|
|
shield->hitlag = attacker->hitlag;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
boolean K_KitchenSinkCollide(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
if (((t1->target == t2) || (!(t2->flags & (MF_ENEMY|MF_BOSS)) && (t1->target == t2->target))) && (t1->threshold > 0 || (t2->type != MT_PLAYER && t2->threshold > 0)))
|
|
return true;
|
|
|
|
if (K_TryPickMeUp(t1, t2, false))
|
|
return true;
|
|
|
|
if (t2->player)
|
|
{
|
|
if (t2->player->flashing > 0 && t2->hitlag == 0)
|
|
return true;
|
|
|
|
S_StartSound(NULL, sfx_bsnipe); // let all players hear it.
|
|
|
|
if (t1->target && !P_MobjWasRemoved(t1->target) && t1->target->player)
|
|
K_SpawnAmps(t1->target->player, 50, t2);
|
|
|
|
HU_SetCEchoFlags(0);
|
|
HU_SetCEchoDuration(5);
|
|
HU_DoCEcho(va("%s\\was hit by a kitchen sink.\\\\\\\\", player_names[t2->player-players]));
|
|
I_OutputMsg("%s was hit by a kitchen sink.\n", player_names[t2->player-players]);
|
|
|
|
P_DamageMobj(t2, t1, t1->target, 1, DMG_INSTAKILL);
|
|
P_KillMobj(t1, t2, t2, DMG_NORMAL);
|
|
}
|
|
else if (t2->flags & MF_SHOOTABLE)
|
|
{
|
|
// Shootable damage
|
|
P_KillMobj(t2, t2, t1->target, DMG_NORMAL);
|
|
if (P_MobjWasRemoved(t2))
|
|
{
|
|
t2 = NULL; // handles the arguments to P_KillMobj
|
|
}
|
|
// This item damage
|
|
P_KillMobj(t1, t2, t2, DMG_NORMAL);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
boolean K_FallingRockCollide(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
if (t2->player || t2->type == MT_FALLINGROCK)
|
|
K_KartBouncing(t2, t1);
|
|
return true;
|
|
}
|
|
|
|
boolean K_PvPTouchDamage(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
if (K_PodiumSequence() == true)
|
|
{
|
|
// Always regular bumps, no ring toss.
|
|
return false;
|
|
}
|
|
|
|
// What the fuck is calling this with stale refs? Whatever, validation's cheap.
|
|
if (P_MobjWasRemoved(t1) || P_MobjWasRemoved(t2) || !t1->player || !t2->player)
|
|
return false;
|
|
|
|
if (G_SameTeam(t1->player, t2->player))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
boolean guard1 = K_PlayerGuard(t1->player);
|
|
boolean guard2 = K_PlayerGuard(t2->player);
|
|
|
|
// Bubble Shield physically extends past guard when inflated,
|
|
// makes some sense to suppress this behavior
|
|
if (t1->player->bubbleblowup)
|
|
guard1 = false;
|
|
if (t2->player->bubbleblowup)
|
|
guard2 = false;
|
|
|
|
if (guard1 && guard2)
|
|
K_DoPowerClash(t1, t2);
|
|
else if (guard1)
|
|
K_DoGuardBreak(t1, t2);
|
|
else if (guard2)
|
|
K_DoGuardBreak(t2, t1);
|
|
|
|
if (guard1 || guard2)
|
|
return false;
|
|
|
|
// Clash instead of damage if both parties have any of these conditions
|
|
auto canClash = [](mobj_t *t1, mobj_t *t2)
|
|
{
|
|
return (K_IsBigger(t1, t2) == true)
|
|
|| (t1->player->invincibilitytimer > 0)
|
|
|| (t1->player->flamedash > 0 && t1->player->itemtype == KITEM_FLAMESHIELD)
|
|
|| (t1->player->curshield == KSHIELD_TOP && !K_IsHoldingDownTop(t1->player))
|
|
|| (t1->player->bubbleblowup > 0);
|
|
};
|
|
|
|
if (canClash(t1, t2) && canClash(t2, t1))
|
|
{
|
|
K_DoPowerClash(t1, t2);
|
|
return false;
|
|
}
|
|
|
|
auto forEither = [t1, t2](auto conditionCallable, auto damageCallable)
|
|
{
|
|
const bool t1Condition = conditionCallable(t1, t2);
|
|
const bool t2Condition = conditionCallable(t2, t1);
|
|
|
|
if (t1Condition == true && t2Condition == false)
|
|
{
|
|
damageCallable(t1, t2);
|
|
return true;
|
|
}
|
|
else if (t1Condition == false && t2Condition == true)
|
|
{
|
|
damageCallable(t2, t1);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
auto doDamage = [](UINT8 damageType)
|
|
{
|
|
return [damageType](mobj_t *t1, mobj_t *t2)
|
|
{
|
|
P_DamageMobj(t2, t1, t1, 1, damageType);
|
|
};
|
|
};
|
|
|
|
// Cause tumble on invincibility
|
|
auto shouldTumble = [](mobj_t *t1, mobj_t *t2)
|
|
{
|
|
return (t1->player->invincibilitytimer > 0);
|
|
};
|
|
|
|
if (forEither(shouldTumble, doDamage(DMG_TUMBLE)))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Flame Shield dash damage
|
|
// Bubble Shield blowup damage
|
|
auto shouldWipeout = [](mobj_t *t1, mobj_t *t2)
|
|
{
|
|
return (t1->player->flamedash > 0 && t1->player->itemtype == KITEM_FLAMESHIELD)
|
|
|| (t1->player->bubbleblowup > 0);
|
|
};
|
|
|
|
if (forEither(shouldWipeout, doDamage(DMG_WIPEOUT | DMG_WOMBO)))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Battle Mode Sneaker damage
|
|
// (Pogo Spring damage is handled in head-stomping code)
|
|
if (gametyperules & GTR_BUMPERS)
|
|
{
|
|
auto shouldSteal = [](mobj_t *t1, mobj_t *t2)
|
|
{
|
|
return ((t1->player->sneakertimer > 0 || t1->player->panelsneakertimer > 0 || t1->player->weaksneakertimer > 0)
|
|
&& !P_PlayerInPain(t1->player)
|
|
&& (t1->player->flashing == 0));
|
|
};
|
|
|
|
if (forEither(shouldSteal, doDamage(DMG_WIPEOUT | DMG_STEAL | DMG_WOMBO)))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Cause stumble on scale difference
|
|
auto shouldStumble = [](mobj_t *t1, mobj_t *t2)
|
|
{
|
|
return K_IsBigger(t1, t2);
|
|
};
|
|
|
|
auto doStumble = [](mobj_t *t1, mobj_t *t2)
|
|
{
|
|
if (gametyperules & GTR_BUMPERS)
|
|
{
|
|
K_StumblePlayer(t2->player);
|
|
K_SpawnAmps(t1->player, K_PvPAmpReward(20, t1->player, t2->player), t2);
|
|
}
|
|
else
|
|
{
|
|
P_DamageMobj(t2, t1, t1, 1, DMG_WHUMBLE);
|
|
}
|
|
};
|
|
|
|
if (forEither(shouldStumble, doStumble))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Ring sting, this is a bit more unique
|
|
auto doSting = [](mobj_t *t1, mobj_t *t2)
|
|
{
|
|
if (t2->player->curshield != KSHIELD_NONE)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
boolean damagedpresting = (t2->player->flashing || P_PlayerInPain(t2->player));
|
|
|
|
// CONS_Printf("T1=%s T2=%s\n", player_names[t1->player - players], player_names[t2->player - players]);
|
|
// CONS_Printf("DPS=%d\n", damagedpresting);
|
|
|
|
if (P_PlayerInPain(t1->player) || t1->player->flashing)
|
|
{
|
|
// CONS_Printf("T1 pain\n");
|
|
if (!(t1->player->pflags2 & PF2_SAMEFRAMESTUNG))
|
|
return false;
|
|
// CONS_Printf("...but ignored\n");
|
|
}
|
|
|
|
bool stung = false;
|
|
|
|
if (RINGTOTAL(t2->player) <= 0 && t2->player->ringboostinprogress == 0 && t2->health == 1 && !(t2->player->pflags2 & PF2_UNSTINGABLE))
|
|
{
|
|
P_DamageMobj(t2, t1, t1, 1, DMG_STING|DMG_WOMBO);
|
|
// CONS_Printf("T2 stung\n");
|
|
if (!damagedpresting)
|
|
{
|
|
t2->player->pflags2 |= PF2_SAMEFRAMESTUNG;
|
|
// CONS_Printf("T2 SFS\n");
|
|
}
|
|
stung = true;
|
|
}
|
|
|
|
P_PlayerRingBurst(t2->player, 1);
|
|
|
|
return stung;
|
|
};
|
|
|
|
// No damage hitlag for stinging.
|
|
auto removeDamageHitlag = [](mobj_t *t1, mobj_t *t2)
|
|
{
|
|
t1->eflags &= ~MFE_DAMAGEHITLAG;
|
|
};
|
|
|
|
// Looks bad, but "forEither" actually runs if t1 XOR t2 were damaged.
|
|
// I don't even think we use the touchdamage return value but I'm too
|
|
// afraid to change it now. Fix this if you're the next guy and annoyed
|
|
if (forEither(doSting, removeDamageHitlag))
|
|
{
|
|
t1->player->pflags2 &= ~PF2_SAMEFRAMESTUNG;
|
|
t2->player->pflags2 &= ~PF2_SAMEFRAMESTUNG;
|
|
return true;
|
|
}
|
|
|
|
t1->player->pflags2 &= ~PF2_SAMEFRAMESTUNG;
|
|
t2->player->pflags2 &= ~PF2_SAMEFRAMESTUNG;
|
|
|
|
return false;
|
|
}
|
|
|
|
void K_PuntHazard(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
// TODO: spawn a unique mobjtype other than MT_GHOST
|
|
mobj_t *img = P_SpawnGhostMobj(t1);
|
|
|
|
K_MakeObjectReappear(t1);
|
|
|
|
img->flags &= ~MF_NOGRAVITY;
|
|
img->renderflags = t1->renderflags & ~RF_DONTDRAW;
|
|
img->extravalue1 = 1;
|
|
img->extravalue2 = 2;
|
|
img->fuse = 2*TICRATE;
|
|
|
|
struct Vector
|
|
{
|
|
fixed_t x_, y_, z_;
|
|
fixed_t h_ = FixedHypot(x_, y_);
|
|
fixed_t speed_ = std::max(60 * mapobjectscale, FixedHypot(h_, z_) * 2);
|
|
|
|
explicit Vector(fixed_t x, fixed_t y, fixed_t z) : x_(x), y_(y), z_(z) {}
|
|
explicit Vector(const mobj_t* mo) :
|
|
Vector(std::max(
|
|
Vector(mo->x - mo->old_x, mo->y - mo->old_y, mo->z - mo->old_z),
|
|
Vector(mo->momx, mo->momy, mo->momz)
|
|
))
|
|
{
|
|
}
|
|
explicit Vector(const Vector&) = default;
|
|
|
|
bool operator<(const Vector& b) const { return speed_ < b.speed_; }
|
|
|
|
void invert()
|
|
{
|
|
x_ = -x_;
|
|
y_ = -y_;
|
|
z_ = -z_;
|
|
}
|
|
|
|
void thrust(mobj_t* mo) const
|
|
{
|
|
angle_t yaw = R_PointToAngle2(0, 0, h_, z_);
|
|
yaw = std::max(AbsAngle(yaw), static_cast<angle_t>(ANGLE_11hh)) + (yaw & ANGLE_180);
|
|
|
|
P_InstaThrust(mo, R_PointToAngle2(0, 0, x_, y_), FixedMul(speed_, FCOS(yaw)));
|
|
mo->momz = FixedMul(speed_, FSIN(yaw));
|
|
}
|
|
};
|
|
|
|
Vector h_vector(t1);
|
|
Vector p_vector(t2);
|
|
|
|
h_vector.invert();
|
|
|
|
std::max(h_vector, p_vector).thrust(img);
|
|
|
|
K_DoPowerClash(img, t2); // applies hitlag
|
|
P_SpawnMobj(t2->x/2 + t1->x/2, t2->y/2 + t1->y/2, t2->z/2 + t1->z/2, MT_ITEMCLASH);
|
|
}
|
|
|
|
boolean K_PuntCollide(mobj_t *t1, mobj_t *t2)
|
|
{
|
|
// MF_SHOOTABLE will get damaged directly, instead
|
|
if (t1->flags & (MF_DONTPUNT | MF_SHOOTABLE))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!t2->player)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!K_PlayerCanPunt(t2->player))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (t1->flags & MF_ELEMENTAL)
|
|
{
|
|
K_MakeObjectReappear(t1);
|
|
|
|
// copied from MT_ITEMCAPSULE
|
|
UINT8 i;
|
|
INT16 spacing = (t1->radius >> 1) / t1->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) * FRACUNIT;
|
|
rand_y = P_RandomRange(PR_ITEM_DEBRIS, -spacing, spacing) * FRACUNIT;
|
|
rand_x = P_RandomRange(PR_ITEM_DEBRIS, -spacing, spacing) * FRACUNIT;
|
|
mobj_t *puff = P_SpawnMobjFromMobj(
|
|
t1,
|
|
rand_x,
|
|
rand_y,
|
|
rand_z,
|
|
MT_SPINDASHDUST
|
|
);
|
|
|
|
puff->momz = puff->scale * P_MobjFlip(puff);
|
|
|
|
P_Thrust(puff, R_PointToAngle2(t2->x, t2->y, puff->x, puff->y), 3*puff->scale);
|
|
|
|
puff->momx += t2->momx / 2;
|
|
puff->momy += t2->momy / 2;
|
|
puff->momz += t2->momz / 2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
K_PuntHazard(t1, t2);
|
|
}
|
|
|
|
return true;
|
|
}
|