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
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)"
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);
#endif
..float current_ammo;
-.float currentegg;
.float weapon_load[WEP_MAXCOUNT];
.float ammo_none; // used by the reloading system, must always be 0
--- /dev/null
+.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?
--- /dev/null
+// 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;
+}
--- /dev/null
+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;
+}
--- /dev/null
+// 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(); }
--- /dev/null
+// 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;
+ }
+}
--- /dev/null
+// 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;
+ }
+}
--- /dev/null
+// 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;
+ }
+}
--- /dev/null
+// 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(); }
--- /dev/null
+// 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;
+ }
+}
--- /dev/null
+// 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);
+}
--- /dev/null
+// 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(); }
--- /dev/null
+// 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");
+}
--- /dev/null
+// 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(); }
--- /dev/null
+// 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");
+}
--- /dev/null
+// 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;
+ }
+}
--- /dev/null
+// 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(); }
--- /dev/null
+// 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(); }
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+// 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
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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
--- /dev/null
+// 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;
+}
#include "w_rifle.qc"
#include "w_fireball.qc"
#include "w_seeker.qc"
-#include "w_incubator.qc"