mirror of
https://bitbucket.org/seefoe/dsrc.git
synced 2026-07-31 01:15:55 -04:00
1727 lines
41 KiB
Plaintext
1727 lines
41 KiB
Plaintext
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<myHateList.length;i++)
|
|
{
|
|
removeHateTarget(self, myHateList[i]);
|
|
removeHateTarget(myHateList[i], self);
|
|
}
|
|
|
|
return SCRIPT_CONTINUE;
|
|
}
|
|
messageHandler handleAggroCheck()
|
|
{
|
|
const obj_id target = params.getObjId("target");
|
|
|
|
if(!isIdValid(target) || !exists(target) || isDead(target))
|
|
return SCRIPT_CONTINUE;
|
|
|
|
ai_aggro.stopAttemptingAggroCheck(target);
|
|
ai_aggro.requestAggroCheck(target);
|
|
|
|
return SCRIPT_CONTINUE;
|
|
}
|
|
|
|
messageHandler handleAggroStart()
|
|
{
|
|
if(beast_lib.isBeast(self))
|
|
{
|
|
return SCRIPT_CONTINUE;
|
|
}
|
|
|
|
const obj_id target = params.getObjId("target");
|
|
|
|
if(isIdValid(target) && exists(target) && !isDead(target))
|
|
{
|
|
if(!stealth.hasServerCoverState(target) && canSee(self, target))
|
|
{
|
|
startCombatWithAssist(self, target);
|
|
}
|
|
}
|
|
|
|
return SCRIPT_CONTINUE;
|
|
}
|
|
|
|
messageHandler handleSawRecapacitation()
|
|
{
|
|
clog("handleSawRecapacitation() self(" + self + ":" + getName(self) + ")");
|
|
|
|
const obj_id player = params.getObjId("player");
|
|
|
|
stopListeningToMessage(player, "handleSawRecapacitation");
|
|
|
|
return SCRIPT_CONTINUE;
|
|
}
|
|
|
|
void clearCombatActions()
|
|
{
|
|
obj_id self = getSelf();
|
|
|
|
removeObjVar(self, "ai.combat.forcedAction");
|
|
removeObjVar(self, "ai.combat.pendingAction");
|
|
}
|
|
|
|
messageHandler checkForSpecials()
|
|
{
|
|
if (!verifyMessage(self, params))
|
|
return SCRIPT_CONTINUE;
|
|
|
|
string bestBeastAbility = beast_lib.getBestAutoRepeatAbility(self);
|
|
|
|
obj_id target = getTarget(self);
|
|
|
|
if (!isIdValid(target))
|
|
{
|
|
return SCRIPT_CONTINUE;
|
|
}
|
|
|
|
if (bestBeastAbility != "")
|
|
{
|
|
queueCommand(getMaster(self), getStringCrc(bestBeastAbility), target, "", COMMAND_PRIORITY_DEFAULT);
|
|
}
|
|
|
|
messageTo(self, "checkForSpecials", stampMessage(self), 1.0f, false);
|
|
|
|
return SCRIPT_CONTINUE;
|
|
}
|
|
|
|
dictionary stampMessage(obj_id self)
|
|
{
|
|
int session = getMessageSession(self);
|
|
utils.setScriptVar(self, "messageStamp", session);
|
|
dictionary dict = new dictionary();
|
|
dict.put("sessionId", session);
|
|
|
|
return dict;
|
|
}
|
|
|
|
int getMessageSession(obj_id self)
|
|
{
|
|
return utils.hasScriptVar(self, "messageStamp") ? utils.getIntScriptVar(self, "messageStamp") : 0;
|
|
}
|
|
|
|
boolean verifyMessage(obj_id self, dictionary params)
|
|
{
|
|
int messageId = params.getInt("sessionId");
|
|
int currentId = utils.getIntScriptVar(self, "messageStamp");
|
|
return messageId == currentId;
|
|
}
|
|
|
|
void bumpSession(obj_id self)
|
|
{
|
|
int session = getMessageSession(self);
|
|
session++;
|
|
utils.setScriptVar(self, "messageStamp", session);
|
|
}
|
|
|
|
//We have to fire off this ultra lame messageTo because in multimob encounters the exitedcombat and enteredcombat fire in the same frame
|
|
//leading to our stamps being out of sync
|
|
messageHandler checkForBeastSpecialsTakeOne()
|
|
{
|
|
if(beast_lib.isBeast(self))
|
|
{
|
|
messageTo(self, "checkForSpecials", stampMessage(self), 1.0f, false);
|
|
}
|
|
return SCRIPT_CONTINUE;
|
|
}
|
|
|
|
messageHandler persist_combat()
|
|
{
|
|
if (!trial.verifySession(self, params, combat.PERSIST_COMBAT))
|
|
return SCRIPT_CONTINUE;
|
|
|
|
if (!combat.isValidPersistCombat(self))
|
|
{
|
|
return SCRIPT_CONTINUE;
|
|
}
|
|
|
|
resetHateTimer(self);
|
|
|
|
messageTo(self, "persist_combat", trial.getSessionDict(self, combat.PERSIST_COMBAT), 5.0f, false);
|
|
return SCRIPT_CONTINUE;
|
|
} |