// SONIC ROBO BLAST 2 //----------------------------------------------------------------------------- // Copyright (C) 1993-1996 by id Software, Inc. // Copyright (C) 1998-2000 by DooM Legacy Team. // Copyright (C) 1999-2020 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 "doomstat.h" #include "i_system.h" #include "am_map.h" #include "g_game.h" #include "m_random.h" #include "p_local.h" #include "p_mobj.h" #include "s_sound.h" #include "r_main.h" #include "st_stuff.h" #include "hu_stuff.h" #include "lua_hook.h" #include "m_cond.h" // unlockables, emblems, etc #include "p_setup.h" #include "m_cheat.h" // objectplace #include "m_misc.h" #include "v_video.h" // video flags for CEchos #include "f_finale.h" // SRB2kart #include "k_kart.h" #include "k_battle.h" #include "k_pwrlv.h" #include "k_grandprix.h" #include "k_boss.h" #include "p_spec.h" #include "k_objects.h" #include "acs/interface.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[g_localplayers[1]]) I_Tactile2(ConstantForce, &ConstantQuake); else if (splitscreen > 1 && FFInfo->player == &players[g_localplayers[2]]) I_Tactile3(ConstantForce, &ConstantQuake); else if (splitscreen > 2 && FFInfo->player == &players[g_localplayers[3]]) I_Tactile4(ConstantForce, &ConstantQuake); } void P_RampConstant(const BasicFF_t *FFInfo, INT32 Start, INT32 End) { JoyFF_t RampQuake; if (!FFInfo || !FFInfo->player) return; RampQuake.ForceX = FFInfo->ForceX; RampQuake.ForceY = FFInfo->ForceY; RampQuake.Duration = FFInfo->Duration; RampQuake.Gain = FFInfo->Gain; RampQuake.Magnitude = FFInfo->Magnitude; RampQuake.Start = Start; RampQuake.End = End; if (FFInfo->player == &players[consoleplayer]) I_Tactile(ConstantForce, &RampQuake); else if (splitscreen && FFInfo->player == &players[g_localplayers[1]]) I_Tactile2(ConstantForce, &RampQuake); else if (splitscreen > 1 && FFInfo->player == &players[g_localplayers[2]]) I_Tactile3(ConstantForce, &RampQuake); else if (splitscreen > 2 && FFInfo->player == &players[g_localplayers[3]]) I_Tactile4(ConstantForce, &RampQuake); } // // GET STUFF // // // P_CanPickupItem // // Returns true if the player is in a state where they can pick up items. // boolean P_CanPickupItem(player_t *player, UINT8 weapon) { if (player->exiting || mapreset || (player->pflags & PF_ELIMINATED)) return false; if ((gametyperules & GTR_BUMPERS) // No bumpers in Match && !weapon && player->bumper <= 0) return false; if (weapon) { // Item slot already taken up if (weapon == 2) { // Invulnerable if (player->flashing > 0 || player->spinouttimer > 0 || player->squishedtimer > 0 || player->invincibilitytimer > 0 || player->growshrinktimer > 0 || player->hyudorotimer > 0 || player->flametimer > 0) return false; // Already have fake if (player->roulettetype == 2 || player->eggmanexplode) return false; } else { // Item-specific timer going off if (player->stealingtimer || player->stolentimer || player->rocketsneakertimer || player->eggmanexplode || (player->growshrinktimer > 0) || player->flametimer) return false; // Item slot already taken up if (player->itemroulette || (weapon != 3 && player->itemamount) || (player->itemflags & IF_ITEMOUT)) return false; if (weapon == 3 && K_GetShieldFromPlayer(player) != KSHIELD_NONE) return false; // No stacking shields! } } return true; } // // P_DoNightsScore // // When you pick up some items in nights, it displays // a score sign, and awards you some drill time. // static 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 = &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 = 2*TICRATE; } } else // Individual link counts*/ { if (++player->linkcount > player->maxlink) player->maxlink = player->linkcount; player->linktimer = 2*TICRATE; } if (player->linkcount < 10) { /*if (player->bonustime) { P_AddPlayerScore(player, player->linkcount*20); P_SetMobjState(dummymo, dummymo->info->xdeathstate+player->linkcount-1); } else*/ { P_AddPlayerScore(player, player->linkcount*10); P_SetMobjState(dummymo, dummymo->info->spawnstate+player->linkcount-1); } } else { /*if (player->bonustime) { P_AddPlayerScore(player, 200); P_SetMobjState(dummymo, dummymo->info->xdeathstate+9); } else*/ { P_AddPlayerScore(player, 100); P_SetMobjState(dummymo, dummymo->info->spawnstate+9); } } // Hoops are the only things that should add to your drill meter //player->drillmeter += TICRATE; dummymo->momz = FRACUNIT; dummymo->fuse = 3*TICRATE; // What?! NO, don't use the camera! Scale up instead! //P_InstaThrust(dummymo, R_PointToAngle2(dummymo->x, dummymo->y, camera[0].x, camera[0].y), 3*FRACUNIT); dummymo->scalespeed = FRACUNIT/25; dummymo->destscale = 2*FRACUNIT; } /** 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; if (objectplacing) return; I_Assert(special != NULL); I_Assert(toucher != NULL); // Dead thing touching. // Can happen with a sliding player corpse. if (toucher->health <= 0) return; if (special->health <= 0) return; if (heightcheck) { fixed_t toucher_bottom = toucher->z; fixed_t special_bottom = special->z; if (toucher->flags & MF_PICKUPFROMBELOW) toucher_bottom -= toucher->height; if (special->flags & MF_PICKUPFROMBELOW) special_bottom -= special->height; if (toucher->momz < 0) { if (toucher_bottom + toucher->momz > special->z + special->height) return; } else if (toucher_bottom > special->z + special->height) return; if (toucher->momz > 0) { if (toucher->z + toucher->height + toucher->momz < special_bottom) return; } else if (toucher->z + toucher->height < special_bottom) return; } player = toucher->player; I_Assert(player != NULL); // Only players can touch stuff! if (player->spectator) return; // Ignore multihits in "ouchie" mode if (special->flags & (MF_ENEMY|MF_BOSS) && special->flags2 & MF2_FRET) return; if (LUA_HookTouchSpecial(special, toucher) || P_MobjWasRemoved(special)) return; if ((special->flags & (MF_ENEMY|MF_BOSS)) && !(special->flags & MF_MISSILE)) { //////////////////////////////////////////////////////// /////ENEMIES & BOSSES!!///////////////////////////////// //////////////////////////////////////////////////////// P_DamageMobj(toucher, special, special, 1, DMG_NORMAL); return; } else if (special->flags & MF_FIRE) { P_DamageMobj(toucher, special, special, 1, DMG_NORMAL); return; } else { // We now identify by object type, not sprite! Tails 04-11-2001 switch (special->type) { case MT_MEMENTOSTP: // Mementos teleport // Teleport player to the other teleporter (special->target). We'll assume there's always only ever 2. if (!special->target) return; // foolproof crash prevention check!!!!! P_SetOrigin(player->mo, special->target->x, special->target->y, special->target->z + (48<mo->angle = special->target->angle; P_SetObjectMomZ(player->mo, 12<mo, player->mo->angle, 20<itemamount && player->itemtype != special->threshold)) return; if ((gametyperules & GTR_BUMPERS) && player->bumper <= 0) return; player->itemtype = special->threshold; if ((UINT16)(player->itemamount) + special->movecount > 255) player->itemamount = 255; else player->itemamount += special->movecount; S_StartSound(special, special->info->deathsound); P_SetTarget(&special->tracer, toucher); special->flags2 |= MF2_NIGHTSPULL; special->destscale = mapobjectscale>>4; special->scalespeed <<= 1; special->flags &= ~MF_SPECIAL; return; case MT_RANDOMITEM: if (!itembreaker && !P_CanPickupItem(player, 1)) return; if ((gametyperules & GTR_BUMPERS) && player->bumper <= 0) { if (player->karmamode || player->karmadelay) return; player->karmamode = 1; } special->momx = special->momy = special->momz = 0; P_SetTarget(&special->target, toucher); P_KillMobj(special, toucher, toucher, DMG_NORMAL); break; case MT_ITEMCAPSULE: if ((gametyperules & GTR_BUMPERS) && player->bumper <= 0) return; if (special->scale < special->extravalue1) // don't break it while it's respawning return; switch (special->threshold) { case KITEM_SUPERRING: if (player->pflags & PF_RINGLOCK) // no cheaty rings return; break; default: if (!P_CanPickupItem(player, 1)) return; break; } S_StartSound(toucher, special->info->deathsound); P_KillMobj(special, toucher, toucher, DMG_NORMAL); return; case MT_KARMAHITBOX: if (!special->target->player) return; if (player == special->target->player) return; if (player->bumper <= 0) return; if (special->target->player->exiting || player->exiting) return; if (P_PlayerInPain(special->target->player)) return; if (special->target->player->karmadelay > 0) return; if (!special->target->player->karmamode) { mobj_t *boom; if (P_DamageMobj(toucher, special, special->target, 1, DMG_KARMA) == false) { return; } boom = P_SpawnMobj(special->target->x, special->target->y, special->target->z, MT_BOOMEXPLODE); boom->scale = special->target->scale; boom->destscale = special->target->scale; boom->momz = 5*FRACUNIT; if (special->target->color) boom->color = special->target->color; else boom->color = SKINCOLOR_KETCHUP; S_StartSound(boom, special->info->attacksound); special->target->player->karthud[khud_yougotem] = 2*TICRATE; special->target->player->karmadelay = comebacktime; } else if (special->target->player->karmamode == 1 && P_CanPickupItem(player, 1)) { mobj_t *poof = P_SpawnMobj(special->x, special->y, special->z, MT_EXPLODE); S_StartSound(poof, special->info->seesound); // Karma fireworks for (i = 0; i < 5; i++) { mobj_t *firework = P_SpawnMobj(special->x, special->y, special->z, MT_KARMAFIREWORK); firework->momx = (special->target->momx + toucher->momx) / 2; firework->momy = (special->target->momy + toucher->momy) / 2; firework->momz = (special->target->momz + toucher->momz) / 2; P_Thrust(firework, FixedAngle((72*i)<scale); P_SetObjectMomZ(firework, P_RandomRange(1,8)*special->scale, false); firework->color = special->target->color; } special->target->player->karmamode = 0; special->target->player->karmapoints++; if (special->target->player->karmapoints >= 2) K_TakeBumpersFromPlayer(special->target->player, player, 1); special->target->player->karmadelay = comebacktime; player->itemroulette = 1; player->roulettetype = 1; } else if (special->target->player->karmamode == 2 && P_CanPickupItem(player, 2)) { mobj_t *poof = P_SpawnMobj(special->x, special->y, special->z, MT_EXPLODE); UINT8 ptadd = 1; // No WANTED bonus for tricking S_StartSound(poof, special->info->seesound); if (player->bumper == 1) // If you have only one bumper left, and see if it's a 1v1 { INT32 numingame = 0; for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator || players[i].bumper <= 0) continue; numingame++; } if (numingame <= 2) // If so, then an extra karma point so they are 100% certain to switch places; it's annoying to end matches with a fake kill ptadd++; } special->target->player->karmamode = 0; special->target->player->karmapoints += ptadd; if (ptadd > 1) special->target->player->karthud[khud_yougotem] = 2*TICRATE; if (special->target->player->karmapoints >= 2) K_TakeBumpersFromPlayer(special->target->player, player, 1); special->target->player->karmadelay = comebacktime; K_DropItems(player); //K_StripItems(player); //K_StripOther(player); player->itemroulette = 1; player->roulettetype = 2; if (special->target->player->eggmanblame >= 0 && special->target->player->eggmanblame < MAXPLAYERS && playeringame[special->target->player->eggmanblame] && !players[special->target->player->eggmanblame].spectator) player->eggmanblame = special->target->player->eggmanblame; else player->eggmanblame = -1; special->target->player->eggmanblame = -1; } return; case MT_SPB: if ((special->target == toucher || special->target == toucher->target) && (special->threshold > 0)) return; if (special->health <= 0 || toucher->health <= 0) return; if (player->spectator) return; if (special->tracer && !P_MobjWasRemoved(special->tracer) && toucher == special->tracer) { mobj_t *spbexplode; if (player->bubbleblowup > 0) { K_DropHnextList(player, false); special->extravalue1 = 2; // WAIT... special->extravalue2 = 52; // Slightly over the respawn timer length return; } if (player->invincibilitytimer > 0 || player->growshrinktimer > 0 || player->hyudorotimer > 0) { //player->flashing = 0; K_DropHnextList(player, false); K_StripItems(player); } S_StopSound(special); // Don't continue playing the gurgle or the siren spbexplode = P_SpawnMobj(toucher->x, toucher->y, toucher->z, MT_SPBEXPLOSION); spbexplode->extravalue1 = 1; // Tell K_ExplodePlayer to use extra knockback if (special->target && !P_MobjWasRemoved(special->target)) P_SetTarget(&spbexplode->target, special->target); P_RemoveMobj(special); } else { P_DamageMobj(player->mo, special, special->target, 1, DMG_NORMAL); } return; /* case MT_EERIEFOG: special->frame &= ~FF_TRANS80; special->frame |= FF_TRANS90; return; */ case MT_SMK_MOLE: if (special->target && !P_MobjWasRemoved(special->target)) return; if (special->health <= 0 || toucher->health <= 0) return; if (!player->mo || player->spectator) return; // kill if (player->invincibilitytimer > 0 || player->growshrinktimer > 0 || player->flamestore > 0) { P_KillMobj(special, toucher, toucher, DMG_NORMAL); return; } // no interaction if (player->flashing > 0 || player->hyudorotimer > 0 || P_PlayerInPain(player)) return; // attach to player! P_SetTarget(&special->target, toucher); S_StartSound(special, sfx_s1a2); return; case MT_CDUFO: // SRB2kart if (special->fuse || !P_CanPickupItem(player, 1) || ((gametyperules & GTR_BUMPERS) && player->bumper <= 0)) return; player->itemroulette = 1; player->roulettetype = 1; // Karma fireworks for (i = 0; i < 5; i++) { mobj_t *firework = P_SpawnMobj(special->x, special->y, special->z, MT_KARMAFIREWORK); firework->momx = toucher->momx; firework->momy = toucher->momy; firework->momz = toucher->momz; P_Thrust(firework, FixedAngle((72*i)<scale); P_SetObjectMomZ(firework, P_RandomRange(1,8)*special->scale, false); firework->color = toucher->color; } S_StartSound(toucher, sfx_cdfm73); // they don't make this sound in the original game but it's nice to have a "reward" for good play //special->momx = special->momy = special->momz = 0; special->momz = -(3*special->scale)/2; //P_SetTarget(&special->target, toucher); special->fuse = 2*TICRATE; break; case MT_BALLOON: // SRB2kart P_SetObjectMomZ(toucher, 20<momx || toucher->momy) && (flip * special->momz <= 0)) { special->momx = toucher->momx; special->momy = toucher->momy; special->momz = flip * max(P_AproxDistance(toucher->momx, toucher->momy) / 4, FixedMul(special->info->speed, special->scale)); if (flip * toucher->momz > 0) special->momz += toucher->momz / 8; if ((statenum_t)(special->state-states) != special->info->seestate) P_SetMobjState(special, special->info->seestate); S_StartSound(special, special->info->activesound); } } return; case MT_BUBBLESHIELDTRAP: if ((special->target == toucher || special->target == toucher->target) && (special->threshold > 0)) return; if (special->tracer && !P_MobjWasRemoved(special->tracer)) return; if (special->health <= 0 || toucher->health <= 0) return; if (!player->mo || player->spectator) return; // attach to player! P_SetTarget(&special->tracer, toucher); toucher->flags |= MF_NOGRAVITY; toucher->momz = (8*toucher->scale) * P_MobjFlip(toucher); // Snap to the unfortunate player and quit moving laterally, or we can end up quite far away special->momx = 0; special->momy = 0; special->x = toucher->x; special->y = toucher->y; special->z = toucher->z; S_StartSound(toucher, sfx_s1b2); return; case MT_RING: case MT_FLINGRING: if (special->extravalue1) return; // No picking up rings while SPB is targetting you if (player->pflags & PF_RINGLOCK) return; // Don't immediately pick up spilled rings if (special->threshold > 0 || P_PlayerInPain(player)) return; if (!(P_CanPickupItem(player, 0))) return; // Reached the cap, don't waste 'em! if (RINGTOTAL(player) >= 20) return; special->momx = special->momy = special->momz = 0; special->extravalue1 = 1; // Ring collect animation timer special->angle = R_PointToAngle2(toucher->x, toucher->y, special->x, special->y); // animation angle P_SetTarget(&special->target, toucher); // toucher for thinker player->pickuprings++; return; // Secret emblem thingy case MT_EMBLEM: { if (demo.playback || special->health > MAXEMBLEMS) return; emblemlocations[special->health-1].collected = true; M_UpdateUnlockablesAndExtraEmblems(); G_SaveGameData(); break; } // CTF Flags case MT_REDFLAG: case MT_BLUEFLAG: return; case MT_STARPOST: P_TouchStarPost(special, player, special->args[1]); return; 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) { if (!P_MobjWasRemoved(special->target)) { 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; case MT_BIGTUMBLEWEED: case MT_LITTLETUMBLEWEED: if (toucher->momx || toucher->momy) { special->momx = toucher->momx; special->momy = toucher->momy; special->momz = P_AproxDistance(toucher->momx, toucher->momy)/4; if (toucher->momz > 0) special->momz += toucher->momz/8; P_SetMobjState(special, special->info->seestate); } return; case MT_WATERDROP: if (special->state == &states[special->info->spawnstate]) { special->z = toucher->z+toucher->height-FixedMul(8*FRACUNIT, special->scale); special->momz = 0; special->flags |= MF_NOGRAVITY; P_SetMobjState (special, special->info->deathstate); S_StartSound (special, special->info->deathsound+(P_RandomKey(special->info->mass))); } return; case MT_LOOPENDPOINT: Obj_LoopEndpointCollide(special, toucher); 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); 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_KART_STILL); } return; default: // SOC or script pickup P_SetTarget(&special->target, toucher); break; } } S_StartSound(toucher, special->info->deathsound); // was NULL, but changed to player so you could hear others pick up rings P_KillMobj(special, NULL, toucher, DMG_NORMAL); special->shadowscale = 0; } /** Saves a player's level progress at a 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) { mobj_t *toucher = player->mo; (void)snaptopost; // Player must have touched all previous starposts if ((post->health - player->starpostnum > 1) && (!K_UsingLegacyCheckpoints())) { if (!player->checkskip) { S_StartSound(toucher, sfx_s26d); if (netgame && cv_antigrief.value) { player->grieftime += TICRATE; } } player->checkskip = 3; return; } // Going backwards triggers sound if ((post->health >= ((numstarposts/2) + player->starpostnum)) && (K_UsingLegacyCheckpoints())) { if (!player->checkskip) S_StartSound(toucher, sfx_s26d); player->checkskip = 3; return; } // With the parameter + angle setup, we can go up to 1365 star posts. Who needs that many? if (post->health > 1365) { CONS_Debug(DBG_GAMELOGIC, "Bad Starpost Number!\n"); return; } if (player->starpostnum >= post->health) return; // Already hit this post // Handles Respawning related things on maps wtih starposts { // Save the player's time and position. player->starposttime = player->realtime; //this makes race mode's timers work correctly whilst not affecting sp -x //player->starposttime = leveltime; player->starpostx = toucher->x>>FRACBITS; player->starposty = toucher->y>>FRACBITS; player->starpostz = post->z>>FRACBITS; player->starpostangle = post->angle; player->starpostflip = post->spawnpoint->options & MTF_OBJECTFLIP; // store flipping } player->grieftime = 0; player->starpostnum = post->health; } // Easily make it so that overtime works offline #define TESTOVERTIMEINFREEPLAY /** 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 (itembreaker) return; if (leveltime < (timelimitintics + starttime)) return; if (gameaction == ga_completed) return; if (cv_overtime.value) { 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 (players[i].exiting) return; 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 (!(gametyperules & GTR_TEAMS)) { //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++; } } if (playercount > MAXPLAYERS) playercount = MAXPLAYERS; //Sort 'em. for (i = 1; i < playercount; i++) { for (k = i; k < playercount; k++) { if (players[playerarray[i-1]].roundscore < players[playerarray[k]].roundscore) { 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]].roundscore == players[playerarray[1]].roundscore) return; } else { //In team match and CTF, determining a tie is much simpler. =P if (redscore == bluescore) return; } } } for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator) continue; if (players[i].exiting) return; P_DoPlayerExit(&players[i]); } } /** 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 (exitcountdown) return; if (!K_CanChangeRules()) return; if (!cv_pointlimit.value) return; if (!(gametyperules & GTR_POINTLIMIT)) return; if (itembreaker) 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) SendNetXCmd(XD_EXITLEVEL, NULL, 0); } } else { for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator) continue; if ((UINT32)cv_pointlimit.value <= players[i].roundscore) { for (i = 0; i < MAXPLAYERS; i++) // AAAAA nested loop using the same iteration variable ;; { if (!playeringame[i] || players[i].spectator) continue; if (players[i].exiting) return; P_DoPlayerExit(&players[i]); } /*if (server) SendNetXCmd(XD_EXITLEVEL, NULL, 0);*/ return; // good thing we're leaving the function immediately instead of letting the loop get mangled! } } } } // Checks whether or not to end a race netgame. boolean P_CheckRacers(void) { UINT8 i; UINT8 numplayersingame = 0; UINT8 numexiting = 0; boolean eliminatelast = cv_karteliminatelast.value; boolean everyonedone = true; boolean eliminatebots = false; const boolean griefed = (spectateGriefed > 0); // Check if all the players in the race have finished. If so, end the level. for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator || players[i].lives <= 0) // Not playing { // Y'all aren't even playing continue; } numplayersingame++; if (players[i].exiting || (players[i].pflags & PF_NOCONTEST)) { numexiting++; } else { if (players[i].bot) { // Isn't a human, thus doesn't matter. (Sorry, robots.) // Set this so that we can check for bots that need to get eliminated, though! eliminatebots = true; continue; } everyonedone = false; } } // If we returned here with bots left, then the last place bot may have a chance to finish the map and NOT get time over. // Not that it affects anything, they didn't make the map take longer or even get any points from it. But... it's a bit inconsistent! // So if there's any bots, we'll let the game skip this, continue onto calculating eliminatelast, THEN we return true anyway. if (everyonedone && !eliminatebots) { // Everyone's finished, we're done here! racecountdown = exitcountdown = 0; return true; } if (numplayersingame <= 1) { // Never do this without enough players. eliminatelast = false; } else { if (grandprixinfo.gp == true) { // Always do this in GP eliminatelast = true; } else if (griefed) { // Don't do this if someone spectated eliminatelast = false; } } if (eliminatelast == true && (numexiting >= numplayersingame-1)) { // Everyone's done playing but one guy apparently. // Just kill everyone who is still playing. for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator || players[i].lives <= 0) { // Y'all aren't even playing continue; } if (players[i].exiting || (players[i].pflags & PF_NOCONTEST)) { // You're done, you're free to go. continue; } P_DoTimeOver(&players[i]); } // Everyone should be done playing at this point now. racecountdown = exitcountdown = 0; return true; } if (everyonedone) { // See above: there might be bots that are still going, but all players are done, so we can exit now. racecountdown = exitcountdown = 0; return true; } // SO, we're not done playing. // Let's see if it's time to start the death counter! if (!racecountdown) { // If the winners are all done, then start the death timer. UINT8 winningpos = 1; winningpos = max(1, numplayersingame/2); if (numplayersingame % 2) // any remainder? { winningpos++; } if (numexiting >= winningpos) { tic_t countdown = 30*TICRATE; // 30 seconds left to finish, get going! if (netgame) { // Custom timer countdown = cv_countdowntime.value * TICRATE; } racecountdown = countdown + 1; } } // We're still playing, but no one else is, so we need to reset spectator griefing. if (numplayersingame <= 1) { spectateGriefed = 0; } // Turns out we're still having a good time & playing the game, we didn't have to do anything :) 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 (target->flags & (MF_ENEMY|MF_BOSS)) target->momx = target->momy = target->momz = 0; // SRB2kart if (target->type != MT_PLAYER && !(target->flags & MF_MONITOR) && !(target->type == MT_ORBINAUT || target->type == MT_ORBINAUT_SHIELD || target->type == MT_JAWZ || target->type == MT_JAWZ_DUD || target->type == MT_JAWZ_SHIELD || target->type == MT_BANANA || target->type == MT_BANANA_SHIELD || target->type == MT_DROPTARGET || target->type == MT_DROPTARGET_SHIELD || target->type == MT_EGGMANITEM || target->type == MT_EGGMANITEM_SHIELD || target->type == MT_BALLHOG || target->type == MT_SPB)) // kart dead items target->flags |= MF_NOGRAVITY; // Don't drop Tails 03-08-2000 else target->flags &= ~MF_NOGRAVITY; // lose it if you for whatever reason have it, I'm looking at you shields // if (target->flags2 & MF2_NIGHTSPULL) { P_SetTarget(&target->tracer, NULL); target->movefactor = 0; // reset NightsItemChase timer } // dead target is no more shootable target->flags &= ~(MF_SHOOTABLE|MF_FLOAT|MF_SPECIAL); target->flags2 &= ~(MF2_SKULLFLY|MF2_NIGHTSPULL); target->health = 0; // This makes it easy to check if something's dead elsewhere. if (target->type != MT_BATTLEBUMPER) { target->shadowscale = 0; } if (LUA_HookMobjDeath(target, inflictor, source, damagetype) || P_MobjWasRemoved(target)) return; P_ActivateThingSpecial(target, source); //K_SetHitLagForObjects(target, inflictor, MAXHITLAGTICS, true); // SRB2kart // I wish I knew a better way to do this if (!P_MobjWasRemoved(target->target) && target->target->player && !P_MobjWasRemoved(target->target->player->mo)) { if ((target->target->player->itemflags & IF_EGGMANOUT) && target->type == MT_EGGMANITEM_SHIELD) target->target->player->itemflags &= ~IF_EGGMANOUT; if (target->target->player->itemflags & IF_ITEMOUT) { if ((target->type == MT_BANANA_SHIELD && target->target->player->itemtype == KITEM_BANANA) // trail items || (target->type == MT_SSMINE_SHIELD && target->target->player->itemtype == KITEM_MINE) || (target->type == MT_DROPTARGET_SHIELD && target->target->player->itemtype == KITEM_DROPTARGET) || (target->type == MT_SINK_SHIELD && target->target->player->itemtype == KITEM_KITCHENSINK)) { if (target->movedir != 0 && target->movedir < (UINT16)target->target->player->itemamount) { if (target->target->hnext) K_KillBananaChain(target->target->hnext, inflictor, source); target->target->player->itemamount = 0; } else if (target->target->player->itemamount) target->target->player->itemamount--; } else if ((target->type == MT_ORBINAUT_SHIELD && target->target->player->itemtype == KITEM_ORBINAUT) // orbit items || (target->type == MT_JAWZ_SHIELD && target->target->player->itemtype == KITEM_JAWZ)) { if (target->target->player->itemamount) target->target->player->itemamount--; if (target->lastlook != 0) { K_RepairOrbitChain(target); } } if (!target->target->player->itemamount) target->target->player->itemflags &= ~IF_ITEMOUT; if (target->target->hnext == target) P_SetTarget(&target->target->hnext, NULL); } } // Above block does not clean up rocket sneakers when a player dies, so we need to do it here target->target is null when using rocket sneakers if (target->player) K_DropRocketSneaker(target->player); // Let EVERYONE know what happened to a player! 01-29-2002 Tails if (target->player && !target->player->spectator) { if (metalrecording) // Ack! Metal Sonic shouldn't die! Cut the tape, end recording! G_StopMetalRecording(true); target->renderflags &= ~RF_DONTDRAW; } // if killed by a player if (source && source->player) { if (target->flags & MF_MONITOR || target->type == MT_RANDOMITEM) { P_SetTarget(&target->target, source); //source->player->numboxes++; if (itembreaker) { target->flags2 |= MF2_BOSSFLEE; target->flags2 |= MF2_DONTRESPAWN; K_SpawnBattlePoints(source->player, NULL, 1); // All targets busted! if (++numtargets >= nummapboxes) { boolean givelife = false; for (int i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator) continue; P_DoPlayerExit(&players[i]); if (!grandprixinfo.gp) continue; P_GivePlayerLives(&players[i], 1); givelife = true; } if (givelife) S_StartSound(NULL, sfx_cdfm73); } } if (cv_itemrespawn.value && modeattacking == ATTACKING_NONE && !itembreaker) { target->fuse = cv_itemrespawntime.value*TICRATE + 2; // Random box generation } } } // if a player avatar dies... if (target->player) { UINT8 i; target->flags &= ~(MF_SOLID|MF_SHOOTABLE); // does not block P_UnsetThingPosition(target); target->flags |= MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; P_SetThingPosition(target); target->standingslope = NULL; target->terrain = NULL; target->pmomz = 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... for (i = 0; i <= r_splitscreen; i++) { if (target->player == &players[displayplayers[i]]) { localaiming[i] = 0; break; } } if (gametyperules & GTR_BUMPERS) K_CheckBumpers(); target->player->pogospring = 0; ACS_RunPlayerDeathScript(target->player); } 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, DMG_NORMAL); 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)<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->angle, (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, DMG_NORMAL); break; case MT_PLAYER: if (damagetype != DMG_SPECTATOR) { target->fuse = 2*TICRATE; // timer before mobj disappears from view (even if not an actual player) target->momx = target->momy = target->momz = 0; if (target->player && !(skins[target->player->skin].flags & SF_NOGIBS)) { angle_t playerFlingAngle; angle_t kartFlingAngle; if (source && !P_MobjWasRemoved(source)) { playerFlingAngle = kartFlingAngle = R_PointToAngle2( source->x - source->momx, source->y - source->momy, target->x, target->y ); } else { kartFlingAngle = target->angle; if (P_RandomByte() & 1) { kartFlingAngle -= ANGLE_45; } else { kartFlingAngle += ANGLE_45; } playerFlingAngle = kartFlingAngle + ANGLE_180; } // Spawn kart frame mobj_t *kart = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_KART_LEFTOVER); if (kart && !P_MobjWasRemoved(kart)) { kart->angle = target->angle; kart->color = target->color; kart->extravalue1 = target->player->kartweight; kart->fuse = 2*TICRATE; // Copy interp data kart->old_angle = target->old_angle; kart->old_x = target->old_x; kart->old_y = target->old_y; kart->old_z = target->old_z; P_InstaThrust(kart, kartFlingAngle, 1 * kart->scale); P_SetObjectMomZ(kart, 10*FRACUNIT, false); const angle_t aOffset = ANGLE_22h; UINT8 i; angle_t tireAngle; mobj_t *tire; // Spawn tires tireAngle = kartFlingAngle - ANGLE_90 - ANGLE_22h; for (i = 0; i < 4; i++) { if (i == 2) tireAngle += ANGLE_90; tire = P_SpawnMobjFromMobj(kart, 0, 0, 0, MT_KART_TIRE); tire->fuse = 2*TICRATE; tire->angle = tireAngle; P_InstaThrust(tire, tireAngle, 3 * tire->scale); P_SetObjectMomZ(tire, 10*FRACUNIT, false); tireAngle += (aOffset * 2); } } P_InstaThrust(target, playerFlingAngle, 4 * target->scale); P_SetObjectMomZ(target, 14*FRACUNIT, false); } if (target->player && (skins[target->player->skin].flags & SF_OLDDEATH)) { P_SetObjectMomZ(target, 14*FRACUNIT, false); } 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; // SRB2Kart: case MT_SMK_ICEBLOCK: { mobj_t *cur = target->hnext; while (cur && !P_MobjWasRemoved(cur)) { P_SetMobjState(cur, S_SMK_ICEBLOCK2); cur = cur->hnext; } target->fuse = 10; S_StartSound(target, sfx_s3k80); } break; case MT_ITEMCAPSULE: { // set respawn fuse if (modeattacking) // no respawns ; else if (target->threshold == KITEM_SUPERRING) target->fuse = 20*TICRATE; else target->fuse = 40*TICRATE; // remove inside item if (target->tracer && !P_MobjWasRemoved(target->tracer)) { P_RemoveMobj(target->tracer); } // give the player an item! if (source && source->player) { player_t *player = source->player; // special behavior for ring capsules if (target->threshold == KITEM_SUPERRING) { K_AwardPlayerRings(player, 5 * target->movecount, true); break; } if (target->threshold < 1 || target->threshold >= NUMKARTITEMS) // bruh moment prevention { player->itemtype = KITEM_SAD; player->itemamount = 1; } else { player->itemtype = target->threshold; if (K_GetShieldFromPlayer(player) != KSHIELD_NONE) // never give more than 1 shield player->itemamount = 1; else player->itemamount = max(1, target->movecount); } player->karthud[khud_itemblink] = TICRATE; player->karthud[khud_itemblinkmode] = 0; player->itemroulette = 0; player->roulettetype = 0; if (P_IsDisplayPlayer(player)) S_StartSound(NULL, sfx_itrolf); } break; } case MT_BATTLEBUMPER: { mobj_t *owner = target->target; mobj_t *overlay; S_StartSound(target, sfx_kc52); target->flags &= ~MF_NOGRAVITY; target->destscale = (3 * target->destscale) / 2; target->scalespeed = FRACUNIT/100; if (owner && !P_MobjWasRemoved(owner)) { P_Thrust(target, R_PointToAngle2(owner->x, owner->y, target->x, target->y), 4 * target->scale); } target->momz += (24 * target->scale) * P_MobjFlip(target); target->fuse = 8; overlay = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_OVERLAY); P_SetTarget(&target->tracer, overlay); P_SetTarget(&overlay->target, target); overlay->color = target->color; P_SetMobjState(overlay, S_INVISIBLE); } break; case MT_DROPTARGET: case MT_DROPTARGET_SHIELD: target->fuse = 1; break; default: break; } if ((target->type == MT_JAWZ || target->type == MT_JAWZ_DUD || target->type == MT_JAWZ_SHIELD) && !(target->flags2 & MF2_AMBUSH)) { target->z += P_MobjFlip(target)*20*target->scale; } // kill tracer if (target->type == MT_FROGGER) { if (target->tracer && !P_MobjWasRemoved(target->tracer)) P_KillMobj(target->tracer, inflictor, source, DMG_NORMAL); } if (target->type == MT_FROGGER || target->type == MT_ROBRA_HEAD || target->type == MT_BLUEROBRA_HEAD) // clean hnext list { mobj_t *cur = target->hnext; while (cur && !P_MobjWasRemoved(cur)) { P_KillMobj(cur, inflictor, source, DMG_NORMAL); cur = cur->hnext; } } // Bounce up on death if (target->type == MT_SMK_PIPE || target->type == MT_SMK_MOLE || target->type == MT_SMK_THWOMP) { target->flags &= (~MF_NOGRAVITY); if (target->eflags & MFE_VERTICALFLIP) target->z -= target->height; else target->z += target->height; S_StartSound(target, target->info->deathsound); P_SetObjectMomZ(target, 8<x, inflictor->y, target->x, target->y)+ANGLE_90, 16<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) { P_SetPlayerMobjState(target, target->info->deathstate); } else #ifdef DEBUG_NULL_DEATHSTATE P_SetMobjState(target, S_NULL); #else P_SetMobjState(target, target->info->deathstate); #endif /** \note For player, the above is redundant because of P_SetMobjState (target, S_PLAY_DIE1) in P_DamageMobj() Graue 12-22-2003 */ } static boolean P_PlayerHitsPlayer(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype) { (void)inflictor; (void)damage; // SRB2Kart: We want to hurt ourselves, so it's now DMG_CANTHURTSELF if (damagetype & DMG_CANTHURTSELF) { // You can't kill yourself, idiot... if (source == target) return false; if (G_GametypeHasTeams()) { // Don't hurt your team, either! if (source->player->ctfteam == target->player->ctfteam) return false; } } return true; } static boolean P_KillPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 type) { (void)source; (void)inflictor; if (player->exiting) { player->mo->destscale = 1; player->mo->flags |= MF_NOCLIPTHING; return false; } K_DestroyBumpers(player, 1); switch (type) { case DMG_DEATHPIT: // Respawn kill types //K_DoIngameRespawn(player); //return false; case DMG_SPECTATOR: // disappearifies, but still gotta put items back in play break; default: // Everything else REALLY kills break; } player->carry = CR_NONE; K_KartResetPlayerColor(player); P_ResetPlayer(player); if (player->spectator == false) { player->mo->renderflags &= ~RF_DONTDRAW; } P_SetPlayerMobjState(player->mo, player->mo->info->deathstate); if (type == DMG_TIMEOVER) { if (gametyperules & GTR_CIRCUIT) { mobj_t *boom; player->mo->flags |= (MF_NOGRAVITY|MF_NOCLIP); player->mo->renderflags |= RF_DONTDRAW; boom = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_FZEROBOOM); boom->scale = player->mo->scale; boom->angle = player->mo->angle; P_SetTarget(&boom->target, player->mo); } K_DestroyBumpers(player, player->bumper); player->pflags |= PF_ELIMINATED; } return true; } // Determines what ShouldX Hook's status should be used. static UINT8 P_ShouldHookDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype) { UINT8 shouldDamage = LUA_HookShouldDamage(target, inflictor, source, damage, damagetype); UINT8 shouldSpin = LUA_HookShouldSpin(target, inflictor, source, damage, damagetype); UINT8 shouldExplode = LUA_HookShouldExplode(target, inflictor, source, damage, damagetype); UINT8 shouldSquish = LUA_HookShouldSquish(target, inflictor, source, damage, damagetype); UINT8 status; const UINT8 type = (damagetype & DMG_TYPEMASK); status = shouldDamage; if (shouldSpin > 0 && ((type == DMG_NORMAL) || (type == DMG_WIPEOUT))) status = shouldSpin; else if (shouldExplode > 0 && ((type == DMG_EXPLODE) || (type == DMG_KARMA))) status = shouldExplode; else if (shouldSquish > 0 && (type == DMG_SQUISH)) status = shouldSquish; return status; } /** 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 (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 = P_ShouldHookDamage(target, inflictor, source, damage, damagetype); if (P_MobjWasRemoved(target)) return (shouldForce == 1); // mobj was removed if (shouldForce == 1) force = true; else if (shouldForce == 2) return false; } if (!force) { if (!(target->flags & MF_SHOOTABLE)) return false; // shouldn't happen... } if (target->flags2 & MF2_SKULLFLY) target->momx = target->momy = target->momz = 0; if (target->flags & (MF_ENEMY|MF_BOSS)) { if (!force && target->flags2 & MF2_FRET) // Currently flashing from being hit return false; if (LUA_HookMobjDamage(target, inflictor, source, damage, damagetype) || P_MobjWasRemoved(target)) return true; if (target->health > 1) target->flags2 |= MF2_FRET; } player = target->player; if (player) // Player is the target { if (player->pflags & PF_GODMODE) return false; if (!force) { // Player hits another player if (source && source->player) { if (!P_PlayerHitsPlayer(target, inflictor, source, damage, damagetype)) return false; } } // Instant-Death if ((damagetype & DMG_DEATHMASK)) { if (!P_KillPlayer(player, inflictor, source, damagetype)) return false; } else if (LUA_HookMobjDamage(target, inflictor, source, damage, damagetype)) { return true; } else { const UINT8 type = (damagetype & DMG_TYPEMASK); const boolean explosioncombo = (type == DMG_EXPLODE); // This damage type can do evil stuff like ALWAYS combo INT16 ringburst = 5; // Check if the player is allowed to be damaged! // If not, then spawn the instashield effect instead. if (!force) { if (gametyperules & GTR_BUMPERS) { if ((player->bumper <= 0 && player->karmadelay) || (player->karmamode == 1)) { // No bumpers & in WAIT, can't be hurt K_DoInstashield(player); return false; } } else { if (damagetype & DMG_STEAL) { // Gametype does not have bumpers, steal damage is intended to not do anything // (No instashield is intentional) return false; } } if (player->invincibilitytimer > 0 || player->growshrinktimer > 0 || player->hyudorotimer > 0) { // Full invulnerability K_DoInstashield(player); return false; } { // Check if we should allow explosion combos. if ((explosioncombo == false) && (player->flashing > 0 || player->squishedtimer > 0)) { // Post-hit invincibility K_DoInstashield(player); return false; } } } // We successfully damaged them! Give 'em some bumpers! if (source && source != player->mo && source->player) { if (gametyperules & GTR_BUMPERS) { K_BattleAwardHit(source->player, player, inflictor, 1); if (damagetype & DMG_STEAL) { K_TakeBumpersFromPlayer(source->player, player, 1); } else if (damage & DMG_KARMA) { source->player->karmapoints++; if (source->player->karmapoints >= 2) { K_TakeBumpersFromPlayer(source->player, player, 1); } else { K_DestroyBumpers(player, 1); } } else { K_DestroyBumpers(player, 1); } } K_TryHurtSoundExchange(target, source); } if (!((damagetype & DMG_NORMAL) || (damagetype & DMG_WIPEOUT))) player->sneakertimer = 0; player->driftboost = 0; player->bubblepop = 0; player->ringboost = 0; player->glanceDir = 0; player->pflags &= ~PF_GAINAX; switch (type) { case DMG_EXPLODE: case DMG_KARMA: ringburst = K_ExplodePlayer(player, inflictor, source); LUA_HookPlayerExplode(target, inflictor, source, damage, damagetype); break; case DMG_SQUISH: K_SquishPlayer(player, inflictor, source); LUA_HookPlayerSquish(target, inflictor, source, damage, damagetype); ringburst = 5; break; case DMG_WIPEOUT: if (P_IsDisplayPlayer(player)) P_StartQuake(32<flashing = K_GetKartFlashing(player); P_PlayRinglossSound(target); if (ringburst > 0) { P_PlayerRingBurst(player, ringburst); } K_PlayPainSound(target, source); if ((explosioncombo == true) || (cv_kartdebughuddrop.value && !modeattacking)) { K_DropItems(player); } else { K_DropHnextList(player, false); } player->instashield = 15; return true; } } else { if (damagetype & DMG_STEAL) { // Not a player, steal damage is intended to not do anything return false; } } // do the damage if (damagetype & DMG_DEATHMASK) target->health = 0; else target->health -= damage; if (source && source->player && target) G_GhostAddHit((INT32) (source->player - players), target); if (target->health <= 0) { P_KillMobj(target, inflictor, source, damagetype); return true; } if (player) P_ResetPlayer(target->player); else P_SetMobjState(target, target->info->painstate); if (!P_MobjWasRemoved(target)) { // if not intent on another player, // chase after this one P_SetTarget(&target->target, source); } return true; } static void P_FlingBurst ( player_t *player, angle_t fa, mobjtype_t objType, tic_t objFuse, fixed_t objScale, INT32 i) { mobj_t *mo; fixed_t ns; fixed_t momxy = 5<> 1; mo = P_SpawnMobjFromMobj(player->mo, 0, 0, 0, objType); mo->threshold = 10; // not useful for spikes mo->fuse = objFuse; P_SetTarget(&mo->target, player->mo); if (objScale != FRACUNIT) { P_SetScale(mo, FixedMul(objScale, mo->scale)); mo->destscale = mo->scale; } /* 0: 0 1: 1 = (1+1)/2 = 1 2: 1 = (2+1)/2 = 1 3: 2 = (3+1)/2 = 2 4: 2 = (4+1)/2 = 2 5: 3 = (4+1)/2 = 2 */ // Angle / height offset changes every other ring momxy -= mx * FRACUNIT; momz += mx * (2<mo->scale); mo->momx = (mo->target->momx/2) + FixedMul(FINECOSINE(fa>>ANGLETOFINESHIFT), ns); mo->momy = (mo->target->momy/2) + FixedMul(FINESINE(fa>>ANGLETOFINESHIFT), ns); ns = FixedMul(momz, player->mo->scale); mo->momz = (mo->target->momz/2) + ((ns) * P_MobjFlip(mo)); } /** Spills an injured player's rings. * * \param player The player who is losing rings. * \param num_rings Number of rings lost. A maximum of 20 rings will be * spawned. * \sa P_PlayerFlagBurst */ void P_PlayerRingBurst(player_t *player, INT32 num_rings) { INT32 num_fling_rings; INT32 i; angle_t fa; // Rings need to be enabled! if ((K_RingsActive() == false) ) return; // Better safe than sorry. if (!player) return; // Have a shield? You get hit, but don't lose your rings! if (K_GetShieldFromPlayer(player) != KSHIELD_NONE) return; // 20 is the maximum number of rings that can be taken from you at once - half the span of your counter if (num_rings > 20) num_rings = 20; else if (num_rings <= 0) return; num_rings = -P_GivePlayerRings(player, -num_rings); num_fling_rings = num_rings+min(0, player->rings); // determine first angle fa = player->mo->angle + ((P_RandomByte() & 1) ? -ANGLE_90 : ANGLE_90); for (i = 0; i < num_fling_rings; i++) { P_FlingBurst(player, fa, MT_FLINGRING, 60*TICRATE, FRACUNIT, i); } }