Files
quakemapping/mod_ad_dev/my_progs/projectiles.qc
2019-12-30 22:24:44 +01:00

1648 lines
67 KiB
Plaintext

/*======================================================================
PROJECTILE Systems
------------------
* Bullets (PROJECTILE)
* Nails
* Rockets
* Grenades
* Plasma
PLAYER Projectile Collision (movetypes)
---------------------------------------
* ID software Quake originally had most player projectiles (rockets/nails)
use a large collision size (MOVETYPE_FLYMISSILE) and this especially
helped with flying enemies (scrags) that strafe around quickly.
* AD changed all the player projectile collision boxes to a smaller
size (MOVETYPE_FLY) to add a greater challenge to combat and this
is noticeable when fighting flying monsters.
This change was linked to the setting of the sv_aim console command,
which has been changed from 0.93 to 1 in most modern quake clients.
The sv_aim command was used to help steer projectiles into monsters
to ultimately make the combat easier for keyboard only players.
* Projectiles move in discreet steps on the server and with a small
movetype they can often skip through objects. This was probably the
reason ID software had large collision and changing them to small
only makes combat more difficult and less fun.
* The projectile size is now linked to a worldspawn variables which
defaults to 0 and will keep the original ID software default while
giving map makers the choice to override this option.
There is also an impulse command (105) to toggle this setting and
its reported on the mod info table on the console (developer 1)
The new worldspawn variable is - no_bigprojectiles
This change will affect the following ammo type/weapons:
shells(SG/SSG/WSG), nails(NG/SNG), rockets(RL) and cells(PG).
* There is a big projectile collision for grenades but ID software
never used it. All grenades are setup with MOVETYPE_BOUNCE
which is always a small collision.
======================================================================*/
void() Particle_Bullet =
{
// Is the touch function blocked?
if (self.waitmin > time) return;
if (self.delay < time) entity_remove(self, 1);
else {
if (random() < 0.5) {
self.oldorigin = crandom() * '1 1 1';
if (random() < 0.8) self.lip = rint(random()*4);
else self.lip = 112 + rint(random()*4);
particle (self.origin, self.oldorigin, self.lip, 1 + rint(random()*2));
}
self.think = Particle_Bullet;
self.nextthink = time + self.speed;
}
};
//----------------------------------------------------------------------
void() Touch_Bullet =
{
if (self.touchedvoid) return; // Marked for removal
if (check_skycontent(self.origin)) {entity_remove(self, 0.1); return;}
if (other == self.owner) return; // Touching self, do nothing
if (other.solid == SOLID_TRIGGER) return; // trigger field, do nothing
if (self.waitmin > time) return; // bullet touch has been disabled
entity_remove(self, 1); // Setup bullet for removal
//----------------------------------------------------------------------
// Hit monster/interactive object, impact blood
//----------------------------------------------------------------------
if (other.takedamage) {
// Due to how difficult projectile shotguns were to hit thing
// An extra +4 damage (dead center tracer) was added to help
// In AD 1.7+ the bullet projectiles are now using wide impact
// against monsters which means more pellets will hit.
// The extra tracer pellet damage is not required anymore.
// Check pellet type for damage
if (self.classtype == CT_PROJ_TRACEPART) self.dmg = 0;
else if (self.classtype == CT_PROJ_TRACE) self.dmg = 0;
// Old code kept for reference
// No extra trace damage for zombies, it will kill them
//if (other.classgroup == CG_ZOMBIE) self.dmg = 0;
//else self.dmg = DAMAGE_PTSHELL;
// Default shell pellet damage
else self.dmg = DAMAGE_PSHELL;
// Check for breakable/pushable no monster damage
if (ai_immunedamage(self.owner, other)) {
self.dmg = 0;
WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
WriteByte (MSG_BROADCAST, TE_GUNSHOT);
WriteCoord (MSG_BROADCAST, self.origin_x);
WriteCoord (MSG_BROADCAST, self.origin_y);
WriteCoord (MSG_BROADCAST, self.origin_z);
}
// Show bullet resistance as small blood+gunshot+smoke
else if (other.resist_shells > 0) {
self.dmg = Resist_Damage(other, IT_SHELLS, self.dmg);
Resist_Shells(other, self.origin, self.velocity, self.dmg);
}
else {
// Hitting monsters does twice the amount of blood effects
if (other.flags & FL_MONSTER) spawn_touchblood (self, other, self.dmg*2);
else spawn_touchblood (self, other, self.dmg);
}
// Finally do the damage, if any available (after resistance)
if (self.dmg > 0) T_Damage (other, self, self.owner, self.dmg, DAMARMOR);
}
//----------------------------------------------------------------------
// Hit world/static object, impact particles
//----------------------------------------------------------------------
else {
// Tracer bullet, dead center and inflight particle emitter
// Originally did no damage, but changed to buff particle SG
if (self.classtype == CT_PROJ_TRACE) {
self.lip = random();
// NG tink sound or SG ricochet sound (hardcoded client sounds)
if (self.lip < 0.5) sound(self, CHAN_VOICE, "weapons/tink1.wav", random()*0.5, ATTN_LOW);
else if (self.lip < 0.7) sound(self, CHAN_VOICE, "weapons/ric2.wav", random()*0.5, ATTN_LOW);
else sound(self, CHAN_VOICE, "weapons/ric3.wav", random()*0.5, ATTN_LOW);
}
else {
WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
WriteByte (MSG_BROADCAST, TE_GUNSHOT);
WriteCoord (MSG_BROADCAST, self.origin_x);
WriteCoord (MSG_BROADCAST, self.origin_y);
WriteCoord (MSG_BROADCAST, self.origin_z);
}
}
};
//----------------------------------------------------------------------
void(vector org, vector dir, float proj_type, float proj_speed) Launch_Bullet =
{
newmis = spawn();
newmis.owner = self;
newmis.classname = "proj_bullet"; // obj name, not really used anymore
newmis.classtype = proj_type; // Class type number, quick identity
newmis.classgroup = CG_PROJSHELLS; // Ammo type
//----------------------------------------------------------------------
// Horrible hack! If the player has the TSG or quad, flag it for zombies
//----------------------------------------------------------------------
if (self.moditems & IT_UPGRADE_SSG || self.super_damage_finished > 0)
newmis.weapon = TRUE;
//----------------------------------------------------------------------
// Setup player/monster projectile collision
//----------------------------------------------------------------------
if (self.classtype == CT_PLAYER) {
if (playerprojsize == 0)
newmis.movetype = MOVETYPE_FLYMISSILE; // Large collision
else newmis.movetype = MOVETYPE_FLY; // Small collision
}
else newmis.movetype = MOVETYPE_FLYMISSILE; // Default = large collision
//----------------------------------------------------------------------
newmis.solid = SOLID_BBOX;
newmis.touch = Touch_Bullet; // touch function
newmis.waitmin = 0; // Touch function active
// Particle tracer (true aim, no damage)
if (proj_type == CT_PROJ_TRACE || proj_type == CT_PROJ_TRACEPART) {
newmis.delay = time + LIFE_SHELLS; // Maximum life time
if (self.weapon & IT_SUPER_SHOTGUN) newmis.count = 1;
else newmis.count = 1 + rint(random()*2); // Default particles for SG
newmis.speed = 0.02; // Next function interval (very high tick)
newmis.think = Particle_Bullet; // Particle trail
newmis.nextthink = time + newmis.speed;
}
// SG/SSG shells (scatter effect)
else {
newmis.think = SUB_Remove;
newmis.nextthink = time + LIFE_SHELLS; // Stop projectile going forever
}
newmis.mdl = MODEL_PROJ_DIAM2;
setmodel(newmis, newmis.mdl); // Diamond model
newmis.frame = random()*15; // Full range of sizes
newmis.skin = 16 + random()*7; // Bright colours
newmis.velocity = dir * proj_speed; // Constant speed multiplier
newmis.angles = vectoangles(dir); // Create direction angle
setsize (newmis, VEC_ORIGIN, VEC_ORIGIN); // Zero size
setorigin (newmis, org); // Move to starting position
newmis.avelocity = vecrand(100,200,FALSE);
};
//----------------------------------------------------------------------
void(float bullet_count, vector bullet_spread, float bullet_type) Launch_Shells =
{
local vector src_origin, bullet_dir, spread_dir;
local float bullet_speed, var_speed;
// Is the player firing the shotgun?
if (self.flags & FL_CLIENT) {
makevectors(self.v_angle);
// Infront of player model and down towards gun
src_origin = self.origin + attack_vector('10 0 8');
if (bullet_type == CT_PROJ_SG) bullet_speed = SPEED_PLAYERSG;
else bullet_speed = SPEED_PLAYERSSG;
// Either straight line or auto aim assist (builtin function 44)
if (autoaim_cvar < 1) bullet_dir = aim(self, SPEED_PLAYAIM);
else bullet_dir = normalize(v_forward * bullet_speed);
}
else {
makevectors(self.angles);
// At the end of the new soldier gun model
src_origin = self.origin + attack_vector(self.attack_offset);
bullet_speed = SPEED_MONSG + (skill*SPEED_MONSGMULT);
// fire somewhat behind the player, so a dodging player is harder to hit
bullet_dir = self.enemy.origin - self.enemy.velocity*0.2;
bullet_dir = normalize (bullet_dir - self.origin);
}
// Setup particle emitter/tracer shot (true aim)
Launch_Bullet(src_origin, bullet_dir, CT_PROJ_TRACE, bullet_speed);
// Reduced the amount of visual noise infront of the player
// The projectiles below are particle trail emitters only
// Launch_Bullet(src_origin+(v_right*(crandom()*10)), bullet_dir, CT_PROJ_TRACEPART, bullet_speed + (crandom()*30) );
// Launch_Bullet(src_origin+(v_right*(crandom()*10)), bullet_dir, CT_PROJ_TRACEPART, bullet_speed + (crandom()*30) );
while (bullet_count > 0) {
var_speed = crandom()*10 + bullet_speed; // Slight speed variance
spread_dir = bullet_dir + (crandom()*bullet_spread_x) * v_right + (crandom()*bullet_spread_y) * v_up;
Launch_Bullet(src_origin, spread_dir, bullet_type, var_speed);
bullet_count = bullet_count - 1;
}
};
/*======================================================================
PLASMA PROJECTILES
* requires special blue/white particle trail
* Has radius and impact touch damage
* used by Eliminator (enforcer), Soldier and Minotaur
======================================================================*/
void() Particle_Plasma =
{
// projectile has touched something
if (self.waitmin > time) return;
// Generate sprite particles? DP not active?
if (random() < 0.5 && !ext_dppart) {
self.oldorigin = crandom() * '1 1 1';
if (self.poisonous) self.lip = 56 + rint(random()*8);
else self.lip = 40 + rint(random()*8);
particle (self.origin, self.oldorigin, self.lip, 4 + rint(random()*4));
if (self.poisonous) self.lip = 182 + rint(random()*8);
else self.lip = 198 + rint(random()*4);
particle (self.origin, self.oldorigin, self.lip, 2 + rint(random()*2));
}
// The Player plasma projectile grows over frames
if (self.classproj == CT_PLAYER) {
if (self.attack_finished < time) {
self.frame = self.frame + 1;
if (self.frame < 6) self.attack_finished = time + 0.1;
else self.frame = 6;
}
}
self.think = Particle_Plasma;
self.nextthink = time + self.speed;
};
//----------------------------------------------------------------------
// Compiler forward reference
void(vector org, vector dir, float proj_type, float proj_speed) launch_plasma;
//----------------------------------------------------------------------
void() Touch_PlasmaProjectile =
{
local vector org, vec, dir;
local entity tself;
if (self.touchedvoid) return; // Marked for removal
if (check_skycontent(self.origin)) {entity_remove(self, 0.1); return;}
if (other == self.owner) return; // Touching self, do nothing
if (other.solid == SOLID_TRIGGER) return; // trigger field, do nothing
if (self.waitmin > time) return; // plasma touch has been disabled
if (self.classproj == CT_PLAYER) self.pos1 = DAMAGE_PGPLAYER;
else if (self.classproj == CT_REFLECTLIGHT) self.pos1 = DAMAGE_PGREFLECT;
else if (self.classproj == CT_REFLECTPLASMA) self.pos1 = DAMAGE_PGREFLECT;
else if (self.classproj == CT_MONMINOTAUR) self.pos1 = DAMAGE_PGMINOTAUR;
else if (self.classproj == CT_MONGAUNT) self.pos1 = DAMAGE_PGGAUNT;
else if (self.classproj == CT_MONNOUR) self.pos1 = DAMAGE_PGNOUR;
else self.pos1 = DAMAGE_PLASMA;
// Check for breakable/pushable immunity first
if (ai_immunedamage(self.owner, other)) self.pos1 = '0 0 0';
self.dmg = self.pos1_x + random()*self.pos1_y;
// Plasma Splashdamage affects everything, check for 0 dmg first
// This will also apply a poisonous debuff from the attacker
if (self.pos1_z > 0)
T_RadiusDamage (self, self.owner, self.pos1_z, world, DAMAGEALL);
//----------------------------------------------------------------------
// Hit monster/interactive object, impact blood
//----------------------------------------------------------------------
if (other.takedamage) {
// Check for breakable/pushable no monster damage
if (ai_immunedamage(self.owner, other)) {
// Using a cut down version of ammo resistance
SpawnExplosion(EXPLODE_BURST_SMOKE, self.origin, SOUND_PLASMA_HIT);
SpawnProjectileSmoke(self.origin, 200, 50, 150);
if (random() < 0.5) SpawnProjectileSmoke(self.origin, 200, 50, 250);
if (random() < 0.5) SpawnProjectileSmoke(self.origin, 300, 50, 150);
}
// Check for plasma/cell resistance
else if (other.resist_cells > 0) {
self.dmg = Resist_Damage(other, IT_CELLS, self.dmg);
Resist_Plasma(other, self.origin);
}
else {
// Hitting monsters does twice the amount of blood effects
if (other.flags & FL_MONSTER) spawn_touchblood (self, other, self.dmg*2);
else spawn_touchblood (self, other, self.dmg);
// Switch poisonous to poison sprite, but keep plasma explosion
// Otherwise this sounds confusing with a rocket explosion
if (self.poisonous) self.lip = EXPLODE_POISON_SMALL;
else {
// Randomly pick between quoth electric and blue explosions
if (random() < 0.3) self.lip = EXPLODE_PLASMA_SMALL;
else self.lip = EXPLODE_ELECT_SMALL;
}
SpawnExplosion(self.lip, self.origin, SOUND_PLASMA_HIT);
}
// Plasma hits always kill any zombie (1 hit)
if (other.classgroup == CG_ZOMBIE) self.dmg = DAMAGE_ZOMBIECLASS;
// Don't bother with damage if there is none!
if (self.dmg > 0) T_Damage (other, self, self.owner, self.dmg, DAMARMOR);
}
//----------------------------------------------------------------------
// Hit world/static object, impact particles
//----------------------------------------------------------------------
else {
// New special coloured particle explosion (rogue expansion)
WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
WriteByte (MSG_BROADCAST, TE_EXPLOSION2);
WriteCoord (MSG_BROADCAST, self.origin_x);
WriteCoord (MSG_BROADCAST, self.origin_y);
WriteCoord (MSG_BROADCAST, self.origin_z);
if (self.poisonous) WriteByte (MSG_BROADCAST, 51);
else WriteByte (MSG_BROADCAST, 35);
WriteByte (MSG_BROADCAST, 8);
// Poisonous sprite explosion instead
if (self.poisonous) self.lip = EXPLODE_POISON_SMALL;
// Classic quoth electric impact explosion
else self.lip = EXPLODE_ELECT_SMALL;
SpawnExplosion(self.lip, self.origin, SOUND_PLASMA_HIT);
}
// Check for any plasma reflection? (player only function)
if (self.classproj == CT_PLAYER && other.reflectplasma) {
org = self.origin;
// Random chance that plasma will reflect straight back
if (random() < 0.2 && self.owner)
dir = normalize(self.owner.origin - org);
else {
// Pick random location instead
vec = org + vecrand(0,1000,1);
dir = normalize(vec - org);
}
// Switch around self to make sure reflection happens once
tself = self; self = other;
launch_plasma(org, dir, CT_REFLECTPLASMA, SPEED_REFLECTION);
self = tself;
}
// Remove plasma bolt
entity_remove(self, 1);
};
//----------------------------------------------------------------------
void(vector org, vector dir, float proj_type, float proj_speed) launch_plasma =
{
newmis = spawn ();
newmis.owner = self;
newmis.classname = "proj_plasma"; // obj name, not really used anymore
newmis.classtype = CT_PROJ_PLASMA; // Class type number, quick identity
newmis.classgroup = CG_PROJCELLS; // Ammo type
newmis.classproj = proj_type; // Monster type number
newmis.attack_speed = proj_speed; // Save for later
// The player plasma projectile use to be linked to autoaim condition
// Switched to larger collision, so that greater chance to hit enemies
// Monsters always use large collision (otherwise infighting would be less)
//----------------------------------------------------------------------
if (proj_type == CT_PLAYER) {
if (playerprojsize == 0)
newmis.movetype = MOVETYPE_FLYMISSILE; // Large collision
else newmis.movetype = MOVETYPE_FLY; // Small collision
}
else {
// Monster get the raw deal, small projectile collision
newmis.movetype = MOVETYPE_FLY;
// Minotaur, Gaunt is rapid fire, don't need glow effect (slow down)
if (proj_type != CT_MONGAUNT && proj_type != CT_MONMINOTAUR)
newmis.effects = EF_DIMLIGHT;
}
//----------------------------------------------------------------------
newmis.solid = SOLID_BBOX;
newmis.touch = Touch_PlasmaProjectile;
newmis.nextthink = time + LIFE_PROJECTILE;
newmis.think = SUB_Remove;
newmis.poisonous = newmis.owner.poisonous;
// Setup model and special parameters (zombie, boss)
//----------------------------------------------------------------------
if (proj_type == CT_MONARMYPLASMA) {
if (newmis.poisonous) setmodel (newmis, MODEL_PROJ_PLASMAGRN);
else setmodel (newmis, MODEL_PROJ_PLASMA);
}
else if (proj_type == CT_MONELIMATOR) {
if (newmis.poisonous) setmodel (newmis, MODEL_PROJ_PLASMAGRN);
else setmodel (newmis, MODEL_PROJ_PLASMA);
}
else if (proj_type == CT_MONCENTURION) {
if (newmis.poisonous) setmodel (newmis, MODEL_PROJ_PLASMAGRN);
else setmodel (newmis, MODEL_PROJ_PLASMA);
}
else if (proj_type == CT_MONGAUNT) {
if (newmis.poisonous) setmodel (newmis, MODEL_PROJ_GAPLASMAGRN);
else setmodel (newmis, MODEL_PROJ_GAPLASMA);
}
else if (proj_type == CT_MONEEL) {
if (newmis.poisonous) setmodel (newmis, MODEL_PROJ_PLASMAGRN);
else setmodel (newmis, MODEL_PROJ_PLASMA);
}
else if (proj_type == CT_MONSEEKER) setmodel (newmis, MODEL_PROJ_PLASMA);
else if (proj_type == CT_MONMINOTAUR) setmodel (newmis, MODEL_PROJ_MPLASMA);
else if (proj_type == CT_MONNOUR) setmodel (newmis, MODEL_PROJ_NOUR3);
else if (proj_type == CT_REFLECTLIGHT) setmodel (newmis, MODEL_PROJ_LIGHTNING2);
else if (proj_type == CT_REFLECTPLASMA) {
setmodel (newmis, MODEL_PROJ_PGPLASMA);
newmis.avelocity = vecrand(0,300,TRUE); // randomize avel
newmis.angles_y = rint(random()*360); // Random angle
newmis.frame = 3; // Medium size
}
else {
setmodel (newmis, MODEL_PROJ_PGPLASMA);
newmis.attack_finished = time + 0.1; // Update model frame every 0.1s
newmis.avelocity = vecrand(0,300,TRUE); // randomize avel
newmis.angles_y = rint(random()*360); // Random angle
newmis.frame = 0; // Start really small
}
//----------------------------------------------------------------------
// Is DP engine active for new particle trail?
if (ext_dppart) {
if (newmis.poisonous) newmis.traileffectnum = particleeffectnum(DPP_TRPOISON);
else newmis.traileffectnum = particleeffectnum(DPP_TRPLASMA);
newmis.effects = 0;
// Extra glow and light colour (DP mostly)
//newmis.effects = EF_BLUE;
//newmis.glow_color = 246; // Blue/gold
}
// Manually generate special particle trail (blue or green)
newmis.count = 1 + rint(random()*2); // Default particles
newmis.speed = 0.02; // Next function interval (very high tick)
newmis.think = Particle_Plasma; // Particle trail
newmis.nextthink = time + newmis.speed;
// Standard projectile setup (origin, size and velocity)
//----------------------------------------------------------------------
newmis.velocity = dir * newmis.attack_speed;
if (proj_type != CT_PLAYER) newmis.angles = vectoangles(dir);
setsize (newmis, VEC_ORIGIN, VEC_ORIGIN);
setorigin (newmis, org);
};
/*======================================================================
SMALL PROJECTILES - used all over the place ...
* player (nailgun, super nailgun)
* Wizards (double spit)
* Hell Knights (magic spray)
* spike shooters (regular, super, laser)
* Enforcers (laser)
* Crossbow Knights (bolts)
* Zombies (flesh)
======================================================================*/
// Forward compiler reference because of peircing nails function
void(vector org, vector dir, float proj_type, float proj_speed) launch_projectile;
//----------------------------------------------------------------------
void() Touch_Projectile =
{
local vector org, dir, vec;
local float proj_type, proj_speed;
local entity tself;
if (self.touchedvoid) return; // Marked for removal
if (check_skycontent(self.origin)) {entity_remove(self, 0.1); return;}
if (other == self.owner) return; // no touching self
if (other.solid == SOLID_TRIGGER) return; // trigger field, do nothing
// No more touch/world interaction
self.touch = SUB_Null;
self.movetype = MOVETYPE_NONE;
self.solid = SOLID_NOT;
//----------------------------------------------------------------------
// Laser impact/stop sound (precached by owner)
if (self.classtype == CT_PROJ_LASER)
sound (self, CHAN_WEAPON, "enforcer/enfstop.wav", 1, ATTN_STATIC);
// Golem rock impact sound (precached by owner)
else if (self.classtype == CT_PROJ_GROCK) {
self.lip = random();
self.volume = 0.5 + random()*0.5;
if (self.lip < 0.25) sound (self, CHAN_WEAPON, SOUND_IMP_ROCK1, self.volume, ATTN_BREAK);
else if (self.lip < 0.5) sound (self, CHAN_WEAPON, SOUND_IMP_ROCK2, self.volume, ATTN_BREAK);
else if (self.lip < 0.75) sound (self, CHAN_WEAPON, SOUND_IMP_ROCK3, self.volume, ATTN_BREAK);
else sound (self, CHAN_WEAPON, SOUND_IMP_ROCK4, self.volume, ATTN_BREAK);
}
//----------------------------------------------------------------------
// hit something that bleeds
if (other.takedamage) {
// SNG spikes are essentially double damage (cheap way of 2 x nails)
if (self.classtype == CT_PROJ_SNG) self.dmg = DAMAGE_SNGSPIKE;
// Reflected nails (can only happen from the player)
else if (self.classtype == CT_PROJ_REFNG) self.dmg = DAMAGE_NGREFSPIKE;
// Bob and enforcer lasers are different
else if (self.owner.classtype == CT_MONJIM)
self.dmg = DAMAGE_BOBLASER + (random() * DAMAGE_BOBLASER);
else if (self.classtype == CT_PROJ_LASER) self.dmg = DAMAGE_LASER;
// Special damage and sound effects for crossbow knights
else if (self.classtype == CT_PROJ_BOLT1) {
// hit4 = flesh wound, hit1 = ting sound
if (random() < 0.3) sound (self, CHAN_WEAPON, "weapons/bolt_hit1.wav", 1, ATTN_NORM);
else sound (self, CHAN_WEAPON, "weapons/bolt_hit4.wav", 1, ATTN_NORM);
// Reduce the damage if the bolt has hit another dcrossbow knight
if (other.classtype == self.owner.classtype) self.dmg = DAMAGE_BOLT0;
else self.dmg = DAMAGE_BOLT1;
}
else if (self.classtype == CT_PROJ_BLBOLT || self.classtype == CT_PROJ_BLBOLT2)
self.dmg = DAMAGE_BOGLORD;
// Zombies have special impact sound
else if (self.classtype == CT_PROJ_FLESH) {
sound (self, CHAN_WEAPON, "zombie/z_hit.wav", 1, ATTN_NORM);
self.dmg = DAMAGE_FLESH;
}
// Scorpion spikes cannot hurt other spider types
// This is to prevent the minion scorpion killing other minions
else if (self.classtype == CT_PROJ_SCORP) {
if (other.classgroup == self.owner.classgroup) self.dmg = 0;
else self.dmg = DAMAGE_NGSPIKE;
}
// Rock projectiles cannot hurt stone monsters!
else if (self.classtype == CT_PROJ_GROCK) {
if (other.classgroup == self.owner.classgroup) self.dmg = 0;
else self.dmg = DAMAGE_NGSPIKE;
}
// Default spike damage (nails)
else self.dmg = DAMAGE_NGSPIKE;
//----------------------------------------------------------------------
// Check for breakable/pushable monster immunity
if (ai_immunedamage(self.owner, other)) {
// Zero damage and make sure no resistance checks
self.dmg = self.classgroup = 0;
// Remove poisonous effect
self.poisonous = FALSE;
// Show ammo resistance effect
WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
WriteByte (MSG_BROADCAST, TE_GUNSHOT);
WriteCoord (MSG_BROADCAST, self.origin_x);
WriteCoord (MSG_BROADCAST, self.origin_y);
WriteCoord (MSG_BROADCAST, self.origin_z);
if (random() < 0.5) SpawnProjectileSmoke(self.origin, 200, 50, 150);
}
//----------------------------------------------------------------------
// Check for poison debuff (using poisonous flag)
if (self.poisonous == TRUE) PoisonDeBuff(other);
//----------------------------------------------------------------------
// Check for NG/SNG nail resistance
if (self.classgroup == CG_PROJNAILS && other.resist_nails > 0) {
self.dmg = Resist_Damage(other, IT_NAILS, self.dmg);
Resist_Nails(other, self.origin);
self.projeffect = 0;
// Check for any nail reflection?
if (other.reflectnails && random() < other.resist_nails) {
org = self.origin;
// Random chance that nail will reflect straight back
if (random() < 0.2 && self.owner)
dir = normalize(self.owner.origin - org);
else {
// Pick random location instead
// Flatten the Z axis so the reflection looks less random
vec = vecrand(0,100,1);
vec_z = random()*25;
vec = vec + org;
dir = normalize(vec - org);
}
// Switch around self to make sure reflection happens once
tself = self; self = other;
launch_projectile(org, dir, CT_PROJ_REFNG, SPEED_REFLECTION);
self = tself;
}
}
//----------------------------------------------------------------------
// Produce blood particles at impact and apply damage to target
if (self.dmg > 0) {
spawn_touchblood (self, other, self.dmg);
T_Damage (other, self, self.owner, self.dmg, DAMARMOR);
}
//----------------------------------------------------------------------
// Nail Piercing affect, move nail through monster
if (self.projeffect & IT_ARTPIERCE) {
// a small pile of gibs!
if (random() < 0.2) SpawnMeatSpray (self, other, crandom() * 100);
// Setup projectile ready for monster tests
self.movetype = MOVETYPE_FLY;
self.solid = SOLID_BBOX;
self.oldorigin = self.origin;
self.count = 0;
// Loop forward 8 times to find space on the other side
while (self.count < 8) {
// Trace forward from current position
self.finaldest = self.oldorigin + self.finalangle*512;
traceline(self.oldorigin, self.finaldest,FALSE,self);
// Still inside bleeding object?
if (trace_ent == other)
self.oldorigin = self.oldorigin + self.finalangle*16;
// On the other side!
else self.count = 8;
self.count = self.count + 1;
}
// Is the other side free space to spawn?
if (pointcontents(self.oldorigin) == CONTENT_EMPTY) {
tself = self;
org = self.oldorigin;
dir = self.finalangle;
proj_type = self.classtype;
proj_speed = self.attack_speed;
self = self.owner;
// Once a projectile hits an object it is impossible
// to reset its velocity/angles and carry on
// It is easier to create a new projectile and delete
// the currently damaged one instead!
launch_projectile(org, dir, proj_type, proj_speed);
self = tself;
}
}
}
//----------------------------------------------------------------------
else {
// Some projectiles need to be left lying around for a while
if (self.bodyfadeaway) {
if (self.classtype == CT_PROJ_FLESH)
sound (self, CHAN_WEAPON, "zombie/z_miss.wav", 1, ATTN_NORM);
else if (self.classtype == CT_PROJ_SPID)
sound (self, CHAN_WEAPON, "spider/miss.wav", 1, ATTN_NORM);
else if (self.classtype == CT_PROJ_SWAMP)
sound (self, CHAN_WEAPON, "swampling/miss.wav", 1, ATTN_NORM);
else if (self.classtype == CT_PROJ_VORE)
sound (self, CHAN_WEAPON, "voreling/miss.wav", 1, ATTN_NORM);
else if (self.classtype == CT_PROJ_ELF)
sound (self, CHAN_WEAPON, "xmas/elf/miss.wav", 1, ATTN_NORM);
else if (self.classtype == CT_PROJ_BOLT1) {
if (random() < 0.5) sound (self, CHAN_WEAPON, "weapons/bolt_hit2.wav", 1, ATTN_NORM);
else sound (self, CHAN_WEAPON, "weapons/bolt_hit3.wav", 1, ATTN_NORM);
}
self.velocity = self.avelocity = '0 0 0';
// Straight away make projectiles vanish
self.nextthink = time + 2 + random();
self.ltime = self.nextthink;
self.think = model_fade;
return;
}
//----------------------------------------------------------------------
// Standard engine impact particles and sounds
else {
WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
if (self.classtype == CT_PROJ_SNG) WriteByte (MSG_BROADCAST, TE_SUPERSPIKE);
else if (self.classtype == CT_PROJ_LASER) WriteByte (MSG_BROADCAST, TE_KNIGHTSPIKE);
else if (self.classtype == CT_PROJ_GROCK) WriteByte (MSG_BROADCAST, TE_GUNSHOT);
else if (self.classtype == CT_PROJ_WIZ) WriteByte (MSG_BROADCAST, TE_WIZSPIKE);
else if (self.classtype == CT_PROJ_SPID) WriteByte (MSG_BROADCAST, TE_WIZSPIKE);
else if (self.classtype == CT_PROJ_SWAMP) WriteByte (MSG_BROADCAST, TE_WIZSPIKE);
else if (self.classtype == CT_PROJ_VORE) WriteByte (MSG_BROADCAST, TE_SUPERSPIKE);
else if (self.classtype == CT_PROJ_ELF) WriteByte (MSG_BROADCAST, TE_GUNSHOT);
else if (self.classtype == CT_PROJ_FURY2) WriteByte (MSG_BROADCAST, TE_WIZSPIKE);
else if (self.classtype == CT_PROJ_NOUR1) WriteByte (MSG_BROADCAST, TE_WIZSPIKE);
else if (self.classtype == CT_PROJ_BLBOLT) WriteByte (MSG_BROADCAST, TE_WIZSPIKE);
else if (self.classtype == CT_PROJ_BLBOLT2) WriteByte (MSG_BROADCAST, TE_KNIGHTSPIKE);
else if (self.classtype == CT_PROJ_HKN) WriteByte (MSG_BROADCAST, TE_KNIGHTSPIKE);
else WriteByte (MSG_BROADCAST, TE_SPIKE);
WriteCoord (MSG_BROADCAST, self.origin_x);
WriteCoord (MSG_BROADCAST, self.origin_y);
WriteCoord (MSG_BROADCAST, self.origin_z);
}
}
// Hide projectile and wait for any sounds to finish playing
entity_remove(self,2);
};
//----------------------------------------------------------------------
void(vector org, vector dir, float proj_type, float proj_speed) launch_projectile =
{
newmis = spawn ();
newmis.owner = self;
newmis.classname = "proj_nail"; // obj name, not really used anymore
newmis.classtype = proj_type; // Class type number, quick identity
if (newmis.classtype == CT_PROJ_NG || newmis.classtype == CT_PROJ_SNG)
newmis.classgroup = CG_PROJNAILS; // Ammo type
else newmis.classgroup = CG_PROJALL; // Ammo type (default)
// Switch model collision based on auto aim functionality
// Monsters always use large collision (otherwise infighting would be less)
//----------------------------------------------------------------------
if (self.classtype == CT_PLAYER) {
if (playerprojsize == 0)
newmis.movetype = MOVETYPE_FLYMISSILE; // Large collision
else newmis.movetype = MOVETYPE_FLY; // Small collision
// Check if the player has the nail piercer effect active
if (self.moditems & IT_ARTPIERCE && newmis.classgroup == CG_PROJNAILS) {
newmis.projeffect = IT_ARTPIERCE;
}
}
// lasers/plasma are small and glowing
else if (proj_type == CT_PROJ_LASER) {
newmis.movetype = MOVETYPE_FLY;
newmis.effects = EF_DIMLIGHT;
}
// Crossbow bolts are small and stick around
else if (proj_type == CT_PROJ_BOLT1) {
newmis.movetype = MOVETYPE_FLY;
newmis.bodyfadeaway = TRUE;
}
// Default = large collision
else newmis.movetype = MOVETYPE_FLYMISSILE;
//----------------------------------------------------------------------
newmis.solid = SOLID_BBOX;
newmis.touch = Touch_Projectile;
newmis.nextthink = time + LIFE_PROJECTILE;
newmis.think = SUB_Remove;
newmis.poisonous = newmis.owner.poisonous;
// Setup model
//----------------------------------------------------------------------
if (proj_type == CT_PROJ_NG) setmodel (newmis, MODEL_PROJ_NG);
else if (proj_type == CT_PROJ_SNG) setmodel (newmis, MODEL_PROJ_SNG);
else if (proj_type == CT_PROJ_REFNG) setmodel (newmis, MODEL_PROJ_NGRED);
// Santa Snowball machine gun!
else if (proj_type == CT_PROJ_SANTA) setmodel (newmis, MODEL_PROJ_SNOWBALL);
// Crossbow Knight has poison/regular bolts
else if (proj_type == CT_PROJ_BOLT1) {
if (newmis.poisonous) setmodel (newmis, MODEL_PROJ_BOLT2);
else setmodel (newmis, MODEL_PROJ_BOLT1);
}
// Hell Knight + Death Knight magic attack (can be poisonous)
else if (proj_type == CT_PROJ_HKN) {
if (newmis.poisonous) setmodel (newmis, MODEL_PROJ_HKNGRN);
else setmodel (newmis, MODEL_PROJ_HKN);
}
// Death Lord - like Death Knight magic attack (spike ball instead)
else if (proj_type == CT_PROJ_DLORD) {
setmodel (newmis, MODEL_PROJ_DLORD1);
// If can see enemy, steer the spike ball
if (visible(self.enemy)) {
newmis.enemy = self.enemy;
newmis.nextthink = time + 0.1;
newmis.think = self.th_updmissile;
}
}
// Enforcer (can be poisonous)
else if (proj_type == CT_PROJ_LASER) {
if (newmis.poisonous) setmodel (newmis, MODEL_PROJ_LASERGRN);
else setmodel (newmis, MODEL_PROJ_LASER);
// If DP/FTE engine use new particle trail, glowing for Fitz engines
if (ext_dppart) {
if (newmis.poisonous) newmis.traileffectnum = particleeffectnum(DPP_TRPOISON);
else newmis.traileffectnum = particleeffectnum(DPP_TRLASER);
newmis.effects = 0; // Remove any glows
}
}
// Wraith fire bones (nails) can be poisonous as well
else if (proj_type == CT_PROJ_WBONE) {
if (newmis.poisonous) setmodel (newmis, MODEL_PROJ_WPOISON);
else setmodel (newmis, MODEL_PROJ_WBONE);
}
// Wizards/Scrags can be poisonous and have a diff skin (head+gibs)
// Cannot show change with projectile because its already green!
else if (proj_type == CT_PROJ_WIZ) setmodel (newmis, MODEL_PROJ_WIZ);
// mpoison = Darker Minotaur, always poisonous
else if (proj_type == CT_PROJ_MPOISON) setmodel (newmis, MODEL_PROJ_MPOISON);
// monng = Red Coloured Nails
else if (proj_type == CT_PROJ_MONNG) setmodel (newmis, MODEL_PROJ_NGRED);
else if (proj_type == CT_PROJ_SCORP) setmodel (newmis, MODEL_PROJ_SCORP);
else if (proj_type == CT_PROJ_FURY2) setmodel (newmis, MODEL_PROJ_FURY2);
else if (proj_type == CT_PROJ_NOUR1) setmodel (newmis, MODEL_PROJ_NOUR1);
// Boglord / Fire Shambler nail/lightning attack
else if (proj_type == CT_PROJ_BLBOLT) {
if (self.spawnflags & MON_BOGL_STRONG) {
newmis.classtype = CT_PROJ_BLBOLT2;
setmodel (newmis, MODEL_PROJ_BLORDBOLT2);
}
else setmodel (newmis, MODEL_PROJ_BLORDBOLT1);
}
// Golem rock storm attack
else if (proj_type == CT_PROJ_GROCK) {
if (random() < 0.5) setmodel (newmis, MODEL_PROJ_GROCK1);
else setmodel (newmis, MODEL_PROJ_GROCK2);
newmis.frame = rint(random()*9);
newmis.avelocity = vecrand(0,200,TRUE);
}
// Standard projectile setup (origin, size and velocity)
//----------------------------------------------------------------------
newmis.attack_speed = proj_speed;
newmis.finalangle = dir;
newmis.velocity = newmis.finalangle * newmis.attack_speed;
newmis.angles = vectoangles(newmis.finalangle);
setsize (newmis, VEC_ORIGIN, VEC_ORIGIN);
setorigin (newmis, org);
};
/*======================================================================
This missile touch function is designed for monsters NOT players
* No DIRECT damage, just radius and a lot lower (40 vs 110)
* Half damage to shamblers (inside T_RadiusDamage)
* Will work with homing or direct missile attacks
* checks for breakables that can be destroyed with explosives
* uses default explosion function
======================================================================*/
void() Touch_HomingMissile =
{
if (self.touchedvoid) return; // Marked for removal
if (check_skycontent(self.origin)) {entity_remove(self, 0.1); return;}
if (other.solid == SOLID_TRIGGER) return; // trigger field, do nothing
//----------------------------------------------------------------------
// Check for breakable/pushable monster immunity
if (ai_immunedamage(self.oldenemy, other)) {
// Show ammo resistance on bmodel
SpawnExplosion(EXPLODE_BURST_SMOKE,self.origin,SOUND_RESIST_ROCKET);
SpawnProjectileSmoke(self.origin, 200, 50, 150);
SpawnProjectileSmoke(self.origin, 200, 50, 250);
SpawnProjectileSmoke(self.origin, 300, 50, 150);
}
else {
//----------------------------------------------------------------------
// Check for breakables that can be triggered
if (ai_foundbreakable(self.oldenemy, other, TRUE) && other.brktrigmissile != 0) {
// Found a breakable which is prone to explosive damage
trigger_ent(other, self.oldenemy);
}
else {
// Homing Missiles always kill any zombie class
if (other.classgroup == CG_ZOMBIE)
T_Damage (other, world, world, DAMAGE_ZOMBIECLASS, NOARMOR);
else {
// Missile explosion base/rnd/splash
self.pos1 = '0 0 0';
if (self.classtype == CT_PROJ_SHUB1) self.pos1 = self.oldenemy.pos2;
// Shalrath/DSerg are using classic voreball damage
else self.pos1_z = DAMAGE_MONROCKET;
// pre-calculate missile damage
self.dmg = self.pos1_x + (random() * self.pos1_y);
// Check for any rocket ammo resistance
if (other.resist_rockets > 0) {
self.dmg = Resist_Damage(other, IT_ROCKETS, self.dmg);
}
// Only call T_ functions if there is damage to do!
if (self.dmg > 0 && other.health > 0)
T_Damage (other, self, self.oldenemy, self.dmg, DAMARMOR);
if (self.pos1_z > 0) {
// Stop multiple enemies of the same class type (same as wraiths)
// killing themselves with their own homing missiles!
// Radius damage also applies poison debuff if present on attacker
T_RadiusDamage (self, self.oldenemy, self.pos1_z, self.oldenemy, IGNORECLASS);
}
else {
// Check for poison debuff (using poisonous flag)
if (self.poisonous == TRUE) PoisonDeBuff(other);
}
}
}
//----------------------------------------------------------------------
// Rocket resistance is shown with puffs of smoke
if (other.resist_rockets > 0) Resist_Rockets(other, self.origin);
else {
// Use special effect for voreball explosions in DP
if (ext_dppart) {
// DP effect name set before homing missile launched
if (self.poisonous == TRUE) self.dpp_name = DPP_TEPOISON;
pointparticles(particleeffectnum(self.dpp_name), self.origin, '0 0 0', 1);
// Play standard explosion sound
sound(self, CHAN_WEAPON, SOUND_REXP3, 1, ATTN_NORM);
}
else {
if (self.poisonous) {
// New special coloured particle explosion (rogue expansion)
WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
WriteByte (MSG_BROADCAST, TE_EXPLOSION2);
WriteCoord (MSG_BROADCAST, self.origin_x);
WriteCoord (MSG_BROADCAST, self.origin_y);
WriteCoord (MSG_BROADCAST, self.origin_z);
WriteByte (MSG_BROADCAST, 51);
WriteByte (MSG_BROADCAST, 8);
// Sprite explosion for Fitz engines
SpawnExplosion(EXPLODE_POISON_SMALL, self.origin, SOUND_REXP3);
}
else {
// Standard explosion
WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
WriteByte (MSG_BROADCAST, TE_EXPLOSION);
WriteCoord (MSG_BROADCAST, self.origin_x);
WriteCoord (MSG_BROADCAST, self.origin_y);
WriteCoord (MSG_BROADCAST, self.origin_z);
// Sprite explosion for Fitz engines
SpawnExplosion(EXPLODE_SMALL, self.origin, SOUND_REXP3);
}
}
}
}
// Hide+Delete homing missile, no longer needed
// Wait for any sounds to finish playing
entity_remove(self, 4);
};
//----------------------------------------------------------------------
// Re-direct any map hacks to the new function replacement
void() ShalMissileTouch = { Touch_HomingMissile(); };
//----------------------------------------------------------------------
void() Steer_HomingMissile =
{
local vector dir, vtemp;
// If tracking enemies dies or end level? remove homing missiles
if (self.enemy.health < 1) { remove(self); return; }
if (intermission_running > 1) { remove(self); return; }
vtemp = self.enemy.origin + self.v_angle;
dir = normalize(vtemp - self.origin);
self.velocity = dir * self.attack_speed;
// After 1s let homing missiles impact on ower
if (self.waitmin < time) self.owner = self;
// Slow speed update that the missile can steer around corners
// sloppy slow updates allows for a better game experience
self.nextthink = time + 0.2;
self.think = Steer_HomingMissile;
};
//----------------------------------------------------------------------
void(vector orgofs, vector targofs, float proj_type, float proj_speed) Launch_HomingMissile =
{
local vector org, dir;
local float dist, flytime;
// Check if there is space to spawn entity
makevectors(self.angles);
org = self.origin + attack_vector(orgofs);
if (entity_pcontent(org)) return;
newmis = spawn ();
// Owner is always excluded from touch functions, save in oldenemy
// and after one second remove owner so will touch everything
newmis.owner = newmis.oldenemy = self;
newmis.classname = "proj_rocket"; // obj name, not really used anymore
newmis.classtype = proj_type; // Class type number, quick identity
newmis.classgroup = CG_PROJROCKETS; // Ammo type
newmis.movetype = MOVETYPE_FLYMISSILE; // Default = large collision
newmis.enemy = self.enemy; // Used for homing target
newmis.v_angle = targofs; // Store for later
//----------------------------------------------------------------------
// Aim the missile slightly above enemy
dir = normalize((self.enemy.origin + newmis.v_angle) - org);
dist = vlen (self.enemy.origin - org);
flytime = dist * 0.002;
if (flytime < 0.1) flytime = 0.1;
//----------------------------------------------------------------------
newmis.solid = SOLID_BBOX;
newmis.touch = Touch_HomingMissile;
newmis.nextthink = flytime + time;
newmis.think = Steer_HomingMissile;
newmis.attack_speed = proj_speed;
// Allow 3s for the homing missile to travel away
newmis.waitmin = time + 3;
newmis.poisonous = newmis.owner.poisonous;
// Setup model for each missile type
//----------------------------------------------------------------------
if (proj_type == CT_PROJ_SHAL) {
if (newmis.poisonous) setmodel(newmis, MODEL_PROJ_SHALGRN);
else setmodel(newmis, MODEL_PROJ_SHAL);
newmis.dpp_name = DPP_TEVORESPIKE;
}
else if (proj_type == CT_PROJ_SERG) {
if (newmis.poisonous) setmodel(newmis, MODEL_PROJ_SHALGRN);
else setmodel(newmis, MODEL_PROJ_SERG);
newmis.dpp_name = DPP_TEEXPLODE;
}
else if (proj_type == CT_PROJ_SHUB1) {
setmodel(newmis, MODEL_PROJ_SHUB1);
newmis.dpp_name = DPP_TEEXPLODE;
// Push the homing missing up from center
makevectors(self.angles);
// The angles key will reverse direction
// for up-side-down Shub
dir = v_up*0.5;
}
// Standard projectile setup (origin, size and velocity)
//----------------------------------------------------------------------
newmis.velocity = dir * newmis.attack_speed;
newmis.avelocity = vecrand(100,200,FALSE);
newmis.angles = vectoangles(dir);
setsize (newmis, VEC_ORIGIN, VEC_ORIGIN);
setorigin (newmis, org);
};
/*======================================================================
ROCKETS
* Half damage to shamblers (T_MissileTouch and T_RadiusDamage)
* checks for breakables that can be destroyed with explosives
* All rockets have direct & radius damage (based on projectile type)
======================================================================*/
void() Touch_Missile =
{
if (self.touchedvoid) return; // Marked for removal
if (check_skycontent(self.origin)) {entity_remove(self, 0.1); return;}
if (other == self.owner) return; // Touching self, do nothing
if (other.solid == SOLID_TRIGGER) return; // trigger field, do nothing
if (self.oldenemy) { // stop packs of owners killing each other
if (other.classtype == self.oldenemy.classtype) return; }
// Guardian skull wizards have special poison explosion
if (self.classtype == CT_PROJ_SKULLW && self.th_updmissile != SUB_Null) {
self.th_updmissile(); return; }
//----------------------------------------------------------------------
// Check for breakable/pushable monster immunity
if (ai_immunedamage(self.owner, other)) {
// Show ammo resistance
SpawnExplosion(EXPLODE_BURST_SMOKE,self.origin,SOUND_RESIST_ROCKET);
SpawnProjectileSmoke(self.origin, 200, 50, 150);
if (random() < 0.5) SpawnProjectileSmoke(self.origin, 200, 50, 250);
if (random() < 0.5) SpawnProjectileSmoke(self.origin, 300, 50, 150);
}
else {
//----------------------------------------------------------------------
// Check for breakables that can be triggered
if (ai_foundbreakable(self.owner, other, TRUE) && other.brktrigmissile != 0) {
// Found a breakable which is prone to explosive damage
trigger_ent(other, self.owner);
}
else {
// Stop any updates to the missile velocity
self.nextthink = time + 1;
self.think = SUB_Null;
// Setup rocket damage (vector = base + random and splash)
if (self.classtype == CT_PROJ_ROCKET) self.pos1 = DAMAGE_RLPLAYER;
// Boss ID - E1M7
else if (self.classtype == CT_PROJ_LAVA) self.pos1 = DAMAGE_RLPLAYER;
// BOSS Custom version with custom damage (self.pos2)
else if (self.classtype == CT_PROJ_CHTHON) self.pos1 = self.owner.pos2;
// BOSS Custom for ad_mountain (Chthon model)
else if (self.classtype == CT_PROJ_FIRETOP) self.pos1 = DAMAGE_RLFIRETOP;
// BOSS Custom for ad_magma (Stone demon model)
else if (self.classtype == CT_PROJ_EIDO1) self.pos1 = DAMAGE_RLEIDO;
// BOSS Custom for ad_azad (Ice Golem model)
else if (self.classtype == CT_PROJ_ICEG1) self.pos1 = DAMAGE_RLICEG;
// BOSS Custom for ad_sepulcher (Fire Shambler)
else if (self.classtype == CT_PROJ_BLORD2) self.pos1 = DAMAGE_RLBLORD;
// BOSS Custom for ad_sepulcher (Green Shambler)
else if (self.classtype == CT_PROJ_BLORD) {
self.pos1 = DAMAGE_RLBLORD;
// Don't spawn gib models at impact (could be solid)
self.oldorigin = self.origin;
self.origin = self.origin - 8*normalize(self.velocity);
// Special gib model, frame and movement pattern
self.gib1mdl = MODEL_PROJ_BLORD1S;
self.gib1frame = 9;
// Throw gib quantity based on skill level
ThrowGib(11, rint(0.5 + random()*3) );
// Restore origin for rest of rocket impact
self.origin = self.oldorigin;
}
else if (self.classtype == CT_PROJ_ARMY) self.pos1 = DAMAGE_RLARMY;
else if (self.classtype == CT_PROJ_DGUARDQ) self.pos1 = DAMAGE_RLDGUARDQ;
else if (self.classtype == CT_PROJ_DROLE) self.pos1 = DAMAGE_RLDROLE;
else if (self.classtype == CT_PROJ_FURY1) self.pos1 = DAMAGE_RLFURY;
else if (self.classtype == CT_PROJ_GARG) self.pos1 = DAMAGE_RLGARG;
else if (self.classtype == CT_PROJ_JIM2) self.pos1 = DAMAGE_RLJIM2;
else if (self.classtype == CT_PROJ_SKULLW) self.pos1 = DAMAGE_RLSKULLW;
else if (self.classtype == CT_PROJ_SEEKER) self.pos1 = DAMAGE_RLSEEKER;
else if (self.classtype == CT_PROJ_RAINDEER) self.pos1 = DAMAGE_RLRAINDEER;
// Instant death explosion for zombies from rocket explosions!
// Is doesn't make sense that this is not a default for explosives
// This will affect _zombie, _zombiek, _mummy monster types
if (other.classgroup == CG_ZOMBIE) {
// Change direct damage direct to make sure it zombie gibs
if (self.pos1_x < DAMAGE_RLKILLZOM) self.pos1_x = DAMAGE_RLKILLZOM;
}
// pre-calculate rocket damage
self.dmg = self.pos1_x + (random() * self.pos1_y);
// Check for any rocket ammo resistance
if (other.resist_rockets > 0) {
self.dmg = Resist_Damage(other, IT_ROCKETS, self.dmg);
}
// Only call T_ functions if there is damage to do!
if (self.dmg > 0 && other.health > 0)
T_Damage (other, self, self.owner, self.dmg, DAMARMOR);
if (self.pos1_z > 0)
T_RadiusDamage (self, self.owner, self.pos1_z, other, DAMAGEALL);
}
// Rocket resistance is shown with puffs of smoke
if (other.resist_rockets > 0) Resist_Rockets(other, self.origin);
else {
//----------------------------------------------------------------------
// Check for poison debuff (using poisonous flag)
if (self.poisonous == TRUE) {
// Use new poison explosion
self.height = EXPLODE_POISON_MED;
// Poisonous projectiles
PoisonDeBuff(other);
}
// Move the explosion effect higher up from point of contact
self.origin = self.origin - 8*normalize(self.velocity);
// Default ID particle explosion for fire explosions
if (self.height < EXPLODE_PLASMA_SMALL) {
WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
WriteByte (MSG_BROADCAST, TE_EXPLOSION);
WriteCoord (MSG_BROADCAST, self.origin_x);
WriteCoord (MSG_BROADCAST, self.origin_y);
WriteCoord (MSG_BROADCAST, self.origin_z);
}
// Play original explosion sound or replacement
if (self.noise == "") self.noise = SOUND_REXP3;
SpawnExplosion(self.height, self.origin, self.noise);
}
}
// Hide + Delete rocket, no longer needed
entity_remove(self, 1);
};
//----------------------------------------------------------------------
// Re-direct any map hacks to the new function replacement
void() T_MissileTouch = { Touch_Missile(); };
//----------------------------------------------------------------------
void(vector org, vector dir, vector avel, float proj_type, float proj_speed) Launch_Missile =
{
// Check if there is space to spawn entity
if (entity_pcontent(org)) return;
newmis = spawn ();
newmis.owner = self;
newmis.classname = "proj_rocket"; // obj name, not really used anymore
newmis.classtype = proj_type; // Class type number, quick identity
newmis.classgroup = CG_PROJROCKETS; // Ammo type
// Player rockets need to take into account autoaim
if (self.classtype == CT_PLAYER) {
if (playerprojsize == 0)
newmis.movetype = MOVETYPE_FLYMISSILE; // Large collision
else newmis.movetype = MOVETYPE_FLY; // Small collision
}
else {
// default monster missile width is thin!
newmis.movetype = MOVETYPE_FLY;
// Horrible exception for the missile touch function
// Gargoyle rockets need to pass through other gargoyles
// otherwise a pack of them would kill each other!
if (self.classtype == CT_MONGARGOYLE) newmis.oldenemy = self;
}
//----------------------------------------------------------------------
newmis.solid = SOLID_BBOX;
newmis.touch = Touch_Missile;
newmis.nextthink = time + LIFE_ROCKET;
newmis.think = SUB_Remove;
newmis.height = EXPLODE_SMALL;
newmis.poisonous = newmis.owner.poisonous;
// Setup model for each missile type
//----------------------------------------------------------------------
if (proj_type == CT_PROJ_ROCKET) setmodel(newmis, MODEL_PROJ_ROCKET);
// Monsters with poisonous flag awareness
else if (proj_type == CT_PROJ_GARG) {
if (newmis.poisonous) setmodel(newmis, MODEL_PROJ_GARGOYLEGRN);
else setmodel(newmis, MODEL_PROJ_GARGOYLE);
}
else if (proj_type == CT_PROJ_FURY1) {
if (newmis.poisonous) setmodel(newmis, MODEL_PROJ_SHALGRN);
else setmodel(newmis, MODEL_PROJ_FURY1);
}
else if (proj_type == CT_PROJ_CHTHON) {
if (self.spawnflags & MON_CHTHON_GREEN) setmodel (newmis, MODEL_PROJ_SLIME);
else setmodel (newmis, MODEL_PROJ_LAVA);
}
else if (proj_type == CT_PROJ_ARMY) {
if (newmis.poisonous) setmodel(newmis, MODEL_PROJ_ROCKETGRN);
else setmodel(newmis, MODEL_PROJ_ROCKET);
newmis.noise = "soldier/rocket_hit.wav";
newmis.nextthink = time + 0.1;
newmis.think = self.th_updmissile;
// If can see enemy, steer the rocket towards them
if (visible(self.enemy)) newmis.enemy = self.enemy;
}
else if (proj_type == CT_PROJ_DGUARDQ) {
setmodel (newmis, MODEL_PROJ_DGUARDQ);
newmis.noise = "dguard/hit.wav";
}
else if (proj_type == CT_PROJ_DROLE) {
if (newmis.poisonous) setmodel (newmis, MODEL_PROJ_DROLEGRN);
else setmodel (newmis, MODEL_PROJ_DROLE);
newmis.noise = "drole/r_explode.wav";
newmis.nextthink = time + 0.025;
newmis.think = self.th_updmissile;
}
else if (proj_type == CT_PROJ_SKULLW) {
// Start with SUB_Null because its tested by impact function
newmis.th_updmissile = SUB_Null;
if (newmis.poisonous) {
setmodel (newmis, MODEL_PROJ_SWSKULLP);
// Copy over poison explosive function just incase
// Skull wizard dies before projectile hits something
newmis.th_updmissile = self.th_updmissile;
}
// Classic skullwizard flaming skull projectile!
else setmodel(newmis, MODEL_PROJ_SWSKULL);
}
// All robot and boss monsters block poisonous flag
else if (proj_type == CT_PROJ_JIM2) {
setmodel(newmis, MODEL_PROJ_ROCKET);
newmis.noise = "jim/rocket_hit.wav";
}
else if (proj_type == CT_PROJ_SEEKER) {
setmodel(newmis, MODEL_PROJ_ROCKET);
newmis.noise = "seeker/rocket_hit.wav";
}
else if (proj_type == CT_PROJ_RAINDEER) {
setmodel(newmis, MODEL_PROJ_RAIND);
newmis.noise = "xmas/raindeer/hit.wav";
}
else if (proj_type == CT_PROJ_LAVA) setmodel (newmis, MODEL_PROJ_LAVA);
else if (proj_type == CT_PROJ_FIRETOP) setmodel (newmis, MODEL_PROJ_LAVA);
else if (proj_type == CT_PROJ_EIDO1) {
setmodel(newmis, MODEL_PROJ_EIDO1);
newmis.frame = 7;
if (random() < 0.5) newmis.noise = "eidolon/rock_hit1.wav";
else newmis.noise = "eidolon/rock_hit2.wav";
if (random() < 0.5) newmis.height = EXPLODE_MED;
}
else if (proj_type == CT_PROJ_ICEG1) {
setmodel (newmis, MODEL_PROJ_GSHARD);
newmis.noise = "golem/iceshard_impact.wav";
newmis.height = EXPLODE_ICE_BIG;
newmis.nextthink = time + 0.1;
newmis.think = self.th_updmissile;
newmis.pos1 = self.enemy.origin;
newmis.attack_timer = 0;
}
else if (proj_type == CT_PROJ_BLORD) {
// If the boglord dies before the impact of the 'rocket'
// then the self.owner field will be invalid.
// Setup the 'correct' projectile type beforehand instead!
if (self.spawnflags & MON_BOGL_STRONG) {
newmis.classtype = CT_PROJ_BLORD2;
setmodel (newmis, MODEL_PROJ_BLORD2B);
}
else setmodel (newmis, MODEL_PROJ_BLORD1B);
newmis.frame = 7; // Large size
newmis.noise = "boglord/slime_explode.wav";
}
// Standard projectile setup (origin, size and velocity)
//----------------------------------------------------------------------
newmis.velocity = dir * proj_speed;
newmis.avelocity = avel;
newmis.angles = vectoangles(newmis.velocity);
setsize (newmis, VEC_ORIGIN, VEC_ORIGIN);
setorigin (newmis, org);
};
/*======================================================================
GRENADES
* Half damage to shamblers (T_MissileTouch and T_RadiusDamage)
* checks for breakables that can be destroyed with explosives
* Grenade damage is based on dmg key (default = player)
======================================================================*/
void() Explode_Grenade =
{
// Block touch functions
if (self.state == STATE_DISABLED) return;
self.touch = SUB_Null;
self.state = STATE_DISABLED;
// Customize damage based on projectile type
if (self.classtype == CT_PROJ_GLMON) self.dmg = DAMAGE_MONGRENADE;
else if (self.classtype == CT_PROJ_MEGG) self.dmg = 0;
else if (self.classtype == CT_PROJ_NOUR2) {
self.dmg = DAMAGE_MONGRENADE;
// Don't spawn gib models at impact (could be solid)
self.oldorigin = self.origin;
self.origin = self.origin - 8*normalize(self.velocity);
// Special gib model, frame and movement pattern
self.gib1mdl = MODEL_PROJ_NOUR2S;
self.gib1sound = GIB_IMPACT_ACID;
self.max_health = MON_GIBFOUNTAIN;
self.gib1frame = 9;
self.gibtype = GIBTYPE_POISON;
// Setup particles from gib on floor
self.gibpartstyle = PARTICLE_BURST_YELLOW;
// Setup damage and impact explosion
self.gib1dmg = rint( 2 + (random() * skill) );
self.gib1exp = EXPLODE_BURST_POISON;
// Throw gib quantity based on skill level
ThrowGib(11, rint( 1 + random() * (1 + skill) ));
// Restore origin for rest of grenade impact
self.origin = self.oldorigin;
self.noise = "nour/explode2.wav";
}
// Default player grenade
else if (!self.dmg) self.dmg = DAMAGE_PLAYGRENADE;
// Rocket resistance is reduced in RadiusDamage
if (self.dmg > 0) T_RadiusDamage (self, self.owner, self.dmg, world, DAMAGEALL);
// Show Rocket resistance with puffs of smoke
if (other.resist_rockets > 0) Resist_Rockets(other, self.origin);
else {
//----------------------------------------------------------------------
// Check for poison debuff (using poisonous flag)
if (self.poisonous == TRUE) {
// Use new poison explosion
self.height = EXPLODE_POISON_MED;
// Poisonous projectiles
PoisonDeBuff(other);
}
// Default ID particle explosion for fire explosions
if (self.height < EXPLODE_PLASMA_SMALL) {
WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
WriteByte (MSG_BROADCAST, TE_EXPLOSION);
WriteCoord (MSG_BROADCAST, self.origin_x);
WriteCoord (MSG_BROADCAST, self.origin_y);
WriteCoord (MSG_BROADCAST, self.origin_z);
}
// Play original explosion sound
SpawnExplosion(self.height, self.origin, SOUND_REXP3);
}
// Hide grenade, no longer needed
entity_remove(self, 1);
};
//----------------------------------------------------------------------
void() Touch_Grenade =
{
if (entity_pcontent(self.origin)) {remove(self); return;}
if (self.touchedvoid) return; // Marked for removal
if (other == self.owner) return; // Touching self, do nothing
if (other.solid == SOLID_TRIGGER) return; // trigger field, do nothing
// Let minion eggs re-bounce in opposite direction
// Don't want them to explode like grenades
if (self.classtype == CT_PROJ_MEGG && other.takedamage == DAMAGE_AIM) {
if (CheckZeroVector(self.velocity) == FALSE) {
self.angles = vectoangles(self.mangle);
self.angles_y = anglemod(self.angles_y + 180);
self.velocity = -self.mangle; // Reverse direction
self.mangle = self.velocity; // Update new direction
sound (self, CHAN_WEAPON, self.noise, 1, ATTN_NORM);
// Reset egg timer if still bouncing a lot
if ( vlen(self.velocity) > 100 ) self.nextthink = time + LIFE_EGG;
}
}
else {
//----------------------------------------------------------------------
// Hit something that bleeds? (allows for grenade impact)
if (other.takedamage == DAMAGE_AIM) {Explode_Grenade(); return;}
else {
//----------------------------------------------------------------------
// Check for breakables that can be triggered
// Any monster firing a grenade at a breakable will explode without bounce
if (ai_foundbreakable(self.owner, other, TRUE) && (self.owner.flags & FL_MONSTER)) {
// Found a breakable which is prone to explosive damage
trigger_ent(other, self.owner);
Explode_Grenade();
return;
}
// bounce sound and stop spinning
sound (self, CHAN_WEAPON, self.noise, 1, ATTN_NORM);
if (CheckZeroVector(self.velocity)) self.avelocity = '0 0 0';
}
}
};
//----------------------------------------------------------------------
// Re-direct any map hacks to the new function replacement
void() OgreGrenadeExplode = { self.classtype = CT_PROJ_GLMON; Explode_Grenade(); };
void() GrenadeExplode = { self.classtype = CT_PROJ_GLMON; Explode_Grenade(); };
void() GrenadeTouch = { self.classtype = CT_PROJ_GLMON; Touch_Grenade(); };
// Compile forward functions for Launch_Grenade
void() Touch_ShellCasing;
//----------------------------------------------------------------------
void(vector org, vector dir, vector avel, float proj_type) Launch_Grenade =
{
// Check if there is space to spawn entity
if (entity_pcontent(org)) return;
newmis = spawn ();
newmis.owner = self;
newmis.classname = "proj_grenade"; // obj name, not really used anymore
newmis.classtype = proj_type; // Class type number, quick identity
newmis.classgroup = CG_PROJROCKETS; // Ammo type
newmis.movetype = MOVETYPE_BOUNCE;
//----------------------------------------------------------------------
newmis.solid = SOLID_BBOX;
newmis.touch = Touch_Grenade;
newmis.nextthink = time + LIFE_GRENADE;
newmis.think = Explode_Grenade;
newmis.noise = "weapons/bounce.wav";
newmis.bbmins = newmis.bbmaxs = VEC_ORIGIN;
newmis.height = EXPLODE_SMALL;
newmis.poisonous = newmis.owner.poisonous;
// Setup model for each missile type
//----------------------------------------------------------------------
// proj_gl is used by the player (separate so easier to change)
if (proj_type == CT_PROJ_GL) setmodel(newmis, MODEL_PROJ_GRENADE);
// proj_glmon is used by any monster firing grenades
else if (proj_type == CT_PROJ_GLMON) {
if (newmis.poisonous) setmodel(newmis, MODEL_PROJ_GRENADEGRN);
else setmodel(newmis, MODEL_PROJ_GRENADE);
}
else if (proj_type == CT_PROJ_FLESH) {
if (newmis.poisonous) setmodel(newmis, MODEL_PROJ_FLESHP);
else setmodel (newmis, MODEL_PROJ_FLESH);
}
else if (proj_type == CT_PROJ_MEGG) {
if (self.classtype == CT_MONWRAITH) {
setmodel( newmis, MODEL_PROJ_WEGG);
newmis.noise = "wraith/bounce.wav";
}
else if (self.classtype == CT_MONSHAL) {
setmodel( newmis, MODEL_PROJ_SEGG);
newmis.noise = "shalrath/bounce.wav";
}
newmis.classgroup = CG_MINIONEGG; // Proper group type
newmis.enemy = SUB_entEnemyTarget(); // Make sure got right enemy
newmis.bbmins = VEC_HULLE_MIN; // Small egg size
newmis.bbmaxs = VEC_HULLE_MAX;
newmis.frame = self.attachment.frame; // Current ball size (frame)
newmis.think = Hatch_Egg; // Eventually hatch
newmis.nextthink = time + LIFE_EGG; // Short timer
}
else if (proj_type == CT_PROJ_SPID) {
setmodel (newmis, MODEL_PROJ_SPID);
newmis.frame = rint((random() * 9));
}
else if (proj_type == CT_PROJ_ELF) {
setmodel (newmis, MODEL_PROJ_ELF);
newmis.frame = rint((random() * 9));
}
else if (proj_type == CT_PROJ_SWAMP) {
setmodel (newmis, MODEL_PROJ_SWAMP);
newmis.frame = rint((random() * 9));
}
else if (proj_type == CT_PROJ_VORE) {
setmodel (newmis, MODEL_PROJ_VORE);
newmis.frame = rint((4 + random() * 4));
}
else if (proj_type == CT_PROJ_NOUR2) {
setmodel (newmis, MODEL_PROJ_NOUR2B);
newmis.skin = self.skin;
newmis.frame = 7; // Large size
newmis.noise = "nour/bounce.wav";
}
else if (proj_type == CT_PROJ_CHTHON2) {
setmodel (newmis, MODEL_PROJ_CHTHON1);
newmis.skin = self.skin;
newmis.frame = rint((random() * 9));
newmis.noise = "chthon/bounce.wav";
newmis.dmg = self.pos3_z;
}
else if (proj_type == CT_PROJ_SHUB2) {
setmodel (newmis, MODEL_PROJ_SHUB2);
newmis.frame = rint((random() * 9));
newmis.noise = "shub/bounce.wav";
newmis.dmg = self.pos3_z;
}
else if (proj_type == CT_PROJ_SHELLC) {
setmodel(newmis, MODEL_PROJ_SHELLC);
newmis.touch = Touch_ShellCasing;
newmis.think = model_fade;
newmis.nextthink = time + random() + LIFE_SHELLS;
newmis.ltime = newmis.nextthink;
}
// These projectile fly like grenades but are really spikes!
//----------------------------------------------------------------------
if (proj_type == CT_PROJ_SPID || proj_type == CT_PROJ_VORE ||
proj_type == CT_PROJ_SWAMP || proj_type == CT_PROJ_ELF ||
proj_type == CT_PROJ_FLESH) {
newmis.touch = Touch_Projectile;
newmis.nextthink = time + LIFE_PROJECTILE;
newmis.think = SUB_Remove;
newmis.bodyfadeaway = TRUE;
}
// Standard projectile setup (origin, size and velocity)
//----------------------------------------------------------------------
newmis.mangle = dir; // Save for later
newmis.velocity = newmis.mangle;
newmis.avelocity = avel;
newmis.angles = vectoangles(newmis.velocity);
setsize (newmis, newmis.bbmins, newmis.bbmaxs);
setorigin (newmis, org);
};
//======================================================================
// Generic functions for firing grenades from monsters
// MonsterGrenadeSound = play generic load grenade sound
// MonsterGrenadeSpeed = return generic attack speed
// MonsterFireGrenade = fire grenade at enemy origin
//
//----------------------------------------------------------------------
void() MonsterGrenadeSound =
{ sound (self, CHAN_WEAPON, "weapons/gl_loadshort.wav", 0.1+random()*0.5, ATTN_LOW); };
float() MonsterGrenadeSpeed =
{ return SPEED_MONGRENADE + (skill * SPEED_MONGLSKILL); };
//----------------------------------------------------------------------
void(vector grenade_org, vector grenade_enemyorg) MonsterFireGrenade =
{
local vector ang, dir, avel;
self.effects = self.effects | EF_MUZZLEFLASH;
sound (self, CHAN_WEAPON, "weapons/grenade.wav", 1, ATTN_NORM);
// Is Z aware disabled?
if ( query_configflag(SVR_ZAWARE) || self.no_zaware ) {
makevectors (self.angles);
dir = normalize(grenade_enemyorg - grenade_org);
// Default grenade speed (player = 600)
dir = dir * SPEED_PLAYGRENADE;
dir_z = ELEV_ZAXIS;
}
else {
// Z Aware tracking is ENABLED (AI track player much better)
// One final angle adjustment (based on actual projectile origin)
self.attack_speed = MonsterGrenadeSpeed();
self.attack_elev = SUB_Elevation(self.attack_elev, grenade_org, grenade_enemyorg, self.attack_speed);
ang = vectoangles(grenade_enemyorg - grenade_org);
ang_x = -self.attack_elev;
makevectors (ang);
dir = v_forward * self.attack_speed;
}
avel = vecrand(100,200,FALSE);
Launch_Grenade(grenade_org, dir, avel, CT_PROJ_GLMON);
};
/*======================================================================
Ejecting Shell Casing for SG/SSG/Upgrade
======================================================================*/
void() Touch_ShellCasing =
{
if (self.touchedvoid) return; // Marked for removal
if (check_skycontent(self.origin)) {entity_remove(self, 0.1); return;}
if (other == self.owner) return; // Touching self, do nothing
if (other.solid == SOLID_TRIGGER) return; // trigger field, do nothing
self.touch = SUB_Null;
if (random() < 0.5)
sound(self, CHAN_VOICE, "weapons/shellc.wav", random()*0.25, ATTN_LOW);
};
//----------------------------------------------------------------------
void(float shell_qty) Launch_ShellCasing =
{
local vector org, dir, avel;
if ( self.health < 1 ) return;
if (query_configflag(SVR_SHOTGCASE)) return;
if (self.flags & FL_CLIENT) makevectors (self.v_angle);
else makevectors (self.angles);
org = self.origin + v_up*10;
while (shell_qty > 0) {
dir = -v_right*75 + v_forward*(random()*50) + v_up*(100 + random()*100);
avel = vecrand(0,300,TRUE);
Launch_Grenade(org, dir, avel, CT_PROJ_SHELLC);
shell_qty = shell_qty - 1;
}
};