I thought it would be fitting and realistic that since most pc spellcasters carry around dozens of backup scrolls of each spell thay have, especially the staples like Magic Missile, Sleep, Fireball,Healing,Stoneskin etc., that npc wizards should at least have a few extra to use. I don't want to go overboard on this and create a spellcaster with unlimited spells, but having a reserve scroll or two of each primary spell seems fair to me. This would be nice for low level wizards who run out of spells very quickly and resort to attacking with their dagger every turn while the PC's chuckle. I'm pretty sure scroll use can't be directly scripted, but I think it can be simulated in three ways. 1) Create a potion which will be triggered by "use potion" in their strategy.tab entry. Good for healing and buffing, but not for offensive spells. 2) Put the actual scrolls into the inventory, then check if they are still there during san_start_combat(), and use them as scripted by calling attachee.cast_spell(spell_whatever, pc), then delete the scroll from inventory. 3) The simplest would be to just do an attachee.spells_pending_to_memorized() once spells run low, to simulate having a scroll of each memorized spell in his inventory. If there is already a thread on this let me know (I looked and saw none).
Bear in mind that this would require reworking every affected caster's strategy/AI in order to get them to use the scrolls effectively and intelligently, which would be a very large task.
This would be for new spellcasters I am creating from scratch, so no tampering with existing protos with preexisting scripts and strategies. But I think it may not be doable, I'm still exploring. I see a ton of scripts where buff spells are being cast, but not a single one where an offensive spell is being cast directly from a script. This leads me to believe it can't be done, as I'm sure modders would have done it by now. I think the easiest route right now is setting a counter in san_start_combat() when spells are cast from their strategy, and doing a spells_pending_to_memorized() when spells get low to simulate using one backup copy of each spell is . edit: got the wizard to cast Sleep directly from san_start_combat(), so I was wrong.
I know very little about this kind of scripting, but could you actually give them an additional memorized spell of the kind you want, then "delete" a scroll from their inventory if they use the last memorized spell?
Hey marc, I found they cast some spells like Sleep pretty well from script but only original ones, not custom Co8 stuff alas. Re scrolls-as-potions, that's quite doable: Liv added potions 8900-8910 to simulate scrolls. Targeting can be difficult though, those are mostly self-buffing spells. Otherwise use spells_pending_to_memorized() as you say, and if you could be bothered, keep track of what's what and remove scrolls from inventory as appropriate (so that players who take the caster down quick get rewarded with his scrolls). Or add them in san_dying.
Ted, couldn't find any spells at all being cast from scripts in the vanilla game scr folder. The only place a spell came up is where Paida and Bing look for a spell being cast to release them from their respective situations. Ted + Rudy, I'll probably do that. Just rememorize all spells, them delete scrolls as they are cast the second time through. Daryk, You're right, he does! But the scroll of Death Knell is actually a potion (obj_t_food) that he "drinks" (or does he force the target to drink it, hmm), I'll need to look look into that case more. Sitra Achara dug out 2 potentially wondrous object functions along with their parameters a few weeks ago posted here: http://www.co8.org/forum/archive/index.php/t-1676.html They are: game.party[X].spell_known_add( nSpellIdx, nClassCode, nSpellLevel ) game.party[X].spell_memorized_add( nSpellIdx, nClassCode, nSpellLevel ) I've sucessfully used the second one to give recruitable spellcasters memorized spells at the time of joining, so they don't have to be selected and rest 8 hours. This is what has worked for me so far: attachee.spell_memorized_add(spell_sleep,17,1) Btw, the post lists the function as spells_memorized_add(), but the "s" is not in the real call which is spell_memorized_add(). Same for both. Edit: The "known" function works great too. Adds the spell to your spell book. attachee.spell_known_add(spell_knock,17,2) How did thise go undocumented and explored for so long. This is gonna help me so much in what I want to do.
I've been thinking of a way to accomplish this more accurately via a combination of spell_memorized_add() and spells_memorized_forget(). This would essentially grant you complete control over what spells the critter has memorized, which along with some scripting to track what has been already cast should enable you to accomplish this. It's also a potentially neat method of making a more convenient generic spellcasting AI without creating custom strategy.tab entries for each variant, which is annoying. As their names imply, spells_memorized_forget() clears all memorized spells, and spell_memorized_add( nSpellNum, nClassCode, nCasterLevel) adds a memorized spell of the class and spell level slot of your choosing. nSpellNum is the spell's number code (which you can get from the enums e.g. spell_fireball; see spell_enum.mes for more), nClassCode is the class code (from stat.mes), starting from 7 for Barbarian and going up alphabetically to 17 for Wizard. Or you can just use stat_level_CLASSNAME. An example pseudocode would be: Code: san_start_combat(..) attachee.spells_memorized_forget() nCureModWoundsUsed = game.global_vars[N1] & int('00000011', 2) CureModWoundsMAX = 3 nFlameStrikeUsed = ( game.global_vars[N1] & int('00011100', 2) ) >> 2 FlameStrikeMAX = 5 if ( ..can cast..) if ( ..friends hurt..) and ( nCureModWoundsUsed < CureModWoundsMAX ) attachee.spells_memorized_add(spell_cure_moderate_wounds, stat_level_cleric, 2) nCureModWoundsUsed += 1 elif ( ..furious anger..) and ( ..nFlameStrikeUsed < FlameStrikeMAX ) attachee.spells_memorized_add(spell_flame_strike, stat_level_cleric, 5) nFlameStrikeUsed += 1 game.global_vars[N1] = nCureModWoundsUsed + ( nFlameStrikeUsed << 2) san_first_heartbeat( ..) game.global_vars[N1] = 0 The strategy.tab entry would incorporate all the spells you could think of (though I think there's a certain limit, maybe 32 items) Haven't tested if it actually works in combat though. edit: damn you, ninja'd! oh well P.S. thanks for the correction, will fix!
This is beyond what I was hoping for. All you have to do is, as you said, put every spell they might need in their strategy.tab entry and then control the spell they need that turn from the script. And if their strategy won't hold enough, which it should for most casters, just switch strategies. You can keep count of their spells cast with globals or those npcvar_1() calls and when their normally memorized spells run out you can switch over to scrolls, and then destroy the scroll in inventory as appropriate. You can even put a little attachee.float_mesfile_line() line that says "Using Scroll of Hold Person" or whatever. It's ironic you solved the parameter of these calls only last week, and it's exactly what I was loking for.:notworthy
Hey thanks. The other thing I'd have to say is that in the long run, I hope to be able to either patch 'use potion' to optionally use scrolls, or create a new AI command altogether. So on the one hand the above method could be rendered obsolete within a few months, but on the other, I don't want to make promises I can't keep. So if possible I'd recommend setting up an abstract system whose implementation can be switched out as necessary later on.
Nice work again Mr Sitra :clap: marc - for examples of spell castings in action, check Liv's scripts in py00302, py00305 and py00306 (in the Co8 mod for ToEE of course).
My pursuit of having an npc use scrolls has led me down an interesting road with the discovery of how obj.spell_memorized_add() works by Sitra Achara. I've developed some functions that will allow an npc to keep his entire list of memorized spells within his script, with the quantity of each spell memorized held in that npc's calls to npcvar_1(),2 and 3. This make every spell known easily examined so the script can be written to cast the right spell at the right time, without wondering if that spell has been used, or worrying if the strategy will trigger properly. Another benefit is you don't have to have the spell listed in their proto (or change the proto if you want to add a spell). Just edit the tuple of spells in san_start_combat(). This now makes it possible for the npc to use scrolls (yay!) by actually casting a spell and just reducing the number of scrolls owned, while sending a float line saying "Casting Spell from Scroll". The only added step is that I have to populate their inventory after dying with the scrolls they never used, which is only fair. In the end I made the decision not to keep the scrolls in inventory, mostly because it was a lot easier to adjust their "inventory" of scrolls internally to the script, instead of having to rely on what the mob has in its inventory every time. Now, I simply have to edit the tuple of scrolls in their san_start_combat() function which take a few seconds. Also, when the scroll is used, I won't have to manage checking the inventory for the scroll and then delete it after use. Being that this is all new, some bugs related to using obj.spell_memorized_add() may rear their head, and they may significant enough to make this all for naught. But for now it's testing very well. Some other concerns: It's dicey only putting one spell in memory when the npc has a complicated strategy in strategy.tab. Before leaving san_start_combat() you have to assume the spell you chose will be cast and then reduce the quantity memorized of that spell before you exit. But sometimes the targeting will fail (a la "cast fireball" where the AI doesn't find a safe area to target) and no spell will be cast, or they may drink a potion instead of casting the spell and then you've used up a spell that never got cast. Solutions that I am bouncing around: -Maybe check if the spell fired from san_spell_cast() and then delete the spell from memory at that point. Need to explore this. - Depending on the spell, add a back-up spell to the chosen spell just in case the first one fails to target. - Initiate potion drinking from the script by calling a separate strategy that only uses a potion, and leave "use potion" out of the strategy. This has been done a lot in existing scripts. I like it. Details: The arrangement of this is pretty simple. There are 3 possible "banks" of spells that the npc can have, each containing 10 spells with up to 7 copies of each spell. The list of SPELLS for each spellbank is kept in a tuple in the san_start_combat() function, while the QUANTITY of each spell memorized (which is dynamic and needs to persist outside of a san_ call) is stored in the 3 object's variables set by those nifty functions called npcvar_1( ), npcvar_2(), and npcvar_3(). (I suppose the tuple of spells could be made a list and editted on the fly to change spells, but I haven't had the need for more than 3 spellbanks yet. A total of 30 spells/scrolls has been sufficient.) Currently, I've been using spellbank 1 for spells in the caster's memory, spellbank 2 for scrolls, and spellbank 3 occasionally for more spells if 10 different spells isn't enough. But these can be used however the script needs them. If you decide the first 5 spells are from memory, and spells 6,7, and 8 are from scrolls, you can put them all into one spellbank. The relevant functions I wrote are used to keep track of the quantity of spells in the spellbank, not the names of the spells themselves. It is up to the scripter to make sure that the number of entries in the tuple of spells defined in san_start_combat() matches up with the number of entries in the list of quantities. This is easy to keep track of visually (see example below). I'll put the code for these functions at the end for scrutiny. spellbank_qty_set() - This sets the npcvar_x() values with the quantity of each spell for a given spellbank. This will be called in san_first_heartbeat() spellbank_qty_get() - This retrieves a list of the the quantities for each spell in a spellbank. Called in san_start_combat() to get the quantity of each spell to be used in deciding which spell to cast. spellbank_qty_decrease() - This adjusts down by 1 the quantity of the spell just cast. Called in san_start_combat() after a spell is selected. Here's a quick example that mostly shows how the banks are set up and accessed. The "AI" is pretty basic, it just cycles thru all 2nd level spells in memory, then 1st levels spells in memory, then uses all the scrolls. A much more intelligent AI can be set up now that all the spells are accessible via script, which is part of the reason for all this, but I just wanted to give a simple example. Code: # Set up the Spellbank Quantities here def san_first_heartbeat( attachee, triggerer ): [B]spellbank_qty_set[/B] (attachee, 1, (2,1,2,1,2)) # set the quantities of spellbank 1 to (2,1,2,3,2) [B]spellbank_qty_set[/B] (attachee, 2, (1,0,5,1)) # set the quantities of spellbank 2 to (1,0,5,1) return RUN_DEFAULT # Declare the Spellbanks themselves in here def san_start_combat( attachee, triggerer ): # Spellbank 1, I'll use it for memorized spells spells = ( (spell_summon_monster_ii, 2), # Spell Index, Spell Level (spell_tashas_hideous_laughter, 2 ), (spell_magic_missile, 1 ), (spell_grease, 1 ) (spell_sleep, 1 ) ) # Spellbank 2, I'll use it for scrolls owned scrolls = ( (spell_summon_monster_ii, 2 ), (spell_tashas_hideous_laughter, 2 ), (spell_magic_missile, 1 ), (spell_sleep, 1 ) ) # Get a list containing the quantity for each spell within the spellbank spells_qty_list = [B]spellbank_qty_get[/B] (attachee, 1, len(spells)) # returns (2,1,2,1,2) initially, but changes as spells are cast scrolls_qty_list = [B]spellbank_qty_get[/B] (attachee, 2, len(spells)) # returns (1,0,5,1) initially, but changes as spells are cast # Erase all spells currently in memory attachee.spells_memorized_forget() spell_found = 0 # Try to cast each spell on the list, in order, until it finds one that is "memorized" for spell in range (0, len(spells)): if spells_qty_list[spell] > 0: attachee.spell_memorized_add (spells[spell][0], 17 ,spells[spell][1]) [B]spellbank_qty_decrease[/B] (attachee, 1, spell) # lower this spell's memorized qty by 1 spell_found = 1 break if spell == 9: break # If spells have run out, start using scrolls if spell_found == 0: for scroll in range (0, len(scrolls)): if scrolls_qty_list[scroll] > 0: attachee.spell_memorized_add (scrolls[scroll][0], 17 ,scrolls[scroll][1]) attachee.float_mesfile_line( 'mes\\float.mes', 201, 1) [B]spellbank_qty_decrease[/B] (attachee, 2, scroll) # lower this scroll's owned qty by 1 break if spell == 9: break return RUN_DEFAULT # Create unused scrolls in the dead body def san_dying( attachee, triggerer ): scrolls = (9468,9490,9288,9438) scrolls_qty_list =[B] spellbank_qty_get[/B] (attachee, 2, len(scrolls)) for s in range (0, len(scrolls)): for qty in range(0,scrolls_qty_list[s]): create_item_in_inventory(scrolls[s],attachee) if should_modify_CR( attachee ): modify_CR( attachee, get_av_level() ) return RUN_DEFAULT Here are the 3 main functions that handle all the bit manipulation and calls to npcvar_x(). I wanted to keep all that bit shifting shennanigans out of the san_ routines and have it contained in separate functions, which I have. Code: #---------------------------------------------------------------------------------------- # Stores the quantity of each spell to the specified bank (npcvar_1, 2 or 3). # The max number of spells in a bank is 10, and the max quantity for each spell is 7. # The quantity for each will be stored as 3 bits in one of the npcvar_x() values. # # attache: The object handle of the spellcaster # bank: The number where the quantities will be stored as an integer. # bank 1 in npcvar_1(), bank 2 in npcvar_1(), bank 3 in npcvar_3() # qty_tuple: A tuple of quantities corresponding to the number of each spell in # the bank. The bank of spells themselves will be declared in the # spellcaster's san_start_combat(). # # Example: spellbank_qty_set(attachee, 2, (1,2,2,0,7)) will set the quatities for # bank 2 as 00000000000000000 111 000 010 010 001 in a call to npcvar_2(). #---------------------------------------------------------------------------------------- def spellbank_qty_set (attachee, bank, qty_tuple,): qty_int, shift = 0, 0 for qty in qty_tuple: if qty > 7: qty = 7 qty_int = qty_int | ( qty << shift ) shift += 3 if shift > 30: break npcvar (attachee, qty_int, bank) #----------------------------------------------------------------------------------------- # Reduce the count by 1 of the spell in the specified slot of the specified bank. # Used after a spell is cast to reduce the number of spells memorized for that spell. # Used after a scroll is used to reduce the number of scrolls owned for that scroll. # # Example: spellbank_qty_decrease(attachee, 2, 4) will reduce the qty of the spell in # slot 4 of bank 2 by one. So (1,2,2,0,7) will become (1,2,2,0,6) #----------------------------------------------------------------------------------------- def spellbank_qty_decrease (attachee, bank, slot): memorized = getvar (attachee, bank) shift = slot * 3 if (memorized >> shift) & (int('111',2)): memorized = memorized - ( 1 << shift ) npcvar (attachee, memorized, bank) return #---------------------------------------------------------------------------------------- # Returns a list of the quantity of each spell from the specified bank. # # Example: spellbank_qty_get(attachee, 1, 5) will return [1,2,2,0,7] if bank 1 # has a value of 00000000000000000 111 000 010 010 001. #---------------------------------------------------------------------------------------- def spellbank_qty_get (attachee, bank, spell_list_size): memorized = getvar (attachee, bank) memorized_list, shift = [], 0 for spell in range(0, spell_list_size): memorized_list.append ( (memorized >> shift) & (int('111',2)) ) shift += 3 if shift > 30: break return memorized_list #------------------------------------------------------------------------------------ # npcvar, getvar #------------------------------------------------------------------------------------ def npcvar (attachee, var, bank): if bank == 1: npcvar_1 (attachee, var) if bank == 2: npcvar_2 (attachee, var) if bank == 3: npcvar_3 (attachee, var) return def getvar (attachee, bank): if bank == 1: var = getvar_1(attachee) if bank == 2: var = getvar_2(attachee) if bank == 3: var = getvar_3(attachee) return var This was inspired by Sitra Achara's post about 5 posts up from this one. I'll edit things as I find bugs and make upgrades.