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.
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:
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.
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.
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)
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.
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
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.
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.