/* server/player/last_stand.qc Logic for Last Stand and Player Reviving. Copyright (C) 2021-2024 NZ:P Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to: Free Software Foundation, Inc. 59 Temple Place - Suite 330 Boston, MA 02111-1307, USA */ #define LASTSTAND_DEATH_TIMER 30 void() W_TakeOut; // // LastStand_SoloRevive() // Gets the client out of last stand after solo revive // timer has completed. // void() LastStand_SoloRevive = { entity old_self = self; self = self.owner; GetUp(); self = old_self; // Give the player their score back. Player_AddScore(self.owner, self.owner.requirespower, false); // No need for us to exist anymore, goodbye! remove(self); }; // // LastStand_InitiateSoloRevive(client) // Kicks off the timer set when a player enters Last // Stand with Quick Revive in solo. // void(entity client) LastStand_InitiateSoloRevive = { #ifdef FTE // FTE-Specific: Broadcast to the player they're reviving themselves. FTE_BroadcastMessage(client, CSQC_BROADCAST_REVIVINGPLAYER, 3, client.playernum); #endif // FTE // Create a new entity to "watch" and be the revivee. entity watcher = spawn(); watcher.owner = client; watcher.think = LastStand_SoloRevive; watcher.nextthink = time + 10; }; // // LastStand_Penalize(client) // Client entered Last Stand, remove their perks and // other minor things. // void(entity client) LastStand_Penalize = { // Reset bought Perk-A-Colas. client.perks = 0; // Set Max Health to that of without Jugger-Nog. client.max_health = client.health = PLAYER_START_HEALTH; // Get rid of Mule Kick Weapon for(float i = 0; i < MAX_PLAYER_WEAPONS; i++) { if (client.weapons[i].is_mulekick_weapon == true) { entity old_self = self; self = client; Weapon_RemoveWeapon(i); Weapon_SetActiveInSlot(0, false); self = old_self; } } }; // // LastStand_AssignWeapon(client) // Chooses what weapon to hand off to the player // based on their loadout upon entering Last // Stand. // void(entity client) LastStand_AssignWeapon = { entity old_self = self; self = client; client.weaponbk = self.weapon; client.currentammobk = self.weapons[0].weapon_reserve; client.currentmagbk = self.weapons[0].weapon_magazine; client.currentmagbk2 = self.weapons[0].weapon_magazine_left; if(Weapon_PlayerHasWeapon(self, W_BIATCH, false) || Weapon_PlayerHasWeapon(self, W_RAY, true) || Weapon_PlayerHasWeapon(self, W_357, true)) { float weapon_slot; float total_ammo; total_ammo = 0; weapon_slot = Weapon_PlayerHasWeapon(self, W_RAY, true); if (weapon_slot == 0) weapon_slot = Weapon_PlayerHasWeapon(self, W_BIATCH, false); if (weapon_slot == 0) weapon_slot = Weapon_PlayerHasWeapon(self, W_357, true); switch(weapon_slot) { case 1: total_ammo = self.weapons[0].weapon_magazine + self.weapons[0].weapon_magazine_left + self.weapons[0].weapon_reserve; break; case 2: total_ammo = self.weapons[1].weapon_magazine + self.weapons[1].weapon_magazine_left + self.weapons[1].weapon_reserve; Weapon_SwapWeapons(true); break; } self.weaponbk = self.weapon; self.currentammobk = self.weapons[0].weapon_reserve; self.currentmagbk = self.weapons[0].weapon_magazine; self.currentmagbk2 = self.weapons[0].weapon_magazine_left; // If it's greater than the mag size, we can fill the magazine. if (total_ammo > getWeaponMag(self.weapon) || total_ammo == 0) { self.weapons[0].weapon_magazine = getWeaponMag(self.weapon); // subtract it from the total ammo if (total_ammo != 0) total_ammo -= self.weapons[0].weapon_magazine; } else { self.weapons[0].weapon_magazine = total_ammo; total_ammo = 0; } // Check for dual wield mag too if (IsDualWeapon(self.weapon)) { if (total_ammo > getWeaponMag(self.weapon) || total_ammo == 0) { self.weapons[0].weapon_magazine_left = getWeaponMag(self.weapon); // subtract it from the total ammo if (total_ammo != 0) total_ammo -= self.weapons[0].weapon_magazine_left; } else { self.weapons[0].weapon_magazine_left = total_ammo; total_ammo = 0; } } // Ray Gun has a special case where we DON'T fill its reserve if (self.weapon != W_RAY && self.weapon != W_PORTER) { // Now see if the reserve ammo is more than max downed capacity if (total_ammo > getWeaponMag(self.weapon)*2) { self.weapons[0].weapon_reserve = getWeaponMag(self.weapon)*2; } else { // It's not so just fill it self.weapons[0].weapon_reserve = total_ammo; } } else { self.weapons[0].weapon_reserve = 0; } } else { if (!player_count) { Weapon_AssignWeapon(0, W_BIATCH, 6, 12); } else { Weapon_AssignWeapon(0, W_COLT, 8, 16); } } // Play Switch Animation self.weaponmodel = GetWeaponModel(self.weapon, 0); float startframe = GetFrame(self.weapon,TAKE_OUT_START); float endframe = GetFrame(self.weapon,TAKE_OUT_END); Set_W_Frame (startframe, endframe, 0, 0, 0, SUB_Null, self.weaponmodel, false, S_BOTH); self = old_self; }; // // LastStand_KillPlayer(client) // Player has bled out, force them into // a spectator position. // void(entity client) LastStand_KillPlayer = { entity old_self = self; self = client; DisableReviveIcon(self.playernum); startspectate(); self = old_self; }; // // LastStand_ReviveTriggerFollow() // Updates the position of the revive trigger // to always track it's owner (downed client), // also handles bleed-out checks. // void() LastStand_ReviveTriggerFollow = { setorigin(self, self.owner.origin); self.nextthink = time + 1; // Iterate our death timer if we're not being revived. if (self.owner.beingrevived == false) self.downed++; if (self.downed >= LASTSTAND_DEATH_TIMER) { // Kill our owner. LastStand_KillPlayer(self.owner); // Kill ourselves. remove(self); } }; // // LastStand_UnlinkRevivee(client) // Reviving has stopped, revert the state of // both clients where necessary. // void(entity client) LastStand_UnlinkRevivee = { // We can move again. client.speed_penalty = client.speed_penalty_time = 0; // We're no longer being revived. client.beingrevived = false; // Remove our broadcast messages for visual feedback. #ifdef FTE FTE_BroadcastMessage(client, CSQC_BROADCAST_NONE, 0, client.playernum); if (client.firer != world) FTE_BroadcastMessage(client.firer, CSQC_BROADCAST_NONE, 0, client.playernum); #endif // FTE if (client.firer != world) { entity old_self = self; self = client.firer; // The previous revivee should deploy their active weapon. W_TakeOut(); // We should also reset their progress bar state. Player_RemoveProgressBar(self); self = old_self; } // We no longer have an active revivee. client.firer.firer = world; client.firer = world; // Revert our revive indicator. ChangeReviveIconState(client.playernum, 1); }; // // LastStand_LinkRevivee(downed_client, revivee) // Reviving has been initiated, link the two clients to // one another for state tracking. // void(entity downed_client, entity revivee) LastStand_LinkRevivee = { // We are, in fact, being revived. downed_client.beingrevived = true; // Link the downed team mate to the client who // initiated the revive. downed_client.firer = revivee; revivee.firer = downed_client; // Downed player should no longer be able to crawl downed_client.speed_penalty = 0.01; downed_client.speed_penalty_time = time + 100; // Set our revive indicator to white. ChangeReviveIconState(downed_client.playernum, 2); #ifdef FTE // FTE-Specific: Broadcast to the downed player they're being revived. FTE_BroadcastMessage(downed_client, CSQC_BROADCAST_BEINGREVIVED, 2, revivee.playernum); // FTE-Specific: Broadcast to revivee that they are reviving the downed player. FTE_BroadcastMessage(revivee, CSQC_BROADCAST_REVIVINGPLAYER, 2, downed_client.playernum); #endif // FTE // Initiate an animation for morphine. entity old_self = self; self = revivee; Set_W_Frame (0, 21, 0, 0, SPRINT, SUB_Null, "models/weapons/morphine/v_morphine.mdl", false, S_RIGHT); self = old_self; }; // // LastStand_TouchReviveTrigger() // Contact made with a downed player's revive trigger, // handles reviving logic. // void() LastStand_TouchReviveTrigger = { // Only trigger for free players, and not ourself. if (other.classname != "player" || other == self.owner) return; // If the team mate is actively being revived, and we are not the one // reviving them, break out. if (self.owner.beingrevived == true && self.owner.firer != other) return; // Is the client touching us down, or are they not facing us, or buying something? if (other.downed || !PlayerIsLooking(other, self.owner) || other.isBuying) { // Were they in the process of reviving us previously? if (self.owner.firer == other) { // Unlink us. LastStand_UnlinkRevivee(self.owner); } return; } // No one is actively reviving the downed team mate. if (self.owner.beingrevived == false) { // Hold F to revive... useprint(other, 13, 0, 0); // Kick-off the revive sequence. if (other.button7) { // Revive time should be significantly shorter with // Quick Revive. float revive_time = 4; if (other.perks & P_REVIVE) revive_time = 2; // Link us together! LastStand_LinkRevivee(self.owner, other); other.fire_delay = other.fire_delay2 = other.reload_delay = other.reload_delay2 = other.knife_delay = time + revive_time; // FIXME // Use ltime on the revive trigger to know when the revive is finished self.ltime = time + revive_time; // Display progress bar for the revivee. Player_InitiateProgressBar(other, revive_time); } return; } // Revivee has released the revive button. if (!self.owner.firer.button7) { LastStand_UnlinkRevivee(self.owner); return; } // Reviving has been executed for the full duration.. if (self.ltime < time) { // Reward the revivee with the points the downed client lost Player_AddScore(other, self.owner.requirespower, false); entity old_self = self; self = self.owner; GetUp(); self = old_self; // Unlink the revivee, no need to stay connected. LastStand_UnlinkRevivee(self.owner); // Remove the revive icon from above the previously downed client DisableReviveIcon(self.owner.playernum); // No reason for the trigger to exist anymore, goodbye! remove(self); } }; // // LastStand_SpawnReviveTrigger(client) // Spawns a trigger to follow a client for reviving. // void(entity client) LastStand_SpawnReviveTrigger = { entity revive_trigger = spawn(); revive_trigger.owner = client; revive_trigger.classname = "revive_trigger"; revive_trigger.think = LastStand_ReviveTriggerFollow; revive_trigger.nextthink = time + 1; revive_trigger.touch = LastStand_TouchReviveTrigger; revive_trigger.solid = SOLID_TRIGGER; setorigin(revive_trigger, client.origin); setsize(revive_trigger, '-52 -52 -16', '52 52 -4'); }; // // LastStand_Begin(client) // Player has gone down, oh noes! Kicks off their // state changes and decides what the game should // do next. // void(entity client) LastStand_Begin = { entity old_self = self; self = client; // 'Pro Gamer Move' achievement. if (rounds <= 1 && self.weapons[0].weapon_magazine == 0 && self.weapons[0].weapon_magazine_left == 0 && self.weapons[0].weapon_reserve == 0 && self.weapons[1].weapon_magazine == 0 && self.weapons[1].weapon_magazine_left == 0 && self.weapons[1].weapon_reserve == 0) { GiveAchievement(9, self); } // Aim out to reset zoom values W_AimOut(); #ifdef FTE self.viewzoom = 1; #endif // FTE // Make any zombies inside of the player's bounding box leave push_away_zombies(); // Force the player to prone. Player_SetStance(self, PLAYER_STANCE_PRONE, false); // Calculate the loss in points, take away points from downed Player. float point_difference; point_difference = self.points; point_difference -= 10*rint((self.points*0.95)/10); Player_RemoveScore(self, point_difference); self.requirespower = point_difference; // Reset state self.velocity = self.zoom = 0; self.downed = true; self.dive_delay = 0; // End the game if no one else is alive. float players_still_alive = PollPlayersAlive(); if ((player_count && !players_still_alive) || (!player_count && !(self.perks & P_REVIVE))) { EndGameSetup(); return; } #ifdef FTE // FTE-Specific: Broadcast that the player has downed to everyone. FTE_BroadcastMessage(world, CSQC_BROADCAST_PLAYERDOWNED, 3, self.playernum); #endif // FTE self.health = 19; // Initiate Self-Revive on Solo if ((self.perks & P_REVIVE) && !player_count) { LastStand_InitiateSoloRevive(self); } // Spawn the Revive Trigger else { LastStand_SpawnReviveTrigger(self); } // Reset the tracker for the achievement "No Perks? No Problem" self.ach_tracker_npnp = 0; // Take away Perks, Mule Kick weapon, etc. LastStand_Penalize(self); // Assign the Player a temporary weapon to use while in Last Stand. LastStand_AssignWeapon(self); // Spawn Revive Sprite in Co-Op if (player_count) { EnableReviveIcon(self.playernum); } // Play Last Stand Animation PAnim_GetDown(); self = old_self; };