ToEE's AI

Discussion in 'Tech Guides and Help Threads' started by Sitra Achara, Aug 9, 2008.

Remove all ads!
  1. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    Should Partial Charge fail, you could try spawning a Beacon object for the AI to target and approach (or perhaps Flank if Approach uses up ALL movement). I've done a similar thing in the Moathouse with the Guardsmen to make them wake each other up.

    However, be aware that past a certain range, the AI may simply fail to be activated at all. I've run in to that problem with Lareth, for instance - if the party were too far away (in the corridor), he would simply skip his turn without executing any move command. I say 'may' because there are instances where the AI does seem to work at range, so this may be some hardcoded per-map behavior.
     
  2. Gaear

    Gaear Bastard Maestro Administrator

    Joined:
    Apr 27, 2004
    Messages:
    11,029
    Likes Received:
    42
    Interesting ... I guess I had basically assumed that for practical purposes, or in other words, target weak, target dumb, target clumsy - and in the case of spellcasters, corresponding to what spells exploit those weaknesses.

    Tarah the slave chick does indeed use wizard spells (and I think may actually be a wizard?) - that's been normal modding behavior since the sorcerer limitations were figured out.

    Good job, marc. :thumbsup:
     
  3. marc1967

    marc1967 Established Member

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

    Oh boy, you've made my day, I think that just might work if I do something like this:

    Code:
    def san_start_combat( attachee, triggerer ):
    
    	# closest target less than 30 feet, cast close range spell
    	if closest_enemy_distance (attachee) <= 30:
    		attachee.obj_set_int(obj_f_critter_strategy, 577)
    
    	# closest target more than 60 feet, approach
    	elsif closest_enemy_distance (attachee) > 60:
    		attachee.obj_set_int(obj_f_critter_strategy, 578)
    
    	# closest target between 30 and 60 feet
    	else:
    		spawn_decoy ( )   # spawn the decoy object 30 feet away
    
    		# approach decoy, then target closest non-decoy enemy which should now be in short range, cast spell
    		# (target closest   approach   target closest   cast single   'Ray of Enfeeblement' class_wizard 1)
    		attachee.obj_set_int(obj_f_critter_strategy, 579)
    
    	return RUN_DEFAULT

    The three challenges are now:

    1) Getting the X,Y where to spawn the decoy. (probably some simple geometry arithmatic computing the half way point between the location of the spellcaster and the closest target)

    2) Having the decoy approachable, but not a valid target for a spell. (maybe the decoy self-destucts on being approached, or has some flag set that makes in untargetable?)

    3) When and where to destoy the decoy. (san_end_combat?, san_heartbeat?)

    I think this can be done though. When and if I figure it all out I'll edit this post with the actual code.
     
  4. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    It's a challenge alright, be prepared for lots of hair pulling and debugging ;)

    For (2): A couple of possible solutions here.
    One is to make the Beacon a friendly target with a very low or very high AC, and use target friend high ac / target friend low ac. I think the moathouse script does that.

    The other is to adapt the 'target damaged' targeting system. This is used in making Co8 critters ignore summons and Spiritual Weapons - see combat_standard_routines.py. In short what it does is temporarily boost the desired target's HP by 1000 and also apply 1000 damage, so it registers as the most damaged.
    Thus the targeting command for approaching the beacon would be target closest, and for attacking it would be 'target damaged' in conjuction with a modified Spiritual_Weapon_Begone() script.

    (3) IIRC I did this via a timed event, but perhaps it should be better managed by checking the combatants' initiatives and selecting which san_start_combat it is appropriate to do it from.
     
  5. marc1967

    marc1967 Established Member

    Joined:
    Jan 19, 2014
    Messages:
    578
    Likes Received:
    60
    It's working! And such a pleasure to see the spellcaster run up 30 feet and then cast his spell just like a player would. It took a surprisingly small amount of code, and your help was invaluable, thanks.

    After trying various methods, the one that seemed to work best was to create the beacon (I call it 'decoy' in my code) as a transparent enemy with a low AC of 1. The NPC will 'target low ac' to approach it, and then 'target high ac' to cast the spell. Then the decoy gets destroyed in the spellcaster's san_end_combat() before the next person's initiative is up. The only glitch is that I had to add it to the initiative with decoy.add_to_initiative() for the spellcaster to recognize it, and occasionally the decoy shows up on the initiative line at the top of the screen until the spellcaster's turn is over.

    The code is still very rudimentary and very specific for this npc (level 1, so short range is 25 feet, move of 30), but this could be made more generic by accessing those specific values for the spellcaster in question. And it may get unreliable in closer dungeon settings with obstructions in the way, but for now it works great in most areas.

    Code:
    
    def san_start_combat( attachee, triggerer ):
    
    	closest_enemy = get_closest_enemy(attachee)
    	distance = attachee.distance_to(closest_enemy)
    	
    	# ENEMY IS IN CLOSE RANGE (target closest, cast spell)
    	if distance <= 25:
    		attachee.obj_set_int(obj_f_critter_strategy, 574)
    
    	# ENEMY IS TOO FAR AWAY (target closest, approach)
    	elif distance > 55:
    		attachee.obj_set_int(obj_f_critter_strategy, 577)
    
    	# ENEMY IS OUT OF CLOSE RANGE, BUT CAN BE BROUGHT INTO RANGE IF THE NPC MOVES CLOSER FIRST
    	else:
    
    		# GET THE COORDINATES OF THE NPC AND THE ENEMY
    		x1,y1 = location_to_axis (attachee.location)
    		x2,y2 = location_to_axis (closest_enemy.location)
    
    		# USE QUESTIONABLE TRIGONOMETRY TO GET A POINT 30 FEET AWAY
    		ang = atan ( float(abs(y2-y1)) / float(abs(x2-x1)) )
    		x30 = 30 * cos(ang) * 0.45
    		y30 = 30 * sin(ang) * 0.45
    		if x1 > x2:
    			x = x1 - x30
    		else:
    			x = x1 + x30
    
    		if y1 > y2:
    			y = y1 - y30
    		else:
    			y = y1 + y30
    
    		# SPAWN A DECOY WITH A LOW AC, 30 FEET TOWARDS THE ENEMY 
    		decoy = game.obj_create ( 14929, location_from_axis(int(x),int(y)) )
    		decoy.add_to_initiative()
    
    		# APPROACH THE DECOY, THEN CAST A SPELL ON AN ENEMY (target low ac, approach, target high ac, cast spell)
    		attachee.obj_set_int(obj_f_critter_strategy, 575)
    
    	return RUN_DEFAULT
    
    
    def san_end_combat( attachee, triggerer ):
    	
    	for obj in game.obj_list_vicinity(attachee.location,OLC_CRITTERS):
    		if obj.name == 14929:
    			obj.destroy()
    	return RUN_DEFAULT
    
    
    def get_closest_enemy (npc):
    
    	closest_distance = 9999
    	closest_enemy = npc
    
    	for enemy in game.party[0].group_list():
    		distance = npc.distance_to(enemy)
    		if (distance < closest_distance) and (not enemy.is_unconscious()) and (not enemy.d20_query_has_spell_condition(sp_Otilukes_Resilient_Sphere)):
    			closest_distance = distance
    			closest_enemy = enemy  
    	return (closest_enemy)
    
     
  6. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    Cool.

    Re. the decoy being added to the initiative bar - it should be less noticeable if it's the same portrait as a pre-existing one. If you want to make the script generic thing, you can try reading / writing obj_f_critter_portrait.
     
  7. marc1967

    marc1967 Established Member

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

    I was intrigued by this, and did some experimenting. This is what I learned:

    "What AI routine was he using? Did he have commands other than spell casting after the spell list? (attack, etc)"

    It doesn't seem to matter, as long as the strategy leaves an extra action to cast a spell after it has finished it's strategy. The spells are selected directly from the spell list in protos.tab.​

    "What were his targets? Did they match the targeting commands?"

    It targets whatever the current target is when the strategy is finished (closest, ranged, etc.). They don't match the targets from the spell's entry in strategy.tab. If there is no current target, it defaults to the first enemy in the initiative order. If the current target was 'target self' or 'target friend' the spells get cast oddly; many fizzle, many still target an enemy.​

    "Does this only apply to cantrips? Were the spells offensive spells or defensive spells, and were they appropriately cast on friend/foe?"

    It will cast most spells that I tested (offensive and defensive), as long as the spell is listed in the creature's proto as a spell level of '0', which is the key. It doesn't need to be a cantrip, as long as the '0' is there. So all of these will work:

    'Fireball' class_wizard 0
    'Magic Missile' class_wizard 0
    'Heroism' class_wizard 0
    'Bull's Strength' class_wizard 0
    'Charm Monster' class_wizard 0
    The good part is that the spell's correct caster level and DC are retained, so a 6th level wizard with Intelligence 18 will still cast Fireball for 6d6 damage, CL = 6, DC = 17 (10+3+4).

    The bad part is that I've never seen them buff themselves, even when the final command was 'target self' in it's strategy. They oddly cast these spells on the enemy too. It seems to be an 'attack the enemy' oriented default action.
    Behavior: The spellcaster attacks with his weapon on half his turns, and casts a spell on the enemy the other half with no particualr logic. So it may be like this:

    turn 1. Attack
    turn 2. cast Magic Missile
    turn 3. Attack
    turn 4. Attack
    turn 5. cast Fireball
    turn 6. cast Acid Splash
    turn 7. Attack
    turn 8. cast Charm Monster
    turn 9. cast Cone of Cold
    turn 10. Attack​

    The only use I can see for this is some creature with spell abilities or a warrior/sorcerer who will attack unpredictably to keep the player on their toes. This would almost work for the Salamanders who dump continuous fireballs and then move to attacking. Once you know the trick it's easy to follow the pattern. Setting their strategy to a simple 'clear target' and letting randomness run amok would crazy things up. Of course such randomness could be scripted too, but just trying to think of a use for this.

    Edit: One last thing, they do a proper 5' step before casting offense
     
  8. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    Fortunately the trial & error is no longer necessary - I rewrote the relevant function for Temple+, which also fixes the 'cantrip only' bug so it now applies to all spells in general. (See ai.cpp in the github repository)

    It's also explicitly random in choosing whether to cast an offensive/defensive spell (or at all), but the chance can be modified in ai_params.mes iirc. The selection of spell from the available list is also random.
     
    Last edited: May 4, 2016
  9. marc1967

    marc1967 Established Member

    Joined:
    Jan 19, 2014
    Messages:
    578
    Likes Received:
    60
    There is a way to target the exact PC you want in strategy.tab, so you don't have to hit-and-miss with commands such as 'target high ac' and 'target ranged'.

    A default target is always selected before a creature's strategy is processed, and any command given in the strategy will act upon that target until a new one is selected or a 'clear target' is given. This target is normally the closest, but it can be defined by script with attachee.attack(obj). So it would go something like this:

    1. Select the exact PC you want as your target, based on the logic you script.
    2. Loop attachee.ai_shitlist_remove() on everyone in game.party to reset previous targets.
    3. Set the PC to be targeted with attachee.attack(obj).
    4. Then in srtategy.tab, the target will be that PC.​

    Here's a quick example where a wizard will target Tasha's Hideous Laughter on a particular PC. The logic of selecting the PC can be made as complicated as you want, and for this example I don't want to get diverted by the scripting of how the PC is selected, so it simply picks the PC with the highest HP total who is not already prone by as previous Tasha's.

    Code:
    def san_start_combat (attachee, triggerer):
       if attachee.leader_get() == OBJ_HANDLE_NULL:
    
         # Find the party memeber with the most HP
         target = OBJ_HANDLE_NULL
         hp_highest = 0
         for obj in game.party:
           hp = obj.stat_level_get(stat_hp_current)
           if obj.d20_query(Q_Prone) or obj.is_unconscious():
             continue
           if obj.distance_to(attachee) > 35:
             continue
           if hp > hp_highest:
             target = obj
             hp_highest = hp
    
         # Make him the default target
         if target != OBJ_HANDLE_NULL:
           for obj in game.party:
             attachee.ai_shitlist_remove(obj)
           attachee.attack(target)
    
       return RUN_DEFAULT
    
    

    Then all you need is a strategy like this:

    Code:
    Wizard   cast single     'Tashas Hideous Laughter' class_wizard 2   target closest       sniper
    Notice no target is selected at the start, as the default is set int san_start_combat().

    In full implementation, you would need to make sure a target is selected intelligently beyond the casting of Tasha's, and the strategy is expanded to handle additional spells and attacking.

    I've never read this in a thread elsewhere, so maybe this is helpful.
     
    Last edited: May 19, 2016
  10. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    That's quite an ingenious trick, hats off!
     
Our Host!