862 lines
31 KiB
Plaintext
862 lines
31 KiB
Plaintext
/***
|
|
Balkoth's Minion Control v2_01 (BMC_v2_01)
|
|
|
|
- Allows some direction of henchmen, summons, animal companions, and familiars
|
|
- Should be attached to an instant feat like the player tools or an
|
|
item's activate item (long range)
|
|
- If targeted on a hostile creature, commands all minions to attack that creature
|
|
- If targeted on a location, commands all minions to move to that location
|
|
- If targeted on a friendly/neutral creature, commands all minions to follow
|
|
that creature.
|
|
- If targeted on a locked door/object, commands any minions designated to help
|
|
with locked doors/objects to try to open the lock (including bashing if necessary)
|
|
- If targeted on a trapped door/object/trigger, commands any minions designated
|
|
to help with traps to attempt to disarm the trap (including triggering the trap
|
|
if necessary)
|
|
|
|
Installation is fairly simple with a short checklist:
|
|
|
|
1. Replace Player Tool 1 (x3_pl_tool01) with this file (or if you don't want to
|
|
use a player tool, then set it up so this script is called with however you're
|
|
calling it -- though I'd strongly suggest something instant like a player tool)
|
|
2. Ensure the players actually HAVE the player tool or whatever you're using
|
|
to call this script
|
|
3. Copy/paste four lines of code to the start of five events (if your henchmen
|
|
use different scripts than summons/animal companions/familiars then you'll need
|
|
to do this part for each set of scripts)
|
|
4. Copy/paste a unique segment of code to the start of OnCombatRoundEnd (again, this
|
|
may need to be done multiple times if you have multiple OnCombatRoundEnd scripts)
|
|
5. Copy/paste a unique segment of code to the start of OnDialogue (again, this may
|
|
need to be done multiple times if you have multiple OnDialogue scripts)
|
|
6. Nothing else, you're done!
|
|
|
|
IMPORTANT NOTE!!!
|
|
IMPORTANT NOTE!!!
|
|
IMPORTANT NOTE!!!
|
|
|
|
If you ever want to force a cancellation of this tool (for the purposes of a cutscene
|
|
or something), then simply call the following line on each minion (oMinion being the
|
|
minion in question):
|
|
|
|
SetLocalInt(oMinion, "bmc_active", 0)
|
|
|
|
The exact method doesn't matter as long as the minon has the local int "bmc_active"
|
|
set to 0. That serves as a kill switch in effect.
|
|
|
|
END IMPORTANT NOTE!!!
|
|
|
|
Let's begin!
|
|
|
|
1. This is straightforward if you're replacing a player tool script. Otherwise,
|
|
you'll have to do...whatever it is you're doing. That's rather hard for me to guess,
|
|
but if you need help you can contact me via the email listed at the bottom of this
|
|
header section.
|
|
|
|
2. If you're not sure how to grant access to Player Tool 1 to players, then assuming
|
|
your module ensures PCs have creature skins you can place the following code segment
|
|
into your module's Heartbeat:
|
|
|
|
object oPC = GetFirstPC();
|
|
while (GetIsObjectValid(oPC))
|
|
{
|
|
if (!GetHasFeat(FEAT_PLAYER_TOOL_01, oPC))
|
|
{
|
|
object oHide = GetItemInSlot(INVENTORY_SLOT_CARMOUR, oPC);
|
|
|
|
if (GetIsObjectValid(oHide))
|
|
{
|
|
FloatingTextStringOnCreature("Player Tool 1 acquired. Use this to direct companions.", oPC, FALSE);
|
|
AddItemProperty(DURATION_TYPE_PERMANENT, ItemPropertyBonusFeat(IP_CONST_FEAT_PLAYER_TOOL_01), oHide);
|
|
}
|
|
}
|
|
oPC = GetNextPC();
|
|
}
|
|
|
|
This will loop through all PCs and attempt to give them the player tool if they lack it.
|
|
A better way to handle it would be to use OnClientEnter or OnAreaEnter or something --
|
|
but then you also have to worry about timing issues (the code won't do anything if
|
|
the PC doesn't have a creature skin equipped yet). Ultimately it's your call -- and
|
|
again, I'm happy to help if you require assistance.
|
|
|
|
If your PCs don't have creature skins for whatever reason, you could either
|
|
adjust this code to spawn a creature skin if necessary or change it to the PC's armor
|
|
or something I guess. Again, this is very module dependent so hard for me to be precise.
|
|
|
|
3. In order to avoid things like telling the minion to move to a location and then
|
|
having it decide to attack something instead, the AI needs to be disabled.
|
|
You will need to put the following lines of code in five creature events. I suggest
|
|
starting by placing the code right at the start of main for each event and then
|
|
consulting the ADVANCED TROUBLESHOOTING section below if you notice any oddities.
|
|
|
|
// Don't do anything if we have have been recently commanded
|
|
if (GetLocalInt(OBJECT_SELF, "bmc_active"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
This literally just tells the AI to not do anything new if we've been issued an order.
|
|
|
|
The five creature events are listed below (along with the corresponding default
|
|
script for summons/familiars/animal companions -- NPC companions will usually
|
|
have different scripts)
|
|
|
|
OnHeartbeat (nw_ch_ac1)
|
|
OnPerceive (nw_ch_ac2)
|
|
OnAttacked (nw_ch_ac5)
|
|
OnDamaged (nw_ch_ac6)
|
|
OnSpellCastAt (nw_ch_acb)
|
|
|
|
4. Great, we've disabled the AI in some cases. Unfortunately, we WANT the AI to respond to
|
|
some things still -- such as healing itself if injured and using spells properly. So we
|
|
need to do something slightly different for
|
|
|
|
OnCombatRoundEnd (nw_c2_ac3)
|
|
|
|
As the name suggests, this is called at the end of each round to determine what to do next.
|
|
We basically want to rerun part of the AI without changing targets, so we put the following
|
|
block of code at the start of the event (the first lines exist to fix Bioware bugs):
|
|
|
|
// Fixes some Bioware bugs
|
|
object oBiowareBash = GetLocalObject(OBJECT_SELF, "NW_GENERIC_DOOR_TO_BASH");
|
|
if (GetIsObjectValid(oBiowareBash))
|
|
{
|
|
// This happens if we have a door transition that was destroyed
|
|
if (GetCurrentHitPoints(oBiowareBash) <= 0)
|
|
{
|
|
ResetHenchmenState();
|
|
}
|
|
else
|
|
{
|
|
// Just use feats as appropriate rather than having to damage the
|
|
// object first (default Bioware behavior)
|
|
if(GetHasFeat(FEAT_IMPROVED_POWER_ATTACK))
|
|
{
|
|
ActionUseFeat(FEAT_IMPROVED_POWER_ATTACK, oBiowareBash);
|
|
return;
|
|
}
|
|
else if(GetHasFeat(FEAT_POWER_ATTACK))
|
|
{
|
|
ActionUseFeat(FEAT_POWER_ATTACK, oBiowareBash);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
ActionAttack(oBiowareBash);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't do anything if we have have been recently commanded
|
|
if (GetLocalInt(OBJECT_SELF, "bmc_active"))
|
|
{
|
|
// See if we have a valid creature to attack
|
|
object oTarget = GetLocalObject(OBJECT_SELF, "bmc_attacktarget");
|
|
if (GetIsObjectValid(oTarget) && GetIsEnemy(oTarget) && !GetIsDead(oTarget))
|
|
{
|
|
DetermineCombatRound(oTarget);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
DeleteLocalObject(OBJECT_SELF, "bmc_attacktarget");
|
|
}
|
|
|
|
oTarget = GetAttackTarget();
|
|
if (GetIsObjectValid(oTarget) && GetIsEnemy(oTarget) && !GetIsDead(oTarget))
|
|
{
|
|
DetermineCombatRound(oTarget);
|
|
return;
|
|
}
|
|
|
|
oTarget = GetAttemptedAttackTarget();
|
|
if (GetIsObjectValid(oTarget) && GetIsEnemy(oTarget) && !GetIsDead(oTarget))
|
|
{
|
|
DetermineCombatRound(oTarget);
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
Now if we're attacking a target or trying to attack a target, we'll check for things like
|
|
self-healing. If we're doing anything else, then we'll trust it was triggered by this script
|
|
since the normal AI is disabled and thus not interfere.
|
|
|
|
5. We've disabled the bulk of the AI except for some specific things we want to check. But what
|
|
if the PLAYER wants to order the minions around with the voice lines (things like
|
|
Attack Nearest, Follow Me, and so on)? That's handled in
|
|
|
|
OnDialogue (nw_ch_ac4)
|
|
|
|
We need a new chunk of code to cancel our AI override in response to certain shouts (but
|
|
still ignore other shouts at times):
|
|
|
|
object oMinionMaster = GetMaster(), oMinionShouter = GetLastSpeaker();
|
|
int nMinionMatch = GetListenPatternNumber();
|
|
|
|
if(GetIsObjectValid(oMinionShouter) && oMinionMaster == oMinionShouter)
|
|
{
|
|
switch (nMinionMatch)
|
|
{
|
|
// These are orders we want to re-enable AI for
|
|
case ASSOCIATE_COMMAND_ATTACKNEAREST:
|
|
case ASSOCIATE_COMMAND_FOLLOWMASTER:
|
|
case ASSOCIATE_COMMAND_GUARDMASTER:
|
|
case ASSOCIATE_COMMAND_STANDGROUND:
|
|
case ASSOCIATE_COMMAND_HEALMASTER:
|
|
case ASSOCIATE_COMMAND_PICKLOCK:
|
|
case ASSOCIATE_COMMAND_DISARMTRAP:
|
|
case ASSOCIATE_COMMAND_MASTERFAILEDLOCKPICK:
|
|
case ASSOCIATE_COMMAND_LEAVEPARTY:
|
|
DeleteLocalObject(OBJECT_SELF, "bmc_attacktarget");
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
break;
|
|
|
|
// These are orders we want to ignore
|
|
case ASSOCIATE_COMMAND_MASTERGOINGTOBEATTACKED:
|
|
case ASSOCIATE_COMMAND_MASTERATTACKEDOTHER:
|
|
case ASSOCIATE_COMMAND_MASTERUNDERATTACK:
|
|
if (GetLocalInt(OBJECT_SELF, "bmc_active"))
|
|
{
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
6. You're done! Enjoy! Ignore the rest of this header if you value your sanity!
|
|
|
|
*** ADVANCED TROUBLESHOOTING ***
|
|
|
|
I'll be blunt: this is a rough workaround designed for easy installation. I've sacrificed some
|
|
efficiency to make it easier for people who aren't programmers to get it working. This is minor
|
|
enough that it should NEVER matter, but before you come to me shouting about how this could be
|
|
more efficient and some stuff is processed twice please think of this from the perspective of
|
|
"How can I make this one code block people can just throw at the top of a main function?" If you
|
|
STILL think it can be improved I would happy to listen, but I am aware (for example) that some code
|
|
is getting duplicated for the OnDialogue code. Again, trying to make this something you can just
|
|
plop down start of main for each script without needing to know anything about how programming works.
|
|
|
|
This also has some other implications such as concentration checks for something like the Black Blade
|
|
of Disaster. Technically speaking the AI suppression should be placed AFTER the concentration check on
|
|
the Heartbeat script as an example. All that really matters is that you return BEFORE stuff like the
|
|
DetermineCombatRound calls if we're in a command state. But that requires people to be able to
|
|
read the code and figure out where to properly place it.
|
|
|
|
If you have custom behavior in these events, that may also cause problems if you avoid that part of the
|
|
code by returning from this AI suppression. Feel free to move the code segments down in the code --
|
|
as I just mentioned you don't HAVE to put them at the start of main, strictly speaking. Just keep them
|
|
before the main combat AI calls because you don't want your minion to change its mind about attacking
|
|
that wizard in the back just because the wizard's summoned badger tried to bite the minion.
|
|
|
|
If you're still having trouble getting this to work properly, feel free to email me at
|
|
|
|
balkothwarcraft@gmail.com
|
|
|
|
and I would be happy to assist you. I did a bunch of custom work for Aielund, as an example.
|
|
|
|
***/
|
|
|
|
// This is used to check whether we got removed from the party mid-command
|
|
object oOriginalMaster;
|
|
|
|
// Distance (in meters) that we consider to be "close enough" when moving
|
|
float fCloseEnough = 1.5;
|
|
|
|
// Necessary for DetermineCombatRound
|
|
#include "NW_I0_GENERIC"
|
|
|
|
// Necessary for opening locks and disarming traps
|
|
#include "x0_inc_henai"
|
|
|
|
// Aielund specific
|
|
//#include "hench_i0_strings"
|
|
|
|
// Handles command status stuff
|
|
int MakeUncommandable();
|
|
|
|
// Handles whether we're trying to start or in a conversation
|
|
int CheckConversation(object oTalker, int nAction);
|
|
|
|
// Regroups the party so no one misses any dialogue
|
|
void RegroupParty();
|
|
|
|
// Check if the creature is a minion directly or indirectly
|
|
int IsMinion(object oMinion);
|
|
|
|
// Handles attacking
|
|
void GroupAttack(object oTarget);
|
|
void SingleAttack(object oTarget);
|
|
void AttackAI(object oTarget, int nCommand, float fTracker, object oArea);
|
|
|
|
// Handles moving to creature
|
|
void GroupMoveToCreature(object oTarget);
|
|
void SingleMoveToCreature(object oTarget);
|
|
void TryToMoveToCreature(object oTarget, int nCommand, object oArea);
|
|
|
|
// Handles moving to a location
|
|
void GroupMoveToLocation(location lTarget);
|
|
void SingleMoveToLocation(location lTarget);
|
|
void TryToMoveToLocation(location lTarget, int nCommand);
|
|
|
|
// Handles unlocking (or bashing) something
|
|
void GroupUnlock(object oTarget);
|
|
void SingleUnlock(object oTarget);
|
|
void CheckLockStatus(object oTarget, int nCommand, object oArea);
|
|
|
|
// What if it's trapped?
|
|
void GroupDisarm(object oTarget);
|
|
void SingleDisarm(object oTarget);
|
|
void CheckDisarmStatus(object oTarget, int nCommand, int nDisarmActive, object oArea);
|
|
|
|
void main()
|
|
{
|
|
// This will need to be changed if an item is used instead of a player feat
|
|
oOriginalMaster = OBJECT_SELF;
|
|
|
|
// Find what we targeted
|
|
object oTarget = GetSpellTargetObject();
|
|
|
|
// If we have a valid target, use that location, otherwise get the location
|
|
location lTarget = GetLocation(oTarget);
|
|
if (oTarget == OBJECT_INVALID)
|
|
{
|
|
lTarget = GetSpellTargetLocation();
|
|
}
|
|
|
|
// If we're in a conversation, prevent new commands as it might
|
|
// break the dialogue if minions move around. Regroup party instead.
|
|
if (IsInConversation(oOriginalMaster))
|
|
{
|
|
FloatingTextStringOnCreature("In a conversation, regrouping party.", oOriginalMaster, FALSE);
|
|
RegroupParty();
|
|
return;
|
|
}
|
|
|
|
// Targeted on object
|
|
if (GetIsObjectValid(oTarget))
|
|
{
|
|
if (GetObjectType(oTarget) == OBJECT_TYPE_CREATURE)
|
|
{
|
|
// Attack enemies
|
|
if (GetIsEnemy(oTarget, oOriginalMaster))
|
|
{
|
|
GroupAttack(oTarget);
|
|
}
|
|
// Move to friendly/neutral targets
|
|
else
|
|
{
|
|
GroupMoveToCreature(oTarget);
|
|
}
|
|
}
|
|
else if (GetObjectType(oTarget) == OBJECT_TYPE_DOOR)
|
|
{
|
|
// Disarm trapped doors
|
|
if (GetTrapDetectedBy(oTarget, oOriginalMaster))
|
|
{
|
|
GroupDisarm(oTarget);
|
|
}
|
|
// Open locked doors
|
|
else if (GetLocked(oTarget))
|
|
{
|
|
GroupUnlock(oTarget);
|
|
}
|
|
else
|
|
{
|
|
FloatingTextStringOnCreature("That door is not locked or trapped.", oOriginalMaster, FALSE);
|
|
}
|
|
}
|
|
else if (GetObjectType(oTarget) == OBJECT_TYPE_ITEM)
|
|
{
|
|
// Not even sure what could be done with this
|
|
FloatingTextStringOnCreature("No functionality for items currently.", oOriginalMaster, FALSE);
|
|
}
|
|
else if (GetObjectType(oTarget) == OBJECT_TYPE_PLACEABLE)
|
|
{
|
|
// Disarm trapped doors
|
|
if (GetTrapDetectedBy(oTarget, oOriginalMaster))
|
|
{
|
|
GroupDisarm(oTarget);
|
|
}
|
|
// Open locked chests and such
|
|
else if (GetLocked(oTarget))
|
|
{
|
|
GroupUnlock(oTarget);
|
|
}
|
|
else
|
|
{
|
|
FloatingTextStringOnCreature("That object is not locked or trapped.", oOriginalMaster, FALSE);
|
|
}
|
|
}
|
|
else if (GetObjectType(oTarget) == OBJECT_TYPE_TRIGGER && GetTrapDetectedBy(oTarget, oOriginalMaster))
|
|
{
|
|
GroupDisarm(oTarget);
|
|
}
|
|
}
|
|
// Targeted on location
|
|
else
|
|
{
|
|
// Move to location
|
|
GroupMoveToLocation(lTarget);
|
|
}
|
|
}
|
|
|
|
int MakeUncommandable()
|
|
{
|
|
int nCommand = GetLocalInt(OBJECT_SELF, "bmc_value");
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 1);
|
|
|
|
// This progresses our bmc_value so we know whether the order is current
|
|
// The %10000 prevents it from potentially going out of bounds -- not sure how
|
|
// NWN handles that so let's be safe
|
|
nCommand = (++nCommand)%10000;
|
|
|
|
SetLocalInt(OBJECT_SELF, "bmc_value", nCommand);
|
|
|
|
return nCommand;
|
|
}
|
|
|
|
// Handles whether we're trying to start or in a conversation
|
|
int CheckConversation(object oTalker, int nAction)
|
|
{
|
|
return (nAction == ACTION_WAIT || IsInConversation(oTalker));
|
|
}
|
|
|
|
// Regroups the party so no one misses any dialogue
|
|
void RegroupParty()
|
|
{
|
|
// Going to loop through members of the party
|
|
object oMinion = GetFirstFactionMember(oOriginalMaster, FALSE);
|
|
while (GetIsObjectValid(oMinion))
|
|
{
|
|
// Regroup the party for a nice chat
|
|
if (IsMinion(oMinion))
|
|
{
|
|
SetLocalInt(oMinion, "bmc_active", 0);
|
|
|
|
if (!CheckConversation(oMinion, GetCurrentAction(oMinion)))
|
|
{
|
|
AssignCommand(oMinion, ClearAllActions(TRUE));
|
|
AssignCommand(oMinion, JumpToObject(oOriginalMaster));
|
|
}
|
|
}
|
|
oMinion = GetNextFactionMember(oOriginalMaster, FALSE);
|
|
}
|
|
}
|
|
|
|
// Check if the creature is a minion directly or indirectly
|
|
int IsMinion(object oMinion)
|
|
{
|
|
return (GetMaster(oMinion) == oOriginalMaster || GetMaster(GetMaster(oMinion)) == oOriginalMaster || GetMaster(GetMaster(GetMaster(oMinion))) == oOriginalMaster);
|
|
}
|
|
|
|
void GroupAttack(object oTarget)
|
|
{
|
|
// Going to loop through members of the party
|
|
object oMinion = GetFirstFactionMember(oOriginalMaster, FALSE);
|
|
while (GetIsObjectValid(oMinion))
|
|
{
|
|
// If the party member's master (or master's master for minion summons or
|
|
// even master's master's master) is the PC, issue orders to attack
|
|
if (IsMinion(oMinion))
|
|
{
|
|
AssignCommand(oMinion, SingleAttack(oTarget));
|
|
}
|
|
oMinion = GetNextFactionMember(oOriginalMaster, FALSE);
|
|
}
|
|
}
|
|
|
|
void SingleAttack(object oTarget)
|
|
{
|
|
int nCommand = MakeUncommandable();
|
|
|
|
ResetHenchmenState();
|
|
SetAssociateState(NW_ASC_MODE_DEFEND_MASTER, FALSE);
|
|
SetAssociateState(NW_ASC_MODE_STAND_GROUND, FALSE);
|
|
|
|
|
|
// Aielund specific, cancel peaceful follow mode
|
|
//if (GetLocalInt(OBJECT_SELF,"DoNotAttack"))
|
|
//{
|
|
// SetLocalInt(OBJECT_SELF,"DoNotAttack",FALSE);
|
|
// SpeakString(sHenchPeacefulModeCancel);
|
|
//}
|
|
|
|
SetLocalObject(OBJECT_SELF, "bmc_attacktarget", oTarget);
|
|
DetermineCombatRound(oTarget);
|
|
DelayCommand(0.5, AttackAI(oTarget, nCommand, 0.0, GetArea(OBJECT_SELF)));
|
|
}
|
|
|
|
void AttackAI(object oTarget, int nCommand, float fTracker, object oArea)
|
|
{
|
|
// Has our command value changed?
|
|
if (nCommand != GetLocalInt(OBJECT_SELF, "bmc_value") || !GetLocalInt(OBJECT_SELF, "bmc_active"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int nAction = GetCurrentAction();
|
|
if (CheckConversation(OBJECT_SELF, nAction) || IsInConversation(oOriginalMaster))
|
|
{
|
|
DeleteLocalObject(OBJECT_SELF, "bmc_attacktarget");
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
RegroupParty();
|
|
return;
|
|
}
|
|
|
|
if (GetIsDead(oTarget) || !GetIsEnemy(oTarget) || GetArea(oTarget) != oArea || GetIsDead(OBJECT_SELF) ||
|
|
(GetMaster() != oOriginalMaster && GetMaster(GetMaster()) != oOriginalMaster && GetMaster(GetMaster(GetMaster())) != oOriginalMaster))
|
|
{
|
|
DeleteLocalObject(OBJECT_SELF, "bmc_attacktarget");
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
return;
|
|
}
|
|
|
|
if (nAction == ACTION_ATTACKOBJECT)
|
|
{
|
|
if (fTracker >= 4.0)
|
|
{
|
|
DetermineCombatRound(oTarget);
|
|
|
|
if (GetCurrentAction() != ACTION_INVALID)
|
|
{
|
|
DelayCommand(0.5, AttackAI(oTarget, nCommand, 0.0, oArea));
|
|
}
|
|
else
|
|
{
|
|
ActionAttack(oTarget);
|
|
DelayCommand(0.5, AttackAI(oTarget, nCommand, fTracker + 0.5, oArea));
|
|
}
|
|
}
|
|
else if (fTracker >= 2.0)
|
|
{
|
|
DelayCommand(0.5, AttackAI(oTarget, nCommand, fTracker + 0.5, oArea));
|
|
}
|
|
else
|
|
{
|
|
DelayCommand(0.5, AttackAI(oTarget, nCommand, 0.0, oArea));
|
|
}
|
|
}
|
|
else if (nAction == ACTION_INVALID)
|
|
{
|
|
if (fTracker >= 1.5)
|
|
{
|
|
ActionAttack(oTarget);
|
|
DelayCommand(0.5, AttackAI(oTarget, nCommand, fTracker + 0.5, oArea));
|
|
}
|
|
else
|
|
{
|
|
DetermineCombatRound(oTarget);
|
|
DelayCommand(0.5, AttackAI(oTarget, nCommand, fTracker + 0.5, oArea));
|
|
}
|
|
}
|
|
else if (nAction == ACTION_FOLLOW)
|
|
{
|
|
ClearAllActions();
|
|
DetermineCombatRound(oTarget);
|
|
DelayCommand(0.5, AttackAI(oTarget, nCommand, fTracker + 0.5, oArea));
|
|
}
|
|
else
|
|
{
|
|
DelayCommand(0.5, AttackAI(oTarget, nCommand, 0.0, oArea));
|
|
}
|
|
}
|
|
|
|
void GroupMoveToLocation(location lTarget)
|
|
{
|
|
// Going to loop through members of the party
|
|
object oMinion = GetFirstFactionMember(oOriginalMaster, FALSE);
|
|
while (GetIsObjectValid(oMinion))
|
|
{
|
|
// If the party member's master (or master's master for minion summons or
|
|
// even master's master's master) is the PC, issue orders to move to the location
|
|
if (IsMinion(oMinion))
|
|
{
|
|
AssignCommand(oMinion, SingleMoveToLocation(lTarget));
|
|
}
|
|
oMinion = GetNextFactionMember(oOriginalMaster, FALSE);
|
|
}
|
|
}
|
|
|
|
void SingleMoveToLocation(location lTarget)
|
|
{
|
|
int nCommand = MakeUncommandable();
|
|
|
|
ResetHenchmenState();
|
|
SetAssociateState(NW_ASC_MODE_DEFEND_MASTER, FALSE);
|
|
SetAssociateState(NW_ASC_MODE_STAND_GROUND, FALSE);
|
|
DeleteLocalObject(OBJECT_SELF, "bmc_attacktarget");
|
|
|
|
TryToMoveToLocation(lTarget, nCommand);
|
|
}
|
|
|
|
void TryToMoveToLocation(location lTarget, int nCommand)
|
|
{
|
|
// Has our command value changed?
|
|
if (nCommand != GetLocalInt(OBJECT_SELF, "bmc_value") || !GetLocalInt(OBJECT_SELF, "bmc_active"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
float fDist = GetDistanceBetweenLocations(lTarget, GetLocation(OBJECT_SELF));
|
|
int nAction = GetCurrentAction();
|
|
if (CheckConversation(OBJECT_SELF, nAction) || IsInConversation(oOriginalMaster))
|
|
{
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
RegroupParty();
|
|
return;
|
|
}
|
|
|
|
// Are we in another area or dead or fired?
|
|
if (fDist < 0.0 || GetIsDead(OBJECT_SELF) || (GetMaster() != oOriginalMaster && GetMaster(GetMaster()) != oOriginalMaster && GetMaster(GetMaster(GetMaster())) != oOriginalMaster))
|
|
{
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
return;
|
|
}
|
|
|
|
// Only give the move command if we're too far away and not moving
|
|
if (fDist > fCloseEnough && nAction != ACTION_MOVETOPOINT)
|
|
{
|
|
ClearAllActions();
|
|
ActionMoveToLocation(lTarget, TRUE);
|
|
}
|
|
|
|
// Call again until we're there, bmc_value changes, or we time out
|
|
DelayCommand(0.5, TryToMoveToLocation(lTarget, nCommand));
|
|
}
|
|
|
|
void GroupMoveToCreature(object oTarget)
|
|
{
|
|
// Going to loop through members of the party
|
|
object oMinion = GetFirstFactionMember(oOriginalMaster, FALSE);
|
|
while (GetIsObjectValid(oMinion))
|
|
{
|
|
// If the party member's master (or master's master for minion summons or
|
|
// even master's master's master) is the PC, issue orders to follow the creature
|
|
if (IsMinion(oMinion))
|
|
{
|
|
AssignCommand(oMinion, SingleMoveToCreature(oTarget));
|
|
}
|
|
oMinion = GetNextFactionMember(oOriginalMaster, FALSE);
|
|
}
|
|
}
|
|
|
|
void SingleMoveToCreature(object oTarget)
|
|
{
|
|
int nCommand = MakeUncommandable();
|
|
|
|
ResetHenchmenState();
|
|
SetAssociateState(NW_ASC_MODE_DEFEND_MASTER, FALSE);
|
|
SetAssociateState(NW_ASC_MODE_STAND_GROUND, FALSE);
|
|
DeleteLocalObject(OBJECT_SELF, "bmc_attacktarget");
|
|
|
|
TryToMoveToCreature(oTarget, nCommand, GetArea(OBJECT_SELF));
|
|
}
|
|
|
|
void TryToMoveToCreature(object oTarget, int nCommand, object oArea)
|
|
{
|
|
// Has our command value changed?
|
|
if (nCommand != GetLocalInt(OBJECT_SELF, "bmc_value") || !GetLocalInt(OBJECT_SELF, "bmc_active"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int nAction = GetCurrentAction();
|
|
if (CheckConversation(OBJECT_SELF, nAction) || IsInConversation(oOriginalMaster))
|
|
{
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
RegroupParty();
|
|
return;
|
|
}
|
|
|
|
// Are we in another area or dead or fired?
|
|
if (GetArea(OBJECT_SELF) != oArea || GetIsDead(OBJECT_SELF) ||
|
|
(GetMaster() != oOriginalMaster && GetMaster(GetMaster()) != oOriginalMaster && GetMaster(GetMaster(GetMaster())) != oOriginalMaster))
|
|
{
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
return;
|
|
}
|
|
|
|
float fDist = GetDistanceBetween(oTarget, OBJECT_SELF);
|
|
|
|
// Only give the command if we're too far away and not moving
|
|
if (fDist > fCloseEnough && GetCurrentAction() != ACTION_MOVETOPOINT)
|
|
{
|
|
ClearAllActions();
|
|
ActionMoveToObject(oTarget, TRUE);
|
|
}
|
|
|
|
// Call again until bmc_value changes or something else cancels us out
|
|
DelayCommand(0.5, TryToMoveToCreature(oTarget, nCommand, oArea));
|
|
}
|
|
|
|
void GroupUnlock(object oTarget)
|
|
{
|
|
// Going to loop through members of the party
|
|
object oMinion = GetFirstFactionMember(oOriginalMaster, FALSE);
|
|
while (GetIsObjectValid(oMinion))
|
|
{
|
|
// If the party member's master is the PC, issue orders to unlock (NOT including summons of minions)
|
|
if (GetMaster(oMinion) == oOriginalMaster)
|
|
{
|
|
if(GetAssociateState(NW_ASC_RETRY_OPEN_LOCKS, oMinion))
|
|
{
|
|
FloatingTextStringOnCreature("Sending " + GetName(oMinion) + " to deal with the lock.", oOriginalMaster, FALSE);
|
|
AssignCommand(oMinion, SingleUnlock(oTarget));
|
|
}
|
|
}
|
|
oMinion = GetNextFactionMember(oOriginalMaster, FALSE);
|
|
}
|
|
}
|
|
|
|
void SingleUnlock(object oTarget)
|
|
{
|
|
int nCommand = MakeUncommandable();
|
|
|
|
ResetHenchmenState();
|
|
SetAssociateState(NW_ASC_MODE_DEFEND_MASTER, FALSE);
|
|
SetAssociateState(NW_ASC_MODE_STAND_GROUND, FALSE);
|
|
DeleteLocalObject(OBJECT_SELF, "bmc_attacktarget");
|
|
|
|
if (!bkAttemptToOpenLock(oTarget))
|
|
{
|
|
FloatingTextStringOnCreature(GetName(OBJECT_SELF) + " cannot deal with the lock.", GetMaster(), FALSE);
|
|
ResetHenchmenState();
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
return;
|
|
}
|
|
|
|
DelayCommand(0.5, CheckLockStatus(oTarget, nCommand, GetArea(OBJECT_SELF)));
|
|
}
|
|
|
|
void CheckLockStatus(object oTarget, int nCommand, object oArea)
|
|
{
|
|
// Has our command value changed?
|
|
if (nCommand != GetLocalInt(OBJECT_SELF, "bmc_value") || !GetLocalInt(OBJECT_SELF, "bmc_active"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int nAction = GetCurrentAction();
|
|
if (CheckConversation(OBJECT_SELF, nAction) || IsInConversation(oOriginalMaster))
|
|
{
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
RegroupParty();
|
|
return;
|
|
}
|
|
|
|
// Are we in another area or dead or fired?
|
|
if (GetArea(OBJECT_SELF) != oArea || GetIsDead(OBJECT_SELF) ||
|
|
(GetMaster() != oOriginalMaster && GetMaster(GetMaster()) != oOriginalMaster && GetMaster(GetMaster(GetMaster())) != oOriginalMaster))
|
|
{
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
return;
|
|
}
|
|
|
|
if (GetLocked(oTarget) && GetCurrentHitPoints(oTarget) > 0)
|
|
{
|
|
if (GetCurrentAction() == ACTION_INVALID)
|
|
{
|
|
ClearAllActions();
|
|
bkAttemptToOpenLock(oTarget);
|
|
}
|
|
DelayCommand(0.5, CheckLockStatus(oTarget, nCommand, oArea));
|
|
}
|
|
else
|
|
{
|
|
ResetHenchmenState();
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
}
|
|
}
|
|
|
|
void GroupDisarm(object oTarget)
|
|
{
|
|
// Going to loop through members of the party
|
|
object oMinion = GetFirstFactionMember(oOriginalMaster, FALSE);
|
|
while (GetIsObjectValid(oMinion))
|
|
{
|
|
// If the party member's master is the PC, issue orders to disarm (NOT including summons of minions)
|
|
if (GetMaster(oMinion) == oOriginalMaster)
|
|
{
|
|
if(GetAssociateState(NW_ASC_DISARM_TRAPS, oMinion))
|
|
{
|
|
FloatingTextStringOnCreature("Sending " + GetName(oMinion) + " to deal with the trap.", oOriginalMaster, FALSE);
|
|
AssignCommand(oMinion, SingleDisarm(oTarget));
|
|
}
|
|
}
|
|
oMinion = GetNextFactionMember(oOriginalMaster, FALSE);
|
|
}
|
|
}
|
|
|
|
void SingleDisarm(object oTarget)
|
|
{
|
|
int nCommand = MakeUncommandable();
|
|
|
|
ResetHenchmenState();
|
|
SetAssociateState(NW_ASC_MODE_DEFEND_MASTER, FALSE);
|
|
SetAssociateState(NW_ASC_MODE_STAND_GROUND, FALSE);
|
|
DeleteLocalObject(OBJECT_SELF, "bmc_attacktarget");
|
|
|
|
CheckDisarmStatus(oTarget, nCommand, 0, GetArea(OBJECT_SELF));
|
|
}
|
|
|
|
void CheckDisarmStatus(object oTarget, int nCommand, int nDisarmActive, object oArea)
|
|
{
|
|
// Has our command value changed?
|
|
if (nCommand != GetLocalInt(OBJECT_SELF, "bmc_value") || !GetLocalInt(OBJECT_SELF, "bmc_active"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int nAction = GetCurrentAction();
|
|
if (CheckConversation(OBJECT_SELF, nAction) || IsInConversation(oOriginalMaster))
|
|
{
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
RegroupParty();
|
|
return;
|
|
}
|
|
|
|
// Are we in another area or dead or fired?
|
|
if (GetArea(OBJECT_SELF) != oArea || GetIsDead(OBJECT_SELF) ||
|
|
(GetMaster() != oOriginalMaster && GetMaster(GetMaster()) != oOriginalMaster && GetMaster(GetMaster(GetMaster())) != oOriginalMaster))
|
|
{
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
return;
|
|
}
|
|
|
|
if (GetTrapDetectedBy(oTarget, OBJECT_SELF))
|
|
{
|
|
if (GetDistanceToObject(oTarget) > 15.0 || !bkGetIsInLineOfSight(oTarget))
|
|
{
|
|
ActionMoveToObject(oTarget, TRUE);
|
|
}
|
|
else
|
|
{
|
|
if (!nDisarmActive)
|
|
{
|
|
ClearAllActions();
|
|
if (!bkAttemptToDisarmTrap(oTarget, TRUE))
|
|
{
|
|
FloatingTextStringOnCreature(GetName(OBJECT_SELF) + " cannot deal with the trap.", GetMaster(), FALSE);
|
|
ResetHenchmenState();
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
return;
|
|
}
|
|
nDisarmActive = 1;
|
|
}
|
|
else if (nAction == ACTION_INVALID && nDisarmActive)
|
|
{
|
|
ClearAllActions();
|
|
bkAttemptToDisarmTrap(oTarget, TRUE);
|
|
}
|
|
}
|
|
DelayCommand(0.1, CheckDisarmStatus(oTarget, nCommand, nDisarmActive, oArea));
|
|
}
|
|
else
|
|
{
|
|
ResetHenchmenState();
|
|
SetLocalInt(OBJECT_SELF, "bmc_active", 0);
|
|
}
|
|
}
|