Can enemy spellcasters use scrolls?

Discussion in 'General Modification' started by marc1967, Mar 16, 2015.

Remove all ads!
  1. marc1967

    marc1967 Established Member

    Joined:
    Jan 19, 2014
    Messages:
    578
    Likes Received:
    60
    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).
     
  2. Gaear

    Gaear Bastard Maestro Administrator

    Joined:
    Apr 27, 2004
    Messages:
    11,029
    Likes Received:
    42
    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.
     
  3. marc1967

    marc1967 Established Member

    Joined:
    Jan 19, 2014
    Messages:
    578
    Likes Received:
    60


    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.
     
    Last edited: Mar 16, 2015
  4. Rudy

    Rudy Established Member

    Joined:
    Jan 30, 2005
    Messages:
    345
    Likes Received:
    2
    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?
     
  5. Shiningted

    Shiningted I want my goat back Administrator

    Joined:
    Oct 23, 2004
    Messages:
    12,655
    Likes Received:
    352
    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.
     
  6. Daryk

    Daryk Veteran Member

    Joined:
    Jan 14, 2012
    Messages:
    1,170
    Likes Received:
    32
    I thought the priest in the Nulb ambush used scrolls. Might be worth looking into...
     
  7. Shiningted

    Shiningted I want my goat back Administrator

    Joined:
    Oct 23, 2004
    Messages:
    12,655
    Likes Received:
    352
    Almost definitely, as does the Skeletal Priest at Emridy Meadows (for Death Knell. Nasty).
     
  8. marc1967

    marc1967 Established Member

    Joined:
    Jan 19, 2014
    Messages:
    578
    Likes Received:
    60
    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.
     
    Last edited: Mar 16, 2015
  9. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    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

    P.S. thanks for the correction, will fix!
     
    Last edited: Mar 16, 2015
  10. marc1967

    marc1967 Established Member

    Joined:
    Jan 19, 2014
    Messages:
    578
    Likes Received:
    60
    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
     
  11. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    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.
     
  12. Daryk

    Daryk Veteran Member

    Joined:
    Jan 14, 2012
    Messages:
    1,170
    Likes Received:
    32
    Marc, thanks for letting me know I wasn't imagining things... :)
     
  13. Shiningted

    Shiningted I want my goat back Administrator

    Joined:
    Oct 23, 2004
    Messages:
    12,655
    Likes Received:
    352
    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).
     
  14. marc1967

    marc1967 Established Member

    Joined:
    Jan 19, 2014
    Messages:
    578
    Likes Received:
    60
    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.
     
    Last edited: Mar 17, 2015
Our Host!