/*====================================================================== AI (monster) FUNCTIONALITY Visibility checks - float(entity targ) visible - float(entity targ, vector s_offset, vector t_offset, float nomonsters) visxray - float(entity targ) visblocked - float (entity targ, vector s_ofset, vector t_ofset) visblocked_wide - float(entity targ) infront - float(vector org_source, vector org_targ) check_liquidblock Distance checks - float(entity targ) range - float(entity targ, float flat2d) range_distance - float (entity source, entity targ) rangeattack_check Angle checks - float(entity source, entity targ, float offset, float reverse) targangle - float(entity source, entity targ) targzone Hunt & Target Enemies - void() HuntTarget - void() FoundTarget - void(float wakeupothers) FoundHuntTarget - float() FindTarget AI animation movement (used mostly in monsters QC file) - void(float dist) ai_forward - void(float dist) ai_back - void(float dist) ai_pain - void(float dist) ai_painforward - void() ai_turn - float() FacingIdeal - void() ai_face - void() ai_resetangles - void(float dist) ai_charge - void(float dist) ai_chargenoturn - void() ai_charge_side - void() ai_charge_front AI ATTACK states - void() ai_run_melee - void() ai_run_missile - void() ai_run_jump - void(float dist) ai_run_slide - void(float dist) ai_run_sidestep - void(float dist) ai_run_backward AI interactions with Breakable System - float(entity source, entity targ, float ignorenoshoot) ai_foundbreakable - void(float brkdmg) ai_damagebreakable - void(float brkdmg) ai_jumpbreakable AI melee checks and damage - float(float attackdist) ai_checkmelee - void() ai_melee - void(float dmg_multiplier) ai_meleesmash - void() ai_melee_side ======================================================================*/ /*====================================================================== check_liquidblock - Checks pointcontents of source+target because traceline parms are not 100% reliable for flagging water/air content - One function check for all the different visibility functions - Designed to work with the new liquidblock entity flag ======================================================================*/ float(vector org_source, vector org_targ) check_liquidblock = { local float pc_source, pc_targ; // Is r_wateralpha set or monsters forced to check liquid surfaces? if (liquid_alpha == 1 || self.liquidblock) { // Check point contents first pc_source = pointcontents(org_source); pc_targ = pointcontents(org_targ); // Are point content different? (like water/air) if (pc_source != pc_targ) return TRUE; // Standard traceline check (not 100% reliable) if (trace_inopen && trace_inwater) return TRUE; } // No liquid check required return FALSE; }; /*====================================================================== visible (target entity) Conditions added to water surface check - most maps are compiled with transparent water nowadays ======================================================================*/ float(entity targ) visible = { local vector spot1, spot2; spot1 = self.origin + self.view_ofs; spot2 = targ.origin + targ.view_ofs; traceline (spot1, spot2, TRUE, self); // see through other monsters // Debug system (enabled via axe) uses funcs from subs_soc.qc // This will draw a diamond model where the AI is looking if (self.debuglvl) { if (!debugent1) debugent1 = spawn_devmarker(self.origin); spot1 = vectoangles(self.origin - trace_endpos); makevectors(spot1); spot2 = trace_endpos + (v_up * -16) + (v_forward * 50); setorigin(debugent1,'0 0 0'); setorigin(debugent1, spot2); } // Check for liquid surface block condition if (check_liquidblock(spot1, spot2) == TRUE) return FALSE; if (trace_fraction == 1) return TRUE; return FALSE; }; /*====================================================================== visxray (variable options) Returns TRUE if there is no blocking, FALSE if something in the way Custom options for variable source/target offsets and monster blocking (nomonsters) TRUE = no block, FALSE = blocked self is excluded from any trace starting points ======================================================================*/ float(entity targ, vector s_offset, vector t_offset, float nomonsters) visxray = { local vector spot1, spot2; // Bmodels don't have proper origins if (self.bsporigin) spot1 = bmodel_origin(self) + s_offset; else spot1 = self.origin + s_offset; spot2 = targ.origin + t_offset; traceline (spot1, spot2, nomonsters, self); // custom options // Check for liquid surface block condition if (check_liquidblock(spot1, spot2) == TRUE) return FALSE; // monster blocking changes which test to use if (nomonsters) { // Has the traceline gone from source>target without anything blocking? if (trace_fraction == 1) return TRUE; return FALSE; } else { // Is the entity hit by the traceline the targ destination? if (trace_ent == targ) return TRUE; return FALSE; } }; /*====================================================================== visblocked (target entity) check to see if sightline is blocked by other monsters Really only used in CheckAttack functions (was in org ID code) ======================================================================*/ float(entity targ) visblocked = { local vector spot1, spot2; spot1 = self.origin + self.view_ofs; spot2 = targ.origin + targ.view_ofs; traceline (spot1, spot2, FALSE, self); // blocked by monsters // Check for liquid surface block condition if (check_liquidblock(spot1, spot2) == TRUE) return FALSE; // Original ID behaviour for this type of function // Traceline entity hit = enemy? if (trace_ent == targ) return FALSE; return TRUE; }; /*====================================================================== viswide (target entity) check to see if sightline is blocked by other monsters extra wide check using 3 traces, +/- 16 left/right and origin All three traces have to complete for a positive result ======================================================================*/ float (entity targ, vector s_ofset, vector t_ofset) visblocked_wide = { local vector spot1, spot2, spot3, tr_line; local float tr_leftmon, tr_rightmon, tr_result; tr_line = '0 0 0'; tr_leftmon = tr_rightmon = 0; makevectors(self.angles); //------------------------------------------------------------------ // Traceline 1 - MIDDLE spot1 = self.origin + s_ofset_x * v_forward + s_ofset_y * v_right + s_ofset_z * v_up; spot2 = targ.origin + t_ofset; traceline (spot1, spot2, FALSE, self); // Is trace entity the same as the target entity? if (trace_ent == targ) tr_line_x = 1; // Check for liquid surface block condition // Only need to do this once for the central traceline // If this fails then the left/right should as well if (check_liquidblock(spot1, spot2) == TRUE) tr_line_x = 0; //------------------------------------------------------------------ // Traceline 2 - 32 units LEFT spot3 = spot1 - (v_right * 16); traceline (spot3, spot2, FALSE, self); // Work out if clear shot, hit world or another entity if (trace_ent == targ) tr_line_y = 1; else if (trace_ent != world) tr_leftmon = 1; if (self.debuglvl) { if (!debugent2) debugent2 = spawn_devmarker(self.origin); setorigin(debugent2, trace_endpos); } //------------------------------------------------------------------ // Traceline 3 - 32 units RIGHT spot3 = spot1 + (v_right * 16); traceline (spot3, spot2, FALSE, self); // Work out if clear shot, hit world or another entity if (trace_ent == targ) tr_line_z = 1; else if (trace_ent != world) tr_rightmon = 1; if (self.debuglvl) { if (!debugent3) debugent3 = spawn_devmarker(self.origin); setorigin(debugent3, trace_endpos); } //------------------------------------------------------------------ // Default = no clear shot (keep aim/moving) tr_result = TRUE; // SIMPLE 3 complete traces = nothing blocking visibility if (tr_line_x + tr_line_y + tr_line_z == 3) tr_result = FALSE; // Central trace is good but either side has hit something? else if (tr_line_x) { // Hit a monster on either side = always fail! if (tr_leftmon || tr_rightmon) tr_result = TRUE; // The crssbow bolt is fired from the LEFT side, only right can be clear else if (self.classtype == CT_MONDCROSS) { if (tr_line_y == 0 && tr_line_z == 1) tr_result = FALSE; } // Default - space available on either side else if (tr_line_y + tr_line_z == 1) tr_result = FALSE; } //------------------------------------------------------------------ if (self.debuglvl) { dprint("Trace ("); dprint(ftos(tr_line_x)); dprint(" "); dprint(ftos(tr_line_y)); dprint(" "); dprint(ftos(tr_line_z)); dprint(") Ent ("); dprint(ftos(tr_leftmon)); dprint(" "); dprint(ftos(tr_rightmon)); dprint(") = ("); dprint(ftos(tr_result)); dprint(")\n"); } return tr_result; }; /*====================================================================== infront (target entity) - relies on self being set correctly before entering this function - returns 1 if the entity is in front (in sight) of self ======================================================================*/ float(entity targ) infront = { local vector vec; local float dot; makevectors (self.angles); // sets v_forward, etc globals vec = normalize (targ.origin - self.origin); dot = vec * v_forward; if ( dot > 0.3) return TRUE; else return FALSE; }; /*====================================================================== range (target entity) returns the range catagorization of an entity reletive to self uses constants defined in defs.qc ======================================================================*/ float(entity targ) range = { local vector spot1, spot2; local float r; spot1 = self.origin + self.view_ofs; spot2 = targ.origin + targ.view_ofs; r = vlen (spot1 - spot2); if (r < MON_RANGE_MELEE) return RANGE_MELEE; // <120 = MELEE else if (r < MON_RANGE_NEAR) return RANGE_NEAR; // <500 = NEAR else if (r < MON_RANGE_MID) return RANGE_MID; // <1000 = MID return RANGE_FAR; // >1000 = FAR }; /*====================================================================== range_distance (target entity) returns the vector distance between two points ======================================================================*/ float(entity targ, float flat2d) range_distance = { local vector spot1, spot2; local float r; spot1 = self.origin + self.view_ofs; spot2 = targ.origin + targ.view_ofs; if (flat2d) { spot1_z = spot2_z = 0; } r = vlen (spot1 - spot2); return r; }; /*====================================================================== rangeattack_check check to see if (source) can get a clear shot at (targ) Ignores any water/air content checks ======================================================================*/ float (entity source, entity targ) rangeattack_check = { local vector spot1, spot2; local float r; // origin+offset, otherwise cannot see player up/down slopes spot1 = source.origin + source.view_ofs; spot2 = targ.origin + targ.view_ofs; r = vlen(spot1 - spot2); // Maximum range attack (was 600 taken from shambler, changed to 1000) if (r > MON_RANGE_MID) return FALSE; // Monsters without melee should always range anything really close if (!source.th_melee && r < MON_RANGE_MELEE) return TRUE; traceline (spot1, spot2, FALSE, source); // trace hits monsters (infighting) if (trace_ent != targ) return FALSE; // don't have a clear shot // Does the world define any water transparency if (liquid_alpha == 1 || source.liquidblock) { // sight line crossed contents if (trace_inopen && trace_inwater) return FALSE; } return TRUE; }; /*====================================================================== targangle Returns what angle the target entity is relative to the source entity Useful for checking where the player is located. source, targ = entity origins to create vector angle reverse = the creation of the vector can be reversed offset = shift the final angle clockwise. (frontal sight cone 315-45) ======================================================================*/ float(entity source, entity targ, float offset, float reverse) targangle = { local float targ_dir, targ_ang; // Calculate vector angle between two points (source, target) if (reverse) targ_dir = vectoyaw(source.origin - targ.origin); else targ_dir = vectoyaw(targ.origin - source.origin); targ_ang = anglemod( (source.angles_y - targ_dir) + offset ); return targ_ang; }; // Modified version of targangle not using entities but supplied origins // Targets Y angles needs to be supplied as no entity reference available float(vector source, vector targ, float offset, float reverse, float dst_ang) viewangle = { local float targ_dir, targ_ang; // Calculate vector angle between two points (source, target) if (reverse) targ_dir = vectoyaw(source - targ); else targ_dir = vectoyaw(targ - source); targ_ang = anglemod( (dst_ang - targ_dir) + offset ); return targ_ang; }; /*====================================================================== targzone (source entity, target entity) - Checks which zone the target enemy is located in relation to source (self) 0 = Behind (315-45), 1 = Left (45-135), 2 = Front (135-225), Right = (225-315) ======================================================================*/ float(entity source, entity targ) targzone = { local float target_ang; // Work out angle the angle of the player based on the monster (source) // Add 45 degree offset to shift clockwise the frontal sight cone target_ang = targangle(source, targ, 45, TRUE); if (target_ang > 270) return TARGET_RIGHT; else if (target_ang > 180) return TARGET_FRONT; else if (target_ang > 90) return TARGET_LEFT; else return TARGET_BACK; }; /*====================================================================== HuntTarget Main routine - constantly running/turning towards the enemy ======================================================================*/ void() HuntTarget = { if (self.health < 1) return; // Dead monsters don't hunt! self.oldorigin = self.origin; // Save origin self.goalentity = self.enemy; // Focus on enemy self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin); // Some monsters have special wakeup animations before attacking self.nextthink = time + 0.1; self.think = self.th_run; // Default 1s wait (NM adjusted) before attacking if (!self.attack_instant) SUB_AttackFinished (1); }; /*====================================================================== FoundTarget - An enemy target has been found, wakeup and hunt target - Used by FindTarget once an enemy target has been found - used by T_Damage (combat.qc) for infighting ======================================================================*/ void() FoundTarget = { if (self.health < 1) return FALSE; // Dead monsters don't hunt! // Check for Liquid surface block entity key // if the monster has range attack, can NOW see through liquid surface if (self.liquidblock == TRUE && self.th_missile) self.liquidblock = FALSE; if (self.enemy.flags & FL_CLIENT) { sight_entity = self; // Highlight an angry monster for others sight_entity_time = time; // reset wakeup timer } //---------------------------------------------------------------------- // This will cause a group of monsters to all attack the same target // Using the global sight enemy system which works with FindTarget() // Always check for an empty sighttarget first because the // the global sight entity could have an empty targetname //---------------------------------------------------------------------- if (self.sighttarget != "") { if (self.enemy.targetname == self.sighttarget) { // Make sure the sight target is alive before chasing it self.oldenemy = find(world, targetname, self.sighttarget); if (self.oldenemy.health > 0) { if (self.oldenemy.takedamage > DAMAGE_NO) { sight_entity = self.enemy; // Highlight target for others sight_entity_time = time; // reset wakeup timer monster_sightsound(); // Always make wakeup noise! } } self.oldenemy = world; } } self.show_hostile = time + 1; // wake up other monsters // Only play wakeup sound if player, not for other monsters if (self.enemy.flags & FL_CLIENT) monster_sightsound(); HuntTarget (); // Keep running after enemy }; /*====================================================================== FoundHuntTarget - This is a combination of FoundTarget and HuntTarget - Does not do any sight sound, needs to be done outside this func - Does not pause or wait for any monster, straight to combat - Has no end funtion state, can decide this afterwards ======================================================================*/ void(float wakeupothers) FoundHuntTarget = { if (self.health < 1) return; // Dead monsters don't hunt! if (!self.enemy) return; // Check for Liquid surface block entity key // if the monster has range attack, can NOW see through liquid surface if (self.liquidblock == TRUE && self.th_missile) self.liquidblock = FALSE; if (self.enemy.flags & FL_CLIENT && wakeupothers == TRUE) { sight_entity = self; // Highlight an angry monster for others sight_entity_time = time; // reset wakeup timer } self.show_hostile = time + 1; // wake up other monsters self.oldorigin = self.origin; // Save origin self.goalentity = self.enemy; // Focus on enemy self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin); SUB_AttackFinished (1); // wait a while before first attack }; /*====================================================================== FindTarget - Main routine for finding a PLAYER target - Returns TRUE if an enemy was sighted ======================================================================*/ float() FindTarget = { local entity client, playtarg; if (self.health < 1) return FALSE; // Dead monsters don't hunt! //---------------------------------------------------------------------- // Does the global sight entity match the enemy sight group? // Always check for an empty sighttarget first because the // the global sight entity could have an empty targetname //---------------------------------------------------------------------- if (self.sighttarget != "") { if (sight_entity.targetname == self.sighttarget) { // Make sure the sight target is alive before chasing it if (sight_entity.health < 0) self.sighttarget = ""; else { // If using enerytarget system, switch off tracking entity first if (self.enemy.classtype == CT_ENEMYTARGET) SUB_switchEnemyTarget(); self.enemy = sight_entity; // Switch to new enemy FoundTarget (); // Run+turn towards new enemy return TRUE; } } } //---------------------------------------------------------------------- // if the first spawnflag bit is set, the monster will only wake up on // really seeing the player, not another monster getting angry // Zombies have their ambush/crucified spawnflag mix up fixed //---------------------------------------------------------------------- if (sight_entity_time >= time - 0.1 && !(self.spawnflags & MON_AMBUSH) ) { client = sight_entity; // Global variable if (client.enemy == self.enemy) return TRUE; } else { client = checkclient (); // Find a client in current PVS if (!client) return FALSE; // If no client found, return } //---------------------------------------------------------------------- // Checkclient is *suppose* to find ONLY players in the current PVS // but it seems the code does something slightly different // Checkclient will return other 'objects' (monsters) who are mad // at the player, which is suppose to be caught with sight_enemy // Find the player entity (client,.enemy,world) for specific checks //---------------------------------------------------------------------- if (client.flags & FL_CLIENT) playtarg = client; else { if (client.enemy) playtarg = client; else playtarg = find(world,targetname,"player"); } //---------------------------------------------------------------------- // If current enemy = client (player), already got *that* target //---------------------------------------------------------------------- if (client == self.enemy) return FALSE; //---------------------------------------------------------------------- // Check for debug mode, invis artifact, menu system active //---------------------------------------------------------------------- if (playtarg.flags & FL_NOTARGET) return FALSE; // Boss monsters can see invisible players! if (playtarg.items & IT_INVISIBILITY && self.bossflag == FALSE) return FALSE; if (intermission_running) return FALSE; //---------------------------------------------------------------------- // Is the client/player >1000 = FAR do nothing, too far away //---------------------------------------------------------------------- enemy_range = range (client); if (enemy_range == RANGE_FAR && !self.attack_sniper) return FALSE; //---------------------------------------------------------------------- // Do not wake up unless the client (player/monster) is visible //---------------------------------------------------------------------- enemy_vis = visible(client); if (!enemy_vis) return FALSE; //---------------------------------------------------------------------- // (Distance >128 && < 500) Is client infront+side and hostile? // show_hostile flag is set when the player fires a weapon //---------------------------------------------------------------------- if (enemy_range == RANGE_NEAR) { if (client.show_hostile < time) { if (self.sight_nofront == FALSE && !infront (client)) return FALSE; } } //---------------------------------------------------------------------- // (Distance 500+) Is client infront? //---------------------------------------------------------------------- else if (enemy_range > RANGE_NEAR) { if (self.sight_nofront == FALSE && !infront (client)) return FALSE; } // At this point if the enemy is <120 (melee range) the enemy // will always wake up and turn around towards the player //---------------------------------------------------------------------- // Finally got a target, check if its a player // This is an odd sitation because at this point client *should* be // the player/client and this code tries to fix the problem by // searching through enemy of enemy chains //---------------------------------------------------------------------- self.enemy = client; if ( !(client.flags & FL_CLIENT) ) { // Is the enemy of the enemy a player? self.enemy = self.enemy.enemy; if ( !(client.enemy.flags & FL_CLIENT) ) { self.enemy = world; return FALSE; } } FoundTarget (); // Run+turn towards new enemy return TRUE; // stop previous routine got a target }; //====================================================================== // ai_face (generic) //---------------------------------------------------------------------- void() ai_face = { if (self.health < 1) return;// Unusual check, caught elsewhere self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin); ChangeYaw (); // Done in code }; //---------------------------------------------------------------------- // ai_forward //---------------------------------------------------------------------- void(float dist) ai_forward = { walkmove (self.angles_y, dist); }; //---------------------------------------------------------------------- // ai_forward + ai_face //---------------------------------------------------------------------- void(float dist) ai_faceforward = { ai_face(); walkmove (self.angles_y, dist); }; //---------------------------------------------------------------------- // ai_back + backface //---------------------------------------------------------------------- void(float dist) ai_back = { walkmove ( (self.angles_y+180), dist); }; void(float dist) ai_backface = { ai_face(); ai_back(dist); }; //---------------------------------------------------------------------- // ai_pain (does nothing, just uses ai_back instead) //---------------------------------------------------------------------- void(float dist) ai_pain = { ai_back (dist); }; //---------------------------------------------------------------------- // ai_painforward //---------------------------------------------------------------------- void(float dist) ai_painforward = { walkmove (self.angles_y, dist); // Not sure why Id used ideal_yaw when other functions use angles_y // walkmove (self.ideal_yaw, dist); }; //---------------------------------------------------------------------- // ai_turn (Very costly function, re-using FindTarget again) //---------------------------------------------------------------------- void() ai_turn = { if (FindTarget ()) return; ChangeYaw (); // Code function }; //---------------------------------------------------------------------- // FacingIdeal //---------------------------------------------------------------------- float() FacingIdeal = { local float delta; delta = anglemod(self.angles_y - self.ideal_yaw); if (delta > 45 && delta < 315) return FALSE; else return TRUE; }; //---------------------------------------------------------------------- // ai_trackenemy //---------------------------------------------------------------------- float() ai_trackenemy = { if (visible(self.enemy)) { self.attack_track = SUB_orgEnemyTarget(); ai_face(); return TRUE; } else return FALSE; }; //---------------------------------------------------------------------- // ai_resetangles //---------------------------------------------------------------------- void() ai_resetangles = { // There is a chance when finished a monster jump that some // of the angles (X/Z) values are wrong and need resetting // Should only be used after jump functions self.angles_x = self.angles_z = 0; }; //---------------------------------------------------------------------- // ai_charge (generic) // - The monster is in a melee attack, // so get as close as possible to .enemy //---------------------------------------------------------------------- void(float dist) ai_charge = { ai_face (); movetogoal (dist); }; //---------------------------------------------------------------------- // ai_chargenoturn (generic) //---------------------------------------------------------------------- void(float dist) ai_chargenoturn = { movetogoal (dist); }; //---------------------------------------------------------------------- // ai_charge_side (generic) //---------------------------------------------------------------------- void() ai_charge_side = { local vector dtemp; local float heading; // aim to the left of the enemy for a flyby self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin); ChangeYaw (); makevectors (self.angles); dtemp = self.enemy.origin - (30 * v_right); heading = vectoyaw(dtemp - self.origin); walkmove(heading, 20); }; //---------------------------------------------------------------------- // ai_charge_front (generic) //---------------------------------------------------------------------- void() ai_charge_front = { local float heading; ai_face(); // Turn towards enemy first makevectors (self.angles); heading = vectoyaw(self.enemy.origin - self.origin); walkmove(heading, 10); }; /*====================================================================== ai_run_melee - Turn and close until within an angle to launch a melee attack ======================================================================*/ void() ai_run_melee = { self.ideal_yaw = enemy_yaw; // This is defined in ai_run ChangeYaw (); // Code function //---------------------------------------------------------------------- // Facing towards the ENEMY target? (ideal_yaw) //---------------------------------------------------------------------- if (FacingIdeal()) { self.th_melee (); self.attack_state = AS_STRAIGHT; } }; /*====================================================================== ai_run_missile - Turn in place until within an angle to launch a missile attack ======================================================================*/ void() ai_run_missile = { self.ideal_yaw = enemy_yaw; // This is defined in ai_run ChangeYaw (); // Code function //---------------------------------------------------------------------- // Facing towards the ENEMY target? (ideal_yaw) //---------------------------------------------------------------------- if (FacingIdeal()) { self.th_missile (); self.attack_state = AS_STRAIGHT; } }; /*====================================================================== ai_run_jump - Turn in place until within an angle to jump attack ======================================================================*/ void() ai_run_jump = { self.ideal_yaw = enemy_yaw; // This is defined in ai_run ChangeYaw (); // Code function //---------------------------------------------------------------------- // Facing towards the ENEMY target? (ideal_yaw) //---------------------------------------------------------------------- if (FacingIdeal()) { self.th_jump (); self.attack_state = AS_STRAIGHT; } }; /*====================================================================== ai_run_slide - Strafe sideways, but stay at aproximately the same range ======================================================================*/ void(float dist) ai_run_slide = { local float ofs; // It is better to re-calcuate this than wait for ai_run to update self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin); // self.ideal_yaw = enemy_yaw; // This is defined in ai_run ChangeYaw (); // Code function if (self.lefty > 0) ofs = 90; else ofs = -90; // Tests if monsters can strafe or not by moving the monster if (walkmove (self.ideal_yaw + ofs, dist)) return; // Switch strafe sides for later use self.lefty = rint(1 - self.lefty); if (walkmove (self.ideal_yaw - ofs, dist)) return; // Try moving backwards if both sides are blocked walkmove (self.ideal_yaw - 180, dist); }; /*====================================================================== ai_run_sidestep - Turn 90 degrees and move to the side ======================================================================*/ void(float dist) ai_run_sidestep = { local float ofs; if (self.attack_sidestep < time) { self.attack_sidestep = time + 4 + random()*4; self.lefty = rint(1 - self.lefty); } if (self.lefty > 0) ofs = 85; else ofs = -85; // Work out angle to face for enemy and then turn +/- 90 degrees // so that the monster is moving sideways to the enemy self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin) + ofs; ChangeYaw (); // Tests if monsters can move sideways if (walkmove (self.ideal_yaw, dist)) return; // Switch strafe sides for later use self.attack_sidestep = -1; }; /*====================================================================== ai_run_backwards - Turn 180 degrees and move backwards ======================================================================*/ void(float dist) ai_run_backward = { // Work out angle to face for enemy and then turn +/- 90 degrees // so that the monster is moving sideways to the enemy self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin) + 180; ChangeYaw (); // Tests if monsters can move backwards if (walkmove (self.ideal_yaw, dist)) return; // Cannot walk backwards, move sideways instead self.attack_state = AS_SIDESTEP; }; /*====================================================================== ai_foundbreakable Check if the monster is allowed to break the breakable!?! ======================================================================*/ float(entity source, entity targ, float ignorenoshoot) ai_foundbreakable = { // Found a breakable in the way? if (targ.classtype == CT_FUNCBREAK) { // Check if a monster and nomonster damage/trigger allowed? if (source.flags & FL_MONSTER && targ.spawnflags & BREAK_NOMONSTER) return FALSE; // Ignoring noshoot spawnflag? (designed for jump/missile events) else if (ignorenoshoot == TRUE) return TRUE; // Can the breakable be damaged? else if (targ.spawnflags & BREAK_NOSHOOT) return FALSE; // Its a breakable that breaks! else return TRUE; } return FALSE; }; /*====================================================================== ai_immunedamage Check if a breakable/pushable is immune to damage from monsters ======================================================================*/ float(entity source, entity targ) ai_immunedamage = { // Found a breakable? if (targ.classtype == CT_FUNCBREAK) { // Check no damage spawnflag? if (source.flags & FL_MONSTER && targ.spawnflags & BREAK_NOMONSTER) return TRUE; } // Found a pushable? else if (targ.classtype == CT_FUNCPUSHABLE) { // Check no damage spawnflag? if (source.flags & FL_MONSTER && targ.spawnflags & PUSH_NOMONSTER) return TRUE; } return FALSE; }; /*====================================================================== ai_damagebreakable Check if the monster (self) can hit any breakables infront (target) Triple trace infront (self.angles) up,middle,down If any breakables found, damage them to see if they will break ======================================================================*/ void(float brkdmg) ai_damagebreakable = { local vector spot1, spot2; local float ldmg; // Setup damage, forward facing vector and initial trace origin ldmg = (random() + random() + random()) * brkdmg; makevectors(self.angles); spot1 = self.origin; // Trace directly infront of entity using angles spot2 = spot1 + v_forward * 100; traceline (spot1, spot2, TRUE, self); if (ai_foundbreakable(self, trace_ent, FALSE)) { // Check for any monster damage modifier on breakable if (trace_ent.brkmondmg>0) ldmg = ldmg * trace_ent.brkmondmg; T_Damage (trace_ent, self, self, ldmg, DAMARMOR); } else { // Trace upwards spot2 = spot1 + v_forward * 100 + v_up * 64; traceline (spot1, spot2, TRUE, self); if (ai_foundbreakable(self, trace_ent, FALSE)) { // Check for any monster damage modifier on breakable if (trace_ent.brkmondmg>0) ldmg = ldmg * trace_ent.brkmondmg; T_Damage (trace_ent, self, self, ldmg, DAMARMOR); } else { // Trace downwards spot2 = spot1 + v_forward * 100 - v_up * 64; traceline (spot1, spot2, TRUE, self); if (ai_foundbreakable(self, trace_ent, FALSE)) { // Check for any monster damage modifier on breakable if (trace_ent.brkmondmg>0) ldmg = ldmg * trace_ent.brkmondmg; T_Damage (trace_ent, self, self, ldmg, DAMARMOR); } } } }; /*====================================================================== ai_jumpbreakable Check if the monster can trigger a breakable from jump attack ======================================================================*/ void(float brkdmg) ai_jumpbreakable = { if (ai_foundbreakable(self, other,TRUE) && other.brktrigjump != 0) { // Found a breakable which is prone to jump damage trigger_ent(other, self); } else ai_damagebreakable(brkdmg); // Damage any breakables }; /*====================================================================== ai_checkmelee Check if the monster (self) can attack enemy (target) and returns TRUE if the monster is within XYZ range ======================================================================*/ float(float attackdist) ai_checkmelee = { local vector spot1, spot2; local float delta, zdiff; // Calculate distance and z axis difference seperate spot1 = SUB_orgEnemyTarget(); spot2 = self.origin; zdiff = fabs(spot1_z - spot2_z); spot1_z = spot2_z = 0; // Flatten Z axis before vector length delta = vlen(spot1 - spot2); // Calculate vector distance // Is the enemy too far away and the zaxis is wrong (too low/high) if (delta < attackdist && zdiff < MONAI_MELEEZAXIS) return TRUE; else return FALSE; }; /*====================================================================== ai_melee (generic) - slashing type of damage facing forward really assuming the monster is stationary while attacking ======================================================================*/ void() ai_melee = { local float ldmg; if (!self.enemy) return; // removed before stroke ai_damagebreakable(10); // Damage any breakables if (!ai_checkmelee(MONAI_MELEEFRONT)) return; // Too far away // Can the target bleed? - no blood/damage, quick exit // This candamage test not in origina id code (sync'd to melee_side) if (!CanDamage (self.enemy, self)) return; // 1-9 damage ldmg = (random() + random() + random()) * 3; if (ldmg < 1) ldmg = 1; T_Damage (self.enemy, self, self, ldmg, DAMARMOR); // Only spawn blood/gore/sound every second if (self.meleetimer < time) { self.meleetimer = time + 1; SpawnMeatSpray (self, self.enemy, random() * 100); } // Check for poisonous blades! if (self.poisonous) PoisonDeBuff(self.enemy); // Some melee weapons have swing and hit as separate sounds // This is trigger_once per melee combat swing // No check if meleehitsound is defined or not if (self.meleecontact) { self.meleecontact = FALSE; sound (self, CHAN_WEAPON, self.meleehitsound, 1, ATTN_NORM); } }; /*====================================================================== ai_meleesmash (generic) - large smashing damage overhead attack, usually a single strike really assuming the monster is stationary while attacking ======================================================================*/ void(float dmg_multiplier) ai_meleesmash = { local float ldmg; if (!self.enemy) return; // removed before stroke ai_damagebreakable(3*dmg_multiplier); // Damage any breakables if (!ai_checkmelee(self.meleerange)) return; // Too far away // Can the target bleed? - no blood/damage, quick exit // This candamage test not in origina id code (sync'd to melee_side) if (!CanDamage (self.enemy, self)) return; // This function is designed for monsters attacking players or infighting // If this is infighting do more damage as it will look more impressive // Also if this damage is enough to kill in a single blow, gib for effect if (self.enemy.flags & FL_MONSTER) dmg_multiplier = dmg_multiplier * 2; // 1-9 damage ldmg = (random() + random() + random()) * dmg_multiplier; if (ldmg < 1) ldmg = 1; if (self.enemy.health < ldmg) ldmg = ldmg*3; T_Damage (self.enemy, self, self, ldmg, DAMARMOR); // Lots of blood and gore SpawnMeatSpray (self, self.enemy, crandom() * 100); SpawnMeatSpray (self, self.enemy, crandom() * 100); // Check for poisonous blades! if (self.poisonous) PoisonDeBuff(self.enemy); // Some melee weapons have swing and hit as separate sounds // This is trigger_once per melee combat swing if (self.meleecontact) { self.meleecontact = FALSE; if (self.meleehitsound) sound (self, CHAN_WEAPON, self.meleehitsound, 1, ATTN_NORM); } }; /*====================================================================== ai_melee_side (generic) - move forward/side and attack (can cause animation sliding errors) this is really designed for glancing blows and monsters which are moving fast like they are charging at the player ======================================================================*/ void() ai_melee_side = { local float ldmg; if (!self.enemy) return; // removed before stroke ai_damagebreakable(10); // Damage any breakables ai_charge_side(); // move (20 units) to the side of enemy if (!ai_checkmelee(MONAI_MELEESIDE)) return; // Too far away // Can the target bleed? - no blood/damage, quick exit if (!CanDamage (self.enemy, self)) return; // 1-9 damage ldmg = (random() + random() + random()) * 3; if (ldmg < 1) ldmg = 1; T_Damage (self.enemy, self, self, ldmg, DAMARMOR); SpawnMeatSpray (self, self.enemy, random() * 50); // Check for poisonous blades! if (self.poisonous) PoisonDeBuff(self.enemy); // Some melee weapons have swing and hit as separate sounds // This is trigger_once per melee combat swing // No check if meleehitsound is defined or not if (self.meleecontact) { self.meleecontact = FALSE; sound (self, CHAN_WEAPON, self.meleehitsound, 1, ATTN_NORM); } }; /*====================================================================== ai_shockwave - produce a large ground slam/shockwave effect - Used by hammer ogres and golems ======================================================================*/ void() ai_shockwave_think = { self.wait = rint((time - self.ltime)*10); if (self.wait > 6) SUB_Remove(); else { self.frame = self.wait; if (self.wait > 4) { if (!self.delay) {self.delay = time;} self.alpha = 1-((time - self.delay)*5); } self.angles_y = anglemod(self.angles_y + rint(random()*20)); self.nextthink = time + TIME_MINTICK; } }; //---------------------------------------------------------------------- void(vector imp_vec, float imp_damage, float imp_radius, float imp_forward, float imp_up) ai_shockwave = { local entity swave, sjump; local vector impact, fvel; local float vdist, vpercent, vpart; local float impzdiff; self.effects = self.effects | EF_MUZZLEFLASH; // Check if the player is close enough for damage? self.meleecontact = TRUE; ai_meleesmash(imp_damage); self.meleecontact = FALSE; // Play impact sound and work out where the impact is going to happen sound (self, CHAN_WEAPON, self.meleehitsound, 1, ATTN_IDLE); makevectors (self.angles); // impact = self.origin + v_forward*imp_vec_x + v_right*imp_vec_y + v_up*imp_vec_z; impact = self.origin + attack_vector(imp_vec); // Push all entities within a certain radius outwards // Do this find search before spawning ring and particles // findradius will find ALL entities regardless of type sjump = findradius(impact, imp_radius); while(sjump) { // Ignore monster (self) if (sjump != self) { // Only affect players and other monsters if (sjump.flags & FL_CLIENT || sjump.flags & FL_MONSTER) { // Exclude statues, stone/heavy monsters and bosses! if (sjump.classgroup != CG_STONE && sjump.bossflag == FALSE) { // Is the entity on the ground? if (sjump.flags & FL_ONGROUND) { // Is the entity too far above or below the impact? impzdiff = fabs(sjump.origin_z - impact_z); if (impzdiff < MONAI_IMPACTZAXIS) { // Distance between impact and enemy vdist = vlen(sjump.origin - impact); // Percentage of impact force vpercent = 1 - (vdist / imp_radius); // Monsters have less impact if (sjump.flags & FL_MONSTER) vpercent = vpercent * 0.75; // Ogre facing angle (easier to understand) makevectors(self.angles); // Combine forward/up force with existing velocity fvel = (v_forward * (imp_forward * vpercent)) + (v_up * (imp_up * vpercent)); sjump.velocity = sjump.velocity + fvel; sjump.flags = sjump.flags - (sjump.flags & FL_ONGROUND); } } } } } sjump = sjump.chain; } // Spawn impact model ring on the ground swave = spawn(); swave.mdl = MODEL_PROJ_RINGSHOCK; setmodel(swave,swave.mdl); setsize(swave, VEC_ORIGIN, VEC_ORIGIN); setorigin(swave, impact); // on floor, slight up (2 pixels) swave.solid = SOLID_NOT; // No interaction with world swave.movetype = MOVETYPE_NONE; // Static item, no movement swave.think = ai_shockwave_think; swave.nextthink = time + TIME_MINTICK; // High time loop swave.ltime = swave.nextthink; // Spawn particle explosion where impact is located vpart = 64 + rint(random()*64); particle_explode(impact, vpart, 0.5, PARTICLE_BURST_FIRE, PARTICLE_BURST_SHOCKWAVE); };