mirror of
https://git.do.srb2.org/STJr/SRB2.git
synced 2024-11-29 07:32:02 +00:00
4466 lines
129 KiB
C
4466 lines
129 KiB
C
// SONIC ROBO BLAST 2
|
|
//-----------------------------------------------------------------------------
|
|
// Copyright (C) 1993-1996 by id Software, Inc.
|
|
// Copyright (C) 1998-2000 by DooM Legacy Team.
|
|
// Copyright (C) 1999-2023 by Sonic Team Junior.
|
|
//
|
|
// 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"
|
|
|
|
// CTF player names
|
|
#define CTFTEAMCODE(pl) pl->ctfteam ? (pl->ctfteam == 1 ? "\x85" : "\x84") : ""
|
|
#define CTFTEAMENDCODE(pl) pl->ctfteam ? "\x80" : ""
|
|
|
|
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[secondarydisplayplayer])
|
|
I_Tactile2(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[secondarydisplayplayer])
|
|
I_Tactile2(ConstantForce, &RampQuake);
|
|
}
|
|
|
|
|
|
//
|
|
// GET STUFF
|
|
//
|
|
|
|
/** Makes sure all previous starposts are cleared.
|
|
* For instance, hitting starpost 5 will clear starposts 1 through 4, even if
|
|
* you didn't touch them. This is how the classic games work, although it can
|
|
* lead to bizarre situations on levels that allow you to make a circuit.
|
|
*
|
|
* \param postnum The number of the starpost just touched.
|
|
*/
|
|
void P_ClearStarPost(INT32 postnum)
|
|
{
|
|
thinker_t *th;
|
|
mobj_t *mo2;
|
|
|
|
// scan the thinkers
|
|
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
|
|
{
|
|
if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
|
|
continue;
|
|
|
|
mo2 = (mobj_t *)th;
|
|
|
|
if (mo2->type != MT_STARPOST)
|
|
continue;
|
|
|
|
if (mo2->health > postnum)
|
|
continue;
|
|
|
|
P_SetMobjState(mo2, mo2->info->seestate);
|
|
}
|
|
return;
|
|
}
|
|
|
|
//
|
|
// P_ResetStarposts
|
|
//
|
|
// Resets all starposts back to their spawn state, used on A_Mixup and some other things.
|
|
//
|
|
void P_ResetStarposts(void)
|
|
{
|
|
// Search through all the thinkers.
|
|
thinker_t *th;
|
|
mobj_t *post;
|
|
|
|
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
|
|
{
|
|
if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
|
|
continue;
|
|
|
|
post = (mobj_t *)th;
|
|
|
|
if (post->type != MT_STARPOST)
|
|
continue;
|
|
|
|
P_SetMobjState(post, post->info->spawnstate);
|
|
}
|
|
}
|
|
|
|
//
|
|
// P_CanPickupItem
|
|
//
|
|
// Returns true if the player is in a state where they can pick up items.
|
|
//
|
|
boolean P_CanPickupItem(player_t *player, boolean weapon)
|
|
{
|
|
if (!player->mo || player->mo->health <= 0)
|
|
return false;
|
|
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
{
|
|
if (weapon)
|
|
return false;
|
|
return P_CanPickupItem(&players[consoleplayer], true); // weapon is true to prevent infinite recursion if p1 is bot - doesn't occur in vanilla, but may be relevant for mods
|
|
}
|
|
|
|
if (player->powers[pw_flashing] > (flashingtics/4)*3 && player->powers[pw_flashing] < UINT16_MAX)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
boolean P_CanPickupEmblem(player_t *player, INT32 emblemID)
|
|
{
|
|
emblem_t *emblem = NULL;
|
|
|
|
if (emblemID < 0 || emblemID >= numemblems)
|
|
{
|
|
// Invalid emblem ID, can't pickup.
|
|
return false;
|
|
}
|
|
|
|
emblem = &emblemlocations[emblemID];
|
|
|
|
if (demoplayback)
|
|
{
|
|
// Never collect emblems in replays.
|
|
return false;
|
|
}
|
|
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
{
|
|
// Your little lap-dog can't grab these for you.
|
|
return false;
|
|
}
|
|
|
|
if (emblem->type == ET_SKIN)
|
|
{
|
|
INT32 skinnum = M_EmblemSkinNum(emblem);
|
|
|
|
if (player->skin != skinnum)
|
|
{
|
|
// Incorrect skin to pick up this emblem.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
boolean P_EmblemWasCollected(INT32 emblemID)
|
|
{
|
|
if (emblemID < 0 || emblemID >= numemblems)
|
|
{
|
|
// Invalid emblem ID, can't pickup.
|
|
return true;
|
|
}
|
|
|
|
if (shareEmblems && !serverGamedata->collected[emblemID])
|
|
{
|
|
// It can be worth collecting again if we're sharing emblems
|
|
// and the server doesn't have it.
|
|
return false;
|
|
}
|
|
|
|
return clientGamedata->collected[emblemID];
|
|
}
|
|
|
|
//
|
|
// P_DoNightsScore
|
|
//
|
|
// When you pick up some items in nights, it displays
|
|
// a score sign, and awards you some drill time.
|
|
//
|
|
void P_DoNightsScore(player_t *player)
|
|
{
|
|
mobj_t *dummymo;
|
|
|
|
if (player->exiting)
|
|
return; // Don't do any fancy shit for failures.
|
|
|
|
dummymo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z+player->mo->height/2, MT_NIGHTSCORE);
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
player = &players[consoleplayer];
|
|
|
|
if (G_IsSpecialStage(gamemap)) // Global link count? Maybe not a good idea...
|
|
{
|
|
INT32 i;
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
if (playeringame[i])
|
|
{
|
|
if (++players[i].linkcount > players[i].maxlink)
|
|
players[i].maxlink = players[i].linkcount;
|
|
players[i].linktimer = nightslinktics;
|
|
}
|
|
}
|
|
else // Individual link counts
|
|
{
|
|
if (++player->linkcount > player->maxlink)
|
|
player->maxlink = player->linkcount;
|
|
player->linktimer = nightslinktics;
|
|
}
|
|
|
|
// Award 10-100 score, doubled if bonus time is active
|
|
P_AddPlayerScore(player, min(player->linkcount,10)*(player->bonustime ? 20 : 10));
|
|
P_SetMobjState(dummymo, (player->bonustime ? dummymo->info->xdeathstate : dummymo->info->spawnstate) + min(player->linkcount,10)-1);
|
|
|
|
// Make objects slowly rise & scale up
|
|
dummymo->momz = FRACUNIT;
|
|
dummymo->fuse = 3*TICRATE;
|
|
dummymo->scalespeed = FRACUNIT/25;
|
|
dummymo->destscale = 2*FRACUNIT;
|
|
|
|
// Add extra values used for color variety
|
|
dummymo->extravalue1 = player->linkcount-1;
|
|
dummymo->extravalue2 = ((player->linkcount-1 >= 300) ? (player->linkcount-1 >= 600) ? 2 : 1 : 0);
|
|
}
|
|
|
|
//
|
|
// P_DoMatchSuper
|
|
//
|
|
// Checks if you have all 7 pw_emeralds, then turns you "super". =P
|
|
//
|
|
void P_DoMatchSuper(player_t *player)
|
|
{
|
|
UINT16 match_emeralds = player->powers[pw_emeralds];
|
|
boolean doteams = false;
|
|
int i;
|
|
|
|
// If this gametype has teams, check every player on your team for emeralds.
|
|
if (G_GametypeHasTeams())
|
|
{
|
|
doteams = true;
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
if (players[i].ctfteam == player->ctfteam)
|
|
match_emeralds |= players[i].powers[pw_emeralds];
|
|
}
|
|
|
|
if (!ALL7EMERALDS(match_emeralds))
|
|
return;
|
|
|
|
// Got 'em all? Turn "super"!
|
|
emeraldspawndelay = invulntics + 1;
|
|
player->powers[pw_emeralds] = 0;
|
|
player->powers[pw_invulnerability] = emeraldspawndelay;
|
|
player->powers[pw_sneakers] = emeraldspawndelay;
|
|
if (P_IsLocalPlayer(player) && !player->powers[pw_super])
|
|
{
|
|
S_StopMusic();
|
|
if (mariomode)
|
|
G_GhostAddColor(GHC_INVINCIBLE);
|
|
strlcpy(S_sfx[sfx_None].caption, "Invincibility", 14);
|
|
S_StartCaption(sfx_None, -1, player->powers[pw_invulnerability]);
|
|
S_ChangeMusicInternal((mariomode) ? "_minv" : "_inv", false);
|
|
}
|
|
|
|
// Also steal 50 points from every enemy, sealing your victory.
|
|
P_StealPlayerScore(player, 50);
|
|
|
|
// In a team game?
|
|
// Check everyone else on your team for emeralds, and turn those helpful assisting players invincible too.
|
|
if (doteams)
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
if (playeringame[i] && players[i].ctfteam == player->ctfteam
|
|
&& players[i].powers[pw_emeralds] != 0)
|
|
{
|
|
players[i].powers[pw_emeralds] = 0;
|
|
player->powers[pw_invulnerability] = invulntics + 1;
|
|
player->powers[pw_sneakers] = player->powers[pw_invulnerability];
|
|
if (P_IsLocalPlayer(player) && !player->powers[pw_super])
|
|
{
|
|
S_StopMusic();
|
|
if (mariomode)
|
|
G_GhostAddColor(GHC_INVINCIBLE);
|
|
strlcpy(S_sfx[sfx_None].caption, "Invincibility", 14);
|
|
S_StartCaption(sfx_None, -1, player->powers[pw_invulnerability]);
|
|
S_ChangeMusicInternal((mariomode) ? "_minv" : "_inv", false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 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;
|
|
INT32 i;
|
|
UINT8 elementalpierce;
|
|
|
|
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)
|
|
{
|
|
if (special->type == MT_FLINGEMERALD) // little hack here...
|
|
{ // flingemerald sprites are low to the ground, so extend collision radius down some.
|
|
if (toucher->z > (special->z + special->height))
|
|
return;
|
|
if (special->z - special->height > (toucher->z + toucher->height))
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
if (toucher->momz < 0) {
|
|
if (toucher->z + toucher->momz > special->z + special->height)
|
|
return;
|
|
} else if (toucher->z > special->z + special->height)
|
|
return;
|
|
if (toucher->momz > 0) {
|
|
if (toucher->z + toucher->height + toucher->momz < special->z)
|
|
return;
|
|
} else if (toucher->z + toucher->height < special->z)
|
|
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;
|
|
|
|
// 0 = none, 1 = elemental pierce, 2 = bubble bounce
|
|
elementalpierce = (((player->powers[pw_shield] & SH_NOSTACK) == SH_ELEMENTAL || (player->powers[pw_shield] & SH_NOSTACK) == SH_BUBBLEWRAP) && (player->pflags & PF_SHIELDABILITY)
|
|
? (((player->powers[pw_shield] & SH_NOSTACK) == SH_ELEMENTAL) ? 1 : 2)
|
|
: 0);
|
|
|
|
if ((special->flags & (MF_ENEMY|MF_BOSS)) && !(special->flags & MF_MISSILE))
|
|
{
|
|
////////////////////////////////////////////////////////
|
|
/////ENEMIES & BOSSES!!/////////////////////////////////
|
|
////////////////////////////////////////////////////////
|
|
|
|
switch (special->type)
|
|
{
|
|
case MT_BLACKEGGMAN:
|
|
{
|
|
P_DamageMobj(toucher, special, special, 1, 0); // ouch
|
|
return;
|
|
}
|
|
case MT_BIGMINE:
|
|
{
|
|
special->momx = toucher->momx/3;
|
|
special->momy = toucher->momy/3;
|
|
special->momz = toucher->momz/3;
|
|
toucher->momx /= -8;
|
|
toucher->momy /= -8;
|
|
toucher->momz /= -8;
|
|
special->flags &= ~MF_SPECIAL;
|
|
if (special->info->activesound)
|
|
S_StartSound(special, special->info->activesound);
|
|
P_SetTarget(&special->tracer, toucher);
|
|
player->homing = 0;
|
|
return;
|
|
}
|
|
case MT_GSNAPPER:
|
|
if (!elementalpierce
|
|
&& toucher->z < special->z + special->height
|
|
&& toucher->z + toucher->height > special->z
|
|
&& P_DamageMobj(toucher, special, special, 1, DMG_SPIKE))
|
|
return; // Can only hit snapper from above
|
|
break;
|
|
|
|
case MT_SPINCUSHION:
|
|
if (P_MobjFlip(toucher)*(toucher->z - (special->z + special->height/2)) > 0)
|
|
{
|
|
if (player->pflags & PF_BOUNCING)
|
|
{
|
|
toucher->momz = -toucher->momz;
|
|
P_DoAbilityBounce(player, false);
|
|
return;
|
|
}
|
|
else if (P_DamageMobj(toucher, special, special, 1, DMG_SPIKE))
|
|
return; // Cannot hit sharp from above
|
|
}
|
|
break;
|
|
case MT_FANG:
|
|
if (!player->powers[pw_flashing]
|
|
&& !(player->charability == CA_TWINSPIN && player->panim == PA_ABILITY)
|
|
&& !(player->charability2 == CA2_MELEE && player->panim == PA_ABILITY2))
|
|
{
|
|
if ((special->state == &states[S_FANG_BOUNCE3]
|
|
|| special->state == &states[S_FANG_BOUNCE4]
|
|
|| special->state == &states[S_FANG_PINCHBOUNCE3]
|
|
|| special->state == &states[S_FANG_PINCHBOUNCE4])
|
|
&& P_MobjFlip(special)*((special->z + special->height/2) - (toucher->z + toucher->height/2)) > (toucher->height/2))
|
|
{
|
|
P_DamageMobj(toucher, special, special, 1, 0);
|
|
P_SetTarget(&special->tracer, toucher);
|
|
|
|
if (special->state == &states[S_FANG_PINCHBOUNCE3]
|
|
|| special->state == &states[S_FANG_PINCHBOUNCE4])
|
|
P_SetMobjState(special, S_FANG_PINCHPATHINGSTART2);
|
|
else
|
|
{
|
|
var1 = var2 = 4;
|
|
A_Boss5ExtraRepeat(special);
|
|
P_SetMobjState(special, S_FANG_PATHINGCONT2); //S_FANG_PATHINGCONT1 if you want him to drop a bomb on the player
|
|
}
|
|
if (special->eflags & MFE_VERTICALFLIP)
|
|
special->z = toucher->z - special->height;
|
|
else
|
|
special->z = toucher->z + toucher->height;
|
|
return;
|
|
}
|
|
}
|
|
break;
|
|
case MT_PYREFLY:
|
|
if (special->extravalue2 == 2 && P_DamageMobj(player->mo, special, special, 1, DMG_FIRE))
|
|
return;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (P_PlayerCanDamage(player, special)) // Do you possess the ability to subdue the object?
|
|
{
|
|
if (special->type == MT_PTERABYTE && special->target == player->mo && special->extravalue1 == 1)
|
|
return; // Can't hurt a Pterabyte if it's trying to pick you up
|
|
|
|
if ((P_MobjFlip(toucher)*toucher->momz < 0) && (elementalpierce != 1) && (!(player->powers[pw_strong] & STR_HEAVY)))
|
|
{
|
|
fixed_t setmomz = -toucher->momz; // Store this, momz get changed by P_DoJump within P_DoBubbleBounce
|
|
|
|
if (elementalpierce == 2) // Reset bubblewrap, part 1
|
|
P_DoBubbleBounce(player);
|
|
toucher->momz = setmomz;
|
|
if (elementalpierce == 2) // Reset bubblewrap, part 2
|
|
{
|
|
boolean underwater = toucher->eflags & MFE_UNDERWATER;
|
|
|
|
if (underwater)
|
|
toucher->momz /= 2;
|
|
toucher->momz -= (toucher->momz/(underwater ? 8 : 4)); // Cap the height!
|
|
}
|
|
}
|
|
if (player->pflags & PF_BOUNCING)
|
|
P_DoAbilityBounce(player, false);
|
|
if (special->info->spawnhealth > 1) // Multi-hit? Bounce back!
|
|
{
|
|
toucher->momx = -toucher->momx;
|
|
toucher->momy = -toucher->momy;
|
|
if (player->charability == CA_FLY && player->panim == PA_ABILITY)
|
|
toucher->momz = -toucher->momz/2;
|
|
else if (player->pflags & PF_GLIDING && !P_IsObjectOnGround(toucher))
|
|
{
|
|
player->pflags &= ~(PF_GLIDING|PF_JUMPED|PF_NOJUMPDAMAGE);
|
|
P_SetPlayerMobjState(toucher, S_PLAY_FALL);
|
|
toucher->momz += P_MobjFlip(toucher) * (player->speed >> 3);
|
|
toucher->momx = 7*toucher->momx>>3;
|
|
toucher->momy = 7*toucher->momy>>3;
|
|
}
|
|
else if ((player->powers[pw_strong] & STR_DASH) && player->panim == PA_DASH)
|
|
P_DoPlayerPain(player, special, special);
|
|
}
|
|
P_DamageMobj(special, toucher, toucher, 1, 0);
|
|
if (player->charability == CA_TWINSPIN && player->panim == PA_ABILITY)
|
|
P_TwinSpinRejuvenate(player, player->thokitem);
|
|
}
|
|
else
|
|
{
|
|
if (special->type == MT_PTERABYTE && special->target == player->mo)
|
|
return; // Don't hurt the player you're trying to grab
|
|
|
|
P_DamageMobj(toucher, special, special, 1, 0);
|
|
}
|
|
|
|
return;
|
|
}
|
|
else if (special->flags & MF_FIRE)
|
|
{
|
|
P_DamageMobj(toucher, special, special, 1, DMG_FIRE);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// We now identify by object type, not sprite! Tails 04-11-2001
|
|
switch (special->type)
|
|
{
|
|
// ***************************************** //
|
|
// Rings, coins, spheres, weapon panels, etc //
|
|
// ***************************************** //
|
|
case MT_REDTEAMRING:
|
|
if (player->ctfteam != 1)
|
|
return;
|
|
/* FALLTHRU */
|
|
case MT_BLUETEAMRING: // Yes, I'm lazy. Oh well, deal with it.
|
|
if (special->type == MT_BLUETEAMRING && player->ctfteam != 2)
|
|
return;
|
|
/* FALLTHRU */
|
|
case MT_RING:
|
|
case MT_FLINGRING:
|
|
case MT_COIN:
|
|
case MT_FLINGCOIN:
|
|
case MT_NIGHTSSTAR:
|
|
if (!(P_CanPickupItem(player, false)) && !(special->flags2 & MF2_NIGHTSPULL))
|
|
return;
|
|
|
|
special->momx = special->momy = special->momz = 0;
|
|
P_GivePlayerRings(player, 1);
|
|
|
|
if ((maptol & TOL_NIGHTS) && special->type != MT_FLINGRING && special->type != MT_FLINGCOIN)
|
|
P_DoNightsScore(player);
|
|
break;
|
|
case MT_BLUESPHERE:
|
|
case MT_FLINGBLUESPHERE:
|
|
case MT_NIGHTSCHIP:
|
|
case MT_FLINGNIGHTSCHIP:
|
|
if (!(P_CanPickupItem(player, false)) && !(special->flags2 & MF2_NIGHTSPULL))
|
|
return;
|
|
|
|
special->momx = special->momy = special->momz = 0;
|
|
P_GivePlayerSpheres(player, 1);
|
|
|
|
if (special->type == MT_BLUESPHERE || special->type == MT_FLINGBLUESPHERE)
|
|
{
|
|
special->destscale = ((player->powers[pw_carry] == CR_NIGHTSMODE) ? 4 : 2)*special->scale;
|
|
if (states[special->info->deathstate].tics > 0)
|
|
special->scalespeed = FixedDiv(FixedDiv(special->destscale, special->scale), states[special->info->deathstate].tics<<FRACBITS);
|
|
else
|
|
special->scalespeed = 4*FRACUNIT/5;
|
|
}
|
|
|
|
if (maptol & TOL_NIGHTS)
|
|
P_DoNightsScore(player);
|
|
break;
|
|
case MT_BOMBSPHERE:
|
|
if (!(P_CanPickupItem(player, false)) && !(special->flags2 & MF2_NIGHTSPULL))
|
|
return;
|
|
|
|
special->momx = special->momy = special->momz = 0;
|
|
P_DamageMobj(toucher, special, special, 1, 0);
|
|
break;
|
|
case MT_AUTOPICKUP:
|
|
case MT_BOUNCEPICKUP:
|
|
case MT_SCATTERPICKUP:
|
|
case MT_GRENADEPICKUP:
|
|
case MT_EXPLODEPICKUP:
|
|
case MT_RAILPICKUP:
|
|
if (!(P_CanPickupItem(player, true)))
|
|
return;
|
|
|
|
// Give the power and ringweapon
|
|
if (special->info->mass >= pw_infinityring && special->info->mass <= pw_railring)
|
|
{
|
|
INT32 pindex = special->info->mass - (INT32)pw_infinityring;
|
|
|
|
player->powers[special->info->mass] += (UINT16)special->reactiontime;
|
|
player->ringweapons |= 1 << (pindex-1);
|
|
|
|
if (player->powers[special->info->mass] > rw_maximums[pindex])
|
|
player->powers[special->info->mass] = rw_maximums[pindex];
|
|
}
|
|
break;
|
|
|
|
// Ammo pickups
|
|
case MT_INFINITYRING:
|
|
case MT_AUTOMATICRING:
|
|
case MT_BOUNCERING:
|
|
case MT_SCATTERRING:
|
|
case MT_GRENADERING:
|
|
case MT_EXPLOSIONRING:
|
|
case MT_RAILRING:
|
|
if (!(P_CanPickupItem(player, true)))
|
|
return;
|
|
|
|
if (special->info->mass >= pw_infinityring && special->info->mass <= pw_railring)
|
|
{
|
|
INT32 pindex = special->info->mass - (INT32)pw_infinityring;
|
|
|
|
player->powers[special->info->mass] += (UINT16)special->health;
|
|
if (player->powers[special->info->mass] > rw_maximums[pindex])
|
|
player->powers[special->info->mass] = rw_maximums[pindex];
|
|
}
|
|
break;
|
|
|
|
// ***************************** //
|
|
// Gameplay related collectibles //
|
|
// ***************************** //
|
|
// Special Stage Token
|
|
case MT_TOKEN:
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
return;
|
|
|
|
P_AddPlayerScore(player, 1000);
|
|
|
|
if (!(gametyperules & GTR_SPECIALSTAGES) || modeattacking) // score only?
|
|
{
|
|
S_StartSound(toucher, sfx_chchng);
|
|
break;
|
|
}
|
|
|
|
tokenlist += special->health;
|
|
|
|
if (ALL7EMERALDS(emeralds)) // Got all 7
|
|
{
|
|
if (continuesInSession)
|
|
{
|
|
player->continues += 1;
|
|
player->gotcontinue = true;
|
|
if (P_IsLocalPlayer(player))
|
|
S_StartSound(NULL, sfx_s3kac);
|
|
else
|
|
S_StartSound(toucher, sfx_chchng);
|
|
}
|
|
else
|
|
{
|
|
P_GiveCoopLives(player, 1, true); // if continues are disabled, a life is a reasonable substitute
|
|
S_StartSound(toucher, sfx_chchng);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
token++;
|
|
S_StartSound(toucher, sfx_token);
|
|
}
|
|
|
|
break;
|
|
|
|
// Emerald Hunt
|
|
case MT_EMERHUNT:
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
return;
|
|
|
|
if (hunt1 == special)
|
|
hunt1 = NULL;
|
|
else if (hunt2 == special)
|
|
hunt2 = NULL;
|
|
else if (hunt3 == special)
|
|
hunt3 = NULL;
|
|
|
|
if (!hunt1 && !hunt2 && !hunt3)
|
|
{
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator)
|
|
continue;
|
|
|
|
players[i].exiting = (14*TICRATE)/5 + 1;
|
|
}
|
|
//S_StartSound(NULL, sfx_lvpass);
|
|
}
|
|
break;
|
|
|
|
// Collectible emeralds
|
|
case MT_EMERALD1:
|
|
case MT_EMERALD2:
|
|
case MT_EMERALD3:
|
|
case MT_EMERALD4:
|
|
case MT_EMERALD5:
|
|
case MT_EMERALD6:
|
|
case MT_EMERALD7:
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
return;
|
|
|
|
if (special->threshold)
|
|
{
|
|
player->powers[pw_emeralds] |= special->info->speed;
|
|
P_DoMatchSuper(player);
|
|
}
|
|
else
|
|
{
|
|
emeralds |= special->info->speed;
|
|
stagefailed = false;
|
|
}
|
|
|
|
if (special->target && special->target->type == MT_EMERALDSPAWN)
|
|
{
|
|
if (special->target->target)
|
|
P_SetTarget(&special->target->target, NULL);
|
|
|
|
special->target->threshold = 0;
|
|
|
|
P_SetTarget(&special->target, NULL);
|
|
}
|
|
break;
|
|
|
|
// Power stones / Match emeralds
|
|
case MT_FLINGEMERALD:
|
|
if (!(P_CanPickupItem(player, true)) || player->tossdelay)
|
|
return;
|
|
|
|
player->powers[pw_emeralds] |= special->threshold;
|
|
P_DoMatchSuper(player);
|
|
break;
|
|
|
|
// Secret emblem thingy
|
|
case MT_EMBLEM:
|
|
{
|
|
const boolean toucherIsServer = ((player - players) == serverplayer);
|
|
const boolean consoleIsServer = (consoleplayer == serverplayer);
|
|
boolean prevCollected = false;
|
|
|
|
if ((special->flags2 & MF2_NIGHTSPULL)
|
|
&& (toucher == special->tracer))
|
|
{
|
|
// Since collecting may not remove the object,
|
|
// we need to manually stop it from chasing.
|
|
P_SetTarget(&special->tracer, NULL);
|
|
special->flags2 &= ~MF2_NIGHTSPULL;
|
|
special->movefactor = 0;
|
|
special->momx = special->momy = special->momz = 0;
|
|
}
|
|
|
|
if (!P_CanPickupEmblem(player, special->health - 1))
|
|
{
|
|
return;
|
|
}
|
|
|
|
prevCollected = P_EmblemWasCollected(special->health - 1);
|
|
|
|
if (toucherIsServer || shareEmblems)
|
|
{
|
|
serverGamedata->collected[special->health-1] = true;
|
|
M_SilentUpdateUnlockablesAndEmblems(serverGamedata);
|
|
}
|
|
|
|
if (P_IsLocalPlayer(player) || (consoleIsServer && shareEmblems))
|
|
{
|
|
clientGamedata->collected[special->health-1] = true;
|
|
M_UpdateUnlockablesAndExtraEmblems(clientGamedata);
|
|
G_SaveGameData(clientGamedata);
|
|
}
|
|
|
|
if (netgame)
|
|
{
|
|
// This always spawns the object to prevent mobjnum issues,
|
|
// but makes the effect invisible to whoever it doesn't matter to.
|
|
mobj_t *spark = P_SpawnMobjFromMobj(special, 0, 0, 0, MT_SPARK);
|
|
|
|
if (prevCollected == false && P_EmblemWasCollected(special->health - 1) == true)
|
|
{
|
|
// Play the sound if it was collected.
|
|
S_StartSound((shareEmblems ? NULL : special), special->info->deathsound);
|
|
}
|
|
else
|
|
{
|
|
// We didn't collect it, make it invisible to us.
|
|
spark->flags2 |= MF2_DONTDRAW;
|
|
}
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
if (prevCollected == false && P_EmblemWasCollected(special->health - 1) == true)
|
|
{
|
|
// Disappear when collecting for local games.
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// CTF Flags
|
|
case MT_REDFLAG:
|
|
case MT_BLUEFLAG:
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
return;
|
|
if (player->powers[pw_flashing] || player->tossdelay)
|
|
return;
|
|
if (!special->spawnpoint)
|
|
return;
|
|
if (special->fuse == 1)
|
|
return;
|
|
// if (special->momz > 0)
|
|
// return;
|
|
{
|
|
UINT8 flagteam = (special->type == MT_REDFLAG) ? 1 : 2;
|
|
sectorspecialflags_t specialflag = (special->type == MT_REDFLAG) ? SSF_REDTEAMBASE : SSF_BLUETEAMBASE;
|
|
const char *flagtext;
|
|
char flagcolor;
|
|
char plname[MAXPLAYERNAME+4];
|
|
|
|
if (special->type == MT_REDFLAG)
|
|
{
|
|
flagtext = M_GetText("Red flag");
|
|
flagcolor = '\x85';
|
|
}
|
|
else
|
|
{
|
|
flagtext = M_GetText("Blue flag");
|
|
flagcolor = '\x84';
|
|
}
|
|
snprintf(plname, sizeof(plname), "%s%s%s",
|
|
CTFTEAMCODE(player),
|
|
player_names[player - players],
|
|
CTFTEAMENDCODE(player));
|
|
|
|
if (player->ctfteam == flagteam) // Player is on the same team as the flag
|
|
{
|
|
// Ignore height, only check x/y for now
|
|
// avoids stupid problems with some flags constantly returning
|
|
if (special->x>>FRACBITS != special->spawnpoint->x
|
|
|| special->y>>FRACBITS != special->spawnpoint->y)
|
|
{
|
|
special->fuse = 1;
|
|
special->flags2 |= MF2_JUSTATTACKED;
|
|
|
|
if (!P_PlayerTouchingSectorSpecialFlag(player, specialflag))
|
|
{
|
|
CONS_Printf(M_GetText("%s returned the %c%s%c to base.\n"), plname, flagcolor, flagtext, 0x80);
|
|
|
|
// The fuse code plays this sound effect
|
|
//if (players[consoleplayer].ctfteam == player->ctfteam)
|
|
// S_StartSound(NULL, sfx_hoop1);
|
|
}
|
|
}
|
|
}
|
|
else if (player->ctfteam) // Player is on the other team (and not a spectator)
|
|
{
|
|
UINT16 flagflag = (special->type == MT_REDFLAG) ? GF_REDFLAG : GF_BLUEFLAG;
|
|
mobj_t **flagmobj = (special->type == MT_REDFLAG) ? &redflag : &blueflag;
|
|
|
|
if (player->powers[pw_super])
|
|
return;
|
|
|
|
player->gotflag |= flagflag;
|
|
CONS_Printf(M_GetText("%s picked up the %c%s%c!\n"), plname, flagcolor, flagtext, 0x80);
|
|
(*flagmobj) = NULL;
|
|
// code for dealing with abilities is handled elsewhere now
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
|
|
// ********************************** //
|
|
// NiGHTS gameplay items and powerups //
|
|
// ********************************** //
|
|
case MT_NIGHTSDRONE:
|
|
{
|
|
boolean spec = G_IsSpecialStage(gamemap);
|
|
boolean cangiveemmy = false;
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
return;
|
|
if (player->exiting)
|
|
return;
|
|
if (player->bonustime)
|
|
{
|
|
if (spec) //After-mare bonus time/emerald reward in special stages.
|
|
{
|
|
// only allow the player with the emerald in-hand to leave.
|
|
if (toucher->tracer
|
|
&& toucher->tracer->type == MT_GOTEMERALD)
|
|
{}
|
|
else // Make sure that SOMEONE has the emerald, at least!
|
|
{
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
if (playeringame[i] && players[i].playerstate == PST_LIVE
|
|
&& players[i].mo->tracer
|
|
&& players[i].mo->tracer->type == MT_GOTEMERALD)
|
|
return;
|
|
// Well no one has an emerald, so exit anyway!
|
|
}
|
|
cangiveemmy = true;
|
|
// Don't play Ideya sound in special stage mode
|
|
}
|
|
else
|
|
S_StartSound(toucher, special->info->activesound);
|
|
}
|
|
else //Initial transformation. Don't allow second chances in special stages!
|
|
{
|
|
if (player->powers[pw_carry] == CR_NIGHTSMODE)
|
|
return;
|
|
|
|
S_StartSound(toucher, sfx_supert);
|
|
}
|
|
P_SwitchSpheresBonusMode(false);
|
|
if (!(netgame || multiplayer) && !(player->powers[pw_carry] == CR_NIGHTSMODE))
|
|
P_SetTarget(&special->tracer, toucher);
|
|
P_SetTarget(&player->drone, special); // Mark the player as 'center into the drone'
|
|
P_NightserizePlayer(player, special->health); // Transform!
|
|
if (!spec)
|
|
{
|
|
if (toucher->tracer) // Move the Ideya to an anchor!
|
|
{
|
|
mobj_t *orbittarget = special->target ? special->target : special;
|
|
mobj_t *hnext = orbittarget->hnext, *anchorpoint = NULL, *anchorpoint2 = NULL;
|
|
mobj_t *mo2;
|
|
thinker_t *th;
|
|
|
|
// The player might have two Ideyas: toucher->tracer and toucher->tracer->hnext
|
|
// so handle their anchorpoints accordingly.
|
|
// scan the thinkers to find the corresponding anchorpoint
|
|
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
|
|
{
|
|
if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
|
|
continue;
|
|
|
|
mo2 = (mobj_t *)th;
|
|
|
|
if (mo2->type != MT_IDEYAANCHOR)
|
|
continue;
|
|
|
|
if (mo2->health == toucher->tracer->health) // do ideya numberes match?
|
|
anchorpoint = mo2;
|
|
else if (toucher->tracer->hnext && mo2->health == toucher->tracer->hnext->health)
|
|
anchorpoint2 = mo2;
|
|
|
|
if ((!toucher->tracer->hnext && anchorpoint)
|
|
|| (toucher->tracer->hnext && anchorpoint && anchorpoint2))
|
|
break;
|
|
}
|
|
|
|
if (anchorpoint)
|
|
{
|
|
toucher->tracer->flags |= MF_GRENADEBOUNCE; // custom radius factors
|
|
toucher->tracer->threshold = 8 << 20; // X factor 0, Y factor 0, Z factor 8
|
|
}
|
|
|
|
if (anchorpoint2)
|
|
{
|
|
toucher->tracer->hnext->flags |= MF_GRENADEBOUNCE; // custom radius factors
|
|
toucher->tracer->hnext->threshold = 8 << 20; // X factor 0, Y factor 0, Z factor 8
|
|
}
|
|
|
|
P_SetTarget(&orbittarget->hnext, toucher->tracer);
|
|
if (!orbittarget->hnext->hnext)
|
|
P_SetTarget(&orbittarget->hnext->hnext, hnext); // Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo.
|
|
else
|
|
P_SetTarget(&orbittarget->hnext->hnext->target, anchorpoint2 ? anchorpoint2 : orbittarget);
|
|
P_SetTarget(&orbittarget->hnext->target, anchorpoint ? anchorpoint : orbittarget);
|
|
P_SetTarget(&toucher->tracer, NULL);
|
|
|
|
if (hnext)
|
|
{
|
|
orbittarget->hnext->extravalue1 = (angle_t)(hnext->extravalue1 - 72*ANG1);
|
|
if (orbittarget->hnext->extravalue1 > hnext->extravalue1)
|
|
orbittarget->hnext->extravalue1 -= (72*ANG1)/orbittarget->hnext->extravalue1;
|
|
}
|
|
}
|
|
if (player->exiting) // ...then move it back?
|
|
{
|
|
mobj_t *hnext = special->target ? special->target : special; // goalpost
|
|
while ((hnext = hnext->hnext))
|
|
{
|
|
hnext->flags &= ~MF_GRENADEBOUNCE;
|
|
hnext->threshold = 0;
|
|
P_SetTarget(&hnext->target, toucher);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!cangiveemmy)
|
|
return;
|
|
|
|
if (player->exiting)
|
|
P_GiveEmerald(false);
|
|
else if (player->mo->tracer && player->mare)
|
|
{
|
|
P_KillMobj(toucher->tracer, NULL, NULL, 0); // No emerald for you just yet!
|
|
S_StartSound(NULL, sfx_ghosty);
|
|
special->flags2 |= MF2_DONTDRAW;
|
|
}
|
|
|
|
return;
|
|
}
|
|
case MT_NIGHTSLOOPHELPER:
|
|
// One second delay
|
|
if (special->fuse < toucher->fuse - TICRATE)
|
|
{
|
|
thinker_t *th;
|
|
mobj_t *mo2;
|
|
INT32 count;
|
|
fixed_t x,y,z, gatherradius;
|
|
angle_t d;
|
|
statenum_t sparklestate = S_NULL;
|
|
|
|
if (special->target != toucher) // These ain't your helpers, pal!
|
|
return;
|
|
|
|
x = special->x>>FRACBITS;
|
|
y = special->y>>FRACBITS;
|
|
z = special->z>>FRACBITS;
|
|
count = 1;
|
|
|
|
// scan the remaining thinkers
|
|
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
|
|
{
|
|
if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
|
|
continue;
|
|
|
|
mo2 = (mobj_t *)th;
|
|
|
|
if (mo2 == special)
|
|
continue;
|
|
|
|
// Not our stuff!
|
|
if (mo2->target != toucher)
|
|
continue;
|
|
|
|
if (mo2->type == MT_NIGHTSPARKLE)
|
|
mo2->tics = 1;
|
|
else if (mo2->type == MT_NIGHTSLOOPHELPER)
|
|
{
|
|
if (mo2->fuse >= special->fuse)
|
|
{
|
|
count++;
|
|
x += mo2->x>>FRACBITS;
|
|
y += mo2->y>>FRACBITS;
|
|
z += mo2->z>>FRACBITS;
|
|
}
|
|
P_RemoveMobj(mo2);
|
|
}
|
|
}
|
|
x = (x/count)<<FRACBITS;
|
|
y = (y/count)<<FRACBITS;
|
|
z = (z/count)<<FRACBITS;
|
|
gatherradius = P_AproxDistance(P_AproxDistance(special->x - x, special->y - y), special->z - z);
|
|
P_RemoveMobj(special);
|
|
|
|
if (player->powers[pw_nights_superloop])
|
|
{
|
|
gatherradius *= 2;
|
|
sparklestate = mobjinfo[MT_NIGHTSPARKLE].seestate;
|
|
}
|
|
|
|
if (gatherradius < 30*FRACUNIT) // Player is probably just sitting there.
|
|
return;
|
|
|
|
for (d = 0; d < 16; d++)
|
|
P_SpawnParaloop(x, y, z, gatherradius, 16, MT_NIGHTSPARKLE, sparklestate, d*ANGLE_22h, false);
|
|
|
|
S_StartSound(toucher, sfx_prloop);
|
|
|
|
// Now we RE-scan all the thinkers to find close objects to pull
|
|
// in from the paraloop. Isn't this just so efficient?
|
|
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
|
|
{
|
|
if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
|
|
continue;
|
|
|
|
mo2 = (mobj_t *)th;
|
|
|
|
if (P_AproxDistance(P_AproxDistance(mo2->x - x, mo2->y - y), mo2->z - z) > gatherradius)
|
|
continue;
|
|
|
|
if (mo2->flags & MF_SHOOTABLE)
|
|
{
|
|
P_DamageMobj(mo2, toucher, toucher, 1, 0);
|
|
continue;
|
|
}
|
|
|
|
// Make these APPEAR!
|
|
// Tails 12-15-2003
|
|
if (mo2->flags & MF_NIGHTSITEM)
|
|
{
|
|
// Requires Bonus Time
|
|
if ((mo2->flags2 & MF2_STRONGBOX) && !player->bonustime)
|
|
continue;
|
|
|
|
if (!(mo2->flags & MF_SPECIAL) && mo2->health)
|
|
{
|
|
mo2->flags2 &= ~MF2_DONTDRAW;
|
|
mo2->flags |= MF_SPECIAL;
|
|
mo2->flags &= ~MF_NIGHTSITEM;
|
|
S_StartSound(toucher, sfx_hidden);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!(mo2->type == MT_RING || mo2->type == MT_COIN
|
|
|| mo2->type == MT_BLUESPHERE || mo2->type == MT_BOMBSPHERE
|
|
|| mo2->type == MT_NIGHTSCHIP || mo2->type == MT_NIGHTSSTAR
|
|
|| ((mo2->type == MT_EMBLEM) && (mo2->reactiontime & GE_NIGHTSPULL) && P_CanPickupEmblem(player, mo2->health - 1) && !P_EmblemWasCollected(mo2->health - 1))))
|
|
continue;
|
|
|
|
// Yay! The thing's in reach! Pull it in!
|
|
mo2->flags |= MF_NOCLIP|MF_NOCLIPHEIGHT;
|
|
mo2->flags2 |= MF2_NIGHTSPULL;
|
|
// New NiGHTS attract speed dummied out because the older behavior
|
|
// is exploited as a mechanic. Uncomment to enable.
|
|
mo2->movefactor = 0; // 32*FRACUNIT; // initialize the NightsItemChase timer
|
|
P_SetTarget(&mo2->tracer, toucher);
|
|
}
|
|
}
|
|
return;
|
|
case MT_EGGCAPSULE:
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
return;
|
|
|
|
// make sure everything is as it should be, THEN take rings from players in special stages
|
|
if (player->powers[pw_carry] == CR_NIGHTSMODE && !toucher->target)
|
|
return;
|
|
|
|
if (toucher->tracer && toucher->tracer->health > 0)
|
|
return; // Don't have multiple ideya, unless it's the first one given (health = 0)
|
|
|
|
if (player->mare != special->threshold) // wrong mare
|
|
return;
|
|
|
|
if (special->reactiontime > 0) // capsule already has a player attacking it, ignore
|
|
return;
|
|
|
|
if (G_IsSpecialStage(gamemap) && !player->exiting)
|
|
{ // In special stages, share spheres. Everyone gives up theirs to the player who touched the capsule
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
if (playeringame[i] && (&players[i] != player) && players[i].spheres > 0)
|
|
{
|
|
player->spheres += players[i].spheres;
|
|
players[i].spheres = 0;
|
|
}
|
|
}
|
|
|
|
if (player->spheres <= 0 || player->exiting)
|
|
return;
|
|
|
|
// Mark the player as 'pull into the capsule'
|
|
P_SetTarget(&player->capsule, special);
|
|
special->reactiontime = (player-players)+1;
|
|
P_SetTarget(&special->target, NULL);
|
|
|
|
// Clear text
|
|
player->texttimer = 0;
|
|
return;
|
|
case MT_NIGHTSBUMPER:
|
|
// Don't trigger if the stage is ended/failed
|
|
if (player->exiting)
|
|
return;
|
|
|
|
if (player->bumpertime <= (TICRATE/2)-5)
|
|
{
|
|
S_StartSound(toucher, special->info->seesound);
|
|
if (player->powers[pw_carry] == CR_NIGHTSMODE)
|
|
{
|
|
player->bumpertime = TICRATE/2;
|
|
if (special->threshold > 0)
|
|
player->flyangle = (special->threshold*30)-1;
|
|
else
|
|
player->flyangle = special->threshold;
|
|
|
|
player->speed = FixedMul(special->info->speed, special->scale);
|
|
P_SetTarget(&player->mo->hnext, special); // Reference bumper for position correction on next tic
|
|
}
|
|
else // More like a spring
|
|
{
|
|
angle_t fa;
|
|
fixed_t xspeed, yspeed;
|
|
const fixed_t speed = FixedMul(FixedDiv(special->info->speed*FRACUNIT,75*FRACUNIT), FixedSqrt(FixedMul(toucher->scale,special->scale)));
|
|
|
|
player->bumpertime = TICRATE/2;
|
|
|
|
P_UnsetThingPosition(toucher);
|
|
toucher->x = special->x;
|
|
toucher->y = special->y;
|
|
P_SetThingPosition(toucher);
|
|
toucher->z = special->z+(special->height/4);
|
|
|
|
if (special->threshold > 0)
|
|
fa = (FixedAngle(((special->threshold*30)-1)*FRACUNIT)>>ANGLETOFINESHIFT) & FINEMASK;
|
|
else
|
|
fa = 0;
|
|
|
|
xspeed = FixedMul(FINECOSINE(fa),speed);
|
|
yspeed = FixedMul(FINESINE(fa),speed);
|
|
|
|
P_InstaThrust(toucher, special->angle, xspeed/10);
|
|
toucher->momz = yspeed/11;
|
|
|
|
toucher->angle = special->angle;
|
|
|
|
P_SetPlayerAngle(player, toucher->angle);
|
|
|
|
P_ResetPlayer(player);
|
|
|
|
P_SetPlayerMobjState(toucher, S_PLAY_FALL);
|
|
}
|
|
}
|
|
return;
|
|
case MT_NIGHTSSUPERLOOP:
|
|
if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
|
|
return;
|
|
if (!G_IsSpecialStage(gamemap))
|
|
player->powers[pw_nights_superloop] = (UINT16)special->info->speed;
|
|
else
|
|
{
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
if (playeringame[i] && players[i].powers[pw_carry] == CR_NIGHTSMODE)
|
|
players[i].powers[pw_nights_superloop] = (UINT16)special->info->speed;
|
|
if (special->info->deathsound != sfx_None)
|
|
S_StartSound(NULL, special->info->deathsound);
|
|
}
|
|
|
|
// CECHO showing you what this item is
|
|
if (player == &players[displayplayer] || G_IsSpecialStage(gamemap))
|
|
{
|
|
HU_SetCEchoFlags(V_AUTOFADEOUT);
|
|
HU_SetCEchoDuration(4);
|
|
HU_DoCEcho(M_GetText("\\\\\\\\\\\\\\\\Super Paraloop"));
|
|
}
|
|
break;
|
|
case MT_NIGHTSDRILLREFILL:
|
|
if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
|
|
return;
|
|
if (!G_IsSpecialStage(gamemap))
|
|
player->drillmeter = special->info->speed;
|
|
else
|
|
{
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
if (playeringame[i] && players[i].powers[pw_carry] == CR_NIGHTSMODE)
|
|
players[i].drillmeter = special->info->speed;
|
|
if (special->info->deathsound != sfx_None)
|
|
S_StartSound(NULL, special->info->deathsound);
|
|
}
|
|
|
|
// CECHO showing you what this item is
|
|
if (player == &players[displayplayer] || G_IsSpecialStage(gamemap))
|
|
{
|
|
HU_SetCEchoFlags(V_AUTOFADEOUT);
|
|
HU_SetCEchoDuration(4);
|
|
HU_DoCEcho(M_GetText("\\\\\\\\\\\\\\\\Drill Refill"));
|
|
}
|
|
break;
|
|
case MT_NIGHTSHELPER:
|
|
if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
|
|
return;
|
|
if (!G_IsSpecialStage(gamemap))
|
|
{
|
|
// A flicky orbits us now
|
|
mobj_t *flickyobj = P_SpawnMobj(toucher->x, toucher->y, toucher->z + toucher->info->height, MT_NIGHTOPIANHELPER);
|
|
P_SetTarget(&flickyobj->target, toucher);
|
|
|
|
player->powers[pw_nights_helper] = (UINT16)special->info->speed;
|
|
}
|
|
else
|
|
{
|
|
mobj_t *flickyobj;
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
if (playeringame[i] && players[i].mo && players[i].powers[pw_carry] == CR_NIGHTSMODE) {
|
|
players[i].powers[pw_nights_helper] = (UINT16)special->info->speed;
|
|
flickyobj = P_SpawnMobj(players[i].mo->x, players[i].mo->y, players[i].mo->z + players[i].mo->info->height, MT_NIGHTOPIANHELPER);
|
|
P_SetTarget(&flickyobj->target, players[i].mo);
|
|
}
|
|
if (special->info->deathsound != sfx_None)
|
|
S_StartSound(NULL, special->info->deathsound);
|
|
}
|
|
|
|
// CECHO showing you what this item is
|
|
if (player == &players[displayplayer] || G_IsSpecialStage(gamemap))
|
|
{
|
|
HU_SetCEchoFlags(V_AUTOFADEOUT);
|
|
HU_SetCEchoDuration(4);
|
|
HU_DoCEcho(M_GetText("\\\\\\\\\\\\\\\\Nightopian Helper"));
|
|
}
|
|
break;
|
|
case MT_NIGHTSEXTRATIME:
|
|
if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
|
|
return;
|
|
if (!G_IsSpecialStage(gamemap))
|
|
{
|
|
player->nightstime += special->info->speed;
|
|
player->startedtime += special->info->speed;
|
|
player->lapstartedtime += special->info->speed;
|
|
P_RestoreMusic(player);
|
|
}
|
|
else
|
|
{
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
if (playeringame[i] && players[i].powers[pw_carry] == CR_NIGHTSMODE)
|
|
{
|
|
players[i].nightstime += special->info->speed;
|
|
players[i].startedtime += special->info->speed;
|
|
players[i].lapstartedtime += special->info->speed;
|
|
P_RestoreMusic(&players[i]);
|
|
}
|
|
if (special->info->deathsound != sfx_None)
|
|
S_StartSound(NULL, special->info->deathsound);
|
|
}
|
|
|
|
// CECHO showing you what this item is
|
|
if (player == &players[displayplayer] || G_IsSpecialStage(gamemap))
|
|
{
|
|
HU_SetCEchoFlags(V_AUTOFADEOUT);
|
|
HU_SetCEchoDuration(4);
|
|
HU_DoCEcho(M_GetText("\\\\\\\\\\\\\\\\Extra Time"));
|
|
}
|
|
break;
|
|
case MT_NIGHTSLINKFREEZE:
|
|
if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
|
|
return;
|
|
if (!G_IsSpecialStage(gamemap))
|
|
{
|
|
player->powers[pw_nights_linkfreeze] = (UINT16)special->info->speed;
|
|
player->linktimer = nightslinktics;
|
|
}
|
|
else
|
|
{
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
if (playeringame[i] && players[i].powers[pw_carry] == CR_NIGHTSMODE)
|
|
{
|
|
players[i].powers[pw_nights_linkfreeze] += (UINT16)special->info->speed;
|
|
players[i].linktimer = nightslinktics;
|
|
}
|
|
if (special->info->deathsound != sfx_None)
|
|
S_StartSound(NULL, special->info->deathsound);
|
|
}
|
|
|
|
// CECHO showing you what this item is
|
|
if (player == &players[displayplayer] || G_IsSpecialStage(gamemap))
|
|
{
|
|
HU_SetCEchoFlags(V_AUTOFADEOUT);
|
|
HU_SetCEchoDuration(4);
|
|
HU_DoCEcho(M_GetText("\\\\\\\\\\\\\\\\Link Freeze"));
|
|
}
|
|
break;
|
|
case MT_HOOPCOLLIDE:
|
|
// This produces a kind of 'domino effect' with the hoop's pieces.
|
|
for (; special->hprev != NULL; special = special->hprev); // Move to the first sprite in the hoop
|
|
i = 0;
|
|
for (; special->type == MT_HOOP; special = special->hnext)
|
|
{
|
|
special->fuse = 11;
|
|
special->movedir = i;
|
|
special->extravalue1 = special->target->extravalue1;
|
|
special->extravalue2 = special->target->extravalue2;
|
|
special->target->threshold = 4242;
|
|
i++;
|
|
}
|
|
// Make the collision detectors disappear.
|
|
{
|
|
mobj_t *hnext;
|
|
for (; special != NULL; special = hnext)
|
|
{
|
|
hnext = special->hnext;
|
|
P_RemoveMobj(special);
|
|
}
|
|
}
|
|
|
|
P_DoNightsScore(player);
|
|
|
|
// Hoops are the only things that should add to the drill meter
|
|
// Also, one tic's worth of drill is too much.
|
|
if (G_IsSpecialStage(gamemap))
|
|
{
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
if (playeringame[i] && players[i].powers[pw_carry] == CR_NIGHTSMODE)
|
|
players[i].drillmeter += TICRATE/2;
|
|
}
|
|
else if (player->bot && player->bot != BOT_MPAI)
|
|
players[consoleplayer].drillmeter += TICRATE/2;
|
|
else
|
|
player->drillmeter += TICRATE/2;
|
|
|
|
// Play hoop sound -- pick one depending on the current link.
|
|
if (player->linkcount <= 5)
|
|
S_StartSound(toucher, sfx_hoop1);
|
|
else if (player->linkcount <= 10)
|
|
S_StartSound(toucher, sfx_hoop2);
|
|
else
|
|
S_StartSound(toucher, sfx_hoop3);
|
|
return;
|
|
|
|
// ***** //
|
|
// Mario //
|
|
// ***** //
|
|
case MT_SHELL:
|
|
{
|
|
boolean bounceon = ((P_MobjFlip(toucher)*(toucher->z - (special->z + special->height/2)) > 0) && (P_MobjFlip(toucher)*toucher->momz < 0));
|
|
if (special->threshold == TICRATE) // it's moving
|
|
{
|
|
if (bounceon)
|
|
{
|
|
// Stop it!
|
|
special->momx = special->momy = 0;
|
|
S_StartSound(toucher, sfx_mario2);
|
|
P_SetTarget(&special->target, NULL);
|
|
special->threshold = TICRATE - 1;
|
|
toucher->momz = -toucher->momz;
|
|
}
|
|
else // can't handle in PIT_CheckThing because of landing-on causing it to stop
|
|
P_DamageMobj(toucher, special, special->target, 1, 0);
|
|
}
|
|
else if (special->threshold == 0)
|
|
{
|
|
// Kick that sucker around!
|
|
special->movedir = ((special->movedir == 1) ? -1 : 1);
|
|
P_InstaThrust(special, toucher->angle, (special->info->speed*special->scale));
|
|
S_StartSound(toucher, sfx_mario2);
|
|
P_SetTarget(&special->target, toucher);
|
|
special->threshold = (3*TICRATE)/2;
|
|
if (bounceon)
|
|
toucher->momz = -toucher->momz;
|
|
}
|
|
}
|
|
return;
|
|
case MT_AXE:
|
|
{
|
|
thinker_t *th;
|
|
mobj_t *mo2;
|
|
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
return;
|
|
|
|
if (special->spawnpoint)
|
|
EV_DoElevator(special->spawnpoint->args[0], NULL, bridgeFall);
|
|
|
|
// scan the remaining thinkers to find koopa
|
|
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
|
|
{
|
|
if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
|
|
continue;
|
|
|
|
mo2 = (mobj_t *)th;
|
|
|
|
if (mo2->type != MT_KOOPA)
|
|
continue;
|
|
|
|
mo2->momz = 5*FRACUNIT;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case MT_LETTER:
|
|
{
|
|
if (special->health && !player->bot)
|
|
{
|
|
F_StartTextPrompt(199, 0, toucher, 0, true, false);
|
|
special->health = 0;
|
|
if (ultimatemode && player->continues < 99)
|
|
player->continues++;
|
|
}
|
|
return;
|
|
}
|
|
case MT_FIREFLOWER:
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
return;
|
|
|
|
S_StartSound(toucher, sfx_mario3);
|
|
|
|
player->powers[pw_shield] = (player->powers[pw_shield] & SH_NOSTACK)|SH_FIREFLOWER;
|
|
|
|
if (!(player->powers[pw_super] || (mariomode && player->powers[pw_invulnerability])))
|
|
{
|
|
player->mo->color = SKINCOLOR_WHITE;
|
|
G_GhostAddColor(GHC_FIREFLOWER);
|
|
}
|
|
|
|
break;
|
|
|
|
// *************** //
|
|
// Misc touchables //
|
|
// *************** //
|
|
case MT_STARPOST:
|
|
P_TouchStarPost(special, player, special->spawnpoint && special->spawnpoint->args[1]);
|
|
return;
|
|
|
|
case MT_FAKEMOBILE:
|
|
{
|
|
fixed_t touchx, touchy, touchspeed;
|
|
angle_t angle;
|
|
|
|
if (P_AproxDistance(toucher->x-special->x, toucher->y-special->y) >
|
|
P_AproxDistance((toucher->x-toucher->momx)-special->x, (toucher->y-toucher->momy)-special->y))
|
|
{
|
|
touchx = toucher->x + toucher->momx;
|
|
touchy = toucher->y + toucher->momy;
|
|
}
|
|
else
|
|
{
|
|
touchx = toucher->x;
|
|
touchy = toucher->y;
|
|
}
|
|
|
|
angle = R_PointToAngle2(special->x, special->y, touchx, touchy);
|
|
touchspeed = P_AproxDistance(toucher->momx, toucher->momy);
|
|
|
|
toucher->momx = P_ReturnThrustX(special, angle, touchspeed);
|
|
toucher->momy = P_ReturnThrustY(special, angle, touchspeed);
|
|
toucher->momz = -toucher->momz;
|
|
if (player->pflags & PF_GLIDING && !P_IsObjectOnGround(toucher))
|
|
{
|
|
player->pflags &= ~(PF_GLIDING|PF_JUMPED|PF_NOJUMPDAMAGE);
|
|
P_SetPlayerMobjState(toucher, S_PLAY_FALL);
|
|
toucher->momz += P_MobjFlip(toucher) * (player->speed >> 3);
|
|
toucher->momx = 7*toucher->momx>>3;
|
|
toucher->momy = 7*toucher->momy>>3;
|
|
}
|
|
player->homing = 0;
|
|
|
|
// Play a bounce sound?
|
|
S_StartSound(toucher, special->info->painsound);
|
|
}
|
|
return;
|
|
|
|
case MT_BLACKEGGMAN_GOOPFIRE:
|
|
if (!player->powers[pw_flashing] && !(player->powers[pw_ignorelatch] & (1<<15)))
|
|
{
|
|
toucher->momx = 0;
|
|
toucher->momy = 0;
|
|
|
|
if (toucher->momz != 0)
|
|
special->momz = toucher->momz;
|
|
|
|
player->powers[pw_carry] = CR_BRAKGOOP;
|
|
P_SetTarget(&toucher->tracer, special);
|
|
|
|
P_ResetPlayer(player);
|
|
|
|
if (special->target && special->target->state == &states[S_BLACKEGG_SHOOT1])
|
|
{
|
|
if (special->target->health <= 2 && P_RandomChance(FRACUNIT/2))
|
|
P_SetMobjState(special->target, special->target->info->missilestate);
|
|
else
|
|
P_SetMobjState(special->target, special->target->info->raisestate);
|
|
}
|
|
}
|
|
return;
|
|
case MT_EGGSHIELD:
|
|
{
|
|
angle_t angle = R_PointToAngle2(special->x, special->y, toucher->x, toucher->y) - special->angle;
|
|
fixed_t touchspeed = P_AproxDistance(toucher->momx, toucher->momy);
|
|
if (touchspeed < special->scale)
|
|
touchspeed = special->scale;
|
|
|
|
// Blocked by the shield?
|
|
if (!(angle > ANGLE_90 && angle < ANGLE_270))
|
|
{
|
|
toucher->momx = P_ReturnThrustX(special, special->angle, touchspeed);
|
|
toucher->momy = P_ReturnThrustY(special, special->angle, touchspeed);
|
|
toucher->momz = -toucher->momz;
|
|
if (player->pflags & PF_GLIDING && !P_IsObjectOnGround(toucher))
|
|
{
|
|
player->pflags &= ~(PF_GLIDING|PF_JUMPED|PF_NOJUMPDAMAGE);
|
|
P_SetPlayerMobjState(toucher, S_PLAY_FALL);
|
|
toucher->momz += P_MobjFlip(toucher) * (player->speed >> 3);
|
|
toucher->momx = 7*toucher->momx>>3;
|
|
toucher->momy = 7*toucher->momy>>3;
|
|
}
|
|
player->homing = 0;
|
|
|
|
// Play a bounce sound?
|
|
S_StartSound(toucher, special->info->painsound);
|
|
|
|
// experimental bounce
|
|
if (special->target)
|
|
special->target->extravalue1 = -special->target->info->speed;
|
|
}
|
|
else
|
|
{
|
|
// Shatter the shield!
|
|
toucher->momx = -toucher->momx/2;
|
|
toucher->momy = -toucher->momy/2;
|
|
toucher->momz = -toucher->momz;
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
|
|
case MT_EGGROBO1:
|
|
if (special->state == &states[special->info->deathstate])
|
|
return;
|
|
if (P_PlayerInPain(player))
|
|
return;
|
|
|
|
P_SetMobjState(special, special->info->meleestate);
|
|
special->angle = special->movedir;
|
|
special->momx = special->momy = 0;
|
|
|
|
// Buenos Dias Mandy
|
|
P_SetPlayerMobjState(toucher, S_PLAY_STUN);
|
|
player->pflags &= ~PF_APPLYAUTOBRAKE;
|
|
P_ResetPlayer(player);
|
|
player->drawangle = special->angle + ANGLE_180;
|
|
P_InstaThrust(toucher, special->angle, FixedMul(3*special->info->speed, special->scale/2));
|
|
toucher->z += P_MobjFlip(toucher);
|
|
if (toucher->eflags & MFE_UNDERWATER) // unlikely.
|
|
P_SetObjectMomZ(toucher, FixedDiv(10511*FRACUNIT,2600*FRACUNIT), false);
|
|
else
|
|
P_SetObjectMomZ(toucher, FixedDiv(69*FRACUNIT,10*FRACUNIT), false);
|
|
if (P_IsLocalPlayer(player))
|
|
{
|
|
quake.intensity = 9*FRACUNIT;
|
|
quake.time = TICRATE/2;
|
|
quake.epicenter = NULL;
|
|
}
|
|
|
|
#if 0 // camera redirection - deemed unnecessary
|
|
toucher->angle = special->angle;
|
|
P_SetPlayerAngle(player, toucher->angle);
|
|
#endif
|
|
|
|
S_StartSound(toucher, special->info->attacksound); // home run
|
|
|
|
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_SMALLGRABCHAIN:
|
|
case MT_BIGGRABCHAIN:
|
|
{
|
|
boolean macespin = false;
|
|
if (P_MobjFlip(toucher)*toucher->momz > 0
|
|
|| (player->powers[pw_carry]))
|
|
return;
|
|
|
|
if (toucher->z > special->z + special->height/2)
|
|
return;
|
|
|
|
if (toucher->z + toucher->height/2 < special->z)
|
|
return;
|
|
|
|
if (player->powers[pw_flashing])
|
|
return;
|
|
|
|
if (special->tracer && !(special->tracer->flags2 & MF2_STRONGBOX))
|
|
macespin = true;
|
|
|
|
if (macespin ? (player->powers[pw_ignorelatch] & (1<<15)) : (player->powers[pw_ignorelatch]))
|
|
return;
|
|
|
|
if (special->movefactor && special->tracer && special->tracer->angle != ANGLE_90 && special->tracer->angle != ANGLE_270)
|
|
{ // I don't expect you to understand this, Mr Bond...
|
|
angle_t ang = R_PointToAngle2(special->x, special->y, toucher->x, toucher->y) - special->tracer->angle;
|
|
if ((special->movefactor > 0) == (special->tracer->angle > ANGLE_90 && special->tracer->angle < ANGLE_270))
|
|
ang += ANGLE_180;
|
|
if (ang < ANGLE_180)
|
|
return; // I expect you to die.
|
|
}
|
|
|
|
P_ResetPlayer(player);
|
|
P_SetTarget(&toucher->tracer, special);
|
|
|
|
if (macespin)
|
|
{
|
|
player->powers[pw_carry] = CR_MACESPIN;
|
|
S_StartSound(toucher, sfx_spin);
|
|
P_SetPlayerMobjState(toucher, S_PLAY_ROLL);
|
|
}
|
|
else
|
|
player->powers[pw_carry] = CR_GENERIC;
|
|
|
|
// Can't jump first frame
|
|
player->pflags |= PF_JUMPSTASIS;
|
|
|
|
return;
|
|
}
|
|
case MT_EGGMOBILE2_POGO:
|
|
// sanity checks
|
|
if (!special->target || !special->target->health)
|
|
return;
|
|
// Goomba Stomp'd!
|
|
if (special->target->momz < 0)
|
|
{
|
|
P_DamageMobj(toucher, special, special->target, 1, 0);
|
|
//special->target->momz = -special->target->momz;
|
|
special->target->momx = special->target->momy = 0;
|
|
special->target->momz = 0;
|
|
special->target->flags |= MF_NOGRAVITY;
|
|
P_SetMobjState(special->target, special->info->raisestate);
|
|
S_StartSound(special->target, special->info->activesound);
|
|
P_RemoveMobj(special);
|
|
}
|
|
return;
|
|
|
|
case MT_EXTRALARGEBUBBLE:
|
|
if (player->powers[pw_shield] & SH_PROTECTWATER)
|
|
return;
|
|
if (maptol & TOL_NIGHTS)
|
|
return;
|
|
if (mariomode)
|
|
return;
|
|
if (special->state-states != S_EXTRALARGEBUBBLE)
|
|
return; // Don't grab the bubble during its spawn animation
|
|
else if (toucher->eflags & MFE_VERTICALFLIP)
|
|
{
|
|
if (special->z+special->height < toucher->z
|
|
|| special->z+special->height > toucher->z + (toucher->height*2/3))
|
|
return; // Only go in the mouth
|
|
}
|
|
else if (special->z < toucher->z
|
|
|| special->z > toucher->z + (toucher->height*2/3))
|
|
return; // Only go in the mouth
|
|
|
|
// Eaten by player!
|
|
if ((!player->bot || player->bot == BOT_MPAI) && (player->powers[pw_underwater] && player->powers[pw_underwater] <= 12*TICRATE + 1))
|
|
{
|
|
player->powers[pw_underwater] = underwatertics + 1;
|
|
P_RestoreMusic(player);
|
|
}
|
|
|
|
if (player->powers[pw_underwater] < underwatertics + 1)
|
|
player->powers[pw_underwater] = underwatertics + 1;
|
|
|
|
if (!player->climbing)
|
|
{
|
|
if (player->bot && player->bot != BOT_MPAI && toucher->state-states != S_PLAY_GASP)
|
|
S_StartSound(toucher, special->info->deathsound); // Force it to play a sound for bots
|
|
P_SetPlayerMobjState(toucher, S_PLAY_GASP);
|
|
P_ResetPlayer(player);
|
|
}
|
|
|
|
toucher->momx = toucher->momy = toucher->momz = 0;
|
|
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
return;
|
|
else
|
|
break;
|
|
|
|
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(special->info->mass)));
|
|
}
|
|
return;
|
|
|
|
case MT_CANARIVORE_GAS:
|
|
// if player and gas touch, attach gas to player (overriding any gas that already attached) and apply slowdown effect
|
|
special->flags |= MF_NOGRAVITY|MF_NOCLIPHEIGHT;
|
|
P_UnsetThingPosition(special);
|
|
special->x = toucher->x - toucher->momx/2;
|
|
special->y = toucher->y - toucher->momy/2;
|
|
special->z = toucher->z - toucher->momz/2;
|
|
P_SetThingPosition(special);
|
|
toucher->momx = FixedMul(toucher->momx, 50*FRACUNIT/51);
|
|
toucher->momy = FixedMul(toucher->momy, 50*FRACUNIT/51);
|
|
special->momx = toucher->momx;
|
|
special->momy = toucher->momy;
|
|
special->momz = toucher->momz;
|
|
return;
|
|
|
|
case MT_MINECARTSPAWNER:
|
|
if (!player->bot && player->bot != BOT_MPAI && special->fuse <= TICRATE && player->powers[pw_carry] != CR_MINECART && !(player->powers[pw_ignorelatch] & (1<<15)))
|
|
{
|
|
mobj_t *mcart = P_SpawnMobj(special->x, special->y, special->z, MT_MINECART);
|
|
P_SetTarget(&mcart->target, toucher);
|
|
mcart->angle = toucher->angle = player->drawangle = special->angle;
|
|
mcart->friction = FRACUNIT;
|
|
|
|
P_ResetPlayer(player);
|
|
player->pflags |= PF_JUMPDOWN;
|
|
player->powers[pw_carry] = CR_MINECART;
|
|
player->pflags &= ~PF_APPLYAUTOBRAKE;
|
|
P_SetTarget(&toucher->tracer, mcart);
|
|
toucher->momx = toucher->momy = toucher->momz = 0;
|
|
|
|
special->fuse = 3*TICRATE;
|
|
special->flags2 |= MF2_DONTDRAW;
|
|
}
|
|
return;
|
|
|
|
case MT_MINECARTEND:
|
|
if (player->powers[pw_carry] == CR_MINECART && toucher->tracer && !P_MobjWasRemoved(toucher->tracer) && toucher->tracer->health)
|
|
{
|
|
fixed_t maxz = max(toucher->z, special->z + 35*special->scale);
|
|
|
|
toucher->momx = toucher->tracer->momx/2;
|
|
toucher->momy = toucher->tracer->momy/2;
|
|
toucher->momz = toucher->tracer->momz + P_AproxDistance(toucher->tracer->momx, toucher->tracer->momy)/2;
|
|
P_ResetPlayer(player);
|
|
player->pflags &= ~PF_APPLYAUTOBRAKE;
|
|
P_SetPlayerMobjState(toucher, S_PLAY_FALL);
|
|
P_SetTarget(&toucher->tracer->target, NULL);
|
|
P_KillMobj(toucher->tracer, toucher, special, 0);
|
|
P_SetTarget(&toucher->tracer, NULL);
|
|
player->powers[pw_carry] = CR_NONE;
|
|
P_UnsetThingPosition(toucher);
|
|
toucher->x = special->x;
|
|
toucher->y = special->y;
|
|
toucher->z = maxz;
|
|
P_SetThingPosition(toucher);
|
|
}
|
|
return;
|
|
|
|
case MT_MINECARTSWITCHPOINT:
|
|
if (player->powers[pw_carry] == CR_MINECART && toucher->tracer && !P_MobjWasRemoved(toucher->tracer) && toucher->tracer->health)
|
|
{
|
|
mobjflag2_t destambush = special->flags2 & MF2_AMBUSH;
|
|
angle_t angdiff = toucher->tracer->angle - special->angle;
|
|
if (angdiff > ANGLE_90 && angdiff < ANGLE_270)
|
|
destambush ^= MF2_AMBUSH;
|
|
toucher->tracer->flags2 = (toucher->tracer->flags2 & ~MF2_AMBUSH) | destambush;
|
|
}
|
|
return;
|
|
default: // SOC or script pickup
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
return;
|
|
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, 0);
|
|
special->shadowscale = 0;
|
|
}
|
|
|
|
/** Saves a player's level progress at a star post
|
|
*
|
|
* \param post The star post to trigger
|
|
* \param player The player that should receive the checkpoint
|
|
* \param snaptopost If true, the respawn point will use the star post's position, otherwise player x/y and star post z
|
|
*/
|
|
void P_TouchStarPost(mobj_t *post, player_t *player, boolean snaptopost)
|
|
{
|
|
size_t i;
|
|
mobj_t *toucher = player->mo;
|
|
mobj_t *checkbase = snaptopost ? post : toucher;
|
|
|
|
if (player->bot && player->bot != BOT_MPAI)
|
|
return;
|
|
// In circuit, player must have touched all previous starposts
|
|
if (circuitmap
|
|
&& post->health - player->starpostnum > 1)
|
|
{
|
|
// blatant reuse of a variable that's normally unused in circuit
|
|
if (!player->tossdelay)
|
|
S_StartSound(toucher, sfx_lose);
|
|
player->tossdelay = 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 Starpost Number!\n");
|
|
return;
|
|
}
|
|
|
|
if (player->starpostnum >= post->health)
|
|
return; // Already hit this post
|
|
|
|
if (cv_coopstarposts.value && G_GametypeUsesCoopStarposts() && (netgame || multiplayer))
|
|
{
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (playeringame[i])
|
|
{
|
|
if (players[i].bot) // ignore dumb, stupid tails
|
|
continue;
|
|
|
|
players[i].starposttime = leveltime;
|
|
players[i].starpostx = checkbase->x>>FRACBITS;
|
|
players[i].starposty = checkbase->y>>FRACBITS;
|
|
players[i].starpostz = post->z>>FRACBITS;
|
|
players[i].starpostangle = post->angle;
|
|
players[i].starpostscale = player->mo->destscale;
|
|
if (post->flags2 & MF2_OBJECTFLIP)
|
|
{
|
|
players[i].starpostscale *= -1;
|
|
players[i].starpostz += post->height>>FRACBITS;
|
|
}
|
|
players[i].starpostnum = post->health;
|
|
|
|
if (cv_coopstarposts.value == 2 && (players[i].playerstate == PST_DEAD || players[i].spectator) && P_GetLives(&players[i]))
|
|
P_SpectatorJoinGame(&players[i]); //players[i].playerstate = PST_REBORN;
|
|
}
|
|
}
|
|
S_StartSound(NULL, post->info->painsound);
|
|
}
|
|
else
|
|
{
|
|
// Save the player's time and position.
|
|
player->starposttime = leveltime;
|
|
player->starpostx = checkbase->x>>FRACBITS;
|
|
player->starposty = checkbase->y>>FRACBITS;
|
|
player->starpostz = post->z>>FRACBITS;
|
|
player->starpostangle = post->angle;
|
|
player->starpostscale = player->mo->destscale;
|
|
if (post->flags2 & MF2_OBJECTFLIP)
|
|
{
|
|
player->starpostscale *= -1;
|
|
player->starpostz += post->height>>FRACBITS;
|
|
}
|
|
player->starpostnum = post->health;
|
|
S_StartSound(toucher, post->info->painsound);
|
|
}
|
|
|
|
P_ClearStarPost(post->health);
|
|
|
|
// Find all starposts in the level with this value - INCLUDING this one!
|
|
if (!(netgame && circuitmap && player != &players[consoleplayer]))
|
|
{
|
|
thinker_t *th;
|
|
mobj_t *mo2;
|
|
|
|
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
|
|
{
|
|
if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
|
|
continue;
|
|
|
|
mo2 = (mobj_t *)th;
|
|
|
|
if (mo2->type != MT_STARPOST)
|
|
continue;
|
|
if (mo2->health != post->health)
|
|
continue;
|
|
|
|
P_SetMobjState(mo2, mo2->info->painstate);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Prints death messages relating to a dying or hit player.
|
|
*
|
|
* \param player Affected player.
|
|
* \param inflictor The attack weapon used, can be NULL.
|
|
* \param source The attacker, can be NULL.
|
|
* \param damagetype The type of damage dealt to the player. If bit 7 (0x80) is set, this was an instant-kill.
|
|
*/
|
|
static void P_HitDeathMessages(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 damagetype)
|
|
{
|
|
const char *str = NULL;
|
|
boolean deathonly = false;
|
|
boolean deadsource = false;
|
|
boolean deadtarget = false;
|
|
// player names complete with control codes
|
|
char targetname[MAXPLAYERNAME+4];
|
|
char sourcename[MAXPLAYERNAME+4];
|
|
|
|
if (!(gametyperules & (GTR_RINGSLINGER|GTR_HURTMESSAGES)))
|
|
return; // Not in coop, etc.
|
|
|
|
if (!player)
|
|
return; // Impossible!
|
|
|
|
if (!player->mo)
|
|
return; // Also impossible!
|
|
|
|
if (player->spectator)
|
|
return; // No messages for dying (crushed) spectators.
|
|
|
|
if (!netgame)
|
|
return; // Presumably it's obvious what's happening in splitscreen.
|
|
|
|
if (LUA_HookHurtMsg(player, inflictor, source, damagetype))
|
|
return;
|
|
|
|
deadtarget = (player->mo->health <= 0);
|
|
|
|
// Don't log every hazard hit if they don't want us to.
|
|
if (!deadtarget && !cv_hazardlog.value)
|
|
return;
|
|
|
|
// Target's name
|
|
snprintf(targetname, sizeof(targetname), "%s%s%s",
|
|
CTFTEAMCODE(player),
|
|
player_names[player - players],
|
|
CTFTEAMENDCODE(player));
|
|
|
|
if (source)
|
|
{
|
|
// inflictor shouldn't be NULL if source isn't
|
|
I_Assert(inflictor != NULL);
|
|
|
|
if (source->player)
|
|
{
|
|
// Source's name (now that we know there is one)
|
|
snprintf(sourcename, sizeof(sourcename), "%s%s%s",
|
|
CTFTEAMCODE(source->player),
|
|
player_names[source->player - players],
|
|
CTFTEAMENDCODE(source->player));
|
|
|
|
// We don't care if it's us.
|
|
// "Player 1's [redacted] killed Player 1."
|
|
if (source->player->playerstate == PST_DEAD && source->player != player &&
|
|
(inflictor->flags2 & MF2_BEYONDTHEGRAVE))
|
|
deadsource = true;
|
|
|
|
if (inflictor->flags & MF_PUSHABLE)
|
|
{
|
|
str = M_GetText("%s%s's playtime with heavy objects %s %s.\n");
|
|
}
|
|
else switch (inflictor->type)
|
|
{
|
|
case MT_PLAYER:
|
|
if (damagetype == DMG_NUKE) // SH_ARMAGEDDON, armageddon shield
|
|
str = M_GetText("%s%s's armageddon blast %s %s.\n");
|
|
else if ((inflictor->player->powers[pw_shield] & SH_NOSTACK) == SH_ELEMENTAL && (inflictor->player->pflags & PF_SHIELDABILITY))
|
|
str = M_GetText("%s%s's elemental stomp %s %s.\n");
|
|
else if (inflictor->player->powers[pw_invulnerability])
|
|
str = M_GetText("%s%s's invincibility aura %s %s.\n");
|
|
else if (inflictor->player->powers[pw_super])
|
|
str = M_GetText("%s%s's super aura %s %s.\n");
|
|
else
|
|
str = M_GetText("%s%s's tagging hand %s %s.\n");
|
|
break;
|
|
case MT_SPINFIRE:
|
|
str = M_GetText("%s%s's elemental fire trail %s %s.\n");
|
|
break;
|
|
case MT_THROWNBOUNCE:
|
|
str = M_GetText("%s%s's bounce ring %s %s.\n");
|
|
break;
|
|
case MT_THROWNINFINITY:
|
|
str = M_GetText("%s%s's infinity ring %s %s.\n");
|
|
break;
|
|
case MT_THROWNAUTOMATIC:
|
|
str = M_GetText("%s%s's automatic ring %s %s.\n");
|
|
break;
|
|
case MT_THROWNSCATTER:
|
|
str = M_GetText("%s%s's scatter ring %s %s.\n");
|
|
break;
|
|
// TODO: For next two, figure out how to determine if it was a direct hit or splash damage. -SH
|
|
case MT_THROWNEXPLOSION:
|
|
str = M_GetText("%s%s's explosion ring %s %s.\n");
|
|
break;
|
|
case MT_THROWNGRENADE:
|
|
str = M_GetText("%s%s's grenade ring %s %s.\n");
|
|
break;
|
|
case MT_REDRING:
|
|
if (inflictor->flags2 & MF2_RAILRING)
|
|
str = M_GetText("%s%s's rail ring %s %s.\n");
|
|
else
|
|
str = M_GetText("%s%s's thrown ring %s %s.\n");
|
|
break;
|
|
default:
|
|
str = M_GetText("%s%s %s %s.\n");
|
|
break;
|
|
}
|
|
|
|
CONS_Printf(str,
|
|
deadsource ? M_GetText("The late ") : "",
|
|
sourcename,
|
|
deadtarget ? M_GetText("killed") : M_GetText("hit"),
|
|
targetname);
|
|
return;
|
|
}
|
|
else switch (source->type)
|
|
{
|
|
case MT_EGGMAN_ICON:
|
|
str = M_GetText("%s was %s by Eggman's nefarious TV magic.\n");
|
|
break;
|
|
case MT_SPIKE:
|
|
case MT_WALLSPIKE:
|
|
str = M_GetText("%s was %s by spikes.\n");
|
|
break;
|
|
default:
|
|
str = M_GetText("%s was %s by an environmental hazard.\n");
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// null source, environment kills
|
|
switch (damagetype)
|
|
{
|
|
case DMG_WATER:
|
|
str = M_GetText("%s was %s by dangerous water.\n");
|
|
break;
|
|
case DMG_FIRE:
|
|
str = M_GetText("%s was %s by molten lava.\n");
|
|
break;
|
|
case DMG_ELECTRIC:
|
|
str = M_GetText("%s was %s by electricity.\n");
|
|
break;
|
|
case DMG_SPIKE:
|
|
str = M_GetText("%s was %s by spikes.\n");
|
|
break;
|
|
case DMG_DROWNED:
|
|
deathonly = true;
|
|
str = M_GetText("%s drowned.\n");
|
|
break;
|
|
case DMG_CRUSHED:
|
|
deathonly = true;
|
|
str = M_GetText("%s was crushed.\n");
|
|
break;
|
|
case DMG_DEATHPIT:
|
|
if (deadtarget)
|
|
{
|
|
deathonly = true;
|
|
str = M_GetText("%s fell into a bottomless pit.\n");
|
|
}
|
|
break;
|
|
case DMG_SPACEDROWN:
|
|
if (deadtarget)
|
|
{
|
|
deathonly = true;
|
|
str = M_GetText("%s asphyxiated in space.\n");
|
|
}
|
|
break;
|
|
default:
|
|
if (deadtarget)
|
|
{
|
|
deathonly = true;
|
|
str = M_GetText("%s died.\n");
|
|
}
|
|
break;
|
|
}
|
|
if (!str)
|
|
str = M_GetText("%s was %s by an environmental hazard.\n");
|
|
}
|
|
|
|
if (!str) // Should not happen! Unless we missed catching something above.
|
|
return;
|
|
|
|
if (deathonly)
|
|
{
|
|
if (!deadtarget)
|
|
return;
|
|
CONS_Printf(str, targetname);
|
|
}
|
|
else
|
|
CONS_Printf(str, targetname, deadtarget ? M_GetText("killed") : M_GetText("hit"));
|
|
}
|
|
|
|
/** 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.
|
|
* Verify that the value of ::cv_timelimit is greater than zero before
|
|
* calling this function.
|
|
*
|
|
* \sa cv_timelimit, P_CheckPointLimit, P_UpdateSpecials
|
|
*/
|
|
void P_CheckTimeLimit(void)
|
|
{
|
|
INT32 i, k;
|
|
|
|
if (!cv_timelimit.value)
|
|
return;
|
|
|
|
if (!(multiplayer || netgame))
|
|
return;
|
|
|
|
if (!(gametyperules & GTR_TIMELIMIT))
|
|
return;
|
|
|
|
if (leveltime < timelimitintics)
|
|
return;
|
|
|
|
if (gameaction == ga_completed)
|
|
return;
|
|
|
|
//Tagmode round end but only on the tic before the
|
|
//XD_EXITLEVEL packet is received by all players.
|
|
if (G_TagGametype())
|
|
{
|
|
if (leveltime == (timelimitintics + 1))
|
|
{
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator
|
|
|| (players[i].pflags & PF_GAMETYPEOVER) || (players[i].pflags & PF_TAGIT))
|
|
continue;
|
|
|
|
CONS_Printf(M_GetText("%s received double points for surviving the round.\n"), player_names[i]);
|
|
P_AddPlayerScore(&players[i], players[i].score);
|
|
}
|
|
}
|
|
|
|
if (server)
|
|
D_SendExitLevel(false);
|
|
}
|
|
|
|
//Optional tie-breaker for Match/CTF
|
|
else if ((cv_overtime.value) && (gametyperules & GTR_OVERTIME))
|
|
{
|
|
INT32 playerarray[MAXPLAYERS];
|
|
INT32 tempplayer = 0;
|
|
INT32 spectators = 0;
|
|
INT32 playercount = 0;
|
|
|
|
//Figure out if we have enough participating players to care.
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (playeringame[i] && players[i].spectator)
|
|
spectators++;
|
|
}
|
|
|
|
if ((D_NumPlayers() - spectators) > 1)
|
|
{
|
|
// Play the starpost sfx after the first second of overtime.
|
|
if (gamestate == GS_LEVEL && (leveltime == (timelimitintics + TICRATE)))
|
|
S_StartSound(NULL, sfx_strpst);
|
|
|
|
// Normal Match
|
|
if (!G_GametypeHasTeams())
|
|
{
|
|
//Store the nodes of participating players in an array.
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (playeringame[i] && !players[i].spectator)
|
|
{
|
|
playerarray[playercount] = i;
|
|
playercount++;
|
|
}
|
|
}
|
|
|
|
//Sort 'em.
|
|
for (i = 1; i < playercount; i++)
|
|
{
|
|
for (k = i; k < playercount; k++)
|
|
{
|
|
if (players[playerarray[i-1]].score < players[playerarray[k]].score)
|
|
{
|
|
tempplayer = playerarray[i-1];
|
|
playerarray[i-1] = playerarray[k];
|
|
playerarray[k] = tempplayer;
|
|
}
|
|
}
|
|
}
|
|
|
|
//End the round if the top players aren't tied.
|
|
if (players[playerarray[0]].score == players[playerarray[1]].score)
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
//In team match and CTF, determining a tie is much simpler. =P
|
|
if (redscore == bluescore)
|
|
return;
|
|
}
|
|
}
|
|
if (server)
|
|
D_SendExitLevel(false);
|
|
}
|
|
|
|
if (server)
|
|
D_SendExitLevel(false);
|
|
}
|
|
|
|
/** Checks if a player's score is over the pointlimit and the round should end.
|
|
* Verify that the value of ::cv_pointlimit is greater than zero before
|
|
* calling this function.
|
|
*
|
|
* \sa cv_pointlimit, P_CheckTimeLimit, P_UpdateSpecials
|
|
*/
|
|
void P_CheckPointLimit(void)
|
|
{
|
|
INT32 i;
|
|
|
|
if (!cv_pointlimit.value)
|
|
return;
|
|
|
|
if (!(multiplayer || netgame))
|
|
return;
|
|
|
|
if (!(gametyperules & GTR_POINTLIMIT))
|
|
return;
|
|
|
|
// pointlimit is nonzero, check if it's been reached by this player
|
|
if (G_GametypeHasTeams())
|
|
{
|
|
// Just check both teams
|
|
if ((UINT32)cv_pointlimit.value <= redscore || (UINT32)cv_pointlimit.value <= bluescore)
|
|
{
|
|
if (server)
|
|
D_SendExitLevel(false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator)
|
|
continue;
|
|
|
|
if ((UINT32)cv_pointlimit.value <= players[i].score)
|
|
{
|
|
if (server)
|
|
D_SendExitLevel(false);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*Checks for untagged remaining players in both tag derivitave modes.
|
|
*If no untagged players remain, end the round.
|
|
*Also serves as error checking if the only IT player leaves.*/
|
|
void P_CheckSurvivors(void)
|
|
{
|
|
INT32 i;
|
|
INT32 survivors = 0;
|
|
INT32 taggers = 0;
|
|
INT32 spectators = 0;
|
|
INT32 survivorarray[MAXPLAYERS];
|
|
|
|
if (!D_NumPlayers()) //no players in the game, no check performed.
|
|
return;
|
|
|
|
for (i=0; i < MAXPLAYERS; i++) //figure out counts of taggers, survivors and spectators.
|
|
{
|
|
if (playeringame[i])
|
|
{
|
|
if (players[i].spectator)
|
|
spectators++;
|
|
else if ((players[i].pflags & PF_TAGIT) && players[i].quittime < 30 * TICRATE)
|
|
taggers++;
|
|
else if (!(players[i].pflags & PF_GAMETYPEOVER) && players[i].quittime < 30 * TICRATE)
|
|
{
|
|
survivorarray[survivors] = i;
|
|
survivors++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!taggers) //If there are no taggers, pick a survivor at random to be it.
|
|
{
|
|
// Exception for hide and seek. If a round has started and the IT player leaves, end the round.
|
|
if ((gametyperules & GTR_HIDEFROZEN) && (leveltime >= (hidetime * TICRATE)))
|
|
{
|
|
CONS_Printf(M_GetText("The IT player has left the game.\n"));
|
|
if (server)
|
|
D_SendExitLevel(false);
|
|
|
|
return;
|
|
}
|
|
|
|
if (survivors)
|
|
{
|
|
INT32 newtagger = survivorarray[P_RandomKey(survivors)];
|
|
|
|
CONS_Printf(M_GetText("%s is now IT!\n"), player_names[newtagger]); // Tell everyone who is it!
|
|
players[newtagger].pflags |= PF_TAGIT;
|
|
|
|
survivors--; //Get rid of the guy we just made IT.
|
|
|
|
//Yeah, we have an eligible tagger, but we may not have anybody for him to tag!
|
|
//If there is only one guy waiting on the game to fill or spectators to enter game, don't bother.
|
|
if (!survivors && (D_NumPlayers() - spectators) > 1)
|
|
{
|
|
CONS_Printf(M_GetText("All players have been tagged!\n"));
|
|
if (server)
|
|
D_SendExitLevel(false);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//If we reach this point, no player can replace the one that was IT.
|
|
//Unless it is one player waiting on a game, end the round.
|
|
if ((D_NumPlayers() - spectators) > 1)
|
|
{
|
|
CONS_Printf(M_GetText("There are no players able to become IT.\n"));
|
|
if (server)
|
|
D_SendExitLevel(false);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//If there are taggers, but no survivors, end the round.
|
|
//Except when the tagger is by himself and the rest of the game are spectators.
|
|
if (!survivors && (D_NumPlayers() - spectators) > 1)
|
|
{
|
|
CONS_Printf(M_GetText("All players have been tagged!\n"));
|
|
if (server)
|
|
D_SendExitLevel(false);
|
|
}
|
|
}
|
|
|
|
// Checks whether or not to end a race netgame.
|
|
boolean P_CheckRacers(void)
|
|
{
|
|
INT32 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].exiting && players[i].lives > 0)
|
|
break;
|
|
}
|
|
|
|
if (i == MAXPLAYERS) // finished
|
|
{
|
|
countdown = 0;
|
|
countdown2 = 0;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** 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)
|
|
{
|
|
mobj_t *mo;
|
|
|
|
if (inflictor && (inflictor->type == MT_SHELL || inflictor->type == MT_FIREBALL))
|
|
S_StartScreamSound(target, sfx_mario2);
|
|
|
|
if (!(maptol & TOL_NIGHTS) && G_IsSpecialStage(gamemap) && target->player && target->player->nightstime > 6)
|
|
target->player->nightstime = 6; // Just let P_Ticker take care of the rest.
|
|
|
|
if (target->flags & (MF_ENEMY|MF_BOSS))
|
|
target->momx = target->momy = target->momz = 0;
|
|
|
|
if (target->type != MT_PLAYER && !(target->flags & MF_MONITOR))
|
|
target->flags |= MF_NOGRAVITY|MF_NOCLIP|MF_NOCLIPHEIGHT; // Don't drop Tails 03-08-2000
|
|
|
|
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 (LUA_HookMobjDeath(target, inflictor, source, damagetype) || P_MobjWasRemoved(target))
|
|
return;
|
|
|
|
// Let EVERYONE know what happened to a player! 01-29-2002 Tails
|
|
if (target->player && !target->player->spectator)
|
|
{
|
|
if (metalrecording) // Ack! Metal Sonic shouldn't die! Cut the tape, end recording!
|
|
G_StopMetalRecording(true);
|
|
if ((gametyperules & GTR_DEATHPENALTY) // note, no team match suicide penalty
|
|
&& ((target == source) || (source == NULL && inflictor == NULL) || (source && !source->player)))
|
|
{ // Suicide penalty
|
|
if (target->player->score >= 50)
|
|
target->player->score -= 50;
|
|
else
|
|
target->player->score = 0;
|
|
}
|
|
|
|
target->flags2 &= ~MF2_DONTDRAW;
|
|
}
|
|
|
|
// if killed by a player
|
|
if (source && source->player)
|
|
{
|
|
if (target->flags & MF_MONITOR)
|
|
{
|
|
P_SetTarget(&target->target, source);
|
|
source->player->numboxes++;
|
|
if (cv_itemrespawn.value && gametype != GT_COOP && (modifiedgame || netgame || multiplayer))
|
|
target->fuse = cv_itemrespawntime.value*TICRATE + 2; // Random box generation
|
|
}
|
|
|
|
// Award Score Tails
|
|
{
|
|
INT32 score = 0;
|
|
|
|
if (maptol & TOL_NIGHTS) // Enemies always worth 200, bosses don't do anything.
|
|
{
|
|
if ((target->flags & MF_ENEMY) && !(target->flags & (MF_MISSILE|MF_BOSS)))
|
|
{
|
|
score = 200;
|
|
|
|
if (source->player->bonustime)
|
|
score *= 2;
|
|
|
|
// Also, add to the link.
|
|
// I don't know if NiGHTS did this, but
|
|
// Sonic Time Attacked did and it seems like a good enough incentive
|
|
// to make people want to actually dash towards/paraloop enemies
|
|
if (++source->player->linkcount > source->player->maxlink)
|
|
source->player->maxlink = source->player->linkcount;
|
|
source->player->linktimer = nightslinktics;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (target->flags & MF_BOSS)
|
|
score = 1000;
|
|
else if ((target->flags & MF_ENEMY) && !(target->flags & MF_MISSILE) && target->info->spawnhealth)
|
|
{
|
|
UINT8 locscoreadd = source->player->scoreadd + target->info->spawnhealth;
|
|
mobj_t *scoremobj;
|
|
UINT32 scorestate = mobjinfo[MT_SCORE].spawnstate;
|
|
|
|
scoremobj = P_SpawnMobj(target->x, target->y, target->z + (target->height / 2), MT_SCORE);
|
|
|
|
// More Sonic-like point system
|
|
if (!mariomode) switch (locscoreadd)
|
|
{
|
|
case 1: score = 100; break;
|
|
case 2: score = 200; scorestate += 1; break;
|
|
case 3: score = 500; scorestate += 2; break;
|
|
case 4: case 5: case 6: case 7: case 8: case 9:
|
|
case 10: case 11: case 12: case 13: case 14:
|
|
score = 1000; scorestate += 3; break;
|
|
default: score = 10000; scorestate += 4; break;
|
|
}
|
|
// Mario Mode has Mario-like chain point values
|
|
else switch (locscoreadd)
|
|
{
|
|
case 1: score = 100; break;
|
|
case 2: score = 200; scorestate += 1; break;
|
|
case 3: score = 400; scorestate += 5; break;
|
|
case 4: score = 800; scorestate += 6; break;
|
|
case 5: score = 1000; scorestate += 3; break;
|
|
case 6: score = 2000; scorestate += 7; break;
|
|
case 7: score = 4000; scorestate += 8; break;
|
|
case 8: score = 8000; scorestate += 9; break;
|
|
default: // 1up for a chain this long
|
|
if (modeattacking) // but 1ups don't exist in record attack!
|
|
{ // So we just go back to 10k points.
|
|
score = 10000; scorestate += 4; break;
|
|
}
|
|
P_GivePlayerLives(source->player, 1);
|
|
P_PlayLivesJingle(source->player);
|
|
scorestate += 10;
|
|
break;
|
|
}
|
|
|
|
P_SetMobjState(scoremobj, scorestate);
|
|
|
|
source->player->scoreadd = locscoreadd;
|
|
}
|
|
}
|
|
|
|
P_AddPlayerScore(source->player, score);
|
|
}
|
|
}
|
|
|
|
// if a player avatar dies...
|
|
if (target->player)
|
|
{
|
|
target->flags &= ~(MF_SOLID|MF_SHOOTABLE); // does not block
|
|
P_UnsetThingPosition(target);
|
|
target->flags |= MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY;
|
|
P_SetThingPosition(target);
|
|
target->standingslope = NULL;
|
|
target->pmomz = 0;
|
|
|
|
if (target->player->powers[pw_super])
|
|
{
|
|
target->player->powers[pw_super] = 0;
|
|
if (P_IsLocalPlayer(target->player))
|
|
{
|
|
music_stack_noposition = true; // HACK: Do not reposition next music
|
|
music_stack_fadeout = MUSICRATE/2; // HACK: Fade out current music
|
|
}
|
|
P_RestoreMusic(target->player);
|
|
|
|
if (!G_CoopGametype())
|
|
{
|
|
HU_SetCEchoFlags(0);
|
|
HU_SetCEchoDuration(5);
|
|
HU_DoCEcho(va("%s\\is no longer super.\\\\\\\\", player_names[target->player-players]));
|
|
}
|
|
}
|
|
|
|
target->color = target->player->skincolor;
|
|
target->colorized = false;
|
|
G_GhostAddColor(GHC_NORMAL);
|
|
|
|
if ((target->player->lives <= 1) && (netgame || multiplayer) && G_GametypeUsesCoopLives() && (cv_cooplives.value == 0))
|
|
;
|
|
else if ((!target->player->bot || target->player->bot == BOT_MPAI) && !target->player->spectator && (target->player->lives != INFLIVES)
|
|
&& G_GametypeUsesLives())
|
|
{
|
|
if (!(target->player->pflags & PF_FINISHED))
|
|
target->player->lives -= 1; // Lose a life Tails 03-11-2000
|
|
|
|
if (target->player->lives <= 0) // Tails 03-14-2000
|
|
{
|
|
boolean gameovermus = false;
|
|
if ((netgame || multiplayer) && G_GametypeUsesCoopLives() && (cv_cooplives.value != 1))
|
|
{
|
|
INT32 i;
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i])
|
|
continue;
|
|
|
|
if (players[i].lives > 0)
|
|
break;
|
|
}
|
|
if (i == MAXPLAYERS)
|
|
gameovermus = true;
|
|
}
|
|
else if (P_IsLocalPlayer(target->player))
|
|
gameovermus = true;
|
|
|
|
if (gameovermus) // Yousa dead now, Okieday? Tails 03-14-2000
|
|
S_ChangeMusicEx("_gover", 0, 0, 0, (2*MUSICRATE) - (MUSICRATE/25), 0); // 1.96 seconds
|
|
//P_PlayJingle(target->player, JT_GOVER); // can't be used because incompatible with track fadeout
|
|
|
|
if (!(netgame || multiplayer || demoplayback || demorecording || metalrecording || modeattacking) && numgameovers < maxgameovers)
|
|
{
|
|
numgameovers++;
|
|
if (!usedCheats && cursaveslot > 0)
|
|
G_SaveGameOver((UINT32)cursaveslot, (target->player->continues <= 0));
|
|
}
|
|
}
|
|
}
|
|
target->player->playerstate = PST_DEAD;
|
|
|
|
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...
|
|
localaiming = 0;
|
|
}
|
|
if (target->player == &players[secondarydisplayplayer])
|
|
{
|
|
// added : 22-02-98: recenter view for next life...
|
|
localaiming2 = 0;
|
|
}
|
|
|
|
//tag deaths handled differently in suicide cases. Don't count spectators!
|
|
if (G_TagGametype()
|
|
&& !(target->player->pflags & PF_TAGIT) && (!source || !source->player) && !(target->player->spectator))
|
|
{
|
|
// if you accidentally die before you run out of time to hide, ignore it.
|
|
// allow them to try again, rather than sitting the whole thing out.
|
|
if (leveltime >= hidetime * TICRATE)
|
|
{
|
|
if (!(gametyperules & GTR_HIDEFROZEN))//suiciding in survivor makes you IT.
|
|
{
|
|
target->player->pflags |= PF_TAGIT;
|
|
CONS_Printf(M_GetText("%s is now IT!\n"), player_names[target->player-players]); // Tell everyone who is it!
|
|
P_CheckSurvivors();
|
|
}
|
|
else
|
|
{
|
|
if (!(target->player->pflags & PF_GAMETYPEOVER))
|
|
{
|
|
//otherwise, increment the tagger's score.
|
|
//in hide and seek, suiciding players are counted as found.
|
|
INT32 w;
|
|
|
|
for (w=0; w < MAXPLAYERS; w++)
|
|
{
|
|
if (players[w].pflags & PF_TAGIT)
|
|
P_AddPlayerScore(&players[w], 100);
|
|
}
|
|
|
|
target->player->pflags |= PF_GAMETYPEOVER;
|
|
CONS_Printf(M_GetText("%s was found!\n"), player_names[target->player-players]);
|
|
P_CheckSurvivors();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (source && target && target->player && source->player)
|
|
P_PlayVictorySound(source); // Killer laughs at you. LAUGHS! BWAHAHAHA!
|
|
|
|
// Other death animation effects
|
|
switch(target->type)
|
|
{
|
|
case MT_BOUNCEPICKUP:
|
|
case MT_RAILPICKUP:
|
|
case MT_AUTOPICKUP:
|
|
case MT_EXPLODEPICKUP:
|
|
case MT_SCATTERPICKUP:
|
|
case MT_GRENADEPICKUP:
|
|
P_SetObjectMomZ(target, FRACUNIT, false);
|
|
target->fuse = target->info->damage;
|
|
break;
|
|
|
|
case MT_BUGGLE:
|
|
if (inflictor && inflictor->player // did a player kill you? Spawn relative to the player so they're bound to get it
|
|
&& P_AproxDistance(inflictor->x - target->x, inflictor->y - target->y) <= inflictor->radius + target->radius + FixedMul(8*FRACUNIT, inflictor->scale) // close enough?
|
|
&& inflictor->z <= target->z + target->height + FixedMul(8*FRACUNIT, inflictor->scale)
|
|
&& inflictor->z + inflictor->height >= target->z - FixedMul(8*FRACUNIT, inflictor->scale))
|
|
mo = P_SpawnMobj(inflictor->x + inflictor->momx, inflictor->y + inflictor->momy, inflictor->z + (inflictor->height / 2) + inflictor->momz, MT_EXTRALARGEBUBBLE);
|
|
else
|
|
mo = P_SpawnMobj(target->x, target->y, target->z, MT_EXTRALARGEBUBBLE);
|
|
mo->destscale = target->scale;
|
|
P_SetScale(mo, mo->destscale);
|
|
P_SetMobjState(mo, mo->info->raisestate);
|
|
break;
|
|
|
|
case MT_YELLOWSHELL:
|
|
P_SpawnMobjFromMobj(target, 0, 0, 0, MT_YELLOWSPRING);
|
|
break;
|
|
|
|
case MT_CRAWLACOMMANDER:
|
|
target->momx = target->momy = target->momz = 0;
|
|
break;
|
|
|
|
case MT_CRUSHSTACEAN:
|
|
if (target->tracer)
|
|
{
|
|
mobj_t *chain = target->tracer->target, *chainnext;
|
|
while (chain)
|
|
{
|
|
chainnext = chain->target;
|
|
P_RemoveMobj(chain);
|
|
chain = chainnext;
|
|
}
|
|
S_StopSound(target->tracer);
|
|
P_KillMobj(target->tracer, inflictor, source, damagetype);
|
|
}
|
|
break;
|
|
|
|
case MT_BANPYURA:
|
|
if (target->tracer)
|
|
{
|
|
S_StopSound(target->tracer);
|
|
P_KillMobj(target->tracer, inflictor, source, damagetype);
|
|
}
|
|
break;
|
|
|
|
case MT_EGGSHIELD:
|
|
P_SetObjectMomZ(target, 4*target->scale, false);
|
|
P_InstaThrust(target, target->angle, 3*target->scale);
|
|
target->flags = (target->flags|MF_NOCLIPHEIGHT) & ~MF_NOGRAVITY;
|
|
break;
|
|
|
|
case MT_DRAGONBOMBER:
|
|
{
|
|
mobj_t *segment = target;
|
|
while (segment->tracer != NULL)
|
|
{
|
|
P_KillMobj(segment->tracer, NULL, NULL, 0);
|
|
segment = segment->tracer;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case MT_EGGMOBILE3:
|
|
{
|
|
mobj_t *mo2;
|
|
thinker_t *th;
|
|
UINT32 i = 0; // to check how many clones we've removed
|
|
|
|
// scan the thinkers to make sure all the old pinch dummies are gone on death
|
|
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
|
|
{
|
|
if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
|
|
continue;
|
|
|
|
mo = (mobj_t *)th;
|
|
if (mo->type != (mobjtype_t)target->info->mass)
|
|
continue;
|
|
if (mo->tracer != target)
|
|
continue;
|
|
|
|
P_KillMobj(mo, inflictor, source, damagetype);
|
|
mo->destscale = mo->scale/8;
|
|
mo->scalespeed = (mo->scale - mo->destscale)/(2*TICRATE);
|
|
mo->momz = mo->info->speed;
|
|
mo->angle = FixedAngle((P_RandomKey(36)*10)<<FRACBITS);
|
|
|
|
mo2 = P_SpawnMobjFromMobj(mo, 0, 0, 0, MT_BOSSJUNK);
|
|
mo2->angle = mo->angle;
|
|
P_SetMobjState(mo2, S_BOSSSEBH2);
|
|
|
|
if (++i == 2) // we've already removed 2 of these, let's stop now
|
|
break;
|
|
else
|
|
S_StartSound(mo, mo->info->deathsound); // done once to prevent sound stacking
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MT_BIGMINE:
|
|
if (inflictor)
|
|
{
|
|
fixed_t dx = target->x - inflictor->x, dy = target->y - inflictor->y, dz = target->z - inflictor->z;
|
|
fixed_t dm = FixedHypot(dz, FixedHypot(dy, dx));
|
|
target->momx = FixedDiv(FixedDiv(dx, dm), dm)*512;
|
|
target->momy = FixedDiv(FixedDiv(dy, dm), dm)*512;
|
|
target->momz = FixedDiv(FixedDiv(dz, dm), dm)*512;
|
|
}
|
|
if (source)
|
|
P_SetTarget(&target->tracer, source);
|
|
break;
|
|
|
|
case MT_BLASTEXECUTOR:
|
|
if (target->spawnpoint)
|
|
P_LinedefExecute(target->spawnpoint->args[0], (source ? source : inflictor), target->subsector->sector);
|
|
break;
|
|
|
|
case MT_SPINBOBERT:
|
|
if (target->hnext)
|
|
P_KillMobj(target->hnext, inflictor, source, damagetype);
|
|
if (target->hprev)
|
|
P_KillMobj(target->hprev, inflictor, source, damagetype);
|
|
break;
|
|
|
|
case MT_EGGTRAP:
|
|
// Time for birdies! Yaaaaaaaay!
|
|
target->fuse = TICRATE;
|
|
break;
|
|
|
|
case MT_MINECART:
|
|
A_Scream(target);
|
|
target->momx = target->momy = target->momz = 0;
|
|
if (target->target && target->target->health)
|
|
P_KillMobj(target->target, target, source, 0);
|
|
break;
|
|
|
|
case MT_PLAYER:
|
|
{
|
|
target->fuse = TICRATE*3; // timer before mobj disappears from view (even if not an actual player)
|
|
target->momx = target->momy = target->momz = 0;
|
|
|
|
if (damagetype == DMG_DROWNED) // drowned
|
|
{
|
|
target->movedir = damagetype; // we're MOVING the Damage Into anotheR function... Okay, this is a bit of a hack.
|
|
if (target->player->charflags & SF_MACHINE)
|
|
S_StartSound(target, sfx_fizzle);
|
|
else
|
|
S_StartSound(target, sfx_drown);
|
|
// Don't jump up when drowning
|
|
}
|
|
else
|
|
{
|
|
P_SetObjectMomZ(target, 14*FRACUNIT, false);
|
|
if (damagetype == DMG_SPIKE) // Spikes
|
|
S_StartSound(target, sfx_spkdth);
|
|
else
|
|
P_PlayDeathSound(target);
|
|
}
|
|
}
|
|
break;
|
|
case MT_METALSONIC_RACE:
|
|
target->fuse = TICRATE*3;
|
|
target->momx = target->momy = target->momz = 0;
|
|
P_SetObjectMomZ(target, 14*FRACUNIT, false);
|
|
target->flags = (target->flags & ~MF_NOGRAVITY)|(MF_NOCLIP|MF_NOCLIPTHING);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// 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(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(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(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(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(5, 7)*scale;
|
|
if (flip)
|
|
target->momz *= -1;
|
|
if (!sprflip)
|
|
target->frame |= FF_VERTICALFLIP;
|
|
}
|
|
else if (target->player)
|
|
{
|
|
if (damagetype == DMG_DROWNED || damagetype == DMG_SPACEDROWN)
|
|
P_SetPlayerMobjState(target, target->info->xdeathstate);
|
|
else
|
|
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 void P_NiGHTSDamage(mobj_t *target, mobj_t *source)
|
|
{
|
|
player_t *player = target->player;
|
|
tic_t oldnightstime = player->nightstime;
|
|
|
|
(void)source; // unused
|
|
|
|
if (!player->powers[pw_flashing])
|
|
{
|
|
angle_t fa;
|
|
|
|
player->angle_pos = player->old_angle_pos;
|
|
player->speed /= 5;
|
|
player->flyangle += 180; // Shuffle's BETTERNIGHTSMOVEMENT?
|
|
player->flyangle %= 360;
|
|
|
|
if (gametyperules & GTR_RACE)
|
|
player->drillmeter -= 5*20;
|
|
else
|
|
{
|
|
if (player->nightstime > 5*TICRATE)
|
|
player->nightstime -= 5*TICRATE;
|
|
else
|
|
player->nightstime = 1;
|
|
}
|
|
|
|
if (player->pflags & PF_TRANSFERTOCLOSEST)
|
|
{
|
|
target->momx = -target->momx;
|
|
target->momy = -target->momy;
|
|
}
|
|
else
|
|
{
|
|
fa = player->old_angle_pos>>ANGLETOFINESHIFT;
|
|
|
|
target->momx = FixedMul(FINECOSINE(fa),target->target->radius);
|
|
target->momy = FixedMul(FINESINE(fa),target->target->radius);
|
|
}
|
|
|
|
player->powers[pw_flashing] = flashingtics;
|
|
P_SetPlayerMobjState(target, S_PLAY_NIGHTS_STUN);
|
|
S_StartSound(target, sfx_nghurt);
|
|
|
|
player->mo->spriteroll = 0;
|
|
|
|
if (oldnightstime > 10*TICRATE
|
|
&& player->nightstime < 10*TICRATE)
|
|
{
|
|
if ((mapheaderinfo[gamemap-1]->levelflags & LF_MIXNIGHTSCOUNTDOWN)
|
|
#ifdef _WIN32
|
|
// win32 MIDI volume hack means we cannot fade down the music
|
|
&& S_MusicType() != MU_MID
|
|
#endif
|
|
)
|
|
{
|
|
S_FadeMusic(0, 10*MUSICRATE);
|
|
S_StartSound(NULL, sfx_timeup); // that creepy "out of time" music from NiGHTS.
|
|
}
|
|
else
|
|
P_PlayJingle(player, ((maptol & TOL_NIGHTS) && !G_IsSpecialStage(gamemap)) ? JT_NIGHTSTIMEOUT : JT_SSTIMEOUT);
|
|
}
|
|
}
|
|
}
|
|
|
|
static boolean P_TagDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
|
|
{
|
|
player_t *player = target->player;
|
|
(void)damage; //unused parm
|
|
|
|
// If flashing or invulnerable, ignore the tag,
|
|
if (player->powers[pw_flashing] || player->powers[pw_invulnerability])
|
|
return false;
|
|
|
|
// Don't allow any damage before the round starts.
|
|
if (leveltime <= hidetime * TICRATE)
|
|
return false;
|
|
|
|
// Ignore IT players shooting each other, unless friendlyfire is on.
|
|
if ((player->pflags & PF_TAGIT && !((cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE) || (damagetype & DMG_CANHURTSELF)) &&
|
|
source && source->player && source->player->pflags & PF_TAGIT)))
|
|
{
|
|
if (inflictor->type == MT_LHRT && !(player->powers[pw_shield] & SH_NOSTACK))
|
|
{
|
|
if (player->revitem != MT_LHRT && player->spinitem != MT_LHRT && player->thokitem != MT_LHRT) // Healers do not get to heal other healers.
|
|
{
|
|
P_SwitchShield(player, SH_PINK);
|
|
S_StartSound(target, mobjinfo[MT_PITY_ICON].seesound);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Don't allow players on the same team to hurt one another,
|
|
// unless cv_friendlyfire is on.
|
|
if (!(cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE) || (damagetype & DMG_CANHURTSELF)) && (player->pflags & PF_TAGIT) == (source->player->pflags & PF_TAGIT))
|
|
{
|
|
if (inflictor->type == MT_LHRT && !(player->powers[pw_shield] & SH_NOSTACK))
|
|
{
|
|
if (player->revitem != MT_LHRT && player->spinitem != MT_LHRT && player->thokitem != MT_LHRT) // Healers do not get to heal other healers.
|
|
{
|
|
P_SwitchShield(player, SH_PINK);
|
|
S_StartSound(target, mobjinfo[MT_PITY_ICON].seesound);
|
|
}
|
|
}
|
|
else if (!(inflictor->flags & MF_FIRE))
|
|
P_GivePlayerRings(player, 1);
|
|
if (inflictor->flags2 & MF2_BOUNCERING)
|
|
inflictor->fuse = 0; // bounce ring disappears at -1 not 0
|
|
return false;
|
|
}
|
|
|
|
if (inflictor->type == MT_LHRT)
|
|
return false;
|
|
|
|
// The tag occurs so long as you aren't shooting another tagger with friendlyfire on.
|
|
if (source->player->pflags & PF_TAGIT && !(player->pflags & PF_TAGIT))
|
|
{
|
|
P_AddPlayerScore(source->player, 100); //award points to tagger.
|
|
P_HitDeathMessages(player, inflictor, source, 0);
|
|
|
|
if (!(gametyperules & GTR_HIDEFROZEN)) //survivor
|
|
{
|
|
player->pflags |= PF_TAGIT; //in survivor, the player becomes IT and helps hunt down the survivors.
|
|
CONS_Printf(M_GetText("%s is now IT!\n"), player_names[player-players]); // Tell everyone who is it!
|
|
}
|
|
else
|
|
{
|
|
player->pflags |= PF_GAMETYPEOVER; //in hide and seek, the player is tagged and stays stationary.
|
|
CONS_Printf(M_GetText("%s was found!\n"), player_names[player-players]); // Tell everyone who is it!
|
|
}
|
|
|
|
//checks if tagger has tagged all players, if so, end round early.
|
|
P_CheckSurvivors();
|
|
}
|
|
|
|
P_DoPlayerPain(player, source, inflictor);
|
|
|
|
// Check for a shield
|
|
if (player->powers[pw_shield])
|
|
{
|
|
P_RemoveShield(player);
|
|
S_StartSound(target, sfx_shldls);
|
|
return true;
|
|
}
|
|
|
|
if (player->powers[pw_carry] == CR_NIGHTSFALL)
|
|
{
|
|
if (player->spheres > 0)
|
|
{
|
|
P_PlayRinglossSound(target);
|
|
P_PlayerRingBurst(player, player->spheres);
|
|
player->spheres = 0;
|
|
}
|
|
}
|
|
else if (player->rings > 0) // Ring loss
|
|
{
|
|
P_PlayRinglossSound(target);
|
|
P_PlayerRingBurst(player, player->rings);
|
|
player->rings = 0;
|
|
}
|
|
else // Death
|
|
{
|
|
P_PlayDeathSound(target);
|
|
P_PlayVictorySound(source); // Killer laughs at you! LAUGHS! BWAHAHAHHAHAA!!
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static boolean P_PlayerHitsPlayer(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
|
|
{
|
|
player_t *player = target->player;
|
|
|
|
if (!(damagetype & DMG_CANHURTSELF))
|
|
{
|
|
// You can't kill yourself, idiot...
|
|
if (source == target)
|
|
return false;
|
|
|
|
// In COOP/RACE, you can't hurt other players unless cv_friendlyfire is on
|
|
if (!(cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE)) && (gametyperules & GTR_FRIENDLY))
|
|
{
|
|
if ((gametyperules & GTR_FRIENDLY) && inflictor->type == MT_LHRT && !(player->powers[pw_shield] & SH_NOSTACK)) // co-op only
|
|
{
|
|
if (player->revitem != MT_LHRT && player->spinitem != MT_LHRT && player->thokitem != MT_LHRT) // Healers do not get to heal other healers.
|
|
{
|
|
P_SwitchShield(player, SH_PINK);
|
|
S_StartSound(target, mobjinfo[MT_PITY_ICON].seesound);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Tag handling
|
|
if (G_TagGametype())
|
|
return P_TagDamage(target, inflictor, source, damage, damagetype);
|
|
else if (damagetype & DMG_CANHURTSELF)
|
|
return true;
|
|
else if (G_GametypeHasTeams()) // CTF + Team Match
|
|
{
|
|
// Don't allow players on the same team to hurt one another,
|
|
// unless cv_friendlyfire is on.
|
|
if (!(cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE)) && target->player->ctfteam == source->player->ctfteam)
|
|
{
|
|
if (inflictor->type == MT_LHRT && !(player->powers[pw_shield] & SH_NOSTACK))
|
|
{
|
|
if (player->revitem != MT_LHRT && player->spinitem != MT_LHRT && player->thokitem != MT_LHRT) // Healers do not get to heal other healers.
|
|
{
|
|
P_SwitchShield(player, SH_PINK);
|
|
S_StartSound(target, mobjinfo[MT_PITY_ICON].seesound);
|
|
}
|
|
}
|
|
else if (!(inflictor->flags & MF_FIRE))
|
|
P_GivePlayerRings(target->player, 1);
|
|
if (inflictor->flags2 & MF2_BOUNCERING)
|
|
inflictor->fuse = 0; // bounce ring disappears at -1 not 0
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (inflictor->type == MT_LHRT)
|
|
return false;
|
|
|
|
// Add pity.
|
|
if (!player->powers[pw_flashing] && !player->powers[pw_invulnerability] && !player->powers[pw_super] && !(player->powers[pw_strong] & STR_GUARD)
|
|
&& source->player->score > player->score)
|
|
player->pity++;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void P_KillPlayer(player_t *player, mobj_t *source, INT32 damage)
|
|
{
|
|
player->pflags &= ~PF_SLIDING;
|
|
|
|
player->powers[pw_carry] = CR_NONE;
|
|
|
|
// Burst weapons and emeralds in Match/CTF only
|
|
if (source)
|
|
{
|
|
if ((gametyperules & GTR_RINGSLINGER) && !(gametyperules & GTR_TAG))
|
|
P_PlayerRingBurst(player, player->rings);
|
|
if (gametyperules & GTR_POWERSTONES)
|
|
P_PlayerEmeraldBurst(player, false);
|
|
}
|
|
|
|
// Get rid of shield
|
|
player->powers[pw_shield] = SH_NONE;
|
|
player->mo->color = player->skincolor;
|
|
|
|
// Get rid of emeralds
|
|
player->powers[pw_emeralds] = 0;
|
|
|
|
P_ForceFeed(player, 40, 10, TICRATE, 40 + min(damage, 100)*2);
|
|
|
|
P_ResetPlayer(player);
|
|
|
|
if (!player->spectator)
|
|
player->mo->flags2 &= ~MF2_DONTDRAW;
|
|
|
|
P_SetPlayerMobjState(player->mo, player->mo->info->deathstate);
|
|
if ((gametyperules & GTR_TEAMFLAGS) && (player->gotflag & (GF_REDFLAG|GF_BLUEFLAG)))
|
|
{
|
|
P_PlayerFlagBurst(player, false);
|
|
if (source && source->player)
|
|
{
|
|
// Award no points when players shoot each other when cv_friendlyfire is on.
|
|
if (!G_GametypeHasTeams() || !(source->player->ctfteam == player->ctfteam && source != player->mo))
|
|
P_AddPlayerScore(source->player, 25);
|
|
}
|
|
}
|
|
if (source && source->player && !player->powers[pw_super]) //don't score points against super players
|
|
{
|
|
// Award no points when players shoot each other when cv_friendlyfire is on.
|
|
if (!G_GametypeHasTeams() || !(source->player->ctfteam == player->ctfteam && source != player->mo))
|
|
P_AddPlayerScore(source->player, 100);
|
|
}
|
|
|
|
// If the player was super, tell them he/she ain't so super nomore.
|
|
if (!G_CoopGametype() && player->powers[pw_super])
|
|
{
|
|
S_StartSound(NULL, sfx_s3k66); //let all players hear it.
|
|
HU_SetCEchoFlags(0);
|
|
HU_SetCEchoDuration(5);
|
|
HU_DoCEcho(va("%s\\is no longer super.\\\\\\\\", player_names[player-players]));
|
|
}
|
|
}
|
|
|
|
static void P_SuperDamage(player_t *player, mobj_t *inflictor, mobj_t *source, INT32 damage)
|
|
{
|
|
fixed_t fallbackspeed;
|
|
angle_t ang;
|
|
|
|
P_ForceFeed(player, 40, 10, TICRATE, 40 + min(damage, 100)*2);
|
|
|
|
if (player->mo->eflags & MFE_VERTICALFLIP)
|
|
player->mo->z--;
|
|
else
|
|
player->mo->z++;
|
|
|
|
if (player->mo->eflags & MFE_UNDERWATER)
|
|
P_SetObjectMomZ(player->mo, FixedDiv(10511*FRACUNIT,2600*FRACUNIT), false);
|
|
else
|
|
P_SetObjectMomZ(player->mo, FixedDiv(69*FRACUNIT,10*FRACUNIT), false);
|
|
|
|
ang = R_PointToAngle2(inflictor->x, inflictor->y, player->mo->x, player->mo->y);
|
|
|
|
// explosion and rail rings send you farther back, making it more difficult
|
|
// to recover
|
|
if (inflictor->flags2 & MF2_SCATTER && source)
|
|
{
|
|
fixed_t dist = P_AproxDistance(P_AproxDistance(source->x-player->mo->x, source->y-player->mo->y), source->z-player->mo->z);
|
|
|
|
dist = FixedMul(128*FRACUNIT, inflictor->scale) - dist/4;
|
|
|
|
if (dist < FixedMul(4*FRACUNIT, inflictor->scale))
|
|
dist = FixedMul(4*FRACUNIT, inflictor->scale);
|
|
|
|
fallbackspeed = dist;
|
|
}
|
|
else if (inflictor->flags2 & MF2_EXPLOSION)
|
|
{
|
|
if (inflictor->flags2 & MF2_RAILRING)
|
|
fallbackspeed = FixedMul(28*FRACUNIT, inflictor->scale); // 7x
|
|
else
|
|
fallbackspeed = FixedMul(20*FRACUNIT, inflictor->scale); // 5x
|
|
}
|
|
else if (inflictor->flags2 & MF2_RAILRING)
|
|
fallbackspeed = FixedMul(16*FRACUNIT, inflictor->scale); // 4x
|
|
else
|
|
fallbackspeed = FixedMul(4*FRACUNIT, inflictor->scale); // the usual amount of force
|
|
|
|
P_InstaThrust(player->mo, ang, fallbackspeed);
|
|
|
|
P_SetPlayerMobjState(player->mo, S_PLAY_STUN);
|
|
|
|
P_ResetPlayer(player);
|
|
|
|
if (player->timeshit != UINT8_MAX)
|
|
++player->timeshit;
|
|
}
|
|
|
|
void P_RemoveShield(player_t *player)
|
|
{
|
|
if (player->powers[pw_shield] & SH_FORCE)
|
|
{ // Multi-hit
|
|
if (player->powers[pw_shield] & SH_FORCEHP)
|
|
player->powers[pw_shield]--;
|
|
else
|
|
player->powers[pw_shield] &= SH_STACK;
|
|
}
|
|
else if (player->powers[pw_shield] & SH_NOSTACK)
|
|
{ // First layer shields
|
|
if ((player->powers[pw_shield] & SH_NOSTACK) == SH_ARMAGEDDON) // Give them what's coming to them!
|
|
{
|
|
P_BlackOw(player); // BAM!
|
|
player->pflags |= PF_JUMPDOWN;
|
|
}
|
|
else
|
|
player->powers[pw_shield] &= SH_STACK;
|
|
}
|
|
else
|
|
{ // Second layer shields
|
|
if (((player->powers[pw_shield] & SH_STACK) == SH_FIREFLOWER) && !(player->powers[pw_super] || (mariomode && player->powers[pw_invulnerability])))
|
|
{
|
|
player->mo->color = player->skincolor;
|
|
G_GhostAddColor(GHC_NORMAL);
|
|
}
|
|
player->powers[pw_shield] = SH_NONE;
|
|
}
|
|
}
|
|
|
|
static void P_ShieldDamage(player_t *player, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
|
|
{
|
|
// Must do pain first to set flashing -- P_RemoveShield can cause damage
|
|
P_DoPlayerPain(player, source, inflictor);
|
|
|
|
P_RemoveShield(player);
|
|
|
|
P_ForceFeed(player, 40, 10, TICRATE, 40 + min(damage, 100)*2);
|
|
|
|
if (damagetype == DMG_SPIKE) // spikes
|
|
S_StartSound(player->mo, sfx_spkdth);
|
|
else
|
|
S_StartSound (player->mo, sfx_shldls); // Ba-Dum! Shield loss.
|
|
|
|
if ((gametyperules & GTR_TEAMFLAGS) && (player->gotflag & (GF_REDFLAG|GF_BLUEFLAG)))
|
|
{
|
|
P_PlayerFlagBurst(player, false);
|
|
if (source && source->player)
|
|
{
|
|
// Award no points when players shoot each other when cv_friendlyfire is on.
|
|
if (!G_GametypeHasTeams() || !(source->player->ctfteam == player->ctfteam && source != player->mo))
|
|
P_AddPlayerScore(source->player, 25);
|
|
}
|
|
}
|
|
if (source && source->player && !player->powers[pw_super]) //don't score points against super players
|
|
{
|
|
// Award no points when players shoot each other when cv_friendlyfire is on.
|
|
if (!G_GametypeHasTeams() || !(source->player->ctfteam == player->ctfteam && source != player->mo))
|
|
P_AddPlayerScore(source->player, 50);
|
|
}
|
|
}
|
|
|
|
static void P_RingDamage(player_t *player, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype, boolean dospheres)
|
|
{
|
|
P_DoPlayerPain(player, source, inflictor);
|
|
|
|
P_ForceFeed(player, 40, 10, TICRATE, 40 + min(damage, 100)*2);
|
|
|
|
if (damagetype == DMG_SPIKE) // spikes
|
|
S_StartSound(player->mo, sfx_spkdth);
|
|
|
|
if (source && source->player && !player->powers[pw_super]) //don't score points against super players
|
|
{
|
|
// Award no points when players shoot each other when cv_friendlyfire is on.
|
|
if (!G_GametypeHasTeams() || !(source->player->ctfteam == player->ctfteam && source != player->mo))
|
|
P_AddPlayerScore(source->player, 50);
|
|
}
|
|
|
|
if ((gametyperules & GTR_TEAMFLAGS) && (player->gotflag & (GF_REDFLAG|GF_BLUEFLAG)))
|
|
{
|
|
P_PlayerFlagBurst(player, false);
|
|
if (source && source->player)
|
|
{
|
|
// Award no points when players shoot each other when cv_friendlyfire is on.
|
|
if (!G_GametypeHasTeams() || !(source->player->ctfteam == player->ctfteam && source != player->mo))
|
|
P_AddPlayerScore(source->player, 25);
|
|
}
|
|
}
|
|
|
|
// Ring loss sound plays despite hitting spikes
|
|
P_PlayRinglossSound(player->mo); // Ringledingle!
|
|
P_PlayerRingBurst(player, damage);
|
|
|
|
if (dospheres)
|
|
{
|
|
player->spheres -= damage;
|
|
if (player->spheres < 0)
|
|
player->spheres = 0;
|
|
}
|
|
else
|
|
{
|
|
player->rings -= damage;
|
|
if (player->rings < 0)
|
|
player->rings = 0;
|
|
}
|
|
}
|
|
|
|
//
|
|
// P_SpecialStageDamage
|
|
//
|
|
// Do old special stage-style damaging
|
|
// Removes 5 seconds from the player, or knocks off their shield if they have one.
|
|
// If they don't have anything, just knock the player back anyway (this doesn't kill them).
|
|
//
|
|
void P_SpecialStageDamage(player_t *player, mobj_t *inflictor, mobj_t *source)
|
|
{
|
|
tic_t oldnightstime = player->nightstime;
|
|
|
|
if (player->powers[pw_invulnerability] || player->powers[pw_flashing] || player->powers[pw_super])
|
|
return;
|
|
|
|
if (!cv_friendlyfire.value && source && source->player)
|
|
{
|
|
if (inflictor->type == MT_LHRT && !(player->powers[pw_shield] & SH_NOSTACK))
|
|
{
|
|
if (player->revitem != MT_LHRT && player->spinitem != MT_LHRT && player->thokitem != MT_LHRT) // Healers do not get to heal other healers.
|
|
{
|
|
P_SwitchShield(player, SH_PINK);
|
|
S_StartSound(player->mo, mobjinfo[MT_PITY_ICON].seesound);
|
|
}
|
|
}
|
|
|
|
if (source->player->ctfteam == player->ctfteam)
|
|
return;
|
|
}
|
|
|
|
if (inflictor && inflictor->type == MT_LHRT)
|
|
return;
|
|
|
|
if (player->powers[pw_shield] || (player->bot && player->bot != BOT_MPAI)) //If One-Hit Shield
|
|
{
|
|
P_RemoveShield(player);
|
|
S_StartSound(player->mo, sfx_shldls); // Ba-Dum! Shield loss.
|
|
}
|
|
else
|
|
{
|
|
S_StartSound(player->mo, sfx_nghurt);
|
|
if (player->nightstime > 5*TICRATE)
|
|
player->nightstime -= 5*TICRATE;
|
|
else
|
|
player->nightstime = 0;
|
|
}
|
|
|
|
P_DoPlayerPain(player, inflictor, source);
|
|
|
|
if ((gametyperules & GTR_TEAMFLAGS) && player->gotflag & (GF_REDFLAG|GF_BLUEFLAG))
|
|
P_PlayerFlagBurst(player, false);
|
|
|
|
if (oldnightstime > 10*TICRATE
|
|
&& player->nightstime < 10*TICRATE)
|
|
{
|
|
if (mapheaderinfo[gamemap-1]->levelflags & LF_MIXNIGHTSCOUNTDOWN)
|
|
{
|
|
S_FadeMusic(0, 10*MUSICRATE);
|
|
S_StartSound(NULL, sfx_timeup); // that creepy "out of time" music from NiGHTS.
|
|
}
|
|
else
|
|
S_ChangeMusicInternal((((maptol & TOL_NIGHTS) && !G_IsSpecialStage(gamemap)) ? "_ntime" : "_drown"), false);
|
|
}
|
|
}
|
|
|
|
/** 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)
|
|
{
|
|
player_t *player;
|
|
boolean force = false;
|
|
|
|
if (objectplacing)
|
|
return false;
|
|
|
|
if (target->health <= 0)
|
|
return false;
|
|
|
|
// Spectator handling
|
|
if (multiplayer)
|
|
{
|
|
if (damagetype != DMG_SPECTATOR && target->player && target->player->spectator)
|
|
return false;
|
|
|
|
if (source && source->player && source->player->spectator)
|
|
return false;
|
|
}
|
|
|
|
// Everything above here can't be forced.
|
|
if (!metalrecording)
|
|
{
|
|
UINT8 shouldForce = LUA_HookShouldDamage(target, inflictor, source, damage, damagetype);
|
|
if (P_MobjWasRemoved(target))
|
|
return (shouldForce == 1); // mobj was removed
|
|
if (P_MobjWasRemoved(source))
|
|
source = NULL;
|
|
if (shouldForce == 1)
|
|
force = true;
|
|
else if (shouldForce == 2)
|
|
return false;
|
|
}
|
|
|
|
if (!force)
|
|
{
|
|
if (!(target->flags & MF_SHOOTABLE))
|
|
return false; // shouldn't happen...
|
|
|
|
if (target->type == MT_BLACKEGGMAN)
|
|
return false;
|
|
|
|
// Make sure that boxes cannot be popped by enemies, red rings, etc.
|
|
if (target->flags & MF_MONITOR && ((!source || !source->player || (source->player->bot && source->player->bot != BOT_MPAI))
|
|
|| (inflictor && (inflictor->type == MT_REDRING || (inflictor->type >= MT_THROWNBOUNCE && inflictor->type <= MT_THROWNGRENADE)))))
|
|
return false;
|
|
}
|
|
|
|
if (target->flags2 & MF2_SKULLFLY)
|
|
target->momx = target->momy = target->momz = 0;
|
|
|
|
if (!force)
|
|
{
|
|
// Special case for team ring boxes
|
|
if (target->type == MT_RING_REDBOX && !(source->player->ctfteam == 1))
|
|
return false;
|
|
|
|
if (target->type == MT_RING_BLUEBOX && !(source->player->ctfteam == 2))
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
|
|
if (player) // Player is the target
|
|
{
|
|
if (!force)
|
|
{
|
|
if (player->exiting)
|
|
return false;
|
|
|
|
if (player->pflags & PF_GODMODE)
|
|
return false;
|
|
|
|
if ((maptol & TOL_NIGHTS) && target->player->powers[pw_carry] != CR_NIGHTSMODE && target->player->powers[pw_carry] != CR_NIGHTSFALL)
|
|
return false;
|
|
|
|
switch (damagetype)
|
|
{
|
|
#define DAMAGECASE(type)\
|
|
case DMG_##type:\
|
|
if (player->powers[pw_shield] & SH_PROTECT##type)\
|
|
return false;\
|
|
break
|
|
DAMAGECASE(WATER);
|
|
DAMAGECASE(FIRE);
|
|
DAMAGECASE(ELECTRIC);
|
|
DAMAGECASE(SPIKE);
|
|
#undef DAMAGECASE
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (player->powers[pw_carry] == CR_NIGHTSMODE) // NiGHTS damage handling
|
|
{
|
|
if (!force)
|
|
{
|
|
if (source == target)
|
|
return false; // Don't hit yourself with your own paraloop, baka
|
|
if (source && source->player && !(cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE))
|
|
&& ((gametyperules & GTR_FRIENDLY)
|
|
|| (G_GametypeHasTeams() && player->ctfteam == source->player->ctfteam)))
|
|
return false; // Don't run eachother over in special stages and team games and such
|
|
}
|
|
if (LUA_HookMobjDamage(target, inflictor, source, damage, damagetype))
|
|
return true;
|
|
P_NiGHTSDamage(target, source); // -5s :(
|
|
return true;
|
|
}
|
|
|
|
if (G_IsSpecialStage(gamemap) && !(damagetype & DMG_DEATHMASK))
|
|
{
|
|
P_SpecialStageDamage(player, inflictor, source);
|
|
return true;
|
|
}
|
|
|
|
if (!force && inflictor && inflictor->flags & MF_FIRE && !(damagetype && damagetype != DMG_FIRE))
|
|
{
|
|
if (player->powers[pw_shield] & SH_PROTECTFIRE)
|
|
return false; // Invincible to fire objects
|
|
|
|
if (G_PlatformGametype() && inflictor && source && source->player)
|
|
return false; // Don't get hurt by fire generated from friends.
|
|
}
|
|
|
|
// Player hits another player
|
|
if (!force && source && source->player)
|
|
{
|
|
if (!P_PlayerHitsPlayer(target, inflictor, source, damage, damagetype))
|
|
return false;
|
|
}
|
|
|
|
// Instant-Death
|
|
if (damagetype & DMG_DEATHMASK)
|
|
P_KillPlayer(player, source, damage);
|
|
else if (metalrecording)
|
|
{
|
|
if (!inflictor)
|
|
inflictor = source;
|
|
if (inflictor && inflictor->flags & MF_ENEMY)
|
|
{ // Metal Sonic destroy enemy !!
|
|
P_KillMobj(inflictor, NULL, target, damagetype);
|
|
return false;
|
|
}
|
|
else if (inflictor && inflictor->flags & MF_MISSILE)
|
|
return false; // Metal Sonic walk through flame !!
|
|
else if (!player->powers[pw_flashing])
|
|
{ // Oh no! Metal Sonic is hit !!
|
|
P_ShieldDamage(player, inflictor, source, damage, damagetype);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
else if (player->powers[pw_invulnerability] || player->powers[pw_flashing] || player->powers[pw_super] || (player->powers[pw_strong] & STR_GUARD)) // ignore bouncing & such in invulnerability
|
|
{
|
|
if (force
|
|
|| (inflictor && inflictor->flags & MF_MISSILE && inflictor->flags2 & MF2_SUPERFIRE)) // Super Sonic is stunned!
|
|
{
|
|
if (!LUA_HookMobjDamage(target, inflictor, source, damage, damagetype))
|
|
P_SuperDamage(player, inflictor, source, damage);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
else if (LUA_HookMobjDamage(target, inflictor, source, damage, damagetype))
|
|
return true;
|
|
else if (player->powers[pw_shield] || (player->bot && player->bot != BOT_MPAI && !ultimatemode)) //If One-Hit Shield
|
|
{
|
|
P_ShieldDamage(player, inflictor, source, damage, damagetype);
|
|
damage = 0;
|
|
}
|
|
else if (player->powers[pw_carry] == CR_NIGHTSFALL)
|
|
{
|
|
// always damage so we can recoil upon losing points
|
|
damage = player->spheres;
|
|
P_RingDamage(player, inflictor, source, damage, damagetype, true);
|
|
damage = 0;
|
|
}
|
|
else if (player->rings > 0) // No shield but have rings.
|
|
{
|
|
damage = player->rings;
|
|
P_RingDamage(player, inflictor, source, damage, damagetype, false);
|
|
damage = 0;
|
|
}
|
|
// To reduce griefing potential, don't allow players to be killed
|
|
// by friendly fire. Spilling their rings and other items is enough.
|
|
else if (!force && G_GametypeHasTeams()
|
|
&& source && source->player && (source->player->ctfteam == player->ctfteam)
|
|
&& (cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE)))
|
|
{
|
|
damage = 0;
|
|
P_ShieldDamage(player, inflictor, source, damage, damagetype);
|
|
}
|
|
else // No shield, no rings, no invincibility.
|
|
{
|
|
damage = 1;
|
|
P_KillPlayer(player, source, damage);
|
|
}
|
|
|
|
P_ForceFeed(player, 40, 10, TICRATE, 40 + min(damage, 100)*2);
|
|
}
|
|
|
|
// Killing dead. Just for kicks.
|
|
// Require source and inflictor be player. Don't hurt for firing rings.
|
|
if (cv_killingdead.value && (source && source->player) && (inflictor && inflictor->player) && P_RandomChance(5*FRACUNIT/16))
|
|
P_DamageMobj(source, target, target, 1, 0);
|
|
|
|
// do the damage
|
|
if (damagetype & DMG_DEATHMASK)
|
|
target->health = 0;
|
|
else
|
|
target->health -= damage;
|
|
|
|
if (player)
|
|
P_HitDeathMessages(player, inflictor, source, damagetype);
|
|
|
|
if (source && source->player && target)
|
|
G_GhostAddHit(target);
|
|
|
|
if (target->health <= 0)
|
|
{
|
|
P_KillMobj(target, inflictor, source, damagetype);
|
|
return true;
|
|
}
|
|
|
|
if (player)
|
|
P_ResetPlayer(target->player);
|
|
else if ((target->type == MT_EGGMOBILE2) // egg slimer
|
|
&& (target->health < target->info->damage)) // in pinch phase
|
|
P_SetMobjState(target, target->info->meleestate); // go to pinch pain state
|
|
else
|
|
P_SetMobjState(target, target->info->painstate);
|
|
|
|
if (P_MobjWasRemoved(target))
|
|
return false;
|
|
|
|
if (target->type == MT_HIVEELEMENTAL)
|
|
target->extravalue1 += 3;
|
|
|
|
target->reactiontime = 0; // we're awake now...
|
|
|
|
if (source && source != target)
|
|
{
|
|
// if not intent on another player,
|
|
// chase after this one
|
|
P_SetTarget(&target->target, source);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/** Spills an injured player's rings.
|
|
*
|
|
* \param player The player who is losing rings.
|
|
* \param num_rings Number of rings lost. A maximum of 32 rings will be
|
|
* spawned.
|
|
* \sa P_PlayerFlagBurst
|
|
*/
|
|
void P_PlayerRingBurst(player_t *player, INT32 num_rings)
|
|
{
|
|
INT32 i;
|
|
mobj_t *mo;
|
|
angle_t fa, va;
|
|
fixed_t ns;
|
|
fixed_t z;
|
|
boolean nightsreplace = ((maptol & TOL_NIGHTS) && !G_IsSpecialStage(gamemap));
|
|
|
|
// Better safe than sorry.
|
|
if (!player)
|
|
return;
|
|
|
|
// If no health, don't spawn ring!
|
|
if (((maptol & TOL_NIGHTS) && player->spheres <= 0) || (!(maptol & TOL_NIGHTS) && player->rings <= 0))
|
|
num_rings = 0;
|
|
|
|
if (num_rings > 32 && player->powers[pw_carry] != CR_NIGHTSFALL)
|
|
num_rings = 32;
|
|
|
|
if (player->powers[pw_emeralds])
|
|
P_PlayerEmeraldBurst(player, false);
|
|
|
|
// Spill weapons first
|
|
P_PlayerWeaponPanelOrAmmoBurst(player);
|
|
|
|
if (abs(player->mo->momx) > player->mo->scale || abs(player->mo->momy) > player->mo->scale)
|
|
va = R_PointToAngle2(player->mo->momx, player->mo->momy, 0, 0)>>ANGLETOFINESHIFT;
|
|
else
|
|
va = player->mo->angle>>ANGLETOFINESHIFT;
|
|
|
|
for (i = 0; i < num_rings; i++)
|
|
{
|
|
INT32 objType = mobjinfo[MT_RING].reactiontime;
|
|
if (mariomode)
|
|
objType = mobjinfo[MT_COIN].reactiontime;
|
|
else if (player->powers[pw_carry] == CR_NIGHTSFALL)
|
|
objType = mobjinfo[(nightsreplace ? MT_NIGHTSCHIP : MT_BLUESPHERE)].reactiontime;
|
|
|
|
z = player->mo->z;
|
|
if (player->mo->eflags & MFE_VERTICALFLIP)
|
|
z += player->mo->height - mobjinfo[objType].height;
|
|
|
|
mo = P_SpawnMobj(player->mo->x, player->mo->y, z, objType);
|
|
|
|
mo->fuse = 8*TICRATE;
|
|
P_SetTarget(&mo->target, player->mo);
|
|
|
|
mo->destscale = player->mo->scale;
|
|
P_SetScale(mo, player->mo->scale);
|
|
|
|
// Angle offset by player angle, then slightly offset by amount of rings
|
|
fa = ((i*FINEANGLES/16) + va - ((num_rings-1)*FINEANGLES/32)) & FINEMASK;
|
|
|
|
// Make rings spill out around the player in 16 directions like SA, but spill like Sonic 2.
|
|
// Technically a non-SA way of spilling rings. They just so happen to be a little similar.
|
|
if (player->powers[pw_carry] == CR_NIGHTSFALL)
|
|
{
|
|
ns = FixedMul(((i*FRACUNIT)/16)+2*FRACUNIT, mo->scale);
|
|
mo->momx = FixedMul(FINECOSINE(fa),ns);
|
|
|
|
if (!(twodlevel || (player->mo->flags2 & MF2_TWOD)))
|
|
mo->momy = FixedMul(FINESINE(fa),ns);
|
|
|
|
P_SetObjectMomZ(mo, 8*FRACUNIT, false);
|
|
mo->fuse = 20*TICRATE; // Adjust fuse for NiGHTS
|
|
|
|
// Toggle bonus time colors
|
|
P_SetMobjState(mo, (player->bonustime ? mo->info->raisestate : mo->info->spawnstate));
|
|
}
|
|
else
|
|
{
|
|
fixed_t momxy, momz; // base horizonal/vertical thrusts
|
|
|
|
if (i > 15)
|
|
{
|
|
momxy = 3*FRACUNIT;
|
|
momz = 4*FRACUNIT;
|
|
}
|
|
else
|
|
{
|
|
momxy = 2*FRACUNIT;
|
|
momz = 3*FRACUNIT;
|
|
}
|
|
|
|
ns = FixedMul(FixedMul(momxy, FRACUNIT + FixedDiv(player->losstime<<FRACBITS, 10*TICRATE<<FRACBITS)), mo->scale);
|
|
mo->momx = FixedMul(FINECOSINE(fa),ns);
|
|
|
|
if (!(twodlevel || (player->mo->flags2 & MF2_TWOD)))
|
|
mo->momy = FixedMul(FINESINE(fa),ns);
|
|
|
|
ns = FixedMul(momz, FRACUNIT + FixedDiv(player->losstime<<FRACBITS, 10*TICRATE<<FRACBITS));
|
|
P_SetObjectMomZ(mo, ns, false);
|
|
|
|
if (i & 1)
|
|
P_SetObjectMomZ(mo, ns, true);
|
|
}
|
|
if (player->mo->eflags & MFE_VERTICALFLIP)
|
|
{
|
|
mo->momz *= -1;
|
|
mo->flags2 |= MF2_OBJECTFLIP;
|
|
}
|
|
}
|
|
|
|
player->losstime += 10*TICRATE;
|
|
|
|
return;
|
|
}
|
|
|
|
void P_PlayerWeaponPanelBurst(player_t *player)
|
|
{
|
|
mobj_t *mo;
|
|
angle_t fa;
|
|
fixed_t ns;
|
|
INT32 i;
|
|
fixed_t z;
|
|
|
|
INT32 num_weapons = M_CountBits((UINT32)player->ringweapons, NUM_WEAPONS-1);
|
|
UINT16 ammoamt = 0;
|
|
|
|
for (i = 0; i < num_weapons; i++)
|
|
{
|
|
mobjtype_t weptype = 0;
|
|
powertype_t power = 0;
|
|
|
|
if (player->ringweapons & RW_BOUNCE) // Bounce
|
|
{
|
|
weptype = MT_BOUNCEPICKUP;
|
|
player->ringweapons &= ~RW_BOUNCE;
|
|
power = pw_bouncering;
|
|
}
|
|
else if (player->ringweapons & RW_RAIL) // Rail
|
|
{
|
|
weptype = MT_RAILPICKUP;
|
|
player->ringweapons &= ~RW_RAIL;
|
|
power = pw_railring;
|
|
}
|
|
else if (player->ringweapons & RW_AUTO) // Auto
|
|
{
|
|
weptype = MT_AUTOPICKUP;
|
|
player->ringweapons &= ~RW_AUTO;
|
|
power = pw_automaticring;
|
|
}
|
|
else if (player->ringweapons & RW_EXPLODE) // Explode
|
|
{
|
|
weptype = MT_EXPLODEPICKUP;
|
|
player->ringweapons &= ~RW_EXPLODE;
|
|
power = pw_explosionring;
|
|
}
|
|
else if (player->ringweapons & RW_SCATTER) // Scatter
|
|
{
|
|
weptype = MT_SCATTERPICKUP;
|
|
player->ringweapons &= ~RW_SCATTER;
|
|
power = pw_scatterring;
|
|
}
|
|
else if (player->ringweapons & RW_GRENADE) // Grenade
|
|
{
|
|
weptype = MT_GRENADEPICKUP;
|
|
player->ringweapons &= ~RW_GRENADE;
|
|
power = pw_grenadering;
|
|
}
|
|
|
|
if (!weptype) // ???
|
|
continue;
|
|
|
|
if (player->powers[power] >= mobjinfo[weptype].reactiontime)
|
|
ammoamt = (UINT16)mobjinfo[weptype].reactiontime;
|
|
else
|
|
ammoamt = player->powers[power];
|
|
|
|
player->powers[power] -= ammoamt;
|
|
|
|
z = player->mo->z;
|
|
if (player->mo->eflags & MFE_VERTICALFLIP)
|
|
z += player->mo->height - mobjinfo[weptype].height;
|
|
|
|
mo = P_SpawnMobj(player->mo->x, player->mo->y, z, weptype);
|
|
mo->reactiontime = ammoamt;
|
|
mo->flags2 |= MF2_DONTRESPAWN;
|
|
mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT);
|
|
P_SetTarget(&mo->target, player->mo);
|
|
mo->fuse = 12*TICRATE;
|
|
mo->destscale = player->mo->scale;
|
|
P_SetScale(mo, player->mo->scale);
|
|
|
|
// Angle offset by player angle
|
|
fa = ((i*FINEANGLES/16) + (player->mo->angle>>ANGLETOFINESHIFT)) & FINEMASK;
|
|
|
|
// Make rings spill out around the player in 16 directions like SA, but spill like Sonic 2.
|
|
// Technically a non-SA way of spilling rings. They just so happen to be a little similar.
|
|
|
|
// >16 ring type spillout
|
|
ns = FixedMul(3*FRACUNIT, mo->scale);
|
|
mo->momx = FixedMul(FINECOSINE(fa),ns);
|
|
|
|
if (!(twodlevel || (player->mo->flags2 & MF2_TWOD)))
|
|
mo->momy = FixedMul(FINESINE(fa),ns);
|
|
|
|
P_SetObjectMomZ(mo, 4*FRACUNIT, false);
|
|
|
|
if (i & 1)
|
|
P_SetObjectMomZ(mo, 4*FRACUNIT, true);
|
|
}
|
|
}
|
|
|
|
void P_PlayerWeaponAmmoBurst(player_t *player)
|
|
{
|
|
mobj_t *mo;
|
|
angle_t fa;
|
|
fixed_t ns;
|
|
INT32 i = 0;
|
|
fixed_t z;
|
|
|
|
mobjtype_t weptype = 0;
|
|
powertype_t power = 0;
|
|
|
|
while (true)
|
|
{
|
|
if (player->powers[pw_bouncering])
|
|
{
|
|
weptype = MT_BOUNCERING;
|
|
power = pw_bouncering;
|
|
}
|
|
else if (player->powers[pw_railring])
|
|
{
|
|
weptype = MT_RAILRING;
|
|
power = pw_railring;
|
|
}
|
|
else if (player->powers[pw_infinityring])
|
|
{
|
|
weptype = MT_INFINITYRING;
|
|
power = pw_infinityring;
|
|
}
|
|
else if (player->powers[pw_automaticring])
|
|
{
|
|
weptype = MT_AUTOMATICRING;
|
|
power = pw_automaticring;
|
|
}
|
|
else if (player->powers[pw_explosionring])
|
|
{
|
|
weptype = MT_EXPLOSIONRING;
|
|
power = pw_explosionring;
|
|
}
|
|
else if (player->powers[pw_scatterring])
|
|
{
|
|
weptype = MT_SCATTERRING;
|
|
power = pw_scatterring;
|
|
}
|
|
else if (player->powers[pw_grenadering])
|
|
{
|
|
weptype = MT_GRENADERING;
|
|
power = pw_grenadering;
|
|
}
|
|
else
|
|
break; // All done!
|
|
|
|
z = player->mo->z;
|
|
if (player->mo->eflags & MFE_VERTICALFLIP)
|
|
z += player->mo->height - mobjinfo[weptype].height;
|
|
|
|
mo = P_SpawnMobj(player->mo->x, player->mo->y, z, weptype);
|
|
mo->health = player->powers[power];
|
|
mo->flags2 |= MF2_DONTRESPAWN;
|
|
mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT);
|
|
P_SetTarget(&mo->target, player->mo);
|
|
|
|
player->powers[power] = 0;
|
|
mo->fuse = 12*TICRATE;
|
|
|
|
mo->destscale = player->mo->scale;
|
|
P_SetScale(mo, player->mo->scale);
|
|
|
|
// Angle offset by player angle
|
|
fa = ((i*FINEANGLES/16) + (player->mo->angle>>ANGLETOFINESHIFT)) & FINEMASK;
|
|
|
|
// Spill them!
|
|
ns = FixedMul(2*FRACUNIT, mo->scale);
|
|
mo->momx = FixedMul(FINECOSINE(fa), ns);
|
|
|
|
if (!(twodlevel || (player->mo->flags2 & MF2_TWOD)))
|
|
mo->momy = FixedMul(FINESINE(fa),ns);
|
|
|
|
P_SetObjectMomZ(mo, 3*FRACUNIT, false);
|
|
|
|
if (i & 1)
|
|
P_SetObjectMomZ(mo, 3*FRACUNIT, true);
|
|
|
|
i++;
|
|
}
|
|
}
|
|
|
|
void P_PlayerWeaponPanelOrAmmoBurst(player_t *player)
|
|
{
|
|
mobj_t *mo;
|
|
angle_t fa;
|
|
fixed_t ns;
|
|
INT32 i = 0;
|
|
fixed_t z;
|
|
|
|
#define SETUP_DROP(thingtype) \
|
|
z = player->mo->z; \
|
|
if (player->mo->eflags & MFE_VERTICALFLIP) \
|
|
z += player->mo->height - mobjinfo[thingtype].height; \
|
|
fa = ((i*FINEANGLES/16) + (player->mo->angle>>ANGLETOFINESHIFT)) & FINEMASK; \
|
|
ns = FixedMul(3*FRACUNIT, player->mo->scale); \
|
|
|
|
#define DROP_WEAPON(rwflag, pickup, ammo, power) \
|
|
if (player->ringweapons & rwflag) \
|
|
{ \
|
|
player->ringweapons &= ~rwflag; \
|
|
SETUP_DROP(pickup) \
|
|
mo = P_SpawnMobj(player->mo->x, player->mo->y, z, pickup); \
|
|
mo->reactiontime = 0; \
|
|
mo->flags2 |= MF2_DONTRESPAWN; \
|
|
mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT); \
|
|
P_SetTarget(&mo->target, player->mo); \
|
|
mo->fuse = 12*TICRATE; \
|
|
mo->destscale = player->mo->scale; \
|
|
P_SetScale(mo, player->mo->scale); \
|
|
mo->momx = FixedMul(FINECOSINE(fa),ns); \
|
|
if (!(twodlevel || (player->mo->flags2 & MF2_TWOD))) \
|
|
mo->momy = FixedMul(FINESINE(fa),ns); \
|
|
P_SetObjectMomZ(mo, 4*FRACUNIT, false); \
|
|
if (i & 1) \
|
|
P_SetObjectMomZ(mo, 4*FRACUNIT, true); \
|
|
if (player->mo->eflags & MFE_VERTICALFLIP) \
|
|
mo->flags2 |= MF2_OBJECTFLIP; \
|
|
++i; \
|
|
} \
|
|
else if (player->powers[power] > 0) \
|
|
{ \
|
|
SETUP_DROP(ammo) \
|
|
mo = P_SpawnMobj(player->mo->x, player->mo->y, z, ammo); \
|
|
mo->health = player->powers[power]; \
|
|
mo->flags2 |= MF2_DONTRESPAWN; \
|
|
mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT); \
|
|
P_SetTarget(&mo->target, player->mo); \
|
|
mo->fuse = 12*TICRATE; \
|
|
mo->destscale = player->mo->scale; \
|
|
P_SetScale(mo, player->mo->scale); \
|
|
mo->momx = FixedMul(FINECOSINE(fa),ns); \
|
|
if (!(twodlevel || (player->mo->flags2 & MF2_TWOD))) \
|
|
mo->momy = FixedMul(FINESINE(fa),ns); \
|
|
P_SetObjectMomZ(mo, 3*FRACUNIT, false); \
|
|
if (i & 1) \
|
|
P_SetObjectMomZ(mo, 3*FRACUNIT, true); \
|
|
if (player->mo->eflags & MFE_VERTICALFLIP) \
|
|
mo->flags2 |= MF2_OBJECTFLIP; \
|
|
player->powers[power] = 0; \
|
|
++i; \
|
|
}
|
|
|
|
DROP_WEAPON(RW_BOUNCE, MT_BOUNCEPICKUP, MT_BOUNCERING, pw_bouncering);
|
|
DROP_WEAPON(RW_RAIL, MT_RAILPICKUP, MT_RAILRING, pw_railring);
|
|
DROP_WEAPON(RW_AUTO, MT_AUTOPICKUP, MT_AUTOMATICRING, pw_automaticring);
|
|
DROP_WEAPON(RW_EXPLODE, MT_EXPLODEPICKUP, MT_EXPLOSIONRING, pw_explosionring);
|
|
DROP_WEAPON(RW_SCATTER, MT_SCATTERPICKUP, MT_SCATTERRING, pw_scatterring);
|
|
DROP_WEAPON(RW_GRENADE, MT_GRENADEPICKUP, MT_GRENADERING, pw_grenadering);
|
|
DROP_WEAPON(0, 0, MT_INFINITYRING, pw_infinityring);
|
|
|
|
#undef DROP_WEAPON
|
|
#undef SETUP_DROP
|
|
}
|
|
|
|
//
|
|
// P_PlayerEmeraldBurst
|
|
//
|
|
// Spills ONLY emeralds.
|
|
//
|
|
void P_PlayerEmeraldBurst(player_t *player, boolean toss)
|
|
{
|
|
INT32 i;
|
|
angle_t fa;
|
|
fixed_t ns;
|
|
fixed_t z = 0, momx = 0, momy = 0;
|
|
|
|
// Better safe than sorry.
|
|
if (!player)
|
|
return;
|
|
|
|
// Spill power stones
|
|
if (player->powers[pw_emeralds])
|
|
{
|
|
INT32 num_stones = 0;
|
|
|
|
if (player->powers[pw_emeralds] & EMERALD1)
|
|
num_stones++;
|
|
if (player->powers[pw_emeralds] & EMERALD2)
|
|
num_stones++;
|
|
if (player->powers[pw_emeralds] & EMERALD3)
|
|
num_stones++;
|
|
if (player->powers[pw_emeralds] & EMERALD4)
|
|
num_stones++;
|
|
if (player->powers[pw_emeralds] & EMERALD5)
|
|
num_stones++;
|
|
if (player->powers[pw_emeralds] & EMERALD6)
|
|
num_stones++;
|
|
if (player->powers[pw_emeralds] & EMERALD7)
|
|
num_stones++;
|
|
|
|
for (i = 0; i < num_stones; i++)
|
|
{
|
|
INT32 stoneflag = 0;
|
|
statenum_t statenum = S_CEMG1;
|
|
mobj_t *mo;
|
|
|
|
if (player->powers[pw_emeralds] & EMERALD1)
|
|
{
|
|
stoneflag = EMERALD1;
|
|
statenum = S_CEMG1;
|
|
}
|
|
else if (player->powers[pw_emeralds] & EMERALD2)
|
|
{
|
|
stoneflag = EMERALD2;
|
|
statenum = S_CEMG2;
|
|
}
|
|
else if (player->powers[pw_emeralds] & EMERALD3)
|
|
{
|
|
stoneflag = EMERALD3;
|
|
statenum = S_CEMG3;
|
|
}
|
|
else if (player->powers[pw_emeralds] & EMERALD4)
|
|
{
|
|
stoneflag = EMERALD4;
|
|
statenum = S_CEMG4;
|
|
}
|
|
else if (player->powers[pw_emeralds] & EMERALD5)
|
|
{
|
|
stoneflag = EMERALD5;
|
|
statenum = S_CEMG5;
|
|
}
|
|
else if (player->powers[pw_emeralds] & EMERALD6)
|
|
{
|
|
stoneflag = EMERALD6;
|
|
statenum = S_CEMG6;
|
|
}
|
|
else if (player->powers[pw_emeralds] & EMERALD7)
|
|
{
|
|
stoneflag = EMERALD7;
|
|
statenum = S_CEMG7;
|
|
}
|
|
|
|
if (!stoneflag) // ???
|
|
continue;
|
|
|
|
player->powers[pw_emeralds] &= ~stoneflag;
|
|
|
|
if (toss)
|
|
{
|
|
fa = player->mo->angle>>ANGLETOFINESHIFT;
|
|
|
|
z = player->mo->z + player->mo->height;
|
|
if (player->mo->eflags & MFE_VERTICALFLIP)
|
|
z -= mobjinfo[MT_FLINGEMERALD].height + player->mo->height;
|
|
ns = FixedMul(8*FRACUNIT, player->mo->scale);
|
|
}
|
|
else
|
|
{
|
|
fa = ((255 / num_stones) * i) * FINEANGLES/256;
|
|
|
|
z = player->mo->z + (player->mo->height / 2);
|
|
if (player->mo->eflags & MFE_VERTICALFLIP)
|
|
z -= mobjinfo[MT_FLINGEMERALD].height;
|
|
ns = FixedMul(4*FRACUNIT, player->mo->scale);
|
|
}
|
|
|
|
momx = FixedMul(FINECOSINE(fa), ns);
|
|
|
|
if (!(twodlevel || (player->mo->flags2 & MF2_TWOD)))
|
|
momy = FixedMul(FINESINE(fa),ns);
|
|
else
|
|
momy = 0;
|
|
|
|
mo = P_SpawnMobj(player->mo->x, player->mo->y, z, MT_FLINGEMERALD);
|
|
mo->health = 1;
|
|
mo->threshold = stoneflag;
|
|
mo->flags2 |= (MF2_DONTRESPAWN|MF2_SLIDEPUSH);
|
|
mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT);
|
|
P_SetTarget(&mo->target, player->mo);
|
|
mo->fuse = 12*TICRATE;
|
|
P_SetMobjState(mo, statenum);
|
|
|
|
mo->momx = momx;
|
|
mo->momy = momy;
|
|
|
|
P_SetObjectMomZ(mo, 3*FRACUNIT, false);
|
|
|
|
if (player->mo->eflags & MFE_VERTICALFLIP)
|
|
{
|
|
mo->momz = -mo->momz;
|
|
mo->flags2 |= MF2_OBJECTFLIP;
|
|
}
|
|
|
|
if (toss)
|
|
player->tossdelay = 2*TICRATE;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Makes an injured or dead player lose possession of the flag.
|
|
*
|
|
* \param player The player with the flag, about to lose it.
|
|
* \sa P_PlayerRingBurst
|
|
*/
|
|
void P_PlayerFlagBurst(player_t *player, boolean toss)
|
|
{
|
|
mobj_t *flag;
|
|
mobjtype_t type;
|
|
|
|
if (!(player->gotflag & (GF_REDFLAG|GF_BLUEFLAG)))
|
|
return;
|
|
|
|
if (player->gotflag & GF_REDFLAG)
|
|
type = MT_REDFLAG;
|
|
else
|
|
type = MT_BLUEFLAG;
|
|
|
|
flag = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, type);
|
|
|
|
if (player->mo->eflags & MFE_VERTICALFLIP)
|
|
{
|
|
flag->z += player->mo->height - flag->height;
|
|
flag->flags2 |= MF2_OBJECTFLIP;
|
|
}
|
|
|
|
if (toss)
|
|
P_InstaThrust(flag, player->mo->angle, FixedMul(6*FRACUNIT, player->mo->scale));
|
|
else
|
|
{
|
|
angle_t fa = P_RandomByte()*FINEANGLES/256;
|
|
flag->momx = FixedMul(FINECOSINE(fa), FixedMul(6*FRACUNIT, player->mo->scale));
|
|
if (!(twodlevel || (player->mo->flags2 & MF2_TWOD)))
|
|
flag->momy = FixedMul(FINESINE(fa), FixedMul(6*FRACUNIT, player->mo->scale));
|
|
}
|
|
|
|
flag->momz = FixedMul(8*FRACUNIT, player->mo->scale);
|
|
if (player->mo->eflags & MFE_VERTICALFLIP)
|
|
flag->momz = -flag->momz;
|
|
|
|
if (type == MT_REDFLAG)
|
|
flag->spawnpoint = rflagpoint;
|
|
else
|
|
flag->spawnpoint = bflagpoint;
|
|
|
|
flag->fuse = cv_flagtime.value * TICRATE;
|
|
P_SetTarget(&flag->target, player->mo);
|
|
|
|
// Flag text
|
|
{
|
|
char plname[MAXPLAYERNAME+4];
|
|
const char *flagtext;
|
|
char flagcolor;
|
|
|
|
snprintf(plname, sizeof(plname), "%s%s%s",
|
|
CTFTEAMCODE(player),
|
|
player_names[player - players],
|
|
CTFTEAMENDCODE(player));
|
|
|
|
if (type == MT_REDFLAG)
|
|
{
|
|
flagtext = M_GetText("Red flag");
|
|
flagcolor = '\x85';
|
|
}
|
|
else
|
|
{
|
|
flagtext = M_GetText("Blue flag");
|
|
flagcolor = '\x84';
|
|
}
|
|
|
|
if (toss)
|
|
CONS_Printf(M_GetText("%s tossed the %c%s%c.\n"), plname, flagcolor, flagtext, 0x80);
|
|
else
|
|
CONS_Printf(M_GetText("%s dropped the %c%s%c.\n"), plname, flagcolor, flagtext, 0x80);
|
|
}
|
|
|
|
player->gotflag = 0;
|
|
|
|
// Pointers set for displaying time value and for consistency restoration.
|
|
if (type == MT_REDFLAG)
|
|
redflag = flag;
|
|
else
|
|
blueflag = flag;
|
|
|
|
if (toss)
|
|
player->tossdelay = 2*TICRATE;
|
|
|
|
return;
|
|
}
|