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