IWDII Scripting Info Gathered from various places - Interplay Discussion Boards, TeamBG Discussion Boards, BlackIsle Studios Message Boards, PREFAB.IDS (came with the game). The list is by no means complete and the boards mentioned above still contain quite a bit of useful information, but the following info is what I gathered in a relatively quick cut and paste raid. I may get around to formating it in a more visually pleasing fashion, but don't count on it. Feel free to use the info contained in however you like. If you are going to use it to create a scripting guide, then please credit the people I have listed as providing the question/info/answer/etc.,. --------- Summoning --------- -Caliban72 SummoningLimitLT(Myself, x) where x is the check against how many creatures are out there currently. If you were testing for Animate Dead for instance, use: SummoningLimitLT(Myself, 5) Because the limit is actually six and AD will bring in one more. -Caliban72 I've been using SummoningLimitLT(Myself, x) to determine whether it's OK to call more or not. Now I'm trying to script Bansihment and Dismissal, which get rid of enemy summoned creatures. How do I determine if, and how many opposing summons are in sight? How do I target them? -Weimer I would try the object specifier "NearestEnemySummoned" or something of the form [GOODCUTOFF.0.0.0.0.SUMMONED]. -caliban72 Q: What about Dismissal? What kind of checks would one do do determine if a target is a summons? Will MSAO validate a target for this spell? -Briareus A: We never used MSAO and Dismissal or Banishment so I doubt MSAO will take care of that for you. We used a specific IF block to cast those. In OBJECT.ids you'll find a new custom object called NearestEnemySummoned. This will point to the nearest summoned creature that is an enemy of you. Note that this might return a summoned Elemental or Demon if they have a red-circle. ------- Timers ------- -Gimble StartTimer() and TimerActive() are local only, so global timers are needed for cross-character coordination. SetGlobalTimer - to coordinate party actions -Gimble As I noted in a post on the old boards, I was still "clinging to" Code: GlobalTimerNotExpired("MyTimer","LOCALS") and related actions/triggers, rather than switching toCode: !TimerActive( 100 ) and related triggers. My reasoning for this was that timer indexes (plain numbers) are hard to track, and hard to remember which ones you have (or haven't) used. However, after doing some reading of old information ( thanks to Caliban for the "BI Analysis.txt" file), I found that string parsing is one of the functions in AI scripting that is very, very slow. As a result, it's probably a pretty significant hit to use GTNE instead of TA as a rule, and should be avoided. I did manage to find a solution, which IMO is an improvement over TA with numeric values. I modified the following lines in ACTION.IDS:Code: 61 StartTimer(I:ID*TIMERS,I:Time*) 232 StartRandomTimer(I:TimerID*TIMERS,I:MinTime*,I:MaxTime*) and the following in TRIGGER.IDS:Code: 0x0022 TimerExpired(I:ID*TIMERS) 0x4097 TimerActive(I:TimerID*TIMERS) Simple enough. Now, I created a TIMERS.IDS file, and placed it into my override directory. For example, my TIMERS.IDS file contains the following:Code: 5 200 SONG 201 MONK 202 SPELL 203 DAILY -Briareus (The 5 tells the compiler how many lines are in the .ids file.) -Gimble cont'd Now, in my script, I can use the following, which makes a bit more sense: Code: IF ActionListEmpty() !TimerActive(DAILY) THEN RESPONSE #100 // Do daily initialization here SetGlobal("MonkFist","LOCALS",1) // ... StartTimer(DAILY, 2400) END ...which is still very readable, and doesn't use any strings. -Gimble Q: What is the relationship between timer ticks ( SetGlobalTimer() ) and this interval? -Briareus A: Times used in SetGlobalTimer() and all the timers are in seconds. Intervals used in functions like AttackReevaluate() are in AI updates. So 15 AI updates would be 1 second. -Caliban72 Q: #1 Daily Timer: Is there an accurate way to set a timer that will fire once per day? -Briareus A: You could set a timer for 28800 seconds. That's 8 hours. That way the code will know the item is reusable once the party rests. -Gimble Trivia of the day: Through the school of hard knocks, I found that the maximum valid timer number is 255. Any timer number referenced above that value is always expired. Code: IF !TimerActive(255) THEN RESPONSE #1 FloatMessage(Myself,1) StartTimer(255,30) END This works as you might expect: it puts a message above the character's head every 30 seconds. Code: IF !TimerActive(256) THEN RESPONSE #1 FloatMessage(Myself,1) StartTimer(256,30) END This, however, does not. It floats a message above the character's head on every AI script run. -Avenger2 Well, apparently the timer ID is just one byte... You must know that object ID's are just one byte as well (gender, class, align, etc..) -------------------- MarkSpellAndObject() -------------------- -Gimble If there is any valid spell in the list, MSAO will find it even if RANDOM is used. -Gimble You probably already know this, but MSAO only allows a maximum of 8 spells (32 characters) in a single call. -Deadmeat I've been playing a bit with MSAO(). I have some questions about the flags in SPLCAST.IDS. Are these tests applied before, after, or during a spell selection? -Gimble MarkSpellAndObject() seems to apply the tests during the "mark" phase of the selection. If a spell is not valid for that target(according to MSAO), it is not marked. An example: Code: IF ActionListEmpty() // I'm not busy ForceMarkedSpell(MARKED_SPELL) // make sure no spell is currently marked SetSpellTarget(Nothing) // and invalidate any previously selected spell target See(NearestEnemyOf(Myself),0) // duh. THEN RESPONSE #100 // 2104 - Charm Person // 2316 - Dire Charm // 3108 - Innate Charm Person or Animal (not used?) // 3119 - Innate Charm Person (not used?) // 1204 - Cleric Charm Person or Mammal // 2507 - Dominate Person // 2911 - Mass Domination // 3220 - Innate Mass Dominate (not used?) MarkSpellAndObject("21042316310831191204250729113220",LastMarkedObject, RANDOM) Continue() END IF !(IsMarkedSpell(MARKED_SPELL)) // If the above selected a spell THEN RESPONSE #100 Spell(SpellTarget, MARKED_SPELL) // cast the spell WaitAnimation(Myself, WALK) // wait for an animation to complete, if necessary WaitAnimation(Myself, CONJURE) // wait for an animation to complete, if necessary WaitAnimation(Myself, CAST) // wait for an animation to complete, if necessary ForceMarkedSpell(MARKED_SPELL) // and then make sure no spell is marked END The above attempts to look at the nearest target, and randomly choose one of your Charm Person / Domination spells, if available (and the target is not immune to charm). If the selection is successful, then the second IF block fires off the marked spell. If it is not, the second IF block fails and is not entered. Note that MSAO doesn't pick a good or bad target, it just states whether or not your spell might have any effect on the target. Not all spells are scripted in MSAO either -- some spells like Bless (IIRC) will be cast repeatedly on a target, even if they're already under the spell effects. -Deadmeat Can a spell be chosen, and then discarded due to other qualifiers (eg. trying to cast Call Lightning indoors)? (Leaving you with no spell to cast). -Gimble I'm not sure what you're asking here. You can manually choose to dump a MSAO()'d spell if you like later on in the selection process. AFAIK, once the spell passes MSAO, the caster will attempt to cast the spell on the target...regardless of whether or not the spell will do anything good. For example, if you do this: Code: IF ActionListEmpty() // I'm not busy ForceMarkedSpell(MARKED_SPELL) // make sure no spell is currently marked SetSpellTarget(Nothing) // and invalidate any previously selected spell target THEN RESPONSE #100 // 1103 - Cure Light Wounds MarkSpellAndObject("1103",Player1,0) Continue() END IF !(IsMarkedSpell(MARKED_SPELL)) // If the above selected a spell THEN RESPONSE #100 Spell(SpellTarget, MARKED_SPELL) // cast the spell WaitAnimation(Myself, WALK) // wait for an animation to complete, if necessary WaitAnimation(Myself, CONJURE) // wait for an animation to complete, if necessary WaitAnimation(Myself, CAST) // wait for an animation to complete, if necessary ForceMarkedSpell(MARKED_SPELL) // and then make sure no spell is marked END The above will attempt to cast Cure Light Wounds on Player 1 whether or not he is injured: MSAO apparently believes that uninjured targets are still valid for cure spells. -Deadmeat Does SPELLCAST_RANDOM pick a spell at random from the list, and then check to see whether you have it, or does it reduce the list to spells you have before picking one? -Gimble That depends on the last argument to MSAO (it's taken from SPLCAST.IDS, and the values are additive). If SPELLCAST_RANDOM (4) isn't selected, then it works the list front to back until it finds a valid spell for your target (or fails). If SPELLCAST_RANDOM is selected, then it selects from the list of valid spells randomly. Do note that other flags can be set to disable certain spell checks, in case you don't want MSAO to validate the target for something or other. For example, if you're wounded (and invisible), you may want to use the SPELLCAST_IGNORE_SEE flag, since when you start the Cure Light Wounds (or similar) spell on yourself, you will become visible. -Gimble Q: Is there a definite number on how many spells you can "chain" into MarkSpellAndObject? I'm presuming that the spell string length can't exceed 255 characters, but is there a lower limit than that? (or a higher one?) -Caliban72 Briareus answered this one for me a while back. It's 8 spells, but you can stack multiple MSAO calls in the same action block like so: -Briareus 32 characters, or 8 spell IDs is the max. -Gimble Q: With a SPELLCAST_RANDOM option, what does MSAO() do if the first selection it makes (randomly) isn't valid? For example, if I use a spell list that includes Mage Armor, and the spell is selected (but already active), does MSAO pick another one at random? Not select any spell? Select the following spell in the list (assuming it's valid)? -Briareus A: MSAO will pick another spell randomly. ******************************************************************************* // 1724 CLERIC_GATE // 1605 CLERIC_PLANAR_ALLY_FIRE_ELEMENTAL // 1702 CLERIC_PLANAR_ALLY_EARTH_ELEMENTAL // 2627 WIZARD_CONJURE_FIRE_ELEMENTAL // 2628 WIZARD_CONJURE_EARTH_ELEMENTAL // 2629 WIZARD_CONJURE_WATER_ELEMENTAL // 2803 WIZARD_SUMMON_MONSTER_VIII // 2809 WIZARD_SUMMON_FIEND // 2902 WIZARD_SUMMON_MONSTER_IX // 1901 CLERIC_ELEMENTAL_LEGION // 2905 WIZARD_GATE // 1723 CLERIC_SHAMBLER // 1850 CLERIC_SUMMON_NATURES_ALLY_8 // 1950 CLERIC_SUMMON_NATURES_ALLY_9 IF ActionListEmpty() ForceMarkedSpell(MARKED_SPELL) SetSpellTarget(Nothing) GlobalGT("TW_Threat","LOCALS",3) SummoningLimitLT(Myself,3) !GlobalTimerNotExpired("TW_Summon","GLOBAL") // Co-Ordinated casting... See(MyTarget,0) THEN RESPONSE #100 MarkSpellAndObject("17241605170226272628262928032809",LastMarkedObject,SPELLCAST_RANDOM) MarkSpellAndObject("290219012905172318501950",LastMarkedObject,SPELLCAST_RANDOM) Continue() END IF ActionListEmpty() !IsMarkedSpell(MARKED_SPELL) THEN RESPONSE #100 FloatMessage(Myself, 11125) // Better make your peace with your demons SetGlobalTimer("TW_Summon","GLOBAL",7) // Co-Ordinated casting... Spell(SpellTarget,MARKED_SPELL) WaitAnimation(Myself,WALK) WaitAnimation(Myself,CONJURE) WaitAnimation(Myself,CAST) ForceMarkedSpell(MARKED_SPELL) END Just imagine the amount of code this would take without MSAO!! ****************************************************************************************** -Caliban72 Q: BTW, Do you know if a MSAO can tell if you're outdoors or not for spell validation? (Call Lightning, Rainstorm, etc...) -Briareus A: Yes, it can. -Caliban72 Wait a minute!! MSAO doesn't check states like advertised! A cleric with two Bless spells memorized using the code below will consistantly cast them sequentially in the same combat! Using himself as the target you'd think already having the BLESS state on himself would invalidate it as a spell to be marked. -Briareus Yeah, we weren't able to get all the state checks into MSAO, though I'm suprised Bless doesn't work. ****************************************************************************************** // 1101 CLERIC_BLESS // 1115 CLERIC_ARMOR_OF_FAITH // 2120 WIZARD_MINOR_MIRROR_IMAGE (Mask) IF ActionListEmpty() ForceMarkedSpell(MARKED_SPELL) SetSpellTarget(Nothing) GlobalGT("TW_Threat","LOCALS",1) See(Myself,0) THEN RESPONSE #100 MarkSpellAndObject("110111152120",LastMarkedObject,0) Continue() END IF ActionListEmpty() !IsMarkedSpell(MARKED_SPELL) THEN RESPONSE #100 Spell(SpellTarget,MARKED_SPELL) WaitAnimation(Myself,WALK) WaitAnimation(Myself,CONJURE) WaitAnimation(Myself,CAST) ForceMarkedSpell(MARKED_SPELL) END ****************************************************************************************** -Caliban72 Since MSAO sn't checking spell states, I did a little experiment with the other two (focusing on BLESS). NotStateCheck(Myself, STATE_BLESS) doesn't work, while !CheckSpellState(Myself, BLESS) does exactly the checking needed. Since MSAO has lost it's multiple spell advantages (except for those spells that share the same states) and requires an extra code block (over the older HaveSpell, Spell logic), why use it? Because if you are scripting for a bard or sorcerer HaveSpell is useless, and MSAO is the only way to get them to fire thier spells properly. -Briareus You could still use it for spells it can check for. MSAO was written to make scripting enemy spell casters easier, not uber PC scripts. As such I'm not suprised that there are issues with it. -Caliban72 Singular buffs can be used with MSAO like so (it seems that at least for Barkskin the internal state checks work): ***************************************************************************************** // 1202 CLERIC_BARKSKIN IF ActionListEmpty() ForceMarkedSpell(MARKED_SPELL) SetSpellTarget(Nothing) GlobalGT("TW_Threat","LOCALS",2) See(Myself,0) THEN RESPONSE #100 MarkSpellAndObject("1202",LastMarkedObject,0) Continue() END IF ActionListEmpty() GlobalGT("TW_Threat","LOCALS",2) See(Player1,0) IsMarkedSpell(MARKED_SPELL) THEN RESPONSE #100 MarkSpellAndObject("1202",LastMarkedObject,0) Continue() END IF ActionListEmpty() GlobalGT("TW_Threat","LOCALS",2) See(Player2,0) IsMarkedSpell(MARKED_SPELL) THEN RESPONSE #100 MarkSpellAndObject("1202",LastMarkedObject,0) Continue() END IF ActionListEmpty() GlobalGT("TW_Threat","LOCALS",2) See(Player3,0) IsMarkedSpell(MARKED_SPELL) THEN RESPONSE #100 MarkSpellAndObject("1202",LastMarkedObject,0) Continue() END IF ActionListEmpty() GlobalGT("TW_Threat","LOCALS",2) See(Player4,0) IsMarkedSpell(MARKED_SPELL) THEN RESPONSE #100 MarkSpellAndObject("1202",LastMarkedObject,0) Continue() END IF ActionListEmpty() GlobalGT("TW_Threat","LOCALS",2) See(Player5,0) IsMarkedSpell(MARKED_SPELL) THEN RESPONSE #100 MarkSpellAndObject("1202",LastMarkedObject,0) Continue() END IF ActionListEmpty() GlobalGT("TW_Threat","LOCALS",2) See(Player6,0) IsMarkedSpell(MARKED_SPELL) THEN RESPONSE #100 MarkSpellAndObject("1202",LastMarkedObject,0) Continue() END IF ActionListEmpty() !IsMarkedSpell(MARKED_SPELL) THEN RESPONSE #100 Spell(SpellTarget,MARKED_SPELL) WaitAnimation(Myself,WALK) WaitAnimation(Myself,CONJURE) WaitAnimation(Myself,CAST) ForceMarkedSpell(MARKED_SPELL) END I know it's redundant in that one of the players is repeated (the internal state check with prevent the spell from being cast twice on the same char) but I thought it better that the caster try and protect him/herself imediately to better the chances of not being interrupted un subsequent castings (also, this scriplet can be given to any character regardless of what "PlayerX" they might be). ***************************************************************************************** -Gimble MarkSpellAndObject("1308",LastSeenBy(Myself),2) You can find the MSAO values for that third argument in SPLCAST.IDS, and the values are additive. So a value of 2 indicates IGNORE_VALID_SPELL_TARGET, meaning that MSAO will just mark the spell, and not check to make sure the target is valid. -Briareus (In reference to a question that asks if I have a stack of MSAO spells that remove negative effects, will MSAO work to select a spell that will get rid of the negative effect) MSAO doesn't check if the target has the effect that one of the spells in the list will remove. You'll have to explicitely check for Paralysis. -Caliban72 Q: What about the casters capability (see post above)? Does MSAO check as to whether the caster has some form of spell failure effect and then abort the spell selection? Or does it strictly do target checks for validity? -Briareus A: It should. I should definitely work for spells like Vipergout. Once you cast that, you cannot cast any spells, though you can move about. Is that what you were referring to? -Caliban72 No, I was more looking for the Spellfailure effects to prevent wasting spells. If I have Miscast Magic laid on me will MSAO still select a spell? Or do I need an explicit check to prevent the atempt to cast. -Briareus You'll need to do explicit checks for spells that provide a percentage chance of failure. --------------- In Combat? --------------- -Gimble I'm looking for a way to determine that my party is in combat. I've seen all of the following, based on who the scripter is: !CombatCounter(0) NumCreatureLT([EVILCUTOFF],1) OR(3) Detect(NearestEnemyOf(Myself)) Detect([EVILCUTOFF]) Detect([GOODBUTRED]) (combined with a global flag so party members would signal each other) probably other ones I don't recall at the moment. Is there any particular advantage/disadvantage to one method over any other? I want a little bit of realism in my scripts, but I don't want to necessarily go to the trouble of tracking a global variable ( internal timers are fine: but global variables require string searches, and I'm a little performance paranoid at the moment). I guess, more specifically, want does CombatCounter do within the engine, and are there any nasty side effects to using it? I wouldn't have thought so, but uScript (which overall seems reasonably well-written) specifically avoids use of CombatCounter(), so I'm paranoid. -Weimer You saw CombatCounter(0) in IWD2? It doesn't appear in my version of IWD2's TRIGGER.IDS. Similarly, ACTION.IDS is lacking StartCombatCounter (275 in IWD2 is SaveGame()) and whatnot. The trigger may "just happen to work" even though it's not in the IDS files (there are a few like that) but I wouldn't count on it. The one that I see the most often is "AnyPCSeesEnemy()". IIRC, this is the one used to tell when you should move on to the next "stage" of the Targos Palisade fight, for example. Cirerrek A simple little test script to tell me if AnyPCSeesEnemy() is working Code: ------------------------------ // AnyPCSeesEnemy() Test // // APCSE.baf IF AnyPCSeesEnemy() THEN RESPONSE #100 FloatMessage(Myself,15897) // "Favored Enemy" END IF !AnyPCSeesEnemy() THEN RESPONSE #100 FloatMessage(Myself,2778) // "Yes, I have - the children are safe." END Observed script behavior: As long as no enemy was around, the second string was displayed. I had one PC scout ahead and as soon as he saw and enemy, he and the other PCs stared displaying the first string. As soon as the enemies body hit the ground, all of the PCs started to display the second string again. Due to the way the fog of war was at the time, I could see the goblin's display strings, but the PCs didn't start displaying the first string until the scout had the goblin in sight. This would lead me to believe that the trigger is limited to visual range (27?). ------------------------------- ----------- Enemy Ally ----------- -Briareus For IwD2, it's: code: [EA.General.Race.SubRace.Class.Kit.Specifics.(Instance).(SpecialCase).ClassMask] Just use the appropriate *.ids file for each slot. There are no ids files for Instance or SpecialCase, nor are there any script functions to set or get their values. -Avenger2 I think Gender and Alignment are also there somewhere, no ??? Unless SpecialCase isn't Gender, hmm. But where is Alignment ? -Gimble GOOD(33) --> Alignment(MyTarget,MASK_GOOD) EVIL(37) --> Alignment(MyTarget,MASK_EVIL) CHAOTIC(61) --> Alignment(MyTarget,MASK_CHAOTIC) LAWFUL(59) --> Alignment(MyTarget,MASK_LAWFUL) UNDEAD(1) --> Race(MyTarget,UNDEAD) GOBLIN(79) --> Race(MyTarget,GOBLIN) ELVES(15) --> Race(MyTarget,ELVES) YUANTI(89) --> Race(MyTarget, YUANTI) GIANT(81) --> Race(MyTarget, GIANT) -Briareus Check out CLASSMSK.ids. You can use any of the values in there. The value you'd want is CLASSMASK_GROUP_CASTERS. The classmask is the 10th entry in the [object] group, so you'd have: code: -------------------------------------------------------------------------------- [ENEMY.0.0.0.0.0.0.0.0.CLASSMASK_GROUP_CASTERS] -------------------------------------------------------------------------------- Would give you the nearest hostile creature that's a cleric, druid, wizard, or sorcerer (and Bard I think as well, but there are no NPC bards so that's moot). -Gimble 1: It seems (after some browsing), that the best way to try to find targets that won't make their saves is through using something like: See([ENEMY.0.0.0.0.0.0.0.0.CLASSMASK_WEAK_WILL_SAVE]) -Briareus Have you tried using CheckStat(), CheckStatGT(), or CheckStatLT()? The values you need are in STATS.ids. -Gimble 9 SAVEFORTITUDE 10 SAVEREFLEX 11 SAVEWILL ------------ AI Framerate ------------ -Gimble Q: How do I know how often my script will be called? -Briareus A: 1/2 the value of your AI Framerate per second. So, the default AI Framerate of 30 means your script is called 15 times a second. Your script is NOT called while the creature running the script is doing actions, though. For example, if you check to see an enemy, then to all the commands necessary to cast a spell, during the time it takes to process the spellcasting commands your script won't be called at all. A bad path found through the pathfinding engine can result in your script not getting called again for quite some time. ------ Attack ------ -Gimble Q: What is the relationship between this interval and the time parameter provided to AttackReevaluate() -Briareus A: Times used in SetGlobalTimer() and all the timers are in seconds. Intervals used in functions like AttackReevaluate() are in AI updates. So 15 AI updates would be 1 second. -Gimble Q: What are the differences between Attack(), AttackOneRound(), and AttackReevaluate(), and is any of the above preferred for any reason? -Briareus A: Attack() will make the creature attack the target and never stop until the target is dead. AttackOneRound() will cause the creature to attack for one round. This one should be used normally as it doesn't finish until the creature gets in enough attacks for one round. AttackReevaluate allows you to specify how long (in AI Updates) to attack the target before the action finishes. Which one you use depends on what you're trying to do. I'd recommend using AttackOneRound() by default. I don't believe any of the scripts that shipped with the game use AttackReevaluate(). Your scripts can get temporarily "stuck" in the Attack() and AttackOneRound() action if the creature can't path to the target (either no path or the target continues to move away). AttackReevaluate() gets around that because it WILL finish when the time is up, but your creature running the script might lose out on some of the attacks it has for that round and will have to wait for the next round. -Gimble Q: If I understand your prior response, the following are true: 1) The execution time of AttackOneRound() and AttackReevaluate(,105) are the same (7 seconds), assuming no special circumstances (movements required, etc.) -Briareus A: Sure, as long as you'll never have to move during the attack, then yes. -Gimble 2) Under special circumstances, AttackReevaluate() will 'time out' at the appropriate time, but may lose any attacks not successfully completed before the timeout. -Briareus A: Correct. -Gimble 3) Conversely, AttackOneRound() will guarantee one round of attacks, but will not guarantee a completion time of 7 seconds. A: Yup. ------ Round ------ -Gimble Q: IIRC you said in the earlier thread that one round is 7 seconds ( thus, 15*7 = 105 ticks ). Is that correct (just to be safe)? -Briareus A: Looks correct to me. ------------------ IncrementInternal ------------------ -Gimble Q2: From the code snippets examined, I deduced that IncrementInternal has the following arguments: Object that has the Internal variable to be incremented SecretFormulaX, set to zero in all examples I saw. I presume that this might be an index offset ( if objects have multiple internal variables that can be fiddled with ) Number to increment (if positive) or decrement(if negative) the internal variable by. -Briareus A: You're assumption about the IncrementInternal() is correct. -Briareus Think of Internals as Global("var","LOCALS"), except you can specify which creature to set the internal for. Each creature has 5 internals, numbered 0 - 4. Only player scripts set internals, so feel free to use whatever ones you want, just be warned that if you set an internal on a creature and save the game, that value is saved with the savegame file ************************************************************************* Ex. - Use of IncrementInternal and Shouts Code: IF ActionListEmpty() !TimerActive(94) AttackedBy([EVILCUTOFF],0) THEN RESPOSE #100 StartTimer(94,14) Shout(5) IncrementInternal(LastAttackerOf(Myself),0,10) Continue() END -Briareus Ex. should do the following: The shout will happen once, each time the character is attacked by a red-circle, but not more often than once every 14 seconds. Note: -Briareus What ID you want to use is up to you. I'd recommend using IDs above 100 to avoid messing up any shipped scripts. ************************************************************************** ----------------- StateCheck() ---------------- STATE.ids -Caliban72 Q2: How can I tell if my character has any remaining Mirror Images left? -Briareus For Mirror Image, I'd use StateCheck(Object, STATE_MIRRORIMAGE). -------------------- CheckSpellState() -------------------- SPLSTATE.ids -Caliban72 Q1: How do I tell if my character currently has Mage Armor (as an example) active? Q3: How do I tell if a character has Barkskin in effect? -Briareus A1&3: For Mage Armor and Barkskin, use CheckSpellState(Object, ARMOR) and CheckSpellState(Object, BARKSKIN), respectively. -Briareus BTW, why do you want to check for these things? If you're using the new spell casting functions (MarkSpellAndObject, etc.), you don't need to explicitly check for spell states as the engine does it by default (unless you set the flag to override that). ------------------------------------- Check for Lowest HP Monster in Sight ------------------------------------ -Gimble Q: Is there any convenient way to look for the lowest-HP monster within sight, without tons of scripting ( since I can't seem to compare creature A's HP to creature B's HP, it makes for a whole lot of static comparisons )? -Briareus A: Nope. You'll have to do a ton of IF checks. Sorry. ------------------------------------- LastMarkedObject ------------------------------------ -Briareus LastMarkedObject is set automagically via certain script triggers and functions. See(), Entered(), Clicked(), LOS(), and others will set LastMarkedObject if the trigger evaluates to TRUE. ------------------- PartyRested() ------------------ -Gimble Q: Does PartyRested() still work? Perhaps he could use that to reset any triggers he might need (used heavily in BG2 scripts that I've browsed). -Briareus Doubtful as none of the shipping scripts use it that I know of. You can always try it out and see if it works. -Gimble Oh, and just to add that PartyRested() doesn't seem to be in the new IDS files, so it may be gone. -Gimble Timers and Resting After doing some experimentation, I've come up with some - umm - unusual numbers. By math, 60 seconds / min * 60 mins / hour * 8 hours / rest = 28,200 So, you would expect that SetGlobalTimer("MyTimer","LOCALS",28200) would kick off once every 8 hours (presumably after each rest). Not so. After browsing around a bit, in GTIMES.IDS, we learn that 28,200 is actually the time value for four complete days, and experimentation seems to bear this out. So, if you want a timer that will go off every eight hours ( presumably after each rest ), it appears that you really need to use SetGlobalTimer("MyTimer","LOCALS",2400), which is: ONE_DAY (7200) / 3 = 2400 This seems kind of odd, since a timer decrements by one per second, but perhaps those are "real" seconds. And, experimentation seems to indicate that 2400 is the appropriate value to set the timer to be. If that's the case (and I'm just musing now), it really seems that one real time second is really 12 in-game seconds ( 28800 / 2400 ). As a result, a combat round is really 12*7=84 seconds long (in game terms), leading us closer to the original "one round = one minute" thought of 2nd Edition. Yes, apparently I'm a geek. But I couldn't figure out why my timers weren't resetting after resting for 8 hours, and started experimenting to find out why. ------------------ See() ------------------ -Gimble Q: What's the second argment ( typically zero ) that is passed to See()? -Briareus 0 = don't see dead creatures. 1 = see dead creatures. In every previous IE game, See() would return a match even if the creature was dead. This resulted in having to do a !Dead() check in almost every IF block. I don't know why you'd want to see dead creatures, but it's there if you do. ------------------------------------------------------------------ Things that might make you not want to cast a spell or unable to ------------------------------------------------------------------ -Caliban72 Q: What are your uncastable conditions? I posted mine above but I totally forgot about Insect Swarm (Plague, whatever). -Gimble After stealing from you (you mentioned some I hadn't considered), here's my current list: StateCheck(Myself, STATE_INVISIBLE) StateCheck(Myself, STATE_POISONED) CheckSpellState(Myself, BLINK) CheckSpellState(Myself, MISCAST_MAGIC) CheckSpellState(Myself, FEEBLEMIND) CheckSpellState(Myself, CANNOT_CAST) CheckStatGT(Myself,25,SPELLFAILUREMAGE) CheckStatGT(Myself,25,SPELLFAILUREPRIEST) I believe that Insect Swarm sets the SPELLFAILUREMAGE value high, but I cannot guarantee it. My personal preferences (not to cast while Invisible... as I normally only use regular Invis when I don't want to be seen) also make a difference. -------------------------------------------------------------- How can you set/unset feats like Expertise and Power Attack? -------------------------------------------------------------- -Gimble How can you set/unset feats like Expertise and Power Attack? -Briareus You can't. While there is a innate spell listed for power attack and expertise, casting those will only set PA and Ex to 1. You'd have to make new spell files that set the appropriate effect. I'm not sure if there are any fan-based spell editors out there or not, or if those would even work with IwD2's spell format. ------------------------------------------------------------- Druid Shapeshifting ------------------------------------------------------------ -caliban72 Is druid shapeshifting scriptable? I have been unsuccessful in my atempts at HaveSpell(INNATE_...) and also using MSAO. -Briareus You won't be able to use HaveSpell() or MSAO with any player innate abilities. Those are handled completely differently in the engine now, though there are some INNATE_* spells for those abilities. You'll have to set your own day timers and usage counters if you want that scripted. -Caliban72 Chad, On Druid shapeshifting and scripting it. I can set the shift counts and shapes possible by using CheckStatXX(Myself,x,CLASSLEVELDRUID) But what about the feat based shapes? Beetle, Panther, & Shambler? How do I detect whether my char has it available or not? Determining this would enable secondary choices for shapes based on the situation. ------------------------------------------------------------ Item Scripting ----------------------------------------------------------- -caliban72 Wandering Sky is a 2H Sword +1 available for purchase in Targos at the start of the game. It has the ability to cast the spell "Doom" once per day. How do I script the casting of this ability? -Briareus With the UseItem() function. Here's a script I wrote to use an item once per day. IIRC, the item is the ring of Mage Armor. ************************************************************ Code: IF !TimerActive( 1 ) IsSpellTargetValid( Myself, WIZARD_ARMOR, 0 ) THEN RESPONSE #1 UseItem( "00Ring78", Myself ) StartTimer( 1, 20000 ) Wait( 7 ) END Yeah, it doesn't use any of those fancy WaitAnimation() calls, but then it doesn't have to. This is the entire script that I used for my monk while playing the game. ********************************************************* -Caliban72 Here is my working code for iron rations: ******************************************* IF ActionListEmpty() Exists([EVILCUTOFF]) HPPercentLT(Myself,100) HasItem("00GENIR",Myself) // Iron Rations THEN RESPONSE #100 FloatMessage(Myself,25855) UseItem("00GENIR",Myself) END **************************************** ---------------------------------------- Targeting Example --------------------------------------- -Caliban72 My targeting is pretty simplistic, even more than the supplied scripts. All my characters hammer the spell casters first then the two front liners go after the closest, and everyone else goes after the furthest. I had inconsistant results with getting FarthestEnemyOf() to work for me and have defaulted back to: ****************************************** Code: IF Global("TW_Caster","LOCALS",0) // have I already targeted a spell caster? // FarthestEnemyOf doesn't seem to work // See(FarthestEnemyOf(Myself),0) Or(10) See(TenthNearestEnemyOf(Myself),0) See(NinthNearestEnemyOf(Myself),0) See(EigthNearestEnemyOf(Myself),0) See(SeventhNearestEnemyOf(Myself),0) See(SixthNearestEnemyOf(Myself),0) See(FifthNearestEnemyOf(Myself),0) See(FourthNearestEnemyOf(Myself),0) See(ThirdNearestEnemyOf(Myself),0) See(SecondNearestEnemyOf(Myself),0) See(NearestEnemyOf(Myself),0) THEN RESPONSE #1 SetMyTarget( LastMarkedObject ) FloatMessage(MyTarget,1077) // Ranger Continue() END This works fairly well for me. **************************************** ---------------------------------- Threat Level Assessment Example --------------------------------- -Caliban72 Thanks! I'll check it out. I'm scripting spells for Wizard, Bard, Druid, Cleric and Paladin right now and I just added level 4 yesterday. With this much going on at once I find the threat levels handy (adding in a little coordinated casting for trolls & summons). Mine is also based primarily on the number of baddies, taking a sugestion from Bri I modeled it like this: // * THREAT-LEVEL 1: LEAST DANGER (Here there be monsters!) // * ============================ IF ActionListEmpty() See([ENEMY],0) // if enemies are visible !TimerActive(111) THEN RESPONSE #100 StartTimer(111, 7) SetGlobal("TW_Threat","LOCALS",1) // the threat is at least level 1 IncrementGlobal("TW_Round","LOCALS",1) Continue() END //////////////////////////////////////////////////////////////////////////////// // * THREAT-LEVEL 2: LOW DANGER //////////////////////////////////////////////////////////////////////////////// IF ActionListEmpty() NumCreaturesGTMyLevel([ENEMY],0) THEN RESPONSE #100 SetGlobal("TW_Threat","LOCALS",2) // the threat is at least level 2 Continue() END IF ActionListEmpty() Or(5) Exists([ENEMY.0.BUGBEAR.0]) Exists([ENEMY.0.SPIDER.0]) Exists([ENEMY.0.ETTERCAP.0]) Exists([ENEMY.0.DOPPLEGANGER.0]) Exists([ENEMY.0.DRIDER.0]) THEN RESPONSE #100 SetGlobal("TW_Threat","LOCALS",2) // the threat is at least level 2 Continue() END //////////////////////////////////////////////////////////////////////////////// // * THREAT-LEVEL 3: MEDIUM DANGER //////////////////////////////////////////////////////////////////////////////// IF ActionListEmpty() Or(2) NumCreaturesGTMyLevel([ENEMY],0) NumCreaturesGTMyLevel([ENEMY],1) THEN RESPONSE #100 SetGlobal("TW_Threat","LOCALS",3) // the threat is at least level 3 Continue() END IF ActionListEmpty() Or(6) Exists([ENEMY.0.GIANT.0]) Exists([ENEMY.0.BASILISK.0]) Exists([ENEMY.0.GOLEM.0]) Exists([ENEMY.0.UMBERHULK.0]) Exists([ENEMY.0.WYVERN.0]) Exists([ENEMY.0.DJINNI.0]) THEN RESPONSE #100 SetGlobal("TW_Threat","LOCALS",3) // the threat is at least level 3 Continue() END //////////////////////////////////////////////////////////////////////////////// // * THREAT-LEVEL 4: EXTREME DANGER //////////////////////////////////////////////////////////////////////////////// IF ActionListEmpty() NumCreaturesGTMyLevel([ENEMY],0) NumCreaturesGTMyLevel([ENEMY],1) THEN RESPONSE #100 SetGlobal("TW_Threat","LOCALS",4) // the threat is at least level 4 Continue() END IF ActionListEmpty() Or(7) Exists([ENEMY.0.TANARI.0]) Exists([ENEMY.0.SALAMANDER.0]) Exists([ENEMY.0.MINDFLAYER.0]) Exists([ENEMY.0.DRAGON.0]) Exists([ENEMY.0.HALF_DRAGON.0]) Exists([ENEMY.0.CHIMERA.0]) Exists([ENEMY.0.BEHOLDER.0]) THEN RESPONSE #100 SetGlobal("TW_Threat","LOCALS",4) // the threat is at least level 4 Continue() END Basing this evaluation on Character level alows it to flow as the characters grows stronger. I have a debug script that floats the threat level every couple of rounds and you can see it fall as hordes are thinned. No spells are cast during mop up, when the threat level drops to one. It seems like at low levels with this system that THREAT=4 a LOT. Which makes sense, since death is pretty close by for low level characters and even a few goblins can be very dangerous. My party (F5/Rog3, P8, C8(Mask), D8, B8, W7(trans)/S1) is just outside the ice temple in chapter 2. ------------------------ HasItemInSlot() ----------------------- -Briareus Looks like HasItemInSlot() doesn't work or was broken in one of the updates to 3E. I'm not suprised as it's probably a function no one used. --------------------------------- SetBestWeapon(O:Object*,I:Range*) --------------------------------- -Briareus calls EquipMostDamagingMelee() and EquipRanged() based on the range to the object. If the object is within the range, then EquipMostDamagingMelee() is called, otherwise EquipRanged() is called. -------------------------------- Global & Local Variables -------------------------------- -Caliban72 When a game is loaded (along with the assigned player scripts), what are the initial states of all Globals referenced? Is there a difference in the initialization based on scope ("GLOBAL" vs. "LOCALS")? Internals are saved/ loaded with the savegame state, correct? What were the official uses of the Internals for Player scripts that the Black Isle scripts use? -Briareus All globals and internals are saved with the savegame. When you reload the game, they're the same as they were when you saved the game. There are no "temp" globals that are not saved with the savegame. ----------------------------- ChangeAIScript() ------------------------------ -Gimble ChangeAIScript(S:ScriptFile*, I:Level*Scrlev) scrlev.ids OVERRIDE SPECIAL_1 TEAM SPECIAL_2 COMBAT SPECIAL_3 MOVEMENT Which levels can be safely overridden by user scripts? -Briareus NEVER use this on NPCs in the game. You will break the game. However, for Players 1 through 6, you can use this on all the slots except Override. The Override slot is used by the Charm-type spells. -Gimble Which level is the AI script at by default? -Briareus IIRC, the script you assign to a PC is in Special3 -Extremist 1. Which script levels are disabled if character is under stun effect? Or in other words, are they all disabled if someone is stunned? -Briareus None, the engine just doesn't bother calling any scripts while stunned. -Extremist 2. Which level contains troll's "if almost dead -> fall down" script? -Briareus Not sure, depends on the troll, but usually it's Special1. ---------------- PREFAB.IDS ----------------- Nearest enemy to my leader=NearestEnemyOf(LeaderOf(Myself)) Nearest enemy to who I am protecting=NearestEnemyOf(ProtectedBy(Myself)) Nearest enemy to me=NearestEnemyOf(Myself) Last enemy targeted by my leader=LastTargetedBy(LeaderOf(Myself)) Last enemy targeted by myself=LastTargetedBy(Myself) Last enemy to attack my protector=LastAttackerOf(ProtectorOf(Myself)) Last enemy to attack my leader=LastAttackerOf(LeaderOf(Myself)) Last enemy to attack who I am protecting=LastAttackerOf(ProtectedBy(Myself)) Last enemy to attack me=LastAttackerOf(Myself) Protector of the enemy leader=ProtectorOf(LeaderOf(NearestEnemyOf(Myself))) Protector of my leader=ProtectorOf(LeaderOf(Myself)) Person I am protecting=ProtectedBy(Myself) Person my leader is protecting=ProtectedBy(LeaderOf(Myself)) Least damaged of enemy group=LeastDamagedOf(GroupOf(NearestEnemyOf(Myself))) Least damaged of my group=LeastDamagedOf(GroupOf(Myself)) Most damaged of enemy group=MostDamagedOf(GroupOf(NearestEnemyOf(Myself))) Most damaged of my group=MostDamagedOf(GroupOf(Myself)) Strongest of enemy group=StrongestOf(GroupOf(NearestEnemyOf(Myself))) Strongest of my group=StrongestOf(GroupOf(Myself)) Weakest of enemy group=WeakestOf(GroupOf(NearestEnemyOf(Myself))) Weakest of my group=WeakestOf(GroupOf(Myself)) Enemy leader=LeaderOf(NearestEnemyOf(Myself)) My protector=ProtectorOf(Myself) My leader=LeaderOf(Myself) Myself=Myself -------------------= Nothing=Nothing() Myself=Myself() LeaderOf=LeaderOf() GroupOf=GroupOf() WeakestOf=WeakestOf() StrongestOf=StrongestOf() MostDamagedOf=MostDamagedOf() LeastDamagedOf=LeastDamagedOf() ProtectedBy=ProtectedBy() ProtectorOf=ProtectorOf() LastAttackerOf=LastAttackerOf() LastTargetedBy=LastTargetedBy() NearestEnemyOf=NearestEnemyOf() LastCommandedBy=LastCommandedBy() Nearest=Nearest() ------------------------ Internal() ----------------------- -Caliban72 [Nicholas, Chad] As for the Internal, each creature has 5 internal variables (0 - 4). They act just like IncrementGlobal "MODAL_STATE","LOCALS",2) with two exceptions: 1) They're parsed a lot faster in the engine and 2) you can set the value of another creature's internal. Area scripts and poly scripts don't have internals. [Nicholas, Chad] Internals only go from 0 through 4. That makes 5 internals for each creature. If you tried to set internal 5, you'd actually be setting internal 0. The only internal used by any of the scripts is 0, and those were used by the player scripts I made. The only reason to avoid using 0 is if you expect to have your scripts work with the ones that shipped with the game. ----------------------------------- Cone spells vs. Blast Area spells ---------------------------------- -Caliban72 How can one determine that there are no allied creatures (party members or summons) in the path of spells like Color Spray, Aganazzar's Scorcher, Shout, etc. ? I've used code like this for area effect spells, and it works reasonably well: //----------------------------------------------------------------------------- // ** Check if I can use an area spell such as, fireball or stinking cloud IF ActionListEmpty() NumCreatureGT([ENEMY],2) Range(Player1,12, LESS_THAN) // see if everyone is grouped close by Range(Player2,12, LESS_THAN) Range(Player3,12, LESS_THAN) Range(Player4,12, LESS_THAN) Range(Player5,12, LESS_THAN) Range(Player6,12, LESS_THAN) Range(MyTarget,25, GREATER_THAN) // check that target is not to0 close THEN RESPONSE #1 SetGlobal("TW_AE","LOCALS",1) Continue() END But this has no directional or line of sight information about where I'm about to spew the magic. Any ideas? -Grog Here's what I use; doesn't work perfect, but it seems to help. -------------------------------------------------------------------------------- Code: //Burning Hands IF See(LastAttackerOf(Myself)) HaveSpell(WIZARD_BURNING_HANDS) Range(LastAttackerOf(Myself),5) CheckStatLT(LastAttackerOf(Myself),26,RESISTFIRE) !LOS(Player1,3) !LOS(Player2,3) !LOS(Player3,3) !LOS(Player4,3) !LOS(Player5,3) !LOS(Player6,3) THEN RESPONSE #100 FaceObject(LastAttackerOf(Myself)) Spell(LastAttackerOf(Myself),WIZARD_BURNING_HANDS) END // Aganazzar's Scorcher IF See(NearestEnemyOf(Myself)) HaveSpell(WIZARD_AGANNAZAR_SCORCHER) CheckStatLT(NearestEnemyOf(Myself),26,RESISTFIRE) !LOS([Player1],7) !LOS([Player2],7) !LOS([Player3],7) !LOS([Player4],7) !LOS([Player5],7) !LOS([Player6],7) THEN RESPONSE #100 FaceObject(NearestEnemyOf(Myself)) Spell(NearestEnemyOf(Myself),WIZARD_AGANNAZAR_SCOR CHER) END -------------------------------------------------------------------------------- For some of the cone effect spells you may want to add a NumCreatureGT([EVILCUTOFF],X) check in your code block ----------- Slot.IDS ----------- -Gimble The Near Infinity editor reports that SLOTS.IDS has the following values: SLOT_WEAPON0 35 SLOT_WEAPON1 36 SLOT_WEAPON7 42 However, script testing seems to indicate the values should be: SLOT_WEAPON0 43 SLOT_WEAPON1 44 SLOT_WEAPON7 50 ... as the slots between 35 and 42 are primarily inventory slots added to the interface. I mention this to inform other scripters of the problem, and hopefully that BIS will fix the IDS file before a later patch. A more verbose, but more accurate description for scripters: -------------------------------------------------------------------------------- Code: SLOT_WEAPON1_PRIMARY 43 SLOT_WEAPON1_OFFHAND 44 SLOT_WEAPON2_PRIMARY 45 SLOT_WEAPON2_OFFHAND 46 SLOT_WEAPON3_PRIMARY 47 SLOT_WEAPON3_OFFHAND 48 SLOT_WEAPON4_PRIMARY 49 SLOT_WEAPON4_OFFHAND 50 -------------------------------------------------------------------------------- ------------------------------ NumCreaturesGTMyLevel( X, Y ) ----------------------------- -Gimble Q:What's the second argument to NumCreaturesGTMyLevel( X, Y )? In other words, what does argument Y do in the preceding sentence? -Briareus Whether or not to do a Hit Dice (HD) check. If false, each creature is counted as 1. If true, each creature's amount of HD is counted towards the total. For example, two goblins with 2 HD each. You're level 3. If the 2nd param is false, then the check will fail since there are only 2 goblins. If the 2nd param is true, then the check is true, since 2 HD + 2 HD = 4, which is > 3. ----------- LOS() ---------- Caliban72 LOS has two parameters. The first is the object you're looking for, and the second is how far as a maximum to look. So, LOS(NearestEnemyOf(Myself),28) would spot the nearest enemy in sight range, visible or not. LOS(NearestEnemyOf(Myself),4) would spot the nearest enemy roughly in melee range. And LOS(NearestEnemyOf(Myself),0) would spot the nearest enemy who was me (in effect, nobody). Another way you can find hidden enemies is Detect(NearestEnemyOf(Myself)), but Detect only works within a fixed range ( probably 28 ) AFAIK. ------------------------------------- HPLost(), HPLostGT() and HPLostLT() ------------------------------------- -Deadmeat I've started to experiment with potion drinking and healing, and found it useful in reducing wastage. ----------------------------------------- Global vs. Internal when using Continue() ------------------------------------------ -Gimble Trivia: SetGlobal("LOCALS") vs. SetInternal(Myself) It has been discussed before that the *Internal() functions are faster than the local variable functions because no string parsing is involved. And, usually, faster is better. However, there is a difference between the two that is quite significant if you use Continue(). Code: IF !Global("LOCALS","3",1) THEN RESPONSE #1 SetGlobal("LOCALS","3",1) Continue() END IF !Internal(Myself,3,1) THEN RESPONSE #1 SetInternal(Myself,3,1) Continue() END IF Global("LOCALS","3",1) THEN RESPONSE #1 FloatMessage(Myself,24088) // 1 Wait(3) Continue() END IF Internal(Myself,3,1) THEN RESPONSE #1 FloatMessage(Myself,24087) // 2 Wait(3) Continue() END IF True() THEN RESPONSE #1 SetGlobal("LOCALS","3",0) SetInternal(Myself,3,0) END In the above snippet, I set the value of "3" (either internal 3 or local variable "3", depending on which piece I'm testing ), and then check it later on in the same script run, and float a message above my head if the condition is true. *Global("LOCALS") works fine with this method, so I deduce that as the script is running the locals are immediately updated during the script run (so that code further down during the same pass will see the new value ). *Internal(Myself), however, does not. As a result, I deduce that SetInternal() is queued up and applied after the script run (and thus before the next pass to this script). So, the result of this script is a recurring "1" above the character's head, but no appearance of "2" above the character's head. This behavior is an important one to note if you use Continue() as it can butcher your expected code logic. Trust me. ------------- XEquipItem() ------------- -Japheth If you need to equip stuff in IWDII, you can use the handy XEquipItem(S:Item*,O:Object*,I:Slot*Slots,I:EquipUnEquip*BOOLEAN) action. Example: XEquipItem("someitem",Myself,SLOT_WEAPON,TRUE) would equip the someitem weapon in the first weapon slot. ------------------------------ MostDamagedOf() - Object.IDS ------------------------------ -Cirerrek My testing has shown this function to only be useful for detecting party members as using it in a target segement with !InParty checks resulted in my party members attacking each other. Note: If no one is injured, it will apparently go after anything with less hitpoints ----------------------------- SetVisualRange() - Action.IDS ---------------------------- -Cirerrek Sets the visual range for all creatures (active? unactive?) in an area. Not 100% sure it sets it for the party or not. I was trying to remove the lag from my Party AI script and thought the viewing range might be causing the lag, but setting it didn't increase the reaction time of the script. It just caused virtually all the creatures in the area to come swarming out of the woodworks where they had been stationary before. ----------------------- THIEF_ALL - Object.IDS ----------------------- -Cirerrek THIEF_ALL apparently includes Monks