mirror of
https://github.com/nzp-team/quakec.git
synced 2024-11-28 14:52:29 +00:00
580 lines
14 KiB
C++
580 lines
14 KiB
C++
/*
|
|
server/clientfuncs.qc
|
|
|
|
used for any sort of down, hit, etc that the player or entity
|
|
experiences
|
|
|
|
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
|
|
|
|
*/
|
|
void (float achievement_id, optional entity who) GiveAchievement;
|
|
void(entity client) LastStand_Begin;
|
|
|
|
#define DMG_SCORE_HEADSHOT 100 // Death by Headshot
|
|
#define DMG_SCORE_MELEE 130 // Death by Melee
|
|
#define DMG_SCORE_UPPERTORSO 60 // Death by gunshot, upper torso.
|
|
#define DMG_SCORE_LOWERTORSO 50 // Death by gunshot, lower torso.
|
|
#define DMG_SCORE_GRENADE 50 // Death by Grenade.
|
|
#define DMG_SCORE_EXPLOSIVE 60 // Death by Explosive Weapon.
|
|
#define DMG_SCORE_TESLA 50 // Death by Tesla.
|
|
#define DMG_SCORE_STDDAMAGE 10 // Standard Damage reward.
|
|
|
|
void() Barrel_Hit;
|
|
void() teddy_react;
|
|
|
|
void() EndGame_Restart =
|
|
{
|
|
localcmd("restart");
|
|
}
|
|
|
|
// Fade to black function, creates another think for restart
|
|
void() EndGame_FadePrompt =
|
|
{
|
|
nzp_screenflash(self, SCREENFLASH_COLOR_BLACK, 6, SCREENFLASH_FADE_IN);
|
|
|
|
#ifdef FTE
|
|
|
|
self.think = EndGame_Restart;
|
|
|
|
#else
|
|
|
|
self.think = Soft_Restart;
|
|
|
|
#endif // FTE
|
|
|
|
self.nextthink = time + 5;
|
|
}
|
|
|
|
//Actual endgame function, all zombies die, music plays
|
|
void() EndGame =
|
|
{
|
|
local entity oldself;
|
|
local entity who;
|
|
|
|
self.health = 0;
|
|
self.origin = '0 0 0';
|
|
setorigin (self, self.origin);
|
|
self.velocity = '0 0 0';
|
|
|
|
oldself = self;
|
|
|
|
who = find(world,classname,"ai_zombie");
|
|
while(who != world)
|
|
{
|
|
if(who.health)
|
|
{
|
|
self = who;
|
|
self.th_die();
|
|
self = oldself;
|
|
}
|
|
|
|
who = find(who,classname,"ai_zombie");
|
|
}
|
|
|
|
self.think = EndGame_FadePrompt;
|
|
self.nextthink = time + 28;
|
|
}
|
|
|
|
// when dead and other players exist and are alive, throw user into spectate mode
|
|
void() startspectate =
|
|
{
|
|
if (!self.downed)
|
|
return;
|
|
|
|
if (self.beingrevived)
|
|
{
|
|
self.think = startspectate;
|
|
self.nextthink = time + 0.1;
|
|
return;
|
|
}
|
|
|
|
self.downedloop = 0;
|
|
self.beingrevived = false;
|
|
self.health = 100;
|
|
self.weaponmodel = "";
|
|
self.weapon2model = "";
|
|
self.downed = 0;
|
|
self.frame = 0;
|
|
|
|
SpectatorSpawn();
|
|
}
|
|
|
|
// searches for players that are alive given which clients have which playernumbers
|
|
// Returns 1 if there IS someone in the world that's not downed
|
|
float() PollPlayersAlive =
|
|
{
|
|
entity players = find(world, classname, "player");
|
|
|
|
while(players != world) {
|
|
if (!players.downed)
|
|
return 1;
|
|
|
|
players = find(players, classname, "player");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Endgamesetup -- think function for setting up the death of everyone
|
|
void() EndGameSetup =
|
|
{
|
|
self.health = 10;
|
|
self.think = EndGame;
|
|
self.nextthink = time + 4;
|
|
self.weapon = 0;
|
|
self.currentammo = 0;
|
|
self.currentmag = 0;
|
|
self.weaponmodel = "";
|
|
self.weapon2model = "";
|
|
self.animend = SUB_Null;
|
|
self.perks = 0;
|
|
self.isspec = true;
|
|
self.movetype = MOVETYPE_TOSS;
|
|
|
|
if (!game_over) {
|
|
sound(self, CHAN_AUTO, "sounds/music/end.wav", 1, ATTN_NONE);
|
|
NotifyGameEnd();
|
|
}
|
|
game_over = true;
|
|
Player_RemoveScore(self, self.points);
|
|
Player_AddScore(self, self.score, false);
|
|
return;
|
|
}
|
|
|
|
float() push_away_zombies;
|
|
|
|
void () GetUp =
|
|
{
|
|
// Play Getting Up Animation
|
|
PAnim_GetUp();
|
|
|
|
Player_SetStance(self, PLAYER_STANCE_STAND, false);
|
|
|
|
// Bad hack: if we're crouching, just play some dummy crouchwalk frames.
|
|
if (self.stance == PLAYER_STANCE_CROUCH)
|
|
PAnim_CrouchWalk7();
|
|
|
|
self.health = 100;
|
|
self.downedloop = 0; // used for death timing vs endgame
|
|
self.downed = 0;
|
|
self.classname = "player";
|
|
|
|
// Take away the ammo that was fired while in last stand.
|
|
if (self.weapon != W_COLT) {
|
|
if (self.weapon == self.weaponbk) {
|
|
self.currentammobk -= self.teslacount;
|
|
|
|
// Take from the mag if the reserve is empty now
|
|
if (self.currentammobk < 0) {
|
|
self.currentmagbk += self.currentammobk;
|
|
self.currentammobk = 0;
|
|
}
|
|
} else if (self.weapon == self.weapons[1].weapon_id) {
|
|
self.weapons[1].weapon_reserve -= self.teslacount;
|
|
|
|
// Take from the mag if the reserve is empty now
|
|
if (self.weapons[1].weapon_reserve < 0) {
|
|
self.weapons[0].weapon_magazine += self.weapons[1].weapon_reserve;
|
|
self.weapons[1].weapon_reserve = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
self.teslacount = 0;
|
|
|
|
Weapon_AssignWeapon(0, self.weaponbk, self.currentmagbk, self.currentammobk);
|
|
};
|
|
|
|
// poll checking whether to see if our revive invoke is active
|
|
void(entity ent) CheckRevive =
|
|
{
|
|
if (self.invoke_revive) {
|
|
GetUp();
|
|
DisableReviveIcon(self.playernum);
|
|
self.invoke_revive = 0;
|
|
self.firer = world;
|
|
}
|
|
}
|
|
|
|
void(entity attacker, float d_style) DieHandler =
|
|
{
|
|
float t;
|
|
|
|
t = random();
|
|
|
|
if (self.classname == "ai_zombie" || self.classname == "ai_dog") {
|
|
self.th_die();
|
|
}
|
|
|
|
if (attacker.classname == "player") {
|
|
attacker.kills++;
|
|
|
|
float points_earned = 0;
|
|
switch(d_style) {
|
|
case DMG_TYPE_HEADSHOT:
|
|
points_earned = DMG_SCORE_HEADSHOT;
|
|
attacker.headshots++;
|
|
break;
|
|
case DMG_TYPE_MELEE:
|
|
points_earned = DMG_SCORE_MELEE;
|
|
break;
|
|
case DMG_TYPE_TESLA:
|
|
points_earned = DMG_SCORE_GRENADE;
|
|
break;
|
|
case DMG_TYPE_FLAMETHROWER:
|
|
points_earned = DMG_SCORE_GRENADE;
|
|
|
|
// override their death sound (FIXME: make a new sound..)
|
|
sound(self, CHAN_BODY, "sounds/pu/drop.wav", 1, ATTN_NORM);
|
|
break;
|
|
case DMG_TYPE_GRENADE:
|
|
points_earned = DMG_SCORE_GRENADE;
|
|
break;
|
|
case DMG_TYPE_EXPLOSIVE:
|
|
points_earned = DMG_SCORE_EXPLOSIVE;
|
|
break;
|
|
case DMG_TYPE_LOWERTORSO:
|
|
points_earned = DMG_SCORE_LOWERTORSO;
|
|
break;
|
|
case DMG_TYPE_UPPERTORSO:
|
|
points_earned = DMG_SCORE_UPPERTORSO;
|
|
break;
|
|
default:
|
|
if (cvar("developer"))
|
|
bprint(PRINT_HIGH, "DieHandler: Received invalid style\n");
|
|
break;
|
|
}
|
|
Player_AddScore(attacker, points_earned, true);
|
|
}
|
|
}
|
|
|
|
void(entity victim, entity attacker, float damage, float d_style) DamageHandler = {
|
|
// don't do any attacking during nuke delay
|
|
if (d_style == DMG_TYPE_ZOMBIESWIPE && nuke_powerup_active > time)
|
|
return;
|
|
|
|
entity old_self;
|
|
if (victim.classname == "ai_zombie" || victim.classname == "ai_dog") {
|
|
|
|
if (attacker.classname == "player" && (victim.health - damage) > 0 && d_style != DMG_TYPE_OTHER) {
|
|
Player_AddScore(attacker, DMG_SCORE_STDDAMAGE, true);
|
|
}
|
|
|
|
victim.health = victim.health - damage;
|
|
|
|
if (d_style == DMG_TYPE_EXPLOSIVE && damage != 0) {
|
|
if (victim.health > 0 && victim.crawling == 2) {
|
|
makeCrawler(victim);
|
|
GiveAchievement(3, attacker);
|
|
}
|
|
}
|
|
|
|
if (victim.health <= 0 || instakill_finished > time) {
|
|
old_self = self;
|
|
self = victim;
|
|
DieHandler(attacker, d_style);
|
|
self = old_self;
|
|
}
|
|
} else if (victim.classname == "player" && !victim.downed) {
|
|
|
|
if (victim.flags & FL_GODMODE) {
|
|
return;
|
|
}
|
|
|
|
// Abstinence Program
|
|
victim.ach_tracker_abst = 1;
|
|
|
|
victim.health -= damage;
|
|
|
|
// Determine how long to wait before regenerating based on our Health ratio
|
|
float ratio = victim.health / victim.max_health;
|
|
|
|
// Badly hurt = longer delay.
|
|
if (ratio <= 0.2) {
|
|
victim.health_delay = time + 5;
|
|
|
|
// We're really low, so let's regen REALLY slow.
|
|
victim.health_was_very_low = true;
|
|
} else {
|
|
victim.health_delay = time + 2.4;
|
|
}
|
|
|
|
// shake the camera on impact
|
|
vector distance;
|
|
distance = attacker.angles - victim.angles;
|
|
|
|
// just to prevent radical punchangles
|
|
while(distance_x > 10 || distance_x < -10) {
|
|
distance_x /= 2;
|
|
}
|
|
while(distance_y > 10 || distance_y < -10) {
|
|
distance_y /= 2;
|
|
}
|
|
|
|
// apply
|
|
victim.punchangle_x = distance_x;
|
|
victim.punchangle_y = distance_y;
|
|
|
|
// Play pain noise if this isn't done by an electric barrier.
|
|
if (d_style != DMG_TYPE_ELECTRICTRAP)
|
|
sound (victim, CHAN_AUTO, "sounds/player/pain4.wav", 1, ATTN_NORM);
|
|
else
|
|
sound (victim, CHAN_AUTO, "sounds/machines/elec_shock.wav", 1, ATTN_NORM);
|
|
|
|
if (victim.sprinting) {
|
|
old_self = self;
|
|
self = victim;
|
|
W_SprintStop();
|
|
self = old_self;
|
|
}
|
|
|
|
victim.sprint_delay = time + 0.75;
|
|
|
|
// Was 20 for.. some reason.
|
|
if (victim.health <= 1)
|
|
{
|
|
LastStand_Begin(victim);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
============
|
|
CanDamage
|
|
|
|
Returns true if the inflictor can directly damage the target. Used for
|
|
explosions and melee attacks.
|
|
============
|
|
*/
|
|
float(entity targ, entity inflictor) CanDamage =
|
|
{
|
|
if (targ.flags == FL_GODMODE)
|
|
return FALSE;
|
|
// bmodels need special checking because their origin is 0,0,0
|
|
if (targ.movetype == MOVETYPE_PUSH)
|
|
{
|
|
traceline(inflictor.origin, 0.5 * (targ.absmin + targ.absmax), TRUE, self);
|
|
if (trace_fraction == 1)
|
|
return TRUE;
|
|
if (trace_ent == targ)
|
|
return TRUE;
|
|
return FALSE;
|
|
}
|
|
|
|
traceline(inflictor.origin, targ.origin, TRUE, self);
|
|
if (trace_fraction == 1)
|
|
return TRUE;
|
|
traceline(inflictor.origin, targ.origin + '15 15 0', TRUE, self);
|
|
if (trace_fraction == 1)
|
|
return TRUE;
|
|
traceline(inflictor.origin, targ.origin + '-15 -15 0', TRUE, self);
|
|
if (trace_fraction == 1)
|
|
return TRUE;
|
|
traceline(inflictor.origin, targ.origin + '-15 15 0', TRUE, self);
|
|
if (trace_fraction == 1)
|
|
return TRUE;
|
|
traceline(inflictor.origin, targ.origin + '15 -15 0', TRUE, self);
|
|
if (trace_fraction == 1)
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
};
|
|
|
|
float(float min, float max, vector org1, vector org2, float radius) calculate_proximity_value =
|
|
{
|
|
float proximity_value;
|
|
float distance = fabs(vlen(org1 - org2));
|
|
|
|
if (distance <= radius) {
|
|
float normalized_distance = distance / radius;
|
|
proximity_value = min + (max - min) * (1 - normalized_distance);
|
|
} else {
|
|
proximity_value = min;
|
|
}
|
|
|
|
return proximity_value;
|
|
};
|
|
|
|
void(entity inflictor, entity attacker, float damage2, float mindamage, float radius) DamgageExplode =
|
|
{
|
|
float final_damage = 0;
|
|
float damage_style = 0;
|
|
entity ent;
|
|
|
|
float multi, r;
|
|
ent = findradius(inflictor.origin, radius);
|
|
|
|
while (ent != world)
|
|
{
|
|
if(ent.classname == "player")
|
|
{
|
|
if (ent.perks & P_FLOP) // PhD Flopper makes us immune to any explosive damage
|
|
final_damage = 0;
|
|
else if (inflictor.classname == "betty") // Self-inflicted betties don't do damage either.
|
|
final_damage = 0;
|
|
else if (inflictor.owner != ent) // we don't want OUR explosives to harm other players..
|
|
final_damage = 0;
|
|
else
|
|
{
|
|
final_damage = (radius - vlen(inflictor.origin - ent.origin))*1.5;
|
|
|
|
if(final_damage < 0)
|
|
{
|
|
ent = ent.chain;
|
|
continue;
|
|
}
|
|
|
|
if (final_damage > radius * 0.75)
|
|
final_damage = 100;
|
|
|
|
if (final_damage < other.health)
|
|
{
|
|
Player_AddScore(self, DMG_SCORE_STDDAMAGE, false);
|
|
}
|
|
else if (final_damage > other.health)
|
|
{
|
|
Player_AddScore(self, DMG_SCORE_STDDAMAGE, false);
|
|
}
|
|
else
|
|
{
|
|
final_damage /= radius;
|
|
final_damage *= 60;
|
|
}
|
|
|
|
// inflicting damage from an explosive introduces a delay before
|
|
// player can sprint again.
|
|
ent.sprint_delay = time + 3;
|
|
}
|
|
// shake the camera on impact
|
|
vector distance;
|
|
distance = inflictor.angles - ent.angles;
|
|
|
|
// just to prevent radical punchangles
|
|
while(distance_x > 10 || distance_x < -10) {
|
|
distance_x /= 2;
|
|
}
|
|
while(distance_y > 10 || distance_y < -10) {
|
|
distance_y /= 2;
|
|
}
|
|
|
|
// apply
|
|
ent.punchangle_x = distance_x;
|
|
ent.punchangle_y = distance_y;
|
|
}
|
|
else if (ent.classname == "explosive_barrel")
|
|
{
|
|
final_damage = radius - vlen(inflictor.origin - ent.origin);
|
|
final_damage *= 4;
|
|
|
|
if (final_damage < 0)
|
|
{
|
|
ent = ent.chain;
|
|
continue;
|
|
}
|
|
|
|
ent.health -= final_damage;
|
|
|
|
entity oldself;
|
|
oldself = self;
|
|
self = ent;
|
|
Barrel_Hit();
|
|
self = oldself;
|
|
}
|
|
else if (ent.classname == "teddy_spawn")
|
|
{
|
|
entity oldself2;
|
|
oldself2 = self;
|
|
self = ent;
|
|
teddy_react();
|
|
self = oldself2;
|
|
}
|
|
else if (ent.takedamage && ent.classname != "ai_zombie_head" && ent.classname != "ai_zombie_larm" && ent.classname != "ai_zombie_rarm")
|
|
{
|
|
if (mapname == "ndu" && ent.classname == "ai_zombie" && inflictor.classname == "explosive_barrel") {
|
|
ach_tracker_barr++;
|
|
|
|
if (ach_tracker_barr >= 15)
|
|
{
|
|
GiveAchievement(13);
|
|
}
|
|
}
|
|
|
|
// cypress -- accurate explosive damage
|
|
// cod likes to override the damage to ai with
|
|
// explosives (yay!).
|
|
if (inflictor.classname == "grenade") {
|
|
// grenades follow the logic of (rounds + (rand 150-500))
|
|
// rounds is basically meaningless though.. wtf?
|
|
final_damage = rounds + rint(random() * 350) + 150;
|
|
damage_style = DMG_TYPE_GRENADE;
|
|
} else if (inflictor.classname == "projectile_grenade") {
|
|
// projectile-based grenades (mustang & sally) seem to do
|
|
// more damage than standard grenades.
|
|
// (rounds + (rand 1200-5000))
|
|
final_damage = rounds + rint(random() * 3800) + 1200;
|
|
damage_style = DMG_TYPE_EXPLOSIVE;
|
|
} else if (inflictor.classname == "rocket") {
|
|
// rockets were kinda tricky to figure out, this is as close
|
|
// as i can get and i'm not super confident..
|
|
// (rounds * (rand 0-100) * weapon_damage/500).
|
|
final_damage = (rounds * rint(random() * 100)) * damage2/500;
|
|
damage_style = DMG_TYPE_EXPLOSIVE;
|
|
} else if (inflictor.classname == "projectile_raybeam") {
|
|
//final_damage = calculate_proximity_value(mindamage, damage2, inflictor.origin, ent.origin, radius);
|
|
//bprint(PRINT_HIGH, strcat("damage: ", ftos(final_damage), "\n"));
|
|
final_damage = damage2;
|
|
damage_style = DMG_TYPE_GRENADE;
|
|
} else if (inflictor.classname == "player") {
|
|
// phd flopper.
|
|
final_damage = calculate_proximity_value(mindamage, damage2, inflictor.origin, ent.origin, radius);
|
|
damage_style = DMG_TYPE_GRENADE;
|
|
} else {
|
|
r = rounds;
|
|
multi = 1.07;
|
|
while(r > 0)
|
|
{
|
|
multi *= 1.05;
|
|
r --;
|
|
}
|
|
|
|
if (mindamage == 75)
|
|
{
|
|
final_damage = (200 * multi) + 185;
|
|
}
|
|
else
|
|
{
|
|
final_damage = (mindamage + damage2)/2;
|
|
}
|
|
}
|
|
|
|
// to decide if this should make a crawler, check if we've done 10% or more damage
|
|
if (final_damage / ent.health > 0.10 && ent.crawling != true && ent.classname != "ai_dog") {
|
|
ent.crawling = 2;
|
|
}
|
|
}
|
|
|
|
if (final_damage > 0) {
|
|
if (CanDamage (ent, inflictor))
|
|
DamageHandler (ent, attacker, final_damage, damage_style);
|
|
}
|
|
ent = ent.chain;
|
|
}
|
|
};
|