quakec/source/server/player/last_stand.qc

505 lines
No EOL
14 KiB
C++

/*
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, false);
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, true);
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;
};