include ai.ai_aggro; include ai.ai_combat; include ai.ai_combat_assist; include ai.ai_combat_damage; include ai.ai_combat_movement; include ai.ai_combat_target; include library.ai_lib; include library.beast_lib; include library.buff; include library.colors; include library.combat; include library.combat_consts; include library.factions; include library.instance; include library.movement; include library.pclib; include library.pet_lib; include library.posture; include library.scout; include library.stealth; include library.storyteller; include library.sui; include library.trial; include library.utils; include library.xp; include library.stealth; inherits systems.combat.combat_base; /// value between 0 and 1. 1 being constant movement (not recommended). This number gets evaluated /// 4 times a second, so it needs to be sufficiently low to not happen too often. Min and max gets interpolated /// based on const float MIN_MOVEMENT_DURING_COMBAT = 0.015f; const float MAX_MOVEMENT_DURING_COMBAT = 0.05f; const float TOO_CLOSE_DISTANCE = 3f; void clog(string text) { if(text != null) { LOGC(aiLoggingEnabled(getSelf()), "debug_ai", "creature_combat::" + text); } } trigger OnEnteredCombat() { //debugSpeakMsg(self, "OnEnteredCombat"); clog("OnEnteredCombat() ----- unit(" + self + ":" + getName(self) + ")"); clearCombatActions(); if (!stealth.isDecoy(self)) { aiEquipPrimaryWeapon(self); } // Once the AI is in combat, there is no need to listen for these trigger volume breaches removeTriggerVolume(ai_lib.ALERT_VOLUME_NAME); removeTriggerVolume(ai_lib.AGGRO_VOLUME_NAME); ai_combat_movement.aiClearMoveMode(); setMovementRun(self); deltadictionary dict = self.getScriptVars(); dict.put("ai.combat.cover.foundTarget", isPlayer(getHateTarget(self)) ? false : true); if(ai_combat_assist.isWaiting()) { ai_combat_movement.aiIdle(); } else { obj_id target = getHateTarget(self); boolean targetCovered = stealth.hasInvisibleBuff(target); boolean targetProne = posture.isProne(target); if(targetCovered || targetProne) showFlyText(self, new string_id("npc_reaction/flytext", "alert"), 2.0f, colors.STEELBLUE); utils.setScriptVar(self, "creature_combat.lastCombatFrame", getGameTime()); if (ai_combat_assist.canCallForAssist()) ai_combat_assist.callForAssist(); if(beast_lib.isBeast(self) && getState(self, STATE_COVER) == 1) { stealth.makeCreatureVisible(self); } doCombatFrame(); } messageTo(self, "checkForBeastSpecialsTakeOne", null, 1.0f, false); setRegenRate(self, HEALTH, 0); if(!beast_lib.isBeast(self)) { setRegenRate(self, ACTION, 20); } else { //This was going to be a really cool BM regen experise so the beast would get a few extra attacks every few rounds int actionRegen = 4; obj_id master = getMaster(self); int expertiseRegen = 0; if(isIdValid(master) && exists(master)) { expertiseRegen = getEnhancedSkillStatisticModifierUncapped(master, "expertise_bm_pet_regen"); } if(expertiseRegen > 0) { actionRegen += (int)((float)actionRegen * ((float)expertiseRegen / 100.0f)); } setRegenRate(self, ACTION, actionRegen); } if (combat.isPersistCombatMode(self)) { messageTo(self, "persist_combat", trial.getSessionDict(self, combat.PERSIST_COMBAT), 5.0f, false); } return SCRIPT_CONTINUE; } trigger OnExitedCombat() { if(!isIdValid(self) || !exists(self)) { return SCRIPT_CONTINUE; } //debugSpeakMsg(self, "OnExitedCombat"); clog("OnExitedCombat() ----- unit(" + self + ":" + getName(self) + ")"); ai_combat_target.clearTargetList(); ai_combat_damage.clearAttackerList(); ai_combat_movement.aiClearMoveMode(); ai_combat_assist.clearNoAssistCall(); setLookAtTarget(self, null); // Makes sure no players keep their evasion buff on the creature, after tethering. utils.removeScriptVarTree(self, "me_evasion"); if(!beast_lib.isBeast(self)) { ai_lib.resetCombatTriggerVolumes(); float healthRegenMod = hasObjVar(self, "regen_mod.health") ? getFloatObjVar(self, "regen_mod.health") : 1.0f; float actionRegenMod = hasObjVar(self, "regen_mod.action") ? getFloatObjVar(self, "regen_mod.action") : 1.0f; float healthRegen = (getMaxAttrib(self, HEALTH) / 10.0f) * healthRegenMod; float actionRegen = (getMaxAttrib(self, ACTION) / 10.0f) * actionRegenMod; setRegenRate(self, HEALTH, (int)healthRegen); // 10% of maximum health regeneration rate setRegenRate(self, ACTION, (int)actionRegen); // 10% of maximum action regeneration rate } else { int healthRegen = 150; int actionRegen = 10; obj_id master = getMaster(self); int expertiseRegen = 0; if(isIdValid(master) && exists(master)) { expertiseRegen = getEnhancedSkillStatisticModifierUncapped(master, "expertise_bm_pet_regen"); } if(expertiseRegen > 0) { actionRegen += (int)((float)actionRegen * ((float)expertiseRegen / 100.0f)); healthRegen += (int)((float)healthRegen * ((float)expertiseRegen / 100.0f)); } setRegenRate(self, HEALTH, healthRegen); setRegenRate(self, ACTION, actionRegen); if(stealth.canBeastStealth(self)) { stealth.makeBeastInvisible(getMaster(self), "appearance/pt_smoke_puff.prt"); } } if(!ai_lib.isAiDead(self)) { if(posture.isKnockedDown(self)) { queueCommand(self, ##"knockDownRecovery", self, "", COMMAND_PRIORITY_DEFAULT); } else if(!posture.isStanding(self)) { posture.stand(self); } if(!beast_lib.isBeast(self)) { if(pet_lib.isPet(self)) { messageTo(self, "postCombatPathHome", null, 1, false); } else { if(!hasObjVar(self, "forceNoMovement")) { aiTether(self); } else { cleanupForTether(self); pathTo(self, aiGetHomeLocation(self)); } } } } bumpSession(self); trial.bumpSession(self, combat.PERSIST_COMBAT); messageTo(self, "ai_assist_check", null, 0.25f, false); return SCRIPT_CONTINUE; } messageHandler ai_assist_check() { if(!isIdValid(self) || !exists(self)) { return SCRIPT_CONTINUE; } ai_combat_assist.callForAssist(); return SCRIPT_CONTINUE; } void cleanupForTether(obj_id self) { if(!isIdValid(self) || !exists(self)) { return; } ai_lib.setMood(self, ai_lib.MOOD_CALM); ai_lib.clearCombatData(); xp.cleanupCreditForKills(); } trigger OnAiTetherStart() { // clog("ai::OnAiTetherStart() --- BEGIN --- self(" + self + ":" + getName(self) + ")"); if(!isIdValid(self) || !exists(self)) { return SCRIPT_CONTINUE; } cleanupForTether(self); // Speed up the AI running back to home // clog("ai::OnAiTetherStart() --- END --- self(" + self + ")"); return SCRIPT_CONTINUE; } trigger OnAiTetherComplete() { clog("ai::OnAiTetherComplete() --- BEGIN --- self(" + self + ":" + getName(self) + ")"); ai_lib.resetAi(); clog("ai::OnAiTetherComplete() --- END --- self(" + self + ":" + getName(self) + ")"); return SCRIPT_CONTINUE; } trigger OnHateTargetChanged(obj_id target) { clog("OnHateTargetChanged() self(" + self + ":" + getName(self) + ") target(" + target + ")"); setLookAtTarget(self, target); if(ai_lib.isInCombat(self)) { ai_combat_assist.clearAssist(); } if(pet_lib.isPet(self) && utils.hasScriptVar(self, "ai.combat.target")) { obj_id combatTarget = utils.getObjIdScriptVar(self, "ai.combat.target"); if(target != combatTarget) { float maxHate = getMaxHate(self); setHate(self, combatTarget, maxHate + 5000); } } return SCRIPT_CONTINUE; } trigger OnHateTargetAdded(obj_id target) { clog("creature_combat::OnHateTargetAdded() self(" + self + ":" + getName(self) + ") target(" + target + ")"); return SCRIPT_CONTINUE; } trigger OnHateTargetRemoved(obj_id target) { clog("creature_combat::OnHateTargetRemoved() self(" + self + ":" + getName(self) + ") target(" + target + ")"); if (!isIdValid(target) || !exists(target)) return SCRIPT_CONTINUE; const int factionStatus = factions.getFactionStatus(self, target); if(factionStatus == factions.STATUS_FRIEND || (factionStatus == factions.STATUS_NEUTRAL && !aiIsAggressive(self))) { // if we are a bounty hunter target, we always // maintain a PEF on the bounty hunter if(hasScript(self, "systems.missions.dynamic.mission_bounty_target")) { obj_id objHunter = getObjIdObjVar(self, "objHunter"); if(!isIdValid(objHunter) || (objHunter != target)) { pvpRemovePersonalEnemyFlags(self, target); } } else { pvpRemovePersonalEnemyFlags(self, target); } } if(pet_lib.isPet(self) && utils.hasScriptVar(self, "ai.combat.target")) { obj_id combatTarget = utils.getObjIdScriptVar(self, "ai.combat.target"); if(target == combatTarget) { utils.removeScriptVar(self, "ai.combat.target"); } } return SCRIPT_CONTINUE; } trigger OnAiPrimaryWeaponEquipped(obj_id primaryWeapon) { clog("OnAiPrimaryWeaponEquipped() ai(" + self + ") primaryWeapon(" + primaryWeapon + ")"); clearCombatActions(); return SCRIPT_CONTINUE; } trigger OnAiSecondaryWeaponEquipped(obj_id secondaryWeapon) { clog("OnAiSecondaryWeaponEquipped() ai(" + self + ") secondaryWeapon(" + secondaryWeapon + ")"); clearCombatActions(); return SCRIPT_CONTINUE; } trigger OnAddedToWorld() { return SCRIPT_CONTINUE; } trigger OnAiCombatFrame() { // PROFILER_START("creature_combat.OnAiCombatFrame"); boolean assistWaiting = ai_combat_assist.isWaiting(); // clog("OnAiCombatFrame assistWaiting: " + assistWaiting); if(assistWaiting || getSkillStatisticModifier(self, "stifle") > 0) { //debugSpeakMsg(self, "Waiting, skipping combat."); const obj_id hateTarget = getHateTarget(self); faceTo(self, hateTarget); if(getCombatDuration(self) > ai_combat_assist.getWaitTime()) { ai_combat_assist.clearAssist(); } } else { doCombatFrame(); } // PROFILER_STOP("creature_combat.OnAiCombatFrame"); return SCRIPT_CONTINUE; } void doCombatFrame() { // PROFILER_START("creature_combat.doCombatFrame"); const obj_id self = getSelf(); const obj_id target = getBestTarget(self); int lastFrame = utils.getIntScriptVar(self, "creature_combat.lastCombatFrame"); // clog("doCombatFrame lastFrame: " + lastFrame + " time elapsed: " + (getGameTime() - lastFrame)); utils.setScriptVar(self, "creature_combat.lastCombatFrame", getGameTime()); if(getState(self, STATE_COVER) == 1) { stealth.makeCreatureVisible(self); } if(utils.hasScriptVar(self, "petIgnoreAttacks")) { // PROFILER_STOP("creature_combat.doCombatFrame"); return; } if(!isIdValid(target)) { // clog("aiCombatLoop() ai(" + self + ") target(" + target + ") Bailing due to invalid target"); // Hate list sanity check. ////////////////////////////////////// obj_id[] haters = getHateList(self); if (haters != null && haters.length > 0 && !utils.hasScriptVar(self, "hateListRetry")) { // We somehow have an invalid target, yet we still have people on our hate list. // CustomerServiceLog("combat_errors: ", "Creature [" + self + "] has a bad target but still has a Hate List with entities in it. Dumping Hate List..."); // 1st let's build a list of valid people. resizeable obj_id[] goodIds = new obj_id[0]; for(int i = 0; i < haters.length; ++i) { // CustomerServiceLog("combat_errors: ", "Creature [" + self + "] Hate List Entry [" + i + "] : " + haters[i]); if(isIdValid(haters[i]) && exists(haters[i]) ) { utils.addElement(goodIds, haters[i]); } } if(goodIds != null & goodIds.length > 0) { // Pick a random new, good target. int targetIndex = rand(0, goodIds.length - 1); // CustomerServiceLog("combat_errors: ", "Creature [" + self + "] forcing Hate Target to [" + goodIds[targetIndex] + "]"); forceHateTarget(self, goodIds[targetIndex]); // Tracking to prevent infinite loops. utils.setScriptVar(self, "hateListRetry", 1); // Force our combat frame to restart. doCombatFrame(); } } if(utils.hasScriptVar(self, "hateListRetry")) utils.removeScriptVar(self, "hateListRetry"); ////////////////////////////////////// if(beast_lib.isBeast(self)) { obj_id master = getMaster(self); if(isIdValid(master) && master.isLoaded()) { beast_lib.beastFollowTarget(self, master); } else { aiSetHomeLocation(self, getLocation(self)); } // PROFILER_STOP("creature_combat.doCombatFrame"); return; } if(pet_lib.isPet(self)) { messageTo(self, "postCombatPathHome", null, 1, false); return; } if(!hasObjVar(self, "forceNoMovement")) { aiTether(self); } else { cleanupForTether(self); pathTo(self, aiGetHomeLocation(self)); } // PROFILER_STOP("creature_combat.doCombatFrame"); return; } boolean checkVE = isIdValid(target) && exists(target) && isIdValid(self) && exists(self); if(!storyteller.storytellerCombatCheck(self, target)) { // debugSpeakMsgc(aiLoggingEnabled(self), self, "Removing non-STORYTELLER target(" + target + ")"); if(checkVE) { removeHateTarget(self, target); } // PROFILER_STOP("creature_combat.doCombatFrame"); return; } if(getState(target, STATE_FEIGN_DEATH) == 1) { // debugSpeakMsgc(aiLoggingEnabled(self), self, "Removing FEIGNED target(" + target + ")"); if(checkVE) { removeHateTarget(self, target); } // PROFILER_STOP("creature_combat.doCombatFrame"); return; } else if(!ai_combat_target.isValidTarget(target)) { // debugSpeakMsgc(aiLoggingEnabled(self), self, "Removing INVALID target(" + target + ")"); if(checkVE) { removeHateTarget(self, target); } // PROFILER_STOP("creature_combat.doCombatFrame"); return; } const boolean targetCovered = stealth.hasInvisibleBuff(target); const boolean targetProne = posture.isProne(target); const int freeShotCount = targetCovered ? 3 : (targetProne ? 1 : 0); deltadictionary dict = self.getScriptVars(); if(!dict.getBoolean("ai.combat.cover.foundTarget")) { if((getCombatDuration(self) > 10) || (freeShotCount == 0) || (ai_combat_damage.getAttackCount(target) > freeShotCount)) { if(freeShotCount > 0 && !hasScript(self, "systems.dungeon_sequencer.ai_controller")) { showFlyText(self, new string_id("npc_reaction/flytext", "threaten"), 2.0f, colors.ORANGERED); } dict.put("ai.combat.cover.foundTarget", true); } } //??????????? int combatRound = utils.getIntScriptVar(self, "aiCombatRoundCounter"); combatRound++; utils.setScriptVar(self, "aiCombatRoundCounter", combatRound); selectWeapon(target); const boolean commandQueueClear = (getCurrentCommand(self) == 0); if(commandQueueClear) { utils.removeScriptVar(self, "creature_combat.commandQueueTime"); if(posture.isKnockedDown(self)) { //const deltadictionary dict = self.getScriptVars(); const int knockDownTime = dict.getInt("ai.combat.knockdown_time"); if((getGameTime() - knockDownTime) > aiGetKnockDownRecoveryTime(self)) { // debugSpeakMsgc(aiLoggingEnabled(self), self, "KnockDownRecovery"); queueCommand(self, ##"knockDownRecovery", self, "", COMMAND_PRIORITY_DEFAULT); } } else if(!posture.isStanding(self)) { posture.stand(self); } else { if(dict.getBoolean("ai.combat.cover.foundTarget")) { attack(target); } } } else { if(!utils.hasScriptVar(self, "creature_combat.commandQueueTime")) { utils.setScriptVar(self, "creature_combat.commandQueueTime", getGameTime()); } else { int time = utils.getIntScriptVar(self, "creature_combat.commandQueueTime"); // clog("commandQueueClear: " + commandQueueClear + " time since last command: " + (getGameTime() - time)); if(getGameTime() - time > 2) { clearCombatActions(); } } } move(target); // PROFILER_STOP("creature_combat.doCombatFrame"); } obj_id getBestTarget(obj_id self) { obj_id primaryTarget = getHateTarget(self); if(isIdValid(primaryTarget) && exists(primaryTarget) && !isDead(primaryTarget) && !stealth.hasInvisibleBuff(primaryTarget)) return primaryTarget; obj_id[] hateList = getHateList(self); if(hateList == null || hateList.length == 0) return null; float topHate = 0.0f; primaryTarget = null; for(int i = 0; i < hateList.length; i++) { if(!isIdValid(hateList[i]) || !exists(primaryTarget)) { continue; } if(isDead(hateList[i]) || stealth.hasInvisibleBuff(hateList[i])) { removeHateTarget(self, hateList[i]); continue; } if (hasObjVar(self, "noPursue") && !combat.cachedCanSee(self, hateList[i])) continue; if(getHate(self, hateList[i]) > topHate) primaryTarget = hateList[i]; } return primaryTarget; } void move(obj_id target) { // PROFILER_START("creature_combat.move"); const obj_id self = getSelf(); if(ai_combat_movement.isAiImmobile(self)) { // PROFILER_STOP("creature_combat.move"); return; } // clog("move() !posture.isStanding(self): " + !posture.isStanding(self) + " !ai_lib.isWithinLeash(self): " + !ai_lib.isWithinLeash(self)); //debugSpeakMsg(self, "1"); if(!posture.isStanding(self)) { // PROFILER_STOP("creature_combat.move"); return; } //debugSpeakMsg(self, "2"); if(!ai_lib.isWithinLeash(self)) { stopCombat(self); // clog("move() stopping combat with stopCombat(self)"); // PROFILER_STOP("creature_combat.move"); return; } //debugSpeakMsg(self, "3"); deltadictionary dict = self.getScriptVars(); setLookAtTarget(self, target); const obj_id weapon = getCurrentWeapon(self); const range_info weaponRange = aiGetWeaponRangeInfo(weapon); const float distanceToTarget = getDistance(self, target); const int weaponCategory = combat.getWeaponCategory(getWeaponType(weapon)); const boolean targetCovered = stealth.hasInvisibleBuff(target); // @TODO: Make sure and have a check somewhere that the ranges are not inverted. if(!canSee(self, target)) { ai_combat_movement.aiMoveToSee(target); //debugSpeakMsg(self, "4"); // PROFILER_STOP("creature_combat.move"); return; } if((weaponCategory == combat.RANGED_WEAPON) && (combat.getWeaponCategory(getWeaponType(aiGetPrimaryWeapon(self))) != combat.MELEE_WEAPON)) { if((distanceToTarget < TOO_CLOSE_DISTANCE) && isPlayer(target)) { ai_combat_movement.aiEvade(target, self, weaponRange, 1f, 1f); // PROFILER_STOP("creature_combat.move"); return; } else if(!ai_combat_movement.aiIsFleeing(self)); { const float midRange = (weaponRange.maxRange - weaponRange.minRange) / 2.0f; const float rangeVariation = midRange * 0.2f; const float minFollowDistance = (weaponRange.minRange + midRange - rangeVariation); const float maxFollowDistance = (weaponRange.minRange + midRange + rangeVariation); // clog("move() aiFollow target: " + target + " mindist: " + minFollowDistance + " maxdist: " + maxFollowDistance); ai_combat_movement.aiFollow(target, minFollowDistance, maxFollowDistance); faceTo(self, target); } } else // MELEE WEAPON { float minFollowDistance = 1.5f; float maxFollowDistance = 3.0f; if(!hasObjVar(self, "ai.noFollow")) { ai_combat_movement.aiFollow(target, minFollowDistance, maxFollowDistance); } else { if(distanceToTarget > 64) { stopCombat(self); resumeMovement(self); // PROFILER_STOP("creature_combat.move"); return; } else { suspendMovement(self); } } faceTo(self, target); } //debugSpeakMsg(self, "5"); ai_combat_movement.aiEvade(target, self, weaponRange, MIN_MOVEMENT_DURING_COMBAT, MAX_MOVEMENT_DURING_COMBAT); // PROFILER_STOP("creature_combat.move"); } void selectWeapon(obj_id target) { //@optimize: Only weapon switch every second. const obj_id self = getSelf(); // Beasts do not switch weapons. if(beast_lib.isBeast(self)) { return; } if(!aiHasPrimaryWeapon(self)) { // Can I just do a WARNING here? //Thread.dumpStack(); } // @TODO: Make sure grenades work for AI // @TODO: Evaluate the weapon of the primary target if(!aiHasSecondaryWeapon(self)) { aiEquipPrimaryWeapon(self); return; } boolean equipPrimary = true; const float distanceToTarget = getDistance(self, target); const obj_id primaryWeapon = aiGetPrimaryWeapon(self); const obj_id secondaryWeapon = aiGetSecondaryWeapon(self); if (!exists(primaryWeapon) || !exists(secondaryWeapon)) return; const range_info primaryWeaponRange = aiGetWeaponRangeInfo(primaryWeapon); const boolean primaryWeaponInRange = (distanceToTarget >= primaryWeaponRange.minRange) && (distanceToTarget <= primaryWeaponRange.maxRange); const range_info secondaryWeaponRange = aiGetWeaponRangeInfo(secondaryWeapon); const boolean secondaryWeaponInRange = (distanceToTarget >= secondaryWeaponRange.minRange) && (distanceToTarget <= secondaryWeaponRange.maxRange); // Rifle/Unarmed // Unarmed/Rifle // Rifle/Pistol // Pistol/Rifle // Pseudo-code // If I am using my primary weapon and // I have a secondary weapon // If my secondary weapon is in range // If my primary weapon is not in range // Switch to my secondary weapon // If my primary weapon has a larger range than my secondary weapon // Switch to my secondary weapon // If I am using my secondary weapon // and my primary weapon is in range // If my secondary weapon is not in range // Switch to my primary weapon // If my secondary weapon has a larger range than my primary weapon // Switch to my primary weapon if(aiUsingPrimaryWeapon(self) && aiHasSecondaryWeapon(self)) { if(secondaryWeaponInRange) { if(!primaryWeaponInRange) { aiEquipSecondaryWeapon(self); } else if(primaryWeaponRange.maxRange > secondaryWeaponRange.maxRange) { aiEquipSecondaryWeapon(self); } } } else if(aiUsingSecondaryWeapon(self) && primaryWeaponInRange) { if(!secondaryWeaponInRange) { aiEquipPrimaryWeapon(self); } else if(secondaryWeaponRange.maxRange > primaryWeaponRange.maxRange) { aiEquipPrimaryWeapon(self); } } } void attack(obj_id target) { // PROFILER_START("creature_combat.attack"); const obj_id self = getSelf(); string DEFAULT_ATTACK = combat.getAttackName(self); //LOGC(aiLoggingEnabled(self), "debug_ai", ("creature_combat::attack() self(" + self + ") target(" + target + ")")); String currentActionString = null; String forcedActionString = getStringObjVar(self, "ai.combat.forcedAction"); String pendingActionString = getStringObjVar(self, "ai.combat.pendingAction"); string oneShotActionString = getStringObjVar(self, "ai.combat.oneShotAction"); if(oneShotActionString != null && !hasObjVar(self, "oneShotActionComplete")) { forcedActionString = oneShotActionString; setObjVar(self, "oneShotActionComplete", 1); } else { if(oneShotActionString != null) { // PROFILER_STOP("creature_combat.attack"); return; } } int pendingActionTime = getIntObjVar(self, "ai.combat.pendingActionTime"); currentActionString = DEFAULT_ATTACK; // Determine the desired combat action this frame clog("attack() pre-check forcedActionString: " + forcedActionString + " pendingActionString: " + pendingActionString + (pendingActionTime > 0 ? " pendingActionTime: " + (getGameTime() - pendingActionTime): "")); if(forcedActionString != null) { currentActionString = forcedActionString; } else if(pendingActionString != null) { if(getGameTime() - pendingActionTime < 2) { currentActionString = pendingActionString; } else { removeObjVar(self, "ai.combat.pendingAction"); removeObjVar(self, "ai.combat.pendingActionTime"); currentActionString = DEFAULT_ATTACK; } } else { pendingActionString = aiGetCombatAction(self); if(pendingActionString != null) { currentActionString = pendingActionString; // Store the pending action in case we need to re-attempt it next frame setObjVar(self, "ai.combat.pendingAction", pendingActionString); setObjVar(self, "ai.combat.pendingActionTime", getGameTime()); } else { currentActionString = DEFAULT_ATTACK; } } clog("attack() post-check currentActionString: " + currentActionString + " pendingActionString: " + pendingActionString); int currentActionCrc = 0; // Determine if we can perform the desired combat action const int result = combat.canPerformAction(currentActionString, self); clog("attack() post-check currentActionString: " + currentActionString + " pendingActionString: " + pendingActionString + " canPerformAction: " + result); if(result == combat.ACTION_SUCCESS || oneShotActionString != null) { currentActionCrc = getStringCrc(currentActionString.toLowerCase()); } else if(result == combat.ACTION_TOO_TIRED) { // If there is a forced action, do not use the default attack when the // AI is too tired to do the current action if(forcedActionString != null) { clog("attack() self(" + self + ") target(" + target + ") ACTION(" + forcedActionString + ") TOO TIRED - WAITING TO ATTEMPT FORCED ACTION"); } else { if(pendingActionString != null) { clog("attack() self(" + self + ") target(" + target + ") ACTION(" + pendingActionString + ") TOO TIRED - ATTEMPTING DEFAULT ATTACK"); } if(currentActionString != DEFAULT_ATTACK) { // If the action failed, the fallback is to attempt a default action until // enough action is available to attempt the requested action. if(combat.canPerformAction(DEFAULT_ATTACK, self) == combat.ACTION_SUCCESS) { currentActionString = DEFAULT_ATTACK; currentActionCrc = getStringCrc(DEFAULT_ATTACK.toLowerCase()); } } } } else if(result == combat.ACTION_INVALID_WEAPON) { // This is somehow a bad action and we need to fix the data clearCombatActions(); clog("attack() ERROR self(" + self + getName(self) + ") ACTION(" + currentActionString + ") weapon(" + (aiUsingPrimaryWeapon(self) ? "primary weapon" : "secondary weapon") + ") INVALID WEAPON FOR ACTION, FIX THE DATA"); } else { // This is somehow a bad action and we need to fix the data clearCombatActions(); clog("attack() ERROR self(" + self + getName(self) + ") ACTION(" + currentActionString + ") UNEXPECTED RESULT FROM combat.canPerformAction()"); } // If we have a valid attack, lets do it if(currentActionCrc != 0) { combat_data cd = combat_engine.getCombatData(currentActionString); if(cd == null) { // PROFILER_STOP("creature_combat.attack"); return; } int groupCrc = getStringCrc(cd.cooldownGroup); const float coolDownLeft = getCooldownTimeLeft(self, groupCrc); if(coolDownLeft <= 0.0f) { const float distanceToTarget = getDistance(self, target); const range_info weaponRange = aiGetWeaponRangeInfo(getCurrentWeapon(self)); if(distanceToTarget < weaponRange.maxRange || distanceToTarget < cd.maxRange) { if(canSee(self, target)) { clog("attack() self(" + self + ") target(" + target + ") ACTION(" + currentActionCrc + ":" + currentActionString + ")"); queueCommand(self, currentActionCrc, target, "", COMMAND_PRIORITY_DEFAULT); if(currentActionString == forcedActionString) { removeObjVar(self, "ai.combat.forcedAction"); } else if(currentActionString == pendingActionString) { removeObjVar(self, "ai.combat.pendingAction"); } } } } } // PROFILER_STOP("creature_combat.attack"); } trigger OnCreatureDamaged(obj_id attacker, obj_id weapon, int[] damage) { int lastFrame = utils.getIntScriptVar(self, "creature_combat.lastCombatFrame"); if (!stealth.hasInvisibleBuff(attacker)) ai_combat_damage.addAttack(attacker); // TODO: This is a temporary fix as of 4/30/2007 to keep creatures/npc's fighting in combat, due to the combat loop not firing. if(getGameTime() - lastFrame > 2) { clog("OnCreatureDamaged() !!!COMBAT STUCK!!! lastFrame: " + lastFrame + " time elapsed: " + (getGameTime() - lastFrame)); doCombatFrame(); } //Changed from a one time call to a persisted threat add on damage taken. ai_lib.triggerAgroLinks(self, attacker); if(!isPlayer(attacker) && isMob(attacker)) { obj_id master = getMaster(attacker); if(isIdValid(master) && exists(master)) { attacker = master; } } if(isIdValid(attacker) && exists(attacker) && isPlayer(attacker) && hasObjVar(self, "requirePlayerSpecialForces") && (pvpGetType(attacker) != PVPTYPE_DECLARED)) { if(pvpNeutralIsMercenaryDeclared(attacker)) { return SCRIPT_CONTINUE; } if(!utils.hasScriptVar(attacker, "gcw.specialForcesWarningTime")) { sui.msgbox(attacker, attacker, "@spam:warn_special_forces_prompt", sui.OK_ONLY, "@spam:warn_special_forces_title", "handleNothing"); utils.setScriptVar(attacker, "gcw.specialForcesWarningTime", getGameTime()); } else { int warningTime = utils.getIntScriptVar(attacker, "gcw.specialForcesWarningTime"); int timeDelta = getGameTime() - warningTime; if(timeDelta > 30) { if(timeDelta > 90) { sui.msgbox(attacker, attacker, "@spam:warn_special_forces_prompt", sui.OK_ONLY, "@spam:warn_special_forces_title", "handleNothing"); utils.setScriptVar(attacker, "gcw.specialForcesWarningTime", getGameTime()); } else { if(0 != factions.pvpGetAlignedFaction(attacker)) { factions.goOvertWithDelay(attacker, 0.0f); } else { int currentMercenaryFaction = factions.pvpNeutralGetMercenaryFaction(attacker); if (##"imperial" == currentMercenaryFaction) { pvpNeutralSetMercenaryFaction(attacker, currentMercenaryFaction, true); } if (##"rebel" == currentMercenaryFaction) { pvpNeutralSetMercenaryFaction(attacker, currentMercenaryFaction, true); } } utils.removeScriptVar(attacker, "gcw.specialForcesWarningTime"); } } } } return SCRIPT_CONTINUE; } trigger OnChangedPosture(int oldPosture, int newPosture) { if(newPosture == POSTURE_KNOCKED_DOWN) { const deltadictionary dict = self.getScriptVars(); dict.put("ai.combat.knockdown_time", getGameTime()); } return SCRIPT_CONTINUE; } messageHandler vocalizeEndCombat() { return SCRIPT_CONTINUE; } messageHandler postCombatPathHome() { if(ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } if(pet_lib.isPet(self) || beast_lib.isBeast(self)) { return SCRIPT_CONTINUE; } if(ai_lib.isFollowing(self)) { ai_lib.resumeFollow(self); } else { pathTo(self, aiGetHomeLocation(self)); } return SCRIPT_CONTINUE; } boolean isNearLair(obj_id npc) { obj_id myLair = getObjIdObjVar(npc, "npc_lair.target"); if(!hasObjVar(npc, "lairSpawn") && !isIdValid(myLair)) { return false; } if(!isIdValid(myLair)) { myLair = getObjIdObjVar(npc, "poi.baseObject"); } if(isIdValid(myLair) && isInWorld(myLair) && exists(myLair)) { if(getDistance(npc, myLair) < 40f) { return true; } } return false; } void killPlayer(obj_id npc, obj_id target) { if(!isIdValid(target) || !isIncapacitated(target) || !isInWorld(target) || !exists(target) || ai_lib.isInCombat(npc)) { return; } obj_id weapon = getCurrentWeapon(npc); int weaponType = getWeaponType(weapon); int weaponCat = combat.getWeaponCategory(weaponType); float range = getDistance(npc, target); if(range > 2.0f) { setObjVar(npc, "ai.pathingToKill", target); ai_lib.pathNear(npc, getLocation(target), 2f); return; } string skeleton = dataTableGetString("datatables/ai/species.iff", ai_lib.aiGetSpecies(npc), "Skeleton"); if(skeleton != null && !skeleton.equals("human")) { doAnimationAction(npc, "eat"); pclib.coupDeGrace(target, npc); } else { pclib.coupDeGrace(target, npc, true); } } trigger OnFleeTargetLost(obj_id target) { if(!ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } clog("OnFleeTargetLost() self(" + self + ":" + getName(self) + ") target(" + target + ")"); stopCombat(self); return SCRIPT_OVERRIDE; } trigger OnFleeWaypoint(obj_id target) { if(!ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } return SCRIPT_OVERRIDE; } trigger OnFleePathNotFound(obj_id target) { if(!ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } clog("OnFleePathNotFound() self(" + self + ":" + getName(self) + ") target(" + target + ")"); stopCombat(self); return SCRIPT_OVERRIDE; } trigger OnFollowWaiting(obj_id target) { //debugSpeakMsg(self, "Follow Waiting"); if(!ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } if(utils.hasScriptVar(self, "ai.combat.pathingToHeal")) { //debugSpeakMsg(self, "i was pathing to heal"); obj_id healingTarget = utils.getObjIdScriptVar(self, "ai.combat.pathingToHeal"); //debugSpeakMsg(self, "healing " + getName(healingTarget)); ai_combat.executeHealingMove(self, healingTarget); return SCRIPT_OVERRIDE; } faceTo(self, target); return SCRIPT_OVERRIDE; } trigger OnFollowMoving(obj_id followTarget) { if(!ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } //clog("OnFollowMoving() self(" + self + ")"); //if moving to heal, then don't do anything if(utils.hasScriptVar(self, "ai.combat.pathingToHeal")) { return SCRIPT_CONTINUE; } return SCRIPT_OVERRIDE; } trigger OnFollowTargetLost(obj_id target) { if(!ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } clog("OnFollowTargetLost() self(" + self + ":" + getName(self) + ") target(" + target + ")"); stopCombat(self); return SCRIPT_OVERRIDE; } trigger OnFollowPathNotFound(obj_id target) { if(!ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } clog("OnFollowPathNotFound() self(" + self + ":" + getName(self) + ") target(" + target + ")"); stopCombat(self); return SCRIPT_OVERRIDE; } trigger OnMovePathComplete() { if(!ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } clog("OnMovePathComplete() self(" + self + ") The AI is dead."); if(ai_combat_movement.aiIsMovingToSee()) { ai_combat_movement.aiClearMoveMode(); ai_combat_movement.aiMoveToSee(ai_combat_movement.aiGetMoveToSeeTarget()); } else if(ai_combat_movement.aiIsMovingToDeathBlow()) { // Need to make sure the target is incapped and valid //killPlayer(self, ai_combat_movement.aiGetMoveToDeathBlowTarget(self)); } return SCRIPT_CONTINUE; } trigger OnMoveMoving() { if(!ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } clog("OnMoveMoving() self(" + self + ") The AI is dead."); return SCRIPT_OVERRIDE; } trigger OnMovePathNotFound() { if(!ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } if(ai_combat_movement.aiIsMovingToSee()) { const obj_id target = ai_combat_movement.aiGetMoveToSeeTarget(); clog("OnMovePathNotFound() self(" + self + ") The AI was moving to see(" + target + ":" + getName(target) + ") and something went wrong so forcing tether."); stopCombat(self); } else if(ai_combat_movement.aiIsMovingToDeathBlow()) { // Need to make sure the target is incapped and valid //killPlayer(self, ai_combat_movement.aiGetMoveToDeathBlowTarget(self)); } return SCRIPT_OVERRIDE; } messageHandler handleMoveRandomClear() { return SCRIPT_CONTINUE; } messageHandler handleTaunt() { if(ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } obj_id target = params.getObjId("player"); if(!isIdValid(target)) { return SCRIPT_CONTINUE; } utils.setScriptVar(self, "ai.combat.isTaunted", (getGameTime() + 20)); //debugSpeakMsg(self, "taunted"); stop(self); //debugSpeakMsg(self, "setting target 1"); setTarget(self, target); return SCRIPT_CONTINUE; } int getConfusionDuration(obj_id npc, obj_id attacker) { if(getDistance(npc, attacker) < 10.0f) { return 0; } int attackerPosture = getPosture(attacker); int duration = 0; if(attackerPosture == POSTURE_CROUCHED) { duration = 5; } else if(attackerPosture == POSTURE_PRONE) { duration = 10; } if(!ai_lib.isMonster(npc)) { return duration; } boolean masked = false; if(scout.isScentMasked(attacker, npc)) { masked = true; duration += 7; } return duration; } trigger OnDestroy() { clog("OnDestroy() unit(" + self + ":" + getName(self) + ")"); if(ai_lib.isAiDead(self)) { return SCRIPT_CONTINUE; } int combatDestructionDelay = 0; if(pet_lib.isPet(self) || beast_lib.isBeast(self)) { return SCRIPT_CONTINUE; } //currently in combat, so try again much later: if(ai_lib.isInCombat(self)) { combatDestructionDelay = getGameTime() + 30; utils.setScriptVar(self, "combatDestructionDelay", combatDestructionDelay); messageTo(self, "handleCombatDestructionDelay", null, 60, false);//try again in 60 return SCRIPT_OVERRIDE; } if(!utils.hasScriptVar(self, "combatDestructionDelay")) { return SCRIPT_CONTINUE; } else { combatDestructionDelay = utils.getIntScriptVar(self, "combatDestructionDelay"); } if(getGameTime() > combatDestructionDelay) { return SCRIPT_CONTINUE;//good to destroy } messageTo(self, "handleCombatDestructionDelay", null, 30, false);//try again in 30 return SCRIPT_OVERRIDE; } messageHandler handleCombatDestructionDelay() { destroyObject(self); return SCRIPT_CONTINUE; } messageHandler OnDotPulse() { clog("OnDotPulse() self(" + self + ":" + getName(self) + ")"); if(ai_lib.isInCombat(self)) { return SCRIPT_CONTINUE; } if(ai_lib.isAiDead(self)) { return SCRIPT_CONTINUE; } if(pet_lib.isPet(self) || beast_lib.isBeast(self)) { return SCRIPT_CONTINUE; } obj_id[] players = pvpGetEnemiesInRange(self, self, 128.0f); if(players == null || players.length < 1) { return SCRIPT_CONTINUE; } for (int i=0; i < players.length; i++) { if(isIdValid(players[i]) && exists(players[i]) && players[i] != self) { startCombat(self, players[i]); return SCRIPT_CONTINUE; } } return SCRIPT_CONTINUE; } trigger OnIncapacitateTarget(obj_id target) { if(!isIdValid(target)) { clog("OnIncapacitateTarget() self(" + self + ":" + getName(self) + ") target(" + target + ":" + getName(target) + ") INVALID TARGET"); return SCRIPT_CONTINUE; } if(!isPlayer(target) && !pet_lib.isPet(target) && !beast_lib.isBeast(target)) { clog("OnIncapacitateTarget() self(" + self + ":" + getName(self) + ") target(" + target + ":" + getName(target) + ") TARGET IS NON-PLAYER"); return SCRIPT_CONTINUE; } if(aiIsKiller(self)) { //debugSpeakMsgc(aiLoggingEnabled(self), self, "DEATH BLOW(" + target + ")"); if(pet_lib.isPet(target)) { clog("OnIncapacitateTarget() self(" + self + ":" + getName(self) + ") target(" + target + ":" + getName(target) + ") DEATH BLOW PET"); pet_lib.killPet(target); } else if(beast_lib.isBeast(target)) { clog("OnIncapacitateTarget() self(" + self + ":" + getName(self) + ") target(" + target + ":" + getName(target) + ") DEATH BLOW BEAST"); beast_lib.killBeast(target, self); } else { clog("OnIncapacitateTarget() self(" + self + ":" + getName(self) + ") target(" + target + ":" + getName(target) + ") DEATH BLOW PLAYER"); pclib.coupDeGrace(target, self); } } return SCRIPT_CONTINUE; } trigger OnIncapacitated(obj_id killer) { obj_id[] myHateList = getHateList(self); if (myHateList == null || myHateList.length == 0) return SCRIPT_CONTINUE; for (int i=0;i