/*====================================================================== MONSTER FUNCTIONS ======================================================================*/ float MONAI_ZOMBIEFLR = 1; // start on floor float MONAI_ZOMBIEUPB = 2; // zombie painb - 28 frames (9 = on ground) float MONAI_ZOMBIEUPD = 4; // knight paind - 35 frames (12 = on ground) float MONAI_ZOMBIEUPE = 8; // knight + zombie paine - 30 frames (12 = on ground) float MONAI_ZOMBGRDTIMER = 2; // Zombie onground timer (between checking) float MONAI_ZOMBGRDBLOCK = 300; // 0.1 * 300 = 30s float MONAI_ZOMBIELOW_DAM = 9; // Pain threshold for animations float MONAI_ZOMBIEHGH_DAM = 28; // Has to be higher than SG (7x4) void(float frameno) zombie_onground; void(float frameno) zombiek_onground; //---------------------------------------------------------------------- // Pre-defined bounding boxes for monsters // Use 'bboxtype' to specify one of these //---------------------------------------------------------------------- float BBOX_TINY = 1; // -16 -16 -24, 16 16 16 float BBOX_SHORT = 4; // -16 -16 -24, 16 16 32 float BBOX_TALL = 5; // -16 -16 -24, 16 16 40 float BBOX_WIDE = 7; // -24 -24 -24, 24 24 40 float BBOX_GIANT = 8; // -24 -24 -24, 24 24 64 float BBOX_MASSIVE = 10; // -32 -32 -24, 32 32 64 float BBOX_GOLEM = 15; // -24 -24 -24, 24 24 72 float BBOX_DOG = 20; // -20 -20 -24, 20 20 16 float BBOX_FISH = 25; // -16 -16 -24, 16 16 24 float BBOX_FISHS = 26; // -12 -12 -16, 12 12 16 float BBOX_EEL = 27; // -16 -16 -16, 16 16 16 float BBOX_HYDRA = 30; // -20 -20 -16, 20 20 16 float BBOX_CUSTOM = 99; // Custom size set already /*====================================================================== monster_use - trigger/make angry ======================================================================*/ void() monster_use = { // Busy with another enemy or dead? if (self.enemy) return; if (self.health < 1) return; // Spiders/Vorelings can be setup on the ceilings // need to trigger/drop instead them before anything else if (self.classtype == CT_MONSPIDER && self.spawnflags & MON_SPIDER_CEILING || self.classtype == CT_MONSWAMPLING && self.spawnflags & MON_SWAMPLING_CEILING || self.classtype == CT_MONVORELING && self.spawnflags & MON_VORELING_CEILING) { // Start with activator as enemy self.enemy = activator; // Work through exceptions if (activator.classtype != CT_PLAYER) self.enemy = world; if (activator.items & IT_INVISIBILITY) self.enemy = world; if (activator.flags & FL_NOTARGET) self.enemy = world; // Drop from ceiling self.th_run(); return; } // Player exceptions if (activator.classtype != CT_PLAYER) return; if (activator.items & IT_INVISIBILITY) return; if (activator.flags & FL_NOTARGET) return; // Monster angry! Hunt enemy!?! self.enemy = activator; // If wakeup trigger setup do not wait, special animation // This is really designed to go with a breakable trigger if (self.wakeuptrigger && self.th_wakeup) { self.wakeuptrigger = FALSE; // Trigger no longer needed, reset FoundHuntTarget(TRUE); // Setup goals and warn other monsters self.th_wakeup(); } else { // delay reaction (wait one frame) so if the monster // has been teleported the sound is heard correctly self.nextthink = time + 0.1; self.think = FoundTarget; } }; /*====================================================================== liquid_check for some reason monsters never take damage from liquids This functions checks for liquid content and applies a modifier damage (they die too slow otherwise) * If a monster is in liquid there is often no way out! * Moves the content check to the monsters feet (shallow liquid) ======================================================================*/ void() monster_liquid_check = { local float monster_dmg; local vector monster_feet; // This function is for monsters only (high damage) if (self.no_liquiddmg) return; if (query_configflag(SVR_LIQDAM)) return; if ( !(self.flags & FL_MONSTER)) return; if (self.health < 1) return; if (self.liquidcheck > time) return; // Reset liquid damage timer self.liquidcheck = time + LIQUID_TIMER; // Check for liquid at monsters feet (bottom of bounding box) monster_feet = self.origin; monster_feet_z = monster_feet_z + self.mins_z; self.liquidbase = pointcontents(monster_feet); // Setup standard damage for liquid types if (self.liquidbase == CONTENT_SLIME) monster_dmg = SLIME_DAMAGE * MON_MULTIPLIER; else if (self.liquidbase == CONTENT_LAVA) monster_dmg = LAVA_DAMAGE * MON_MULTIPLIER; else return; // Gib monster if about to die if (self.health < monster_dmg + 5) { monster_dmg = self.health + 5; self.gibondeath = 1; } // Liquid damage self.pain_finished = 0; // Always pain T_Damage (self, world, world, monster_dmg, DAMARMOR); }; /*====================================================================== monster_idle_sound - use one routine so more stuff can be added easily - easier to add any exceptions this way - zombies have their own unique idle routine - tarby added using sight sound ======================================================================*/ void() monster_idle_sound = { // No monsters, dead and timer not reset? if ( !(self.flags & FL_MONSTER) ) return; if (self.health < 1) return; if (self.idletimer > time) return; if (self.spawnflags & MON_SPAWN_NOIDLE) return; self.idletimer = time + 5 + (random() * 3); if (!self.idlemoreoften && random() > MON_IDLE_SOUND) return; // Is the monster active in combat (special idle sound) if (self.enemy && self.idle_soundcom != "") { if (self.idle_soundcom2 != "") { if (random() < 0.5) sound (self, CHAN_VOICE, self.idle_soundcom, 1, ATTN_NORM); else sound (self, CHAN_VOICE, self.idle_soundcom2, 1, ATTN_NORM); } else sound (self, CHAN_VOICE, self.idle_soundcom, 1, ATTN_NORM); } else { // setup each monster with unique idle sounds (easier and quicker) if (self.idle_sound2 != "") { if (random() < 0.5) sound (self, CHAN_VOICE, self.idle_sound, 1, ATTN_NORM); else sound (self, CHAN_VOICE, self.idle_sound2, 1, ATTN_NORM); } else sound (self, CHAN_VOICE, self.idle_sound, 1, ATTN_NORM); } }; /*====================================================================== monster_sightsound - Switched sound channel to CHAN_BODY so death always stops it - Randomnly get sound file cut off using voice channel ======================================================================*/ void() monster_sightsound = { local float rsnd; // No monsters, dead and timer not reset? if ( !(self.flags & FL_MONSTER) ) return; if (self.health < 1) return; if (!self.sight_sound) return; if (self.spawnflags & MON_SPAWN_NOSIGHT) return; if (intermission_running > 0) return; // intermission or finale // Only do a sight sound when necessary, otherwise it overlaps and gets messy if (self.sight_timeout < time) { self.sight_timeout = time + MON_SIGHTSOUND; // Use pre-defined reset values rsnd = random(); // The id enforcer has four sight sounds, which is a bit excessive // Check through sight strings to find out quantity if (self.sight_count == 2) { if (rsnd < 0.5) sound (self, CHAN_BODY, self.sight_sound, 1, ATTN_NORM); else sound (self, CHAN_BODY, self.sight_sound2, 1, ATTN_NORM); } else if (self.sight_count == 3) { if (rsnd < 0.3) sound (self, CHAN_BODY, self.sight_sound, 1, ATTN_NORM); else if (rsnd < 0.6) sound (self, CHAN_BODY, self.sight_sound2, 1, ATTN_NORM); else sound (self, CHAN_BODY, self.sight_sound3, 1, ATTN_NORM); } else if (self.sight_count == 4) { if (rsnd < 0.25) sound (self, CHAN_BODY, self.sight_sound, 1, ATTN_NORM); else if (rsnd < 0.5) sound (self, CHAN_BODY, self.sight_sound2, 1, ATTN_NORM); else if (rsnd < 0.75) sound (self, CHAN_BODY, self.sight_sound3, 1, ATTN_NORM); else sound (self, CHAN_BODY, self.sight_sound4, 1, ATTN_NORM); } // setup each monster with unique sight sounds (easier and quicker) else sound (self, CHAN_BODY, self.sight_sound, 1, ATTN_NORM); } }; /*====================================================================== monster_footstep This function will play a footstep sound * Types : Slow, Drag, Light, Medium, Heavy, Large * called from animation blocks to sync with sound ======================================================================*/ void(float altfoot) monster_footstep = { local float footstep, footstepnext, footvol; local string footstepwav; if (query_configflag(SVR_FOOTSTEP)) return; // Default(0) = ON if (self.health < 1) return; if (self.movetype == MOVETYPE_NOCLIP) return; if (self.watertype < CONTENT_EMPTY) return; if (self.steptype == FS_FLYING) return; // Wide volume range for player if (self.flags & FL_CLIENT) footvol = 0.2 + random()*0.6; // feetsteps need to be louder during combat else if (self.enemy) footvol = 1; // Dogs have too many legs making noise, reduce volume else if (self.classtype == CT_MONDOG) footvol = 0.2 + random()*0.6; // Slight random volume level out of combat else footvol = 0.7 + random()*0.3; // Decide which foot sound to play, long live clubfoot! if (altfoot) { // Switch to ALTernative foot sound footstep = self.altsteptype; if (self.altsteplast < 1) self.altsteplast = rint(1 + random()*4); // Increase footstep sound index by one footstepnext = rint(self.altsteplast + 1); // Every loop around, randomly reset if (footstepnext > 5) footstepnext = rint(1 + random()*4); self.altsteplast = footstepnext; } else { // Setup last footstep sound footstep = self.steptype; if (self.steplast < 1) self.steplast = rint(1 + random()*4); // Player is randomly selected footsteps if (self.flags & FL_CLIENT) { // Quake random function is not really super random // Probably a bad seed starting point for function // Use 3 randoms to create something more random! footstepnext = rint(self.steplast + random() + random() + random()); if (footstepnext > 5) footstepnext = footstepnext - 5; } else { // Increase footstep sound index by one footstepnext = rint(self.steplast + 1); // Every loop around, randomly reset if (footstepnext > 5) footstepnext = rint(1 + random()*4); } // update last footstep index self.steplast = footstepnext; } // Cycle through all footstep types and work out correct sound file // All footstep types reduced down to 5 possible choices (speed things up) // Could store sound files with entity and cycle round quicker, req more memory // re-checking the sound file every footstep is costly on time // Luckly most footstep sounds are several frames apart (too noisy as well) if (footstep == FS_TYPELIGHT) { // Light heal/ paw sound if (footstepnext < 2) footstepwav = SOUND_FS_LIGHT1; else if (footstepnext < 3) footstepwav = SOUND_FS_LIGHT2; else if (footstepnext < 4) footstepwav = SOUND_FS_LIGHT3; else if (footstepnext < 5) footstepwav = SOUND_FS_LIGHT4; else footstepwav = SOUND_FS_LIGHT5; } else if (footstep == FS_TYPEMEDIUM) { // Average foot/boot sound if (footstepnext < 2) footstepwav = SOUND_FS_MEDIUM1; else if (footstepnext < 3) footstepwav = SOUND_FS_MEDIUM2; else if (footstepnext < 4) footstepwav = SOUND_FS_MEDIUM3; else if (footstepnext < 5) footstepwav = SOUND_FS_MEDIUM4; else footstepwav = SOUND_FS_MEDIUM5; } else if (footstep == FS_TYPEHEAVY) { // Heavy foot with slight echo if (footstepnext < 2) footstepwav = SOUND_FS_HEAVY1; else if (footstepnext < 3) footstepwav = SOUND_FS_HEAVY2; else if (footstepnext < 4) footstepwav = SOUND_FS_HEAVY3; else if (footstepnext < 5) footstepwav = SOUND_FS_HEAVY4; else footstepwav = SOUND_FS_HEAVY5; } else if (footstep == FS_TYPELARGE) { // Large foot with large echo if (footstepnext < 2) footstepwav = SOUND_FS_LARGE1; else if (footstepnext < 3) footstepwav = SOUND_FS_LARGE2; else if (footstepnext < 4) footstepwav = SOUND_FS_LARGE3; else if (footstepnext < 5) footstepwav = SOUND_FS_LARGE4; else footstepwav = SOUND_FS_LARGE5; } else if (footstep == FS_TYPEGIANT) { // Giant foot with long echo if (footstepnext < 2) footstepwav = SOUND_FS_GIANT1; else if (footstepnext < 3) footstepwav = SOUND_FS_GIANT2; else if (footstepnext < 4) footstepwav = SOUND_FS_GIANT3; else if (footstepnext < 5) footstepwav = SOUND_FS_GIANT4; else footstepwav = SOUND_FS_GIANT5; } else if (footstep == FS_TYPECUSTOM) { // Custom feet sounds (usually boss type creatures) if (footstepnext < 2) footstepwav = self.stepc1; else if (footstepnext < 3) footstepwav = self.stepc2; else if (footstepnext < 4) footstepwav = self.stepc3; else if (footstepnext < 5) footstepwav = self.stepc4; else footstepwav = self.stepc5; } else if (footstep == FS_TYPEDRAG) { // Small scraping foot on ground if (footstepnext < 2) footstepwav = SOUND_FS_DRAG1; else if (footstepnext < 3) footstepwav = SOUND_FS_DRAG2; else if (footstepnext < 4) footstepwav = SOUND_FS_DRAG3; else if (footstepnext < 5) footstepwav = SOUND_FS_DRAG4; else footstepwav = SOUND_FS_DRAG5; } // FS_TYPESLOW (default) else { // Souless shoe foot sound if (footstepnext < 2) footstepwav = SOUND_FS_SLOW1; else if (footstepnext < 3) footstepwav = SOUND_FS_SLOW2; else if (footstepnext < 4) footstepwav = SOUND_FS_SLOW3; else if (footstepnext < 5) footstepwav = SOUND_FS_SLOW4; else footstepwav = SOUND_FS_SLOW5; } // Play the sound (large feet need to be heard further away) if (footstep == FS_TYPELARGE) sound (self, CHAN_FEET, footstepwav, footvol, ATTN_FEETL); else sound (self, CHAN_FEET, footstepwav, footvol, ATTN_FEET); }; //====================================================================== // PRE/POST CHECK conditions for monster death //====================================================================== void() monster_death_precheck = { if (self.flags & FL_MONSTER) { // Check for minion monster and update parent counters if (self.owner.minion_active) update_minioncount(self.owner, -1); self.deadflag = DEAD_DEAD; // Its dead jim! self.effects = 0; // Remove effects on death self.think = SUB_Null; // No more thinking/animation self.nextthink = -1; // Never fire think monster_check_gib(); // Check for gib state } }; //---------------------------------------------------------------------- void() monster_fade = { // Make sure model/entity is removed self.height = 0; self.think = model_fade; self.nextthink = time + 0.1; self.ltime = self.nextthink; }; //---------------------------------------------------------------------- void() monster_death_postcheck = { local float bodytimer; self.blockudeath = TRUE; // Body is dead, no human death noise if (!self.gibbed) self.bodyonflr = MON_ONFLR; // Allow for the dead body/head to touch triggers (void/hurt) setorigin(self, self.origin); self.solid = SOLID_TRIGGER; self.touch = SUB_Null; // Check for global or individual body fading mechanic if (self.bodyfadeaway > 0 || map_bodyfadeaway > 0) { if (map_bodyfadeaway > 0) bodytimer = map_bodyfadeaway; else bodytimer = self.bodyfadeaway; self.pain_finished = time + 10 + random() * bodytimer; } // Body sticks around else self.pain_finished = time + LARGE_TIMER; }; //---------------------------------------------------------------------- void() monster_deadbody_check = { // Remove/Fade out body if timer is finished if (self.pain_finished < time) { monster_fade(); return; } // Exit condition, body no longer has any interaction if (self.deadflag == DEAD_FINISHED) return; // Check for deadflag first, if touched something this will be changed if (self.deadflag == DEAD_REMOVE || self.deadflag == DEAD_EXPLODE && self.gibbed == FALSE) { // make sure touch,solid and body axe interaction are off self.touch = SUB_Null; self.solid = SOLID_NOT; self.bodyonflr = ""; self.gibhealth = TRUE; // Only two options available, gib explode or remove if (self.deadflag == DEAD_EXPLODE) self.think = monster_ThrowGib; // Replace animation frame group think function else self.think = SUB_Remove; self.nextthink = time + 0.1; // Prevent this function from running again self.deadflag = DEAD_FINISHED; } else { // Check global map variable first (default = off) // Check floor below dead body (global function) if (map_bodyflrcheck == TRUE) ent_floorcheck(self, FLOOR_TRACE_MONSTER); // Keep checking for dead body/head conditions self.think = monster_deadbody_check; self.nextthink = time + 0.1; } }; /*====================================================================== monster_death_use - When a mosnter dies, it fires all of its targets with the current enemy as activator. ======================================================================*/ void() monster_death_use = { // fall to ground or stop swimming if (self.flags & FL_FLY) self.flags = self.flags - FL_FLY; if (self.flags & FL_SWIM) self.flags = self.flags - FL_SWIM; // Always use a deathtarget if one defined if (self.deathtarget != "") { // Validate deathtarget exists before firing it self.movelast = find(world, targetname, self.deathtarget); // Deathtarget valid, switch around and fire targets if (self.movelast) self.target = self.deathtarget; } // Is there no target defined? if (self.target == "") return; activator = self.enemy; SUB_UseTargets (); }; /*====================================================================== monster_paincheck - Tests all pain conditions and returns what to do * 0 = Nothing * 1 = Sound + Animation * 2 = Sound + Long Animation * 3 = Sound ONLY ======================================================================*/ void(entity attacker, float damage) monster_pain_check = { self.pain_check = 0; // Reset pain check // already dying, don't go into pain frame if (self.health < 1) self.pain_check = 0; // The new axe forces monsters into long pain animations (if they exist) else if (attacker.moditems & IT_UPGRADE_AXE && self.axhitme > 0 && self.pain_longanim) self.pain_check = 2; // always go into pain frame if it has been a while (first hit = pain) else if (time - self.pain_finstate > PAIN_ALWAY) self.pain_check = 1; // Dangerous liquids should kill the monster and keeping them in // constant pain animations which makes them look better than doing nothing! else if (self.liquidbase == CONTENT_SLIME || self.liquidbase == CONTENT_LAVA) self.pain_check = 1; // If being attacked by a world object (shooter, electricity, bmodel) in pain else if (attacker == world) self.pain_check = 1; // Random chance to flinch and not ignore the pain (play sound only) else if (random()* self.pain_flinch > damage ) self.pain_check = 3; // DEFAULT : pain animation + sound (last condition) else self.pain_check = 1; }; /*====================================================================== monster_targets (second part of setup function) - Checks for any targets to get angry at or stand/walk around ======================================================================*/ void() monster_targets = { // Reset everything first self.enemy = self.goalentity = self.movetarget = world; //---------------------------------------------------------------------- // * Monsters can spawn angry at the player/activator // * the target key can point to a path_corner // * If the target key points at another monster they will infight //---------------------------------------------------------------------- if (self.spawnflags & MON_SPAWN_ANGRY || self.angrytarget) { // Check if the activator is a player? if not find a player if (activator.flags & FL_CLIENT) self.enemy = activator; else self.enemy = checkclient (); // Double check enemy is player and has notarget or wearing RoS? if (self.enemy.flags & FL_CLIENT) { if (self.enemy.flags & FL_NOTARGET) self.enemy = world; else if (self.enemy.items & IT_INVISIBILITY) self.enemy = world; } // If the activator not a player, reset to world // Cannot get angry at triggers, use angrytarget for infighting else self.enemy = world; // If an alternative angry target defined, find it and attack! if (self.angrytarget) { self.oldenemy = find(world, targetname, self.angrytarget); // Is the attack target a monster and spawned/active (take damage) if (self.oldenemy.flags & FL_MONSTER && self.oldenemy.takedamage != DAMAGE_NO) self.enemy = self.oldenemy; } // Check the enemy is a player/monster and alive if (self.enemy.flags & (FL_CLIENT | FL_MONSTER) && self.enemy.health > 0) { self.nextthink = time + 0.1; self.think = FoundTarget; monster_sightsound (); // Wake up sound return; } } else { // If the monster has a target check if path_corner or something to attack? // Do nothing with patrol paths if spawning angry with the player if (self.target != "") { self.movetarget = find(world, targetname, self.target); // Cannot chase a target if setup as a turret if (self.movetarget && self.movespeed != -1) { if (self.movetarget.classtype == CT_PATHCORNER) { self.goalentity = self.movetarget; self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin); self.nextthink = time + 0.1 + random()*0.5; self.think = self.th_walk; return; } } } } // no angrytarget, enemy or path corner, stand around waiting self.nextthink = time + 0.1 + random()*0.5; self.think = self.th_stand; // Stand around }; /*====================================================================== monster_setup - Setup monster ready for action ======================================================================*/ void() monster_spawn = { //---------------------------------------------------------------------- // Check for Axe / Shotgun / LG upgrade monster spawn exceptions? if (self.upgrade_axe || self.upgrade_ssg || self.upgrade_lg) { // Has ANY player (server test not individual) // picked up the relevant upgrade weapons? if (self.upgrade_axe && !query_configflag(SVR_UPDAXE) ) return; if (self.upgrade_ssg && !query_configflag(SVR_UPDSSG) ) return; if (self.upgrade_lg && !query_configflag(SVR_UPDLG) ) return; // Update monster count (not added to monster count until spawned) total_monsters = total_monsters + 1; update_hud_totals(HUD_MONSTERS); } //---------------------------------------------------------------------- // Check if the monster can spawn without telefragging something if (self.spawnnotelefrag > 0) { // Switch on world interaction self.solid = SOLID_SLIDEBOX; self.movetype = MOVETYPE_STEP; // restore model/bbox to get world collision setmodel(self, self.mdl); setsize (self, self.bbmins, self.bbmaxs); // reset origin, just in case been moved since last setorigin(self, self.oldorigin); // Make sure movement flag types are correct if (self.classmove == MON_MOVEFLY ) self.flags = self.flags | FL_FLY; else if (self.classmove == MON_MOVESWIM ) self.flags = self.flags | FL_SWIM; // Can the monster walk without collision? if (!walkmove (0, 0)) { dprint("BLOCKED "); dprint(self.classname); dprint(" "); // Switch off monster and wait again self.solid = SOLID_NOT; setmodel(self,""); setsize(self, VEC_ORIGIN, VEC_ORIGIN); self.think = monster_spawn; self.nextthink = time + 0.1 + random()*0.5; return; } else { // make sure monster is not displayed yet // might have a nosight check to do self.solid = SOLID_NOT; setmodel(self,""); setsize(self, VEC_ORIGIN, VEC_ORIGIN); } } //---------------------------------------------------------------------- // Check for spawnnosight conditions if (self.spawnnosight > 0) { // Setup maximum time limit for spawning regardless if (self.attack_finished == FALSE) { // Default is 30s (check for override condition first) if (self.spawnnosighttime < 1) self.spawnnosighttime = 30; self.attack_finished = time + self.spawnnosighttime; } // Reset sight condition first and find client in PVS self.lefty = FALSE; self.oldenemy = checkclient(); // Did the client PVS check return an entity? if (self.oldenemy) { // is the client a player? if (self.oldenemy.flags & FL_CLIENT) { // Check if the player can be seen? enemy_vis = visible(self.oldenemy); if (enemy_vis) self.lefty = TRUE; // Check for an insight spawn distance if (self.spawnnosight > 1) { // Find distance (3d vector distance) self.enemydist = range_distance(self.oldenemy, FALSE); // check for player being too close for spawn if (self.enemydist > self.spawnnosight) self.lefty = FALSE; } } // Checks for notarget/inv player conditions // disabled - not really necessary as a spawn condition //if (self.oldenemy.flags & FL_NOTARGET) self.lefty = FALSE; //if (self.oldenemy.items & IT_INVISIBILITY) self.lefty = FALSE; } // Can the player see the spawn location? wait instead if (self.lefty == TRUE && self.attack_finished > time) { self.think = monster_spawn; self.nextthink = time + 0.1; return; } } //---------------------------------------------------------------------- // Time to finally spawn the monster!?! All conditions met! if (self.think1) self.use = self.think1; // different use function? else self.use = monster_use; // default trigger event self.solid = SOLID_SLIDEBOX; // Standard monster movement self.movetype = MOVETYPE_STEP; // Standard monster movement setmodel(self, self.mdl); // Setup model self.skin = self.skin_override; // Restore any skins self.frame = self.frame_override; // Restore any frames setsize (self, self.bbmins, self.bbmaxs); // Restore BB size // Check for delay spawn monster count if (self.delaymonstercount) { // Reset both monster count conditions self.nomonstercount = self.delaymonstercount = 0; total_monsters = total_monsters + 1; update_hud_totals(HUD_MONSTERS); } // Restore any effect flags settings (various dlight glows) if (self.savedeffects > 0) self.effects = self.savedeffects; // Should grenades bounce off the body? if (self.bouncegrenade) self.takedamage = DAMAGE_YES; else self.takedamage = DAMAGE_AIM; // Can receive damage self.velocity = '0 0 0'; // Make sure stationary self.deadflag = DEAD_NO; // used to stop death re-triggering self.liquidbase = self.liquidcheck = 0; // Used for liquid content damage self.dmgcombined = self.dmgtimeframe = 0; // combined damage over 0.1s if (!self.pain_flinch) self.pain_flinch = self.health; if (!self.switchoverride) self.switchoverride = 1; // Double check that all ammo resistance are within range Resist_CheckRange(self); // Check for tether system (special target field) setup_tethersystem(); // Long time before first idle sound (except scorpions!) if (self.idletimer < 1) { if (self.classtype == CT_MONSCORPION) self.idletimer = time + 0.1 + random(); else self.idletimer = time + 4 + (random() * 4); } // Reset all enemy tracking entities if (!self.enemy) self.enemy = self.movetarget = self.goalentity = world; self.pausetime = LARGE_TIMER; // Setup pain tolerence level based on current skill level if (!self.pain_timeout) self.pain_timeout = 1; // Default if (skill == SKILL_HARD) self.pain_timeout = self.pain_timeout + 1; if (skill == SKILL_NIGHTMARE) self.pain_timeout = self.pain_timeout + 3; //---------------------------------------------------------------------- // Setup different skin options //---------------------------------------------------------------------- if (self.exactskin > 0) self.skin = rint(self.exactskin); else if (self.randomskin > 1) self.skin = rint(random()*(self.randomskin-1)); if (self.skin < 0) self.skin = 0; // Double check no negatives //---------------------------------------------------------------------- self.ideal_yaw = self.angles * '0 1 0'; if (self.classmove == MON_MOVEWALK ) { // Walking Monsters if (!self.yaw_speed) self.yaw_speed = 20; } else if (self.classmove == MON_MOVEFLY ) { // Flying Monsters if (!self.yaw_speed) self.yaw_speed = 15; self.flags = self.flags | FL_FLY; } else if (self.classmove == MON_MOVESWIM ) { // Swimming Monsters if (!self.yaw_speed) self.yaw_speed = 10; self.flags = self.flags | FL_SWIM; } // Not all bounding boxes extend up very high, make sure the view_ofs // is relevant to where the top of the bounding box is if (self.view_ofs_z == 0) { if (self.maxs_z <= MON_VIEWOFS) self.view_ofs_z = self.maxs_z*0.5; else self.view_ofs_z = MON_VIEWOFS; } //---------------------------------------------------------------------- // Is the monster a zombie (type) lying on the floor? (starting pose) // Angles key used for specific facing direction, 0 = random setup // This function check is before any droptofloor or content checks // It will allow onfloor monsters to exist in tight spaces, shallow graves //---------------------------------------------------------------------- if (self.spawnflags & MON_ONFLOOR && self.classgroup == CG_ZOMBIE) { if (self.classtype == CT_MONZOMBIEK) zombiek_onground(MONAI_ZOMBIEFLR); else zombie_onground(MONAI_ZOMBIEFLR); return; } //---------------------------------------------------------------------- // Special minions (start small and grow fast) // CT_MINIONSPIDER, CT_MINIONVORELING, CT_MINIONGARGOYLE //---------------------------------------------------------------------- if (self.minion_active) { self.angles = vectoangles(self.enemy.origin - self.origin); self.angles_x = self.angles_z = 0; update_hud_totals(HUD_MONSTERS); self.pain_finished = time + 1.2; self.th_stand(); return; } //---------------------------------------------------------------------- // Perched Gargoyles/Gaunt have special idle animation (sitting) // and need to fly up before resuming any normal behaviour // (most think functions are intercepted) //---------------------------------------------------------------------- if (self.classtype == CT_MONGARGOYLE && self.spawnflags & MON_GARGOYLE_PERCH || self.classtype == CT_MONGAUNT && self.spawnflags & MON_GAUNT_PERCH ) { self.flags = self.flags | FL_FLY; // Stop any ground checks self.th_stand(); return; } //---------------------------------------------------------------------- // Ceiling critters have special idle animation (rotated) // and need to let go of the ceiling before resuming any // normal behaviour (most think functions are intercepted) //---------------------------------------------------------------------- if (self.classtype == CT_MONSPIDER && self.spawnflags & MON_SPIDER_CEILING || self.classtype == CT_MONSWAMPLING && self.spawnflags & MON_SWAMPLING_CEILING || self.classtype == CT_MONVORELING && self.spawnflags & MON_VORELING_CEILING) { // Work out where the ceiling is (traceline upwards) traceline (self.origin, self.origin+'0 0 4096', TRUE, self); // Check for empty content before moving if (pointcontents(trace_endpos) == CONTENT_EMPTY) { self.flags = self.flags | FL_FLY; // Stop any ground checks self.classmove = MON_MOVEFLY; // Avoid ground check function self.origin = trace_endpos; // Move critter to ceiling setorigin(self, self.origin); // If the view_ofs is not underneath the critter, no sight // functions will work (checkclient for example) self.view_ofs = '0 0 -24'; self.yaw_speed = 20; // Ground yaw speed setsize(self, '-16 -16 -24', '16 16 0'); } else { dprint("\b[MONSTER]\b Trying to place on ceiling, no space!\n"); spawn_marker(self.origin, SPNMARK_YELLOW); remove(self); return; } } //---------------------------------------------------------------------- // Check to see if monster (walking) are stuck in geometry if (self.classmove == MON_MOVEWALK) { self.origin_z = self.origin_z + 1; droptofloor(); if (!walkmove(0,0)) { // Tempoaraily use lip variable for stuck condition self.lip = TRUE; // Some monster placement in the original ID maps NEED original Bounding Boxes // this is something that cannot be easily fixed, so go back to original BB // and test the monster placement again if (CheckZeroVector(self.idmins) == FALSE) { self.bbmins = self.idmins; self.bbmaxs = self.idmaxs; setsize (self, self.bbmins, self.bbmaxs); // Restore ID BB size self.origin_z = self.origin_z + 1; droptofloor(); // re-test monster movement using ID Bounding Box if (walkmove(0,0)) self.lip = FALSE; } // It seems that some mappers want to spawn stuff in mid air // and let it drop naturally over time (>1 frame) // check for empty point content at origin as final test if (pointcontents(self.origin) == CONTENT_EMPTY) self.lip = FALSE; // Is the monster stuck with new/old bounding box? // Stuck monsters contribute towards level monster totals if (self.lip) { // If the monster is stuck and delay spawned, gib instead if (self.spawnflags & MON_SPAWN_DELAY) { self.health = self.gibhealth; Killed(self, self); } else { // this condition should be a map spawn event only dprint ("\b[STUCK]\b "); dprint (self.classname); dprint (" at "); dprint (vtos(self.origin)); dprint ("\n"); spawn_marker(self.origin, SPNMARK_YELLOW); remove(self); } return; } } } //---------------------------------------------------------------------- // Telefrag (kill) anything at monster position // Phased monsters cannot telefrag or produce spawn effects if (!self.bodyphased) { // New spawn condition via entity key nospawndamage // This is to prevent excessive damage to breakables // Monsters can spawn in very wierd situations (inside things) // if spawning damage is disabled (spawn_tdeath) if (self.nospawndamage == 0) spawn_tdeath(self.origin, self); // 50K damage! // Don't show spawning effect if delay spawn + nogfx if (self.spawnflags & MON_SPAWN_DELAY && !(self.spawnflags & MON_SPAWN_NOGFX)) spawn_tfog(self.origin); } //---------------------------------------------------------------------- // Hell Knights can be setup to point at a target and fire lightning // This uses the magicB attack by ID software that was never used // When the Hell Knight recieves damage from the player or triggered // will revert back to normal walking/talking/fighting monster! // ** Has unique use/think function //---------------------------------------------------------------------- if (self.classtype == CT_MONHELLK && self.spawnflags & MON_POINT_KNIGHT) { self.nextthink = time + 0.1 + random()*0.5; self.think = self.th_stand; // statue function return; } //---------------------------------------------------------------------- // Knights, Hell Knights, Golems and Gargoyles can start as statues // They are frozen in an selected pose and wake up on trigger // Have different skin, high pain resistance and stone gibs! // Added double check for statue spawnflag, just in case // ** Has unique use/think function //---------------------------------------------------------------------- if (self.spawnstatue && self.spawnflags & MON_STATUE) { self.takedamage = DAMAGE_NO; // No damage till wakeup self.nextthink = time + 0.1 + random()*0.5; self.think = self.th_stand; // statue function return; } //---------------------------------------------------------------------- // Start in special standby mode with a blue halo glowing shield // Will wait for a trigger before reverted back to normal gameplay // ** Has unique use/think function //---------------------------------------------------------------------- if (self.classtype == CT_MONSEEKER && self.spawnflags & MON_SEEK_SHIELD) { self.takedamage = DAMAGE_NO; // No damage till wakeup self.nextthink = time + 0.1 + random()*0.5; self.think = self.th_stand; // statue function return; } //---------------------------------------------------------------------- // Once the skull wizard is setup on the ground, phase out body //---------------------------------------------------------------------- if (self.classtype == CT_MONSKULLW && self.bodyphased == MONAI_SKULLWINVIS) { self.takedamage = DAMAGE_NO; self.solid = SOLID_NOT; setmodel(self,""); } //---------------------------------------------------------------------- // No more spawn exception check for targets monster_targets(); }; //---------------------------------------------------------------------- // Setup bounding box for monster (based on types) //---------------------------------------------------------------------- void() monster_bbox = { if (self.bboxtype == BBOX_CUSTOM) return; else if (self.bboxtype == BBOX_TINY) // Lost Soul, Scorpion, Spider, Voreling { self.bbmins = '-16 -16 -24'; self.bbmaxs = '16 16 16'; } else if (self.bboxtype == BBOX_SHORT) // Player, Death Guards, Knights, Crossbow Knights, // Army, Army_Rocket, Army_Grenade, Army_Plasma, Jim // Zombie, Poison Zombie, Zombie Knight { self.bbmins = '-16 -16 -24'; self.bbmaxs = '16 16 32'; } else if (self.bboxtype == BBOX_TALL) // Hell Knight, Death Knight, Fury Knight, Sergeant // Enforcer, Defender, Eliminator, Pyro, Centurion // Skull Wizard, Wizard, Gargoyles, Tarbaby, Wraith { self.bbmins = '-16 -16 -24'; self.bbmaxs = '16 16 40'; } else if (self.bboxtype == BBOX_WIDE) // Demon, Ogre, Hunter Ogre, _Mace, _Hammer, Shalrath { self.bbmins = '-24 -24 -24'; self.bbmaxs = '24 24 40'; } else if (self.bboxtype == BBOX_GIANT) // Drole, Minotaur { self.bbmins = '-24 -24 -24'; self.bbmaxs = '24 24 64'; } else if (self.bboxtype == BBOX_MASSIVE) // Shambler, ID Ogre { self.bbmins = '-32 -32 -24'; self.bbmaxs = '32 32 64'; } // Some monsters are custom sizes else if (self.bboxtype == BBOX_DOG) { self.bbmins = '-20 -20 -24'; self.bbmaxs = '20 20 16'; } else if (self.bboxtype == BBOX_FISH) { self.bbmins = '-16 -16 -24'; self.bbmaxs = '16 16 24'; } else if (self.bboxtype == BBOX_FISHS) { self.bbmins = '-12 -12 -14'; self.bbmaxs = '12 12 14'; } else if (self.bboxtype == BBOX_EEL) { self.bbmins = '-16 -16 -16'; self.bbmaxs = '16 16 16'; } else if (self.bboxtype == BBOX_HYDRA) { self.bbmins = '-20 -20 -16'; self.bbmaxs = '20 20 16'; } else if (self.bboxtype == BBOX_GOLEM) { self.bbmins = '-28 -28 -24'; self.bbmaxs = '28 28 80'; } // default bounding box = TALL else { self.bbmins = '-16 -16 -24'; self.bbmaxs = '16 16 40'; } }; /*====================================================================== walkmonster_start - Main entry point for ALL monster routines ======================================================================*/ void() monster_start = { self.flags = FL_MONSTER; // Always reset this flag self.skin_override = self.skin; // Save for later self.frame_override = self.frame; // Check for spawning conditions (nightmare, coop) if (check_nightmare() == TRUE) return; if (check_coop() == TRUE) return; // Warning if effects flag is active before spawning if (self.effects) { dprint("\b[MONSTER]\b Effects flag active\n"); self.savedeffects = self.effects; } // Reset effects flag because some engines will show effects // This is especially obvious for delay spawned monsters self.effects = 0; self.oldorigin = self.origin; // Save origin self.max_health = self.health; // Save max health if (!self.gibhealth) self.gibhealth = 0 - self.health; // Default gib health if (self.turrethealth < 0 || self.turrethealth > 1) self.turrethealth = 0.5; // Check if jump function has been disabled? if (self.jump_flag < 0) self.jump_flag = LARGE_TIMER; // Default attack function and class group if (!self.th_checkattack) self.th_checkattack = CheckAttack; if (!self.classgroup) self.classgroup = CG_MONSTERS; // Highlight monsters with forced no_zaware entity key if (self.no_zaware && developer > 0 && !query_configflag(SVR_DEVHELPER)) spawn_marker(self.origin+'0 0 32', SPNMARK_WHITE); // Setup bounding box based on presets monster_bbox(); // Cannot have multiple upgrade restrictions on monsters remove_duplicate_upgrades(); // Cannot delay spawn a monster if nothing can trigger it!?! if (self.spawnflags & MON_SPAWN_DELAY && self.targetname == "") { dprint("\b[MONSTER]\b Cannot delay spawn without targetname!\n"); spawn_marker(self.origin, SPNMARK_YELLOW); remove(self); return; } // Check for Axe / Shotgun upgrade monster exceptions? // Don't add these kind of monster to the count until spawned if (self.upgrade_axe || self.upgrade_ssg || self.upgrade_lg) { if ( !(self.spawnflags & MON_SPAWN_DELAY) ) { dprint("\b[MONSTER]\b need spawn delay for axe/shotgun/lg\n"); spawn_marker(self.origin, SPNMARK_YELLOW); remove(self); return; } // make sure the monster has no nomonstercount exceptions if (self.nomonstercount) self.nomonstercount = 0; } else { // Allow mappers to spawn monsters that don't affect monster count // I know this can be a dangerous if used incorrectly, but good for statues // delaymonstercount = update count on spawn instead of death if (self.nomonstercount > 0 || self.delaymonstercount > 0) { if (developer > 1) { dprint("\b[MONSTER]\b ("); dprint(self.targetname); dprint(") - no monster count\n"); } } // Default state - update monster total // HUD update done with client setup else total_monsters = total_monsters + 1; } //---------------------------------------------------------------------- // Detect monster armour map hack if (self.armorvalue || self.armortype) { // This hack really should be stopped, no point upsetting mappers at this point // self.armorvalue = self.armortype = 0; dprint("\b[MAPHACKS]\b Using armor on monsters, use health key instead!\n"); } if (self.spawnflags & MON_SPAWN_DELAY) { setmodel(self, string_null); self.solid = SOLID_NOT; // No world interaction self.use = monster_spawn; // Wait for trigger if (developer > 0 && !query_configflag(SVR_DEVHELPER)) { self.movetype = MOVETYPE_NONE; self.solid = SOLID_NOT; setmodel(self, MODEL_BROKEN); if (self.nomonstercount == 1) self.skin = SPNMARK_GREEN; else self.skin = SPNMARK_BLUE; self.frame = 0; } } else { // Variable start delay on all monsters to reduce packet errors self.nextthink = time + 0.1 + random()*0.4; self.think = monster_spawn; } }; //====================================================================== // Map hacks are not supported in this MOD // There is no use in pretending otherwise! //====================================================================== void() walkmonster_start_go = { remove(self); return; }; void() flymonster_start_go = { remove(self); return; }; void() swimmonster_start_go = { remove(self); return; }; /*====================================================================== /*QUAKED monster_x (1 0 0) (x y z) (x y z) AMBUSH x x NOSIGHT NOIDLE NOGFX STARTOFF ANGRY Not_Easy Not_Normal Not_Hard Not_DM XXX, x health points. -------- KEYS -------- targetname : monster/trigger name target : Starting position or path_corner to walk towards (always define) target2 : Additional trigger function (need target to be defined as well) angrytarget : something (monster/player) to get angry at once spawned deathtarget : entity to trigger upon death (useful if target field already in use) health : Override default health settings exactskin : Override default skin selection of 0 (no error checking) upgrade_ssg : 1 = will only spawn if shotgun upgrade active on server upgrade_axe : 1 = will only spawn if axe upgrade active on server upgrade_lg : 1 = will only spawn if lightning gun upgrade active on server nomonstercount : will not be included in any monster count functionality infightextra : Damage multiplier for infighting damage pain_ignore : 1 = Ignore pain when hit by other monsters noinfighting : Will not react to any infighting (it can look stupid) no_liquiddmg : Blocks all liquid (slime/lava) damage checks no_zaware : All Z Aware projectiles will be disabled bboxtype : Change bbox 1=Tiny,4=Short,5=Tall,7=Wide,8=Giant,10=Massive gibondeath : 1 = always explode in a shower of gibs on death bodyfadeaway : Time (secs) before body/head will fade away (default=0) movespeed : -1 = no movement(turret), =0/1 free movement (default) turrethealth : = 0.0->1.0; % of HP when monster turret is released turrettarget : Target(s) to fire when turret % HP is released --- Knights --- frame: statue frame to be frozen in. (default 44) STATUE : Stone statue until triggered NOTFROZEN : Will start active (works with statue spawnflag) --- Hell Knight --- frame: statue frame to be frozen in. (default 73) STATUE : Stone statue until triggered NOTFROZEN : Will start active (works with statue spawnflag) --- Crossbow Knight --- SNIPER : no max range limitations for enemies (sniper mode) TRACKING : Enable tracking for the firing of bolts (really hard) --- Golem --- frame: statue frame to be frozen in. (default 48) STATUE : Stone statue until triggered --- Tarbaby --- death_dmg : Damage on Death (def=120) poisonous : 0=Jump attack (default, 1=Poison attack exactskin : 0-1=Blue, 2-3=Green1, 4-5=Green2, 6-7=Green3 --- Skull Wizards --- bodyphased : Spawn phased out and wait to see player bodystatic : Prevents skull wizard from teleporting -------- SPAWNFLAGS -------- AMBUSH : the monster will only wake up on seeing the player, not by another monster NOSIGHT : No sight sound NOIDLE : No idle sound NOGFX : No spawn effect or sound when triggered STARTOFF : Trigger Spawn ANGRY : Trigger Spawn angry at the player -------- NOTES -------- XXX, x health points. ======================================================================*/