--- /dev/null
+.vector iorigin1, iorigin2;
+.float spawntime;
+.vector trail_oldorigin;
+.float trail_oldtime;
+.float fade_time, fade_rate;
+
+void SUB_Stop()
+{
+ self.move_velocity = self.move_avelocity = '0 0 0';
+ self.move_movetype = MOVETYPE_NONE;
+}
+
+.float alphamod;
+.float count; // set if clientside projectile
+.float cnt; // sound index
+.float gravity;
+.float snd_looping;
+.float silent;
+
+void Projectile_ResetTrail(vector to)
+{
+ self.trail_oldorigin = to;
+ self.trail_oldtime = time;
+}
+
+void Projectile_DrawTrail(vector to)
+{
+ vector from;
+ float t0;
+
+ from = self.trail_oldorigin;
+ t0 = self.trail_oldtime;
+ self.trail_oldorigin = to;
+ self.trail_oldtime = time;
+
+ // force the effect even for stationary firemine
+ if(self.cnt == PROJECTILE_FIREMINE)
+ if(from == to)
+ from_z += 1;
+
+ if (self.traileffect)
+ {
+ particles_alphamin = particles_alphamax = particles_fade = sqrt(self.alpha);
+ boxparticles(self.traileffect, self, from, to, self.velocity, self.velocity, 1, PARTICLES_USEALPHA | PARTICLES_USEFADE | PARTICLES_DRAWASTRAIL);
+ }
+}
+
+void Projectile_Draw()
+{
+ vector rot;
+ vector trailorigin;
+ float f;
+ float drawn;
+ float t;
+ float a;
+
+ f = self.move_flags;
+
+ if(self.count & 0x80)
+ {
+ //self.move_flags &= ~FL_ONGROUND;
+ if(self.move_movetype == MOVETYPE_NONE || self.move_movetype == MOVETYPE_FLY)
+ Movetype_Physics_NoMatchServer();
+ // the trivial movetypes do not have to match the
+ // server's ticrate as they are ticrate independent
+ // NOTE: this assumption is only true if MOVETYPE_FLY
+ // projectiles detonate on impact. If they continue
+ // moving, we might still be ticrate dependent.
+ else
+ Movetype_Physics_MatchServer(autocvar_cl_projectiles_sloppy);
+ if(!(self.move_flags & FL_ONGROUND))
+ if(self.velocity != '0 0 0')
+ self.move_angles = self.angles = vectoangles(self.velocity);
+ }
+ else
+ {
+ InterpolateOrigin_Do();
+ }
+
+ if(self.count & 0x80)
+ {
+ drawn = (time >= self.spawntime - 0.02);
+ t = max(time, self.spawntime);
+ }
+ else
+ {
+ drawn = (self.iflags & IFLAG_VALID);
+ t = time;
+ }
+
+ if(!(f & FL_ONGROUND))
+ {
+ rot = '0 0 0';
+ switch(self.cnt)
+ {
+ /*
+ case PROJECTILE_GRENADE:
+ rot = '-2000 0 0'; // forward
+ break;
+ */
+ case PROJECTILE_GRENADE_BOUNCING:
+ rot = '0 -1000 0'; // sideways
+ break;
+ case PROJECTILE_NADE_RED_BURN:
+ case PROJECTILE_NADE_RED:
+ case PROJECTILE_NADE_BLUE_BURN:
+ case PROJECTILE_NADE_BLUE:
+ case PROJECTILE_NADE_YELLOW_BURN:
+ case PROJECTILE_NADE_YELLOW:
+ case PROJECTILE_NADE_PINK_BURN:
+ case PROJECTILE_NADE_PINK:
+ case PROJECTILE_NADE_BURN:
+ case PROJECTILE_NADE:
+ rot = self.avelocity;
+ break;
+ case PROJECTILE_HOOKBOMB:
+ rot = '1000 0 0'; // forward
+ break;
+ default:
+ break;
+ }
+ self.angles = AnglesTransform_ToAngles(AnglesTransform_Multiply(AnglesTransform_FromAngles(self.angles), rot * (t - self.spawntime)));
+ }
+
+ vector ang;
+ ang = self.angles;
+ ang_x = -ang_x;
+ makevectors(ang);
+
+ a = 1 - (time - self.fade_time) * self.fade_rate;
+ self.alpha = bound(0, self.alphamod * a, 1);
+ if(self.alpha <= 0)
+ drawn = 0;
+ self.renderflags = 0;
+
+ trailorigin = self.origin;
+ switch(self.cnt)
+ {
+ case PROJECTILE_NADE_RED_BURN:
+ case PROJECTILE_NADE_RED:
+ case PROJECTILE_NADE_BLUE_BURN:
+ case PROJECTILE_NADE_BLUE:
+ case PROJECTILE_NADE_YELLOW_BURN:
+ case PROJECTILE_NADE_YELLOW:
+ case PROJECTILE_NADE_PINK_BURN:
+ case PROJECTILE_NADE_PINK:
+ case PROJECTILE_NADE_BURN:
+ case PROJECTILE_NADE:
+ trailorigin += v_up * 4;
+ break;
+ case PROJECTILE_GRENADE:
+ case PROJECTILE_GRENADE_BOUNCING:
+ trailorigin += v_right * 1 + v_forward * -10;
+ break;
+ default:
+ break;
+ }
+ if(drawn)
+ Projectile_DrawTrail(trailorigin);
+ else
+ Projectile_ResetTrail(trailorigin);
+
+ self.drawmask = 0;
+
+ if(!drawn)
+ return;
+
+ switch(self.cnt)
+ {
+ // Possibly add dlights here.
+ default:
+ break;
+ }
+
+ self.drawmask = MASK_NORMAL;
+}
+
+void loopsound(entity e, float ch, string samp, float vol, float attn)
+{
+ if(self.silent)
+ return;
+
+ sound(e, ch, samp, vol, attn);
+ e.snd_looping = ch;
+}
+
+void Ent_RemoveProjectile()
+{
+ if(self.count & 0x80)
+ {
+ tracebox(self.origin, self.mins, self.maxs, self.origin + self.velocity * 0.05, MOVE_NORMAL, self);
+ Projectile_DrawTrail(trace_endpos);
+ }
+}
+
+void Ent_Projectile()
+{
+ float f;
+
+ // projectile properties:
+ // kind (interpolated, or clientside)
+ //
+ // modelindex
+ // origin
+ // scale
+ // if clientside:
+ // velocity
+ // gravity
+ // soundindex (hardcoded list)
+ // effects
+ //
+ // projectiles don't send angles, because they always follow the velocity
+
+ f = ReadByte();
+ self.count = (f & 0x80);
+ self.iflags = (self.iflags & IFLAG_INTERNALMASK) | IFLAG_AUTOANGLES | IFLAG_ANGLES | IFLAG_ORIGIN;
+ self.solid = SOLID_TRIGGER;
+ //self.effects = EF_NOMODELFLAGS;
+
+ // this should make collisions with bmodels more exact, but it leads to
+ // projectiles no longer being able to lie on a bmodel
+ self.move_nomonsters = MOVE_WORLDONLY;
+ if(f & 0x40)
+ self.move_flags |= FL_ONGROUND;
+ else
+ self.move_flags &= ~FL_ONGROUND;
+
+ if(!self.move_time)
+ {
+ // for some unknown reason, we don't need to care for
+ // sv_gameplayfix_delayprojectiles here.
+ self.move_time = time;
+ self.spawntime = time;
+ }
+ else
+ self.move_time = max(self.move_time, time);
+
+ if(!(self.count & 0x80))
+ InterpolateOrigin_Undo();
+
+ if(f & 1)
+ {
+ self.origin_x = ReadCoord();
+ self.origin_y = ReadCoord();
+ self.origin_z = ReadCoord();
+ setorigin(self, self.origin);
+ if(self.count & 0x80)
+ {
+ self.velocity_x = ReadCoord();
+ self.velocity_y = ReadCoord();
+ self.velocity_z = ReadCoord();
+ if(f & 0x10)
+ self.gravity = ReadCoord();
+ else
+ self.gravity = 0; // none
+ self.move_origin = self.origin;
+ self.move_velocity = self.velocity;
+ }
+
+ if(time == self.spawntime || (self.count & 0x80) || (f & 0x08))
+ {
+ self.trail_oldorigin = self.origin;
+ if(!(self.count & 0x80))
+ InterpolateOrigin_Reset();
+ }
+
+ if(f & 0x20)
+ {
+ self.fade_time = time + ReadByte() * ticrate;
+ self.fade_rate = 1 / (ReadByte() * ticrate);
+ }
+ else
+ {
+ self.fade_time = 0;
+ self.fade_rate = 0;
+ }
+ }
+
+ if(f & 2)
+ {
+ self.cnt = ReadByte();
+
+ self.silent = (self.cnt & 0x80);
+ self.cnt = (self.cnt & 0x7F);
+
+ self.scale = 1;
+ self.traileffect = 0;
+ switch(self.cnt)
+ {
+ case PROJECTILE_ELECTRO: setmodel(self, "models/ebomb.mdl");self.traileffect = particleeffectnum("TR_NEXUIZPLASMA"); break;
+ case PROJECTILE_ROCKET: setmodel(self, "models/rocket.md3");self.traileffect = particleeffectnum("TR_ROCKET"); self.scale = 2; break;
+ case PROJECTILE_CRYLINK: setmodel(self, "models/plasmatrail.mdl");self.traileffect = particleeffectnum("TR_CRYLINKPLASMA"); break;
+ case PROJECTILE_CRYLINK_BOUNCING: setmodel(self, "models/plasmatrail.mdl");self.traileffect = particleeffectnum("TR_CRYLINKPLASMA"); break;
+ case PROJECTILE_ELECTRO_BEAM: setmodel(self, "models/elaser.mdl");self.traileffect = particleeffectnum("TR_NEXUIZPLASMA"); break;
+ case PROJECTILE_GRENADE: setmodel(self, "models/grenademodel.md3");self.traileffect = particleeffectnum("TR_GRENADE"); break;
+ case PROJECTILE_GRENADE_BOUNCING: setmodel(self, "models/grenademodel.md3");self.traileffect = particleeffectnum("TR_GRENADE"); break;
+ case PROJECTILE_MINE: setmodel(self, "models/mine.md3");self.traileffect = particleeffectnum("TR_GRENADE"); break;
+ case PROJECTILE_BLASTER: setmodel(self, "models/laser.mdl");self.traileffect = particleeffectnum(""); break;
+ case PROJECTILE_HLAC: setmodel(self, "models/hlac_bullet.md3");self.traileffect = particleeffectnum(""); break;
+ case PROJECTILE_PORTO_RED: setmodel(self, "models/grenademodel.md3");self.traileffect = particleeffectnum("TR_WIZSPIKE"); self.scale = 4; break;
+ case PROJECTILE_PORTO_BLUE: setmodel(self, "models/grenademodel.md3");self.traileffect = particleeffectnum("TR_WIZSPIKE"); self.scale = 4; break;
+ case PROJECTILE_HOOKBOMB: setmodel(self, "models/grenademodel.md3");self.traileffect = particleeffectnum("TR_KNIGHTSPIKE"); break;
+ case PROJECTILE_HAGAR: setmodel(self, "models/hagarmissile.mdl");self.traileffect = particleeffectnum("tr_hagar"); self.scale = 0.75; break;
+ case PROJECTILE_HAGAR_BOUNCING: setmodel(self, "models/hagarmissile.mdl");self.traileffect = particleeffectnum("tr_hagar"); self.scale = 0.75; break;
+ case PROJECTILE_FIREBALL: self.model = ""; self.modelindex = 0; self.traileffect = particleeffectnum("fireball"); break; // particle effect is good enough
+ case PROJECTILE_FIREMINE: self.model = ""; self.modelindex = 0; self.traileffect = particleeffectnum("firemine"); break; // particle effect is good enough
+ case PROJECTILE_TAG: setmodel(self, "models/laser.mdl"); self.traileffect = particleeffectnum("TR_ROCKET"); break;
+ case PROJECTILE_FLAC: setmodel(self, "models/hagarmissile.mdl"); self.scale = 0.4; self.traileffect = particleeffectnum("TR_SEEKER"); break;
+ case PROJECTILE_SEEKER: setmodel(self, "models/tagrocket.md3"); self.traileffect = particleeffectnum("TR_SEEKER"); break;
+
++ case PROJECTILE_MAGE_SPIKE: setmodel(self, "models/ebomb.mdl"); self.traileffect = particleeffectnum("TR_VORESPIKE"); break;
++ case PROJECTILE_SHAMBLER_LIGHTNING: setmodel(self, "models/ebomb.mdl"); self.traileffect = particleeffectnum("TR_NEXUIZPLASMA"); break;
++
+ case PROJECTILE_RAPTORBOMB: setmodel(self, "models/vehicles/clusterbomb.md3"); self.gravity = 1; self.avelocity = '0 0 180'; self.traileffect = particleeffectnum(""); break;
+ case PROJECTILE_RAPTORBOMBLET: setmodel(self, "models/vehicles/bomblet.md3"); self.gravity = 1; self.avelocity = '0 0 180'; self.traileffect = particleeffectnum(""); break;
+ case PROJECTILE_RAPTORCANNON: setmodel(self, "models/plasmatrail.mdl"); self.traileffect = particleeffectnum("TR_CRYLINKPLASMA"); break;
+
+ case PROJECTILE_SPIDERROCKET: setmodel(self, "models/vehicles/rocket02.md3"); self.traileffect = particleeffectnum("spiderbot_rocket_thrust"); break;
+ case PROJECTILE_WAKIROCKET: setmodel(self, "models/vehicles/rocket01.md3"); self.traileffect = particleeffectnum("wakizashi_rocket_thrust"); break;
+ case PROJECTILE_WAKICANNON: setmodel(self, "models/laser.mdl"); self.traileffect = particleeffectnum(""); break;
+
+ case PROJECTILE_BUMBLE_GUN: setmodel(self, "models/elaser.mdl");self.traileffect = particleeffectnum("TR_NEXUIZPLASMA"); break;
+ case PROJECTILE_BUMBLE_BEAM: setmodel(self, "models/elaser.mdl");self.traileffect = particleeffectnum("TR_NEXUIZPLASMA"); break;
+
+ case PROJECTILE_NADE_RED: setmodel(self, "models/weapons/v_ok_grenade.md3");self.traileffect = particleeffectnum("nade_red"); break;
+ case PROJECTILE_NADE_RED_BURN: setmodel(self, "models/weapons/v_ok_grenade.md3");self.traileffect = particleeffectnum("nade_red_burn"); break;
+ case PROJECTILE_NADE_BLUE: setmodel(self, "models/weapons/v_ok_grenade.md3");self.traileffect = particleeffectnum("nade_blue"); break;
+ case PROJECTILE_NADE_BLUE_BURN: setmodel(self, "models/weapons/v_ok_grenade.md3");self.traileffect = particleeffectnum("nade_blue_burn"); break;
+ case PROJECTILE_NADE_YELLOW: setmodel(self, "models/weapons/v_ok_grenade.md3");self.traileffect = particleeffectnum("nade_yellow"); break;
+ case PROJECTILE_NADE_YELLOW_BURN: setmodel(self, "models/weapons/v_ok_grenade.md3");self.traileffect = particleeffectnum("nade_yellow_burn"); break;
+ case PROJECTILE_NADE_PINK: setmodel(self, "models/weapons/v_ok_grenade.md3");self.traileffect = particleeffectnum("nade_pink"); break;
+ case PROJECTILE_NADE_PINK_BURN: setmodel(self, "models/weapons/v_ok_grenade.md3");self.traileffect = particleeffectnum("nade_pink_burn"); break;
+ case PROJECTILE_NADE: setmodel(self, "models/weapons/v_ok_grenade.md3");self.traileffect = particleeffectnum("nade"); break;
+ case PROJECTILE_NADE_BURN: setmodel(self, "models/weapons/v_ok_grenade.md3");self.traileffect = particleeffectnum("nade_burn"); break;
+
+ default:
+ error("Received invalid CSQC projectile, can't work with this!");
+ break;
+ }
+
+ self.mins = '0 0 0';
+ self.maxs = '0 0 0';
+ self.colormod = '0 0 0';
+ self.move_touch = SUB_Stop;
+ self.move_movetype = MOVETYPE_TOSS;
+ self.alphamod = 1;
+
+ switch(self.cnt)
+ {
+ case PROJECTILE_ELECTRO:
+ // only new engines support sound moving with object
+ loopsound(self, CH_SHOTS_SINGLE, "weapons/electro_fly.wav", VOL_BASE, ATTEN_NORM);
+ self.mins = '0 0 -4';
+ self.maxs = '0 0 -4';
+ self.move_movetype = MOVETYPE_BOUNCE;
+ self.move_touch = func_null;
+ self.move_bounce_factor = g_balance_electro_secondary_bouncefactor;
+ self.move_bounce_stopspeed = g_balance_electro_secondary_bouncestop;
+ break;
+ case PROJECTILE_ROCKET:
+ loopsound(self, CH_SHOTS_SINGLE, "weapons/rocket_fly.wav", VOL_BASE, ATTEN_NORM);
+ self.mins = '-3 -3 -3';
+ self.maxs = '3 3 3';
+ break;
+ case PROJECTILE_GRENADE:
+ self.mins = '-3 -3 -3';
+ self.maxs = '3 3 3';
+ break;
+ case PROJECTILE_NADE_RED_BURN:
+ case PROJECTILE_NADE_RED:
+ case PROJECTILE_NADE_BLUE_BURN:
+ case PROJECTILE_NADE_BLUE:
+ self.mins = '-3 -3 -3';
+ self.maxs = '3 3 3';
+ self.move_movetype = MOVETYPE_BOUNCE;
+ self.move_touch = func_null;
+ self.scale = 1.5;
+ self.avelocity = randomvec() * 720;
+ break;
+ case PROJECTILE_GRENADE_BOUNCING:
+ self.mins = '-3 -3 -3';
+ self.maxs = '3 3 3';
+ self.move_movetype = MOVETYPE_BOUNCE;
+ self.move_touch = func_null;
+ self.move_bounce_factor = g_balance_grenadelauncher_bouncefactor;
+ self.move_bounce_stopspeed = g_balance_grenadelauncher_bouncestop;
+ break;
+ case PROJECTILE_NADE_RED_BURN:
+ case PROJECTILE_NADE_RED:
+ case PROJECTILE_NADE_BLUE_BURN:
+ case PROJECTILE_NADE_BLUE:
+ case PROJECTILE_NADE_YELLOW_BURN:
+ case PROJECTILE_NADE_YELLOW:
+ case PROJECTILE_NADE_PINK_BURN:
+ case PROJECTILE_NADE_PINK:
+ case PROJECTILE_NADE_BURN:
+ case PROJECTILE_NADE:
+ self.mins = '-16 -16 -16';
+ self.maxs = '16 16 16';
+ self.move_movetype = MOVETYPE_BOUNCE;
+ self.move_touch = func_null;
+ self.scale = 1.5;
+ self.avelocity = randomvec() * 720;
+ break;
++ case PROJECTILE_SHAMBLER_LIGHTNING:
++ self.mins = '-8 -8 -8';
++ self.maxs = '8 8 8';
++ self.scale = 2.5;
++ self.avelocity = randomvec() * 720;
++ break;
+ case PROJECTILE_MINE:
+ self.mins = '-4 -4 -4';
+ self.maxs = '4 4 4';
+ break;
+ case PROJECTILE_PORTO_RED:
+ self.colormod = '2 1 1';
+ self.alphamod = 0.5;
+ self.move_movetype = MOVETYPE_BOUNCE;
+ self.move_touch = func_null;
+ break;
+ case PROJECTILE_PORTO_BLUE:
+ self.colormod = '1 1 2';
+ self.alphamod = 0.5;
+ self.move_movetype = MOVETYPE_BOUNCE;
+ self.move_touch = func_null;
+ break;
+ case PROJECTILE_HAGAR_BOUNCING:
+ self.move_movetype = MOVETYPE_BOUNCE;
+ self.move_touch = func_null;
+ break;
+ case PROJECTILE_CRYLINK_BOUNCING:
+ self.move_movetype = MOVETYPE_BOUNCE;
+ self.move_touch = func_null;
+ break;
+ case PROJECTILE_FIREBALL:
+ loopsound(self, CH_SHOTS_SINGLE, "weapons/fireball_fly2.wav", VOL_BASE, ATTEN_NORM);
+ self.mins = '-16 -16 -16';
+ self.maxs = '16 16 16';
+ break;
+ case PROJECTILE_FIREMINE:
+ loopsound(self, CH_SHOTS_SINGLE, "weapons/fireball_fly.wav", VOL_BASE, ATTEN_NORM);
+ self.move_movetype = MOVETYPE_BOUNCE;
+ self.move_touch = func_null;
+ self.mins = '-4 -4 -4';
+ self.maxs = '4 4 4';
+ break;
+ case PROJECTILE_TAG:
+ self.mins = '-2 -2 -2';
+ self.maxs = '2 2 2';
+ break;
+ case PROJECTILE_FLAC:
+ self.mins = '-2 -2 -2';
+ self.maxs = '2 2 2';
+ break;
+ case PROJECTILE_SEEKER:
+ loopsound(self, CH_SHOTS_SINGLE, "weapons/tag_rocket_fly.wav", VOL_BASE, ATTEN_NORM);
+ self.mins = '-4 -4 -4';
+ self.maxs = '4 4 4';
+ break;
+ case PROJECTILE_RAPTORBOMB:
+ self.mins = '-3 -3 -3';
+ self.maxs = '3 3 3';
+ break;
+ case PROJECTILE_RAPTORBOMBLET:
+ break;
+ case PROJECTILE_RAPTORCANNON:
+ break;
+ case PROJECTILE_SPIDERROCKET:
+ loopsound(self, CH_SHOTS_SINGLE, "weapons/tag_rocket_fly.wav", VOL_BASE, ATTEN_NORM);
+ break;
+ case PROJECTILE_WAKIROCKET:
+ loopsound(self, CH_SHOTS_SINGLE, "weapons/tag_rocket_fly.wav", VOL_BASE, ATTEN_NORM);
+ break;
+ /*
+ case PROJECTILE_WAKICANNON:
+ break;
+ case PROJECTILE_BUMBLE_GUN:
+ // only new engines support sound moving with object
+ loopsound(self, CH_SHOTS_SINGLE, "weapons/electro_fly.wav", VOL_BASE, ATTEN_NORM);
+ self.mins = '0 0 -4';
+ self.maxs = '0 0 -4';
+ self.move_movetype = MOVETYPE_BOUNCE;
+ self.move_touch = func_null;
+ self.move_bounce_factor = g_balance_electro_secondary_bouncefactor;
+ self.move_bounce_stopspeed = g_balance_electro_secondary_bouncestop;
+ break;
+ */
+ default:
+ break;
+ }
+ setsize(self, self.mins, self.maxs);
+ }
+
+ if(self.gravity)
+ {
+ if(self.move_movetype == MOVETYPE_FLY)
+ self.move_movetype = MOVETYPE_TOSS;
+ if(self.move_movetype == MOVETYPE_BOUNCEMISSILE)
+ self.move_movetype = MOVETYPE_BOUNCE;
+ }
+ else
+ {
+ if(self.move_movetype == MOVETYPE_TOSS)
+ self.move_movetype = MOVETYPE_FLY;
+ if(self.move_movetype == MOVETYPE_BOUNCE)
+ self.move_movetype = MOVETYPE_BOUNCEMISSILE;
+ }
+
+ if(!(self.count & 0x80))
+ InterpolateOrigin_Note();
+
+ self.draw = Projectile_Draw;
+ self.entremove = Ent_RemoveProjectile;
+}
+
+void Projectile_Precache()
+{
+ precache_model("models/ebomb.mdl");
+ precache_model("models/elaser.mdl");
+ precache_model("models/grenademodel.md3");
+ precache_model("models/mine.md3");
+ precache_model("models/hagarmissile.mdl");
+ precache_model("models/hlac_bullet.md3");
+ precache_model("models/laser.mdl");
+ precache_model("models/plasmatrail.mdl");
+ precache_model("models/rocket.md3");
+ precache_model("models/tagrocket.md3");
+ precache_model("models/tracer.mdl");
+
+ precache_model("models/weapons/v_ok_grenade.md3");
+
+ precache_sound("weapons/electro_fly.wav");
+ precache_sound("weapons/rocket_fly.wav");
+ precache_sound("weapons/fireball_fly.wav");
+ precache_sound("weapons/fireball_fly2.wav");
+ precache_sound("weapons/tag_rocket_fly.wav");
+
+}
--- /dev/null
- RadiusDamage (self, self.realowner, (autocvar_g_monster_mage_attack_spike_damage), (autocvar_g_monster_mage_attack_spike_damage) * 0.5, (autocvar_g_monster_mage_attack_spike_radius), world, 0, DEATH_MONSTER_MAGE, other);
+ #ifdef REGISTER_MONSTER
+ REGISTER_MONSTER(
+ /* MON_##id */ MAGE,
+ /* function */ m_mage,
+ /* spawnflags */ MON_FLAG_MELEE | MON_FLAG_RANGED,
+ /* mins,maxs */ '-36 -36 -24', '36 36 50',
+ /* model */ "mage.dpm",
+ /* netname */ "mage",
+ /* fullname */ _("Mage")
+ );
+
+ #else
+ #ifdef SVQC
+ float autocvar_g_monster_mage_health;
+ float autocvar_g_monster_mage_attack_spike_damage;
+ float autocvar_g_monster_mage_attack_spike_radius;
+ float autocvar_g_monster_mage_attack_spike_delay;
+ float autocvar_g_monster_mage_attack_spike_accel;
+ float autocvar_g_monster_mage_attack_spike_decel;
+ float autocvar_g_monster_mage_attack_spike_turnrate;
+ float autocvar_g_monster_mage_attack_spike_speed_max;
+ float autocvar_g_monster_mage_attack_spike_smart;
+ float autocvar_g_monster_mage_attack_spike_smart_trace_min;
+ float autocvar_g_monster_mage_attack_spike_smart_trace_max;
+ float autocvar_g_monster_mage_attack_spike_smart_mindist;
+ float autocvar_g_monster_mage_attack_push_damage;
+ float autocvar_g_monster_mage_attack_push_radius;
+ float autocvar_g_monster_mage_attack_push_delay;
+ float autocvar_g_monster_mage_attack_push_force;
+ float autocvar_g_monster_mage_heal_self;
+ float autocvar_g_monster_mage_heal_allies;
+ float autocvar_g_monster_mage_heal_minhealth;
+ float autocvar_g_monster_mage_heal_range;
+ float autocvar_g_monster_mage_heal_delay;
+ float autocvar_g_monster_mage_shield_time;
+ float autocvar_g_monster_mage_shield_delay;
+ float autocvar_g_monster_mage_shield_blockpercent;
+ float autocvar_g_monster_mage_speed_stop;
+ float autocvar_g_monster_mage_speed_run;
+ float autocvar_g_monster_mage_speed_walk;
+
+ const float mage_anim_idle = 0;
+ const float mage_anim_walk = 1;
+ const float mage_anim_attack = 2;
+ const float mage_anim_pain = 3;
+ const float mage_anim_death = 4;
+ const float mage_anim_run = 5;
+
+ void() mage_heal;
+ void() mage_shield;
+
+ .entity mage_spike;
+ .float shield_ltime;
+
+ float friend_needshelp(entity e)
+ {
+ if(e == world)
+ return FALSE;
+ if(e.health <= 0)
+ return FALSE;
+ if(DIFF_TEAM(e, self) && e != self.monster_owner)
+ return FALSE;
+ if(e.freezetag_frozen)
+ return FALSE;
+ if(!IS_PLAYER(e))
+ return ((e.flags & FL_MONSTER) && e.health < e.max_health);
+ if(e.items & IT_INVINCIBLE)
+ return FALSE;
+
+ switch(self.skin)
+ {
+ case 0: return (e.health < autocvar_g_balance_health_regenstable);
+ case 1: return ((e.ammo_cells && e.ammo_cells < g_pickup_cells_max) || (e.ammo_rockets && e.ammo_rockets < g_pickup_rockets_max) || (e.ammo_nails && e.ammo_nails < g_pickup_nails_max) || (e.ammo_shells && e.ammo_shells < g_pickup_shells_max));
+ case 2: return (e.armorvalue < autocvar_g_balance_armor_regenstable);
+ case 3: return (e.health > 0);
+ }
+
+ return FALSE;
+ }
+
+ void mage_spike_explode()
+ {
+ self.event_damage = func_null;
+
+ sound(self, CH_SHOTS, "weapons/grenade_impact.wav", VOL_BASE, ATTEN_NORM);
+
+ self.realowner.mage_spike = world;
+
+ pointparticles(particleeffectnum("explosion_small"), self.origin, '0 0 0', 1);
- RadiusDamage (self, self, (autocvar_g_monster_mage_attack_push_damage), (autocvar_g_monster_mage_attack_push_damage), (autocvar_g_monster_mage_attack_push_radius), world, (autocvar_g_monster_mage_attack_push_force), DEATH_MONSTER_MAGE, self.enemy);
++ RadiusDamage (self, self.realowner, (autocvar_g_monster_mage_attack_spike_damage), (autocvar_g_monster_mage_attack_spike_damage) * 0.5, (autocvar_g_monster_mage_attack_spike_radius), world, world, 0, DEATH_MONSTER_MAGE, other);
+
+ remove (self);
+ }
+
+ void mage_spike_touch()
+ {
+ PROJECTILE_TOUCH;
+
+ mage_spike_explode();
+ }
+
+ // copied from W_Seeker_Think
+ void mage_spike_think()
+ {
+ entity e;
+ vector desireddir, olddir, newdir, eorg;
+ float turnrate;
+ float dist;
+ float spd;
+
+ if (time > self.ltime || self.enemy.health <= 0 || self.owner.health <= 0)
+ {
+ self.projectiledeathtype |= HITTYPE_SPLASH;
+ mage_spike_explode();
+ }
+
+ spd = vlen(self.velocity);
+ spd = bound(
+ spd - (autocvar_g_monster_mage_attack_spike_decel) * frametime,
+ (autocvar_g_monster_mage_attack_spike_speed_max),
+ spd + (autocvar_g_monster_mage_attack_spike_accel) * frametime
+ );
+
+ if (self.enemy != world)
+ if (self.enemy.takedamage != DAMAGE_AIM || self.enemy.deadflag != DEAD_NO)
+ self.enemy = world;
+
+ if (self.enemy != world)
+ {
+ e = self.enemy;
+ eorg = 0.5 * (e.absmin + e.absmax);
+ turnrate = (autocvar_g_monster_mage_attack_spike_turnrate); // how fast to turn
+ desireddir = normalize(eorg - self.origin);
+ olddir = normalize(self.velocity); // get my current direction
+ dist = vlen(eorg - self.origin);
+
+ // Do evasive maneuvers for world objects? ( this should be a cpu hog. :P )
+ if ((autocvar_g_monster_mage_attack_spike_smart) && (dist > (autocvar_g_monster_mage_attack_spike_smart_mindist)))
+ {
+ // Is it a better idea (shorter distance) to trace to the target itself?
+ if ( vlen(self.origin + olddir * self.wait) < dist)
+ traceline(self.origin, self.origin + olddir * self.wait, FALSE, self);
+ else
+ traceline(self.origin, eorg, FALSE, self);
+
+ // Setup adaptive tracelength
+ self.wait = bound((autocvar_g_monster_mage_attack_spike_smart_trace_min), vlen(self.origin - trace_endpos), self.wait = (autocvar_g_monster_mage_attack_spike_smart_trace_max));
+
+ // Calc how important it is that we turn and add this to the desierd (enemy) dir.
+ desireddir = normalize(((trace_plane_normal * (1 - trace_fraction)) + (desireddir * trace_fraction)) * 0.5);
+ }
+
+ newdir = normalize(olddir + desireddir * turnrate); // take the average of the 2 directions; not the best method but simple & easy
+ self.velocity = newdir * spd; // make me fly in the new direction at my flight speed
+ }
+ else
+ dist = 0;
+
+ ///////////////
+
+ //self.angles = vectoangles(self.velocity); // turn model in the new flight direction
+ self.nextthink = time;// + 0.05; // csqc projectiles
+ UpdateCSQCProjectile(self);
+ }
+
+ void mage_attack_spike()
+ {
+ entity missile;
+ vector dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
+
+ makevectors(self.angles);
+
+ missile = spawn ();
+ missile.owner = missile.realowner = self;
+ missile.think = mage_spike_think;
+ missile.ltime = time + 7;
+ missile.nextthink = time;
+ missile.solid = SOLID_BBOX;
+ missile.movetype = MOVETYPE_FLYMISSILE;
+ missile.flags = FL_PROJECTILE;
+ setorigin(missile, self.origin + v_forward * 14 + '0 0 30' + v_right * -14);
+ setsize (missile, '0 0 0', '0 0 0');
+ missile.velocity = dir * 400;
+ missile.avelocity = '300 300 300';
+ missile.enemy = self.enemy;
+ missile.touch = mage_spike_touch;
+
+ self.mage_spike = missile;
+
+ CSQCProjectile(missile, TRUE, PROJECTILE_MAGE_SPIKE, TRUE);
+ }
+
+ void mage_heal()
+ {
+ entity head;
+ float washealed = FALSE;
+
+ for(head = findradius(self.origin, (autocvar_g_monster_mage_heal_range)); head; head = head.chain) if(friend_needshelp(head))
+ {
+ washealed = TRUE;
+ string fx = "";
+ if(IS_PLAYER(head))
+ {
+ switch(self.skin)
+ {
+ case 0:
+ if(head.health < autocvar_g_balance_health_regenstable) head.health = bound(0, head.health + (autocvar_g_monster_mage_heal_allies), autocvar_g_balance_health_regenstable);
+ fx = "healing_fx";
+ break;
+ case 1:
+ if(head.ammo_cells) head.ammo_cells = bound(head.ammo_cells, head.ammo_cells + 1, g_pickup_cells_max);
+ if(head.ammo_rockets) head.ammo_rockets = bound(head.ammo_rockets, head.ammo_rockets + 1, g_pickup_rockets_max);
+ if(head.ammo_shells) head.ammo_shells = bound(head.ammo_shells, head.ammo_shells + 2, g_pickup_shells_max);
+ if(head.ammo_nails) head.ammo_nails = bound(head.ammo_nails, head.ammo_nails + 5, g_pickup_nails_max);
+ fx = "ammoregen_fx";
+ break;
+ case 2:
+ if(head.armorvalue < autocvar_g_balance_armor_regenstable)
+ {
+ head.armorvalue = bound(0, head.armorvalue + (autocvar_g_monster_mage_heal_allies), autocvar_g_balance_armor_regenstable);
+ fx = "armorrepair_fx";
+ }
+ break;
+ case 3:
+ head.health = bound(0, head.health - ((head == self) ? (autocvar_g_monster_mage_heal_self) : (autocvar_g_monster_mage_heal_allies)), autocvar_g_balance_health_regenstable);
+ fx = "rage";
+ break;
+ }
+
+ pointparticles(particleeffectnum(fx), head.origin, '0 0 0', 1);
+ }
+ else
+ {
+ pointparticles(particleeffectnum("healing_fx"), head.origin, '0 0 0', 1);
+ head.health = bound(0, head.health + (autocvar_g_monster_mage_heal_allies), head.max_health);
+ WaypointSprite_UpdateHealth(head.sprite, head.health);
+ }
+ }
+
+ if(washealed)
+ {
+ self.frame = mage_anim_attack;
+ self.attack_finished_single = time + (autocvar_g_monster_mage_heal_delay);
+ }
+ }
+
+ void mage_push()
+ {
+ sound(self, CH_SHOTS, "weapons/tagexp1.wav", 1, ATTEN_NORM);
++ RadiusDamage (self, self, (autocvar_g_monster_mage_attack_push_damage), (autocvar_g_monster_mage_attack_push_damage), (autocvar_g_monster_mage_attack_push_radius), world, world, (autocvar_g_monster_mage_attack_push_force), DEATH_MONSTER_MAGE, self.enemy);
+ pointparticles(particleeffectnum("TE_EXPLOSION"), self.origin, '0 0 0', 1);
+
+ self.frame = mage_anim_attack;
+ self.attack_finished_single = time + (autocvar_g_monster_mage_attack_push_delay);
+ }
+
+ void mage_teleport()
+ {
+ if(vlen(self.enemy.origin - self.origin) >= 500)
+ return;
+
+ makevectors(self.enemy.angles);
+ tracebox(self.enemy.origin + ((v_forward * -1) * 200), self.mins, self.maxs, self.origin, MOVE_NOMONSTERS, self);
+
+ if(trace_fraction < 1)
+ return;
+
+ pointparticles(particleeffectnum("spawn_event_neutral"), self.origin, '0 0 0', 1);
+ setorigin(self, self.enemy.origin + ((v_forward * -1) * 200));
+
+ self.attack_finished_single = time + 0.2;
+ }
+
+ void mage_shield_remove()
+ {
+ self.effects &= ~(EF_ADDITIVE | EF_BLUE);
+ self.armorvalue = 0;
+ self.m_armor_blockpercent = autocvar_g_monsters_armor_blockpercent;
+ }
+
+ void mage_shield()
+ {
+ self.effects |= (EF_ADDITIVE | EF_BLUE);
+ self.lastshielded = time + (autocvar_g_monster_mage_shield_delay);
+ self.m_armor_blockpercent = (autocvar_g_monster_mage_shield_blockpercent);
+ self.armorvalue = self.health;
+ self.shield_ltime = time + (autocvar_g_monster_mage_shield_time);
+ self.frame = mage_anim_attack;
+ self.attack_finished_single = time + 1;
+ }
+
+ float mage_attack(float attack_type)
+ {
+ switch(attack_type)
+ {
+ case MONSTER_ATTACK_MELEE:
+ {
+ if(random() <= 0.7)
+ {
+ mage_push();
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+ case MONSTER_ATTACK_RANGED:
+ {
+ if(!self.mage_spike)
+ {
+ if(random() <= 0.4)
+ {
+ mage_teleport();
+ return TRUE;
+ }
+ else
+ {
+ self.frame = mage_anim_attack;
+ self.attack_finished_single = time + (autocvar_g_monster_mage_attack_spike_delay);
+ defer(0.2, mage_attack_spike);
+ return TRUE;
+ }
+ }
+
+ if(self.mage_spike)
+ return TRUE;
+ else
+ return FALSE;
+ }
+ }
+
+ return FALSE;
+ }
+
+ void spawnfunc_monster_mage()
+ {
+ self.classname = "monster_mage";
+
+ self.monster_spawnfunc = spawnfunc_monster_mage;
+
+ if(Monster_CheckAppearFlags(self))
+ return;
+
+ if(!monster_initialize(MON_MAGE, FALSE)) { remove(self); return; }
+ }
+
+ // compatibility with old spawns
+ void spawnfunc_monster_shalrath() { spawnfunc_monster_mage(); }
+
+ float m_mage(float req)
+ {
+ switch(req)
+ {
+ case MR_THINK:
+ {
+ entity head;
+ float need_help = FALSE;
+
+ for(head = findradius(self.origin, (autocvar_g_monster_mage_heal_range)); head; head = head.chain)
+ if(head != self)
+ if(friend_needshelp(head))
+ {
+ need_help = TRUE;
+ break;
+ }
+
+ if(self.health < (autocvar_g_monster_mage_heal_minhealth) || need_help)
+ if(time >= self.attack_finished_single)
+ if(random() < 0.5)
+ mage_heal();
+
+ if(time >= self.shield_ltime && self.armorvalue)
+ mage_shield_remove();
+
+ if(self.enemy)
+ if(self.health < self.max_health)
+ if(time >= self.lastshielded)
+ if(random() < 0.5)
+ mage_shield();
+
+ monster_move((autocvar_g_monster_mage_speed_run), (autocvar_g_monster_mage_speed_walk), (autocvar_g_monster_mage_speed_stop), mage_anim_walk, mage_anim_run, mage_anim_idle);
+ return TRUE;
+ }
+ case MR_DEATH:
+ {
+ self.frame = mage_anim_death;
+ return TRUE;
+ }
+ case MR_SETUP:
+ {
+ if(!self.health) self.health = (autocvar_g_monster_mage_health);
+
+ self.monster_loot = spawnfunc_item_health_large;
+ self.monster_attackfunc = mage_attack;
+ self.frame = mage_anim_walk;
+
+ return TRUE;
+ }
+ case MR_PRECACHE:
+ {
+ precache_model ("models/monsters/mage.dpm");
+ precache_sound ("weapons/grenade_impact.wav");
+ precache_sound ("weapons/tagexp1.wav");
+ return TRUE;
+ }
+ }
+
+ return TRUE;
+ }
+
+ #endif // SVQC
+ #ifdef CSQC
+ float m_mage(float req)
+ {
+ switch(req)
+ {
+ case MR_PRECACHE:
+ {
+ precache_model ("models/monsters/mage.dpm");
+ return TRUE;
+ }
+ }
+
+ return TRUE;
+ }
+
+ #endif // CSQC
+ #endif // REGISTER_MONSTER
--- /dev/null
- RadiusDamage (self, self.realowner, (autocvar_g_monster_shambler_attack_lightning_damage), (autocvar_g_monster_shambler_attack_lightning_damage), (autocvar_g_monster_shambler_attack_lightning_radius), world, (autocvar_g_monster_shambler_attack_lightning_force), self.projectiledeathtype, other);
+ #ifdef REGISTER_MONSTER
+ REGISTER_MONSTER(
+ /* MON_##id */ SHAMBLER,
+ /* function */ m_shambler,
+ /* spawnflags */ MONSTER_SIZE_BROKEN | MON_FLAG_SUPERMONSTER | MON_FLAG_MELEE | MON_FLAG_RANGED,
+ /* mins,maxs */ '-41 -41 -31', '41 41 65',
+ /* model */ "shambler.mdl",
+ /* netname */ "shambler",
+ /* fullname */ _("Shambler")
+ );
+
+ #else
+ #ifdef SVQC
+ float autocvar_g_monster_shambler_health;
+ float autocvar_g_monster_shambler_attack_smash_damage;
+ float autocvar_g_monster_shambler_attack_claw_damage;
+ float autocvar_g_monster_shambler_attack_lightning_damage;
+ float autocvar_g_monster_shambler_attack_lightning_force;
+ float autocvar_g_monster_shambler_attack_lightning_radius;
+ float autocvar_g_monster_shambler_attack_lightning_radius_zap;
+ float autocvar_g_monster_shambler_attack_lightning_speed;
+ float autocvar_g_monster_shambler_attack_lightning_speed_up;
+ float autocvar_g_monster_shambler_speed_stop;
+ float autocvar_g_monster_shambler_speed_run;
+ float autocvar_g_monster_shambler_speed_walk;
+
+ const float shambler_anim_stand = 0;
+ const float shambler_anim_walk = 1;
+ const float shambler_anim_run = 2;
+ const float shambler_anim_smash = 3;
+ const float shambler_anim_swingr = 4;
+ const float shambler_anim_swingl = 5;
+ const float shambler_anim_magic = 6;
+ const float shambler_anim_pain = 7;
+ const float shambler_anim_death = 8;
+
+ .float shambler_lastattack; // delay attacks separately
+
+ void shambler_smash()
+ {
+ makevectors(self.angles);
+ pointparticles(particleeffectnum("explosion_medium"), (self.origin + (v_forward * 150)) - ('0 0 1' * self.maxs_z), '0 0 0', 1);
+ sound(self, CH_SHOTS, "weapons/rocket_impact.wav", VOL_BASE, ATTEN_NORM);
+
+ tracebox(self.origin + v_forward * 50, self.mins * 0.5, self.maxs * 0.5, self.origin + v_forward * 500, MOVE_NORMAL, self);
+
+ if(trace_ent.takedamage)
+ Damage(trace_ent, self, self, (autocvar_g_monster_shambler_attack_smash_damage) * Monster_SkillModifier(), DEATH_MONSTER_SHAMBLER_SMASH, trace_ent.origin, normalize(trace_ent.origin - self.origin));
+ }
+
+ void shambler_swing()
+ {
+ float r = (random() < 0.5);
+ monster_melee(self.enemy, (autocvar_g_monster_shambler_attack_claw_damage), ((r) ? shambler_anim_swingr : shambler_anim_swingl), self.attack_range, 0.8, DEATH_MONSTER_SHAMBLER_CLAW, TRUE);
+ if(r)
+ {
+ defer(0.5, shambler_swing);
+ self.attack_finished_single += 0.5;
+ }
+ }
+
+ void shambler_lightning_explode()
+ {
+ entity head;
+
+ sound(self, CH_SHOTS, "weapons/electro_impact.wav", VOL_BASE, ATTEN_NORM);
+ pointparticles(particleeffectnum("electro_impact"), '0 0 0', '0 0 0', 1);
+
+ self.event_damage = func_null;
+ self.takedamage = DAMAGE_NO;
+ self.movetype = MOVETYPE_NONE;
+ self.velocity = '0 0 0';
+
+ if(self.movetype == MOVETYPE_NONE)
+ self.velocity = self.oldvelocity;
+
- W_SetupProjectileVelocityEx(gren, v_forward, v_up, (autocvar_g_monster_shambler_attack_lightning_speed), (autocvar_g_monster_shambler_attack_lightning_speed_up), 0, 0, FALSE);
++ RadiusDamage (self, self.realowner, (autocvar_g_monster_shambler_attack_lightning_damage), (autocvar_g_monster_shambler_attack_lightning_damage), (autocvar_g_monster_shambler_attack_lightning_radius), world, world, (autocvar_g_monster_shambler_attack_lightning_force), self.projectiledeathtype, other);
+
+ for(head = findradius(self.origin, (autocvar_g_monster_shambler_attack_lightning_radius_zap)); head; head = head.chain) if(head != self.realowner) if(head.takedamage)
+ {
+ te_csqc_lightningarc(self.origin, head.origin);
+ Damage(head, self, self.realowner, (autocvar_g_monster_shambler_attack_lightning_damage) * Monster_SkillModifier(), DEATH_MONSTER_SHAMBLER_ZAP, head.origin, '0 0 0');
+ }
+
+ self.think = SUB_Remove;
+ self.nextthink = time + 0.2;
+ }
+
+ void shambler_lightning_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+ {
+ if (self.health <= 0)
+ return;
+
+ if (!W_CheckProjectileDamage(inflictor.realowner, self.realowner, deathtype, -1)) // no exceptions
+ return; // g_projectiles_damage says to halt
+
+ self.health = self.health - damage;
+
+ if (self.health <= 0)
+ W_PrepareExplosionByDamage(attacker, self.use);
+ }
+
+ void shambler_lightning_touch()
+ {
+ PROJECTILE_TOUCH;
+
+ self.use ();
+ }
+
+ void shambler_lightning_think()
+ {
+ self.nextthink = time;
+ if (time > self.cnt)
+ {
+ other = world;
+ shambler_lightning_explode();
+ return;
+ }
+ }
+
+ void shambler_lightning()
+ {
+ entity gren;
+
+ monster_makevectors(self.enemy);
+
+ gren = spawn ();
+ gren.owner = gren.realowner = self;
+ gren.classname = "grenade";
+ gren.bot_dodge = TRUE;
+ gren.bot_dodgerating = (autocvar_g_monster_shambler_attack_lightning_damage);
+ gren.movetype = MOVETYPE_BOUNCE;
+ PROJECTILE_MAKETRIGGER(gren);
+ gren.projectiledeathtype = DEATH_MONSTER_SHAMBLER_ZAP;
+ setorigin(gren, CENTER_OR_VIEWOFS(self));
+ setsize(gren, '-8 -8 -8', '8 8 8');
+ gren.scale = 2.5;
+
+ gren.cnt = time + 5;
+ gren.nextthink = time;
+ gren.think = shambler_lightning_think;
+ gren.use = shambler_lightning_explode;
+ gren.touch = shambler_lightning_touch;
+
+ gren.takedamage = DAMAGE_YES;
+ gren.health = 50;
+ gren.damageforcescale = 0;
+ gren.event_damage = shambler_lightning_damage;
+ gren.damagedbycontents = TRUE;
+ gren.missile_flags = MIF_SPLASH | MIF_ARC;
- self.weapon = WEP_NEX;
++ W_SetupProjVelocity_Explicit(gren, v_forward, v_up, (autocvar_g_monster_shambler_attack_lightning_speed), (autocvar_g_monster_shambler_attack_lightning_speed_up), 0, 0, FALSE);
+
+ gren.angles = vectoangles (gren.velocity);
+ gren.flags = FL_PROJECTILE;
+
+ CSQCProjectile(gren, TRUE, PROJECTILE_SHAMBLER_LIGHTNING, TRUE);
+ }
+
+ float shambler_attack(float attack_type)
+ {
+ switch(attack_type)
+ {
+ case MONSTER_ATTACK_MELEE:
+ {
+ shambler_swing();
+ return TRUE;
+ }
+ case MONSTER_ATTACK_RANGED:
+ {
+ if(time >= self.shambler_lastattack) // shambler doesn't attack much
+ if(random() <= 0.5 && vlen(self.enemy.origin - self.origin) <= 500)
+ {
+ self.frame = shambler_anim_smash;
+ defer(0.7, shambler_smash);
+ self.attack_finished_single = time + 1.1;
+ self.shambler_lastattack = time + 3;
+ return TRUE;
+ }
+ else if(random() <= 0.1) // small chance, don't want this spammed
+ {
+ self.frame = shambler_anim_magic;
+ self.attack_finished_single = time + 1.1;
+ self.shambler_lastattack = time + 3;
+ defer(0.6, shambler_lightning);
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+ }
+
+ return FALSE;
+ }
+
+ void spawnfunc_monster_shambler()
+ {
+ self.classname = "monster_shambler";
+
+ self.monster_spawnfunc = spawnfunc_monster_shambler;
+
+ if(Monster_CheckAppearFlags(self))
+ return;
+
+ if(!monster_initialize(MON_SHAMBLER, FALSE)) { remove(self); return; }
+ }
+
+ float m_shambler(float req)
+ {
+ switch(req)
+ {
+ case MR_THINK:
+ {
+ monster_move((autocvar_g_monster_shambler_speed_run), (autocvar_g_monster_shambler_speed_walk), (autocvar_g_monster_shambler_speed_stop), shambler_anim_run, shambler_anim_walk, shambler_anim_stand);
+ return TRUE;
+ }
+ case MR_DEATH:
+ {
+ self.frame = shambler_anim_death;
+ return TRUE;
+ }
+ case MR_SETUP:
+ {
+ if(!self.health) self.health = (autocvar_g_monster_shambler_health);
+ if(!self.attack_range) self.attack_range = 150;
+
+ self.monster_loot = spawnfunc_item_health_mega;
+ self.monster_attackfunc = shambler_attack;
+ self.frame = shambler_anim_stand;
++ self.weapon = WEP_VORTEX;
+
+ return TRUE;
+ }
+ case MR_PRECACHE:
+ {
+ precache_model ("models/monsters/shambler.mdl");
+ return TRUE;
+ }
+ }
+
+ return TRUE;
+ }
+
+ #endif // SVQC
+ #ifdef CSQC
+ float m_shambler(float req)
+ {
+ switch(req)
+ {
+ case MR_PRECACHE:
+ {
+ precache_model ("models/monsters/shambler.mdl");
+ return TRUE;
+ }
+ }
+
+ return TRUE;
+ }
+
+ #endif // CSQC
+ #endif // REGISTER_MONSTER
--- /dev/null
- RadiusDamage(self, self.realowner, 0, 0, 25, world, 25, self.projectiledeathtype, world);
+ #ifdef REGISTER_MONSTER
+ REGISTER_MONSTER(
+ /* MON_##id */ SPIDER,
+ /* function */ m_spider,
+ /* spawnflags */ MON_FLAG_MELEE | MON_FLAG_RANGED,
+ /* mins,maxs */ '-18 -18 -25', '18 18 30',
+ /* model */ "spider.dpm",
+ /* netname */ "spider",
+ /* fullname */ _("Spider")
+ );
+
+ #else
+ #ifdef SVQC
+ float autocvar_g_monster_spider_health;
+ float autocvar_g_monster_spider_attack_bite_damage;
+ float autocvar_g_monster_spider_attack_bite_delay;
+ float autocvar_g_monster_spider_attack_web_damagetime;
+ float autocvar_g_monster_spider_attack_web_speed;
+ float autocvar_g_monster_spider_attack_web_speed_up;
+ float autocvar_g_monster_spider_attack_web_delay;
+ float autocvar_g_monster_spider_speed_stop;
+ float autocvar_g_monster_spider_speed_run;
+ float autocvar_g_monster_spider_speed_walk;
+
+ const float spider_anim_idle = 0;
+ const float spider_anim_walk = 1;
+ const float spider_anim_attack = 2;
+ const float spider_anim_attack2 = 3;
+
+ .float spider_web_delay;
+
+ void spider_web_explode()
+ {
+ entity e;
+ if(self)
+ {
+ pointparticles(particleeffectnum("electro_impact"), self.origin, '0 0 0', 1);
- W_SetupProjectileVelocityEx(proj, v_forward, v_up, (autocvar_g_monster_spider_attack_web_speed), (autocvar_g_monster_spider_attack_web_speed_up), 0, 0, FALSE);
++ RadiusDamage(self, self.realowner, 0, 0, 25, world, world, 25, self.projectiledeathtype, world);
+
+ for(e = findradius(self.origin, 25); e; e = e.chain) if(e != self) if(e.takedamage && e.deadflag == DEAD_NO) if(e.health > 0)
+ e.spider_slowness = time + (autocvar_g_monster_spider_attack_web_damagetime);
+
+ remove(self);
+ }
+ }
+
+ void spider_web_touch()
+ {
+ PROJECTILE_TOUCH;
+
+ spider_web_explode();
+ }
+
+ void spider_shootweb()
+ {
+ monster_makevectors(self.enemy);
+
+ sound(self, CH_SHOTS, "weapons/electro_fire2.wav", VOL_BASE, ATTEN_NORM);
+
+ entity proj = spawn ();
+ proj.classname = "plasma";
+ proj.owner = proj.realowner = self;
+ proj.use = spider_web_touch;
+ proj.think = adaptor_think2use_hittype_splash;
+ proj.bot_dodge = TRUE;
+ proj.bot_dodgerating = 0;
+ proj.nextthink = time + 5;
+ PROJECTILE_MAKETRIGGER(proj);
+ proj.projectiledeathtype = DEATH_MONSTER_SPIDER;
+ setorigin(proj, CENTER_OR_VIEWOFS(self));
+
+ //proj.glow_size = 50;
+ //proj.glow_color = 45;
+ proj.movetype = MOVETYPE_BOUNCE;
++ W_SetupProjVelocity_Explicit(proj, v_forward, v_up, (autocvar_g_monster_spider_attack_web_speed), (autocvar_g_monster_spider_attack_web_speed_up), 0, 0, FALSE);
+ proj.touch = spider_web_touch;
+ setsize(proj, '-4 -4 -4', '4 4 4');
+ proj.takedamage = DAMAGE_NO;
+ proj.damageforcescale = 0;
+ proj.health = 500;
+ proj.event_damage = func_null;
+ proj.flags = FL_PROJECTILE;
+ proj.damagedbycontents = TRUE;
+
+ proj.bouncefactor = 0.3;
+ proj.bouncestop = 0.05;
+ proj.missile_flags = MIF_SPLASH | MIF_ARC;
+
+ CSQCProjectile(proj, TRUE, PROJECTILE_ELECTRO, TRUE);
+ }
+
+ float spider_attack(float attack_type)
+ {
+ switch(attack_type)
+ {
+ case MONSTER_ATTACK_MELEE:
+ {
+ return monster_melee(self.enemy, (autocvar_g_monster_spider_attack_bite_damage), ((random() > 0.5) ? spider_anim_attack : spider_anim_attack2), self.attack_range, (autocvar_g_monster_spider_attack_bite_delay), DEATH_MONSTER_SPIDER, TRUE);
+ }
+ case MONSTER_ATTACK_RANGED:
+ {
+ if(time >= self.spider_web_delay)
+ {
+ self.frame = spider_anim_attack2;
+ self.attack_finished_single = time + (autocvar_g_monster_spider_attack_web_delay);
+ spider_shootweb();
+ self.spider_web_delay = time + 3;
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+ }
+
+ return FALSE;
+ }
+
+ void spawnfunc_monster_spider()
+ {
+ self.classname = "monster_spider";
+
+ self.monster_spawnfunc = spawnfunc_monster_spider;
+
+ if(Monster_CheckAppearFlags(self))
+ return;
+
+ if(!monster_initialize(MON_SPIDER, FALSE)) { remove(self); return; }
+ }
+
+ float m_spider(float req)
+ {
+ switch(req)
+ {
+ case MR_THINK:
+ {
+ monster_move((autocvar_g_monster_spider_speed_run), (autocvar_g_monster_spider_speed_walk), (autocvar_g_monster_spider_speed_stop), spider_anim_walk, spider_anim_walk, spider_anim_idle);
+ return TRUE;
+ }
+ case MR_DEATH:
+ {
+ self.frame = spider_anim_attack;
+ self.angles_x = 180;
+ return TRUE;
+ }
+ case MR_SETUP:
+ {
+ if(!self.health) self.health = (autocvar_g_monster_spider_health);
+
+ self.monster_loot = spawnfunc_item_health_medium;
+ self.monster_attackfunc = spider_attack;
+ self.frame = spider_anim_idle;
+
+ return TRUE;
+ }
+ case MR_PRECACHE:
+ {
+ precache_model ("models/monsters/spider.dpm");
+ precache_sound ("weapons/electro_fire2.wav");
+ return TRUE;
+ }
+ }
+
+ return TRUE;
+ }
+
+ #endif // SVQC
+ #ifdef CSQC
+ float m_spider(float req)
+ {
+ switch(req)
+ {
+ case MR_PRECACHE:
+ {
+ precache_model ("models/monsters/spider.dpm");
+ return TRUE;
+ }
+ }
+
+ return TRUE;
+ }
+
+ #endif // CSQC
+ #endif // REGISTER_MONSTER
--- /dev/null
- RadiusDamage(self, self.realowner, (autocvar_g_monster_wyvern_attack_fireball_damage), (autocvar_g_monster_wyvern_attack_fireball_edgedamage), (autocvar_g_monster_wyvern_attack_fireball_force), world, (autocvar_g_monster_wyvern_attack_fireball_radius), self.projectiledeathtype, world);
+ #ifdef REGISTER_MONSTER
+ REGISTER_MONSTER(
+ /* MON_##id */ WYVERN,
+ /* function */ m_wyvern,
+ /* spawnflags */ MONSTER_TYPE_FLY | MONSTER_SIZE_BROKEN | MON_FLAG_RANGED,
+ /* mins,maxs */ '-20 -20 -58', '20 20 20',
+ /* model */ "wizard.mdl",
+ /* netname */ "wyvern",
+ /* fullname */ _("Wyvern")
+ );
+
+ #else
+ #ifdef SVQC
+ float autocvar_g_monster_wyvern_health;
+ float autocvar_g_monster_wyvern_attack_fireball_damage;
+ float autocvar_g_monster_wyvern_attack_fireball_edgedamage;
+ float autocvar_g_monster_wyvern_attack_fireball_damagetime;
+ float autocvar_g_monster_wyvern_attack_fireball_force;
+ float autocvar_g_monster_wyvern_attack_fireball_radius;
+ float autocvar_g_monster_wyvern_attack_fireball_speed;
+ float autocvar_g_monster_wyvern_speed_stop;
+ float autocvar_g_monster_wyvern_speed_run;
+ float autocvar_g_monster_wyvern_speed_walk;
+
+ const float wyvern_anim_hover = 0;
+ const float wyvern_anim_fly = 1;
+ const float wyvern_anim_magic = 2;
+ const float wyvern_anim_pain = 3;
+ const float wyvern_anim_death = 4;
+
+ void wyvern_fireball_explode()
+ {
+ entity e;
+ if(self)
+ {
+ pointparticles(particleeffectnum("fireball_explode"), self.origin, '0 0 0', 1);
+
++ RadiusDamage(self, self.realowner, (autocvar_g_monster_wyvern_attack_fireball_damage), (autocvar_g_monster_wyvern_attack_fireball_edgedamage), (autocvar_g_monster_wyvern_attack_fireball_force), world, world, (autocvar_g_monster_wyvern_attack_fireball_radius), self.projectiledeathtype, world);
+
+ for(e = world; (e = findfloat(e, takedamage, DAMAGE_AIM)); ) if(vlen(e.origin - self.origin) <= (autocvar_g_monster_wyvern_attack_fireball_radius))
+ Fire_AddDamage(e, self, 5 * Monster_SkillModifier(), (autocvar_g_monster_wyvern_attack_fireball_damagetime), self.projectiledeathtype);
+
+ remove(self);
+ }
+ }
+
+ void wyvern_fireball_touch()
+ {
+ PROJECTILE_TOUCH;
+
+ wyvern_fireball_explode();
+ }
+
+ void wyvern_fireball()
+ {
+ entity missile = spawn();
+ vector dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
+
+ monster_makevectors(self.enemy);
+
+ missile.owner = missile.realowner = self;
+ missile.solid = SOLID_TRIGGER;
+ missile.movetype = MOVETYPE_FLYMISSILE;
+ missile.projectiledeathtype = DEATH_MONSTER_WYVERN;
+ setsize(missile, '-6 -6 -6', '6 6 6');
+ setorigin(missile, self.origin + self.view_ofs + v_forward * 14);
+ missile.flags = FL_PROJECTILE;
+ missile.velocity = dir * (autocvar_g_monster_wyvern_attack_fireball_speed);
+ missile.avelocity = '300 300 300';
+ missile.nextthink = time + 5;
+ missile.think = wyvern_fireball_explode;
+ missile.enemy = self.enemy;
+ missile.touch = wyvern_fireball_touch;
+ CSQCProjectile(missile, TRUE, PROJECTILE_FIREMINE, TRUE);
+ }
+
+ float wyvern_attack(float attack_type)
+ {
+ switch(attack_type)
+ {
+ case MONSTER_ATTACK_MELEE:
+ case MONSTER_ATTACK_RANGED:
+ {
+ self.attack_finished_single = time + 1.2;
+
+ wyvern_fireball();
+
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+ }
+
+ void spawnfunc_monster_wyvern()
+ {
+ self.classname = "monster_wyvern";
+
+ self.monster_spawnfunc = spawnfunc_monster_wyvern;
+
+ if(Monster_CheckAppearFlags(self))
+ return;
+
+ if(!monster_initialize(MON_WYVERN, TRUE)) { remove(self); return; }
+ }
+
+ // compatibility with old spawns
+ void spawnfunc_monster_wizard() { spawnfunc_monster_wyvern(); }
+
+ float m_wyvern(float req)
+ {
+ switch(req)
+ {
+ case MR_THINK:
+ {
+ monster_move((autocvar_g_monster_wyvern_speed_run), (autocvar_g_monster_wyvern_speed_walk), (autocvar_g_monster_wyvern_speed_stop), wyvern_anim_fly, wyvern_anim_hover, wyvern_anim_hover);
+ return TRUE;
+ }
+ case MR_DEATH:
+ {
+ self.frame = wyvern_anim_death;
+ self.velocity_x = -200 + 400 * random();
+ self.velocity_y = -200 + 400 * random();
+ self.velocity_z = 100 + 100 * random();
+ return TRUE;
+ }
+ case MR_SETUP:
+ {
+ if(!self.health) self.health = (autocvar_g_monster_wyvern_health);
+
+ self.monster_loot = spawnfunc_item_cells;
+ self.monster_attackfunc = wyvern_attack;
+ self.frame = wyvern_anim_hover;
+
+ return TRUE;
+ }
+ case MR_PRECACHE:
+ {
+ precache_model ("models/monsters/wizard.mdl");
+ return TRUE;
+ }
+ }
+
+ return TRUE;
+ }
+
+ #endif // SVQC
+ #ifdef CSQC
+ float m_wyvern(float req)
+ {
+ switch(req)
+ {
+ case MR_PRECACHE:
+ {
+ precache_model ("models/monsters/wizard.mdl");
+ return TRUE;
+ }
+ }
+
+ return TRUE;
+ }
+
+ #endif // CSQC
+ #endif // REGISTER_MONSTER
--- /dev/null
- self.weapon = WEP_NEX;
+ // =========================
+ // SVQC Monster Properties
+ // =========================
+
+
+ void monster_item_spawn()
+ {
+ if(self.monster_loot)
+ self.monster_loot();
+
+ self.gravity = 1;
+ self.reset = SUB_Remove;
+ self.noalign = TRUE;
+ self.velocity = randomvec() * 175 + '0 0 325';
+ self.classname = "droppedweapon"; // hax
+ self.item_spawnshieldtime = time + 0.7;
+
+ SUB_SetFade(self, time + autocvar_g_monsters_drop_time, 1);
+ }
+
+ void monster_dropitem()
+ {
+ if(!self.candrop || !self.monster_loot)
+ return;
+
+ vector org = self.origin + ((self.mins + self.maxs) * 0.5);
+ entity e = spawn();
+
+ setorigin(e, org);
+
+ e.monster_loot = self.monster_loot;
+
+ other = e;
+ MUTATOR_CALLHOOK(MonsterDropItem);
+ e = other;
+
+ if(e)
+ {
+ e.think = monster_item_spawn;
+ e.nextthink = time + 0.3;
+ }
+ }
+
+ float Monster_SkillModifier()
+ {
+ float t = 0.5+self.monster_skill*((1.2-0.3)/10);
+
+ return t;
+ }
+
+ float monster_isvalidtarget (entity targ, entity ent)
+ {
+ if(!targ || !ent)
+ return FALSE; // someone doesn't exist
+
+ if(targ == ent)
+ return FALSE; // don't attack ourselves
+
+ traceline(ent.origin, targ.origin, MOVE_NORMAL, ent);
+
+ if(trace_ent != targ)
+ return FALSE;
+
+ if(targ.vehicle_flags & VHF_ISVEHICLE)
+ if(!((get_monsterinfo(ent.monsterid)).spawnflags & MON_FLAG_RANGED))
+ return FALSE; // melee attacks are useless against vehicles
+
+ if(time < game_starttime)
+ return FALSE; // monsters do nothing before the match has started
+
+ if(vlen(targ.origin - ent.origin) >= ent.target_range)
+ return FALSE; // enemy is too far away
+
+ if(targ.takedamage == DAMAGE_NO)
+ return FALSE; // enemy can't be damaged
+
+ if(targ.items & IT_INVISIBILITY)
+ return FALSE; // enemy is invisible
+
+ if(substring(targ.classname, 0, 10) == "onslaught_")
+ return FALSE; // don't attack onslaught targets
+
+ if(IS_SPEC(targ) || IS_OBSERVER(targ))
+ return FALSE; // enemy is a spectator
+
+ if(!(targ.vehicle_flags & VHF_ISVEHICLE))
+ if(targ.deadflag != DEAD_NO || ent.deadflag != DEAD_NO || targ.health <= 0 || ent.health <= 0)
+ return FALSE; // enemy/self is dead
+
+ if(ent.monster_owner == targ)
+ return FALSE; // don't attack our master
+
+ if(targ.monster_owner == ent)
+ return FALSE; // don't attack our pet
+
+ if(!(targ.vehicle_flags & VHF_ISVEHICLE))
+ if(targ.flags & FL_NOTARGET)
+ return FALSE; // enemy can't be targeted
+
+ if(!autocvar_g_monsters_typefrag)
+ if(targ.BUTTON_CHAT)
+ return FALSE; // no typefragging!
+
+ if(SAME_TEAM(targ, ent))
+ return FALSE; // enemy is on our team
+
+ if (targ.freezetag_frozen)
+ return FALSE; // ignore frozen
+
+ if(autocvar_g_monsters_target_infront || ent.spawnflags & MONSTERFLAG_INFRONT)
+ if(ent.enemy != targ)
+ {
+ float dot;
+
+ makevectors (ent.angles);
+ dot = normalize (targ.origin - ent.origin) * v_forward;
+
+ if(dot <= 0.3)
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+ entity FindTarget (entity ent)
+ {
+ if(MUTATOR_CALLHOOK(MonsterFindTarget)) { return ent.enemy; } // Handled by a mutator
+
+ entity head, closest_target = world;
+ head = findradius(ent.origin, ent.target_range);
+
+ while(head) // find the closest acceptable target to pass to
+ {
+ if(head.monster_attack)
+ if(monster_isvalidtarget(head, ent))
+ {
+ // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
+ vector head_center = CENTER_OR_VIEWOFS(head);
+ vector ent_center = CENTER_OR_VIEWOFS(ent);
+
+ //if(ctf_CheckPassDirection(head_center, ent_center, ent.v_angle, head.WarpZone_findradius_nearest))
+ if(closest_target)
+ {
+ vector closest_target_center = CENTER_OR_VIEWOFS(closest_target);
+ if(vlen(ent_center - head_center) < vlen(ent_center - closest_target_center))
+ { closest_target = head; }
+ }
+ else { closest_target = head; }
+ }
+
+ head = head.chain;
+ }
+
+ return closest_target;
+ }
+
+ void MonsterTouch ()
+ {
+ if(other == world)
+ return;
+
+ if(self.enemy != other)
+ if(!(other.flags & FL_MONSTER))
+ if(monster_isvalidtarget(other, self))
+ self.enemy = other;
+ }
+
+ string get_monster_model_datafilename(string m, float sk, string fil)
+ {
+ if(m)
+ m = strcat(m, "_");
+ else
+ m = "models/monsters/*_";
+ if(sk >= 0)
+ m = strcat(m, ftos(sk));
+ else
+ m = strcat(m, "*");
+ return strcat(m, ".", fil);
+ }
+
+ void PrecacheMonsterSounds(string f)
+ {
+ float fh;
+ string s;
+ fh = fopen(f, FILE_READ);
+ if(fh < 0)
+ return;
+ while((s = fgets(fh)))
+ {
+ if(tokenize_console(s) != 3)
+ {
+ dprint("Invalid sound info line: ", s, "\n");
+ continue;
+ }
+ PrecacheGlobalSound(strcat(argv(1), " ", argv(2)));
+ }
+ fclose(fh);
+ }
+
+ void precache_monstersounds()
+ {
+ string m = (get_monsterinfo(self.monsterid)).model;
+ float globhandle, n, i;
+ string f;
+
+ globhandle = search_begin(strcat(m, "_*.sounds"), TRUE, FALSE);
+ if (globhandle < 0)
+ return;
+ n = search_getsize(globhandle);
+ for (i = 0; i < n; ++i)
+ {
+ //print(search_getfilename(globhandle, i), "\n");
+ f = search_getfilename(globhandle, i);
+ PrecacheMonsterSounds(f);
+ }
+ search_end(globhandle);
+ }
+
+ void ClearMonsterSounds()
+ {
+ #define _MSOUND(m) if(self.monstersound_##m) { strunzone(self.monstersound_##m); self.monstersound_##m = string_null; }
+ ALLMONSTERSOUNDS
+ #undef _MSOUND
+ }
+
+ .string GetMonsterSoundSampleField(string type)
+ {
+ GetMonsterSoundSampleField_notFound = 0;
+ switch(type)
+ {
+ #define _MSOUND(m) case #m: return monstersound_##m;
+ ALLMONSTERSOUNDS
+ #undef _MSOUND
+ }
+ GetMonsterSoundSampleField_notFound = 1;
+ return string_null;
+ }
+
+ float LoadMonsterSounds(string f, float first)
+ {
+ float fh;
+ string s;
+ var .string field;
+ fh = fopen(f, FILE_READ);
+ if(fh < 0)
+ {
+ dprint("Monster sound file not found: ", f, "\n");
+ return 0;
+ }
+ while((s = fgets(fh)))
+ {
+ if(tokenize_console(s) != 3)
+ continue;
+ field = GetMonsterSoundSampleField(argv(0));
+ if(GetMonsterSoundSampleField_notFound)
+ continue;
+ if(self.field)
+ strunzone(self.field);
+ self.field = strzone(strcat(argv(1), " ", argv(2)));
+ }
+ fclose(fh);
+ return 1;
+ }
+
+ .float skin_for_monstersound;
+ void UpdateMonsterSounds()
+ {
+ entity mon = get_monsterinfo(self.monsterid);
+
+ if(self.skin == self.skin_for_monstersound)
+ return;
+ self.skin_for_monstersound = self.skin;
+ ClearMonsterSounds();
+ //LoadMonsterSounds("sound/monsters/default.sounds", 1);
+ if(!autocvar_g_debug_defaultsounds)
+ if(!LoadMonsterSounds(get_monster_model_datafilename(mon.model, self.skin, "sounds"), 0))
+ LoadMonsterSounds(get_monster_model_datafilename(mon.model, 0, "sounds"), 0);
+ }
+
+ void MonsterSound(.string samplefield, float sound_delay, float delaytoo, float chan)
+ {
+ if(delaytoo && time < self.msound_delay)
+ return; // too early
+ GlobalSound(self.samplefield, chan, VOICETYPE_PLAYERSOUND);
+
+ self.msound_delay = time + sound_delay;
+ }
+
+ void monster_makevectors(entity e)
+ {
+ vector v;
+
+ v = e.origin + (e.mins + e.maxs) * 0.5;
+ self.v_angle = vectoangles(v - (self.origin + self.view_ofs));
+ self.v_angle_x = -self.v_angle_x;
+
+ makevectors(self.v_angle);
+ }
+
+ float monster_melee(entity targ, float damg, float anim, float er, float anim_finished, float deathtype, float dostop)
+ {
+ if (self.health <= 0)
+ return FALSE; // attacking while dead?!
+
+ if(dostop)
+ {
+ self.velocity_x = 0;
+ self.velocity_y = 0;
+ self.state = MONSTER_STATE_ATTACK_MELEE;
+ }
+
+ self.frame = anim;
+
+ if(anim_finished != 0)
+ self.attack_finished_single = time + anim_finished;
+
+ monster_makevectors(targ);
+
+ traceline(self.origin + self.view_ofs, self.origin + v_forward * er, 0, self);
+
+ if(trace_ent.takedamage)
+ Damage(trace_ent, self, self, damg * Monster_SkillModifier(), deathtype, trace_ent.origin, normalize(trace_ent.origin - self.origin));
+
+ return TRUE;
+ }
+
+ void Monster_CheckMinibossFlag ()
+ {
+ if(MUTATOR_CALLHOOK(MonsterCheckBossFlag))
+ return;
+
+ float chance = random() * 100;
+
+ // g_monsters_miniboss_chance cvar or spawnflags 64 causes a monster to be a miniboss
+ if ((self.spawnflags & MONSTERFLAG_MINIBOSS) || (chance < autocvar_g_monsters_miniboss_chance))
+ {
+ self.health += autocvar_g_monsters_miniboss_healthboost;
+ self.effects |= EF_RED;
+ if(!self.weapon)
++ self.weapon = WEP_VORTEX;
+ }
+ }
+
+ float Monster_CanRespawn(entity ent)
+ {
+ other = ent;
+ if(ent.deadflag == DEAD_DEAD) // don't call when monster isn't dead
+ if(MUTATOR_CALLHOOK(MonsterRespawn))
+ return TRUE; // enabled by a mutator
+
+ if(ent.spawnflags & MONSTERFLAG_NORESPAWN)
+ return FALSE;
+
+ if(!autocvar_g_monsters_respawn)
+ return FALSE;
+
+ return TRUE;
+ }
+
+ void Monster_Fade ()
+ {
+ if(Monster_CanRespawn(self))
+ {
+ self.spawnflags |= MONSTERFLAG_RESPAWNED;
+ self.think = self.monster_spawnfunc;
+ self.nextthink = time + self.respawntime;
+ self.ltime = 0;
+ self.deadflag = DEAD_RESPAWNING;
+ if(self.spawnflags & MONSTER_RESPAWN_DEATHPOINT)
+ {
+ self.pos1 = self.origin;
+ self.pos2 = self.angles;
+ }
+ self.event_damage = func_null;
+ self.takedamage = DAMAGE_NO;
+ setorigin(self, self.pos1);
+ self.angles = self.pos2;
+ self.health = self.max_health;
+ setmodel(self, "null");
+ }
+ else
+ {
+ // number of monsters spawned with mobspawn command
+ totalspawned -= 1;
+
+ if(IS_CLIENT(self.realowner))
+ self.realowner.monstercount -= 1;
+
+ SUB_SetFade(self, time + 3, 1);
+ }
+ }
+
+ float Monster_CanJump (vector vel)
+ {
+ if(self.state)
+ return FALSE; // already attacking
+ if(!(self.flags & FL_ONGROUND))
+ return FALSE; // not on the ground
+ if(self.health <= 0)
+ return FALSE; // called when dead?
+ if(time < self.attack_finished_single)
+ return FALSE; // still attacking
+
+ vector old = self.velocity;
+
+ self.velocity = vel;
+ tracetoss(self, self);
+ self.velocity = old;
+ if (trace_ent != self.enemy)
+ return FALSE;
+
+ return TRUE;
+ }
+
+ float monster_leap (float anm, void() touchfunc, vector vel, float anim_finished)
+ {
+ if(!Monster_CanJump(vel))
+ return FALSE;
+
+ self.frame = anm;
+ self.state = MONSTER_STATE_ATTACK_LEAP;
+ self.touch = touchfunc;
+ self.origin_z += 1;
+ self.velocity = vel;
+ self.flags &= ~FL_ONGROUND;
+
+ self.attack_finished_single = time + anim_finished;
+
+ return TRUE;
+ }
+
+ void monster_checkattack(entity e, entity targ)
+ {
+ if(e == world)
+ return;
+ if(targ == world)
+ return;
+
+ if(!e.monster_attackfunc)
+ return;
+
+ if(time < e.attack_finished_single)
+ return;
+
+ if(vlen(targ.origin - e.origin) <= e.attack_range)
+ if(e.monster_attackfunc(MONSTER_ATTACK_MELEE))
+ {
+ MonsterSound(monstersound_melee, 0, FALSE, CH_VOICE);
+ return;
+ }
+
+ if(vlen(targ.origin - e.origin) > e.attack_range)
+ if(e.monster_attackfunc(MONSTER_ATTACK_RANGED))
+ {
+ MonsterSound(monstersound_ranged, 0, FALSE, CH_VOICE);
+ return;
+ }
+ }
+
+ void monster_use ()
+ {
+ if(!self.enemy)
+ if(self.health > 0)
+ if(monster_isvalidtarget(activator, self))
+ self.enemy = activator;
+ }
+
+ .float last_trace;
+ .float last_enemycheck; // for checking enemy
+ vector monster_pickmovetarget(entity targ)
+ {
+ // enemy is always preferred target
+ if(self.enemy)
+ {
+ makevectors(self.angles);
+ self.monster_movestate = MONSTER_MOVE_ENEMY;
+ self.last_trace = time + 1.2;
+ return self.enemy.origin;
+ }
+
+ switch(self.monster_moveflags)
+ {
+ case MONSTER_MOVE_OWNER:
+ {
+ self.monster_movestate = MONSTER_MOVE_OWNER;
+ self.last_trace = time + 0.3;
+ return (self.monster_owner) ? self.monster_owner.origin : self.origin;
+ }
+ case MONSTER_MOVE_SPAWNLOC:
+ {
+ self.monster_movestate = MONSTER_MOVE_SPAWNLOC;
+ self.last_trace = time + 2;
+ return self.pos1;
+ }
+ case MONSTER_MOVE_NOMOVE:
+ {
+ self.monster_movestate = MONSTER_MOVE_NOMOVE;
+ self.last_trace = time + 2;
+ return self.origin;
+ }
+ default:
+ case MONSTER_MOVE_WANDER:
+ {
+ vector pos;
+ self.monster_movestate = MONSTER_MOVE_WANDER;
+ self.last_trace = time + 2;
+
+ self.angles_y = rint(random() * 500);
+ makevectors(self.angles);
+ pos = self.origin + v_forward * 600;
+
+ if(self.flags & FL_FLY || self.flags & FL_SWIM)
+ if(self.spawnflags & MONSTERFLAG_FLY_VERTICAL)
+ {
+ pos_z = random() * 200;
+ if(random() >= 0.5)
+ pos_z *= -1;
+ }
+
+ if(targ)
+ {
+ self.last_trace = time + 0.5;
+ pos = targ.origin;
+ }
+
+ return pos;
+ }
+ }
+ }
+
+ void monster_move(float runspeed, float walkspeed, float stopspeed, float manim_run, float manim_walk, float manim_idle)
+ {
+ fixedmakevectors(self.angles);
+
+ if(self.target2)
+ self.goalentity = find(world, targetname, self.target2);
+
+ entity targ;
+
+ if(self.flags & FL_SWIM)
+ {
+ if(self.waterlevel < WATERLEVEL_WETFEET)
+ {
+ if(time >= self.last_trace)
+ {
+ self.fish_wasdrowning = TRUE;
+ self.last_trace = time + 0.4;
+
+ Damage (self, world, world, 2, DEATH_DROWN, self.origin, '0 0 0');
+ self.angles = '90 90 0';
+ if(random() < 0.5)
+ {
+ self.velocity_y += random() * 50;
+ self.velocity_x -= random() * 50;
+ }
+ else
+ {
+ self.velocity_y -= random() * 50;
+ self.velocity_x += random() * 50;
+ }
+ self.velocity_z += random() * 150;
+ }
+
+
+ self.movetype = MOVETYPE_BOUNCE;
+ //self.velocity_z = -200;
+
+ return;
+ }
+ else if(self.fish_wasdrowning)
+ {
+ self.fish_wasdrowning = FALSE;
+ self.angles_x = 0;
+ self.movetype = MOVETYPE_WALK;
+ }
+ }
+
+ targ = self.goalentity;
+
+ monster_target = targ;
+ monster_speed_run = runspeed;
+ monster_speed_walk = walkspeed;
+
+ if(MUTATOR_CALLHOOK(MonsterMove) || gameover || (round_handler_IsActive() && !round_handler_IsRoundStarted()) || time < game_starttime || (autocvar_g_campaign && !campaign_bots_may_start) || time < self.spawn_time)
+ {
+ runspeed = walkspeed = 0;
+ if(time >= self.spawn_time)
+ self.frame = manim_idle;
+ movelib_beak_simple(stopspeed);
+ return;
+ }
+
+ targ = monster_target;
+ runspeed = bound(0, monster_speed_run * Monster_SkillModifier(), runspeed * 2); // limit maxspeed to prevent craziness
+ walkspeed = bound(0, monster_speed_walk * Monster_SkillModifier(), walkspeed * 2); // limit maxspeed to prevent craziness
+
+ if(time < self.spider_slowness)
+ {
+ runspeed *= 0.5;
+ walkspeed *= 0.5;
+ }
+
+ if(teamplay)
+ if(autocvar_g_monsters_teams)
+ if(DIFF_TEAM(self.monster_owner, self))
+ self.monster_owner = world;
+
+ if(self.enemy && self.enemy.health < 1)
+ self.enemy = world; // enough!
+
+ if(time >= self.last_enemycheck)
+ {
+ if(!monster_isvalidtarget(self.enemy, self))
+ self.enemy = world;
+
+ if(!self.enemy)
+ {
+ self.enemy = FindTarget(self);
+ if(self.enemy)
+ MonsterSound(monstersound_sight, 0, FALSE, CH_VOICE);
+ }
+
+ self.last_enemycheck = time + 0.5;
+ }
+
+ if(self.state == MONSTER_STATE_ATTACK_MELEE && time >= self.attack_finished_single)
+ self.state = 0;
+
+ if(self.state != MONSTER_STATE_ATTACK_MELEE) // don't move if set
+ if(time >= self.last_trace || self.enemy) // update enemy instantly
+ self.moveto = monster_pickmovetarget(targ);
+
+ if(!self.enemy)
+ MonsterSound(monstersound_idle, 7, TRUE, CH_VOICE);
+
+ if(self.state != MONSTER_STATE_ATTACK_LEAP && self.state != MONSTER_STATE_ATTACK_MELEE)
+ self.steerto = steerlib_attract2(self.moveto, 0.5, 500, 0.95);
+
+ if(self.state == MONSTER_STATE_ATTACK_LEAP && (self.flags & FL_ONGROUND))
+ {
+ self.state = 0;
+ self.touch = MonsterTouch;
+ }
+
+ //self.steerto = steerlib_attract2(self.moveto, 0.5, 500, 0.95);
+
+ float turny = 0;
+ vector real_angle = vectoangles(self.steerto) - self.angles;
+
+ if(self.state != MONSTER_STATE_ATTACK_LEAP && self.state != MONSTER_STATE_ATTACK_MELEE)
+ turny = 20;
+
+ if(self.flags & FL_SWIM)
+ turny = vlen(self.angles - self.moveto);
+
+ if(turny)
+ {
+ turny = bound(turny * -1, shortangle_f(real_angle_y, self.angles_y), turny);
+ self.angles_y += turny;
+ }
+
+ if(self.state == MONSTER_STATE_ATTACK_MELEE)
+ self.moveto = self.origin;
+
+ if(self.enemy && self.enemy.vehicle)
+ runspeed = 0;
+
+ if(((self.flags & FL_FLY) || (self.flags & FL_SWIM)) && self.spawnflags & MONSTERFLAG_FLY_VERTICAL)
+ v_forward = normalize(self.moveto - self.origin);
+ else
+ self.moveto_z = self.origin_z;
+
+ if(vlen(self.origin - self.moveto) > 64)
+ {
+ if(self.flags & FL_FLY || self.flags & FL_SWIM)
+ movelib_move_simple(v_forward, ((self.enemy) ? runspeed : walkspeed), 0.6);
+ else
+ movelib_move_simple_gravity(v_forward, ((self.enemy) ? runspeed : walkspeed), 0.6);
+
+ if(time > self.pain_finished)
+ if(time > self.attack_finished_single)
+ if(vlen(self.velocity) > 10)
+ self.frame = ((self.enemy) ? manim_run : manim_walk);
+ else
+ self.frame = manim_idle;
+ }
+ else
+ {
+ entity e = find(world, targetname, self.target2);
+ if(e.target2)
+ self.target2 = e.target2;
+ else if(e.target)
+ self.target2 = e.target;
+
+ movelib_beak_simple(stopspeed);
+ if(time > self.attack_finished_single)
+ if(time > self.pain_finished)
+ if (vlen(self.velocity) <= 30)
+ self.frame = manim_idle;
+ }
+
+ monster_checkattack(self, self.enemy);
+ }
+
+ void monster_remove(entity mon)
+ {
+ if(!mon)
+ return; // nothing to remove
+
+ pointparticles(particleeffectnum("item_pickup"), mon.origin, '0 0 0', 1);
+
+ if(mon.weaponentity)
+ remove(mon.weaponentity);
+
+ WaypointSprite_Kill(mon.sprite);
+
+ remove(mon);
+ }
+
+ void monster_dead_think()
+ {
+ self.nextthink = time + self.ticrate;
+
+ CSQCMODEL_AUTOUPDATE();
+
+ if(self.ltime != 0)
+ if(time >= self.ltime)
+ {
+ Monster_Fade();
+ return;
+ }
+ }
+
+ void monsters_setstatus()
+ {
+ self.stat_monsters_total = monsters_total;
+ self.stat_monsters_killed = monsters_killed;
+ }
+
+ void Monster_Appear()
+ {
+ self.enemy = activator;
+ self.spawnflags &= ~MONSTERFLAG_APPEAR;
+ self.monster_spawnfunc();
+ }
+
+ float Monster_CheckAppearFlags(entity ent)
+ {
+ if(!(ent.spawnflags & MONSTERFLAG_APPEAR))
+ return FALSE;
+
+ ent.think = func_null;
+ ent.nextthink = 0;
+ ent.use = Monster_Appear;
+ ent.flags = FL_MONSTER; // set so this monster can get butchered
+
+ return TRUE;
+ }
+
+ void monsters_reset()
+ {
+ setorigin(self, self.pos1);
+ self.angles = self.pos2;
+
+ self.health = self.max_health;
+ self.velocity = '0 0 0';
+ self.enemy = world;
+ self.goalentity = world;
+ self.attack_finished_single = 0;
+ self.moveto = self.origin;
+ }
+
+ void monsters_corpse_damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+ {
+ self.health -= damage;
+
+ Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, self, attacker);
+
+ if(self.health <= -100) // 100 health until gone?
+ {
+ Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, self, attacker);
+
+ // number of monsters spawned with mobspawn command
+ totalspawned -= 1;
+
+ if(IS_CLIENT(self.realowner))
+ self.realowner.monstercount -= 1;
+
+ self.think = SUB_Remove;
+ self.nextthink = time + 0.1;
+ }
+ }
+
+ void monster_die(entity attacker, float gibbed)
+ {
+ self.think = monster_dead_think;
+ self.nextthink = time;
+ self.ltime = time + 5;
+
+ monster_dropitem();
+
+ MonsterSound(monstersound_death, 0, FALSE, CH_VOICE);
+
+ if(!(self.spawnflags & MONSTERFLAG_SPAWNED) && !(self.spawnflags & MONSTERFLAG_RESPAWNED))
+ monsters_killed += 1;
+
+ if(IS_PLAYER(attacker))
+ if(autocvar_g_monsters_score_spawned || !((self.spawnflags & MONSTERFLAG_SPAWNED) || (self.spawnflags & MONSTERFLAG_RESPAWNED)))
+ PlayerScore_Add(attacker, SP_SCORE, +autocvar_g_monsters_score_kill);
+
+ if(gibbed)
+ {
+ // number of monsters spawned with mobspawn command
+ totalspawned -= 1;
+
+ if(IS_CLIENT(self.realowner))
+ self.realowner.monstercount -= 1;
+ }
+
+ if(self.candrop && self.weapon)
+ W_ThrowNewWeapon(self, self.weapon, 0, self.origin, randomvec() * 150 + '0 0 325');
+
+ self.event_damage = monsters_corpse_damage;
+ self.solid = SOLID_CORPSE;
+ self.takedamage = DAMAGE_AIM;
+ self.deadflag = DEAD_DEAD;
+ self.enemy = world;
+ self.movetype = MOVETYPE_TOSS;
+ self.moveto = self.origin;
+ self.touch = MonsterTouch; // reset incase monster was pouncing
+ self.reset = func_null;
+ self.state = 0;
+ self.attack_finished_single = 0;
+
+ if(!(self.flags & FL_FLY))
+ self.velocity = '0 0 0';
+
+ MON_ACTION(self.monsterid, MR_DEATH);
+ }
+
+ void monsters_damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+ {
+ if(time < self.pain_finished && deathtype != DEATH_KILL)
+ return;
+
+ if(time < self.spawnshieldtime && deathtype != DEATH_KILL)
+ return;
+
+ vector v;
+ float take, save;
+
+ v = healtharmor_applydamage(self.armorvalue, self.m_armor_blockpercent, deathtype, damage);
+ take = v_x;
+ save = v_y;
+
+ self.health -= take;
+
+ WaypointSprite_UpdateHealth(self.sprite, self.health);
+
+ self.dmg_time = time;
+
+ if(sound_allowed(MSG_BROADCAST, attacker) && deathtype != DEATH_DROWN)
+ spamsound (self, CH_PAIN, "misc/bodyimpact1.wav", VOL_BASE, ATTEN_NORM); // FIXME: PLACEHOLDER
+
+ self.velocity += force * self.damageforcescale;
+
+ if(deathtype != DEATH_DROWN)
+ {
+ Violence_GibSplash_At(hitloc, force, 2, bound(0, take, 200) / 16, self, attacker);
+ if (take > 50)
+ Violence_GibSplash_At(hitloc, force * -0.1, 3, 1, self, attacker);
+ if (take > 100)
+ Violence_GibSplash_At(hitloc, force * -0.2, 3, 1, self, attacker);
+ }
+
+ if(self.health <= 0)
+ {
+ if(deathtype == DEATH_KILL)
+ self.candrop = FALSE; // killed by mobkill command
+
+ // TODO: fix this?
+ activator = attacker;
+ other = self.enemy;
+ SUB_UseTargets();
+ self.target2 = self.oldtarget2; // reset to original target on death, incase we respawn
+
+ monster_die(attacker, (self.health <= -100 || deathtype == DEATH_KILL));
+
+ WaypointSprite_Kill(self.sprite);
+
+ frag_attacker = attacker;
+ frag_target = self;
+ MUTATOR_CALLHOOK(MonsterDies);
+
+ if(self.health <= -100 || deathtype == DEATH_KILL) // check if we're already gibbed
+ {
+ Violence_GibSplash(self, 1, 0.5, attacker);
+
+ self.think = SUB_Remove;
+ self.nextthink = time + 0.1;
+ }
+ }
+ }
+
+ void monster_setupcolors()
+ {
+ if(IS_PLAYER(self.monster_owner))
+ self.colormap = self.monster_owner.colormap;
+ else if(teamplay && self.team)
+ self.colormap = 1024 + (self.team - 1) * 17;
+ else
+ {
+ if(self.monster_skill <= MONSTER_SKILL_EASY)
+ self.colormap = 1029;
+ else if(self.monster_skill <= MONSTER_SKILL_MEDIUM)
+ self.colormap = 1027;
+ else if(self.monster_skill <= MONSTER_SKILL_HARD)
+ self.colormap = 1038;
+ else if(self.monster_skill <= MONSTER_SKILL_INSANE)
+ self.colormap = 1028;
+ else if(self.monster_skill <= MONSTER_SKILL_NIGHTMARE)
+ self.colormap = 1032;
+ else
+ self.colormap = 1024;
+ }
+ }
+
+ void monster_think()
+ {
+ self.think = monster_think;
+ self.nextthink = self.ticrate;
+
+ if(self.ltime)
+ if(time >= self.ltime)
+ {
+ Damage(self, self, self, self.health + self.max_health, DEATH_KILL, self.origin, self.origin);
+ return;
+ }
+
+ MON_ACTION(self.monsterid, MR_THINK);
+
+ CSQCMODEL_AUTOUPDATE();
+ }
+
+ float monster_spawn()
+ {
+ MON_ACTION(self.monsterid, MR_SETUP);
+
+ if(!(self.spawnflags & MONSTERFLAG_RESPAWNED))
+ {
+ Monster_CheckMinibossFlag();
+ self.health *= Monster_SkillModifier();
+ }
+
+ self.max_health = self.health;
+ self.pain_finished = self.nextthink;
+
+ if(IS_PLAYER(self.monster_owner))
+ self.effects |= EF_DIMLIGHT;
+
+ if(!(self.spawnflags & MONSTERFLAG_RESPAWNED))
+ if(!self.skin)
+ self.skin = rint(random() * 4);
+
+ if(!self.attack_range)
+ self.attack_range = autocvar_g_monsters_attack_range;
+
+ precache_monstersounds();
+ UpdateMonsterSounds();
+
+ if(teamplay)
+ self.monster_attack = TRUE; // we can have monster enemies in team games
+
+ MonsterSound(monstersound_spawn, 0, FALSE, CH_VOICE);
+
+ WaypointSprite_Spawn(M_NAME(self.monsterid), 0, 1024, self, '0 0 1' * (self.maxs_z + 15), world, self.team, self, sprite, TRUE, RADARICON_DANGER, ((self.team) ? Team_ColorRGB(self.team) : '1 0 0'));
+ WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
+ WaypointSprite_UpdateHealth(self.sprite, self.health);
+
+ self.think = monster_think;
+ self.nextthink = time + self.ticrate;
+
+ if(MUTATOR_CALLHOOK(MonsterSpawn))
+ return FALSE;
+
+ return TRUE;
+ }
+
+ float monster_initialize(float mon_id, float nodrop)
+ {
+ if(!autocvar_g_monsters)
+ return FALSE;
+
+ entity mon = get_monsterinfo(mon_id);
+
+ if(!self.monster_skill)
+ self.monster_skill = cvar("g_monsters_skill");
+
+ // support for quake style removing monsters based on skill
+ if(self.monster_skill == MONSTER_SKILL_EASY) if(self.spawnflags & MONSTERSKILL_NOTEASY) { return FALSE; }
+ if(self.monster_skill == MONSTER_SKILL_MEDIUM) if(self.spawnflags & MONSTERSKILL_NOTMEDIUM) { return FALSE; }
+ if(self.monster_skill == MONSTER_SKILL_HARD) if(self.spawnflags & MONSTERSKILL_NOTHARD) { return FALSE; }
+
+ if(self.team && !teamplay)
+ self.team = 0;
+
+ if(!(self.spawnflags & MONSTERFLAG_SPAWNED)) // naturally spawned monster
+ if(!(self.spawnflags & MONSTERFLAG_RESPAWNED))
+ monsters_total += 1;
+
+ setmodel(self, mon.model);
+ setsize(self, mon.mins, mon.maxs);
+ self.flags = FL_MONSTER;
+ self.takedamage = DAMAGE_AIM;
+ self.bot_attack = TRUE;
+ self.iscreature = TRUE;
+ self.teleportable = TRUE;
+ self.damagedbycontents = TRUE;
+ self.monsterid = mon_id;
+ self.damageforcescale = 0;
+ self.event_damage = monsters_damage;
+ self.touch = MonsterTouch;
+ self.use = monster_use;
+ self.solid = SOLID_BBOX;
+ self.movetype = MOVETYPE_WALK;
+ self.spawnshieldtime = time + autocvar_g_monsters_spawnshieldtime;
+ self.enemy = world;
+ self.velocity = '0 0 0';
+ self.moveto = self.origin;
+ self.pos1 = self.origin;
+ self.pos2 = self.angles;
+ self.reset = monsters_reset;
+ self.netname = mon.netname;
+ self.monster_name = M_NAME(mon_id);
+ self.candrop = TRUE;
+ self.view_ofs = '0 0 1' * (self.maxs_z * 0.5);
+ self.oldtarget2 = self.target2;
+ self.deadflag = DEAD_NO;
+ self.scale = 1;
+ self.noalign = nodrop;
+ self.spawn_time = time;
+ self.spider_slowness = 0;
+ self.gravity = 1;
+ self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_MONSTERCLIP;
+
+ if(autocvar_g_fullbrightplayers)
+ self.effects |= EF_FULLBRIGHT;
+
+ if(autocvar_g_nodepthtestplayers)
+ self.effects |= EF_NODEPTHTEST;
+
+ if(mon.spawnflags & MONSTER_TYPE_SWIM)
+ self.flags |= FL_SWIM;
+
+ if(mon.spawnflags & MONSTER_TYPE_FLY)
+ {
+ self.flags |= FL_FLY;
+ self.movetype = MOVETYPE_FLY;
+ }
+
+ if(mon.spawnflags & MONSTER_SIZE_BROKEN)
+ self.scale = 1.3;
+
+ if(!self.ticrate)
+ self.ticrate = autocvar_g_monsters_think_delay;
+
+ self.ticrate = bound(sys_frametime, self.ticrate, 60);
+
+ if(!self.m_armor_blockpercent)
+ self.m_armor_blockpercent = 0.5;
+
+ if(!self.target_range)
+ self.target_range = autocvar_g_monsters_target_range;
+
+ if(!self.respawntime)
+ self.respawntime = autocvar_g_monsters_respawn_delay;
+
+ if(!self.monster_moveflags)
+ self.monster_moveflags = MONSTER_MOVE_WANDER;
+
+ if(!self.noalign)
+ {
+ setorigin(self, self.origin + '0 0 20');
+ tracebox(self.origin + '0 0 64', self.mins, self.maxs, self.origin - '0 0 10000', MOVE_WORLDONLY, self);
+ setorigin(self, trace_endpos);
+ }
+
+ if(!monster_spawn())
+ return FALSE;
+
+ if(!(self.spawnflags & MONSTERFLAG_RESPAWNED))
+ monster_setupcolors();
+
+ CSQCMODEL_AUTOINIT();
+
+ return TRUE;
+ }
--- /dev/null
- is_player = (trace_ent.classname == "player" || trace_ent.classname == "body");
+#ifdef REGISTER_WEAPON
+REGISTER_WEAPON(
+/* WEP_##id */ SHOCKWAVE,
+/* function */ W_Shockwave,
+/* ammotype */ ammo_none,
+/* impulse */ 2,
+/* flags */ WEP_FLAG_NORMAL | WEP_TYPE_HITSCAN,
+/* rating */ BOT_PICKUP_RATING_LOW,
+/* color */ '0.5 0.25 0',
+/* modelname */ "shotgun",
+/* simplemdl */ "foobar",
+/* crosshair */ "gfx/crosshairshotgun 0.7",
+/* wepimg */ "weaponshotgun",
+/* refname */ "shockwave",
+/* wepname */ _("Shockwave")
+);
+
+#define SHOCKWAVE_SETTINGS(w_cvar,w_prop) SHOCKWAVE_SETTINGS_LIST(w_cvar, w_prop, SHOCKWAVE, shockwave)
+#define SHOCKWAVE_SETTINGS_LIST(w_cvar,w_prop,id,sn) \
+ w_cvar(id, sn, NONE, blast_animtime) \
+ w_cvar(id, sn, NONE, blast_damage) \
+ w_cvar(id, sn, NONE, blast_distance) \
+ w_cvar(id, sn, NONE, blast_edgedamage) \
+ w_cvar(id, sn, NONE, blast_force) \
+ w_cvar(id, sn, NONE, blast_force_forwardbias) \
+ w_cvar(id, sn, NONE, blast_force_zscale) \
+ w_cvar(id, sn, NONE, blast_jump_damage) \
+ w_cvar(id, sn, NONE, blast_jump_edgedamage) \
+ w_cvar(id, sn, NONE, blast_jump_force) \
+ w_cvar(id, sn, NONE, blast_jump_force_velocitybias) \
+ w_cvar(id, sn, NONE, blast_jump_force_zscale) \
+ w_cvar(id, sn, NONE, blast_jump_multiplier_accuracy) \
+ w_cvar(id, sn, NONE, blast_jump_multiplier_distance) \
+ w_cvar(id, sn, NONE, blast_jump_multiplier_min) \
+ w_cvar(id, sn, NONE, blast_jump_radius) \
+ w_cvar(id, sn, NONE, blast_multiplier_accuracy) \
+ w_cvar(id, sn, NONE, blast_multiplier_distance) \
+ w_cvar(id, sn, NONE, blast_multiplier_min) \
+ w_cvar(id, sn, NONE, blast_refire) \
+ w_cvar(id, sn, NONE, blast_splash_damage) \
+ w_cvar(id, sn, NONE, blast_splash_edgedamage) \
+ w_cvar(id, sn, NONE, blast_splash_force) \
+ w_cvar(id, sn, NONE, blast_splash_force_forwardbias) \
+ w_cvar(id, sn, NONE, blast_splash_multiplier_accuracy) \
+ w_cvar(id, sn, NONE, blast_splash_multiplier_distance) \
+ w_cvar(id, sn, NONE, blast_splash_multiplier_min) \
+ w_cvar(id, sn, NONE, blast_splash_radius) \
+ w_cvar(id, sn, NONE, blast_spread_max) \
+ w_cvar(id, sn, NONE, blast_spread_min) \
+ w_cvar(id, sn, NONE, melee_animtime) \
+ w_cvar(id, sn, NONE, melee_damage) \
+ w_cvar(id, sn, NONE, melee_delay) \
+ w_cvar(id, sn, NONE, melee_force) \
+ w_cvar(id, sn, NONE, melee_multihit) \
+ w_cvar(id, sn, NONE, melee_no_doubleslap) \
+ w_cvar(id, sn, NONE, melee_nonplayerdamage) \
+ w_cvar(id, sn, NONE, melee_range) \
+ w_cvar(id, sn, NONE, melee_refire) \
+ w_cvar(id, sn, NONE, melee_swing_side) \
+ w_cvar(id, sn, NONE, melee_swing_up) \
+ w_cvar(id, sn, NONE, melee_time) \
+ w_cvar(id, sn, NONE, melee_traces) \
+ w_prop(id, sn, float, switchdelay_raise, switchdelay_raise) \
+ w_prop(id, sn, float, switchdelay_drop, switchdelay_drop) \
+ w_prop(id, sn, string, weaponreplace, weaponreplace) \
+ w_prop(id, sn, float, weaponstart, weaponstart) \
+ w_prop(id, sn, float, weaponstartoverride, weaponstartoverride) \
+ w_prop(id, sn, float, weaponthrowable, weaponthrowable)
+
+#ifdef SVQC
+SHOCKWAVE_SETTINGS(WEP_ADD_CVAR, WEP_ADD_PROP)
+#endif
+#else
+#ifdef SVQC
+void spawnfunc_weapon_shockwave()
+{
+ //if(autocvar_sv_q3acompat_machineshockwaveswap) // WEAPONTODO
+ if(autocvar_sv_q3acompat_machineshotgunswap)
+ if(self.classname != "droppedweapon")
+ {
+ weapon_defaultspawnfunc(WEP_MACHINEGUN);
+ return;
+ }
+ weapon_defaultspawnfunc(WEP_SHOCKWAVE);
+}
+
+#define MAX_SHOCKWAVE_HITS 10
+#define DEBUG_SHOCKWAVE
+
+.float swing_prev;
+.entity swing_alreadyhit;
+.float shockwave_blasttime;
+entity shockwave_hit[MAX_SHOCKWAVE_HITS];
+float shockwave_hit_damage[MAX_SHOCKWAVE_HITS];
+vector shockwave_hit_force[MAX_SHOCKWAVE_HITS];
+
+// MELEE ATTACK MODE
+void W_Shockwave_Melee_Think()
+{
+ // declarations
+ float i, f, swing, swing_factor, swing_damage, meleetime, is_player;
+ entity target_victim;
+ vector targpos;
+
+ // check to see if we can still continue, otherwise give up now
+ if((self.realowner.deadflag != DEAD_NO) && WEP_CVAR(shockwave, melee_no_doubleslap))
+ {
+ remove(self);
+ return;
+ }
+
+ // set start time of melee
+ if(!self.cnt)
+ {
+ self.cnt = time;
+ W_PlayStrengthSound(self.realowner);
+ }
+
+ // update values for v_* vectors
+ makevectors(self.realowner.v_angle);
+
+ // calculate swing percentage based on time
+ meleetime = WEP_CVAR(shockwave, melee_time) * W_WeaponRateFactor();
+ swing = bound(0, (self.cnt + meleetime - time) / meleetime, 10);
+ f = ((1 - swing) * WEP_CVAR(shockwave, melee_traces));
+
+ // perform the traces needed for this frame
+ for(i=self.swing_prev; i < f; ++i)
+ {
+ swing_factor = ((1 - (i / WEP_CVAR(shockwave, melee_traces))) * 2 - 1);
+
+ targpos = (self.realowner.origin + self.realowner.view_ofs
+ + (v_forward * WEP_CVAR(shockwave, melee_range))
+ + (v_up * swing_factor * WEP_CVAR(shockwave, melee_swing_up))
+ + (v_right * swing_factor * WEP_CVAR(shockwave, melee_swing_side)));
+
+ WarpZone_traceline_antilag(
+ self.realowner,
+ (self.realowner.origin + self.realowner.view_ofs),
+ targpos,
+ FALSE,
+ self.realowner,
+ ANTILAG_LATENCY(self.realowner)
+ );
+
+ // draw lightning beams for debugging
+ te_lightning2(world, targpos, self.realowner.origin + self.realowner.view_ofs + v_forward * 5 - v_up * 5);
+ te_customflash(targpos, 40, 2, '1 1 1');
+
++ is_player = (trace_ent.classname == "player" || trace_ent.classname == "body" || (trace_ent.flags & FL_MONSTER));
+
+ if((trace_fraction < 1) // if trace is good, apply the damage and remove self if necessary
+ && (trace_ent.takedamage == DAMAGE_AIM)
+ && (trace_ent != self.swing_alreadyhit)
+ && (is_player || WEP_CVAR(shockwave, melee_nonplayerdamage)))
+ {
+ target_victim = trace_ent; // so it persists through other calls
+
+ if(is_player) // this allows us to be able to nerf the non-player damage done in e.g. assault or onslaught
+ swing_damage = (WEP_CVAR(shockwave, melee_damage) * min(1, swing_factor + 1));
+ else
+ swing_damage = (WEP_CVAR(shockwave, melee_nonplayerdamage) * min(1, swing_factor + 1));
+
+ // trigger damage with this calculated info
+ Damage(
+ target_victim,
+ self.realowner,
+ self.realowner,
+ swing_damage,
+ (WEP_SHOCKWAVE | HITTYPE_SECONDARY),
+ (self.realowner.origin + self.realowner.view_ofs),
+ (v_forward * WEP_CVAR(shockwave, melee_force))
+ );
+
+ // handle accuracy
+ if(accuracy_isgooddamage(self.realowner, target_victim))
+ { accuracy_add(self.realowner, WEP_SHOCKWAVE, 0, swing_damage); }
+
+ #ifdef DEBUG_SHOCKWAVE
+ print(sprintf(
+ "MELEE: %s hitting %s with %f damage (factor: %f) at %f time.\n",
+ self.realowner.netname,
+ target_victim.netname,
+ swing_damage,
+ swing_factor,
+ time
+ ));
+ #endif
+
+ // allow multiple hits with one swing, but not against the same player twice
+ if(WEP_CVAR(shockwave, melee_multihit))
+ {
+ self.swing_alreadyhit = target_victim;
+ continue; // move along to next trace
+ }
+ else
+ {
+ remove(self);
+ return;
+ }
+ }
+ }
+
+ if(time >= self.cnt + meleetime)
+ {
+ // melee is finished
+ remove(self);
+ return;
+ }
+ else
+ {
+ // set up next frame
+ self.swing_prev = i;
+ self.nextthink = time;
+ }
+}
+
+void W_Shockwave_Melee()
+{
+ sound(self, CH_WEAPON_A, "weapons/shotgun_melee.wav", VOL_BASE, ATTN_NORM);
+ weapon_thinkf(WFRAME_FIRE2, WEP_CVAR(shockwave, melee_animtime), w_ready);
+
+ entity meleetemp;
+ meleetemp = spawn();
+ meleetemp.owner = meleetemp.realowner = self;
+ meleetemp.think = W_Shockwave_Melee_Think;
+ meleetemp.nextthink = time + WEP_CVAR(shockwave, melee_delay) * W_WeaponRateFactor();
+ W_SetupShot_Range(self, TRUE, 0, "", 0, WEP_CVAR(shockwave, melee_damage), WEP_CVAR(shockwave, melee_range));
+}
+
+// SHOCKWAVE ATTACK MODE
+float W_Shockwave_Attack_CheckSpread(
+ vector targetorg,
+ vector nearest_on_line,
+ vector sw_shotorg,
+ vector attack_endpos)
+{
+ float spreadlimit;
+ float distance_of_attack = vlen(sw_shotorg - attack_endpos);
+ float distance_from_line = vlen(targetorg - nearest_on_line);
+
+ spreadlimit = (distance_of_attack ? min(1, (vlen(sw_shotorg - nearest_on_line) / distance_of_attack)) : 1);
+ spreadlimit =
+ (
+ (WEP_CVAR(shockwave, blast_spread_min) * (1 - spreadlimit))
+ +
+ (WEP_CVAR(shockwave, blast_spread_max) * spreadlimit)
+ );
+
+ if(
+ (spreadlimit && (distance_from_line <= spreadlimit))
+ &&
+ ((vlen(normalize(targetorg - sw_shotorg) - normalize(attack_endpos - sw_shotorg)) * RAD2DEG) <= 90)
+ )
+ { return bound(0, (distance_from_line / spreadlimit), 1); }
+ else
+ { return FALSE; }
+}
+
+float W_Shockwave_Attack_IsVisible(
+ entity head,
+ vector nearest_on_line,
+ vector sw_shotorg,
+ vector attack_endpos)
+{
+ vector nearest_to_attacker = head.WarpZone_findradius_nearest;
+ vector center = (head.origin + (head.mins + head.maxs) * 0.5);
+ vector corner;
+ float i;
+
+ // STEP ONE: Check if the nearest point is clear
+ if(W_Shockwave_Attack_CheckSpread(nearest_to_attacker, nearest_on_line, sw_shotorg, attack_endpos))
+ {
+ WarpZone_TraceLine(sw_shotorg, nearest_to_attacker, MOVE_NOMONSTERS, self);
+ if(trace_fraction == 1) { return TRUE; } // yes, the nearest point is clear and we can allow the damage
+ }
+
+ // STEP TWO: Check if shotorg to center point is clear
+ if(W_Shockwave_Attack_CheckSpread(center, nearest_on_line, sw_shotorg, attack_endpos))
+ {
+ WarpZone_TraceLine(sw_shotorg, center, MOVE_NOMONSTERS, self);
+ if(trace_fraction == 1) { return TRUE; } // yes, the center point is clear and we can allow the damage
+ }
+
+ // STEP THREE: Check each corner to see if they are clear
+ for(i=1; i<=8; ++i)
+ {
+ corner = get_corner_position(head, i);
+ if(W_Shockwave_Attack_CheckSpread(corner, nearest_on_line, sw_shotorg, attack_endpos))
+ {
+ WarpZone_TraceLine(sw_shotorg, corner, MOVE_NOMONSTERS, self);
+ if(trace_fraction == 1) { return TRUE; } // yes, this corner is clear and we can allow the damage
+ }
+ }
+
+ return FALSE;
+}
+
+float W_Shockwave_Attack_CheckHit(
+ float queue,
+ entity head,
+ vector final_force,
+ float final_damage)
+{
+ if(!head) { return FALSE; }
+ float i;
+
+ for(i = 0; i <= queue; ++i)
+ {
+ if(shockwave_hit[i] == head)
+ {
+ if(vlen(final_force) > vlen(shockwave_hit_force[i])) { shockwave_hit_force[i] = final_force; }
+ if(final_damage > shockwave_hit_damage[i]) { shockwave_hit_damage[i] = final_damage; }
+ return FALSE;
+ }
+ }
+
+ shockwave_hit[queue] = head;
+ shockwave_hit_force[queue] = final_force;
+ shockwave_hit_damage[queue] = final_damage;
+ return TRUE;
+}
+
+void W_Shockwave_Attack()
+{
+ // declarations
+ float multiplier, multiplier_from_accuracy, multiplier_from_distance;
+ float final_damage;
+ vector final_force, center, vel;
+ entity head;
+
+ float i, queue = 0;
+
+ // set up the shot direction
+ W_SetupShot(self, FALSE, 3, "weapons/lasergun_fire.wav", CH_WEAPON_B, WEP_CVAR(shockwave, blast_damage));
+ vector attack_endpos = (w_shotorg + (w_shotdir * WEP_CVAR(shockwave, blast_distance)));
+ WarpZone_TraceLine(w_shotorg, attack_endpos, MOVE_NOMONSTERS, self);
+ vector attack_hitpos = trace_endpos;
+ float distance_to_end = vlen(w_shotorg - attack_endpos);
+ float distance_to_hit = vlen(w_shotorg - attack_hitpos);
+ //entity transform = WarpZone_trace_transform;
+
+ // do the firing effect now
+ //SendCSQCShockwaveParticle(attack_endpos); // WEAPONTODO
+ Damage_DamageInfo(
+ attack_hitpos,
+ WEP_CVAR(shockwave, blast_splash_damage),
+ WEP_CVAR(shockwave, blast_splash_edgedamage),
+ WEP_CVAR(shockwave, blast_splash_radius),
+ w_shotdir * WEP_CVAR(shockwave, blast_splash_force),
+ WEP_SHOCKWAVE,
+ 0,
+ self
+ );
+
+ // splash damage/jumping trace
+ head = WarpZone_FindRadius(
+ attack_hitpos,
+ max(
+ WEP_CVAR(shockwave, blast_splash_radius),
+ WEP_CVAR(shockwave, blast_jump_radius)
+ ),
+ FALSE
+ );
+
+ while(head)
+ {
+ if(head.takedamage)
+ {
+ float distance_to_head = vlen(attack_hitpos - head.WarpZone_findradius_nearest);
+
+ if((head == self) && (distance_to_head <= WEP_CVAR(shockwave, blast_jump_radius)))
+ {
+ // ========================
+ // BLAST JUMP CALCULATION
+ // ========================
+
+ // calculate importance of distance and accuracy for this attack
+ multiplier_from_accuracy = (1 -
+ (distance_to_head ?
+ min(1, (distance_to_head / WEP_CVAR(shockwave, blast_jump_radius)))
+ :
+ 0
+ )
+ );
+ multiplier_from_distance = (1 -
+ (distance_to_hit ?
+ min(1, (distance_to_hit / distance_to_end))
+ :
+ 0
+ )
+ );
+ multiplier =
+ max(
+ WEP_CVAR(shockwave, blast_jump_multiplier_min),
+ (
+ (multiplier_from_accuracy * WEP_CVAR(shockwave, blast_jump_multiplier_accuracy))
+ +
+ (multiplier_from_distance * WEP_CVAR(shockwave, blast_jump_multiplier_distance))
+ )
+ );
+
+ // calculate damage from multiplier: 1 = "highest" damage, 0 = "lowest" edgedamage
+ final_damage =
+ (
+ (WEP_CVAR(shockwave, blast_jump_damage) * multiplier)
+ +
+ (WEP_CVAR(shockwave, blast_jump_edgedamage) * (1 - multiplier))
+ );
+
+ // figure out the direction of force
+ vel = normalize(combine_to_vector(head.velocity_x, head.velocity_y, 0));
+ vel *=
+ (
+ bound(0, (vlen(vel) / autocvar_sv_maxspeed), 1)
+ *
+ WEP_CVAR(shockwave, blast_jump_force_velocitybias)
+ );
+ final_force = normalize((CENTER_OR_VIEWOFS(head) - attack_hitpos) + vel);
+
+ // now multiply the direction by force units
+ final_force *= (WEP_CVAR(shockwave, blast_jump_force) * multiplier);
+ final_force_z *= WEP_CVAR(shockwave, blast_jump_force_zscale);
+
+ // trigger damage with this calculated info
+ Damage(
+ head,
+ self,
+ self,
+ final_damage,
+ WEP_SHOCKWAVE,
+ head.origin,
+ final_force
+ );
+
+ #ifdef DEBUG_SHOCKWAVE
+ print(sprintf(
+ "SELF HIT: multiplier = %f, damage = %f, force = %f... "
+ "multiplier_from_accuracy = %f, multiplier_from_distance = %f.\n",
+ multiplier,
+ final_damage,
+ vlen(final_force),
+ multiplier_from_accuracy,
+ multiplier_from_distance
+ ));
+ #endif
+ }
+ else if(distance_to_head <= WEP_CVAR(shockwave, blast_splash_radius))
+ {
+ // ==========================
+ // BLAST SPLASH CALCULATION
+ // ==========================
+
+ // calculate importance of distance and accuracy for this attack
+ multiplier_from_accuracy = (1 -
+ (distance_to_head ?
+ min(1, (distance_to_head / WEP_CVAR(shockwave, blast_splash_radius)))
+ :
+ 0
+ )
+ );
+ multiplier_from_distance = (1 -
+ (distance_to_hit ?
+ min(1, (distance_to_hit / distance_to_end))
+ :
+ 0
+ )
+ );
+ multiplier =
+ max(
+ WEP_CVAR(shockwave, blast_splash_multiplier_min),
+ (
+ (multiplier_from_accuracy * WEP_CVAR(shockwave, blast_splash_multiplier_accuracy))
+ +
+ (multiplier_from_distance * WEP_CVAR(shockwave, blast_splash_multiplier_distance))
+ )
+ );
+
+ // calculate damage from multiplier: 1 = "highest" damage, 0 = "lowest" edgedamage
+ final_damage =
+ (
+ (WEP_CVAR(shockwave, blast_splash_damage) * multiplier)
+ +
+ (WEP_CVAR(shockwave, blast_splash_edgedamage) * (1 - multiplier))
+ );
+
+ // figure out the direction of force
+ final_force = (w_shotdir * WEP_CVAR(shockwave, blast_splash_force_forwardbias));
+ final_force = normalize(CENTER_OR_VIEWOFS(head) - (attack_hitpos - final_force));
+ //te_lightning2(world, attack_hitpos, (attack_hitpos + (final_force * 200)));
+
+ // now multiply the direction by force units
+ final_force *= (WEP_CVAR(shockwave, blast_splash_force) * multiplier);
+ final_force_z *= WEP_CVAR(shockwave, blast_force_zscale);
+
+ // queue damage with this calculated info
+ if(W_Shockwave_Attack_CheckHit(queue, head, final_force, final_damage)) { queue = min(queue + 1, MAX_SHOCKWAVE_HITS); }
+
+ #ifdef DEBUG_SHOCKWAVE
+ print(sprintf(
+ "SPLASH HIT: multiplier = %f, damage = %f, force = %f... "
+ "multiplier_from_accuracy = %f, multiplier_from_distance = %f.\n",
+ multiplier,
+ final_damage,
+ vlen(final_force),
+ multiplier_from_accuracy,
+ multiplier_from_distance
+ ));
+ #endif
+ }
+ }
+ head = head.chain;
+ }
+
+ // cone damage trace
+ head = WarpZone_FindRadius(w_shotorg, WEP_CVAR(shockwave, blast_distance), FALSE);
+ while(head)
+ {
+ if((head != self) && head.takedamage)
+ {
+ // ========================
+ // BLAST CONE CALCULATION
+ // ========================
+
+ // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
+ center = CENTER_OR_VIEWOFS(head);
+
+ // find the closest point on the enemy to the center of the attack
+ float ang; // angle between shotdir and h
+ float h; // hypotenuse, which is the distance between attacker to head
+ float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
+
+ h = vlen(center - self.origin);
+ ang = acos(dotproduct(normalize(center - self.origin), w_shotdir));
+ a = h * cos(ang);
+ // WEAPONTODO: replace with simpler method
+
+ vector nearest_on_line = (w_shotorg + a * w_shotdir);
+ vector nearest_to_attacker = WarpZoneLib_NearestPointOnBox(center + head.mins, center + head.maxs, nearest_on_line);
+
+ if((vlen(head.WarpZone_findradius_dist) <= WEP_CVAR(shockwave, blast_distance))
+ && (W_Shockwave_Attack_IsVisible(head, nearest_on_line, w_shotorg, attack_endpos)))
+ {
+ // calculate importance of distance and accuracy for this attack
+ multiplier_from_accuracy = (1 -
+ W_Shockwave_Attack_CheckSpread(
+ nearest_to_attacker,
+ nearest_on_line,
+ w_shotorg,
+ attack_endpos
+ )
+ );
+ multiplier_from_distance = (1 -
+ (distance_to_hit ?
+ min(1, (vlen(head.WarpZone_findradius_dist) / distance_to_end))
+ :
+ 0
+ )
+ );
+ multiplier =
+ max(
+ WEP_CVAR(shockwave, blast_multiplier_min),
+ (
+ (multiplier_from_accuracy * WEP_CVAR(shockwave, blast_multiplier_accuracy))
+ +
+ (multiplier_from_distance * WEP_CVAR(shockwave, blast_multiplier_distance))
+ )
+ );
+
+ // calculate damage from multiplier: 1 = "highest" damage, 0 = "lowest" edgedamage
+ final_damage =
+ (
+ (WEP_CVAR(shockwave, blast_damage) * multiplier)
+ +
+ (WEP_CVAR(shockwave, blast_edgedamage) * (1 - multiplier))
+ );
+
+ // figure out the direction of force
+ final_force = (w_shotdir * WEP_CVAR(shockwave, blast_force_forwardbias));
+ final_force = normalize(center - (nearest_on_line - final_force));
+ //te_lightning2(world, nearest_on_line, (attack_hitpos + (final_force * 200)));
+
+ // now multiply the direction by force units
+ final_force *= (WEP_CVAR(shockwave, blast_force) * multiplier);
+ final_force_z *= WEP_CVAR(shockwave, blast_force_zscale);
+
+ // queue damage with this calculated info
+ if(W_Shockwave_Attack_CheckHit(queue, head, final_force, final_damage)) { queue = min(queue + 1, MAX_SHOCKWAVE_HITS); }
+
+ #ifdef DEBUG_SHOCKWAVE
+ print(sprintf(
+ "BLAST HIT: multiplier = %f, damage = %f, force = %f... "
+ "multiplier_from_accuracy = %f, multiplier_from_distance = %f.\n",
+ multiplier,
+ final_damage,
+ vlen(final_force),
+ multiplier_from_accuracy,
+ multiplier_from_distance
+ ));
+ #endif
+ }
+ }
+ head = head.chain;
+ }
+
+ for(i = 1; i <= queue; ++i)
+ {
+ head = shockwave_hit[i-1];
+ final_force = shockwave_hit_force[i-1];
+ final_damage = shockwave_hit_damage[i-1];
+
+ Damage(
+ head,
+ self,
+ self,
+ final_damage,
+ WEP_SHOCKWAVE,
+ head.origin,
+ final_force
+ );
+
+ if(accuracy_isgooddamage(self.realowner, head))
+ {
+ print("wtf\n");
+ accuracy_add(self.realowner, WEP_SHOCKWAVE, 0, final_damage);
+ }
+
+ #ifdef DEBUG_SHOCKWAVE
+ print(sprintf(
+ "SHOCKWAVE by %s: damage = %f, force = %f.\n",
+ self.netname,
+ final_damage,
+ vlen(final_force)
+ ));
+ #endif
+
+ shockwave_hit[i-1] = world;
+ shockwave_hit_force[i-1] = '0 0 0';
+ shockwave_hit_damage[i-1] = 0;
+ }
+}
+
+float W_Shockwave(float req)
+{
+ switch(req)
+ {
+ case WR_AIM:
+ {
+ if(vlen(self.origin - self.enemy.origin) <= WEP_CVAR(shockwave, melee_range))
+ { self.BUTTON_ATCK2 = bot_aim(1000000, 0, 0.001, FALSE); }
+ else
+ { self.BUTTON_ATCK = bot_aim(1000000, 0, 0.001, FALSE); }
+
+ return TRUE;
+ }
+ case WR_THINK:
+ {
+ if(self.BUTTON_ATCK)
+ {
+ if(time >= self.shockwave_blasttime) // handle refire separately so the secondary can be fired straight after a primary
+ {
+ if(weapon_prepareattack(0, WEP_CVAR(shockwave, blast_animtime)))
+ {
+ W_Shockwave_Attack();
+ self.shockwave_blasttime = time + WEP_CVAR(shockwave, blast_refire) * W_WeaponRateFactor();
+ weapon_thinkf(WFRAME_FIRE1, WEP_CVAR(shockwave, blast_animtime), w_ready);
+ }
+ }
+ }
+ else if(self.BUTTON_ATCK2)
+ {
+ //if(self.clip_load >= 0) // we are not currently reloading
+ if(!self.crouch) // no crouchmelee please
+ if(weapon_prepareattack(1, WEP_CVAR(shockwave, melee_refire)))
+ {
+ // attempt forcing playback of the anim by switching to another anim (that we never play) here...
+ weapon_thinkf(WFRAME_FIRE1, 0, W_Shockwave_Melee);
+ }
+ }
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_model("models/uziflash.md3");
+ precache_model("models/weapons/g_shockwave.md3");
+ precache_model("models/weapons/v_shockwave.md3");
+ precache_model("models/weapons/h_shockwave.iqm");
+ precache_sound("misc/itempickup.wav");
+ precache_sound("weapons/shockwave_fire.wav");
+ precache_sound("weapons/shockwave_melee.wav");
+ SHOCKWAVE_SETTINGS(WEP_SKIP_CVAR, WEP_SET_PROP)
+ return TRUE;
+ }
+ case WR_CHECKAMMO1:
+ case WR_CHECKAMMO2:
+ {
+ // shockwave has infinite ammo
+ return TRUE;
+ }
+ case WR_CONFIG:
+ {
+ SHOCKWAVE_SETTINGS(WEP_CONFIG_WRITE_CVARS, WEP_CONFIG_WRITE_PROPS)
+ return TRUE;
+ }
+ case WR_SUICIDEMESSAGE:
+ {
+ return WEAPON_THINKING_WITH_PORTALS;
+ }
+ case WR_KILLMESSAGE:
+ {
+ if(w_deathtype & HITTYPE_SECONDARY)
+ return WEAPON_SHOCKWAVE_MURDER_SLAP;
+ else
+ return WEAPON_SHOCKWAVE_MURDER;
+ }
+ }
+ return FALSE;
+}
+#endif
+#ifdef CSQC
+float W_Shockwave(float req)
+{
+ switch(req)
+ {
+ case WR_IMPACTEFFECT:
+ {
+ vector org2;
+ org2 = w_org + w_backoff * 2;
+ pointparticles(particleeffectnum("laser_impact"), org2, w_backoff * 1000, 1); // WEAPONTODO: replace with proper impact effect
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ //precache_sound("weapons/ric1.wav");
+ //precache_sound("weapons/ric2.wav");
+ //precache_sound("weapons/ric3.wav");
+ return FALSE;
+ }
+ case WR_ZOOMRETICLE:
+ {
+ // no weapon specific image for this weapon
+ return FALSE;
+ }
+ }
+ return FALSE;
+}
+#endif
+#endif
--- /dev/null
- is_player = (IS_PLAYER(trace_ent) || trace_ent.classname == "body");
+#ifdef REGISTER_WEAPON
+REGISTER_WEAPON(
+/* WEP_##id */ SHOTGUN,
+/* function */ W_Shotgun,
+/* ammotype */ ammo_none,
+/* impulse */ 2,
+/* flags */ WEP_FLAG_RELOADABLE | WEP_TYPE_HITSCAN | WEP_FLAG_MUTATORBLOCKED,
+/* rating */ BOT_PICKUP_RATING_LOW,
+/* color */ '0.5 0.25 0',
+/* modelname */ "shotgun",
+/* simplemdl */ "foobar",
+/* crosshair */ "gfx/crosshairshotgun 0.65",
+/* wepimg */ "weaponshotgun",
+/* refname */ "shotgun",
+/* wepname */ _("Shotgun")
+);
+
+#define SHOTGUN_SETTINGS(w_cvar,w_prop) SHOTGUN_SETTINGS_LIST(w_cvar, w_prop, SHOTGUN, shotgun)
+#define SHOTGUN_SETTINGS_LIST(w_cvar,w_prop,id,sn) \
+ w_cvar(id, sn, PRI, ammo) \
+ w_cvar(id, sn, BOTH, animtime) \
+ w_cvar(id, sn, BOTH, refire) \
+ w_cvar(id, sn, PRI, bullets) \
+ w_cvar(id, sn, BOTH, damage) \
+ w_cvar(id, sn, BOTH, force) \
+ w_cvar(id, sn, PRI, solidpenetration) \
+ w_cvar(id, sn, PRI, spread) \
+ w_cvar(id, sn, NONE, secondary) \
+ w_cvar(id, sn, SEC, melee_time) \
+ w_cvar(id, sn, SEC, melee_no_doubleslap) \
+ w_cvar(id, sn, SEC, melee_traces) \
+ w_cvar(id, sn, SEC, melee_swing_up) \
+ w_cvar(id, sn, SEC, melee_swing_side) \
+ w_cvar(id, sn, SEC, melee_nonplayerdamage) \
+ w_cvar(id, sn, SEC, melee_multihit) \
+ w_cvar(id, sn, SEC, melee_delay) \
+ w_cvar(id, sn, SEC, melee_range) \
+ w_prop(id, sn, float, reloading_ammo, reload_ammo) \
+ w_prop(id, sn, float, reloading_time, reload_time) \
+ w_prop(id, sn, float, switchdelay_raise, switchdelay_raise) \
+ w_prop(id, sn, float, switchdelay_drop, switchdelay_drop) \
+ w_prop(id, sn, string, weaponreplace, weaponreplace) \
+ w_prop(id, sn, float, weaponstart, weaponstart) \
+ w_prop(id, sn, float, weaponstartoverride, weaponstartoverride) \
+ w_prop(id, sn, float, weaponthrowable, weaponthrowable)
+
+#ifdef SVQC
+SHOTGUN_SETTINGS(WEP_ADD_CVAR, WEP_ADD_PROP)
+#endif
+#else
+#ifdef SVQC
+void spawnfunc_weapon_shotgun(void) { weapon_defaultspawnfunc(WEP_SHOTGUN); }
+
+void W_Shotgun_Attack (void)
+{
+ float sc;
+ entity flash;
+
+ W_DecreaseAmmo(WEP_CVAR_PRI(shotgun, ammo));
+
+ W_SetupShot (self, TRUE, 5, "weapons/shotgun_fire.wav", CH_WEAPON_A, WEP_CVAR_PRI(shotgun, damage) * WEP_CVAR_PRI(shotgun, bullets));
+ for (sc = 0;sc < WEP_CVAR_PRI(shotgun, bullets);sc = sc + 1)
+ fireBullet(w_shotorg, w_shotdir, WEP_CVAR_PRI(shotgun, spread), WEP_CVAR_PRI(shotgun, solidpenetration), WEP_CVAR_PRI(shotgun, damage), WEP_CVAR_PRI(shotgun, force), WEP_SHOTGUN, 0);
+
+ pointparticles(particleeffectnum("shotgun_muzzleflash"), w_shotorg, w_shotdir * 1000, WEP_CVAR_PRI(shotgun, ammo));
+
+ // casing code
+ if (autocvar_g_casings >= 1)
+ for (sc = 0;sc < WEP_CVAR_PRI(shotgun, ammo);sc = sc + 1)
+ SpawnCasing (((random () * 50 + 50) * v_right) - (v_forward * (random () * 25 + 25)) - ((random () * 5 - 30) * v_up), 2, vectoangles(v_forward),'0 250 0', 100, 1, self);
+
+ // muzzle flash for 1st person view
+ flash = spawn();
+ setmodel(flash, "models/uziflash.md3"); // precision set below
+ flash.think = SUB_Remove;
+ flash.nextthink = time + 0.06;
+ flash.effects = EF_ADDITIVE | EF_FULLBRIGHT | EF_LOWPRECISION;
+ W_AttachToShotorg(flash, '5 0 0');
+}
+
+.float swing_prev;
+.entity swing_alreadyhit;
+void W_Shotgun_Melee_Think()
+{
+ // declarations
+ float i, f, swing, swing_factor, swing_damage, meleetime, is_player;
+ entity target_victim;
+ vector targpos;
+
+ if(!self.cnt) // set start time of melee
+ {
+ self.cnt = time;
+ W_PlayStrengthSound(self.realowner);
+ }
+
+ makevectors(self.realowner.v_angle); // update values for v_* vectors
+
+ // calculate swing percentage based on time
+ meleetime = WEP_CVAR_SEC(shotgun, melee_time) * W_WeaponRateFactor();
+ swing = bound(0, (self.cnt + meleetime - time) / meleetime, 10);
+ f = ((1 - swing) * WEP_CVAR_SEC(shotgun, melee_traces));
+
+ // check to see if we can still continue, otherwise give up now
+ if((self.realowner.deadflag != DEAD_NO) && WEP_CVAR_SEC(shotgun, melee_no_doubleslap))
+ {
+ remove(self);
+ return;
+ }
+
+ // if okay, perform the traces needed for this frame
+ for(i=self.swing_prev; i < f; ++i)
+ {
+ swing_factor = ((1 - (i / WEP_CVAR_SEC(shotgun, melee_traces))) * 2 - 1);
+
+ targpos = (self.realowner.origin + self.realowner.view_ofs
+ + (v_forward * WEP_CVAR_SEC(shotgun, melee_range))
+ + (v_up * swing_factor * WEP_CVAR_SEC(shotgun, melee_swing_up))
+ + (v_right * swing_factor * WEP_CVAR_SEC(shotgun, melee_swing_side)));
+
+ WarpZone_traceline_antilag(self, self.realowner.origin + self.realowner.view_ofs, targpos, FALSE, self, ANTILAG_LATENCY(self.realowner));
+
+ // draw lightning beams for debugging
+ //te_lightning2(world, targpos, self.realowner.origin + self.realowner.view_ofs + v_forward * 5 - v_up * 5);
+ //te_customflash(targpos, 40, 2, '1 1 1');
+
++ is_player = (IS_PLAYER(trace_ent) || trace_ent.classname == "body" || (trace_ent.flags & FL_MONSTER));
+
+ if((trace_fraction < 1) // if trace is good, apply the damage and remove self
+ && (trace_ent.takedamage == DAMAGE_AIM)
+ && (trace_ent != self.swing_alreadyhit)
+ && (is_player || WEP_CVAR_SEC(shotgun, melee_nonplayerdamage)))
+ {
+ target_victim = trace_ent; // so it persists through other calls
+
+ if(is_player) // this allows us to be able to nerf the non-player damage done in e.g. assault or onslaught.
+ swing_damage = (WEP_CVAR_SEC(shotgun, damage) * min(1, swing_factor + 1));
+ else
+ swing_damage = (WEP_CVAR_SEC(shotgun, melee_nonplayerdamage) * min(1, swing_factor + 1));
+
+ //print(strcat(self.realowner.netname, " hitting ", target_victim.netname, " with ", strcat(ftos(swing_damage), " damage (factor: ", ftos(swing_factor), ") at "), ftos(time), " seconds.\n"));
+
+ Damage(target_victim, self.realowner, self.realowner,
+ swing_damage, WEP_SHOTGUN | HITTYPE_SECONDARY,
+ self.realowner.origin + self.realowner.view_ofs,
+ v_forward * WEP_CVAR_SEC(shotgun, force));
+
+ if(accuracy_isgooddamage(self.realowner, target_victim)) { accuracy_add(self.realowner, WEP_SHOTGUN, 0, swing_damage); }
+
+ // draw large red flash for debugging
+ //te_customflash(targpos, 200, 2, '15 0 0');
+
+ if(WEP_CVAR_SEC(shotgun, melee_multihit)) // allow multiple hits with one swing, but not against the same player twice.
+ {
+ self.swing_alreadyhit = target_victim;
+ continue; // move along to next trace
+ }
+ else
+ {
+ remove(self);
+ return;
+ }
+ }
+ }
+
+ if(time >= self.cnt + meleetime)
+ {
+ // melee is finished
+ remove(self);
+ return;
+ }
+ else
+ {
+ // set up next frame
+ self.swing_prev = i;
+ self.nextthink = time;
+ }
+}
+
+void W_Shotgun_Attack2 (void)
+{
+ sound (self, CH_WEAPON_A, "weapons/shotgun_melee.wav", VOL_BASE, ATTEN_NORM);
+ weapon_thinkf(WFRAME_FIRE2, WEP_CVAR_SEC(shotgun, animtime), w_ready);
+
+ entity meleetemp;
+ meleetemp = spawn();
+ meleetemp.realowner = self;
+ meleetemp.think = W_Shotgun_Melee_Think;
+ meleetemp.nextthink = time + WEP_CVAR_SEC(shotgun, melee_delay) * W_WeaponRateFactor();
+ W_SetupShot_Range(self, TRUE, 0, "", 0, WEP_CVAR_SEC(shotgun, damage), WEP_CVAR_SEC(shotgun, melee_range));
+}
+
+.float shotgun_primarytime;
+
+float W_Shotgun(float req)
+{
+ switch(req)
+ {
+ case WR_AIM:
+ {
+ if(vlen(self.origin-self.enemy.origin) <= WEP_CVAR_SEC(shotgun, melee_range))
+ self.BUTTON_ATCK2 = bot_aim(1000000, 0, 0.001, FALSE);
+ else
+ self.BUTTON_ATCK = bot_aim(1000000, 0, 0.001, FALSE);
+
+ return TRUE;
+ }
+ case WR_THINK:
+ {
+ if(WEP_CVAR(shotgun, reload_ammo) && self.clip_load < WEP_CVAR_PRI(shotgun, ammo)) // forced reload
+ {
+ // don't force reload an empty shotgun if its melee attack is active
+ if (!WEP_CVAR(shotgun, secondary))
+ WEP_ACTION(self.weapon, WR_RELOAD);
+ }
+ else
+ {
+ if (self.BUTTON_ATCK)
+ {
+ if (time >= self.shotgun_primarytime) // handle refire separately so the secondary can be fired straight after a primary
+ {
+ if(weapon_prepareattack(0, WEP_CVAR_PRI(shotgun, animtime)))
+ {
+ W_Shotgun_Attack();
+ self.shotgun_primarytime = time + WEP_CVAR_PRI(shotgun, refire) * W_WeaponRateFactor();
+ weapon_thinkf(WFRAME_FIRE1, WEP_CVAR_PRI(shotgun, animtime), w_ready);
+ }
+ }
+ }
+ }
+ if (self.clip_load >= 0) // we are not currently reloading
+ if (!self.crouch) // no crouchmelee please
+ if (self.BUTTON_ATCK2 && WEP_CVAR(shotgun, secondary))
+ if (weapon_prepareattack(1, WEP_CVAR_SEC(shotgun, refire)))
+ {
+ // attempt forcing playback of the anim by switching to another anim (that we never play) here...
+ weapon_thinkf(WFRAME_FIRE1, 0, W_Shotgun_Attack2);
+ }
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_model ("models/uziflash.md3");
+ precache_model ("models/weapons/g_shotgun.md3");
+ precache_model ("models/weapons/v_shotgun.md3");
+ precache_model ("models/weapons/h_shotgun.iqm");
+ precache_sound ("misc/itempickup.wav");
+ precache_sound ("weapons/shotgun_fire.wav");
+ precache_sound ("weapons/shotgun_melee.wav");
+ SHOTGUN_SETTINGS(WEP_SKIP_CVAR, WEP_SET_PROP)
+ return TRUE;
+ }
+ case WR_SETUP:
+ {
+ self.ammo_field = ammo_none;
+ return TRUE;
+ }
+ case WR_CHECKAMMO1:
+ case WR_CHECKAMMO2:
+ {
+ // shotgun has infinite ammo
+ return TRUE;
+ }
+ case WR_CONFIG:
+ {
+ SHOTGUN_SETTINGS(WEP_CONFIG_WRITE_CVARS, WEP_CONFIG_WRITE_PROPS)
+ return TRUE;
+ }
+ case WR_RELOAD:
+ {
+ W_Reload(WEP_CVAR_PRI(shotgun, ammo), "weapons/reload.wav"); // WEAPONTODO
+ return TRUE;
+ }
+ case WR_SUICIDEMESSAGE:
+ {
+ return WEAPON_THINKING_WITH_PORTALS;
+ }
+ case WR_KILLMESSAGE:
+ {
+ if(w_deathtype & HITTYPE_SECONDARY)
+ return WEAPON_SHOTGUN_MURDER_SLAP;
+ else
+ return WEAPON_SHOTGUN_MURDER;
+ }
+ }
+ return FALSE;
+}
+#endif
+#ifdef CSQC
+.float prevric;
+float W_Shotgun(float req)
+{
+ switch(req)
+ {
+ case WR_IMPACTEFFECT:
+ {
+ vector org2;
+ org2 = w_org + w_backoff * 2;
+ pointparticles(particleeffectnum("shotgun_impact"), org2, w_backoff * 1000, 1);
+ if(!w_issilent && time - self.prevric > 0.25)
+ {
+ if(w_random < 0.0165)
+ sound(self, CH_SHOTS, "weapons/ric1.wav", VOL_BASE, ATTEN_NORM);
+ else if(w_random < 0.033)
+ sound(self, CH_SHOTS, "weapons/ric2.wav", VOL_BASE, ATTEN_NORM);
+ else if(w_random < 0.05)
+ sound(self, CH_SHOTS, "weapons/ric3.wav", VOL_BASE, ATTEN_NORM);
+ self.prevric = time;
+ }
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_sound("weapons/ric1.wav");
+ precache_sound("weapons/ric2.wav");
+ precache_sound("weapons/ric3.wav");
+ return TRUE;
+ }
+ case WR_ZOOMRETICLE:
+ {
+ // no weapon specific image for this weapon
+ return FALSE;
+ }
+ }
+ return FALSE;
+}
+#endif
+#endif
../common/campaign_file.qc
../common/campaign_setup.qc
../common/mapinfo.qc
-../common/items.qc
+../common/weapons/weapons.qc // TODO
../common/urllib.qc
+ ../common/monsters/monsters.qc
../warpzonelib/mathlib.qc
float autocvar_g_balance_health_rot;
float autocvar_g_balance_health_rotlinear;
float autocvar_g_balance_health_rotstable;
- float autocvar_g_balance_health_start;
-float autocvar_g_balance_hlac_primary_ammo;
-float autocvar_g_balance_hlac_primary_animtime;
-float autocvar_g_balance_hlac_primary_damage;
-float autocvar_g_balance_hlac_primary_edgedamage;
-float autocvar_g_balance_hlac_primary_force;
-float autocvar_g_balance_hlac_primary_lifetime;
-float autocvar_g_balance_hlac_primary_radius;
-float autocvar_g_balance_hlac_primary_refire;
-float autocvar_g_balance_hlac_primary_speed;
-float autocvar_g_balance_hlac_primary_spread_add;
-float autocvar_g_balance_hlac_primary_spread_crouchmod;
-float autocvar_g_balance_hlac_primary_spread_max;
-float autocvar_g_balance_hlac_primary_spread_min;
-float autocvar_g_balance_hlac_secondary;
-float autocvar_g_balance_hlac_secondary_ammo;
-float autocvar_g_balance_hlac_secondary_animtime;
-float autocvar_g_balance_hlac_secondary_damage;
-float autocvar_g_balance_hlac_secondary_edgedamage;
-float autocvar_g_balance_hlac_secondary_force;
-float autocvar_g_balance_hlac_secondary_lifetime;
-float autocvar_g_balance_hlac_secondary_radius;
-float autocvar_g_balance_hlac_secondary_refire;
-float autocvar_g_balance_hlac_secondary_shots;
-float autocvar_g_balance_hlac_secondary_speed;
-float autocvar_g_balance_hlac_secondary_spread;
-float autocvar_g_balance_hlac_secondary_spread_crouchmod;
-float autocvar_g_balance_hlac_reload_ammo;
-float autocvar_g_balance_hlac_reload_time;
-float autocvar_g_balance_hook_primary_animtime;
-float autocvar_g_balance_hook_primary_fuel;
-float autocvar_g_balance_hook_primary_hooked_fuel;
-float autocvar_g_balance_hook_primary_hooked_time_free;
-float autocvar_g_balance_hook_primary_hooked_time_max;
-float autocvar_g_balance_hook_primary_refire;
-float autocvar_g_balance_hook_secondary_ammo;
-float autocvar_g_balance_hook_secondary_animtime;
-float autocvar_g_balance_hook_secondary_damage;
-float autocvar_g_balance_hook_secondary_duration;
-float autocvar_g_balance_hook_secondary_edgedamage;
-float autocvar_g_balance_hook_secondary_force;
-float autocvar_g_balance_hook_secondary_gravity;
-float autocvar_g_balance_hook_secondary_lifetime;
-float autocvar_g_balance_hook_secondary_power;
-float autocvar_g_balance_hook_secondary_radius;
-float autocvar_g_balance_hook_secondary_refire;
-float autocvar_g_balance_hook_secondary_speed;
-float autocvar_g_balance_hook_secondary_health;
-float autocvar_g_balance_hook_secondary_damageforcescale;
float autocvar_g_balance_keyhunt_damageforcescale;
float autocvar_g_balance_keyhunt_delay_collect;
float autocvar_g_balance_keyhunt_delay_return;
../common/command/shared_defs.qh
../common/net_notice.qh
../common/animdecide.qh
+ ../common/monsters/monsters.qh
+ ../common/monsters/sv_monsters.qh
+ ../common/monsters/spawn.qh
+../common/weapons/config.qh
+../common/weapons/weapons.qh // TODO
+weapons/accuracy.qh
+weapons/common.qh
+weapons/csqcprojectile.qh // TODO
+weapons/hitplot.qh
+weapons/selection.qh
+weapons/spawning.qh
+weapons/throwing.qh
+weapons/tracing.qh
+weapons/weaponstats.qh
+weapons/weaponsystem.qh
+
+t_items.qh
+
autocvars.qh
constants.qh
defs.qh // Should rename this, it has fields and globals
round_handler.qc
-../common/explosion_equation.qc
-
+ ../common/monsters/sv_monsters.qc
+ ../common/monsters/monsters.qc
+
+ ../common/monsters/spawn.qc
+
mutators/base.qc
mutators/gamemode_assault.qc
mutators/gamemode_ca.qc
{
self.deadflag = DEAD_DEAD;
self.vehicle_exit(VHEF_NORMAL);
- RadiusDamage (self, self.enemy, 250, 15, 250, world, world, 250, DEATH_VH_RAPT_DEATH, world);
+
- autocvar_g_vehicle_raptor_blowup_radius, world,
+ RadiusDamage(self, self.enemy, autocvar_g_vehicle_raptor_blowup_coredamage,
+ autocvar_g_vehicle_raptor_blowup_edgedamage,
++ autocvar_g_vehicle_raptor_blowup_radius, world, world,
+ autocvar_g_vehicle_raptor_blowup_forceintensity, DEATH_VH_RAPT_DEATH, world);
self.alpha = -1;
self.movetype = MOVETYPE_NONE;
SUB_SetFade(g1, time, min(autocvar_g_vehicle_spiderbot_respawntime, 10));
SUB_SetFade(g2, time, min(autocvar_g_vehicle_spiderbot_respawntime, 10));
- RadiusDamage (self, self.enemy, 250, 15, 250, world, world, 250, DEATH_VH_SPID_DEATH, world);
+ RadiusDamage(self, self.enemy, autocvar_g_vehicle_spiderbot_blowup_coredamage,
+ autocvar_g_vehicle_spiderbot_blowup_edgedamage,
- autocvar_g_vehicle_spiderbot_blowup_radius, world,
++ autocvar_g_vehicle_spiderbot_blowup_radius, world, world,
+ autocvar_g_vehicle_spiderbot_blowup_forceintensity, DEATH_VH_SPID_DEATH, world);
self.alpha = self.tur_head.alpha = self.gun1.alpha = self.gun2.alpha = -1;
self.movetype = MOVETYPE_NONE;
--- /dev/null
- if(IS_CLIENT(targ))
+float accuracy_byte(float n, float d)
+{
+ //printf("accuracy: %d / %d\n", n, d);
+ if(n <= 0)
+ return 0;
+ if(n > d)
+ return 255;
+ return 1 + rint(n * 100.0 / d);
+}
+
+float accuracy_send(entity to, float sf)
+{
+ float w, f;
+ entity a;
+ WriteByte(MSG_ENTITY, ENT_CLIENT_ACCURACY);
+
+ a = self.owner;
+ if(IS_SPEC(a))
+ a = a.enemy;
+ a = a.accuracy;
+
+ if(to != a.owner)
+ if (!(self.owner.cvar_cl_accuracy_data_share && autocvar_sv_accuracy_data_share))
+ sf = 0;
+ // note: zero sendflags can never be sent... so we can use that to say that we send no accuracy!
+ WriteInt24_t(MSG_ENTITY, sf);
+ if(sf == 0)
+ return TRUE;
+ // note: we know that client and server agree about SendFlags...
+ for(w = 0, f = 1; w <= WEP_LAST - WEP_FIRST; ++w)
+ {
+ if(sf & f)
+ WriteByte(MSG_ENTITY, accuracy_byte(self.(accuracy_hit[w]), self.(accuracy_fired[w])));
+ if(f == 0x800000)
+ f = 1;
+ else
+ f *= 2;
+ }
+ return TRUE;
+}
+
+// init/free
+void accuracy_init(entity e)
+{
+ e.accuracy = spawn();
+ e.accuracy.owner = e;
+ e.accuracy.classname = "accuracy";
+ e.accuracy.drawonlytoclient = e;
+ Net_LinkEntity(e.accuracy, FALSE, 0, accuracy_send);
+}
+
+void accuracy_free(entity e)
+{
+ remove(e.accuracy);
+}
+
+// force a resend of a player's accuracy stats
+void accuracy_resend(entity e)
+{
+ e.accuracy.SendFlags = 0xFFFFFF;
+}
+
+// update accuracy stats
+.float hit_time;
+.float fired_time;
+
+void accuracy_add(entity e, float w, float fired, float hit)
+{
+ entity a;
+ float b;
+ if(IS_INDEPENDENT_PLAYER(e))
+ return;
+ a = e.accuracy;
+ if(!a || !(hit || fired))
+ return;
+ w -= WEP_FIRST;
+ b = accuracy_byte(a.(accuracy_hit[w]), a.(accuracy_fired[w]));
+ if(hit)
+ a.(accuracy_hit[w]) += hit;
+ if(fired)
+ a.(accuracy_fired[w]) += fired;
+
+ if(hit && a.hit_time != time) // only run this once per frame
+ {
+ a.(accuracy_cnt_hit[w]) += 1;
+ a.hit_time = time;
+ }
+
+ if(fired && a.fired_time != time) // only run this once per frame
+ {
+ a.(accuracy_cnt_fired[w]) += 1;
+ a.fired_time = time;
+ }
+
+ if(b == accuracy_byte(a.(accuracy_hit[w]), a.(accuracy_fired[w])))
+ return;
+ w = pow(2, mod(w, 24));
+ a.SendFlags |= w;
+ FOR_EACH_CLIENT(a)
+ if(IS_SPEC(a))
+ if(a.enemy == e)
+ a.SendFlags |= w;
+}
+
+float accuracy_isgooddamage(entity attacker, entity targ)
+{
++ frag_attacker = attacker;
++ frag_target = targ;
++ float mutator_check = MUTATOR_CALLHOOK(AccuracyTargetValid);
++
+ if(!warmup_stage)
+ if(targ.deadflag == DEAD_NO)
++ if(mutator_check == MUT_ACCADD_INVALID || (mutator_check == MUT_ACCADD_VALID && IS_CLIENT(targ)))
+ if(DIFF_TEAM(attacker, targ))
+ return TRUE;
+ return FALSE;
+}
+
+float accuracy_canbegooddamage(entity attacker)
+{
+ if(!warmup_stage)
+ return TRUE;
+ return FALSE;
+}