]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
The first of many bad mistakes (forgot to include the new files)
authorMario <mario.mario@y7mail.com>
Tue, 22 Jan 2013 10:24:21 +0000 (21:24 +1100)
committerMario <mario.mario@y7mail.com>
Tue, 22 Jan 2013 10:24:21 +0000 (21:24 +1100)
27 files changed:
balanceXonotic.cfg
defaultXonotic.cfg
qcsrc/server/command/cmd.qh
qcsrc/server/defs.qh
qcsrc/server/monsters/lib/defs.qh [new file with mode: 0644]
qcsrc/server/monsters/lib/monsters.qc [new file with mode: 0644]
qcsrc/server/monsters/lib/spawn.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/demon.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/dog.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/enforcer.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/fish.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/hknight.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/knight.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/ogre.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/shalrath.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/shambler.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/soldier.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/spawner.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/spider.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/tarbaby.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/wizard.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/zombie.qc [new file with mode: 0644]
qcsrc/server/monsters/monsters.qh [new file with mode: 0644]
qcsrc/server/mutators/gamemode_td.qc [new file with mode: 0644]
qcsrc/server/mutators/gamemode_td.qh [new file with mode: 0644]
qcsrc/server/mutators/mutator_zombie_apocalypse.qc [new file with mode: 0644]
qcsrc/server/w_all.qc

index 6e65eced5c264653691d2bb1458dab99c6e57b9e..871b9a390f2a39fc8daa1b59d6f0bbe34f3b50fa 100644 (file)
@@ -15,7 +15,6 @@ set g_start_weapon_porto -1 "0 = never provide the weapon, 1 = always provide th
 set g_start_weapon_hook -1 "0 = never provide the weapon, 1 = always provide the weapon, -1 = game mode default"
 set g_start_weapon_tuba -1 "0 = never provide the weapon, 1 = always provide the weapon, -1 = game mode default"
 set g_start_weapon_fireball -1 "0 = never provide the weapon, 1 = always provide the weapon, -1 = game mode default"
-set g_start_weapon_incubator -1 "0 = never provide the weapon, 1 = always provide the weapon, -1 = game mode default"
 set g_balance_health_start 100
 set g_balance_armor_start 0
 set g_start_ammo_shells 15
index fc397ca7a740641d2a132da8b93c948a97d40988..37102ef08422e84d2e20256fd2d615b280314e78 100644 (file)
@@ -1388,7 +1388,6 @@ set g_weaponreplace_minstanex ""
 set g_weaponreplace_hook ""
 set g_weaponreplace_tuba ""
 set g_weaponreplace_fireball ""
-set g_weaponreplace_incubator ""
 set sv_q3acompat_machineshotgunswap 0 "shorthand for swapping uzi and shotgun (for Q3A map compatibility in mapinfo files)"
 
 set g_movement_highspeed 1 "movement speed modification factor (only changes movement when above maxspeed)"
index 120bf3e02ebfd3a4ab0154490d5d2114d8b60ae4..b0118c5cf327503b77ee3245f3b9082cd4a37b72 100644 (file)
@@ -13,5 +13,7 @@ float totalspawned;
 
 string MapVote_Suggest(string m);
 
+entity spawnmonster(string monster, entity spawnedby, entity own, vector orig, float respwn, float moveflag);
+
 // used by common/command/generic.qc:GenericCommand_dumpcommands to list all commands into a .txt file
 void ClientCommand_macro_write_aliases(float fh);
index c2cc06dadc61d98b3aed4a4c475e695b204c2f9a..e0cfc591b5b7e92c37d39621a2a5bd14db799e42 100644 (file)
@@ -584,7 +584,6 @@ float client_cefc_accumulatortime;
 #endif
 
 ..float current_ammo;
-.float currentegg;
 
 .float weapon_load[WEP_MAXCOUNT];
 .float ammo_none; // used by the reloading system, must always be 0
