set g_ft_respawn_waves 0
set g_ft_respawn_delay 0
set g_ft_weapon_stay 0
+set g_invasion_respawn_waves 0
+set g_invasion_respawn_delay 0
+set g_invasion_weapon_stay 0
- // =======
- // arena
- // =======
- set g_arena 0 "Arena: many one-on-one rounds are played to find the winner"
- set g_arena_maxspawned 2 "maximum number of players to spawn at once (the rest is spectating, waiting for their turn)"
- set g_arena_roundbased 1 "if disabled, the next player will spawn as soon as someone dies"
- set g_arena_round_timelimit 180
- set g_arena_warmup 5 "time, newly spawned players have to prepare themselves in round based matches"
-
-
// =========
// assault
// =========
else
drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, scoreboard_alpha_bg, DRAWFLAG_NORMAL);
drawborderlines(autocvar_scoreboard_border_thickness, pos, tmp, '0 0 0', scoreboard_alpha_bg * 0.75, DRAWFLAG_NORMAL);
-
+
+ // draw monsters
+ if(stat_monsters_total)
+ {
+ val = sprintf("%d/%d", stat_monsters_killed, stat_monsters_total);
+ pos = HUD_DrawKeyValue(pos, _("Monsters killed:"), val);
+ }
+
// draw secrets
- val = sprintf("%d/%d", stat_secrets_found, stat_secrets_total);
- pos = HUD_DrawKeyValue(pos, _("Secrets found:"), val);
+ if(stat_secrets_total)
+ {
+ val = sprintf("%d/%d", stat_secrets_found, stat_secrets_total);
+ pos = HUD_DrawKeyValue(pos, _("Secrets found:"), val);
+ }
-
+
// update position
pos_y += 1.25 * hud_fontsize_y;
return pos;
CSQCMODEL_PROPERTY(512, float, ReadApproxPastTime, WriteApproxPastTime, anim_upper_time) \
CSQCMODEL_PROPERTY(1024, float, ReadAngle, WriteAngle, v_angle_x) \
CSQCMODEL_ENDIF \
- CSQCMODEL_PROPERTY_SCALED(4096, float, ReadShort, WriteShort, scale, 256, 0, 16384)
+ CSQCMODEL_IF(!isplayer) \
+ CSQCMODEL_PROPERTY(2048, float, ReadByte, WriteByte, monsterid) \
+ CSQCMODEL_ENDIF \
+ CSQCMODEL_PROPERTY_SCALED(4096, float, ReadByte, WriteByte, scale, 16, 0, 255)
// TODO get rid of colormod/glowmod here, find good solution for nex charge glowmod hack; also get rid of some useless properties on non-players that only exist for CopyBody
// add hook function calls here
--- /dev/null
- /* MON_##id */ MAGE,
- /* function */ m_mage,
+#ifdef REGISTER_MONSTER
+REGISTER_MONSTER(
- /* model */ "mage.dpm",
- /* netname */ "mage",
- /* fullname */ _("Mage")
++/* MON_##id */ MAGE,
++/* function */ m_mage,
+/* spawnflags */ MON_FLAG_MELEE | MON_FLAG_RANGED,
+/* mins,maxs */ '-36 -36 -24', '36 36 50',
- const float mage_anim_walk = 1;
- const float mage_anim_attack = 2;
- const float mage_anim_pain = 3;
- const float mage_anim_death = 4;
++/* 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.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;
- 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);
++
+ pointparticles(particleeffectnum("explosion_small"), self.origin, '0 0 0', 1);
+ 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);
+
+ 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)
+ {
- desireddir = normalize(((trace_plane_normal * (1 - trace_fraction)) + (desireddir * trace_fraction)) * 0.5);
++ 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;
- setsize (missile, '0 0 0', '0 0 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, (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
- /* MON_##id */ SHAMBLER,
- /* function */ m_shambler,
+#ifdef REGISTER_MONSTER
+REGISTER_MONSTER(
- /* model */ "shambler.mdl",
- /* netname */ "shambler",
- /* fullname */ _("Shambler")
++/* 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',
- 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;
++/* 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;
+
+ 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);
-
++
+ 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;
+ 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);
+
+ 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_NEX;
-
++
+ 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
- /* MON_##id */ SPIDER,
- /* function */ m_spider,
+#ifdef REGISTER_MONSTER
+REGISTER_MONSTER(
- /* model */ "spider.dpm",
- /* netname */ "spider",
- /* fullname */ _("Spider")
++/* 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);
+ RadiusDamage(self, self.realowner, 0, 0, 25, 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_SetupProjectileVelocityEx(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;
+ }
+ }
- void spawnfunc_monster_spider()
++
+ 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
- /* MON_##id */ WYVERN,
- /* function */ m_wyvern,
+#ifdef REGISTER_MONSTER
+REGISTER_MONSTER(
- /* model */ "wizard.mdl",
- /* netname */ "wyvern",
- /* fullname */ _("Wyvern")
++/* MON_##id */ WYVERN,
++/* function */ m_wyvern,
+/* spawnflags */ MONSTER_TYPE_FLY | MONSTER_SIZE_BROKEN | MON_FLAG_RANGED,
+/* mins,maxs */ '-20 -20 -58', '20 20 20',
- 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;
++/* 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, (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);
- setsize(missile, '-6 -6 -6', '6 6 6');
++
+ 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
- /* MON_##id */ ZOMBIE,
- /* function */ m_zombie,
+#ifdef REGISTER_MONSTER
+REGISTER_MONSTER(
- /* model */ "zombie.dpm",
- /* netname */ "zombie",
- /* fullname */ _("Zombie")
++/* MON_##id */ ZOMBIE,
++/* function */ m_zombie,
+/* spawnflags */ MON_FLAG_MELEE,
+/* mins,maxs */ '-18 -18 -25', '18 18 47',
- const float zombie_anim_blockend = 7;
++/* model */ "zombie.dpm",
++/* netname */ "zombie",
++/* fullname */ _("Zombie")
+);
+
+#else
+#ifdef SVQC
+float autocvar_g_monster_zombie_health;
+float autocvar_g_monster_zombie_attack_melee_damage;
+float autocvar_g_monster_zombie_attack_melee_delay;
+float autocvar_g_monster_zombie_attack_leap_damage;
+float autocvar_g_monster_zombie_attack_leap_force;
+float autocvar_g_monster_zombie_attack_leap_speed;
+float autocvar_g_monster_zombie_attack_leap_delay;
+float autocvar_g_monster_zombie_speed_stop;
+float autocvar_g_monster_zombie_speed_run;
+float autocvar_g_monster_zombie_speed_walk;
+
+const float zombie_anim_attackleap = 0;
+const float zombie_anim_attackrun1 = 1;
+const float zombie_anim_attackrun2 = 2;
+const float zombie_anim_attackrun3 = 3;
+const float zombie_anim_attackstanding1 = 4;
+const float zombie_anim_attackstanding2 = 5;
+const float zombie_anim_attackstanding3 = 6;
- const float zombie_anim_idle = 19;
- const float zombie_anim_painback1 = 20;
- const float zombie_anim_painback2 = 21;
++const float zombie_anim_blockend = 7;
+const float zombie_anim_blockstart = 8;
+const float zombie_anim_deathback1 = 9;
+const float zombie_anim_deathback2 = 10;
+const float zombie_anim_deathback3 = 11;
+const float zombie_anim_deathfront1 = 12;
+const float zombie_anim_deathfront2 = 13;
+const float zombie_anim_deathfront3 = 14;
+const float zombie_anim_deathleft1 = 15;
+const float zombie_anim_deathleft2 = 16;
+const float zombie_anim_deathright1 = 17;
+const float zombie_anim_deathright2 = 18;
- const float zombie_anim_runbackwards = 24;
- const float zombie_anim_runbackwardsleft = 25;
- const float zombie_anim_runbackwardsright = 26;
++const float zombie_anim_idle = 19;
++const float zombie_anim_painback1 = 20;
++const float zombie_anim_painback2 = 21;
+const float zombie_anim_painfront1 = 22;
+const float zombie_anim_painfront2 = 23;
- const float zombie_anim_runforwardleft = 28;
++const float zombie_anim_runbackwards = 24;
++const float zombie_anim_runbackwardsleft = 25;
++const float zombie_anim_runbackwardsright = 26;
+const float zombie_anim_runforward = 27;
- const float zombie_anim_spawn = 30;
++const float zombie_anim_runforwardleft = 28;
+const float zombie_anim_runforwardright = 29;
-
++const float zombie_anim_spawn = 30;
+
+void zombie_attack_leap_touch()
+{
+ if (self.health <= 0)
+ return;
-
++
+ vector angles_face;
+
+ if(other.takedamage)
+ {
+ angles_face = vectoangles(self.moveto - self.origin);
+ angles_face = normalize(angles_face) * (autocvar_g_monster_zombie_attack_leap_force);
+ Damage(other, self, self, (autocvar_g_monster_zombie_attack_leap_damage) * Monster_SkillModifier(), DEATH_MONSTER_ZOMBIE_JUMP, other.origin, angles_face);
+ self.touch = MonsterTouch; // instantly turn it off to stop damage spam
+ }
+
+ if (trace_dphitcontents)
+ self.touch = MonsterTouch;
+}
+
+void zombie_blockend()
+{
+ if(self.health <= 0)
+ return;
+
+ self.frame = zombie_anim_blockend;
+ self.armorvalue = 0;
+ self.m_armor_blockpercent = autocvar_g_monsters_armor_blockpercent;
+}
+
+float zombie_block()
+{
+ self.frame = zombie_anim_blockstart;
+ self.armorvalue = 100;
+ self.m_armor_blockpercent = 0.9;
+ self.state = MONSTER_STATE_ATTACK_MELEE; // freeze monster
+ self.attack_finished_single = time + 2.1;
-
++
+ defer(2, zombie_blockend);
-
++
+ return TRUE;
+}
+
+float zombie_attack(float attack_type)
+{
+ switch(attack_type)
+ {
+ case MONSTER_ATTACK_MELEE:
+ {
+ float rand = random(), chosen_anim;
-
++
+ if(rand < 0.33)
+ chosen_anim = zombie_anim_attackstanding1;
+ else if(rand < 0.66)
+ chosen_anim = zombie_anim_attackstanding2;
+ else
+ chosen_anim = zombie_anim_attackstanding3;
-
++
+ if(random() < 0.3 && self.health < 75 && self.enemy.health > 10)
+ return zombie_block();
-
++
+ return monster_melee(self.enemy, (autocvar_g_monster_zombie_attack_melee_damage), chosen_anim, self.attack_range, (autocvar_g_monster_zombie_attack_melee_delay), DEATH_MONSTER_ZOMBIE_MELEE, TRUE);
+ }
+ case MONSTER_ATTACK_RANGED:
+ {
+ makevectors(self.angles);
+ return monster_leap(zombie_anim_attackleap, zombie_attack_leap_touch, v_forward * (autocvar_g_monster_zombie_attack_leap_speed) + '0 0 200', (autocvar_g_monster_zombie_attack_leap_delay));
+ }
+ }
- void spawnfunc_monster_zombie()
++
+ return FALSE;
+}
+
-
++void spawnfunc_monster_zombie()
+{
+ self.classname = "monster_zombie";
-
++
+ self.monster_spawnfunc = spawnfunc_monster_zombie;
-
++
+ self.spawnflags |= MONSTER_RESPAWN_DEATHPOINT;
-
++
+ if(Monster_CheckAppearFlags(self))
+ return;
-
++
+ if(!monster_initialize(MON_ZOMBIE, FALSE)) { remove(self); return; }
+}
+
+float m_zombie(float req)
+{
+ switch(req)
+ {
+ case MR_THINK:
+ {
+ monster_move((autocvar_g_monster_zombie_speed_run), (autocvar_g_monster_zombie_speed_walk), (autocvar_g_monster_zombie_speed_stop), zombie_anim_runforward, zombie_anim_runforward, zombie_anim_idle);
+ return TRUE;
+ }
+ case MR_DEATH:
+ {
+ self.armorvalue = 0;
+ self.m_armor_blockpercent = autocvar_g_monsters_armor_blockpercent;
+ self.frame = ((random() > 0.5) ? zombie_anim_deathback1 : zombie_anim_deathfront1);
+ return TRUE;
+ }
+ case MR_SETUP:
+ {
+ if(!self.health) self.health = (autocvar_g_monster_zombie_health);
-
++
+ if(self.spawnflags & MONSTERFLAG_NORESPAWN)
+ self.spawnflags &= ~MONSTERFLAG_NORESPAWN; // zombies always respawn
-
++
+ self.monster_loot = spawnfunc_item_health_medium;
+ self.monster_attackfunc = zombie_attack;
+ self.frame = zombie_anim_spawn;
+ self.spawn_time = time + 2.1;
+ self.spawnshieldtime = self.spawn_time;
+ self.respawntime = 0.2;
-
++
+ return TRUE;
+ }
+ case MR_PRECACHE:
+ {
+ precache_model ("models/monsters/zombie.dpm");
+ return TRUE;
+ }
+ }
-
++
+ return TRUE;
+}
+
+#endif // SVQC
+#ifdef CSQC
+float m_zombie(float req)
+{
+ switch(req)
+ {
+ case MR_PRECACHE:
+ {
+ precache_model ("models/monsters/zombie.dpm");
+ return TRUE;
+ }
+ }
++
+ return TRUE;
+}
+
+#endif // CSQC
+#endif // REGISTER_MONSTER
--- /dev/null
-
+#include "all.qh"
+
+// MONSTER PLUGIN SYSTEM
+entity monster_info[MON_MAXCOUNT];
+entity dummy_monster_info;
+
+void register_monster(float id, float(float) func, float monsterflags, vector min_s, vector max_s, string modelname, string shortname, string mname)
+{
+ entity e;
+ monster_info[id - 1] = e = spawn();
+ e.classname = "monster_info";
+ e.monsterid = id;
+ e.netname = shortname;
+ e.monster_name = mname;
+ e.monster_func = func;
+ e.mdl = modelname;
+ e.spawnflags = monsterflags;
+ e.mins = min_s;
+ e.maxs = max_s;
+ e.model = strzone(strcat("models/monsters/", modelname));
++
+ #ifndef MENUQC
+ func(MR_PRECACHE);
+ #endif
+}
+float m_null(float dummy) { return 0; }
+void register_monsters_done()
+{
+ dummy_monster_info = spawn();
+ dummy_monster_info.classname = "monster_info";
+ dummy_monster_info.monsterid = 0; // you can recognize dummies by this
+ dummy_monster_info.netname = "";
+ dummy_monster_info.monster_name = "Monster";
+ dummy_monster_info.monster_func = m_null;
+ dummy_monster_info.mdl = "";
+ dummy_monster_info.mins = '-0 -0 -0';
+ dummy_monster_info.maxs = '0 0 0';
+ dummy_monster_info.model = "";
+}
+entity get_monsterinfo(float id)
+{
+ entity m;
+ if(id < MON_FIRST || id > MON_LAST)
+ return dummy_monster_info;
+ m = monster_info[id - 1];
+ if(m)
+ return m;
+ return dummy_monster_info;
+}
--- /dev/null
- #define MR_SETUP 1 // (SERVER) setup monster data
+// monster requests
- #define MR_DEATH 3 // (SERVER) called when monster dies
- #define MR_PRECACHE 4 // (BOTH) precaches models/sounds used by this monster
++#define MR_SETUP 1 // (SERVER) setup monster data
+#define MR_THINK 2 // (SERVER) logic to run every frame
- // Monster Registration
++#define MR_DEATH 3 // (SERVER) called when monster dies
++#define MR_PRECACHE 4 // (BOTH) precaches models/sounds used by this monster
+
+// functions:
+entity get_monsterinfo(float id);
+
+// special spawn flags
+const float MONSTER_RESPAWN_DEATHPOINT = 16; // re-spawn where we died
+const float MONSTER_TYPE_FLY = 32;
+const float MONSTER_TYPE_SWIM = 64;
+const float MONSTER_SIZE_BROKEN = 128; // TODO: remove when bad models are replaced
+const float MON_FLAG_SUPERMONSTER = 256; // incredibly powerful monster
+const float MON_FLAG_RANGED = 512; // monster shoots projectiles
+const float MON_FLAG_MELEE = 1024;
+
+// entity properties of monsterinfo:
+.float monsterid; // MON_...
+.string netname; // short name
+.string monster_name; // human readable name
+.float(float) monster_func; // m_...
+.string mdl; // currently a copy of the model
+.string model; // full name of model
+.float spawnflags;
+.vector mins, maxs; // monster hitbox size
+
+// other useful macros
+#define MON_ACTION(monstertype,mrequest) (get_monsterinfo(monstertype)).monster_func(mrequest)
+#define M_NAME(monstertype) (get_monsterinfo(monstertype)).monster_name
+
+// =====================
++// Monster Registration
+// =====================
+
+float m_null(float dummy);
+void register_monster(float id, float(float) func, float monsterflags, vector min_s, vector max_s, string modelname, string shortname, string mname);
+void register_monsters_done();
+
+const float MON_MAXCOUNT = 24;
+#define MON_FIRST 1
+float MON_COUNT;
+float MON_LAST;
+
+#define REGISTER_MONSTER_2(id,func,monsterflags,min_s,max_s,modelname,shortname,mname) \
+ float id; \
+ float func(float); \
+ void RegisterMonsters_##id() \
+ { \
+ MON_LAST = (id = MON_FIRST + MON_COUNT); \
+ ++MON_COUNT; \
+ register_monster(id,func,monsterflags,min_s,max_s,modelname,shortname,mname); \
+ } \
+ ACCUMULATE_FUNCTION(RegisterMonsters, RegisterMonsters_##id)
+#ifdef MENUQC
+#define REGISTER_MONSTER(id,func,monsterflags,min_s,max_s,modelname,shortname,mname) \
+ REGISTER_MONSTER_2(MON_##id,m_null,monsterflags,min_s,max_s,modelname,shortname,mname)
+#else
+#define REGISTER_MONSTER(id,func,monsterflags,min_s,max_s,modelname,shortname,mname) \
+ REGISTER_MONSTER_2(MON_##id,func,monsterflags,min_s,max_s,modelname,shortname,mname)
+#endif
+
+#include "all.qh"
+
+#undef REGISTER_MONSTER
+ACCUMULATE_FUNCTION(RegisterMonsters, register_monsters_done);
--- /dev/null
-
+entity spawnmonster (string monster, float monster_id, entity spawnedby, entity own, vector orig, float respwn, float moveflag)
+{
+ // ensure spawnfunc database is initialized
+ initialize_field_db();
-
++
+ entity e = spawn();
-
++
+ e.spawnflags = MONSTERFLAG_SPAWNED;
-
++
+ if(!respwn)
+ e.spawnflags |= MONSTERFLAG_NORESPAWN;
-
++
+ setorigin(e, orig);
-
++
+ if(monster != "")
+ {
+ float i, found = 0;
+ entity mon;
+ for(i = MON_FIRST; i <= MON_LAST; ++i)
+ {
+ mon = get_monsterinfo(i);
+ if(mon.netname == monster)
+ {
+ found = TRUE;
+ break;
+ }
+ }
+ if(!found)
+ monster = (get_monsterinfo(MON_FIRST)).netname;
+ }
-
++
+ if(monster == "")
+ if(monster_id)
+ monster = (get_monsterinfo(monster_id)).netname;
-
++
+ e.realowner = spawnedby;
-
++
+ if(moveflag)
+ e.monster_moveflags = moveflag;
-
++
+ if(IS_PLAYER(spawnedby))
+ {
+ if(teamplay && autocvar_g_monsters_teams)
+ e.team = spawnedby.team; // colors handled in spawn code
-
++
+ if(autocvar_g_monsters_owners)
+ e.monster_owner = own; // using .owner makes the monster non-solid for its master
-
++
+ e.angles = spawnedby.angles;
+ }
-
++
+ monster = strcat("$ spawnfunc_monster_", monster);
-
++
+ target_spawn_edit_entity(e, monster, world, world, world, world, world);
++
+ return e;
+}
--- /dev/null
- // SVQC Monster Properties
+// =========================
-
++// 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.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_NEX;
+ }
+}
+
+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.monster_respawned = TRUE;
+ 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
+ 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.frozen)
+ {
+ self.revive_progress = bound(0, self.revive_progress + self.ticrate * self.revive_speed, 1);
+ self.health = max(1, self.max_health * self.revive_progress);
-
++
+ WaypointSprite_UpdateHealth(self.sprite, self.health);
-
++
+ movelib_beak_simple(stopspeed);
-
++
+ self.frame = manim_idle;
-
++
+ self.enemy = world;
+ self.nextthink = time + self.ticrate;
+
+ if(self.revive_progress >= 1)
+ Unfreeze(self); // wait for next think before attacking
+
+ return; // no moving while frozen
+ }
+
+ 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);
-
++
+ if(mon.iceblock)
+ remove(mon.iceblock);
-
++
+ 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;
-
++
+ Unfreeze(self); // remove any icy remains
+
+ 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);
-
++
+ if(IS_CLIENT(self.realowner))
+ if(!self.monster_respawned)
+ 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;
-
++
+ if ( self.frozen )
+ {
+ Unfreeze(self); // remove any icy remains
+ self.health = 0; // reset by Unfreeze
+ }
-
++
+ monster_dropitem();
+
+ MonsterSound(monstersound_death, 0, FALSE, CH_VOICE);
+
+ if(!(self.spawnflags & MONSTERFLAG_SPAWNED) && !self.monster_respawned)
+ monsters_killed += 1;
- if( autocvar_g_monsters_score_spawned ||
++
+ if(IS_PLAYER(attacker))
-
++ if( autocvar_g_monsters_score_spawned ||
+ ( !(self.spawnflags & MONSTERFLAG_SPAWNED) && !self.monster_respawned) )
+ PlayerScore_Add(attacker, SP_SCORE, +autocvar_g_monsters_score_kill);
+
+ if(!Monster_CanRespawn(self) || gibbed)
+ {
+ // number of monsters spawned with mobspawn command
+ totalspawned -= 1;
-
++
+ if(IS_CLIENT(self.realowner))
+ if(!self.monster_respawned)
+ 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(self.frozen && deathtype != DEATH_KILL)
+ return;
+
+ 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.monster_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.monster_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.monster_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.monster_respawned)
+ monster_setupcolors();
++
+ CSQCMODEL_AUTOINIT();
+
+ return TRUE;
+}
--- /dev/null
- _MSOUND(idle)
+.string spawnmob;
+.float monster_attack;
+
+.entity monster_owner; // new monster owner entity, fixes non-solid monsters
+.float monstercount; // per player monster count
+
+.float stat_monsters_killed; // stats
+.float stat_monsters_total;
+float monsters_total;
+float monsters_killed;
+void monsters_setstatus(); // monsters.qc
+.float monster_moveflags; // checks where to move when not attacking
+
+.float spider_slowness; // special spider timer
+
+void monster_remove(entity mon); // removes a monster
+
+.float(float attack_type) monster_attackfunc;
+const float MONSTER_ATTACK_MELEE = 1;
+const float MONSTER_ATTACK_RANGED = 2;
+
+.float monster_skill;
+const float MONSTER_SKILL_EASY = 1;
+const float MONSTER_SKILL_MEDIUM = 3;
+const float MONSTER_SKILL_HARD = 5;
+const float MONSTER_SKILL_INSANE = 7;
+const float MONSTER_SKILL_NIGHTMARE = 10;
+
+.float fish_wasdrowning; // used to reset a drowning fish's angles if it reaches water again
+
+.float candrop;
+
+.float attack_range;
+
+.float spawn_time; // stop monster from moving around right after spawning
+
+.string oldtarget2;
+.float lastshielded;
+
+.vector oldangles;
+
+.float m_armor_blockpercent;
+
+// monster sounds
+// copied from player sounds
+.float msound_delay; // temporary antilag system
+#define ALLMONSTERSOUNDS \
+ _MSOUND(death) \
+ _MSOUND(sight) \
+ _MSOUND(ranged) \
+ _MSOUND(melee) \
+ _MSOUND(pain) \
+ _MSOUND(spawn) \
- const float MONSTERFLAG_MINIBOSS = 64; // monster spawns as mini-boss (also has a chance of naturally becoming one)
++ _MSOUND(idle)
+
+#define _MSOUND(m) .string monstersound_##m;
+ALLMONSTERSOUNDS
+#undef _MSOUND
+
+float GetMonsterSoundSampleField_notFound;
+
+.float monster_respawned; // used to make sure we're not recounting respawned monster stats
+
+const float MONSTERSKILL_NOTEASY = 256; // monster will not spawn on skill <= 1
+const float MONSTERSKILL_NOTMEDIUM = 512; // monster will not spawn on skill 2
+const float MONSTERSKILL_NOTHARD = 1024; // monster will not spawn on skill >= 3
+
+// new flags
+const float MONSTERFLAG_APPEAR = 2; // delay spawn until triggered
+const float MONSTERFLAG_NORESPAWN = 4;
+const float MONSTERFLAG_FLY_VERTICAL = 8; // fly/swim vertically
+const float MONSTERFLAG_INFRONT = 32; // only check for enemies infront of us
-
++const float MONSTERFLAG_MINIBOSS = 64; // monster spawns as mini-boss (also has a chance of naturally becoming one)
+const float MONSTERFLAG_SPAWNED = 16384; // flag for spawned monsters
+
+.void() monster_spawnfunc;
+
+.float monster_movestate; // used to tell what the monster is currently doing
+const float MONSTER_MOVE_OWNER = 1; // monster will move to owner if in range, or stand still
+const float MONSTER_MOVE_WANDER = 2; // monster will ignore owner & wander around
+const float MONSTER_MOVE_SPAWNLOC = 3; // monster will move to its spawn location when not attacking
+const float MONSTER_MOVE_NOMOVE = 4; // monster simply stands still
+const float MONSTER_MOVE_ENEMY = 5; // used only as a movestate
+
+const float MONSTER_STATE_ATTACK_LEAP = 1;
+const float MONSTER_STATE_ATTACK_MELEE = 2;
i = spawnXonoticTeamSelectDialog();
i.configureDialog(i);
me.addItemCentered(me, i, i.intendedWidth * eX + i.intendedHeight * eY, SKINALPHAS_MAINMENU_z);
-
+
+ i = spawnXonoticMonsterToolsDialog();
+ i.configureDialog(i);
+ me.addItemCentered(me, i, i.intendedWidth * eX + i.intendedHeight * eY, SKINALPHAS_MAINMENU_z * SKINALPHA_DIALOG_SANDBOXTOOLS);
-
-
++
+
// main dialogs/windows
me.mainNexposee = n = spawnXonoticNexposee();
/*
MUTATOR_CALLHOOK(MakePlayerObserver);
Portal_ClearAll(self);
-
+
+ Unfreeze(self);
-
++
if(self.alivetime)
{
if(!warmup_stage)
{
if(gameover) return;
if(self.player_blocked) return;
- if(self.freezetag_frozen) return;
+ if(self.frozen) return;
-
+
ClientKill_TeamChange(0);
}
if(frametime)
player_anim();
-
+
// secret status
secrets_setstatus();
-
+
+ // monsters status
+ monsters_setstatus();
-
++
self.dmg_team = max(0, self.dmg_team - autocvar_g_teamdamage_resetspeed * frametime);
//self.angles_y=self.v_angle_y + 90; // temp
force = force * g_weaponforcefactor;
mirrorforce *= g_weaponforcefactor;
}
-
+
+ if(targ.frozen && deathtype != DEATH_HURTTRIGGER)
+ {
+ damage = 0;
+ force *= 0.2;
+ }
-
++
// should this be changed at all? If so, in what way?
frag_attacker = attacker;
frag_target = targ;
ctf_EventLog("return", flag.team, player);
// scoring
- PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
- PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
+ if(IS_PLAYER(player))
+ {
+ PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
+ PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
+ }
TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
-
- if(flag.ctf_dropper)
+
+ if(flag.ctf_dropper)
{
PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
- ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
+ ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
}
-
+
// reset the flag
ctf_RespawnFlag(flag);
}
ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
return;
}
-
+
// special touch behaviors
- if(toucher.vehicle_flags & VHF_ISVEHICLE)
+ if(toucher.frozen) { return; }
+ else if(toucher.vehicle_flags & VHF_ISVEHICLE)
{
if(autocvar_g_ctf_allow_vehicle_touch)
toucher = toucher.owner; // the player is actually the vehicle owner, not other
}
else if(toucher.deadflag != DEAD_NO) { return; }
- switch(self.ctf_status)
- {
+ switch(self.ctf_status)
+ {
case FLAG_BASE:
{
- if(SAME_TEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self))
+ if(SAME_TEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && !(toucher.flags & FL_MONSTER))
ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
- else if(DIFF_TEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
+ else if(DIFF_TEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && !(toucher.flags & FL_MONSTER))
ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
break;
}
--- /dev/null
-
+void invasion_spawnpoint()
+{
+ if(!g_invasion) { remove(self); return; }
-
++
+ self.classname = "invasion_spawnpoint";
+}
+
+float invasion_PickMonster(float supermonster_count)
+{
+ if(autocvar_g_invasion_zombies_only)
+ return MON_ZOMBIE;
+
+ float i;
+ entity mon;
-
++
+ RandomSelection_Init();
-
++
+ for(i = MON_FIRST; i <= MON_LAST; ++i)
+ {
+ mon = get_monsterinfo(i);
+ if((mon.spawnflags & MONSTER_TYPE_FLY) || (mon.spawnflags & MONSTER_TYPE_SWIM) || (mon.spawnflags & MON_FLAG_SUPERMONSTER && supermonster_count >= 1))
+ continue; // flying/swimming monsters not yet supported
-
++
+ RandomSelection_Add(world, i, "", 1, 1);
+ }
-
++
+ return RandomSelection_chosen_float;
+}
+
+entity invasion_PickSpawn()
+{
+ entity e;
-
++
+ RandomSelection_Init();
-
++
+ for(e = world;(e = find(e, classname, "invasion_spawnpoint")); )
+ RandomSelection_Add(e, 0, string_null, 1, 1);
-
++
+ return RandomSelection_chosen_ent;
+}
+
+void invasion_SpawnChosenMonster(float mon)
+{
+ entity spawn_point, monster;
-
++
+ spawn_point = invasion_PickSpawn();
-
++
+ if(spawn_point == world)
+ {
+ dprint("Warning: couldn't find any invasion_spawnpoint spawnpoints, no monsters will spawn!\n");
+ return;
+ }
-
++
+ monster = spawnmonster("", mon, spawn_point, spawn_point, spawn_point.origin, FALSE, 2);
-
++
+ if(roundcnt >= maxrounds)
+ monster.spawnflags |= MONSTERFLAG_MINIBOSS;
+}
+
+void invasion_SpawnMonsters(float supermonster_count)
+{
+ float chosen_monster = invasion_PickMonster(supermonster_count);
-
++
+ invasion_SpawnChosenMonster(chosen_monster);
+}
+
+float Invasion_CheckWinner()
+{
+ entity head;
+ if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
+ {
+ FOR_EACH_MONSTER(head)
+ monster_remove(head);
-
++
+ if(roundcnt >= maxrounds)
+ {
+ NextLevel();
+ return 1;
+ }
-
++
+ Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER);
+ Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER);
+ round_handler_Init(5, autocvar_g_invasion_warmup, autocvar_g_invasion_round_timelimit);
+ return 1;
+ }
-
++
+ // boss round
+ if(roundcnt >= maxrounds)
+ {
+ if(numspawned < 1)
+ {
+ maxspawned = 1;
+ invasion_SpawnMonsters(0);
+ }
+ }
+ else
+ {
+ float total_alive_monsters = 0, supermonster_count = 0;
-
++
+ FOR_EACH_MONSTER(head) if(head.health > 0)
+ {
+ if((get_monsterinfo(head.monsterid)).spawnflags & MON_FLAG_SUPERMONSTER)
+ ++supermonster_count;
+ ++total_alive_monsters;
+ }
+
+ if((total_alive_monsters + numkilled) < maxspawned && maxcurrent < 10) // 10 at a time should be plenty
+ {
+ if(time >= last_check)
+ {
+ invasion_SpawnMonsters(supermonster_count);
+ last_check = time + 2;
+ }
-
++
+ return 0;
+ }
+ }
-
++
+ if(numspawned < 1 || numkilled < maxspawned)
+ return 0; // nothing has spawned yet, or there are still alive monsters
-
++
+ if(roundcnt >= maxrounds)
+ {
+ NextLevel();
+ return 1;
+ }
-
++
+ entity winner = world;
+ float winning_score = 0;
-
++
+ FOR_EACH_PLAYER(head)
+ {
+ float cs = PlayerScore_Add(head, SP_KILLS, 0);
+ if(cs > winning_score)
+ {
+ winning_score = cs;
+ winner = head;
+ }
+ }
-
++
+ FOR_EACH_MONSTER(head)
+ monster_remove(head);
+
+ if(winner)
+ {
+ Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_PLAYER_WIN, winner.netname);
+ Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_PLAYER_WIN, winner.netname);
+ }
-
++
+ round_handler_Init(5, autocvar_g_invasion_warmup, autocvar_g_invasion_round_timelimit);
+
+ return 1;
+}
+
+float Invasion_CheckPlayers()
+{
+ return TRUE;
+}
+
+void Invasion_RoundStart()
+{
+ entity e;
+ float numplayers = 0;
+ FOR_EACH_PLAYER(e)
+ {
+ e.player_blocked = 0;
+ ++numplayers;
+ }
-
++
+ roundcnt += 1;
-
++
+ invasion_monsterskill = roundcnt + (numplayers * 0.3);
-
++
+ maxcurrent = 0;
+ numspawned = 0;
+ numkilled = 0;
-
++
+ if(roundcnt > 1)
+ maxspawned = rint(autocvar_g_invasion_monster_count * (roundcnt * 0.5));
+ else
+ maxspawned = autocvar_g_invasion_monster_count;
+}
+
+MUTATOR_HOOKFUNCTION(invasion_MonsterDies)
+{
+ if(!self.monster_respawned)
+ {
+ numkilled += 1;
+ maxcurrent -= 1;
-
++
+ if(IS_PLAYER(frag_attacker))
+ PlayerScore_Add(frag_attacker, SP_KILLS, +1);
+ }
-
++
+ return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(invasion_MonsterSpawn)
+{
+ if(!(self.spawnflags & MONSTERFLAG_SPAWNED))
+ {
+ monster_remove(self);
+ return FALSE;
+ }
-
++
+ if(roundcnt < maxrounds && self.spawnflags & MONSTERFLAG_MINIBOSS)
+ self.spawnflags &= ~MONSTERFLAG_MINIBOSS;
-
++
+ if(!self.monster_respawned)
+ {
+ numspawned += 1;
+ maxcurrent += 1;
+ }
-
++
+ self.monster_skill = invasion_monsterskill;
-
++
+ if((get_monsterinfo(self.monsterid)).spawnflags & MON_FLAG_SUPERMONSTER)
+ Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_INVASION_SUPERMONSTER, M_NAME(self.monsterid));
-
++
+ self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_MONSTERCLIP;
-
++
+ return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(invasion_PlayerThink)
+{
+ monsters_total = maxspawned; // TODO: make sure numspawned never exceeds maxspawned
+ monsters_killed = numkilled;
-
++
+ return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(invasion_PlayerSpawn)
+{
+ self.bot_attack = FALSE;
+ return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(invasion_PlayerDamage)
+{
+ if(IS_PLAYER(frag_attacker) && IS_PLAYER(frag_target) && frag_attacker != frag_target)
+ {
+ frag_damage = 0;
+ frag_force = '0 0 0';
+ }
-
++
+ return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(invasion_PlayerCommand)
+{
+ if(MUTATOR_RETURNVALUE) // command was already handled?
+ return FALSE;
-
++
+ if(cmd_name == "debuginvasion")
+ {
+ sprint(self, strcat("maxspawned = ", ftos(maxspawned), "\n"));
+ sprint(self, strcat("numspawned = ", ftos(numspawned), "\n"));
+ sprint(self, strcat("numkilled = ", ftos(numkilled), "\n"));
+ sprint(self, strcat("roundcnt = ", ftos(roundcnt), "\n"));
+ sprint(self, strcat("monsters_total = ", ftos(monsters_total), "\n"));
+ sprint(self, strcat("monsters_killed = ", ftos(monsters_killed), "\n"));
+ sprint(self, strcat("invasion_monsterskill = ", ftos(invasion_monsterskill), "\n"));
-
++
+ return TRUE;
+ }
-
++
+ return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(invasion_SetStartItems)
+{
+ start_health = 200;
+ start_armorvalue = 200;
-
++
+ return FALSE;
+}
+
+void invasion_ScoreRules()
+{
+ ScoreRules_basics(0, 0, 0, FALSE);
+ ScoreInfo_SetLabel_PlayerScore(SP_KILLS, "frags", SFL_SORT_PRIO_PRIMARY);
+ ScoreRules_basics_end();
+}
+
+void invasion_Initialize()
+{
+ independent_players = 1; // to disable extra useless scores
+
+ invasion_ScoreRules();
-
++
+ independent_players = 0;
+
+ round_handler_Spawn(Invasion_CheckPlayers, Invasion_CheckWinner, Invasion_RoundStart);
+ round_handler_Init(5, autocvar_g_invasion_warmup, autocvar_g_invasion_round_timelimit);
-
++
+ allowed_to_spawn = TRUE;
-
++
+ roundcnt = 0;
+}
+
+MUTATOR_DEFINITION(gamemode_invasion)
+{
+ MUTATOR_HOOK(MonsterDies, invasion_MonsterDies, CBC_ORDER_ANY);
+ MUTATOR_HOOK(MonsterSpawn, invasion_MonsterSpawn, CBC_ORDER_ANY);
+ MUTATOR_HOOK(PlayerPreThink, invasion_PlayerThink, CBC_ORDER_ANY);
+ MUTATOR_HOOK(PlayerSpawn, invasion_PlayerSpawn, CBC_ORDER_ANY);
+ MUTATOR_HOOK(PlayerDamage_Calculate, invasion_PlayerDamage, CBC_ORDER_ANY);
+ MUTATOR_HOOK(SV_ParseClientCommand, invasion_PlayerCommand, CBC_ORDER_ANY);
+ MUTATOR_HOOK(SetStartItems, invasion_SetStartItems, CBC_ORDER_ANY);
-
++
+ MUTATOR_ONADD
+ {
+ if(time > 1) // game loads at time 1
+ error("This is a game type and it cannot be added at runtime.");
+ invasion_Initialize();
++
+ cvar_settemp("g_monsters", "1");
+ }
+
+ MUTATOR_ONROLLBACK_OR_REMOVE
+ {
+ // we actually cannot roll back invasion_Initialize here
+ // BUT: we don't need to! If this gets called, adding always
+ // succeeds.
+ }
+
+ MUTATOR_ONREMOVE
+ {
+ print("This is a game type and it cannot be removed at runtime.");
+ return -1;
+ }
+
+ return 0;
+}
float velocity_difference;
float clean_up_and_do_nothing;
float horiz_speed = autocvar_sv_dodging_horiz_speed;
-
+
- if(self.freezetag_frozen)
+ if(self.frozen)
horiz_speed = autocvar_sv_dodging_horiz_speed_frozen;
if (self.deadflag != DEAD_NO)
tap_direction_x = 0;
tap_direction_y = 0;
-
+
float frozen_dodging;
- frozen_dodging = (self.freezetag_frozen && autocvar_sv_dodging_frozen);
+ frozen_dodging = (self.frozen && autocvar_sv_dodging_frozen);
float dodge_detected;
if (g_dodging == 0)
../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_arena.qc
mutators/gamemode_ca.qc
mutators/gamemode_ctf.qc
mutators/gamemode_domination.qc
for(self = world; (self = findfloat(self, damagedbycontents, TRUE)); )
{
if (self.movetype == MOVETYPE_NOCLIP) { continue; }
-
+
float vehic = (self.vehicle_flags & VHF_ISVEHICLE);
float projectile = (self.flags & FL_PROJECTILE);
-
+ float monster = (self.flags & FL_MONSTER);
+
if (self.watertype <= CONTENT_WATER && self.waterlevel > 0) // workaround a retarded bug made by id software :P (yes, it's that old of a bug)
{
if (!(self.flags & FL_INWATER))
self.team = self.owner.team;
self.flags -= FL_NOTARGET;
-
+ self.monster_attack = TRUE;
+
if (IS_REAL_CLIENT(other))
{
msg_entity = other;
_vehicle = vh_vehicle;
_vehicle.team = _vehicle.tur_head.team;
-
+
sound (_vehicle, CH_TRIGGER_SINGLE, "misc/null.wav", 1, ATTEN_NORM);
- _vehicle.vehicle_hudmodel.viewmodelforclient = _vehicle;
+ _vehicle.vehicle_hudmodel.viewmodelforclient = _vehicle;
_vehicle.phase = time + 1;
-
+ _vehicle.monster_attack = FALSE;
+
_vehicle.vehicle_exit(eject);
-
+
vehicles_setreturn();
- vehicles_reset_colors();
+ vehicles_reset_colors();
_vehicle.owner = world;
self = _oldself;
-
+
vehicles_exit_running = FALSE;
}
+ (v_right * swing_factor * autocvar_g_balance_shotgun_secondary_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_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");
+ is_monster = (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.takedamage == DAMAGE_AIM)
&& (trace_ent != self.swing_alreadyhit)
- && (is_player || autocvar_g_balance_shotgun_secondary_melee_nonplayerdamage))
+ && ((is_player || is_monster) || autocvar_g_balance_shotgun_secondary_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.
+ if(is_player || is_monster) // this allows us to be able to nerf the non-player damage done in e.g. assault or onslaught.
swing_damage = (autocvar_g_balance_shotgun_secondary_damage * min(1, swing_factor + 1));
else
swing_damage = (autocvar_g_balance_shotgun_secondary_melee_nonplayerdamage * min(1, swing_factor + 1));