513 lines
20 KiB
Plaintext
513 lines
20 KiB
Plaintext
/*======================================================================
|
|
COMBAT FUNCTIONS
|
|
|
|
Returns true if the inflictor can directly damage the target.
|
|
Used for explosions and melee attacks.
|
|
======================================================================*/
|
|
void(entity targ, entity attacker) ClientObituary;
|
|
|
|
//-----------------------------------------------------------
|
|
float(entity targ, entity inflictor) CanDamage =
|
|
{
|
|
// Any entity marked with no damage is automatically excluded
|
|
if (targ.takedamage == DAMAGE_NO) return FALSE;
|
|
|
|
// bmodels need special checking because their origin is 0,0,0
|
|
// Not all bmodels use push movetype, use bsporigin instead
|
|
// if (targ.movetype == MOVETYPE_PUSH) {
|
|
if (targ.bsporigin == TRUE) {
|
|
traceline(inflictor.origin, 0.5 * (targ.absmin + targ.absmax), TRUE, inflictor);
|
|
if (trace_fraction == 1) return TRUE;
|
|
if (trace_ent == targ) return TRUE;
|
|
return FALSE;
|
|
}
|
|
|
|
// Trace around the target to see if it can hit
|
|
traceline(inflictor.origin, targ.origin, TRUE, inflictor);
|
|
if (trace_fraction == 1) return TRUE;
|
|
traceline(inflictor.origin, targ.origin + '15 15 0', TRUE, inflictor);
|
|
if (trace_fraction == 1) return TRUE;
|
|
traceline(inflictor.origin, targ.origin + '-15 -15 0', TRUE, inflictor);
|
|
if (trace_fraction == 1) return TRUE;
|
|
traceline(inflictor.origin, targ.origin + '-15 15 0', TRUE, inflictor);
|
|
if (trace_fraction == 1) return TRUE;
|
|
traceline(inflictor.origin, targ.origin + '15 -15 0', TRUE, inflictor);
|
|
if (trace_fraction == 1) return TRUE;
|
|
|
|
return FALSE;
|
|
};
|
|
|
|
//-----------------------------------------------------------
|
|
void(entity targ, entity attacker) Killed =
|
|
{
|
|
local entity oself;
|
|
|
|
// Switch self for death functions
|
|
oself = self; self = targ;
|
|
self.activate = attacker; // Always keep track of what did damage
|
|
|
|
// func bmodels, triggers, breakables etc
|
|
// Horrible hack for pushables; They switch movetype, hard to catch
|
|
if (self.movetype == MOVETYPE_PUSH || self.movetype == MOVETYPE_NONE ||
|
|
self.classtype == CT_FUNCPUSHABLE) {
|
|
self.th_die ();
|
|
}
|
|
else {
|
|
if (!self.deadflag && (self.flags & FL_MONSTER || self.flags & FL_CLIENT) ) {
|
|
// Object is dying, stop re-triggering
|
|
self.deadflag = DEAD_DYING;
|
|
self.enemy = attacker; // Enemy dealing final blow
|
|
if (self.enemy.classtype == CT_WORLD) self.enemy = self;
|
|
|
|
// bump the monster counter
|
|
if (self.flags & FL_MONSTER) {
|
|
// If a monster marked with no monster count dies
|
|
// update HUD totals to reflect death properly
|
|
if (self.nomonstercount) {
|
|
total_monsters = total_monsters + 1;
|
|
update_hud_totals(HUD_MONSTERS);
|
|
}
|
|
// Update global monster death totals
|
|
killed_monsters = killed_monsters + 1;
|
|
WriteByte (MSG_ALL, SVC_KILLEDMONSTER);
|
|
}
|
|
|
|
ClientObituary(self, attacker);
|
|
self.touch = SUB_Null;
|
|
|
|
if (self.flags & FL_MONSTER) monster_death_use();
|
|
self.th_die ();
|
|
}
|
|
// Completely fuckup setup for the oldone boss with a weird target
|
|
// field and mess of triggers around it! This elseif is just an
|
|
// exception so it works, anyway no one fights the oldone!
|
|
else if (!self.deadflag && self.classtype == CT_MONIDSHUB) {
|
|
self.deadflag = DEAD_DEAD;
|
|
killed_monsters = killed_monsters + 1;
|
|
WriteByte (MSG_ALL, SVC_KILLEDMONSTER);
|
|
self.th_die ();
|
|
}
|
|
}
|
|
// Restore self
|
|
self = oself;
|
|
};
|
|
|
|
/*======================================================================
|
|
T_Damage
|
|
The damage is coming from inflictor, but get mad at attacker
|
|
This should be the only function that ever reduces health.
|
|
Parameters:
|
|
targ = entity receiving damage (monster)
|
|
inflictor = entity causing damage (bullets)
|
|
attacker = parent entity of inflictor (player firing bullets)
|
|
damage = quantity (before modifiers, quad etc)
|
|
checkarmor = can bypass armour (DAMARMOR, NOARMOR)
|
|
======================================================================*/
|
|
void(entity targ, entity inflictor, entity attacker, float damage, float checkarmor) T_Damage =
|
|
{
|
|
local vector dir;
|
|
local entity oldself;
|
|
local float save, take, flinch;
|
|
|
|
// Cannot hurt the world or enemy targets (fake flying markers)
|
|
if (targ.classtype == CT_WORLD) return;
|
|
if (targ.classtype == CT_ENEMYTARGET) return;
|
|
|
|
// Is the monster dying and going through death animation?
|
|
if (targ.flags & FL_MONSTER && targ.health < 1) {
|
|
if (targ.health > targ.gibhealth) {
|
|
// SSG projectiles need to do more damage (higher chance of gib)
|
|
if (inflictor.classtype == CT_PROJ_SSG) take = 1 + random()*0.25;
|
|
// Lowered the chance of extra gibs from other weapons
|
|
// way to much gibbing was happening to all monsters
|
|
else take = 1;
|
|
|
|
// Keep taking health off after death
|
|
targ.health = targ.health - (damage*take);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If target cannot take damage, no point doing damage
|
|
if (targ.takedamage == DAMAGE_NO) return;
|
|
|
|
// Check for breakable/pushables being immune to monster damage
|
|
// This check is present in all projectiles functions instead
|
|
// Its not really necessary here because its about monsters
|
|
// if (ai_immunedamage(attacker, targ)) return;
|
|
|
|
// used by buttons and triggers to set activator for target firing
|
|
damage_attacker = attacker;
|
|
|
|
// check for quad damage powerup on the attacker
|
|
if (attacker.super_damage_finished > 0) damage = damage * 4;
|
|
|
|
// co-op team play damage avoidance
|
|
if ( (teamplay == 1) && (targ.team > 0) && (targ.team == attacker.team) ) return;
|
|
|
|
// Poor zombies, they have so many exceptional ways to die!
|
|
if (targ.classgroup == CG_ZOMBIE) {
|
|
// If a zombie falls into slime or lava, instant death
|
|
if (targ.watertype == CONTENT_SLIME || targ.watertype == CONTENT_LAVA) {
|
|
damage = fabs(targ.gibhealth * 4);
|
|
//dprint("\b[T_DAMAGE]\b Instant death, in slime or lava\n");
|
|
}
|
|
// Exploding boils always destroy zombies
|
|
if (attacker.classtype == CT_MONBOIL) damage = fabs(targ.gibhealth * 2);
|
|
}
|
|
|
|
// Are monsters infighting and is there a damage modifier active?
|
|
if (attacker.infightextra && targ.flags & FL_MONSTER)
|
|
damage = damage * attacker.infightextra;
|
|
|
|
//----------------------------------------------------------------------
|
|
// Check for plasma burn/explosion on fatal hit
|
|
if (inflictor.classtype == CT_PROJ_PLASMA && attacker.flags & FL_CLIENT) {
|
|
if ((targ.health-damage < 0) && attacker.plasma_burn < time && random() < 0.2) {
|
|
// Stop the plasma burn effect not happening alot at once
|
|
attacker.plasma_burn = time + 4 + random()*4;
|
|
// Don't add anymore damage if quad active
|
|
if (attacker.super_damage_finished == 0)
|
|
damage = (targ.health - targ.gibhealth);
|
|
// Big blue sprite explosion + particles
|
|
particle_explode(targ.origin-'0 0 16', 50+random()*50, 2, PARTICLE_BURST_BLUE, PARTICLE_BURST_UPWARD);
|
|
SpawnExplosion(EXPLODE_PLASMA_BIG, inflictor.origin+'0 0 8', SOUND_REXP3);
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// Check for monster/breakable extra damage key condition
|
|
// The key is usually checked with ai_damagebreakable function
|
|
// Needs to be checked with projectiles as well
|
|
if (targ.classgroup == CG_BREAKABLE && attacker.flags & FL_MONSTER) {
|
|
if (targ.brkmondmg > 0) damage = damage + targ.brkmondmg;
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// save damage based on the target's armor level
|
|
//----------------------------------------------------------------------
|
|
save = 0;
|
|
if (checkarmor == DAMARMOR) {
|
|
save = ceil(targ.armortype*damage);
|
|
if (save >= targ.armorvalue) {
|
|
save = targ.armorvalue;
|
|
targ.armortype = 0; // lost all armor
|
|
targ.items = targ.items - (targ.items & (IT_ARMOR1 | IT_ARMOR2 | IT_ARMOR3));
|
|
}
|
|
// Reduce armour based on save forumla above
|
|
targ.armorvalue = targ.armorvalue - save;
|
|
// CEIL function will round up any fractional damage
|
|
// TBH it is better to round up than round down
|
|
take = ceil(damage-save);
|
|
}
|
|
else take = damage;
|
|
|
|
//----------------------------------------------------------------------
|
|
// add to the damage total for clients, which will be sent as a single
|
|
// message at the end of the frame
|
|
// FIXME: remove after combining shotgun blasts?
|
|
//----------------------------------------------------------------------
|
|
if (targ.flags & FL_CLIENT) {
|
|
targ.dmg_take = targ.dmg_take + take;
|
|
targ.dmg_save = targ.dmg_save + save;
|
|
targ.dmg_inflictor = inflictor;
|
|
}
|
|
|
|
// figure momentum add
|
|
if ( (inflictor != world) && (targ.movetype == MOVETYPE_WALK) ) {
|
|
dir = targ.origin - (inflictor.absmin + inflictor.absmax) * 0.5;
|
|
dir = normalize(dir);
|
|
targ.velocity = targ.velocity + dir*damage*8;
|
|
}
|
|
|
|
// check for godmode or invincibility
|
|
if (targ.flags & FL_GODMODE || targ.invincible_finished >= time) {
|
|
if (targ.invincible_sound < time) {
|
|
sound (targ, CHAN_ITEM, SOUND_ARTPENT3, 1, ATTN_NORM);
|
|
targ.invincible_sound = time + 2;
|
|
}
|
|
return;
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// do the damage
|
|
//----------------------------------------------------------------------
|
|
targ.health = targ.health - take;
|
|
|
|
//----------------------------------------------------------------------
|
|
// Check monster turret is to be released based on % health
|
|
// Fire any targets (once only) when this condition is met
|
|
//----------------------------------------------------------------------
|
|
if (targ.turrethealth > 0 && targ.movespeed < 0) {
|
|
if (targ.health < (targ.max_health * targ.turrethealth)) {
|
|
if (targ.turrettarget != "") {
|
|
// Fire all targets and remove trigger string
|
|
trigger_strs(targ.turrettarget, attacker);
|
|
targ.turrettarget = "";
|
|
}
|
|
// Release turret, free movement
|
|
targ.movespeed = 1;
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// When the SG projectile system is active each monster needs to keep
|
|
//track of cumulative damage so that pain flinches work correctly
|
|
// AmmoShells classgroup is only active when SG projectiles are fired
|
|
//----------------------------------------------------------------------
|
|
// Is the Shotguns projectile system active?
|
|
if (inflictor.classgroup == CG_PROJSHELLS) {
|
|
// Does the frame damage acculator need to be reset?
|
|
if (targ.dmgtimeframe <= time) {
|
|
targ.dmgtimeframe = time + 0.1;
|
|
targ.dmgcombined = 0;
|
|
}
|
|
// Accumulate damage until next fromae
|
|
targ.dmgcombined = targ.dmgcombined + take;
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// When the SG projectile system is active ZOMBIES need to know if
|
|
// there has been enough damage over a single frame to die or not
|
|
// AmmoShells classgroup is only active when SG projectiles are fired
|
|
//----------------------------------------------------------------------
|
|
if (targ.classgroup == CG_ZOMBIE) {
|
|
// Zombies MUST ALWAYS RUN their pain function!?!
|
|
self.pain_finished = 0;
|
|
// Horrible Hack - Launch_Bullet sets the weapon flag on projectiles
|
|
// Only register damage against zombies if SSG upgrade or Quad!
|
|
if (inflictor.classgroup == CG_PROJSHELLS && inflictor.weapon) {
|
|
// Has the accumulator gone over the max health?
|
|
if (targ.dmgcombined > targ.max_health) {
|
|
targ.health = targ.gibhealth;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
// Axe upgrade will gib enemies if they are killed with the axe
|
|
// A bit excessive on the gibbing, disabled for the moment
|
|
if (attacker.moditems & IT_SHADAXE && attacker.weapon == IT_AXE) {
|
|
if (targ.health < 1 && targ.gibhealth) targ.health = targ.gibhealth;
|
|
} */
|
|
|
|
//----------------------------------------------------------------------
|
|
// Has the monster died?
|
|
//----------------------------------------------------------------------
|
|
if (targ.health < 1) {
|
|
Killed (targ, attacker);
|
|
return;
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// Switch self (foundtarget and pain function need self correct)
|
|
// self restored before end of function
|
|
oldself = self;
|
|
self = targ;
|
|
//----------------------------------------------------------------------
|
|
// react to the damage - only if a monster
|
|
// No reaction if attacker is world (env trap, door, etc)
|
|
//----------------------------------------------------------------------
|
|
if ( (self.flags & FL_MONSTER) && attacker != world) {
|
|
// Is the current attacker not self and not current enemy target?
|
|
if (self != attacker && attacker != SUB_entEnemyTarget()) {
|
|
if ( self.noinfighting == TRUE && attacker.flags & FL_MONSTER
|
|
|| attacker.noinfighting == TRUE) {
|
|
// Prevent monsters from infighting
|
|
// but let them react to the player
|
|
}
|
|
else {
|
|
// If previous switch target dead, focus on new target
|
|
if (self.switchattacker.health < 1) self.switchtimer = 0;
|
|
// Is the attacker a different classgroup and switching target available?
|
|
if ( self.classgroup != attacker.classgroup && self.switchtimer < time) {
|
|
// Prevent the monster from rapidily switching targets
|
|
if (self.switchattacker != attacker) self.switchtimer = time + self.switchoverride;
|
|
else self.switchtimer = time + ( (self.switchoverride + random()) * 0.5);
|
|
// Switch to new target
|
|
self.switchattacker = attacker;
|
|
|
|
// Shift previous enemy to old enemy ready for new target
|
|
// Only switch if the new enemy is the player! (id logic)
|
|
// Check for new enemy target system before switching
|
|
if (SUB_flagsEnemyTarget(FL_CLIENT)) self.oldenemy = SUB_entEnemyTarget();
|
|
if (self.enemy.classtype == CT_ENEMYTARGET) SUB_switchEnemyTarget();
|
|
self.enemy = attacker; // attack the attacker!
|
|
FoundTarget (); // Run+turn towards new enemy
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self = oldself;
|
|
|
|
//----------------------------------------------------------------------
|
|
// Check for ammo type resistance and immunity to pain
|
|
// Targetdummies rely on the pain function to display stats
|
|
if (targ.classtype != CT_TARGETDUMMY) {
|
|
if (targ.resist_shells > 0 && inflictor.classgroup == CG_PROJSHELLS) return;
|
|
else if (targ.resist_nails > 0 && inflictor.classgroup == CG_PROJNAILS) return;
|
|
else if (targ.resist_rockets > 0 && inflictor.classgroup == CG_PROJROCKETS) return;
|
|
else if (targ.resist_cells > 0 && inflictor.classgroup == CG_PROJCELLS) return;
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// Switch self (foundtarget and pain function need self correct)
|
|
// self restored before end of function
|
|
oldself = self;
|
|
self = targ;
|
|
//----------------------------------------------------------------------
|
|
|
|
if (self.th_pain && self.pain_finished < time) {
|
|
// Save pain finished state for later
|
|
self.pain_finstate = self.pain_finished;
|
|
|
|
// The damage from projectile shotguns trinkle at the monster
|
|
// which means it cannot trigger the flinch function
|
|
// Let the combined damage over 1 frame accumulate first
|
|
// Exception - Zombies don't flinch, they must run pain function
|
|
if (inflictor.classgroup == CG_PROJSHELLS && targ.classgroup != CG_ZOMBIE) {
|
|
// Demon, Shambler, Shalrath and Wizard all have high flinch values
|
|
if (self.pain_flinch > 40) flinch = 40;
|
|
else flinch = self.pain_flinch + 1;
|
|
if (self.dmgcombined < flinch) {self = oldself; return;}
|
|
}
|
|
|
|
// Block pain re-trigger event
|
|
self.pain_finished = time + self.pain_timeout;
|
|
|
|
// Ignoring pain from other monsters? (infighting)
|
|
if (self.pain_ignore && attacker.flags & FL_MONSTER)
|
|
self.pain_finished = self.pain_finished + 1 + random();
|
|
else {
|
|
// If accumulated pain is higher, use that instead
|
|
// Added classgroup exception otherwise damage qty is wrong
|
|
if (inflictor.classgroup == CG_PROJSHELLS && take < self.dmgcombined)
|
|
self.th_pain (inflictor, attacker, self.dmgcombined);
|
|
else self.th_pain (inflictor, attacker, take);
|
|
|
|
// nightmare mode monsters don't go into pain frames often
|
|
if (skill == SKILL_NIGHTMARE && targ.classgroup != CG_ZOMBIE) self.pain_finished = time + 5;
|
|
|
|
// Exceptions to pain not being blocked (even nightmare!)
|
|
// If the monster is in slime or lava always go into pain
|
|
if (self.watertype == CONTENT_SLIME || self.watertype == CONTENT_LAVA)
|
|
self.pain_finished = time + 0.3;
|
|
}
|
|
}
|
|
self = oldself;
|
|
};
|
|
|
|
/*======================================================================
|
|
T_RadiusDamage
|
|
|
|
inflictor -(self) = projectile
|
|
attacker - (self.owner) = monster/player OR (self) = direct attack
|
|
ignore - (other) = Original target OR (world) = no exceptions
|
|
======================================================================*/
|
|
void(entity inflictor, entity attacker, float damage, entity ignore, float checkclass) T_RadiusDamage =
|
|
{
|
|
local float points;
|
|
local entity head;
|
|
local vector org, imporg;
|
|
|
|
// Cannot hurt the world or enemy targets (fake flying markers)
|
|
if (attacker.noradiusdmg) return;
|
|
if (attacker.classtype == CT_WORLD) return;
|
|
if (attacker.classtype == CT_ENEMYTARGET) return;
|
|
|
|
// check if inflictor is a bmodel (different origin location)
|
|
if (inflictor.bsporigin) imporg = bmodel_origin(inflictor);
|
|
else imporg = inflictor.origin;
|
|
|
|
head = findradius(imporg, damage+40);
|
|
|
|
while (head) {
|
|
if (head != ignore) {
|
|
// This is used mostly for wraiths so when they die they don't
|
|
// kill each other, can't get the code logic to work so its
|
|
// setup as a dead exception path
|
|
if (checkclass == IGNORECLASS && head.classtype == ignore.classtype) { }
|
|
else {
|
|
// Check for breakable/pushable monster immunity
|
|
// Do nothing, radius damage has no miss effect
|
|
if (ai_immunedamage(attacker, head)) {
|
|
}
|
|
// Check for any breakables which are prone to explosive damage
|
|
else if (ai_foundbreakable(attacker, head,TRUE) && head.brktrigmissile !=0) {
|
|
trigger_ent(head, attacker);
|
|
}
|
|
else {
|
|
// Can be damaged and NOT immune to radius (splash) damage
|
|
if (head.takedamage > 0 && head.noradiusdmg == 0) {
|
|
// This formula is using bounding box size which means
|
|
// the damage amount is lower if the monster is larger
|
|
// It should really be based on bounding box edge
|
|
// not monster origin
|
|
org = head.origin + (head.mins + head.maxs)*0.5;
|
|
points = 0.5*vlen (imporg - org);
|
|
if (points < 0) points = 0;
|
|
points = damage - points;
|
|
|
|
// Half damage if caught by own explosion
|
|
if (head == attacker) points = points * 0.5;
|
|
|
|
if (points > 0 && head.health > 0) {
|
|
// Need CanDamage to check for anything blocking LoS
|
|
// It will do several traces (source->target) to check
|
|
// for any blocking entities (lifts, doors, walls etc)
|
|
if (CanDamage (head, inflictor)) {
|
|
// Splash damage is part of rocket resistance
|
|
if (head.resist_rockets > 0)
|
|
points = Resist_Damage(head, IT_ROCKETS, points);
|
|
|
|
// Zombies should really be blown from explosives
|
|
// especially from ogre grenades right under their feet
|
|
// Check for glancing damage or if too far away
|
|
if (head.classgroup == CG_ZOMBIE && points > (head.health*0.25))
|
|
points = DAMAGE_ZOMBIECLASS;
|
|
// Any damage left? Check for poison as well
|
|
if (points > 0) T_Damage (head, inflictor, attacker, points, DAMARMOR);
|
|
if (attacker.poisonous) PoisonDeBuff(head);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
head = head.chain;
|
|
}
|
|
};
|
|
|
|
/*======================================================================
|
|
T_BeamDamage (never used, leftover dev crap!)
|
|
======================================================================
|
|
void(entity attacker, float damage) T_BeamDamage =
|
|
{
|
|
local float points;
|
|
local entity head;
|
|
|
|
// Cannot hurt the world or enemy targets (fake flying markers)
|
|
if (attacker.classtype == CT_WORLD) return;
|
|
if (attacker.classtype == CT_ENEMYTARGET) return;
|
|
|
|
head = findradius(attacker.origin, damage+40);
|
|
|
|
while (head) {
|
|
if (head.takedamage) {
|
|
points = 0.5*vlen (attacker.origin - head.origin);
|
|
if (points < 0) points = 0;
|
|
points = damage - points;
|
|
if (head == attacker) points = points * 0.5;
|
|
if (points > 0) {
|
|
if (CanDamage (head, attacker)) {
|
|
if (head.resist_rockets > 0)
|
|
points = points * head.resist_rockets;
|
|
T_Damage (head, attacker, attacker, points,DAMARMOR);
|
|
}
|
|
}
|
|
}
|
|
head = head.chain;
|
|
}
|
|
}; */
|
|
|