diff --git a/qcsrc/server/monsters/lib/defs.qh b/qcsrc/server/monsters/lib/defs.qh
new file mode 100644 (file)
index 0000000..6ea667f
--- /dev/null
@@ -0,0 +1,40 @@
+.float sprite_height;
+
+.void()                attack_melee;
+.float()       attack_ranged;
+.float()       checkattack;
+
+entity(entity ent) FindTarget;
+
+.float spawner_monstercount;
+
+.float monster_respawned; // used to make sure we're not recounting respawned monster stats
+
+float monsters_spawned;
+
+const float MONSTERSKILL_NOTEASY = 256; // monster will not spawn on skill <= 2
+const float MONSTERSKILL_NOTMEDIUM = 512; // monster will not spawn on skill 3
+const float MONSTERSKILL_NOTHARD = 1024; // monster will not spawn on skill 4
+const float MONSTERSKILL_NOTINSANE = 2048; // monster will not spawn on skill 5
+const float MONSTERSKILL_NOTNIGHTMARE = 4096; // monster will not spawn on skill >= 6
+
+const float MONSTERFLAG_NORESPAWN = 2;
+const float MONSTERFLAG_MINIBOSS = 64;  // monster spawns as mini-boss (also has a chance of naturally becoming one)
+const float MONSTERFLAG_NOWANDER = 128; // disable wandering around (currently unused)
+const float MONSTERFLAG_APPEAR = 256; // delay spawn until triggered
+const float MONSTERFLAG_GIANT = 512; // experimental giant monsters feature
+const float MONSTERFLAG_SPAWNED = 1024; // flag for spawned monsters
+
+.void() monster_spawnfunc;
+.void() monster_die;
+.void() monster_delayedattack;
+
+.float monster_moveflags; // checks where to move when not attacking (currently unused)
+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
+
+float enemy_range () { return vlen(self.enemy.origin - self.origin); }
+
+float MONSTER_STATE_ATTACK_LEAP = 1; // the start of something big?
diff --git a/qcsrc/server/monsters/lib/monsters.qc b/qcsrc/server/monsters/lib/monsters.qc
new file mode 100644 (file)
index 0000000..97cac39
--- /dev/null
@@ -0,0 +1,747 @@
+// TODO: clean up this file?
+
+void M_Item_Touch ()
+{
+       if(self && other.classname == STR_PLAYER && other.deadflag == DEAD_NO)
+       {
+               Item_Touch();
+               self.think = SUB_Remove;
+               self.nextthink = time + 0.1;
+       }
+}
+
+void Monster_DropItem (string itype, string itemsize)
+{
+       if(itype == "0")
+               return; // someone didnt want an item...
+       vector backuporigin = self.origin + ((self.mins + self.maxs) * 0.5);
+       entity oldself;
+       
+       oldself = self;
+       self = spawn();
+       
+       if (itype == "armor")
+       {
+               if(itemsize == "large") spawnfunc_item_armor_large();
+               else if (itemsize == "small") spawnfunc_item_armor_small();
+               else if (itemsize == "medium") spawnfunc_item_armor_medium();
+               else print("Invalid monster drop item selected.\n");
+       }
+       else if (itype == "health")
+       {
+               if(itemsize == "large") spawnfunc_item_health_large();
+               else if (itemsize == "small") spawnfunc_item_health_small();
+               else if (itemsize == "medium") spawnfunc_item_health_medium();
+               else if (itemsize == "mega") spawnfunc_item_health_mega();
+               else print("Invalid monster drop item selected.\n");
+       }
+       else if (itype == "ammo")
+       {
+               if(itemsize == "shells") spawnfunc_item_shells();
+               else if (itemsize == "cells") spawnfunc_item_cells();
+               else if (itemsize == "bullets") spawnfunc_item_bullets();
+               else if (itemsize == "rockets") spawnfunc_item_rockets();
+               else print("Invalid monster drop item selected.\n");
+       }
+       
+       self.velocity = randomvec() * 175 + '0 0 325';
+       
+       self.gravity = 1;
+       self.origin = backuporigin;
+       
+       self.touch = M_Item_Touch;
+       
+       SUB_SetFade(self, time + 5, 1);
+       
+       self = oldself;
+}
+
+float monster_isvalidtarget (entity targ, entity ent, float neutral)
+{
+       if(!targ || !ent)
+               return FALSE; // this check should fix a crash
+               
+       if(targ.vehicle_flags & VHF_ISVEHICLE)
+               targ = targ.vehicle;
+               
+       if(time < game_starttime)
+               return FALSE; // monsters do nothing before the match has started
+               
+       traceline(ent.origin, targ.origin, FALSE, ent);
+       
+       if(vlen(targ.origin - ent.origin) >= 2000)
+               return FALSE; // enemy is too far away
+
+       if(trace_ent != targ)
+               return FALSE; // we can't see the enemy
+               
+       if(neutral == TRUE)
+               return TRUE; // we come in peace!
+               
+       if(targ.takedamage == DAMAGE_NO)
+               return FALSE; // enemy can't be damaged
+               
+       if(targ.items & IT_INVISIBILITY)
+               return FALSE; // enemy is invisible
+       
+       if(targ.classname == STR_SPECTATOR || targ.classname == STR_OBSERVER)
+               return FALSE; // enemy is a spectator
+       
+       if(targ.deadflag != DEAD_NO || ent.deadflag != DEAD_NO || targ.health <= 0 || ent.health <= 0)
+               return FALSE; // enemy/self is dead
+       
+       if(targ.monster_owner == ent || ent.monster_owner == targ)
+               return FALSE; // enemy owns us, or we own them
+       
+       if(targ.flags & FL_NOTARGET)
+               return FALSE; // enemy can't be targetted
+       
+       if not(autocvar_g_monsters_typefrag)
+       if(targ.BUTTON_CHAT)
+               return FALSE; // no typefragging!
+       
+       if(teamplay)
+       if(targ.team == ent.team)
+               return FALSE; // enemy is on our team
+       
+       return TRUE;
+}
+
+void MonsterTouch ()
+{
+       if(other == world)
+               return;
+               
+       if(self.enemy != other)
+       if(monster_isvalidtarget(other, self, FALSE))
+               self.enemy = other;
+}
+
+void monster_melee (entity targ, float damg, float er, float deathtype)
+{
+       float bigdmg = 0, rdmg = damg * random();
+
+       if (self.health <= 0)
+               return;
+       if (targ == world)
+               return;
+
+       if (vlen(self.origin - targ.origin) > er * self.scale)
+               return;
+               
+       bigdmg = rdmg * self.scale;
+       
+       if(random() < 0.01) // critical hit ftw
+               bigdmg = 200;
+       
+       Damage(targ, self, self, bigdmg * monster_skill, deathtype, targ.origin, normalize(targ.origin - self.origin));
+}
+
+void Monster_CheckDropCvars (string mon)
+{
+       string dropitem;
+       string dropsize;
+       
+       dropitem = cvar_string(strcat("g_monster_", mon, "_drop"));
+       dropsize = cvar_string(strcat("g_monster_", mon, "_drop_size"));
+       
+       monster_dropitem = dropitem;
+       monster_dropsize = dropsize;
+       MUTATOR_CALLHOOK(MonsterDropItem);
+       dropitem = monster_dropitem;
+       dropsize = monster_dropsize;
+       
+       if(autocvar_g_monsters_forcedrop)
+               Monster_DropItem(autocvar_g_monsters_drop_type, autocvar_g_monsters_drop_size);
+       else if(dropitem != "")
+               Monster_DropItem(dropitem, dropsize);      
+       else
+               Monster_DropItem("armor", "medium");
+}
+
+void ScaleMonster (float scle)
+{
+       // this should prevent monster from falling through floor when scale changes
+       self.scale = scle;
+       setorigin(self, self.origin + ('0 0 30' * scle));
+}
+
+void Monster_CheckMinibossFlag ()
+{
+       if(MUTATOR_CALLHOOK(MonsterCheckBossFlag))
+               return;
+               
+       float healthboost = autocvar_g_monsters_miniboss_healthboost;
+       float r = random() * 4;
+
+       // g_monsters_miniboss_chance cvar or spawnflags 64 causes a monster to be a miniboss
+       if ((self.spawnflags & MONSTERFLAG_MINIBOSS) || (random() * 100 < autocvar_g_monsters_miniboss_chance))
+       {
+               if (r < 2 || self.team == COLOR_TEAM2)
+               {
+                       self.strength_finished = -1;  
+                       healthboost *= monster_skill;
+                       self.effects |= (EF_FULLBRIGHT | EF_BLUE);
+               }
+               else if (r >= 1 || self.team == COLOR_TEAM1)
+               {
+                       self.invincible_finished = -1;
+                       healthboost *= bound(0.5, monster_skill, 1.5);
+                       self.effects |= (EF_FULLBRIGHT | EF_RED);
+               }
+               self.health += healthboost;
+               self.cnt += 20;
+               ScaleMonster(1.5);
+               self.flags |= MONSTERFLAG_MINIBOSS;
+               if(teamplay && autocvar_g_monsters_teams)
+                       return;
+               do
+               {
+                       self.colormod_x = random();
+                       self.colormod_y = random();
+                       self.colormod_z = random();
+                       self.colormod = normalize(self.colormod);
+               }
+               while (self.colormod_x > 0.6 && self.colormod_y > 0.6 && self.colormod_z > 0.6);
+       }
+}
+
+void Monster_Fade ()
+{
+       if not(self.spawnflags & MONSTERFLAG_NORESPAWN)
+       if(autocvar_g_monsters_respawn)
+       {
+               self.monster_respawned = TRUE;
+               setmodel(self, "");
+               self.think = self.monster_spawnfunc;
+               self.nextthink = time + autocvar_g_monsters_respawn_delay;
+               setorigin(self, self.pos1);
+               self.angles = self.pos2;
+               self.health = 0;
+               return;
+       }
+       self.think = SUB_Remove;
+       self.nextthink = time + 4;
+       SUB_SetFade(self, time + 3, 1);
+}
+
+float Monster_CanJump (vector vel)
+{
+       local 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 not(self.flags & FL_ONGROUND)
+               return FALSE;
+       if(self.health < 1)
+               return FALSE; // called when dead?
+       if not(Monster_CanJump(vel))
+               return FALSE;
+               
+       self.frame = anm;
+       self.state = MONSTER_STATE_ATTACK_LEAP;
+       self.touch = touchfunc;
+       self.origin_z += 1;
+       self.velocity = vel;
+       if (self.flags & FL_ONGROUND)
+               self.flags -= FL_ONGROUND;
+               
+       self.attack_finished_single = time + anim_finished;
+       
+       return TRUE;
+}
+
+float GenericCheckAttack ()
+{
+       // checking attack while dead?
+       if (self.health <= 0 || self.enemy == world)
+               return FALSE;
+               
+       if(self.monster_delayedattack && self.delay != -1)
+       {
+               if(time < self.delay)
+                       return FALSE;
+                       
+               self.monster_delayedattack();
+       }
+       
+       if (time < self.attack_finished_single)
+               return FALSE;
+       
+       if (enemy_range() > 2000) // long traces are slow
+               return FALSE;   
+               
+       if(self.attack_melee)
+       if(enemy_range() <= 100 * self.scale)
+       {
+               self.attack_melee(); // don't wait for nextthink - too slow
+               return TRUE;
+       }
+       
+       // monster doesn't have a ranged attack function, so stop here
+       if(!self.attack_ranged)
+               return FALSE;
+
+       // see if any entities are in the way of the shot
+       if (!findtrajectorywithleading(self.origin, '0 0 0', '0 0 0', self.enemy, 800, 0, 2.5, 0, self))
+               return FALSE;
+
+       self.attack_ranged(); // don't wait for nextthink - too slow
+       return TRUE;
+}
+
+void monster_use ()
+{
+       if (self.enemy)
+               return;
+       if (self.health <= 0)
+               return;
+
+       if(!monster_isvalidtarget(activator, self, -1))
+               return;
+
+       self.enemy = activator;
+}
+
+float trace_path(vector from, vector to)
+{
+       vector dir = normalize(to - from) * 15, offset = '0 0 0';
+       float trace1 = trace_fraction;
+       
+       offset_x = dir_y;
+       offset_y = -dir_x;
+       traceline (from+offset, to+offset, TRUE, self);
+       
+       traceline(from-offset, to-offset, TRUE, self);
+               
+       return ((trace1 < trace_fraction) ? trace1 : trace_fraction);
+}
+
+vector monster_pickmovetarget(entity targ)
+{
+       // enemy is always preferred target
+       if(self.enemy && trace_path(self.origin + '0 0 10', self.enemy.origin + '0 0 10') > 0.99)
+               return self.enemy.origin + 60 * normalize(self.enemy.origin - self.origin);
+       
+       switch(self.monster_moveflags)
+       {
+               case MONSTER_MOVE_OWNER:
+               {
+                       if(self.monster_owner && self.monster_owner.classname != "monster_swarm" && trace_path(self.origin + '0 0 10', self.monster_owner.origin + '0 0 10') > 0.99)
+                               return self.monster_owner.origin;
+               }
+               case MONSTER_MOVE_WANDER:
+               {
+                       if(targ)
+                               return targ.origin;
+                               
+                       self.angles_y = random() * 500;
+                       makevectors(self.angles);
+                       return self.origin + v_forward * 600;
+               }
+               case MONSTER_MOVE_SPAWNLOC:
+                       return self.pos1;
+               default:
+               case MONSTER_MOVE_NOMOVE:
+                       return self.origin;
+       }
+}
+
+.float last_trace;
+.float breath_checks;
+void monster_move(float runspeed, float walkspeed, float stopspeed, float manim_run, float manim_walk, float manim_idle)
+{
+       if(self.target)
+               self.goalentity = find(world, targetname, self.target);
+               
+       float l = vlen(self.moveto - self.origin);
+       float t1 = trace_path(self.origin+'0 0 10', self.moveto+'0 0 10');
+       float t2 = trace_path(self.origin-'0 0 15', self.moveto-'0 0 15'); 
+       entity targ = self.goalentity;
+
+       if(self.frozen)
+       {
+               self.revive_progress = bound(0, self.revive_progress + frametime * self.revive_speed, 1);
+               self.health = max(1, self.revive_progress * self.max_health);
+               
+               if(self.sprite)
+               {
+                       WaypointSprite_UpdateHealth(self.sprite, self.health);
+               }
+                       
+               self.velocity = '0 0 0';
+               self.enemy = world;
+               if(self.revive_progress >= 1)
+                       Unfreeze(self); // wait for next think before attacking
+               self.nextthink = time + 0.1;
+                       
+               return; // no moving while frozen
+       }
+       
+       if(self.flags & FL_SWIM)
+       {
+               if(self.waterlevel < WATERLEVEL_WETFEET)
+               {
+                       self.breath_checks += 1;
+                       self.angles = '0 0 -90';
+                       if(self.breath_checks == 25)
+                       {
+                               if not(self.flags & FL_ONGROUND)
+                                       self.flags |= FL_ONGROUND;
+                               self.monster_die();
+                               if(self.realowner.flags & FL_CLIENT)
+                                       self.realowner.monstercount -= 1;
+                               //if(!(self.spawnflags & MONSTERFLAG_SPAWNED) && !self.monster_respawned)
+                                       //monsters_killed += 1;
+                               self.movetype = MOVETYPE_TOSS;
+                               return;
+                       }
+                       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;
+                       if (self.flags & FL_ONGROUND)
+                               self.flags -= FL_ONGROUND;
+                       self.movetype = MOVETYPE_BOUNCE;
+                       self.velocity_z = -200;
+                       return;
+               }
+               else
+               {
+                       self.angles = '0 0 0';
+                       self.movetype = MOVETYPE_WALK;
+                       self.breath_checks = 0;
+               }
+       }
+       
+       if(gameover || time < game_starttime)
+       {
+               runspeed = walkspeed = 0;
+               self.frame = manim_idle;
+               movelib_beak_simple(stopspeed);
+               return;
+       }
+       
+       runspeed *= monster_skill;
+       walkspeed *= monster_skill;
+       
+       monster_target = targ;
+       monster_speed_run = runspeed;
+       monster_speed_walk = walkspeed;
+       MUTATOR_CALLHOOK(MonsterMove);
+       targ = monster_target;
+       runspeed = monster_speed_run;
+       walkspeed = monster_speed_walk;
+               
+       if(IsDifferentTeam(self.monster_owner, self))
+               self.monster_owner = world;
+       
+       if(self.enemy.health <= 0 || (!autocvar_g_monsters_typefrag && self.enemy.BUTTON_CHAT))
+               self.enemy = world;
+               
+       if not(self.enemy)
+               self.enemy = FindTarget(self);
+               
+       if(time >= self.last_trace)
+       {
+               if(self.monster_moveflags & MONSTER_MOVE_WANDER)
+                       self.last_trace = time + 2;
+               else
+                       self.last_trace = time + 0.5;
+               self.moveto = monster_pickmovetarget(targ);
+       }
+       
+       vector angles_face = vectoangles(self.moveto - self.origin);
+       self.angles_y = angles_face_y;
+       
+       if(self.state == MONSTER_STATE_ATTACK_LEAP && (self.flags & FL_ONGROUND))
+       {
+               self.state = 0;
+               self.touch = MonsterTouch;
+       }
+        
+       v_forward = normalize(self.moveto - self.origin);
+       
+       if(t1*l-t2*l>50 && (t1*l > 100 || t1 > 0.8))
+       if(self.flags & FL_ONGROUND)
+               movelib_jump_simple(100);
+
+       if(vlen(self.moveto - self.origin) > 64)
+       {
+               if(self.flags & FL_FLY)
+                       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)
+                               self.frame = ((self.enemy) ? manim_run : manim_walk);
+       }
+       else
+       {
+               movelib_beak_simple(stopspeed);
+                       if(time > self.attack_finished_single)
+                               if(time > self.pain_finished)
+                                       if (vlen(self.velocity) <= 30)
+                                               self.frame = manim_idle;
+       }
+               
+       if(self.enemy)
+       {
+               if(!self.checkattack)
+                       return; // to stop other code from crashing here
+                       
+               self.checkattack();
+       }
+}
+
+void monsters_setstatus()
+{
+       self.stat_monsters_total = monsters_total;
+       self.stat_monsters_killed = monsters_killed;
+}
+
+
+/*
+===================
+
+Monster spawn code
+
+===================
+*/
+
+void Monster_Appear ()
+{
+       self.enemy = activator;
+       self.spawnflags &~= MONSTERFLAG_APPEAR;
+       self.monster_spawnfunc();
+}
+
+entity FindTarget (entity ent) 
+{
+       if(MUTATOR_CALLHOOK(MonsterFindTarget)) { return self.goalentity; } // Handled by a mutator
+       local entity e;
+       for(e = world; (e = findflags(e, monster_attack, TRUE)); ) 
+       {
+               if(monster_isvalidtarget(e, ent, FALSE))
+               {
+                       return e;
+               }
+       }
+       return world;
+}
+
+void monsters_damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+{
+       if(self.frozen)
+               return;
+               
+       if(monster_isvalidtarget(attacker, self, FALSE))
+               self.enemy = attacker;
+       
+       self.health -= damage;
+       
+       if(self.sprite)
+       {
+               WaypointSprite_UpdateHealth(self.sprite, self.health);
+       }
+               
+       self.dmg_time = time;
+
+       if(sound_allowed(MSG_BROADCAST, attacker))
+               spamsound (self, CH_PAIN, "misc/bodyimpact1.wav", VOL_BASE, ATTN_NORM);  // FIXME: PLACEHOLDER
+       
+       if(self.damageforcescale < 1 && self.damageforcescale > 0)
+               self.velocity += force * self.damageforcescale;
+       else
+               self.velocity += force;
+               
+       Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, self, attacker);
+       if (damage > 50)
+               Violence_GibSplash_At(hitloc, force * -0.1, 3, 1, self, attacker);
+       if (damage > 100)
+               Violence_GibSplash_At(hitloc, force * -0.2, 3, 1, self, attacker);
+               
+       if(self.health <= 0)
+       {        
+               if(self.sprite)
+               {
+                       // Update one more time to avoid waypoint fading without emptying healthbar
+                       WaypointSprite_UpdateHealth(self.sprite, 0);
+               }
+               
+               if(self.flags & MONSTERFLAG_MINIBOSS) // TODO: cvarise the weapon drop?
+                       W_ThrowNewWeapon(self, WEP_NEX, 0, self.origin, self.velocity);
+                       
+               activator = attacker;
+               other = self.enemy;
+               self.target = self.target2;
+               self.target2 = "";
+               SUB_UseTargets();
+       
+               self.monster_die();     
+       }
+}
+
+// used to hook into monster post death functions without a mutator
+void monster_hook_death()
+{
+       if(self.sprite)
+        WaypointSprite_Kill(self.sprite);
+               
+       if(self.realowner.flags & FL_CLIENT)
+               self.realowner.monstercount -= 1;
+               
+       if(!(self.spawnflags & MONSTERFLAG_SPAWNED) && !self.monster_respawned)
+               monsters_killed += 1;
+               
+       if(self.realowner.flags & FL_CLIENT)
+                       self.realowner.monstercount -= 1;
+               
+       totalspawned -= 1;
+       
+       MUTATOR_CALLHOOK(MonsterDies);
+}
+
+// used to hook into monster post spawn functions without a mutator
+void monster_hook_spawn()
+{
+       self.health *= monster_skill; // skill based monster health?
+       self.max_health = self.health;
+       
+       if(teamplay && autocvar_g_monsters_teams)
+       {
+               self.colormod = TeamColor(self.team);
+               self.monster_attack = TRUE;
+       }
+       
+       if (self.target)
+       {
+               self.target2 = self.target;
+               self.goalentity = find(world, targetname, self.target);
+       }
+               
+       if(autocvar_g_monsters_healthbars)
+       {
+               WaypointSprite_Spawn(self.netname, 0, 600, self, '0 0 1' * self.sprite_height, world, 0, self, sprite, FALSE, RADARICON_DANGER, ((teamplay) ? TeamColor(self.team) : '1 0 0')); 
+               WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
+               WaypointSprite_UpdateHealth(self.sprite, self.health);
+       }
+       
+       MUTATOR_CALLHOOK(MonsterSpawn);
+}
+
+float monster_initialize(string  net_name,
+                                                string  bodymodel,
+                                                vector  min_s,
+                                                vector  max_s,
+                                                float   nodrop,
+                                                void() dieproc,
+                                                void() spawnproc)
+{
+       if not(autocvar_g_monsters)
+               return FALSE;
+               
+       // support for quake style removing monsters based on skill
+       if(autocvar_skill <= autocvar_g_monsters_skill_easy && (self.spawnflags & MONSTERSKILL_NOTEASY)) { return FALSE; }
+       else if(autocvar_skill == autocvar_g_monsters_skill_normal && (self.spawnflags & MONSTERSKILL_NOTMEDIUM)) { return FALSE; }
+       else if(autocvar_skill == autocvar_g_monsters_skill_hard && (self.spawnflags & MONSTERSKILL_NOTHARD)) { return FALSE; }
+       else if(autocvar_skill == autocvar_g_monsters_skill_insane && (self.spawnflags & MONSTERSKILL_NOTINSANE)) { return FALSE; }
+       else if(autocvar_skill >= autocvar_g_monsters_skill_nightmare && (self.spawnflags & MONSTERSKILL_NOTNIGHTMARE)) { return FALSE; }
+
+       if(self.model == "")
+       if(bodymodel == "")
+               error("monsters: missing bodymodel!");
+
+       if(self.netname == "")
+       {
+               if(net_name != "" && self.realowner.classname == STR_PLAYER)
+                       net_name = strzone(strdecolorize(sprintf("%s's %s", self.realowner.netname, net_name)));
+               self.netname = ((net_name == "") ? self.classname : net_name);
+       }
+       
+       if(self.spawnflags & MONSTERFLAG_GIANT && !autocvar_g_monsters_nogiants)
+               ScaleMonster(5);
+       else if(!self.scale)
+               ScaleMonster(1);
+       else
+               ScaleMonster(self.scale);
+               
+       Monster_CheckMinibossFlag();
+               
+       min_s *= self.scale;
+       max_s *= self.scale;
+
+       if(self.team && !teamplay)
+               self.team = 0;
+
+       self.flags = FL_MONSTER;
+       
+       if(self.model != "")
+               bodymodel = self.model;
+               
+       if not(self.spawnflags & MONSTERFLAG_SPAWNED) // naturally spawned monster
+       if not(self.monster_respawned)
+               monsters_total += 1;
+       
+       precache_model(bodymodel);
+
+       setmodel(self, bodymodel);
+       
+       setsize(self, min_s, max_s);
+
+       self.takedamage                 = DAMAGE_AIM;
+       self.bot_attack                 = TRUE;
+       self.iscreature                 = TRUE;
+       self.teleportable               = TRUE;
+       self.damagedbycontents  = TRUE;
+       self.damageforcescale   = 0.003;
+       self.monster_die                = dieproc;
+       self.event_damage               = monsters_damage;
+       self.touch                              = MonsterTouch;
+       self.use                                = monster_use;
+       self.solid                              = SOLID_BBOX;
+       self.movetype                   = MOVETYPE_WALK;
+       self.delay                              = -1; // used in attack delay code
+       monsters_spawned           += 1;
+       self.think                              = spawnproc;
+       self.nextthink                  = time;
+       self.enemy                              = world;
+       self.velocity                   = '0 0 0';
+       self.moveto                             = self.origin;
+       self.pos1                               = self.origin;
+       self.pos2                               = self.angles;
+       
+       if not(self.monster_moveflags)
+               self.monster_moveflags = MONSTER_MOVE_WANDER;
+
+       if(autocvar_g_nodepthtestplayers)
+               self.effects |= EF_NODEPTHTEST;
+
+       if(autocvar_g_fullbrightplayers)
+               self.effects |= EF_FULLBRIGHT;
+
+       if not(nodrop)
+       {
+               setorigin(self, self.origin);
+               tracebox(self.origin + '0 0 100', min_s, max_s, self.origin - '0 0 10000', MOVE_WORLDONLY, self);
+               setorigin(self, trace_endpos);
+       }
+
+       return TRUE;
+}
diff --git a/qcsrc/server/monsters/lib/spawn.qc b/qcsrc/server/monsters/lib/spawn.qc
new file mode 100644 (file)
index 0000000..6a9fa3f
--- /dev/null
@@ -0,0 +1,70 @@
+float spawnmonster_checkinlist(string monster, string list)
+{
+       string l = strcat(" ", list, " ");
+       
+       if(strstrofs(l, strcat(" ", monster, " "), 0) >= 0)
+               return TRUE;
+       
+       return FALSE;
+}
+
+entity spawnmonster (string monster, entity spawnedby, entity own, vector orig, float respwn, float moveflag)
+{
+       if not(autocvar_g_monsters)
+       {
+               if(spawnedby.flags & FL_CLIENT)
+                       sprint(spawnedby, "Monsters are disabled. Enable g_monsters to spawn monsters\n");
+               return world;
+       }
+       
+       if(spawnedby.vehicle) // no vehicle player spawning...
+               return world;
+       
+       if(!spawncode_first_load)
+       {
+               initialize_field_db();
+               spawncode_first_load = TRUE;
+       }
+       
+       entity e = spawn();
+       
+       e.spawnflags = MONSTERFLAG_SPAWNED;
+       
+       if not(respwn)
+               e.spawnflags |= MONSTERFLAG_NORESPAWN;
+       
+       setorigin(e, orig);
+       
+       if not(spawnmonster_checkinlist(monster, monsterlist()))
+               monster = "knight";
+       
+       e.realowner = spawnedby;
+       
+       if(moveflag)
+               e.monster_moveflags = moveflag;
+       
+       if (spawnedby.classname == "monster_swarm")
+               e.monster_owner = own;  
+       else if(spawnedby.flags & FL_CLIENT)
+       {
+               if(teamplay && autocvar_g_monsters_teams)
+                       e.team = spawnedby.team; // colors handled in spawn code
+                       
+               if not(teamplay)
+                       e.colormap = spawnedby.colormap;
+                       
+               if(autocvar_g_monsters_owners)
+                       e.monster_owner = own; // using owner makes the monster non-solid for its master
+                       
+               e.angles = spawnedby.angles;
+       }
+       
+       if(autocvar_g_monsters_giants_only)
+               e.spawnflags |= MONSTERFLAG_GIANT;
+               
+       monster = strcat("$ spawnfunc_monster_", monster);
+               
+       target_spawn_edit_entity(e, monster, world, world, world, world, world);
+               
+       return e;
+}
diff --git a/qcsrc/server/monsters/monster/demon.qc b/qcsrc/server/monsters/monster/demon.qc
new file mode 100644 (file)
index 0000000..1e54bec
--- /dev/null
@@ -0,0 +1,138 @@
+// cvars
+float autocvar_g_monster_demon;
+float autocvar_g_monster_demon_health;
+float autocvar_g_monster_demon_attack_jump_damage;
+float autocvar_g_monster_demon_damage;
+float autocvar_g_monster_demon_speed_walk;
+float autocvar_g_monster_demon_speed_run;
+
+// size
+const vector DEMON_MIN = '-32 -32 -24';
+const vector DEMON_MAX = '32 32 24';
+
+// animation
+#define demon_anim_stand  0
+#define demon_anim_walk   1
+#define demon_anim_run 2
+#define demon_anim_leap   3
+#define demon_anim_pain   4
+#define demon_anim_death  5
+#define demon_anim_attack 6
+
+void demon_think ()
+{
+       self.think = demon_think;
+       self.nextthink = time + 0.3;
+       
+       monster_move(autocvar_g_monster_demon_speed_run, autocvar_g_monster_demon_speed_walk, 100, demon_anim_run, demon_anim_walk, demon_anim_stand);
+}
+
+void demon_attack_melee ()
+{
+       float bigdmg = autocvar_g_monster_demon_damage * self.scale;
+       
+       self.frame = demon_anim_attack;
+       self.attack_finished_single = time + 1;
+       
+       monster_melee(self.enemy, bigdmg * monster_skill, 120, DEATH_MONSTER_DEMON_MELEE);
+}
+
+void Demon_JumpTouch ()
+{
+       if (self.health <= 0)
+               return;
+               
+       float bigdmg = autocvar_g_monster_demon_attack_jump_damage * self.scale;
+
+       if (monster_isvalidtarget(other, self, FALSE))
+       {
+               if (vlen(self.velocity) > 300)
+               {
+                       Damage(other, self, self, bigdmg * monster_skill, DEATH_MONSTER_DEMON_JUMP, other.origin, normalize(other.origin - self.origin));
+                       self.touch = MonsterTouch; // instantly turn it off to stop damage spam
+               }
+       }
+
+       if(self.flags & FL_ONGROUND)
+               self.touch = MonsterTouch;
+}
+
+float demon_jump ()
+{
+       makevectors(self.angles);
+       if(monster_leap(demon_anim_leap, Demon_JumpTouch, v_forward * 700 + '0 0 300', 0.8))
+               return TRUE;
+               
+       return FALSE;
+}
+
+void demon_die ()
+{
+       Monster_CheckDropCvars ("demon");
+       
+       self.frame                      = demon_anim_death;
+       self.think                      = Monster_Fade; 
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.movetype           = MOVETYPE_TOSS;
+       self.enemy                      = world;
+       self.nextthink          = time + 3;
+       self.pain_finished  = self.nextthink;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void demon_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_demon_health * self.scale;
+
+       self.damageforcescale   = 0;
+       self.classname                  = "monster_demon";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_melee               = demon_attack_melee;
+       self.attack_ranged              = demon_jump;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.frame                              = demon_anim_stand;
+       self.think                              = demon_think;
+       self.sprite_height              = 30 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+/* QUAKED monster_demon (1 0 0) (-32 -32 -24) (32 32 64) Ambush */
+void spawnfunc_monster_demon ()
+{      
+       if not(autocvar_g_monster_demon)
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_demon;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Fiend",
+                        "models/monsters/demon.mdl",
+                        DEMON_MIN, DEMON_MAX,
+                        FALSE,
+                        demon_die, demon_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
+
+// Compatibility with old spawns
+void spawnfunc_monster_demon1 () { spawnfunc_monster_demon(); }
diff --git a/qcsrc/server/monsters/monster/dog.qc b/qcsrc/server/monsters/monster/dog.qc
new file mode 100644 (file)
index 0000000..aa65d6d
--- /dev/null
@@ -0,0 +1,127 @@
+// size
+const vector DOG_MAX = '16 16 12';
+const vector DOG_MIN = '-16 -16 -24';
+
+// cvars
+float autocvar_g_monster_dog;
+float autocvar_g_monster_dog_health;
+float autocvar_g_monster_dog_bite_damage;
+float autocvar_g_monster_dog_attack_jump_damage;
+float autocvar_g_monster_dog_speed_walk;
+float autocvar_g_monster_dog_speed_run;
+
+// animations
+#define dog_anim_idle          0
+#define dog_anim_walk          1
+#define dog_anim_run           2
+#define dog_anim_attack        3
+#define dog_anim_die           4
+#define dog_anim_pain          5
+
+void Dog_JumpTouch ()
+{
+       float bigdmg = autocvar_g_monster_dog_attack_jump_damage * self.scale;
+       if (self.health <= 0)
+               return;
+
+       if (other.takedamage)
+       {
+               if (vlen(self.velocity) > 300)
+                       Damage(self.enemy, self, self, bigdmg * monster_skill, DEATH_MONSTER_DOG_JUMP, self.enemy.origin, normalize(self.enemy.origin - self.origin));
+       }
+
+       if(self.flags & FL_ONGROUND)
+               self.touch = MonsterTouch;
+}
+
+void dog_think ()
+{
+       self.think = dog_think;
+       self.nextthink = time + 0.3;
+       
+       monster_move(autocvar_g_monster_dog_speed_run, autocvar_g_monster_dog_speed_walk, 50, dog_anim_run, dog_anim_walk, dog_anim_idle);
+}
+
+void dog_attack ()
+{
+       float bigdmg = autocvar_g_monster_dog_bite_damage * self.scale;
+       
+       self.frame = dog_anim_attack;
+       self.attack_finished_single = time + 0.7;
+
+       monster_melee(self.enemy, bigdmg * monster_skill, 100, DEATH_MONSTER_DOG_BITE);
+}
+
+float dog_jump ()
+{
+       makevectors(self.angles);
+       if(monster_leap(dog_anim_attack, Dog_JumpTouch, v_forward * 300 + '0 0 200', 0.8))
+               return TRUE;
+               
+       return FALSE;
+}
+
+void dog_die ()
+{
+       Monster_CheckDropCvars ("dog");
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.nextthink          = time + 2.1;
+       self.think                      = Monster_Fade;
+       self.pain_finished  = self.nextthink;
+       self.movetype           = MOVETYPE_TOSS;
+       self.frame                      = dog_anim_die;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void dog_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_dog_health * self.scale;
+
+       self.damageforcescale   = 0;
+       self.classname                  = "monster_dog";
+       self.attack_melee               = dog_attack;
+       self.attack_ranged              = dog_jump;
+       self.checkattack                = GenericCheckAttack;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = dog_think;
+       self.frame                              = dog_anim_idle;
+       self.sprite_height              = 20 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_dog ()
+{      
+       if not(autocvar_g_monster_dog)
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_dog;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       if not (monster_initialize(
+                        "Cerberus",
+                        "models/monsters/dog.dpm",
+                        DOG_MIN, DOG_MAX,
+                        FALSE,
+                        dog_die, dog_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
diff --git a/qcsrc/server/monsters/monster/enforcer.qc b/qcsrc/server/monsters/monster/enforcer.qc
new file mode 100644 (file)
index 0000000..2e3fa1a
--- /dev/null
@@ -0,0 +1,211 @@
+// size
+const vector ENFORCER_MIN = '-16 -16 -24';
+const vector ENFORCER_MAX = '16 16 24';
+
+// cvars
+float autocvar_g_monster_enforcer;
+float autocvar_g_monster_enforcer_health;
+float autocvar_g_monster_enforcer_speed_walk;
+float autocvar_g_monster_enforcer_speed_run;
+float autocvar_g_monster_enforcer_attack_uzi_bullets;
+
+// animations
+#define enforcer_anim_stand    0
+#define enforcer_anim_walk             1
+#define enforcer_anim_run              2
+#define enforcer_anim_attack   3
+#define enforcer_anim_death1   4
+#define enforcer_anim_death2   5
+#define enforcer_anim_pain1    6
+#define enforcer_anim_pain2    7
+#define enforcer_anim_pain3    8
+#define enforcer_anim_pain4    9
+
+void enforcer_think ()
+{
+       self.think = enforcer_think;
+       self.nextthink = time + 0.3;
+       
+       if(self.delay != -1)
+               self.nextthink = self.delay;
+       
+       if(time < self.attack_finished_single)
+               monster_move(0, 0, 0, enforcer_anim_attack, enforcer_anim_attack, enforcer_anim_attack);
+       else
+               monster_move(autocvar_g_monster_enforcer_speed_run, autocvar_g_monster_enforcer_speed_walk, 100, enforcer_anim_run, enforcer_anim_walk, enforcer_anim_stand);
+}
+
+void enforcer_laser ()
+{
+       self.frame = enforcer_anim_attack;
+       self.attack_finished_single = time + 0.8;
+       W_Laser_Attack(0);
+}
+
+float enf_missile_laser ()
+{
+       enforcer_laser();
+       return TRUE;
+}
+
+void enforcer_shotgun ()
+{
+       self.frame = enforcer_anim_attack;
+       self.attack_finished_single = time + 0.8;
+       W_Shotgun_Attack();
+}
+
+float enf_missile_shotgun ()
+{
+       enforcer_shotgun();
+       return TRUE;
+}
+
+.float enf_cycles;
+void enforcer_uzi_fire ()
+{
+       self.enf_cycles += 1;
+       
+       if(self.enf_cycles > autocvar_g_monster_enforcer_attack_uzi_bullets)
+       {
+               self.monster_delayedattack = func_null;
+               self.delay = -1;
+               return;
+       }
+       W_UZI_Attack(DEATH_MONSTER_ENFORCER_NAIL);
+       self.delay = time + 0.1;
+       self.monster_delayedattack = enforcer_uzi_fire;
+}
+
+void enforcer_uzi ()
+{
+       self.frame = enforcer_anim_attack;
+       self.attack_finished_single = time + 0.8;
+       self.delay = time + 0.1;
+       self.monster_delayedattack = enforcer_uzi_fire;
+}
+
+float enf_missile_uzi ()
+{
+       self.enf_cycles = 0;
+       enforcer_uzi();
+       return TRUE;
+}
+
+void enforcer_rl ()
+{
+       self.frame = enforcer_anim_attack;
+       self.attack_finished_single = time + 0.8;
+       W_Rocket_Attack();
+}
+
+float enf_missile_rocket ()
+{
+       enforcer_rl();
+       return TRUE;
+}
+
+void enforcer_electro ()
+{
+       self.frame = enforcer_anim_attack;
+       self.attack_finished_single = time + 0.8;
+       W_Electro_Attack();
+}
+
+float enf_missile_plasma ()
+{
+       enforcer_electro();
+       return TRUE;
+}
+
+void enforcer_die ()
+{
+       Monster_CheckDropCvars ("enforcer");
+       
+       self.solid                      = SOLID_NOT;
+       self.movetype           = MOVETYPE_TOSS;
+       self.think                      = Monster_Fade;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       
+       if (self.attack_ranged == enf_missile_rocket)
+               W_ThrowNewWeapon(self, WEP_ROCKET_LAUNCHER, 0, self.origin, self.velocity);
+       else if (self.attack_ranged == enf_missile_plasma)
+               W_ThrowNewWeapon(self, WEP_ELECTRO, 0, self.origin, self.velocity);
+       else if (self.attack_ranged == enf_missile_shotgun)
+               W_ThrowNewWeapon(self, WEP_SHOTGUN, 0, self.origin, self.velocity);        
+       else if (self.attack_ranged == enf_missile_uzi)
+               W_ThrowNewWeapon(self, WEP_UZI, 0, self.origin, self.velocity);
+       else
+               W_ThrowNewWeapon(self, WEP_LASER, 0, self.origin, self.velocity);
+               
+       if (random() > 0.5)
+               self.frame = enforcer_anim_death1;
+       else
+               self.frame = enforcer_anim_death2;
+               
+       monster_hook_death(); // for post-death mods
+}
+
+void enforcer_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_enforcer_health * self.scale;
+
+       self.damageforcescale   = 0;
+       self.classname                  = "monster_enforcer";
+       self.checkattack                = GenericCheckAttack;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = enforcer_think;
+       self.items                              = (IT_SHELLS | IT_ROCKETS | IT_NAILS | IT_CELLS);
+       self.sprite_height              = 30 * self.scale;
+       
+       local float r = random();
+       if (r < 0.20)
+               self.attack_ranged = enf_missile_rocket;
+       else if (r < 0.40)
+               self.attack_ranged = enf_missile_plasma;
+       else if (r < 0.60)
+               self.attack_ranged = enf_missile_shotgun;         
+       else if (r < 0.80)
+               self.attack_ranged = enf_missile_uzi;
+       else
+               self.attack_ranged = enf_missile_laser;
+               
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_enforcer ()
+{      
+       if not(autocvar_g_monster_enforcer)
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_enforcer;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Enforcer",
+                        "models/monsters/enforcer.mdl",
+                        ENFORCER_MIN, ENFORCER_MAX,
+                        FALSE,
+                        enforcer_die, enforcer_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
diff --git a/qcsrc/server/monsters/monster/fish.qc b/qcsrc/server/monsters/monster/fish.qc
new file mode 100644 (file)
index 0000000..970fd47
--- /dev/null
@@ -0,0 +1,99 @@
+// size
+const vector FISH_MIN = '-16 -16 -24';
+const vector FISH_MAX = '16 16 16';
+
+// cvars
+float autocvar_g_monster_fish;
+float autocvar_g_monster_fish_health;
+float autocvar_g_monster_fish_damage;
+float autocvar_g_monster_fish_speed_walk;
+float autocvar_g_monster_fish_speed_run;
+
+// animations
+#define fish_anim_attack 0
+#define fish_anim_death  1
+#define fish_anim_swim   2
+#define fish_anim_pain   3
+
+void fish_think ()
+{
+       self.think = fish_think;
+       self.nextthink = time + 0.3;
+       
+       monster_move(autocvar_g_monster_fish_speed_run, autocvar_g_monster_fish_speed_walk, 10, fish_anim_swim, fish_anim_swim, fish_anim_swim);
+}
+
+void fish_attack ()
+{
+       float bigdmg = autocvar_g_monster_fish_damage * self.scale;
+       
+       self.frame = fish_anim_attack;
+       self.attack_finished_single = time + 0.5;
+
+       monster_melee(self.enemy, bigdmg * monster_skill, 60, DEATH_MONSTER_FISH_BITE);
+}
+
+void fish_die ()
+{
+       Monster_CheckDropCvars ("fish");
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.pain_finished  = self.nextthink;
+       self.frame                      = fish_anim_death;
+       self.think                      = Monster_Fade;
+       self.nextthink          = time + 2.1;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void fish_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_fish_health * self.scale;
+
+       self.damageforcescale   = 0.5;
+       self.classname                  = "monster_fish";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_melee               = fish_attack;
+       self.flags                         |= FL_SWIM;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = fish_think;
+       self.sprite_height              = 20 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_fish ()
+{      
+       if not(autocvar_g_monster_fish)
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_fish;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Rotfish",
+                        "models/monsters/fish.mdl",
+                        FISH_MIN, FISH_MAX,
+                        TRUE,
+                        fish_die, fish_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
diff --git a/qcsrc/server/monsters/monster/hknight.qc b/qcsrc/server/monsters/monster/hknight.qc
new file mode 100644 (file)
index 0000000..f1d7e3c
--- /dev/null
@@ -0,0 +1,473 @@
+// size
+const vector HELLKNIGHT_MIN = '-16 -16 -24';
+const vector HELLKNIGHT_MAX = '16 16 32';
+
+// cvars
+float autocvar_g_monster_hellknight;
+float autocvar_g_monster_hellknight_health;
+float autocvar_g_monster_hellknight_melee_damage;
+float autocvar_g_monster_hellknight_inferno_damage;
+float autocvar_g_monster_hellknight_inferno_damagetime;
+float autocvar_g_monster_hellknight_inferno_chance;
+float autocvar_g_monster_hellknight_speed_walk;
+float autocvar_g_monster_hellknight_speed_run;
+float autocvar_g_monster_hellknight_fireball_damage;
+float autocvar_g_monster_hellknight_fireball_force;
+float autocvar_g_monster_hellknight_fireball_radius;
+float autocvar_g_monster_hellknight_fireball_chance;
+float autocvar_g_monster_hellknight_fireball_edgedamage;
+float autocvar_g_monster_hellknight_spike_chance;
+float autocvar_g_monster_hellknight_spike_force;
+float autocvar_g_monster_hellknight_spike_radius;
+float autocvar_g_monster_hellknight_spike_edgedamage;
+float autocvar_g_monster_hellknight_spike_damage;
+float autocvar_g_monster_hellknight_jump_chance;
+float autocvar_g_monster_hellknight_jump_damage;
+float autocvar_g_monster_hellknight_jump_dist;
+
+// animations
+#define hellknight_anim_stand  0
+#define hellknight_anim_walk   1
+#define hellknight_anim_run    2
+#define hellknight_anim_pain   3
+#define hellknight_anim_death1         4
+#define hellknight_anim_death2         5
+#define hellknight_anim_charge1 6
+#define hellknight_anim_magic1         7
+#define hellknight_anim_magic2         8
+#define hellknight_anim_charge2 9
+#define hellknight_anim_slice  10
+#define hellknight_anim_smash  11
+#define hellknight_anim_wattack 12
+#define hellknight_anim_magic3         13
+
+void hknight_spike_think()
+{
+       if(self)
+       {
+               RadiusDamage (self, self.realowner, autocvar_g_monster_hellknight_spike_damage * self.realowner.scale, autocvar_g_monster_hellknight_spike_edgedamage, autocvar_g_monster_hellknight_spike_force, world, autocvar_g_monster_hellknight_spike_radius, WEP_CRYLINK, other);
+               remove(self);
+       }
+}
+
+void hknight_spike_touch()
+{
+       PROJECTILE_TOUCH;
+       
+       pointparticles(particleeffectnum("TE_WIZSPIKE"), self.origin, '0 0 0', 1);
+       
+       hknight_spike_think();
+}
+
+void() hellknight_think;
+void hknight_shoot ()
+{
+       local   entity  missile = world;
+       local   vector  dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
+       local   float   dist = vlen (self.enemy.origin - self.origin), flytime = 0;
+
+       flytime = dist * 0.002;
+       if (flytime < 0.1)
+               flytime = 0.1;
+
+       self.effects |= EF_MUZZLEFLASH;
+       sound (self, CHAN_WEAPON, "weapons/spike.wav", 1, ATTN_NORM);
+
+       missile = spawn ();
+       missile.owner = missile.realowner = self;
+       missile.solid = SOLID_TRIGGER;
+       missile.movetype = MOVETYPE_FLYMISSILE;
+       setsize (missile, '0 0 0', '0 0 0');            
+       setorigin(missile, self.origin + '0 0 10' + v_forward * 14);
+       missile.scale = self.scale;
+       missile.velocity = dir * 400;
+       missile.avelocity = '300 300 300';
+       missile.nextthink = time + 5;
+       missile.think = hknight_spike_think;
+       missile.enemy = self.enemy;
+       missile.touch = hknight_spike_touch;
+       CSQCProjectile(missile, TRUE, PROJECTILE_CRYLINK, TRUE);
+}
+
+void hknight_inferno ()
+{
+       traceline((self.absmin + self.absmax) * 0.5, (self.enemy.absmin + self.enemy.absmax) * 0.5, TRUE, world);
+       if (trace_fraction != 1)
+               return; // not visible
+       if(enemy_range() <= 2000)
+               Fire_AddDamage(self.enemy, self, autocvar_g_monster_hellknight_inferno_damage * monster_skill, autocvar_g_monster_hellknight_inferno_damagetime, self.projectiledeathtype);
+}
+
+void hknight_infernowarning ()
+{
+       if(!self.enemy)
+               return;
+               
+       traceline((self.absmin + self.absmax) * 0.5, (self.enemy.absmin + self.enemy.absmax) * 0.5, TRUE, world);
+       if (trace_fraction != 1)
+               return; // not visible
+       self.enemy.effects |= EF_MUZZLEFLASH;
+       sound(self.enemy, CHAN_AUTO, "player/lava.wav", 1, ATTN_NORM);
+       
+       hknight_inferno();
+}
+
+float() hknight_magic;
+float hknight_checkmagic ()
+{
+       local vector v1 = '0 0 0', v2 = '0 0 0';
+       local float dot = 0;
+
+       // use magic to kill zombies as they heal too fast for sword
+       if (self.enemy.classname == "monster_zombie")
+       {
+               traceline((self.absmin + self.absmax) * 0.5, (self.enemy.absmin + self.enemy.absmax) * 0.5, FALSE, self);
+               if (trace_ent == self.enemy)
+               {
+                       hknight_magic();
+                       return TRUE;
+               }
+       }
+
+       if (random() < 0.25)
+               return FALSE; // 25% of the time it won't do anything
+       v1 = normalize(self.enemy.velocity);
+       v2 = normalize(self.enemy.origin - self.origin);
+       dot = v1 * v2;
+       if (dot >= 0.7) // moving away
+       if (vlen(self.enemy.velocity) >= 150) // walking/running away
+               return hknight_magic();
+       return FALSE;
+}
+
+void() hellknight_charge;
+void CheckForCharge ()
+{
+       // check for mad charge
+       if (time < self.attack_finished_single)
+               return;
+       if (fabs(self.origin_z - self.enemy.origin_z) > 20)
+               return;         // too much height change
+       if (vlen (self.origin - self.enemy.origin) < 80)
+               return;         // use regular attack
+       if (hknight_checkmagic())
+               return; // chose magic
+
+       // charge
+       hellknight_charge();
+}
+
+void CheckContinueCharge ()
+{
+       if(hknight_checkmagic())
+               return; // chose magic
+       if(time >= self.attack_finished_single)
+       {
+               hellknight_think();
+               return;         // done charging
+       }
+}
+
+void hellknight_think ()
+{
+       self.think = hellknight_think;
+       self.nextthink = time + 0.3;
+       
+       monster_move(autocvar_g_monster_hellknight_speed_run, autocvar_g_monster_hellknight_speed_walk, 100, hellknight_anim_run, hellknight_anim_walk, hellknight_anim_stand);
+}
+
+.float hknight_cycles;
+void hellknight_magic ()
+{
+       self.hknight_cycles += 1;
+       self.think = hellknight_magic;
+       
+       if(self.hknight_cycles >= 5)
+       {
+               self.frame = hellknight_anim_magic1;
+               self.attack_finished_single = time + 0.7;
+               hknight_infernowarning();
+               self.think = hellknight_think;
+       }
+       
+       self.nextthink = time + 0.1;
+}
+
+void hknight_fireball_explode(entity targ)
+{
+       float scle = self.realowner.scale;
+       if(self)
+       {
+               RadiusDamage (self, self.realowner, autocvar_g_monster_hellknight_fireball_damage * scle, autocvar_g_monster_hellknight_fireball_edgedamage * scle, autocvar_g_monster_hellknight_fireball_force * scle, world, autocvar_g_monster_hellknight_fireball_radius * scle, WEP_FIREBALL, targ);
+               if(targ)
+                       Fire_AddDamage(targ, self, 5 * monster_skill, autocvar_g_monster_hellknight_inferno_damagetime, self.projectiledeathtype);
+               remove(self);
+       }
+}
+
+void hknight_fireball_think()
+{
+       hknight_fireball_explode(world);
+}
+
+void hknight_fireball_touch()
+{
+       PROJECTILE_TOUCH;
+       
+       hknight_fireball_explode(other);
+}
+
+void hellknight_fireball ()
+{
+       local   entity  missile = spawn();
+       local   vector  dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
+       vector fmins = ((self.scale >= 2) ? '-16 -16 -16' : '-4 -4 -4'), fmaxs = ((self.scale >= 2) ? '16 16 16' : '4 4 4');
+
+       self.effects |= EF_MUZZLEFLASH;
+       sound (self, CHAN_WEAPON, "weapons/fireball2.wav", 1, ATTN_NORM);
+
+       missile.owner = missile.realowner = self;
+       missile.solid = SOLID_TRIGGER;
+       missile.movetype = MOVETYPE_FLYMISSILE;
+       setsize (missile, fmins, fmaxs);                
+       setorigin(missile, self.origin + '0 0 10' + v_forward * 14);
+       missile.velocity = dir * 400;
+       missile.avelocity = '300 300 300';
+       missile.nextthink = time + 5;
+       missile.think = hknight_fireball_think;
+       missile.enemy = self.enemy;
+       missile.touch = hknight_fireball_touch;
+       CSQCProjectile(missile, TRUE, ((self.scale >= 2) ? PROJECTILE_FIREBALL : PROJECTILE_FIREMINE), TRUE);
+       
+       self.delay = -1;
+}
+
+void hellknight_magic2 ()
+{
+       self.frame = hellknight_anim_magic2;
+       self.attack_finished_single = time + 1.2;
+       self.delay = time + 0.4;
+       self.monster_delayedattack = hellknight_fireball;
+}
+
+void hellknight_spikes ()
+{
+       self.think = hellknight_spikes;
+       self.nextthink = time + 0.1;
+       self.hknight_cycles += 1;
+       hknight_shoot();
+       if(self.hknight_cycles >= 7)
+               self.think = hellknight_think;
+}
+
+void hellknight_magic3 ()
+{
+       self.frame = hellknight_anim_magic3;
+       self.attack_finished_single = time + 1;
+       self.think = hellknight_spikes;
+       self.nextthink = time + 0.4;
+}
+
+void hellknight_charge ()
+{
+       self.frame = hellknight_anim_charge1;
+       self.attack_finished_single = time + 0.5;
+       
+       hknight_checkmagic();
+       monster_melee(self.enemy, autocvar_g_monster_hellknight_melee_damage, 70, DEATH_MONSTER_MELEE);
+       hknight_checkmagic();
+}
+
+void hellknight_charge2 ()
+{
+       self.frame = hellknight_anim_charge2;
+       self.attack_finished_single = time + 0.5;
+       
+       CheckContinueCharge ();
+       monster_melee(self.enemy, autocvar_g_monster_hellknight_melee_damage, 70, DEATH_MONSTER_MELEE);
+}
+
+void hellknight_slice ()
+{
+       self.frame = hellknight_anim_slice;
+       self.attack_finished_single = time + 0.7;
+       monster_melee(self.enemy, autocvar_g_monster_hellknight_melee_damage, 70, DEATH_MONSTER_MELEE);
+}
+
+void hellknight_smash ()
+{
+       self.frame = hellknight_anim_smash;
+       self.attack_finished_single = time + 0.7;
+       monster_melee(self.enemy, autocvar_g_monster_hellknight_melee_damage, 70, DEATH_MONSTER_MELEE);
+}
+
+void hellknight_weapon_attack ()
+{
+       self.frame = hellknight_anim_wattack;
+       self.attack_finished_single = time + 0.7;
+       monster_melee(self.enemy, autocvar_g_monster_hellknight_melee_damage, 70, DEATH_MONSTER_MELEE);
+}
+
+float hknight_type;
+void hknight_melee ()
+{
+       hknight_type += 1;
+
+       if (hknight_type == 1)
+               hellknight_slice();
+       else if (hknight_type == 2)
+               hellknight_smash();
+       else
+       {
+               hellknight_weapon_attack();
+               hknight_type = 0;
+       }
+}
+
+float hknight_magic ()
+{
+       if not(self.flags & FL_ONGROUND)
+               return FALSE;
+               
+       if not(self.enemy)
+               return FALSE; // calling attack check with no enemy?!
+               
+       if(time < self.attack_finished_single)
+               return FALSE;
+               
+       self.hknight_cycles = 0;
+
+       if (self.enemy.classname == "monster_zombie")
+       {
+               // always use fireball to kill zombies
+               hellknight_magic2();
+               self.attack_finished_single = time + 2;
+               return TRUE;
+       }
+       RandomSelection_Init();
+       RandomSelection_Add(world, 0, "fireball", autocvar_g_monster_hellknight_fireball_chance, 1);
+       RandomSelection_Add(world, 0, "inferno", autocvar_g_monster_hellknight_inferno_chance, 1);
+       RandomSelection_Add(world, 0, "spikes", autocvar_g_monster_hellknight_spike_chance, 1);
+       if(self.health >= 100)
+               RandomSelection_Add(world, 0, "jump", ((enemy_range() > autocvar_g_monster_hellknight_jump_dist * self.scale) ? 1 : autocvar_g_monster_hellknight_jump_chance), 1);
+       
+       switch(RandomSelection_chosen_string)
+       {
+               case "fireball":
+               {
+                       hellknight_magic2();
+                       self.attack_finished_single = time + 2;
+                       return TRUE;
+               }
+               case "spikes":
+               {
+                       hellknight_magic3();
+                       self.attack_finished_single = time + 3;
+                       return TRUE;
+               }
+               case "inferno":
+               {
+                       hellknight_magic();
+                       self.attack_finished_single = time + 3;
+                       return TRUE;
+               }
+               case "jump":
+               {
+                       if (enemy_range() >= 400)
+                       if (findtrajectorywithleading(self.origin, self.mins, self.maxs, self.enemy, 1000, 0, 10, 0, self))
+                       {
+                               self.velocity = findtrajectory_velocity;
+                               Damage(self.enemy, self, self, autocvar_g_monster_hellknight_jump_damage * monster_skill, DEATH_VHCRUSH, self.enemy.origin, normalize(self.enemy.origin - self.origin));
+                               self.attack_finished_single = time + 2;
+                               return TRUE;
+                       }
+                       return FALSE;
+               }
+               default:
+                       return FALSE;
+       }
+       // never get here
+}
+
+void hellknight_die ()
+{
+       float chance = random();
+       Monster_CheckDropCvars ("hellknight");
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.movetype           = MOVETYPE_TOSS;
+       self.think                      = Monster_Fade;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       
+       if(chance < 0.10 || self.flags & MONSTERFLAG_MINIBOSS)
+       {
+               self.superweapons_finished = time + autocvar_g_balance_superweapons_time;
+               W_ThrowNewWeapon(self, WEP_FIREBALL, 0, self.origin, self.velocity);
+       }
+       
+       if (random() > 0.5)
+               self.frame = hellknight_anim_death1;
+       else
+               self.frame = hellknight_anim_death2;
+               
+       monster_hook_death(); // for post-death mods
+}
+
+void hellknight_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_hellknight_health * self.scale;
+
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_hellknight";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_melee               = hknight_melee;
+       self.attack_ranged              = hknight_magic;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = hellknight_think;
+       self.sprite_height              = 30 * self.scale;
+       self.frame                              = hellknight_anim_stand;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_hell_knight ()
+{      
+       if not(autocvar_g_monster_hellknight)
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_hell_knight;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Hell-knight",
+                        "models/monsters/hknight.mdl",
+                        HELLKNIGHT_MIN, HELLKNIGHT_MAX,
+                        FALSE,
+                        hellknight_die, hellknight_spawn))
+       {
+               remove(self);
+               return;
+       }
+       
+       precache_sound ("weapons/spike.wav");
+}
+
+// compatibility with old spawns
+void spawnfunc_monster_hellknight () { spawnfunc_monster_hell_knight(); }
diff --git a/qcsrc/server/monsters/monster/knight.qc b/qcsrc/server/monsters/monster/knight.qc
new file mode 100644 (file)
index 0000000..4aa6254
--- /dev/null
@@ -0,0 +1,108 @@
+// size
+const vector KNIGHT_MIN = '-16 -16 -24';
+const vector KNIGHT_MAX = '16 16 32';
+       
+// cvars
+float autocvar_g_monster_knight;
+float autocvar_g_monster_knight_health;
+float autocvar_g_monster_knight_melee_damage;
+float autocvar_g_monster_knight_speed_walk;
+float autocvar_g_monster_knight_speed_run;
+
+// animations
+#define knight_anim_stand              0
+#define knight_anim_run                1
+#define knight_anim_runattack  2
+#define knight_anim_pain1              3
+#define knight_anim_pain2              4
+#define knight_anim_attack             5
+#define knight_anim_walk               6
+#define knight_anim_kneel              7
+#define knight_anim_standing   8
+#define knight_anim_death1             9
+#define knight_anim_death2             10
+
+void knight_think ()
+{
+       self.think = knight_think;
+       self.nextthink = time + 0.3;
+       
+       monster_move(autocvar_g_monster_knight_speed_run, autocvar_g_monster_knight_speed_walk, 50, knight_anim_run, knight_anim_walk, knight_anim_stand);
+}
+
+void knight_attack ()
+{
+       local float len = vlen(self.velocity);
+
+       self.frame = ((len < 50) ? knight_anim_attack : knight_anim_runattack);
+       
+       self.attack_finished_single = time + 0.9;
+       
+       monster_melee(self.enemy, autocvar_g_monster_knight_melee_damage, 80, DEATH_MONSTER_MELEE);
+}
+
+void knight_die ()
+{
+       Monster_CheckDropCvars ("knight");
+               
+       self.frame                      = ((random() > 0.5) ? knight_anim_death1 : knight_anim_death2);
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.think                      = Monster_Fade;
+       self.movetype           = MOVETYPE_TOSS;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void knight_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_knight_health * self.scale;
+
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_knight";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_melee               = knight_attack;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = knight_think;
+       self.sprite_height              = 30 * self.scale;
+       self.frame                              = knight_anim_stand;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_knight ()
+{      
+       if not(autocvar_g_monster_knight)
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_knight;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Knight",
+                        "models/monsters/knight.mdl",
+                        KNIGHT_MIN, KNIGHT_MAX,
+                        FALSE,
+                        knight_die, knight_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
diff --git a/qcsrc/server/monsters/monster/ogre.qc b/qcsrc/server/monsters/monster/ogre.qc
new file mode 100644 (file)
index 0000000..6b30c8e
--- /dev/null
@@ -0,0 +1,213 @@
+// size
+const vector OGRE_MIN = '-32 -32 -24';
+const vector OGRE_MAX = '32 32 32';
+// cvars
+float autocvar_g_monster_ogre;
+float autocvar_g_monster_ogre_health;
+float autocvar_g_monster_ogre_chainsaw_damage;
+float autocvar_g_monster_ogre_speed_walk;
+float autocvar_g_monster_ogre_speed_run;
+float autocvar_g_monster_ogre_attack_uzi_bullets;
+
+// animations
+#define ogre_anim_stand        0
+#define ogre_anim_walk                 1
+#define ogre_anim_run          2
+#define ogre_anim_swing        3
+#define ogre_anim_smash        4
+#define ogre_anim_shoot        5
+#define ogre_anim_pain1        6
+#define ogre_anim_pain2        7
+#define ogre_anim_pain3        8
+#define ogre_anim_pain4        9
+#define ogre_anim_pain5        10
+#define ogre_anim_death1       11
+#define ogre_anim_death2       12
+#define ogre_anim_pull                 13
+
+void chainsaw (float side)
+{
+       if (!self.enemy)
+               return;
+
+       if (enemy_range() > 100 * self.scale)
+               return;
+
+       Damage(self.enemy, self, self, autocvar_g_monster_ogre_chainsaw_damage * monster_skill, DEATH_MONSTER_OGRE_CHAINSAW, self.enemy.origin, normalize(self.enemy.origin - self.origin));
+}
+
+void ogre_think ()
+{
+       self.think = ogre_think;
+       self.nextthink = time + 0.3;
+       
+       if(self.delay != -1)
+               self.nextthink = self.delay;
+       
+       monster_move(autocvar_g_monster_ogre_speed_run, autocvar_g_monster_ogre_speed_walk, 300, ogre_anim_run, ogre_anim_walk, ogre_anim_stand);
+}
+
+.float ogre_cycles;
+void ogre_swing ()
+{
+       self.ogre_cycles += 1;
+       self.frame = ogre_anim_swing;
+       if(self.ogre_cycles == 1)
+               self.attack_finished_single = time + 1.3;
+       self.angles_y = self.angles_y + random()* 25;
+       self.nextthink = time + 0.2;
+       self.think = ogre_swing;
+       
+       if(self.ogre_cycles <= 3)
+               chainsaw(200);
+       else if(self.ogre_cycles <= 8)
+               chainsaw(-200);
+       else
+               chainsaw(0);
+       
+       if(self.ogre_cycles >= 10)
+               self.think = ogre_think;
+}
+
+void ogre_smash_2 ()
+{
+       chainsaw(0);
+}
+
+void ogre_smash ()
+{
+       self.frame = ogre_anim_smash;
+       self.attack_finished_single = time + 0.5;
+       chainsaw(0);
+       self.monster_delayedattack = ogre_smash_2;
+       self.delay = time + 0.1;
+}
+
+void ogre_uzi_fire ()
+{
+       self.ogre_cycles += 1;
+       
+       if(self.ogre_cycles > autocvar_g_monster_ogre_attack_uzi_bullets)
+       {
+               self.monster_delayedattack = func_null;
+               self.delay = -1;
+               return;
+       }
+       W_UZI_Attack(DEATH_MONSTER_OGRE_NAIL);
+       self.delay = time + 0.1;
+       self.monster_delayedattack = ogre_uzi_fire;
+}
+
+void ogre_uzi ()
+{
+       self.frame = ogre_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+       self.delay = time + 0.1;
+       self.monster_delayedattack = ogre_uzi_fire;
+}
+
+void ogre_gl ()
+{
+       W_Grenade_Attack2();
+       self.frame = ogre_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+}
+
+float ogre_missile ()
+{
+       self.ogre_cycles = 0;
+       if (random() < 0.20)
+       {
+               ogre_uzi();
+               return TRUE;
+       }
+       else
+       {
+               ogre_gl();
+               return TRUE;
+       }
+}
+
+void ogre_melee ()
+{
+       self.ogre_cycles = 0;
+       if (random() > 0.5)
+               ogre_smash();
+       else
+               ogre_swing();
+}
+
+void ogre_die()
+{
+       Monster_CheckDropCvars ("ogre");
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       self.movetype           = MOVETYPE_TOSS;
+       self.think                      = Monster_Fade;
+       
+       W_ThrowNewWeapon(self, WEP_GRENADE_LAUNCHER, 0, self.origin, self.velocity);
+       if (random() < 0.5)
+               self.frame = ogre_anim_death1;
+       else
+               self.frame = ogre_anim_death2;
+               
+       monster_hook_death(); // for post-death mods
+}
+
+void ogre_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_ogre_health * self.scale;
+
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_ogre";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_melee               = ogre_melee;
+       self.frame                              = ogre_anim_pull;
+       self.attack_ranged              = ogre_missile;
+       self.nextthink                  = time + 1;
+       self.think                              = ogre_think;
+       self.sprite_height              = 40 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_ogre ()
+{      
+       if not(autocvar_g_monster_ogre)
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_ogre;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Ogre",
+                        "models/monsters/ogre.mdl",
+                        OGRE_MIN, OGRE_MAX,
+                        FALSE,
+                        ogre_die, ogre_spawn))
+       {
+               remove(self);
+               return;
+       }
+       
+       weapon_action(WEP_GRENADE_LAUNCHER, WR_PRECACHE);
+}
diff --git a/qcsrc/server/monsters/monster/shalrath.qc b/qcsrc/server/monsters/monster/shalrath.qc
new file mode 100644 (file)
index 0000000..c19e1ce
--- /dev/null
@@ -0,0 +1,250 @@
+// size
+const vector SHALRATH_MIN = '-32 -32 -24';
+const vector SHALRATH_MAX = '32 32 32';
+
+// cvars
+float autocvar_g_monster_shalrath;
+float autocvar_g_monster_shalrath_health;
+float autocvar_g_monster_shalrath_damage;
+float autocvar_g_monster_shalrath_speed;
+
+// animations
+#define shalrath_anim_attack   0
+#define shalrath_anim_pain             1
+#define shalrath_anim_death    2
+#define shalrath_anim_walk             3
+
+void() ShalMissile;
+
+void shalrath_think ()
+{
+       self.think = shalrath_think;
+       self.nextthink = time + 0.3;
+       
+       if(self.delay != -1)
+               self.nextthink = self.delay;
+       
+       monster_move(autocvar_g_monster_shalrath_speed, autocvar_g_monster_shalrath_speed, 50, shalrath_anim_walk, shalrath_anim_walk, shalrath_anim_walk);
+}
+
+void shalrath_attack ()
+{
+       self.frame = shalrath_anim_attack;
+       self.delay = time + 0.1;
+       self.attack_finished_single = time + 0.7;
+       self.monster_delayedattack = ShalMissile;
+}
+
+void shalrathattack_melee ()
+{
+       float bigdmg = 0, rdmg = autocvar_g_monster_shalrath_damage * random();
+
+       bigdmg = rdmg * self.scale;
+
+       monster_melee(self.enemy, bigdmg * monster_skill, 120, DEATH_MONSTER_SHALRATH_MELEE);
+}
+
+void shalrath_attack_melee ()
+{
+       self.monster_delayedattack = shalrathattack_melee;
+       self.delay = time + 0.2;
+       self.frame = shalrath_anim_attack;
+       self.attack_finished_single = time + 0.7;
+}
+
+float shal_missile ()
+{
+       // don't throw if it is blocked
+       traceline(self.origin + '0 0 10', self.enemy.origin + '0 0 10', FALSE, self);
+       if (enemy_range() > 1000)
+               return FALSE;
+       if (trace_ent != self.enemy)
+               return FALSE;
+       shalrath_attack();
+       return TRUE;
+}
+
+void() ShalHome;
+void ShalMissile_Spawn ()
+{
+       local   vector  dir = '0 0 0';
+       local   float   dist = 0;
+       
+       self.realowner.effects |= EF_MUZZLEFLASH;
+       
+       dir = normalize((self.owner.enemy.origin + '0 0 10') - self.owner.origin);
+       dist = vlen (self.owner.enemy.origin - self.owner.origin);
+       
+       self.solid = SOLID_BBOX;
+       self.movetype = MOVETYPE_FLYMISSILE;
+       CSQCProjectile(self, TRUE, PROJECTILE_CRYLINK, TRUE);
+       
+       self.realowner.v_angle = self.realowner.angles;
+       makevectors (self.realowner.angles);
+
+       setsize (self, '0 0 0', '0 0 0');               
+
+       setorigin (self, self.realowner.origin + v_forward * 14 + '0 0 30' + v_right * -14);
+       self.velocity = dir * 400;
+       self.avelocity = '300 300 300';
+       self.enemy = self.realowner.enemy;
+       self.touch = W_Plasma_TouchExplode;
+       ShalHome();
+}
+
+void ShalMissile ()
+{
+       local   entity  missile = world;
+
+       sound (self, CHAN_WEAPON, "weapons/spike.wav", 1, ATTN_NORM);
+
+       missile = spawn ();
+       missile.owner = missile.realowner = self;
+
+       missile.think = ShalMissile_Spawn;
+       missile.nextthink = time;
+}
+
+.float shal_cycles;
+void ShalHome ()
+{
+       local vector dir = '0 0 0', vtemp = self.enemy.origin + '0 0 10';
+       
+       self.shal_cycles += 1;
+       if (self.enemy.health <= 0 || self.owner.health <= 0 || self.shal_cycles >= 20)
+       {
+               remove(self);
+               return;
+       }
+       dir = normalize(vtemp - self.origin);
+       UpdateCSQCProjectile(self);
+       if (autocvar_skill == 3)
+               self.velocity = dir * 350;
+       else
+               self.velocity = dir * 250;
+       self.nextthink = time + 0.2;
+       self.think = ShalHome;  
+}
+
+float ShalrathCheckAttack ()
+{
+       local vector spot1 = '0 0 0', spot2 = '0 0 0';
+       local entity targ = self.enemy;
+
+       if (self.health <= 0 || targ == world || targ.health < 1)
+               return FALSE;
+       
+       if(self.monster_delayedattack && self.delay != -1)
+       {
+               if(time < self.delay)
+                       return FALSE;
+                       
+               self.monster_delayedattack();
+               self.delay = -1;
+               self.monster_delayedattack = func_null;
+       }
+       
+       if(time < self.attack_finished_single)
+               return FALSE;
+       
+       if (vlen(self.enemy.origin - self.origin) <= 120)
+       {       // melee attack
+               if (self.attack_melee)
+               {
+                       self.attack_melee();
+                       return TRUE;
+               }
+       }
+
+       if (vlen(targ.origin - self.origin) >= 2000) // long traces are slow
+               return FALSE;
+
+// see if any entities are in the way of the shot
+       spot1 = self.origin + '0 0 10';
+       spot2 = targ.origin + '0 0 10';
+
+       traceline (spot1, spot2, FALSE, self);
+
+       if (trace_ent != targ && trace_fraction < 1)
+               return FALSE; // don't have a clear shot
+
+       //if (trace_inopen && trace_inwater)
+       //      return FALSE; // sight line crossed contents
+
+       if (random() < 0.2)
+       if (self.attack_ranged())
+               return TRUE;
+
+       return FALSE;
+}
+
+void shalrath_die ()
+{
+       Monster_CheckDropCvars ("shalrath");
+       
+       self.think                      = Monster_Fade;
+       self.frame                      = shalrath_anim_death;
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;   
+       self.movetype           = MOVETYPE_TOSS;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void shalrath_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_shalrath_health * self.scale;
+
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_shalrath";
+       self.checkattack                = ShalrathCheckAttack;
+       self.attack_ranged              = shal_missile;
+       self.attack_melee               = shalrath_attack_melee;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = shalrath_think;
+       self.frame                              = shalrath_anim_walk;
+       self.sprite_height              = 40 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_shalrath ()
+{      
+       if not(autocvar_g_monster_shalrath)
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_shalrath;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Vore",
+                        "models/monsters/shalrath.mdl",
+                        SHALRATH_MIN, SHALRATH_MAX,
+                        FALSE,
+                        shalrath_die, shalrath_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
+
+// compatibility with old spawns
+void spawnfunc_monster_vore () { spawnfunc_monster_shalrath(); }
diff --git a/qcsrc/server/monsters/monster/shambler.qc b/qcsrc/server/monsters/monster/shambler.qc
new file mode 100644 (file)
index 0000000..0f82cc7
--- /dev/null
@@ -0,0 +1,209 @@
+// size
+const vector SHAMBLER_MIN = '-32 -32 -24';
+const vector SHAMBLER_MAX = '32 32 64';
+
+// cvars
+float autocvar_g_monster_shambler;
+float autocvar_g_monster_shambler_health;
+float autocvar_g_monster_shambler_damage;
+float autocvar_g_monster_shambler_attack_lightning_damage;
+float autocvar_g_monster_shambler_attack_claw_damage;
+float autocvar_g_monster_shambler_speed_walk;
+float autocvar_g_monster_shambler_speed_run;
+
+// animations
+#define shambler_anim_stand    0
+#define shambler_anim_walk             1
+#define shambler_anim_run              2
+#define shambler_anim_smash    3
+#define shambler_anim_swingr   4
+#define shambler_anim_swingl   5
+#define shambler_anim_magic    6
+#define shambler_anim_pain             7
+#define shambler_anim_death    8
+
+void shambler_think ()
+{
+       self.think = shambler_think;
+       self.nextthink = time + 0.3;
+       
+       monster_move(autocvar_g_monster_shambler_speed_run, autocvar_g_monster_shambler_speed_walk, 300, shambler_anim_run, shambler_anim_walk, shambler_anim_stand);
+}
+
+void shambler_smash ()
+{
+       float bigdmg = autocvar_g_monster_shambler_damage * self.scale;
+       
+       self.think = shambler_think;
+       self.attack_finished_single = time + 0.4;
+       self.nextthink = self.attack_finished_single;
+
+       if (!self.enemy)
+               return;
+
+       if (enemy_range() > 100 * self.scale)
+               return;
+       
+       Damage(self.enemy, self, self, bigdmg * monster_skill, DEATH_MONSTER_SHAMBLER_MELEE, self.enemy.origin, normalize(self.enemy.origin - self.origin));
+}
+
+void shambler_delayedsmash ()
+{
+       self.frame = shambler_anim_smash;
+       self.think = shambler_smash;
+       self.nextthink = time + 0.7;
+}
+
+void ShamClaw (float side)
+{
+       float bigdmg = autocvar_g_monster_shambler_attack_claw_damage * self.scale;
+       
+       monster_melee(self.enemy, bigdmg * monster_skill, 100, DEATH_MONSTER_SHAMBLER_CLAW);
+}
+
+void() shambler_swing_right;
+void shambler_swing_left ()
+{
+       self.frame = shambler_anim_swingl;
+       ShamClaw(250);
+       self.attack_finished_single = time + 0.8;
+       self.nextthink = self.attack_finished_single;
+       self.think = shambler_think;
+       if(random() < 0.5)
+               self.think = shambler_swing_right;
+}
+
+void shambler_swing_right ()
+{
+       self.frame = shambler_anim_swingr;
+       ShamClaw(-250);
+       self.attack_finished_single = time + 0.8;
+       self.nextthink = self.attack_finished_single;
+       self.think = shambler_think;
+       if(random() < 0.5)
+               self.think = shambler_swing_left;
+}
+
+void sham_melee ()
+{
+       local float chance = random();
+
+       if (chance > 0.6)
+               shambler_delayedsmash();
+       else if (chance > 0.3)
+               shambler_swing_right ();
+       else
+               shambler_swing_left ();
+}
+
+void CastLightning ()
+{
+       self.nextthink = time + 0.4;
+       self.think = shambler_think;
+       
+       local vector org = '0 0 0', dir = '0 0 0';
+       vector v = '0 0 0';
+
+       self.effects |= EF_MUZZLEFLASH;
+
+       org = self.origin + '0 0 40' * self.scale;
+
+       dir = self.enemy.origin + '0 0 16' - org;
+       dir = normalize (dir);
+
+       traceline (org, self.origin + dir * 1000, TRUE, self);
+               
+       FireRailgunBullet (org, org + dir * 1000, autocvar_g_monster_shambler_attack_lightning_damage * monster_skill, 0, 0, 0, 0, 0, DEATH_MONSTER_SHAMBLER_LIGHTNING);
+       
+       // teamcolor / hit beam effect
+       v = WarpZone_UnTransformOrigin(WarpZone_trace_transform, trace_endpos);
+       WarpZone_TrailParticles(world, particleeffectnum("TE_TEI_G3"), org, v);
+}
+
+void shambler_magic ()
+{
+       self.frame = shambler_anim_magic;
+       self.attack_finished_single = time + 1.1;
+       self.nextthink = time + 0.6;
+       self.think = CastLightning;
+}
+       
+float sham_lightning ()
+{
+       shambler_magic();
+       return TRUE;
+}
+
+void shambler_die ()
+{
+       Monster_CheckDropCvars ("shambler");
+       
+       W_ThrowNewWeapon(self, WEP_NEX, 0, self.origin, self.velocity);
+       
+       self.think                      = Monster_Fade;
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.nextthink          = time + 2.1;
+       self.frame                      = shambler_anim_death;
+       self.pain_finished  = self.nextthink;
+       self.movetype           = MOVETYPE_TOSS;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void shambler_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_shambler_health * self.scale;
+
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_shambler";
+       self.attack_melee               = sham_melee;
+       self.checkattack                = GenericCheckAttack;
+       self.attack_ranged              = sham_lightning;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.frame                              = shambler_anim_stand;
+       self.think                              = shambler_think;
+       self.sprite_height              = 70 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_shambler ()
+{      
+       if not(autocvar_g_monster_shambler)
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_shambler;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Shambler",
+                        "models/monsters/shambler.mdl",
+                        SHAMBLER_MIN, SHAMBLER_MAX,
+                        FALSE,
+                        shambler_die, shambler_spawn))
+       {
+               remove(self);
+               return;
+       }
+       
+       precache_model ("progs/beam.mdl");
+       precache_model ("models/weapons/g_nex.md3");
+       
+       precache_sound ("weapons/lgbeam_fire.wav");
+}
diff --git a/qcsrc/server/monsters/monster/soldier.qc b/qcsrc/server/monsters/monster/soldier.qc
new file mode 100644 (file)
index 0000000..b4e46e0
--- /dev/null
@@ -0,0 +1,340 @@
+// size
+const vector SOLDIER_MIN = '-16 -16 -30';
+const vector SOLDIER_MAX = '16 16 32';
+
+// cvars
+float autocvar_g_monster_soldier;
+float autocvar_g_monster_soldier_health;
+float autocvar_g_monster_soldier_melee_damage;
+float autocvar_g_monster_soldier_speed_walk;
+float autocvar_g_monster_soldier_speed_run;
+float autocvar_g_monster_soldier_ammo;
+float autocvar_g_monster_soldier_weapon_laser_chance;
+float autocvar_g_monster_soldier_weapon_shotgun_chance;
+float autocvar_g_monster_soldier_weapon_machinegun_chance;
+float autocvar_g_monster_soldier_weapon_rocketlauncher_chance;
+float autocvar_g_monster_soldier_attack_uzi_bullets;
+
+// animations
+#define soldier_anim_stand     0
+#define soldier_anim_death1 1
+#define soldier_anim_death2 2
+#define soldier_anim_reload 3
+#define soldier_anim_pain1     4
+#define soldier_anim_pain2     5
+#define soldier_anim_pain3     6
+#define soldier_anim_run       7
+#define soldier_anim_shoot     8
+#define soldier_anim_prowl     9
+
+void soldier_think ()
+{
+       self.think = soldier_think;
+       self.nextthink = time + 0.3;
+       
+       if(self.delay != -1)
+               self.nextthink = self.delay;
+       
+       if(time < self.attack_finished_single)
+               monster_move(0, 0, 0, soldier_anim_shoot, soldier_anim_shoot, soldier_anim_shoot);
+       else
+               monster_move(autocvar_g_monster_soldier_speed_run, autocvar_g_monster_soldier_speed_walk, 50, soldier_anim_run, soldier_anim_prowl, soldier_anim_stand);
+}
+
+void soldier_reload ()
+{
+       self.frame = soldier_anim_reload;
+       self.attack_finished_single = time + 2;
+       self.currentammo = autocvar_g_monster_soldier_ammo;
+       sound (self, CH_SHOTS, "weapons/reload.wav", VOL_BASE, ATTN_LARGE);
+}
+
+float SoldierCheckAttack ()
+{
+       local vector spot1 = '0 0 0', spot2 = '0 0 0';
+       local entity targ = self.enemy;
+       local float chance = 0;
+
+       if (self.health <= 0 || targ.health < 1 || targ == world)
+               return FALSE;
+
+       if (vlen(targ.origin - self.origin) > 2000) // long traces are slow
+               return FALSE;
+
+       // see if any entities are in the way of the shot
+       spot1 = self.origin + self.view_ofs;
+       spot2 = targ.origin + targ.view_ofs;
+
+       traceline (spot1, spot2, FALSE, self);
+
+       if (trace_ent != targ)
+               return FALSE; // don't have a clear shot
+
+       if (trace_inwater)
+       if (trace_inopen)
+               return FALSE; // sight line crossed contents
+               
+       if(self.monster_delayedattack && self.delay != -1)
+       {
+               if(time < self.delay)
+                       return FALSE;
+                       
+               self.monster_delayedattack();
+       }
+
+       // missile attack
+       if (time < self.attack_finished_single)
+               return FALSE;
+
+       if (enemy_range() >= 2000)
+               return FALSE;
+
+       if (enemy_range() <= 120)
+               chance = 0.9;
+       else if (enemy_range() <= 500)
+               chance = 0.6; // was 0.4
+       else if (enemy_range() <= 1000)
+               chance = 0.3; // was 0.05
+       else
+               chance = 0;
+
+       if (chance > 0)
+       if (chance > random())
+               return FALSE;
+               
+       if(self.currentammo <= 0 && enemy_range() <= 120)
+       {
+               self.attack_melee();
+               return TRUE;
+       }
+       
+       if(self.currentammo <= 0)
+       {
+               soldier_reload();
+               return FALSE;
+       }
+
+       if (self.attack_ranged())
+               return TRUE;
+
+       return FALSE;
+}
+
+void soldier_laser ()
+{
+       self.frame = soldier_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+       W_Laser_Attack(0);
+}
+
+float soldier_missile_laser ()
+{
+       // FIXME: check if it would hit
+       soldier_laser();
+       return TRUE;
+}
+
+.float grunt_cycles;
+void soldier_uzi_fire ()
+{
+       self.currentammo -= 1;
+       if(self.currentammo <= 0)
+               return;
+               
+       self.grunt_cycles += 1;
+       
+       if(self.grunt_cycles > autocvar_g_monster_soldier_attack_uzi_bullets)
+       {
+               self.monster_delayedattack = func_null;
+               self.delay = -1;
+               return;
+       }
+       W_UZI_Attack(DEATH_MONSTER_SOLDIER_NAIL);
+       self.delay = time + 0.1;
+       self.monster_delayedattack = soldier_uzi_fire;
+}
+
+void soldier_uzi ()
+{
+       if(self.currentammo <= 0)
+               return;
+               
+       self.frame = soldier_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+       self.delay = time + 0.1;
+       self.monster_delayedattack = soldier_uzi_fire;
+}
+
+float soldier_missile_uzi ()
+{
+       self.grunt_cycles = 0;
+       // FIXME: check if it would hit
+       soldier_uzi();
+       return TRUE;
+}
+
+void soldier_shotgun ()
+{
+       self.currentammo -= 1;
+       if(self.currentammo <= 0)
+               return;
+               
+       self.frame = soldier_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+       W_Shotgun_Attack();
+}
+
+float soldier_missile_shotgun ()
+{
+       // FIXME: check if it would hit
+       self.grunt_cycles = 0;
+       soldier_shotgun();
+       return TRUE;
+}
+
+void soldier_rl ()
+{
+       self.currentammo -= 1;
+       if(self.currentammo <= 0)
+               return;
+               
+       self.frame = soldier_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+       W_Rocket_Attack();
+}
+
+float soldier_missile_rl ()
+{
+       // FIXME: check if it would hit
+       soldier_rl();
+       return TRUE;
+}
+
+void soldier_bash ()
+{
+       self.frame = soldier_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+       monster_melee(self.enemy, autocvar_g_monster_soldier_melee_damage, 70, DEATH_MONSTER_SOLDIER_NAIL);
+}
+
+void soldier_die()
+{
+       Monster_CheckDropCvars ("soldier");
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.movetype           = MOVETYPE_TOSS;
+       self.think                      = Monster_Fade;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       
+       if (self.attack_ranged == soldier_missile_uzi)
+               W_ThrowNewWeapon(self, WEP_UZI, 0, self.origin, self.velocity);    
+       else if (self.attack_ranged == soldier_missile_shotgun)
+               W_ThrowNewWeapon(self, WEP_SHOTGUN, 0, self.origin, self.velocity);
+       else if (self.attack_ranged == soldier_missile_rl)
+               W_ThrowNewWeapon(self, WEP_ROCKET_LAUNCHER, 0, self.origin, self.velocity);
+       else
+               W_ThrowNewWeapon(self, WEP_LASER, 0, self.origin, self.velocity);
+
+       if (random() < 0.5)
+               self.frame = soldier_anim_death1;
+       else
+               self.frame = soldier_anim_death2;
+               
+       monster_hook_death(); // for post-death mods
+}
+
+void soldier_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_soldier_health * self.scale;
+
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_soldier";
+       self.checkattack                = SoldierCheckAttack;
+       self.attack_melee               = soldier_bash;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = soldier_think;
+       self.sprite_height              = 30 * self.scale;
+       self.items                              = (IT_SHELLS | IT_ROCKETS | IT_NAILS);
+       
+       RandomSelection_Init();
+       RandomSelection_Add(world, WEP_LASER, string_null, autocvar_g_monster_soldier_weapon_laser_chance, 1);
+       RandomSelection_Add(world, WEP_SHOTGUN, string_null, autocvar_g_monster_soldier_weapon_shotgun_chance, 1);
+       RandomSelection_Add(world, WEP_UZI, string_null, autocvar_g_monster_soldier_weapon_machinegun_chance, 1);
+       RandomSelection_Add(world, WEP_ROCKET_LAUNCHER, string_null, autocvar_g_monster_soldier_weapon_rocketlauncher_chance, 1);
+       
+       if (RandomSelection_chosen_float == WEP_ROCKET_LAUNCHER)
+       {
+               self.weapon = WEP_ROCKET_LAUNCHER;
+               self.currentammo = self.ammo_rockets;
+               self.armorvalue = 10;
+               self.attack_ranged = soldier_missile_rl;
+       }
+       else if (RandomSelection_chosen_float == WEP_UZI)
+       {
+               self.weapon = WEP_UZI;
+               self.currentammo = self.ammo_nails;
+               self.armorvalue = 100;
+               self.attack_ranged = soldier_missile_uzi;
+       }
+       else if (RandomSelection_chosen_float == WEP_SHOTGUN)
+       {
+               self.weapon = WEP_SHOTGUN;
+               self.currentammo = self.ammo_shells;
+               self.armorvalue = 25;
+               self.attack_ranged = soldier_missile_shotgun;
+       }
+       else
+       {
+               self.weapon = WEP_LASER;
+               self.armorvalue = 60;
+               self.currentammo = self.ammo_none;
+               self.attack_ranged = soldier_missile_laser;
+       }
+
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_soldier ()
+{      
+       if not(autocvar_g_monster_soldier)
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_soldier;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Grunt",
+                        "models/monsters/soldier.mdl",
+                        SOLDIER_MIN, SOLDIER_MAX,
+                        FALSE,
+                        soldier_die, soldier_spawn))
+       {
+               remove(self);
+               return;
+       }
+       
+       precache_sound ("weapons/shotgun_fire.wav");
+       precache_sound ("weapons/uzi_fire.wav");
+       precache_sound ("weapons/laser_fire.wav");
+       precache_sound ("weapons/reload.wav");
+}
+
+// compatibility with old spawns
+void spawnfunc_monster_army () { spawnfunc_monster_soldier(); }
diff --git a/qcsrc/server/monsters/monster/spawner.qc b/qcsrc/server/monsters/monster/spawner.qc
new file mode 100644 (file)
index 0000000..627cb77
--- /dev/null
@@ -0,0 +1,199 @@
+// size
+const vector SPAWNER_MIN = '-35 -35 -10';
+const vector SPAWNER_MAX = '35 35 70';
+
+// cvars
+float autocvar_g_monster_spawner;
+float autocvar_g_monster_spawner_health;
+float autocvar_g_monster_spawner_target_recheck_delay;
+float autocvar_g_monster_spawner_target_range;
+float autocvar_g_monster_spawner_spawn_range;
+float autocvar_g_monster_spawner_maxmobs;
+string autocvar_g_monster_spawner_forcespawn;
+
+void() spawner_think;
+
+void spawnmonsters ()
+{
+       if(self.spawner_monstercount >= autocvar_g_monster_spawner_maxmobs || self.frozen || self.freezetag_frozen)
+               return;
+               
+       vector posi1 = '0 0 0', posi2 = '0 0 0', posi3 = '0 0 0', posi4 = '0 0 0', chosenposi = '0 0 0';
+       float r = random();
+       string type = string_null;
+       entity e = world;
+       
+       self.spawner_monstercount += 1;
+       
+       if(self.spawnmob != "")
+               type = self.spawnmob;
+               
+       if(autocvar_g_monster_spawner_forcespawn != "0")
+               type = autocvar_g_monster_spawner_forcespawn;
+               
+       if(type == "" || type == "spawner") // spawner spawning spawners?!
+               type = "knight";
+       
+       posi1 = self.origin - '0 70 -50' * self.scale;
+       posi2 = self.origin + '0 70 50' * self.scale;
+       posi3 = self.origin - '70 0 -50' * self.scale;
+       posi4 = self.origin + '70 0 -50' * self.scale;
+          
+       if (r < 0.20)
+               chosenposi = posi1;
+       else if (r < 0.50)
+               chosenposi = posi2;
+       else if (r < 80)
+               chosenposi = posi3;
+       else
+               chosenposi = posi4;
+
+       e = spawnmonster(type, self, self, chosenposi, FALSE, MONSTER_MOVE_WANDER);
+       
+       if(teamplay && autocvar_g_monsters_teams)
+               e.team = self.team;
+       
+       if(self.spawnflags & MONSTERFLAG_GIANT)
+               e.spawnflags = MONSTERFLAG_GIANT;
+               
+       if(self.flags & MONSTERFLAG_MINIBOSS)
+               e.spawnflags = MONSTERFLAG_MINIBOSS;
+}
+
+void spawner_die () 
+{
+       setmodel(self, "");
+       pointparticles(particleeffectnum(((self.scale > 3) ? "explosion_big" : "explosion_medium")), self.origin, '0 0 0', 1);
+       sound (self, CH_SHOTS, "weapons/rocket_impact.wav", VOL_BASE, ATTN_NORM);
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.think                      = Monster_Fade;
+       self.nextthink          = time + 1;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void spawner_recount()
+{
+       self.spawner_monstercount = 0;
+       self.think = spawner_think;
+       self.nextthink = time;
+}
+
+void spawner_think() 
+{
+       float finished = FALSE, enemyDistance = 0;
+       self.think = spawner_think;
+       
+       if(self.spawner_monstercount == autocvar_g_monster_spawner_maxmobs)
+       {
+               self.think = spawner_recount;
+               self.nextthink = time + 20;
+               return;
+       }
+         
+       // remove enemy that ran away
+       if (self.enemy)
+       if (self.delay <= time) // check if we can do the rescan now
+       if (vlen(self.origin - self.enemy.origin) > autocvar_g_monster_spawner_target_range * self.scale)
+               self.enemy = world;
+       else
+               self.delay = time + autocvar_g_monster_spawner_target_recheck_delay;
+       
+       if not(self.enemy) 
+       {
+               self.enemy = FindTarget(self);
+               if (self.enemy)
+                       self.delay = time + autocvar_g_monster_spawner_target_recheck_delay;
+       }
+
+       if (self.enemy) 
+       {
+               // this spawner has an enemy
+               traceline(self.origin, self.enemy.origin, FALSE, self);
+               enemyDistance = vlen(trace_endpos - self.origin);
+               
+               if (trace_ent == self.enemy)
+               if (self.enemy.deadflag == DEAD_NO)
+               if (self.spawner_monstercount <= autocvar_g_monster_spawner_maxmobs)
+               if (enemyDistance <= autocvar_g_monster_spawner_spawn_range * self.scale) 
+               {
+                       spawnmonsters();
+                       finished = TRUE;
+               }               
+       }
+       
+       self.nextthink = time + 1;
+
+       if(self.spawner_monstercount <= autocvar_g_monster_spawner_maxmobs)
+               self.nextthink = time + 0.1;
+       
+       if not(finished) 
+       {
+               if (self.enemy)
+                       self.nextthink = time + 0.1;
+       }  
+}
+
+void spawner_spawn() 
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_spawner_health * self.scale;
+       
+       self.classname                  = "monster_spawner";
+       self.nextthink                  = time + 2.1;
+       self.velocity                   = '0 0 0';
+       self.think                              = spawner_think;
+       self.touch                              = func_null;    
+       self.sprite_height      = 80 * self.scale;
+       
+       self.spawner_monstercount = 0;
+       
+       droptofloor();
+       self.movetype = MOVETYPE_NONE;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+/*QUAKED monster_spawner (1 0 0) (-18 -18 -25) (18 18 47)
+---------NOTES----------
+Spawns monsters when a player is nearby
+-------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY --------
+modeldisabled="models/containers/crate01.md3"
+*/
+void spawnfunc_monster_spawner() 
+{
+       if not(autocvar_g_monster_spawner) 
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_spawner;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 0.8;
+       
+       if not (monster_initialize(
+                        "Monster spawner",
+                        "models/containers/crate01.md3",
+                        SPAWNER_MIN, SPAWNER_MAX,
+                        FALSE,
+                        spawner_die, spawner_spawn))
+       {
+               remove(self);
+               return;
+       }
+
+       precache_sound("weapons/rocket_impact.wav");
+}
diff --git a/qcsrc/server/monsters/monster/spider.qc b/qcsrc/server/monsters/monster/spider.qc
new file mode 100644 (file)
index 0000000..2007e5e
--- /dev/null
@@ -0,0 +1,314 @@
+// cvars
+float autocvar_g_monster_spider;
+float autocvar_g_monster_spider_stopspeed;
+float autocvar_g_monster_spider_attack_leap_delay;
+float autocvar_g_monster_spider_attack_leap_range;
+float autocvar_g_monster_spider_attack_stand_damage;
+float autocvar_g_monster_spider_attack_stand_delay;
+float autocvar_g_monster_spider_attack_stand_range;
+float autocvar_g_monster_spider_health;
+float autocvar_g_monster_spider_idle_timer_min;
+float autocvar_g_monster_spider_speed_walk;
+float autocvar_g_monster_spider_speed_run;
+float autocvar_g_monster_spider_target_recheck_delay;
+float autocvar_g_monster_spider_target_range;
+float autocvar_g_monster_spider_attack_type;
+
+// spider animations
+#define spider_anim_idle                       0
+#define spider_anim_walk                       1
+#define spider_anim_attack                     2
+#define spider_anim_attack2                    3
+
+const vector SPIDER_MIN                                 = '-18 -18 -25';
+const vector SPIDER_MAX                                 = '18 18 30';
+
+.float spider_type; // used to switch between fire & ice attacks
+const float SPIDER_TYPE_ICE            = 0;
+const float SPIDER_TYPE_FIRE   = 1;
+
+void spider_spawn();
+void spawnfunc_monster_spider();
+void spider_think();
+
+void spider_die ()
+{
+       Monster_CheckDropCvars ("spider");
+       
+       self.angles += '180 0 0';
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.movetype           = MOVETYPE_TOSS;
+       self.think                      = Monster_Fade;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       self.frame                      = spider_anim_attack;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+/**
+ * Performe a standing attack on self.enemy.
+ */
+void spider_attack_standing() {
+       float dot = 0, bigdmg = autocvar_g_monster_spider_attack_stand_damage * self.scale;
+
+       self.velocity_x = 0;
+       self.velocity_y = 0;
+       
+       if(self.monster_owner == self.enemy)
+       {
+               self.enemy = world;
+               return;
+       }
+
+       makevectors (self.angles);
+       dot = normalize (self.enemy.origin - self.origin) * v_forward;
+       if(dot > 0.3)
+       {
+               Damage(self.enemy, self, self, bigdmg * monster_skill, DEATH_MONSTER_MELEE, self.origin, '0 0 0');
+       }
+       
+       if (!monster_isvalidtarget(self.enemy, self, FALSE))
+               self.enemy = world;
+               
+       if(random() < 0.50)
+               self.frame = spider_anim_attack;
+       else
+               self.frame = spider_anim_attack2;
+
+       self.nextthink = time + autocvar_g_monster_spider_attack_stand_delay;
+}
+
+void spider_web_explode ()
+{
+       RadiusDamage (self, self.realowner, 0, 0, 1, world, 0, self.projectiledeathtype, other);
+       remove (self);
+}
+
+void spider_web_touch ()
+{
+       PROJECTILE_TOUCH;
+       if (other.takedamage == DAMAGE_AIM)
+               Freeze(other, 0.3);
+               
+       spider_web_explode();
+}
+
+void spider_shootweb()
+{
+       // clone of the electro secondary attack, with less bouncing
+       entity proj = world;
+       
+       makevectors(self.angles);
+
+       W_SetupShot_ProjectileSize (self, '0 0 -4', '0 0 -4', FALSE, 2, "weapons/electro_fire2.wav", CH_WEAPON_A, 0);
+
+       w_shotdir = v_forward; // no TrueAim for grenades please
+
+       pointparticles(particleeffectnum("electro_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
+
+       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 + autocvar_g_balance_electro_secondary_lifetime;
+       PROJECTILE_MAKETRIGGER(proj);
+       proj.projectiledeathtype = WEP_ELECTRO | HITTYPE_SECONDARY;
+       setorigin(proj, w_shotorg);
+
+       //proj.glow_size = 50;
+       //proj.glow_color = 45;
+       proj.movetype = MOVETYPE_BOUNCE;
+       W_SETUPPROJECTILEVELOCITY_UP(proj, g_balance_electro_secondary);
+       proj.touch = spider_web_touch;
+       setsize(proj, '0 0 -4', '0 0 -4');
+       proj.takedamage = DAMAGE_YES;
+       proj.damageforcescale = 0;
+       proj.health = 500;
+       proj.event_damage = W_Plasma_Damage;
+       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, FALSE); // no culling, it has sound
+
+       other = proj; MUTATOR_CALLHOOK(EditProjectile);
+}
+
+void spider_attack_leap()
+{
+       vector angles_face = vectoangles(self.enemy.origin - self.origin);
+
+       // face the enemy       
+       self.frame = spider_anim_attack2;
+       self.angles_y = angles_face_y ;
+       self.nextthink = time + autocvar_g_monster_spider_attack_leap_delay;
+       
+       makevectors(self.angles);
+       
+       switch(self.spider_type)
+       {
+               default:
+               case SPIDER_TYPE_ICE:
+                       spider_shootweb(); break; // must... remember... breaks!
+               case SPIDER_TYPE_FIRE:
+                       W_Fireball_Attack2(); break;
+       }
+}
+
+void spider_think()
+{
+       float finished = FALSE, enemyDistance = 0, mySpeed = 0;
+
+       self.think = spider_think;
+       
+       if(self.enemy && !monster_isvalidtarget(self.enemy, self, FALSE))
+               self.enemy = world;
+       
+       if (self.enemy)
+       if (self.enemy.team == self.team || self.monster_owner == self.enemy)
+               self.enemy = world;
+       
+       if(teamplay && autocvar_g_monsters_teams && self.monster_owner.team != self.team)
+               self.monster_owner = world;     
+       
+       // remove enemy that ran away
+       if (self.enemy)
+       if (self.delay <= time) // check if we can do the rescan now
+       if (vlen(self.origin - self.enemy.origin) > autocvar_g_monster_spider_target_range * self.scale) 
+       {
+               //print("removing enemy, he is too far: ", ftos(vlen(self.origin - self.enemy.origin)), "\n");
+               //print("delay was ", ftos(self.delay), "\n");
+               self.enemy = world;
+       } 
+       else
+               self.delay = time + autocvar_g_monster_spider_target_recheck_delay;
+       
+       // find an enemy if no enemy available
+       if not(self.enemy) 
+       {
+               self.enemy = FindTarget(self);
+               if (self.enemy)
+                       self.delay = time + autocvar_g_monster_spider_target_recheck_delay;
+       }
+
+       if (self.enemy) 
+       {
+               // this spider has an enemy, attack if close enough, go to it if not!
+               traceline(self.origin, self.enemy.origin, FALSE, self);
+               enemyDistance = vlen(trace_endpos - self.origin);
+               mySpeed = vlen(self.velocity);
+               
+               //print("speed ", ftos(mySpeed), "\n");
+               
+               if (trace_ent == self.enemy)
+               if (self.enemy.deadflag == DEAD_NO)
+                       if (enemyDistance <= autocvar_g_monster_spider_attack_stand_range * self.scale && mySpeed <= 30) 
+                       {
+                               
+                               //RadiusDamage (entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity ignore, float forceintensity, float deathtype, entity directhitentity)
+                               spider_attack_standing();
+                               finished = TRUE;
+                       } 
+                       else if (enemyDistance <= autocvar_g_monster_spider_attack_leap_range * self.scale && !self.enemy.frozen) 
+                       {
+                               // do attackleap (set yaw, velocity, and check do damage on the first player entity it touches)
+                               spider_attack_leap();
+                               finished = TRUE;
+                       }
+               
+       }
+       
+       self.nextthink = time + 1;
+
+       if not(finished) 
+       {
+               monster_move(autocvar_g_monster_spider_speed_run, autocvar_g_monster_spider_speed_walk, autocvar_g_monster_spider_stopspeed, spider_anim_walk, spider_anim_walk, spider_anim_idle);
+               
+               if (self.enemy || self.monster_owner)
+               {
+                       self.nextthink = time + 0.1;
+                       return;
+               }   
+       }
+       
+       if not(self.enemy || self.monster_owner || self.goalentity) 
+       {
+               // stay idle
+               //print("spider is idling while waiting for some fresh meat...\n");
+               if (mySpeed <= 10)
+                       self.frame = spider_anim_idle;
+               else
+                       self.frame = spider_anim_walk;
+               self.nextthink = time + autocvar_g_monster_spider_idle_timer_min * random();    
+       }
+}
+
+/**
+ * Spawn the spider.
+ */
+void spider_spawn() 
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_spider_health * self.scale;
+       
+       self.classname                  = "monster_spider";
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.pain_finished      = self.nextthink;
+       self.frame                              = spider_anim_idle;
+       self.think                              = spider_think;
+       self.sprite_height      = 40 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+/*QUAKED monster_spider (1 0 0) (-18 -18 -25) (18 18 47)
+Spider, 60 health points.
+-------- KEYS --------
+-------- SPAWNFLAGS --------
+MONSTERFLAG_APPEAR: monster will spawn when triggered.
+---------NOTES----------
+-------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY --------
+modeldisabled="models/monsters/spider.dpm"
+*/
+void spawnfunc_monster_spider() 
+{
+       if not(autocvar_g_monster_spider) 
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_spider;
+       self.classname = "monster_spider";
+       if(!self.spider_type)
+               self.spider_type = autocvar_g_monster_spider_attack_type;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       if not (monster_initialize(
+                        "Spider",
+                        "models/monsters/spider.dpm",
+                        SPIDER_MIN, SPIDER_MAX,
+                        FALSE,
+                        spider_die, spider_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
diff --git a/qcsrc/server/monsters/monster/tarbaby.qc b/qcsrc/server/monsters/monster/tarbaby.qc
new file mode 100644 (file)
index 0000000..355648e
--- /dev/null
@@ -0,0 +1,161 @@
+// size
+const vector TARBABY_MIN = '-16 -16 -24';
+const vector TARBABY_MAX = '16 16 16';
+
+// cvars
+float autocvar_g_monster_tarbaby;
+float autocvar_g_monster_tarbaby_health;
+float autocvar_g_monster_tarbaby_speed_walk;
+float autocvar_g_monster_tarbaby_speed_run;
+
+// animations
+#define tarbaby_anim_walk              0
+#define tarbaby_anim_run               1
+#define tarbaby_anim_jump              2
+#define tarbaby_anim_fly               3
+#define tarbaby_anim_explode   4
+
+void tarbaby_think ()
+{
+       self.think = tarbaby_think;
+       self.nextthink = time + 0.3;
+       
+       monster_move(autocvar_g_monster_tarbaby_speed_run, autocvar_g_monster_tarbaby_speed_walk, 20, tarbaby_anim_run, tarbaby_anim_walk, tarbaby_anim_walk);
+}
+
+void Tar_JumpTouch ()
+{
+       // dunno why this would be called when dead, but to be safe
+       if (self.health <= 0)
+               return;
+               
+       if (other.takedamage)
+       if (vlen(self.velocity) > 200)
+       {
+               // make the monster die
+               self.event_damage(self, self, self.health + self.max_health, DEATH_TOUCHEXPLODE, self.origin, '0 0 0');
+                       
+               return;
+       }
+
+       if (trace_dphitcontents)
+       {
+               if not(self.flags & FL_ONGROUND)
+               {
+                       self.touch = MonsterTouch;
+                       self.flags |= FL_ONGROUND;
+                       self.movetype = MOVETYPE_WALK;
+               }
+       }
+}
+
+void tarbaby_jump ()
+{
+       if not(self.flags & FL_ONGROUND)
+               return;
+       self.frame = tarbaby_anim_jump;
+       // dunno why this would be called when dead, but to be safe
+       if (self.health <= 0)
+               return;
+       self.movetype = MOVETYPE_BOUNCE;
+       self.touch = Tar_JumpTouch;
+       makevectors (self.angles);
+       self.origin_z += 1;
+       self.velocity = v_forward * 600 + '0 0 200';
+       self.velocity_z += random()*150;
+       if (self.flags & FL_ONGROUND)
+               self.flags -= FL_ONGROUND;
+               
+       self.attack_finished_single = time + 0.5;
+}
+
+float tbaby_jump ()
+{
+       tarbaby_jump();
+       return TRUE;
+}
+
+void tarbaby_blowup ()
+{
+       float bigboom = 250 * (self.scale * 0.7);
+       RadiusDamage(self, self, 250 * monster_skill, 15, bigboom * (monster_skill * 0.7), world, 250, DEATH_MONSTER_TARBABY_BLOWUP, world);
+       pointparticles(particleeffectnum(((self.scale > 3) ? "explosion_big" : "explosion_medium")), self.origin, '0 0 0', 1);
+       sound(self, CH_SHOTS, "weapons/rocket_impact.wav", VOL_BASE, ATTN_NORM);
+       
+       Monster_CheckDropCvars ("tarbaby"); // drop items after exploding to prevent player picking up item before dying
+       
+       setmodel(self, "");
+}
+
+void tarbaby_explode()
+{
+       tarbaby_blowup();
+       
+       monster_hook_death(); // calling this next frame should be ok...
+}
+
+void tarbaby_die ()
+{
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.movetype           = MOVETYPE_NONE;
+       self.enemy                      = world;
+       self.think                      = tarbaby_explode;
+       self.nextthink          = time + 0.1;
+}
+
+void tarbaby_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_tarbaby_health * self.scale;
+       
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_tarbaby";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_ranged              = tbaby_jump;
+       self.attack_melee               = tarbaby_jump;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = tarbaby_think;
+       self.sprite_height              = 20 * self.scale;
+       self.frame                              = tarbaby_anim_walk;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_tarbaby ()
+{      
+       if not(autocvar_g_monster_tarbaby)
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_tarbaby;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Spawn",
+                        "models/monsters/tarbaby.mdl",
+                        TARBABY_MIN, TARBABY_MAX,
+                        FALSE,
+                        tarbaby_die, tarbaby_spawn))
+       {
+               remove(self);
+               return;
+       }
+       
+       precache_sound ("weapons/rocket_impact.wav");
+}
+
+// compatibility with old spawns
+void spawnfunc_monster_spawn () { spawnfunc_monster_tarbaby(); }
diff --git a/qcsrc/server/monsters/monster/wizard.qc b/qcsrc/server/monsters/monster/wizard.qc
new file mode 100644 (file)
index 0000000..4460e02
--- /dev/null
@@ -0,0 +1,184 @@
+// size
+const vector WIZARD_MIN = '-16 -16 -24';
+const vector WIZARD_MAX = '16 16 24';
+
+// cvars
+float autocvar_g_monster_wizard;
+float autocvar_g_monster_wizard_health;
+float autocvar_g_monster_wizard_speed_walk;
+float autocvar_g_monster_wizard_speed_run;
+float autocvar_g_monster_wizard_spike_damage;
+float autocvar_g_monster_wizard_spike_edgedamage;
+float autocvar_g_monster_wizard_spike_radius;
+float autocvar_g_monster_wizard_spike_speed;
+
+// animations
+#define wizard_anim_hover      0
+#define wizard_anim_fly        1
+#define wizard_anim_magic      2
+#define wizard_anim_pain       3
+#define wizard_anim_death      4
+
+void Wiz_FastExplode()
+{
+       self.event_damage = func_null;
+       self.takedamage = DAMAGE_NO;
+       RadiusDamage (self, self.realowner, autocvar_g_monster_wizard_spike_damage, autocvar_g_monster_wizard_spike_edgedamage, autocvar_g_monster_wizard_spike_radius, world, 0, self.projectiledeathtype, other);
+
+       remove (self);
+}
+
+void Wiz_FastTouch ()
+{
+       PROJECTILE_TOUCH;
+       
+       if(other == self.owner)
+               return;
+               
+       if(teamplay)
+       if(other.team == self.owner.team)
+               return;
+               
+       pointparticles(particleeffectnum("TE_WIZSPIKE"), self.origin, '0 0 0', 1);
+       
+       Wiz_FastExplode();
+}
+
+void Wiz_StartFast ()
+{
+       local   entity  missile;
+       local   vector  dir = '0 0 0';
+       local   float   dist = 0, flytime = 0;
+
+       dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
+       dist = vlen (self.enemy.origin - self.origin);
+       flytime = dist * 0.002;
+       if (flytime < 0.1)
+               flytime = 0.1;
+       
+       self.v_angle = self.angles;
+       makevectors (self.angles);
+
+       missile = spawn ();
+       missile.owner = missile.realowner = self;
+       setsize (missile, '0 0 0', '0 0 0');            
+       setorigin (missile, self.origin + v_forward * 14 + '0 0 30' + v_right * 14);
+       missile.enemy = self.enemy;
+       missile.nextthink = time + 3;
+       missile.think = Wiz_FastExplode;
+       missile.velocity = dir * autocvar_g_monster_wizard_spike_speed;
+       missile.avelocity = '300 300 300';
+       missile.solid = SOLID_BBOX;
+       missile.movetype = MOVETYPE_FLYMISSILE;
+       missile.touch = Wiz_FastTouch;
+       CSQCProjectile(missile, TRUE, PROJECTILE_CRYLINK, TRUE);
+       
+       missile = spawn ();
+       missile.owner = missile.realowner = self;
+       setsize (missile, '0 0 0', '0 0 0');            
+       setorigin (missile, self.origin + v_forward * 14 + '0 0 30' + v_right * -14);
+       missile.enemy = self.enemy;
+       missile.nextthink = time + 3;
+       missile.touch = Wiz_FastTouch;
+       missile.solid = SOLID_BBOX;
+       missile.movetype = MOVETYPE_FLYMISSILE;
+       missile.think = Wiz_FastExplode;
+       missile.velocity = dir * autocvar_g_monster_wizard_spike_speed;
+       missile.avelocity = '300 300 300';
+       CSQCProjectile(missile, TRUE, PROJECTILE_CRYLINK, TRUE);
+}
+
+void wizard_think ()
+{
+       self.think = wizard_think;
+       self.nextthink = time + 0.3;
+       
+       monster_move(autocvar_g_monster_wizard_speed_run, autocvar_g_monster_wizard_speed_walk, 300, wizard_anim_fly, wizard_anim_hover, wizard_anim_hover);
+}
+
+void wizard_fastattack ()
+{
+       Wiz_StartFast();
+}
+
+void wizard_die ()
+{
+       Monster_CheckDropCvars ("wizard");
+       
+       self.think                      = Monster_Fade;
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.movetype           = MOVETYPE_TOSS;
+       self.flags                      = FL_ONGROUND;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink; 
+       self.velocity_x         = -200 + 400*random();
+       self.velocity_y         = -200 + 400*random();
+       self.velocity_z         = 100 + 100*random();
+       self.frame                      = wizard_anim_death;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+float Wiz_Missile ()
+{
+       wizard_fastattack();
+       return TRUE;
+}
+
+void wizard_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_wizard_health * self.scale;
+       
+       self.classname                  = "monster_wizard";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_ranged              = Wiz_Missile;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.movetype                   = MOVETYPE_FLY; // TODO: make it fly up/down
+       self.flags                         |= FL_FLY;
+       self.think                              = wizard_think;
+       self.sprite_height              = 30 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_wizard ()
+{      
+       if not(autocvar_g_monster_wizard)
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_wizard;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Scrag",
+                        "models/monsters/wizard.mdl",
+                        WIZARD_MIN, WIZARD_MAX,
+                        TRUE,
+                        wizard_die, wizard_spawn))
+       {
+               remove(self);
+               return;
+       }
+       
+       precache_model ("models/spike.mdl");
+       precache_sound ("weapons/spike.wav");
+}
+
+// compatibility with old spawns
+void spawnfunc_monster_scrag () { spawnfunc_monster_wizard(); }
diff --git a/qcsrc/server/monsters/monster/zombie.qc b/qcsrc/server/monsters/monster/zombie.qc
new file mode 100644 (file)
index 0000000..108deed
--- /dev/null
@@ -0,0 +1,329 @@
+/**
+ * Special purpose fields:
+ * .delay - time at which to check if zombie's enemy is still in range
+ * .enemy - enemy of this zombie
+ * .state - state of the zombie, see ZOMBIE_STATE_*
+ */
+// cvars
+float autocvar_g_monster_zombie;
+float autocvar_g_monster_zombie_stopspeed;
+float autocvar_g_monster_zombie_attack_leap_damage;
+float autocvar_g_monster_zombie_attack_leap_delay;
+float autocvar_g_monster_zombie_attack_leap_force;
+float autocvar_g_monster_zombie_attack_leap_range;
+float autocvar_g_monster_zombie_attack_leap_speed;
+float autocvar_g_monster_zombie_attack_stand_damage;
+float autocvar_g_monster_zombie_attack_stand_delay;
+float autocvar_g_monster_zombie_attack_stand_range;
+float autocvar_g_monster_zombie_health;
+float autocvar_g_monster_zombie_idle_timer;
+float autocvar_g_monster_zombie_speed_walk;
+float autocvar_g_monster_zombie_speed_run;
+float autocvar_g_monster_zombie_target_recheck_delay;
+float autocvar_g_monster_zombie_target_range;
+
+// zombie animations
+#define zombie_anim_attackleap                 0
+#define zombie_anim_attackrun1                 1
+#define zombie_anim_attackrun2                 2
+#define zombie_anim_attackrun3                 3
+#define zombie_anim_attackstanding1            4
+#define zombie_anim_attackstanding2            5
+#define zombie_anim_attackstanding3            6
+#define zombie_anim_blockend                   7
+#define zombie_anim_blockstart                 8
+#define zombie_anim_deathback1                 9
+#define zombie_anim_deathback2                 10
+#define zombie_anim_deathback3                 11
+#define zombie_anim_deathfront1                        12
+#define zombie_anim_deathfront2                        13
+#define zombie_anim_deathfront3                        14
+#define zombie_anim_deathleft1                 15
+#define zombie_anim_deathleft2                 16
+#define zombie_anim_deathright1                        17
+#define zombie_anim_deathright2                        18
+#define zombie_anim_idle                               19
+#define zombie_anim_painback1                  20
+#define zombie_anim_painback2                  21
+#define zombie_anim_painfront1                 22
+#define zombie_anim_painfront2                 23
+#define zombie_anim_runbackwards               24
+#define zombie_anim_runbackwardsleft           25
+#define zombie_anim_runbackwardsright          26
+#define zombie_anim_runforward                 27
+#define zombie_anim_runforwardleft             28
+#define zombie_anim_runforwardright            29
+#define zombie_anim_spawn                              30
+
+const vector ZOMBIE_MIN                                 = '-18 -18 -25';
+const vector ZOMBIE_MAX                                 = '18 18 47';
+
+#define ZOMBIE_STATE_SPAWNING          0
+#define ZOMBIE_STATE_IDLE                      1
+#define ZOMBIE_STATE_ANGRY                     2
+#define ZOMBIE_STATE_ATTACK_LEAP       3
+
+void zombie_spawn();
+void spawnfunc_monster_zombie();
+void zombie_think();
+
+void zombie_die ()
+{
+       Monster_CheckDropCvars ("zombie");
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.movetype           = MOVETYPE_TOSS;
+       self.think                      = Monster_Fade;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       
+       if (random() > 0.5)
+               self.frame = zombie_anim_deathback1;
+       else
+               self.frame = zombie_anim_deathfront1;
+               
+       monster_hook_death(); // for post-death mods
+}
+
+void zombie_attack_standing()
+{
+       float rand = random(), dot = 0, bigdmg = 0;
+
+       self.velocity_x = 0;
+       self.velocity_y = 0;
+       
+       if(self.monster_owner == self.enemy)
+       {
+               self.enemy = world;
+               return;
+       }
+       
+       bigdmg = autocvar_g_monster_zombie_attack_stand_damage * self.scale;
+
+       //print("zombie attacks!\n");
+       makevectors (self.angles);
+       dot = normalize (self.enemy.origin - self.origin) * v_forward;
+       if(dot > 0.3)
+       {
+               Damage(self.enemy, self, self, bigdmg * monster_skill, DEATH_MONSTER_MELEE, self.origin, '0 0 0');
+       }
+       
+       if (!monster_isvalidtarget(self.enemy, self, FALSE))
+               self.enemy = world;
+               
+       if (rand < 0.33)
+               self.frame = zombie_anim_attackstanding1;
+       else if (rand < 0.66)
+               self.frame = zombie_anim_attackstanding2;
+       else
+               self.frame = zombie_anim_attackstanding3;
+
+       self.nextthink = time + autocvar_g_monster_zombie_attack_stand_delay;
+}
+
+void zombie_attack_leap_touch()
+{
+       vector angles_face = '0 0 0';
+       float bigdmg = autocvar_g_monster_zombie_attack_leap_damage * self.scale;
+       
+       if (other.deadflag != DEAD_NO)
+               return;
+               
+       if (self.monster_owner == other)
+               return;
+       
+       if (other.takedamage == DAMAGE_NO)
+               return;
+               
+       //void Damage (entity targ, entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+       traceline(self.origin, other.origin, FALSE, self);
+
+       angles_face = vectoangles(self.moveto - self.origin);
+       angles_face = normalize(angles_face) * autocvar_g_monster_zombie_attack_leap_force;
+       Damage(other, self, self, bigdmg * monster_skill, DEATH_MONSTER_MELEE, trace_endpos, angles_face);      
+
+       // make this guy zombie's priority if it wasn't already
+       if (other.deadflag == DEAD_NO)
+       if (self.enemy != other)
+               self.enemy = other;
+               
+       self.touch = MonsterTouch;
+}
+
+void zombie_attack_leap()
+{
+       vector angles_face = '0 0 0', vel = '0 0 0';
+
+       // face the enemy       
+       self.state = ZOMBIE_STATE_ATTACK_LEAP;
+       self.frame = zombie_anim_attackleap;
+       angles_face = vectoangles(self.enemy.origin - self.origin);
+       self.angles_y = angles_face_y ;
+       self.nextthink = time + autocvar_g_monster_zombie_attack_leap_delay;
+       self.touch = zombie_attack_leap_touch;
+       makevectors(self.angles);
+       vel = normalize(v_forward);
+       self.velocity = vel * autocvar_g_monster_zombie_attack_leap_speed;
+}
+
+void zombie_think()
+{
+       float finished = FALSE, enemyDistance = 0, mySpeed = 0;
+       
+       self.think = zombie_think;
+       
+       if (self.state == ZOMBIE_STATE_ATTACK_LEAP) {
+               // reset to angry
+               self.state = ZOMBIE_STATE_ANGRY;
+               self.touch = func_null;
+       }
+       
+       if (self.state == ZOMBIE_STATE_SPAWNING) {
+               // become idle when zombie spawned
+               self.frame = zombie_anim_idle;
+               self.state = ZOMBIE_STATE_IDLE;
+       }
+       
+       if(self.enemy && !monster_isvalidtarget(self.enemy, self, FALSE))
+               self.enemy = world;
+       
+       if (self.enemy)
+       if (self.enemy.team == self.team || self.monster_owner == self.enemy)
+               self.enemy = world;
+       
+       if(teamplay && autocvar_g_monsters_teams && self.monster_owner.team != self.team)
+               self.monster_owner = world;     
+       
+       // remove enemy that ran away
+       if (self.enemy)
+       if (self.delay <= time) // check if we can do the rescan now
+       if (vlen(self.origin - self.enemy.origin) > autocvar_g_monster_zombie_target_range * self.scale) 
+       {
+               //print("removing enemy, he is too far: ", ftos(vlen(self.origin - self.enemy.origin)), "\n");
+               //print("delay was ", ftos(self.delay), "\n");
+               self.enemy = world;
+       } 
+       else
+               self.delay = time + autocvar_g_monster_zombie_target_recheck_delay;
+       
+       // find an enemy if no enemy available
+       if not(self.enemy) 
+       {
+               self.enemy = FindTarget(self);
+               if (self.enemy)
+                       self.delay = time + autocvar_g_monster_zombie_target_recheck_delay;
+       }
+
+       if (self.enemy) 
+       {
+               // make sure zombie is angry
+               self.state = ZOMBIE_STATE_ANGRY;
+               
+
+               // this zombie has an enemy, attack if close enough, go to it if not!
+               traceline(self.origin, self.enemy.origin, FALSE, self);
+               enemyDistance = vlen(trace_endpos - self.origin);
+               mySpeed = vlen(self.velocity);
+               
+               //print("speed ", ftos(mySpeed), "\n");
+               
+               if (trace_ent == self.enemy)
+               if (self.enemy.deadflag == DEAD_NO)
+               if (mySpeed <= 30)
+                       if (enemyDistance <= autocvar_g_monster_zombie_attack_stand_range * self.scale) 
+                       {
+                               //RadiusDamage (entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity ignore, float forceintensity, float deathtype, entity directhitentity)
+                               zombie_attack_standing();
+                               finished = TRUE;
+                       } 
+                       else if (enemyDistance <= autocvar_g_monster_zombie_attack_leap_range * self.scale) 
+                       {
+                               // do attackleap (set yaw, velocity, and check do damage on the first player entity it touches)
+                               zombie_attack_leap();
+                               finished = TRUE;
+                       }
+               
+       }
+       
+       self.nextthink = time + 1;
+
+       if not(finished) 
+       {
+               monster_move(autocvar_g_monster_zombie_speed_run, autocvar_g_monster_zombie_speed_walk, autocvar_g_monster_zombie_stopspeed, zombie_anim_runforward, zombie_anim_runforward, zombie_anim_idle);
+               
+               if (self.enemy || self.monster_owner)
+               {
+                       self.nextthink = time + 0.1;
+                       return;
+               }   
+       }
+       
+       if not(self.enemy || self.monster_owner || self.goalentity) 
+       {
+               // stay idle
+               //print("zombie is idling while waiting for some fresh meat...\n");
+               self.frame = ((mySpeed <= 20) ? zombie_anim_idle : zombie_anim_runforward);
+               self.nextthink = time + autocvar_g_monster_zombie_idle_timer * random();        
+       }
+}
+
+void zombie_spawn() 
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_zombie_health * self.scale;
+       
+       self.classname                  = "monster_zombie";
+       self.nextthink                  = time + 2.1;
+       self.pain_finished      = self.nextthink;
+       self.state                              = ZOMBIE_STATE_SPAWNING;
+       self.frame                              = zombie_anim_spawn;
+       self.think                              = zombie_think;
+       self.sprite_height      = 50 * self.scale;
+       self.skin                               = rint(random() * 4);
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+/*QUAKED monster_zombie (1 0 0) (-18 -18 -25) (18 18 47)
+Zombie, 60 health points.
+-------- KEYS --------
+-------- SPAWNFLAGS --------
+MONSTERFLAG_APPEAR: monster will spawn when triggered.
+---------NOTES----------
+Original Quake 1 zombie entity used a smaller box ('-16 -16 -24', '16 16 32').
+-------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY --------
+modeldisabled="models/monsters/zombie.dpm"
+*/
+void spawnfunc_monster_zombie() 
+{
+       if not(autocvar_g_monster_zombie) 
+       {
+               remove(self);
+               return;
+       }
+       
+       self.monster_spawnfunc = spawnfunc_monster_zombie;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       if not (monster_initialize(
+                        "Zombie",
+                        "models/monsters/zombie.dpm",
+                        ZOMBIE_MIN, ZOMBIE_MAX,
+                        FALSE,
+                        zombie_die, zombie_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
diff --git a/qcsrc/server/monsters/monsters.qh b/qcsrc/server/monsters/monsters.qh
new file mode 100644 (file)
index 0000000..67eec4b
--- /dev/null
@@ -0,0 +1,22 @@
+// Lib
+#include "lib/defs.qh"
+#include "lib/monsters.qc"
+
+// Monsters
+#include "lib/spawn.qc"
+#include "monster/ogre.qc"
+#include "monster/demon.qc"
+#include "monster/shambler.qc"
+#include "monster/knight.qc"
+#include "monster/soldier.qc"
+#include "monster/wizard.qc"
+#include "monster/dog.qc"
+#include "monster/tarbaby.qc"
+#include "monster/hknight.qc"
+#include "monster/fish.qc"
+#include "monster/shalrath.qc"
+#include "monster/enforcer.qc"
+#include "monster/zombie.qc"
+#include "monster/spider.qc"
+#include "monster/spawner.qc"
+string monsterlist () { return "ogre demon shambler knight soldier scrag dog spawn hellknight fish vore enforcer zombie spawner spider"; }
\ No newline at end of file
diff --git a/qcsrc/server/mutators/gamemode_td.qc b/qcsrc/server/mutators/gamemode_td.qc
new file mode 100644 (file)
index 0000000..fb859a6
--- /dev/null
@@ -0,0 +1,1092 @@
+// Tower Defense
+// Gamemode by Mario
+void spawnfunc_td_controller()
+{
+       if(autocvar_g_td_force_settings)
+       {
+               self.dontend = FALSE;
+               self.maxwaves = 0;
+               self.monstercount = 0;
+               self.startwave = 0;
+               self.maxturrets = 0;
+       }
+               
+       self.netname = "Tower Defense controller entity";
+       self.classname = "td_controller";
+               
+       gensurvived = FALSE;
+       td_dont_end = ((self.dontend) ? self.dontend : autocvar_g_td_generator_dontend);                
+       max_waves = ((self.maxwaves) ? self.maxwaves : autocvar_g_td_max_waves);        
+       totalmonsters = ((self.monstercount) ? self.monstercount : autocvar_g_td_monster_count);
+       wave_count = ((self.startwave) ? self.startwave : autocvar_g_td_start_wave);
+       max_turrets = ((self.maxturrets) ? self.maxturrets : autocvar_g_td_turret_max);
+               
+       wave_end(TRUE);
+}
+
+void td_generator_die() 
+{
+       entity tail;
+       
+       print((td_gencount > 1) ? "A generator was destroyed!\n" : "The generator was destroyed.\n");
+               
+       if(autocvar_sv_eventlog)
+               GameLogEcho(":gendestroyed");
+               
+       gendestroyed = TRUE;
+               
+       FOR_EACH_PLAYER(tail)
+       {
+               Send_CSQC_Centerprint_Generic(tail, CPID_KH_MSG, ((td_gencount > 1) ? "A generator was destroyed!" : "The generator was destroyed."), 0, 0);
+       }
+       
+       setmodel(self, "models/onslaught/generator_dead.md3");
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       td_gencount                     -= 1;
+               
+       pointparticles(particleeffectnum("explosion_medium"), self.origin, '0 0 0', 1);
+       
+       WaypointSprite_Kill(self.sprite);
+}
+
+void td_generator_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force) 
+{
+       if(attacker.classname == STR_PLAYER || attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET || attacker.vehicle_flags & VHF_ISVEHICLE)
+               return;
+               
+       entity tail;
+       
+       FOR_EACH_PLAYER(tail)
+       {
+               Send_CSQC_Centerprint_Generic(tail, CPID_KH_MSG, "The generator is under attack!", 0, 0);
+               gendmg += damage;
+       }
+       
+       self.health -= damage;
+       
+       WaypointSprite_UpdateHealth(self.sprite, self.health);
+               
+       if(self.health <= 0) 
+               td_generator_die();
+}
+
+void spawnfunc_td_generator() 
+{
+       if not(g_td) 
+       {
+               remove(self);
+               return;
+       }
+       
+       gendestroyed = FALSE;
+       
+       if not(self.health)
+               self.health = autocvar_g_td_generator_health;
+
+       // precache generator model
+       precache_model("models/onslaught/generator.md3");
+       precache_model("models/onslaught/generator_dead.md3");   
+       
+       self.model                  = "models/onslaught/generator.md3";
+       setmodel(self, self.model);
+       self.classname      = "td_generator";
+       self.solid                  = SOLID_BBOX;
+       self.takedamage     = DAMAGE_AIM;
+       self.event_damage   = td_generator_damage;
+       self.enemy                  = world;
+       self.nextthink      = -1;
+       self.think                  = func_null;
+       self.max_health     = self.health;
+       self.movetype       = MOVETYPE_NONE;
+       self.monster_attack = TRUE;
+       td_gencount                += 1;
+       self.netname            = "Generator";
+       
+       setsize(self, GENERATOR_MIN, GENERATOR_MAX);
+       
+       droptofloor();
+       
+       WaypointSprite_SpawnFixed(self.netname, self.origin + '0 0 60', self, sprite, RADARICON_OBJECTIVE, '1 0.5 0');  
+       WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
+       WaypointSprite_UpdateHealth(self.sprite, self.health);
+}
+
+void spawn_td_fuel(float fuel_size)
+{
+       if not(g_td)
+       {
+               remove(self);
+               return;
+       }
+       self.ammo_fuel = fuel_size * monster_skill;
+       StartItem("models/items/g_fuel.md3", "misc/itempickup.wav", g_pickup_respawntime_ammo, g_pickup_respawntimejitter_ammo, "Turret Fuel", IT_FUEL, 0, 0, commodity_pickupevalfunc, BOT_PICKUP_RATING_LOW);
+       
+       self.velocity = randomvec() * 175 + '0 0 325';
+}
+
+void spawnfunc_td_waypoint() 
+{
+       if not(g_td)
+       {
+               remove(self);
+               return;
+       }
+       string t1 = self.target;
+       
+       self.classname = "td_waypoint";
+       
+       if(self.target2 != "")
+       {
+               RandomSelection_Init();
+               RandomSelection_Add(world, 0, t1, 1, 1);
+               RandomSelection_Add(world, 0, self.target2, 1, 1);
+               
+               self.target = RandomSelection_chosen_string;
+       }
+}
+
+void spawnfunc_monster_swarm()
+{
+       if not(g_td)
+       {
+               remove(self);
+               return;
+       }
+       
+       string t1 = self.target;
+
+       swarmcount += 1;
+       
+       switch(self.spawntype)
+       {
+               case SWARM_SWIM:
+                       waterspawns_count += 1; break;
+               case SWARM_FLY:
+                       flyspawns_count += 1; break;
+               default:
+                       break;
+       }
+       
+       switch(self.spawnflags)
+       {
+               case SWARM_STRONG:
+                       self.classname = "swarm_strong"; break;
+               case SWARM_WEAK:
+                       self.classname = "swarm_weak"; break;
+               default:
+                       self.classname = "monster_swarm"; break;
+       }
+               
+       if(!self.protection_radius)
+               self.protection_radius = autocvar_g_td_monster_spawn_protection_radius;
+       
+       if(self.target2 != "")
+       {
+               RandomSelection_Init();
+               RandomSelection_Add(world, 0, t1, 1, 1);
+               RandomSelection_Add(world, 0, self.target2, 1, 1);
+               
+               self.target = RandomSelection_chosen_string;
+       }       
+       
+       WaypointSprite_SpawnFixed("Monsters", self.origin + '0 0 60', self, sprite, RADARICON_HERE, '1 0.5 0');
+       
+       if(self.target == "")
+               print("monster_swarm entity without a valid target, monsters will try to follow waypoints instead.\n");
+}
+
+void spawnturret(entity spawnedby, entity own, string turet, vector orig)
+{
+       if(spawnedby.classname != STR_PLAYER)
+       {
+               print("Warning: A non-player entity tried to spawn a turret.\n");
+               return;
+       }
+               
+       entity oldself;
+       
+       oldself = self;
+       self = spawn();
+       
+       self.origin = orig;
+       self.spawnflags = TSL_NO_RESPAWN;
+       self.monster_attack = TRUE;
+       self.realowner = own;
+       self.angles_y = spawnedby.v_angle_y;
+       spawnedby.turret_cnt += 1;
+       self.colormap = spawnedby.colormap;
+       
+       switch(turet)
+       {
+               default:
+               case "turret_plasma": spawnfunc_turret_plasma(); break;
+               case "turret_mlrs": spawnfunc_turret_mlrs(); break;
+               case "turret_phaser": spawnfunc_turret_phaser(); break;
+               case "turret_hellion": spawnfunc_turret_hellion(); break;
+               case "turret_walker": spawnfunc_turret_walker(); break;
+               case "turret_flac": spawnfunc_turret_flac(); break;
+               case "turret_tesla": spawnfunc_turret_tesla(); break;
+               case "turret_fusionreactor": spawnfunc_turret_fusionreactor(); break;
+       }
+               
+       self = oldself;
+}
+
+void buffturret (entity tur, float buff)
+{
+       tur.turret_buff           += 1;
+       tur.max_health            *= buff;
+       tur.tur_health             = tur.max_health;
+       tur.health                         = tur.max_health;
+       tur.ammo_max              *= buff;
+       tur.ammo_recharge     *= buff;
+    tur.shot_dmg          *= buff;
+    tur.shot_refire       -= buff * 0.2;
+    tur.shot_radius       *= buff;
+    tur.shot_speed        *= buff;
+    tur.shot_spread       *= buff;
+    tur.shot_force        *= buff;
+}
+
+void AnnounceSpawn(string anounce)
+{
+       entity tail;
+       FOR_EACH_PLAYER(tail)
+       {
+               Send_CSQC_Centerprint_Generic(tail, CPID_KH_MSG, strcat("^1A ", anounce, " has arrived!"), 0, 0);
+       }
+}
+
+entity PickSpawn (string strngth, string type)
+{
+       entity e;
+       RandomSelection_Init();
+       for(e = world;(e = find(e, classname, strngth)); ) 
+       {
+               RandomSelection_Add(e, 0, string_null, 1, 1); 
+       }
+
+       return RandomSelection_chosen_ent;
+}
+
+void TD_SpawnMonster(string mnster, string strngth, string type)
+{
+       entity e, mon;
+       
+       e = PickSpawn(strngth, type);
+       
+       if(e == world)
+               e = PickSpawn("monster_swarm", "");
+  
+       mon = spawnmonster(mnster, e, e, e.origin, FALSE, 0);
+       mon.target = e.target;
+}
+
+string Monster_GetStrength(string mnster)
+{
+       switch(mnster)
+       {
+               case "knight":
+               case "wizard":
+               case "soldier":
+               case "enforcer":
+               case "zombie":
+               case "tarbaby":
+               case "dog":
+               case "spider":
+               case "fish":
+                       return "swarm_weak";
+               case "ogre":
+               case "shambler":
+               case "shalrath":
+               case "hellknight":
+               case "demon":
+                       return "swarm_strong";
+               default:
+                       return "monster_swarm";
+       }
+}
+
+string Monster_GetType(string mnster)
+{
+       switch(mnster)
+       {
+               default:
+               case "knight":
+               case "soldier":
+               case "enforcer":
+               case "zombie":
+               case "spider":
+               case "tarbaby":
+               case "dog":
+               case "ogre":
+               case "shambler":
+               case "shalrath":
+               case "hellknight":
+               case "demon":
+                       return "monster_swarm";
+               case "wizard":
+                       return "monster_fly";
+               case "fish":
+                       return "monster_swim";
+       }
+}
+
+string RandomMonster()
+{
+       RandomSelection_Init();
+       
+       if(n_demons) RandomSelection_Add(world, 0, "demon", 1, 1);
+       if(n_wizards && flyspawns_count > 0) RandomSelection_Add(world, 0, "scrag", 1, 1);
+       if(n_shalraths) RandomSelection_Add(world, 0, "vore", 1, 1);
+       if(n_soldiers) RandomSelection_Add(world, 0, "soldier", 1, 1);
+       if(n_hknights) RandomSelection_Add(world, 0, "hellknight", 1, 1);
+       if(n_enforcers) RandomSelection_Add(world, 0, "enforcer", 1, 1);
+       if(n_zombies) RandomSelection_Add(world, 0, "zombie", 1, 1);
+       if(n_spiders) RandomSelection_Add(world, 0, "spider", 1, 1);
+       if(n_ogres) RandomSelection_Add(world, 0, "ogre", 1, 1);
+       if(n_dogs) RandomSelection_Add(world, 0, "dog", 1, 1);
+       if(n_knights) RandomSelection_Add(world, 0, "knight", 1, 1);
+       if(n_shamblers) RandomSelection_Add(world, 0, "shambler", 0.2, 0.2);
+       if(n_tarbabies) RandomSelection_Add(world, 0, "spawn", 0.2, 0.2);
+       if(n_fish && waterspawns_count > 0) RandomSelection_Add(world, 0, "fish", 0.2, 0.2);
+       
+       return RandomSelection_chosen_string;
+}
+
+void combat_phase()
+{
+       string monstrngth, whichmon, montype;
+       
+       current_phase = PHASE_COMBAT;
+       
+       if(monster_count <= 0)
+       {
+               wave_end(FALSE);
+               return;
+       }
+       
+       self.think = combat_phase;
+       
+       whichmon = RandomMonster();
+       
+       monstrngth = Monster_GetStrength(whichmon);
+       montype = Monster_GetType(whichmon);
+       
+       if(current_monsters < autocvar_g_td_current_monsters && whichmon != "")
+       {
+               TD_SpawnMonster(whichmon, monstrngth, montype);
+               self.nextthink = time + 3;
+       }
+       else
+               self.nextthink = time + 6;
+}
+
+void queue_monsters(float maxmonsters)
+{
+       float mc = 11; // note: shambler + tarbaby = 1
+       
+       if(waterspawns_count > 0)
+               mc += 1;
+       if(flyspawns_count > 0)
+               mc += 1;
+               
+       DistributeEvenly_Init(maxmonsters, mc);
+       n_demons        = DistributeEvenly_Get(1);
+       n_ogres         = DistributeEvenly_Get(1);
+       n_dogs          = DistributeEvenly_Get(1);
+       n_knights   = DistributeEvenly_Get(1);
+       n_shalraths = DistributeEvenly_Get(1);
+       n_soldiers  = DistributeEvenly_Get(1);
+       n_hknights  = DistributeEvenly_Get(1);
+       n_enforcers = DistributeEvenly_Get(1);
+       n_zombies   = DistributeEvenly_Get(1);
+       n_spiders   = DistributeEvenly_Get(1);
+       n_tarbabies = DistributeEvenly_Get(0.7);
+       n_shamblers = DistributeEvenly_Get(0.3);
+       if(flyspawns_count > 0)
+               n_wizards   = DistributeEvenly_Get(1);
+       if(waterspawns_count > 0)
+               n_fish = DistributeEvenly_Get(1);
+}
+
+void combat_phase_begin()
+{
+       if(autocvar_g_td_hardcore)
+               cvar_set("g_td_respawn_delay", "999");
+               
+       monster_count = totalmonsters;
+       entity head, tail;
+       
+       print("^1Combat phase!\n");
+       FOR_EACH_PLAYER(tail)
+       {
+               Send_CSQC_Centerprint_Generic(tail, CPID_KH_MSG, "^1Combat phase!", 0, 0);
+       }
+       if(autocvar_sv_eventlog)
+               GameLogEcho(":combatphase");
+       self.think = combat_phase;
+       self.nextthink = time + 1;
+       
+       for(head = world;(head = find(head, classname, "td_generator")); )
+       {
+               head.takedamage = DAMAGE_AIM;
+       }
+}
+
+float cphase_updates;
+void combat_phase_announce() // TODO: clean up these fail nextthinks...
+{
+       cphase_updates += 1;
+       
+       if(cphase_updates == 0)
+               Announce("prepareforbattle");
+       else if(cphase_updates == 3)
+               Announce("3");
+       else if(cphase_updates == 4)
+               Announce("2");
+       else if(cphase_updates == 5)
+               Announce("1");
+       else if(cphase_updates == 6)
+       {
+               Announce("begin");
+               oldrespawncvar = cvar("g_td_respawn_delay");
+               combat_phase_begin();
+       }
+       
+       if(cphase_updates >= 6)
+               return;
+
+       self.think = combat_phase_announce;
+       self.nextthink = time + 1;
+}
+
+void build_phase()
+{
+       entity head;
+       float n_players = 0, gen_washealed = FALSE, player_washealed = FALSE;
+    string buildmsg, healmsg, countmsg, startmsg, genhealmsg;
+       
+       current_phase = PHASE_BUILD;
+       
+       cvar_set("g_td_respawn_delay", ftos(oldrespawncvar));
+       
+       for(head = world;(head = find(head, classname, "td_generator")); )
+       {
+               if(head.health <= 5 && head.max_health > 10)
+                       Announce("lastsecond");
+                       
+               if(head.health < head.max_health)
+               {
+                       gen_washealed = TRUE;
+                       head.health = head.max_health;
+                       WaypointSprite_UpdateHealth(head.sprite, head.health);
+               }
+               head.takedamage = DAMAGE_NO;
+       }
+       
+       FOR_EACH_PLAYER(head)
+       {
+               if(head.health < 100)
+               {
+                       player_washealed = TRUE;
+                       break; // we found 1, so no need to check the others
+               }
+       }
+               
+       totalmonsters += autocvar_g_td_monster_count_increment * wave_count;
+       monster_skill += autocvar_g_td_monsters_skill_increment;
+       
+       monsters_total = totalmonsters;
+       monsters_killed = 0;
+       
+       if(wave_count < 1) wave_count = 1;
+
+       genhealmsg = (gen_washealed) ? ((td_gencount == 1) ? " and generator " : " and generators ") : "";
+       buildmsg = sprintf("%s build phase... ", (wave_count == max_waves) ? "^1Final wave^3" : sprintf("Wave %d", wave_count));
+       healmsg = (player_washealed) ? sprintf("All players %shealed. ", genhealmsg) : "";      
+    countmsg = sprintf("Next monsters: %d. ", totalmonsters);
+    startmsg = sprintf("Wave starts in %d seconds", autocvar_g_td_buildphase_time);
+       
+       FOR_EACH_PLAYER(head) 
+    {
+               if(head.health < 100)
+                       head.health = 100;
+                       
+               if(gen_washealed)
+                       PlayerScore_Add(head, SP_TD_SCORE, -autocvar_g_td_generator_damaged_points);
+                       
+        n_players += 1;
+        Send_CSQC_Centerprint_Generic(head, CPID_KH_MSG, strcat(buildmsg, healmsg, countmsg, startmsg), 5, 0);
+               
+    }
+       
+       gendmg = 0;
+    
+    FOR_EACH_MONSTER(head)
+    {
+               if(head.health <= 0)
+                       continue;
+        print(strcat("Warning: Monster still alive during build phase! Monster name: ", head.netname, "\n"));
+        remove(head);
+    }
+       
+       if(n_players >= 2)
+       {
+               totalmonsters += n_players;
+               monster_skill += n_players * 0.05;
+       }
+       
+       if(monster_skill < 1) monster_skill = 1;
+               
+       if(totalmonsters < 1) totalmonsters = ((autocvar_g_td_monster_count > 0) ? autocvar_g_td_monster_count : 10);
+       
+       rint(totalmonsters); // to be safe
+       
+       print(strcat(buildmsg, healmsg, countmsg, startmsg, "\n"));
+               
+       queue_monsters(totalmonsters);
+       
+       cphase_updates = -1;
+       
+       if(autocvar_sv_eventlog)
+        GameLogEcho(sprintf(":buildphase:%d:%d", wave_count, totalmonsters));
+       
+       self.think = combat_phase_announce;
+       self.nextthink = time + autocvar_g_td_buildphase_time - 6;
+}
+
+void wave_end(float starting)
+{
+       entity tail;
+       FOR_EACH_PLAYER(tail)
+       {
+               if(starting)
+                       Send_CSQC_Centerprint_Generic(tail, CPID_KH_MSG, "Defend the generator from waves of monsters!", 0, 0);
+               else
+                       Send_CSQC_Centerprint_Generic(tail, CPID_KH_MSG, ((wave_count >= max_waves) ? "Level victory!" : "Wave victory!"), 0, 0);
+       }
+       
+       if not(starting)
+       {
+               print((wave_count >= max_waves) ? "^2Level victory!\n" : "^2Wave victory!\n");
+               if(autocvar_sv_eventlog)
+            GameLogEcho(sprintf(":wave:%d:victory", wave_count));
+       }
+       
+       if(wave_count >= max_waves)
+       {
+               gensurvived = TRUE;
+               return;
+       }
+       
+       if(starting)
+               monster_skill = autocvar_g_td_monsters_skill_start;
+       else
+               wave_count += 1;
+               
+       self.think = build_phase;
+       self.nextthink = time + 3;
+}
+
+void td_ScoreRules()
+{
+       ScoreInfo_SetLabel_PlayerScore(SP_TD_SCORE,             "score",         SFL_SORT_PRIO_PRIMARY);
+       ScoreInfo_SetLabel_PlayerScore(SP_TD_KILLS,             "kills",         SFL_LOWER_IS_BETTER);
+       ScoreInfo_SetLabel_PlayerScore(SP_TD_TURKILLS,  "frags",        SFL_LOWER_IS_BETTER);
+       ScoreInfo_SetLabel_PlayerScore(SP_TD_DEATHS,    "deaths",    SFL_LOWER_IS_BETTER);
+       ScoreInfo_SetLabel_PlayerScore(SP_TD_SUICIDES,  "suicides",  SFL_LOWER_IS_BETTER | SFL_ALLOW_HIDE);
+       ScoreRules_basics_end();
+}
+
+void td_SpawnController()
+{
+       entity oldself = self;
+       self = spawn();
+       self.classname = "td_controller";
+       spawnfunc_td_controller();
+       self = oldself;
+}
+
+void td_DelayedInit()
+{
+       if(find(world, classname, "td_controller") == world)
+       {
+               print("No ""td_controller"" entity found on this map, creating it anyway.\n");
+               td_SpawnController();
+       }
+       
+       td_ScoreRules();
+}
+
+void td_Init()
+{
+       InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
+}
+
+MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
+{
+       if(turret.turrcaps_flags & TFL_TURRCAPS_SUPPORT && turret_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
+               return TRUE;
+       else if not(turret_target.flags & FL_MONSTER)
+               turret_target = world;
+               
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_PlayerThink)
+{
+       self.stat_current_wave = wave_count;
+       self.stat_totalwaves = max_waves;
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_PlayerCanJoin)
+{
+       entity player;
+       float n_players = 0;
+       
+       FOR_EACH_REALPLAYER(player) { if(clienttype(player) != CLIENTTYPE_BOT) n_players += 1; }
+       
+       if(current_phase == PHASE_COMBAT && n_players >= 1)
+               return TRUE;
+               
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
+{
+       self.bot_attack = FALSE;
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_PlayerDies)
+{
+       if(frag_attacker.flags & FL_MONSTER)
+               PlayerScore_Add(frag_target, SP_TD_DEATHS, 1);
+               
+       if(frag_target == frag_attacker)
+               PlayerScore_Add(frag_attacker, SP_TD_SUICIDES, 1);
+
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_GiveFragsForKill)
+{
+       frag_score = 0;
+               
+       return TRUE; // no frags counted in td
+}
+
+MUTATOR_HOOKFUNCTION(td_PlayerDamage_Calculate)
+{
+       if(frag_attacker.realowner == frag_target)
+               frag_damage = 0;
+               
+       if(frag_target.vehicle_flags & VHF_ISVEHICLE && !DEATH_ISMONSTER(frag_deathtype))
+               frag_damage = 0;
+               
+       if(DEATH_ISVEHICLE(frag_deathtype) && !(frag_target.flags & FL_MONSTER))
+               frag_damage = 0;
+               
+       if(!autocvar_g_td_pvp && frag_attacker != frag_target && frag_target.classname == STR_PLAYER && frag_attacker.classname == STR_PLAYER)
+               frag_damage = 0;
+               
+       if(frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET && frag_target.classname == STR_PLAYER)
+               frag_damage = 0;
+               
+       if((frag_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && !(DEATH_ISMONSTER(frag_deathtype) || frag_attacker.turrcaps_flags & TFL_TURRCAPS_SUPPORT))
+               frag_damage = 0;
+               
+       if((frag_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && frag_attacker == frag_target.realowner && frag_target.realowner.classname == STR_PLAYER)
+       {
+               if(frag_attacker.turret_removemode)
+               {
+                       frag_attacker.turret_cnt -= 1;
+                       frag_attacker.turret_removemode = 0;
+                       sprint(frag_attacker, strcat("You removed your ", frag_target.netname, "\n"));
+                       remove(frag_target);
+                       return FALSE;
+               }
+               else if(frag_attacker.turret_buffmode)
+               {
+                       if(frag_attacker.ammo_fuel < 100)       
+                       {
+                               sprint(frag_attacker, "You need 100 fuel to increase this turret's power.\n");
+                               frag_attacker.turret_buffmode = 0;
+                               return FALSE;
+                       }
+                       else if(frag_target.turret_buff >= 3)
+                       {
+                               sprint(frag_attacker, "This turret cannot be buffed up any higher.\n");
+                               frag_attacker.turret_buffmode = 0;
+                               return FALSE;
+                       }
+                       
+                       frag_attacker.ammo_fuel -= 100;
+                       
+                       buffturret(frag_target, 1.2);
+                       
+                       frag_attacker.turret_buffmode = 0;
+                       sprint(frag_attacker, "Turret power increased by 20%!\n");
+                       return FALSE;
+               }
+               return FALSE;
+       }
+               
+       return TRUE;
+}
+
+MUTATOR_HOOKFUNCTION(td_MonsterCheckBossFlag)
+{
+       // No minibosses in tower defense
+       return TRUE;
+}
+
+MUTATOR_HOOKFUNCTION(td_MonsterMove)
+{
+       entity player;
+       float n_players = 0;
+       FOR_EACH_PLAYER(player) { ++n_players; }
+       
+       if(n_players < 1) // no players online, so do nothing
+       {
+               monster_target = world;
+               monster_speed_run = monster_speed_walk = 0;
+               return FALSE;
+       }
+       
+       monster_speed_run = 110 * monster_skill;
+       monster_speed_walk = 75 * monster_skill;
+       
+       if(vlen(self.realowner.origin - self.origin) < self.realowner.protection_radius && self.realowner.classname == "monster_swarm")
+        self.takedamage = DAMAGE_NO;
+    else
+        self.takedamage = DAMAGE_AIM;
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
+{
+       if(self.realowner && self.realowner.flags & FL_CLIENT)
+       {
+               sprint(self.realowner, "You can't spawn monsters in Tower Defense mode. Removed monster.\n");
+               if(self.sprite)
+                       WaypointSprite_Kill(self.sprite);
+               remove(self);
+               return TRUE;
+       }
+       
+       if(self.realowner == world) // nothing spawned it, so kill it
+       {
+               remove(self);
+               return TRUE;
+       }
+       
+       self.lastcheck = time;
+       
+       self.drop_size = self.health * 0.05;
+       
+       if(self.drop_size < 1) self.drop_size = 1;
+       
+       if(self.target) // follow target if available
+               self.goalentity = find(world, targetname, self.target);
+       
+       self.origin += '0 0 25'; // hopefully this fixes monsters falling through the floor
+       
+       switch(self.classname)
+       {
+               case "monster_knight": n_knights -= 1; break;
+               case "monster_dog": n_dogs -= 1; break;
+               case "monster_ogre": n_ogres -= 1; break;
+               case "monster_shambler": n_shamblers -= 1; AnnounceSpawn("Shambler"); break;
+               case "monster_wizard": n_wizards -= 1; break;
+               case "monster_shalrath": n_shalraths -= 1; break;
+               case "monster_soldier": n_soldiers -= 1; break;
+               case "monster_hellknight": n_hknights -= 1; break;
+               case "monster_enforcer": n_enforcers -= 1; break;
+               case "monster_demon": n_demons -= 1; break;
+               case "monster_zombie": n_zombies -= 1; break;
+               case "monster_spider": n_spiders -= 1; break;
+               case "monster_tarbaby": n_tarbabies -= 1; break;
+       }
+       
+       return TRUE;
+}
+
+MUTATOR_HOOKFUNCTION(td_MonsterDies)
+{
+       entity oldself;
+       vector backuporigin;
+
+       monster_count -= 1;
+       current_monsters -= 1;
+       monsters_killed += 1;
+       
+       if(frag_attacker.classname == STR_PLAYER)
+       {
+               PlayerScore_Add(frag_attacker, SP_TD_SCORE, autocvar_g_td_kill_points);
+               PlayerScore_Add(frag_attacker, SP_TD_KILLS, 1);
+               frag_attacker.monsterskilled += 1;
+       }
+       else if(frag_attacker.realowner.classname == STR_PLAYER)
+       {
+               PlayerScore_Add(frag_attacker.realowner, SP_TD_SCORE, autocvar_g_td_turretkill_points);
+               PlayerScore_Add(frag_attacker.realowner, SP_TD_TURKILLS, 1);
+               frag_attacker.realowner.monsterskilled += 1;
+       }
+
+       backuporigin = self.origin;
+       oldself = self;
+       self = spawn();
+       
+       self.gravity = 1;
+       setorigin(self, backuporigin + '0 0 5');
+       spawn_td_fuel(oldself.drop_size);
+       self.touch = M_Item_Touch;
+       if(self == world)
+       {
+               self = oldself;
+               return FALSE;
+       }
+       SUB_SetFade(self, time + 5, 1);
+       
+       self = oldself;
+
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
+{
+       float n_players = 0;
+       entity head, player;
+       local entity e;
+       
+       FOR_EACH_PLAYER(player) { ++n_players; }
+       
+       if(n_players < 1) // no players online, so do nothing
+       {
+               return TRUE;
+       }
+       
+       if(vlen(self.goalentity.origin - self.origin) <= 100 && self.goalentity.classname == "waypoint")
+               self.goalentity.lastchecked = self;
+               
+       if((vlen(self.goalentity.origin - self.origin) <= 100 && self.goalentity.classname == "td_waypoint") || (vlen(self.goalentity.origin - self.origin) <= 200 && self.flags & FL_FLY && self.goalentity.classname == "td_waypoint"))
+       {
+               self.goalentity = find(world, targetname, self.goalentity.target);
+               self.target = self.goalentity.target;
+       }
+       
+       if(generator == world)
+       {
+               if(td_gencount == 1)
+                       generator = find(world, classname, "td_generator");
+               else
+               {
+                       RandomSelection_Init();
+                       for(head = world;(head = find(head, classname, "td_generator")); )
+                       {
+                               RandomSelection_Add(head, 0, string_null, 1, 1);
+                       }
+                       generator = RandomSelection_chosen_ent; 
+               }
+       }
+       
+       for(e = world;(e = findflags(e, monster_attack, TRUE)); ) 
+       {
+               if(monster_isvalidtarget(e, self, FALSE))
+               if((vlen(trace_endpos - self.origin) < 100 && e.turrcaps_flags & TFL_TURRCAPS_ISTURRET) || (vlen(trace_endpos - self.origin) < 200 && e != generator) || (vlen(trace_endpos - self.origin) < 500 && e == generator))
+               {
+                       self.enemy = e;
+                       return TRUE;
+               }
+       }
+       if(self.target) // follow target if available
+       {
+               self.goalentity = find(world, targetname, self.target);
+               if(self.goalentity == world)
+                       self.goalentity = generator;
+               return TRUE;
+       }
+       else
+               self.goalentity = generator;
+               
+       for(e = world;(e = find(e, classname, "waypoint")); )
+       {
+               if(vlen(e.origin - self.origin) < 500)
+               if(e.lastchecked != self)
+               if(vlen(e.origin - self.origin) > 50)
+               {
+                       //print(strcat("Goal found at ", vtos(e.origin), "\n"));
+                       self.goalentity = e;
+               }
+       }
+       
+       return TRUE;
+}
+
+MUTATOR_HOOKFUNCTION(td_SetStartItems)
+{
+       // no start ammo, so player must rely on monster droppings (TODO: random drops for monsters)
+       start_ammo_rockets = 0;
+       start_ammo_cells = 0;
+       start_ammo_nails = 0;
+       start_ammo_fuel = 150; // to be nice...
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_TurretSpawn)
+{
+       self.bot_attack = FALSE;
+       self.turret_buff = 1;
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_PlayerCommand)
+{
+       if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
+       if(cmd_name == "turretspawn")
+       {
+               if(argv(1) == "list")
+               {
+                       sprint(self, "Available turrets:\n");
+                       sprint(self, "^3mlrs walker plasma towerbuff\n");
+                       return TRUE;
+               }
+               if(self.classname != STR_PLAYER || self.health <= 0)
+               { 
+                       sprint(self, "Can't spawn turrets while spectating/dead.\n");
+                       return TRUE;
+               }
+               if(self.turret_cnt >= max_turrets)
+               {
+                       sprint(self, sprintf("Can't spawn more than %d turrets.\n", max_turrets));
+                       return TRUE;
+               }
+               makevectors(self.v_angle);
+               WarpZone_TraceLine(self.origin + self.view_ofs, self.origin + self.view_ofs + v_forward * 100, MOVE_NORMAL, self);
+               switch(argv(1))
+               {
+                       case "plasma":
+                       {
+                if(self.ammo_fuel < autocvar_g_td_turret_plasma_cost) break;
+                               self.ammo_fuel -= autocvar_g_td_turret_plasma_cost;
+                               spawnturret(self, self, "turret_plasma", trace_endpos);
+                               sprint(self, "Spawned 1 plasma turret", "\n");
+                               return TRUE;
+                       }
+                       case "mlrs":
+                       {
+                if(self.ammo_fuel < autocvar_g_td_turret_mlrs_cost) break;
+                               self.ammo_fuel -= autocvar_g_td_turret_mlrs_cost;
+                               spawnturret(self, self, "turret_mlrs", trace_endpos);
+                               sprint(self, "Spawned 1 MLRS turret", "\n");
+                               return TRUE;
+                       }
+                       case "walker":
+                       {
+                if(self.ammo_fuel < autocvar_g_td_turret_walker_cost) break;
+                               self.ammo_fuel -= autocvar_g_td_turret_walker_cost;
+                               spawnturret(self, self, "turret_walker", trace_endpos);
+                               sprint(self, "Spawned 1 walker turret", "\n");
+                               return TRUE;
+                       }
+                       case "towerbuff":
+                       {
+                if(self.ammo_fuel < autocvar_g_td_tower_buff_cost) break;
+                               self.ammo_fuel -= autocvar_g_td_tower_buff_cost;
+                               spawnturret(self, self, "turret_fusionreactor", trace_endpos);
+                               sprint(self, "Spawned 1 tower buff turret\n");
+                               return TRUE;
+                       }
+                       default:
+                       {
+                               sprint(self, "Invalid turret. type 'cmd turret list' to see a list of all available turrets.\n");
+                               return TRUE;
+                       }
+               }
+               sprint(self, sprintf("You do not have enough fuel to spawn a %s turret.\n", argv(1)));
+               return TRUE;
+       }
+       if(cmd_name == "buffturret")
+       {
+               sprint(self, "Shoot your turret to buff it up!\n");
+               self.turret_buffmode = 1;
+               return TRUE;
+       }
+       if(cmd_name == "turretremove")
+       {
+               sprint(self, "Shoot your turret to remove it\n");
+               self.turret_removemode = 1;
+               return TRUE;
+       }
+       if(cmd_name == "debugmonsters")
+       {
+        sprint(self, strcat("^3Current wave: ^1", ftos(wave_count), "\n"));
+               sprint(self, strcat("^3Maximum waves: ^1", ftos(max_waves), "\n"));
+               sprint(self, strcat("^3Monster skill: ^1", ftos(monster_skill), "\n"));
+               sprint(self, strcat("^3Monster spawns: ^1", ftos(swarmcount), "\n"));
+               sprint(self, strcat("^3Current monsters: ^1", ftos(monster_count), "\n"));
+               sprint(self, strcat("^3Maximum monsters: ^1", ftos(totalmonsters), "\n"));
+        sprint(self, strcat("^3Current ogres: ^1", ftos(n_ogres), "\n"));
+        sprint(self, strcat("^3Current knights: ^1", ftos(n_knights), "\n"));
+        sprint(self, strcat("^3Current dogs: ^1", ftos(n_dogs), "\n"));
+        sprint(self, strcat("^3Current shamblers: ^1", ftos(n_shamblers), "\n"));
+        sprint(self, strcat("^3Current scrags: ^1", ftos(n_wizards), "\n"));
+        sprint(self, strcat("^3Current vores: ^1", ftos(n_shalraths), "\n"));
+        sprint(self, strcat("^3Current grunts: ^1", ftos(n_soldiers), "\n"));
+        sprint(self, strcat("^3Current hell knights: ^1", ftos(n_hknights), "\n"));
+        sprint(self, strcat("^3Current enforcers: ^1", ftos(n_enforcers), "\n"));
+        sprint(self, strcat("^3Current fiends: ^1", ftos(n_demons), "\n"));
+               sprint(self, strcat("^3Current zombies: ^1", ftos(n_zombies), "\n"));
+               sprint(self, strcat("^3Current spawns: ^1", ftos(n_tarbabies), "\n"));
+               sprint(self, strcat("^3Current rotfish: ^1", ftos(n_fish), "\n"));
+               sprint(self, strcat("^3Current spiders: ^1", ftos(n_spiders), "\n"));
+               return TRUE;
+       }
+       
+       return FALSE;
+}
+
+MUTATOR_DEFINITION(gamemode_td)
+{
+       MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterMove, td_MonsterMove, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterCheckBossFlag, td_MonsterCheckBossFlag, CBC_ORDER_ANY);
+       MUTATOR_HOOK(SetStartItems, td_SetStartItems, CBC_ORDER_ANY);
+       MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
+       MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
+       MUTATOR_HOOK(GiveFragsForKill, td_GiveFragsForKill, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerCanJoin, td_PlayerCanJoin, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerPreThink, td_PlayerThink, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerDies, td_PlayerDies, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerDamage_Calculate, td_PlayerDamage_Calculate, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
+       MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, 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.");        
+               cvar_settemp("g_monsters", "1");
+               td_Init();
+       }
+
+       MUTATOR_ONREMOVE
+       {
+               error("This is a game type and it cannot be removed at runtime.");
+       }
+
+       return FALSE;
+}
diff --git a/qcsrc/server/mutators/gamemode_td.qh b/qcsrc/server/mutators/gamemode_td.qh
new file mode 100644 (file)
index 0000000..6926876
--- /dev/null
@@ -0,0 +1,60 @@
+// Counters
+float monster_count, totalmonsters;
+float n_knights, n_dogs, n_ogres, n_shamblers, n_wizards, n_shalraths, n_soldiers, n_hknights, n_enforcers, n_demons, n_zombies, n_tarbabies, n_fish, n_spiders;
+float current_monsters;
+float waterspawns_count, flyspawns_count;
+float wave_count, max_waves;
+float swarmcount;
+float max_turrets;
+.float monsterskilled;
+
+// Monster defs
+.float drop_size;
+
+// Turret defs
+.float turret_removemode;
+.float turret_buffmode;
+.float turret_buff;
+
+// TD defs
+.float stat_current_wave;
+.float stat_totalwaves;
+.float spawntype;
+float SWARM_NORMAL     = 0;
+float SWARM_WEAK       = 1;
+float SWARM_STRONG     = 2;
+float SWARM_FLY                = 3;
+float SWARM_SWIM       = 4;
+float td_dont_end;
+.float lastcheck;
+void(float starting) wave_end;
+.float turret_cnt;
+float td_gencount;
+void() spawnfunc_td_controller;
+float oldrespawncvar;
+.float protection_radius;
+float current_phase;
+#define PHASE_BUILD    1
+#define PHASE_COMBAT   2
+
+// Scores
+#define SP_TD_KILLS    0
+#define SP_TD_TURKILLS         2
+#define SP_TD_SCORE    4
+#define SP_TD_DEATHS   6
+#define SP_TD_SUICIDES         8
+
+// Controller
+.float maxwaves;
+.float monstercount;
+.float startwave;
+.float dontend;
+.float maxturrets;
+
+// Generator
+float gendestroyed;
+.entity lastchecked;
+entity generator; // global generator entity (TODO: replace with a script for multi generator support?)
+float gendmg;
+#define GENERATOR_MIN '-52 -52 -14'
+#define GENERATOR_MAX '52 52 75'
\ No newline at end of file
diff --git a/qcsrc/server/mutators/mutator_zombie_apocalypse.qc b/qcsrc/server/mutators/mutator_zombie_apocalypse.qc
new file mode 100644 (file)
index 0000000..85b21dc
--- /dev/null
@@ -0,0 +1,107 @@
+// Zombie Apocalypse mutator - small side project
+// Spawns a defined number of zombies at the start of a match
+
+float za_numspawns;
+entity PickZombieSpawn()
+{
+       entity sp;
+       
+       RandomSelection_Init();
+       
+       if(teamplay)
+       {
+               for(sp = world; (sp = find(sp, classname, "info_player_team1")); )
+               {
+                       RandomSelection_Add(sp, 0, string_null, 1, 1);
+               }
+       }
+       else
+       {
+               for(sp = world; (sp = find(sp, classname, "info_player_deathmatch")); )
+               {
+                       RandomSelection_Add(sp, 0, string_null, 1, 1);
+               }
+       }
+       
+       return RandomSelection_chosen_ent;
+}
+
+void zombie_spawn_somewhere ()
+{
+       if(gameover) { return; }
+    
+    entity mon, sp;
+       
+       if(MoveToRandomMapLocation(self, DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_PLAYERCLIP, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_BODY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 10, 1024, 256))
+       {
+               mon = spawnmonster("zombie", self, self, self.origin, TRUE, 2);
+               tracebox(mon.origin, mon.mins, mon.maxs, mon.origin, MOVE_NOMONSTERS, mon);
+
+               if(trace_startsolid)
+               {
+                       sp = PickZombieSpawn();
+                       if(sp)
+                               setorigin(mon, sp.origin);
+               }
+                       
+        za_numspawns += 1;
+       }
+       else
+               zombie_spawn_somewhere();
+}
+
+void spawn_zombies ()
+{      
+    float numzoms;
+    entity e;
+    
+    print("Them zombies be spawnin'!\n");
+
+       numzoms = autocvar_g_za_monster_count;
+
+       while(numzoms > 0)
+       {
+        e = spawn();
+               e.think = zombie_spawn_somewhere;
+        e.nextthink = time;
+
+               numzoms -= 1;
+       }
+       
+       if(self)
+        remove(self);
+}
+
+void za_init ()
+{
+    entity e;
+    
+    e = spawn();
+       e.think = spawn_zombies;
+       e.nextthink = time + 3;
+}
+
+MUTATOR_HOOKFUNCTION(Zombies_BuildMutatorsString)
+{
+       ret_string = strcat(ret_string, ":Zombies");
+       return 0;
+}
+
+MUTATOR_HOOKFUNCTION(Zombies_BuildMutatorsPrettyString)
+{
+       ret_string = strcat(ret_string, ", Zombies");
+       return 0;
+}
+
+MUTATOR_DEFINITION(mutator_zombie_apocalypse)
+{
+       MUTATOR_HOOK(BuildMutatorsString, Zombies_BuildMutatorsString, CBC_ORDER_ANY);
+       MUTATOR_HOOK(BuildMutatorsPrettyString, Zombies_BuildMutatorsPrettyString, CBC_ORDER_ANY);
+    
+    MUTATOR_ONADD
+    {
+        za_init();
+    }
+
+       return 0;
+}
index f8240dcc6888119284fa41d5ad584267689c6c43..ac4ef47ed3bc74c009b3b9d79e1433975102c3c7 100644 (file)
@@ -19,4 +19,3 @@
 #include "w_rifle.qc"
 #include "w_fireball.qc"
 #include "w_seeker.qc"
-#include "w_incubator.qc"