Pierce through multiple enemies in a line

Users who are viewing this thread

Solution (working code base by SupaNinjaMan) marked below.
My expanded code with bugfixes and tweaks in this post: https://forums.taleworlds.com/index...ultiple-enemies-in-a-line.458998/post-9873397

Hello my dear friends.
I've had this idea, and I really wish to make it happen, but I'm probably really overestimating my abilities here... so any help or advice is welcome.

Basically, when attacking with a couched lance, I kill the 1st opponent hit, my lance goes up and that's it.
Lances however (especially long ones, and that's what I have) are famous for piercing multiple opponents at once, for example Polish hussar was recorded to pierce through 6 Moscow footmen at once in 1660.06.28 in battle of Połonka.

History aside, here's what I'm thinking:
1. [easy] detect a hit with lance (probably better to limit it to killing blows)
2. [hard] make sure it was couched hit (if there is no way to check it natively, can probably check base thrust damage, then compare with damage dealt? complicated, but with possible workarounds)
3. [easy] get killer agent's position
4. [doable] calculate position about 2m from that position (some testing required with fixed point multipliers, I guess)
5. [how???] detect agents whose hitboxes are in a straight line from that position
6. [easy] deal damage to them, [hard] preferably less and less with every consecutive agent
 
Last edited:
Solution
hb_item_r should be defined inside header_common, if not, the Fate bitbucket has it inside header_common or header_constants. You can also check the human skeleton inside OpenBRF.

I had time to copy it over, slow at work.

Code:
#human bones
hb_abdomen = 0
hb_thigh_l = 1
hb_calf_l = 2
hb_foot_l = 3
hb_thigh_r = 4
hb_calf_r = 5
hb_foot_r = 6
hb_spine = 7
hb_thorax = 8
hb_head = 9
hb_shoulder_l = 10
hb_upperarm_l = 11
hb_forearm_l = 12
hb_hand_l = 13
hb_item_l = 14
hb_shoulder_r = 15
hb_upperarm_r = 16
hb_forearm_r = 17
hb_hand_r = 18
hb_item_r = 19

Keep in mind it failed for my use, but I re-read your initial post and intended use, this is going to be easy-peasy to apply to the ti_on_agent_hit trigger and work just about...
Could also be a solution to have an invisible arrow (via add_missile) getting released when a first enemy is hit, doing the rest on its own.
I recall something about that if it has a very high speed it pierces multiple enemies if they are directly behind each other. It's also something I experienced sometimes at multiplayer at the NeoGK servers when a ballista sometimes shoots down rows of enemies.
https://earendil_ardamire.gitlab.io...on_Module_System/Module_Items.html#footnote-5
 
Upvote 0
I may be misunderstanding, but the article you linked seems to suggest that missile will *pass* through multiple enemies if it's too fast, but the result is skipping an agent and only damaging somebody behind him. Doesn't seem like the missile would damage all of them in a row and trigger the appropriate triggers on all of them.

Anyway, balista seems like exactly the same mechanic that I'm looking for, so maybe I can find some code on how to make an invisible balista on agent hit 🤔
 
Upvote 0
Ideally you could cast a ray the length of the weapon and see if it collided with an agent and deal damage that way, but, the base engine's cast_ray operation only works for scene collisions. The workaround I have for this is, hacky, but it works. I have learned more about ray casts since this was written 3 years ago, so it could definitely be refined, but it will be a good enough jumping off point for your work.

From my post here: Requires my lookat! script or the one from VC.
Python:
        (mouse_get_world_projection, pos30, pos31),   
        # So, this is used when we click to figure out where the cursor would be in world space.
        # Returns current camera coordinates (pos30) and mouse projection to the back of the world (pos31)
        # Unfortunately, you cannot use these as is to determine where you clicked, but it does draw a straight line
        # from the cursor to the rear of the world, so if you were to use look at to point 30 to 31, you get a perfections
        # caster for cast_ray, which will return where in 3d space the cursor actually was.

       
            (call_script, "script_lookat", pos30, pos31),    # This makes the cursor screen position face the cursor projection position
            (cast_ray, reg1, pos3, pos30),                    # Take pos30, facing pos31, and make a ray to see what is in the way (what we clicked)
            (get_distance_between_positions_in_meters, ":distance", pos30, pos3),    # we are going to do a range check each meter along the ray. This can be expensive
            (val_add, ":distance", 1),
        
            (try_for_range, ":iteration", 0, ":distance"),
                (copy_position, pos12, pos30),
                (val_mul, ":iteration", 100),
                (position_move_y, pos12, ":iteration"),
                (try_for_agents, ":agent", pos12, 180), # This is a bit big, 1.8m, but since we need to grab the agent's origin to know if it's within range, it seems fair.
                    (neq, ":agent", ":player"),
                    (str_store_agent_name, s10, ":agent"),
                    (display_message, "@This seems to indicate the ray hit an agent named {s10}"),
                    (try_for_range, ":bone", 0, 19),
                        (agent_get_bone_position, pos13, ":agent", ":bone", 1),
                        (position_transform_position_to_local, pos14, pos30, pos13), # We want to see how close the bone position is relative to the raycaster, this is to determine how far off-axis it is from the caster
                        (position_get_x, reg12, pos14),
                        (position_get_y, reg13, pos14),
                        (position_get_z, reg14, pos14),
                        (is_between, reg12, -15, 16), # within 30cm of the caster's forward axis
                        (is_between, reg14, -15, 16), # within 30cm of the caster's forward axis
                        (val_add, ":bone", "str_bone_abdomen"), # I have strings that reflect the bone names, for debugging purposes
                        (str_store_string, s11, ":bone"),
                        (display_message, "@At Bone {s11}"),
                    (try_end),
                (try_end),
            (try_end),

You will have to adapt this from a presentation to a trigger, and there is a lot of bloat related to camera controls and it is reliant on lots of strings and named registers from my mod, but the core of the idea will be there for you to dig out.

To break it down:
  1. Perform an actual raycast to determine the furthest point we have to check to see if the agents are in front of the "ray" caster
    1. For your purposes you won't need to do this since you know the maximum distance already
  2. Advance from the cast position by a fixed distance to figure out if there is an agent within it's range
    1. For your purposes it would be more cheaper to create a range about a meter longer than the weapon's reach and do it that way, but w/e
  3. Determine the locations of their bones to fake collisions to some degree
    1. Ideally you would also check along the length of the bones, but, I was lazy and this worked okay.
I'm at work rn so I can't work on the post too much, but tonight I'll comeback and make it actually make sense, lol
 
Last edited:
Upvote 1
Wow, this looks like it's gonna be useful. I will probably spend a few weeks digesting it, because jeez it's as difficult as I thought it would be... But at least it no longer looks impossible!
 
Upvote 0
This is how I would do it
  1. Set up agent slots:
    1. slot_agent_last_action
      1. We will use this to track if the current action is new, and set up the next slot
    2. slot_agent_couchable_targets
      1. We will use this to track how many more people we can pierce with the lance
    3. slot_agent_intangibility_timer
      1. We will use this to track how much longer an agent will be intangible, so we don't hit them twice with a couched attack
  2. Set up item slots:
    1. slot_item_max_pierce
      1. This way you can set up different tiers of piercing, totally optional
  3. Make mission template triggers that:
    1. per 0.1, checks an agent's animation compared to last_action slot
      1. determine action based on animations
      2. if different, set the current action as last action, and set up relevant slots
      3. i.e., if idle to couch, set couchable_targets to the item_max_pierce and seet last_action as couch
      4. If this sounds confusing, this is my implementation of this same action tracking system. It's easier than it sounds. I use this system in all of my mods.
    2. per frame, check an agent last_action slot is eq, couching
      1. if they are couching, check that their couchable_targets is gt, 0, if so do the collision check faked above, giving it an additional length depending on their horse agent's speed. We want to catch a couch before it happens. Skip targets who have some value in their intangibility timer. If there will be a couched strike, set their intangiblity_timer to 2 or 3, set their agent_set_no_dynamics to true, deal damage with a faked deal couched lance damage message, subtract 1 from the agent's couchable_targets
      2. This is going to be the most expensive, as it fires a try_for_agents loop each frame. You may want to make it have a very small amount of time, but per-frame is going to be more accurate
    3. per 0.1, check an agent's intangibility timer, if gt 0, subtract 1.
      1. if eq, 0, set agent's agent_set_no_dynamics to false. Allowing for a short window of non-collision so they cannot be hit twice.
      2. you can easily fold this into the last_action trigger to make it more performance friendly.
The hard part is obviously the collision check, so it's easy to gloss over it and act like this is a simple system with only a few steps.

I need to update my Multihit trigger for the Dragonslayer in B:R, so if you haven't had a breakthrough in a few days, I'll post my version using a swinging sword instead of a couched lance.

Additionally, I lied above, the lookat is only necessary for the camera. You can fake the collision check by using the agent's own item.r bone. It points the length of the weapon.

Additionally:Additionally, WSE2 has a cast_ray operation for agents that I coincidentally commissioned for the inclusion of. That would make your life 1000000000x easier. It does limit the available player base for you mod, however.

After some testing, agent_set_no_dynamics does not disable the collision for the purposes of attacks. The agent is still tangible for attacks and will stop the weapon from continuing through them. Their blocks and shields will also continue to work, even if they are damaged a few frames prior.
 
Last edited:
Upvote 0
I tried to experiment, and essentially add a passive secondary hitbox to have an accurate cut-through mechanic but ultimately it was a failure for my use case.

Even though the new passive hitbox dealt damage to the additional expected enemies, the issue was that the weapon will still collide with the body, shield or block of that first enemy in the swing and stop dead in its tracks and there's no way to disable hitboxes temporarily. I could get around this by making the attack just play the appropriate attack animation and not be a true attack, but that leaves me without a real attack after the max cut-throughs have been reached.

The core code is just the same as above where I captured nearby agents and checked if their bones were within a reasonable range of the axis of the caster, just with a distance from the caster being checked as well.

Python:
(try_for_range, ":bone", 0, 19),
    (agent_get_bone_position, pos13, ":nearby", ":bone", 1),
    (position_transform_position_to_local, pos14, pos2, pos13), # We want to see how close the bone position is relative to the raycaster, this is to determine how far off-axis it is from the caster
    (position_get_x, reg12, pos14),
    (position_get_y, reg13, pos14),
    (position_get_z, reg14, pos14),
    (is_between, reg12, -15, 16), # within 30cm of the caster's forward axis
    (is_between, reg14, -15, 16), # within 30cm of the caster's forward axis
    (is_between, reg13, 0, ":i_length"),    # Within reach of my weapon
    (assign, ":struck", 1),
(try_end),

(eq, ":struck", 1),
# Deal Damage
With pos2 being the damaging agent's hb_item_r bone in global space and ":i_length" being the length of the wielded item.

For your use case, this would still work. Determine when a couched lance strike has happened and just continue through the struck agent to check if there are agents within range and who would be hit and dealing damage.
 
Upvote 0
@SupaNinjaMan Thank you, I will get to testing it this weekend. Just a quick question: when setting pos2 with agent_get_bone_position, which number is hb_item_r bone? In the loop I see you referring to bones by numbers 0-19, so I wonder if I can just use "hb_item_r" explicitly.
 
Upvote 0
hb_item_r should be defined inside header_common, if not, the Fate bitbucket has it inside header_common or header_constants. You can also check the human skeleton inside OpenBRF.

I had time to copy it over, slow at work.

Code:
#human bones
hb_abdomen = 0
hb_thigh_l = 1
hb_calf_l = 2
hb_foot_l = 3
hb_thigh_r = 4
hb_calf_r = 5
hb_foot_r = 6
hb_spine = 7
hb_thorax = 8
hb_head = 9
hb_shoulder_l = 10
hb_upperarm_l = 11
hb_forearm_l = 12
hb_hand_l = 13
hb_item_l = 14
hb_shoulder_r = 15
hb_upperarm_r = 16
hb_forearm_r = 17
hb_hand_r = 18
hb_item_r = 19

Keep in mind it failed for my use, but I re-read your initial post and intended use, this is going to be easy-peasy to apply to the ti_on_agent_hit trigger and work just about perfectly.

Python:
lance_penetration_system = (
 ti_on_agent_hit, 0, 0, [],
[
    (set_fixed_point_multiplier, 100),        # I wanna work in centimeters
    (store_trigger_param_1, ":agent"),        # Who is being hit, to ignore for the piercing damage
    (store_trigger_param_2, ":attacker"),    # Who did the damage?
    (agent_get_horse, ":horse", ":attacker"),    # And their horse! To prevent killing it!
    (assign, ":attacker_weapon", reg0),        # and with what?
    (agent_get_position, pos10, ":attacker"),        #
    (gt, ":attacker_weapon", 0),                    # prevent errors from the item_get_weapon_length getting served -1
    (neq, ":attacker_weapon", "itm_pitch_fork"),    # prevent stabthrough damage from repeating the checks. You can use whatever.
    (agent_get_animation, ":upper_anim", ":attacker", 1),    # What is there upper body doing?
    (item_get_weapon_length, ":range", ":attacker_weapon"),    # Range for the try_for_agents
    (store_add, ":length", ":range", 25), # I want to fudge the length a bit, just to give the poke more ~poke~
    (val_add, ":range", 150),    # Add an additional 1.5m to find nearby agents

    (is_between, ":upper_anim", "anim_lancer_ride_4", "anim_ride_rear"), # couched anims live here
    (agent_get_bone_position, pos13, ":attacker", 19, 1),    # bone 19 is the right hand item bone, btw
 
    (try_for_agents, ":nearby", pos10, ":range"),
        (neq, ":nearby", ":attacker"),   
        (neq, ":nearby", ":agent"),
        (neq, ":nearby", ":horse"),
        (agent_is_active, ":nearby"),
        (agent_is_alive, ":nearby"),
        (neg|agent_is_wounded, ":nearby"),    # You don't want to check dead guys, they don't have bones and will break the engine
        (neg|agent_slot_ge, ":nearby", 200, 1), # We want to prevent an infinite loop of agents being hit when a single pierce-through goes off, lol.
        (assign, ":struck", 0),    # Loop breaker + strike determination
        (try_for_range, ":bone", 0, 19),
            (neq, ":struck", 1), # Prevent some operations in case of a detected hit. Just to save some performance
            (agent_get_bone_position, pos15, ":nearby", ":bone", 1),
            (position_transform_position_to_local, pos14, pos15, pos13), # We want to see how close the bone position is relative to the raycaster, this is to determine how far off-axis it is from the caster
            (position_get_x, reg12, pos14),
            (position_get_y, reg13, pos14),
            (position_get_z, reg14, pos14),
            (is_between, reg12, -35, 36), # within 60cm of the caster's forward axis
            (is_between, reg14, -35, 36), # within 60cm of the caster's forward axis
            (is_between, reg13, 0, ":length"),    # Within reach of my weapon
            (assign, ":struck", 1),
        (try_end),
        (eq, ":struck", 1),
        (agent_deliver_damage_to_agent, ":attacker", ":nearby", 50, "itm_pitch_fork"), # You can get more complicated and figure out a formula based on agent speed, distance from attacker, etc. Flat 50 for now tho.
        (agent_set_slot, ":nearby", 200, 2),    # Prevents a potential infinite loop where being stabbed through creates a loop of stabbing the ones before/after them again.
    (try_end),
])

couched_timer = (0.1, 0, 0, [], [
    (try_for_agents, ":agent"),
        (agent_slot_ge, ":agent", 200, 1),
        (agent_get_slot, ":timer", ":agent", 200),
        (val_sub, ":timer", 1),
        (agent_set_slot, ":agent", 200, ":timer"),
    (try_end),
])

One note, it doesn't seem that agents are ever close enough to one another to actually do this with a realistic lance length and extra distance.
 
Last edited:
Upvote 1
Solution
@SupaNinjaMan I did some quick testing and it seems like it's working pretty well! Not much engine strain, good results, easily portable into my own code. There is only 1 thing that I think needs fixing.

My testing seems to indicate that pos14 (which is basically a vector pointing out along the weapon's length and determines the area of effect), is pointing to the ground.
I mean, obviously sitting high up on a horse we have to aim the lance somewhat downwards, because enemies are below us.
The thing is, when piercing multiple agents, the lance will (either break or) align itself parallel to ground level AFTER striking the 1st target.
But for our code there is no "after", we calculate everything at the moment of first impact.
So, to simulate the correct area of effect, we should tweak pos14, so that it has smaller downwards tilt. (or fully horizontal, 100% parallel to the ground, but positioned lower, at the height of the point of initial agent being hit.
I have no idea how to do this, so if you could please help with this one last thing.
I understand your code thanks to plentiful comments, but I'm still having trouble *making* something like this. All those local pos, global pos, is still 4D chess for me. I really appreciate the help.
So, if you have a Paypal or something, I'm willing to send you $20. It's not much by American standards, but should be enough for a beer to celebrate work well done.

Another thing that really needs fixing is friendly fire. I tweaked the area of effect up for testing, to see if the code works at all, and my squad wiped out half of itself in 1 charge. So no damage to allies and horses of allies, damage only to enemies and horses of enemies. But I think I should manage this myself. Will just check teams for humans, and for horses get rider and check teams again.
 
Last edited:
Upvote 0
No need to send anything. Modding is, for me, all about the pure act of creation within restrictions. Helping the community is just a good way to take on challenges and find creative solutions to problems.

Inside ti_on_agent_hit pos1 by default stores the position the lance hits the enemy. You can use that instead of the hb_item_r pos as the point from which you are checking for collisions.

I don't know what the rotations look like in the hit position so you might need to use the operation position_copy_rotation to copy the rotation of the attacker position over the hit position. That should make the rotation parallel to the ground and go in same direction as the rider.
 
Upvote 0
I'm digesting the bones information.
Your code does (try_for_range,":var",0,19) - which means looping through 19 bones numbered 0-18.
Meanwhile you've listed 20 bones numbered 0-19.
On top of that, I checked in OpenBRF and it turns out that 4 of those bones don't have hitboxes? (the "Exists" checkbox is unchecked for them).
To make things worse, horses have different bones - 28 of them, where 8 has no hitboxes.
Should I somehow skip them? Prepare an array of relevant bones and loop only through that?
 
Upvote 0
I think I skipped 19 intentionally since it is an item bone, but it might have been a mistake.

Whether or not a hitbox exists doesn't matter since we are checking relative position not collisions.

Just go through the full loop but have a (neq, ":var", X), replace X with the id of the bones you want to skip.
 
Upvote 0
Ah, I get it. Thanks. This will allow me to proceed.
I will do something like
(assign, num_bones, 20),
if horse, (val_add, ":num_bones", 8 ),
(try_for_range, ":bone", 0, ":num_bones),
 
Upvote 0
I will post finished code once I'm done tweaking it. May take a long time though... Every time I think I get it and start making changes, the whole thing breaks - gotta test after each line I change. So thank god that I got code that works out of the box. Makes testing my changes possible. Progress is slow, but I'm getting there.

So far I noticed a few crucial things to fix:
1. the 1st agent hit (that fired up the trigger) sometimes survives 🤔
I execute code triggered by him getting hit with 450 dmg, trigger finishes and he's just standing there like:
33sr7r.png

Fixed by adding (set_trigger_result,":damage"),
2. friendly fire control is needed
3. because some lances are blunt and some aren't, can't deal damage with pitchfork, have to use attacker's weapon, so the neq weapon is pitchfork test is out - gotta use agent slots instead (we actually set the slot already, just gotta set it right before dealing damage, not right after)
4. timer doesn't have to be a timer, can be simplified to just marking agent as hit or not, and clearing later
5. some lances in my mod crush through blocks, so gotta accept 1 more anim (couched attack parried)
6. vector of attack needs tweaks, engine is weird, both weapon direction and blow direction sometimes point to who knows where (gotta copy more reliable agent rotation)
7. once friendly fire code is implemented, a lot of checks can be removed
8. better control for human/horse bones
9. should scan for targets around spear tip, not player - this way we can reduce the radius by a lot

It's this horrible combination of my inexperience and my perfectionism. Really slowing me down.

Edit:
So far I'm testing on a small party of 5 Looters, using agent_set_scripted_destination to clump them up. Allows me to count casualties.
Now that I have smoothed out a few irregularities, I gotta say it feels really satisfying to pierce all 5 in one stroke. Like bowling alley, man!
 
Last edited:
Upvote 0
This is what I came up with:
Python:
#MOD BEGIN - lance piercing multiple targets
slot_item_lance_pierce_best_multihit_score = 1 #for lances (same slot as food bonus)
slot_agent_just_pierced_by = 13 #this slot is free in my mod, might need to use different number if it's taken
#MOD END - lance piercing multiple targets
Python:
#MOD BEGIN - lance piercing multiple targets
#lance penetration system adapted from code by SupaNinjaMan - https://forums.taleworlds.com/index.php?threads/pierce-through-multiple-enemies-in-a-line.458998/post-9872324
rndl_lance_penetration_system_on_agent_hit = (ti_on_agent_hit,0,0,
    [],
    [
        #cheat sheet of used pos:
        #pos10 = point of weapon impact (on 1st agent hit)
        #pos11 = attacker
        #pos12 = struck bone (of next agent in line)
        #pos13 = local coordinates of pos12 relative to pos10
        #loop prevention check (must be done first)
        (store_trigger_param_1,":triggering_agent"), #get agent on whom the trigger fired
        (agent_slot_eq,":triggering_agent",slot_agent_just_pierced_by,-1), #make sure he wasn't hit by previous instance of this trigger (being the 2nd pierced through damage scripted below also triggers ti_on_agent_hit, so we're preventing an infinite loop here)
        #gather trigger data because something overwrites it in memory
        (store_trigger_param_2,":attacker"), #get attacker
        (store_trigger_param_3,":damage"), #damage amount from hit that fired this trigger
        (assign,":attacker_weapon",reg0), #get striking weapon that fired the trigger
        (copy_position,pos10,pos0), #get weapon's point of impact and blow direction
        #check if it was a couched lance hit
        (gt,":attacker_weapon",0), #prevent errors from the item_get_weapon_length getting served -1
        (agent_get_animation,":upper_anim",":attacker",1), #What is there upper body doing?
        (this_or_next|eq,":upper_anim","anim_lancer_ride_4"), #couched lance anim with shield equipped
        (this_or_next|eq,":upper_anim","anim_lancer_ride_4_no_shield"), #couched lance anim without shield
        (             eq,":upper_anim","anim_lancer_charge_parried"), #couched hit was parried (this anim will play with lance used its crush through blocks capability to deal damage
        #now we can proceed to hurt additional targets
        #determine strike vector
        (agent_get_position,pos11,":attacker"), #get attacker's direction
        (position_copy_rotation,pos10,pos11), #use it to correct the wonky direction of blow - this gives us a vector rooted where 1st pierced agent was hit, and pointing in the direction of attacker's movement (where the tip of the lance would go)
        #determine affected area
        (set_fixed_point_multiplier,100), #setting to working in centimeters (multiplier=1 would be meters)
        #when lance hits, the horse will continue moving forward, until reaching the enemy line, and then push into the enemy line a bit - the tip of the lance will move forward the same distance, so that's weapon's actual reach
        (item_get_weapon_length,":reach_of_attack",":attacker_weapon"), #the distance the horse will go is roughly weapon length (minus distance between weapon's grip and horse's front, plus it will push a bit into enemy line actually... so this bracketed part cancels out to roughly zero)
        #finally, we don't deal with hitboxes, but centers of bones - gotta increase reach by radius of bone
        (assign,":bone_diameter",60), #each bone is different, so it's a rough estimation
        (store_div,":bone_radius",":bone_diameter",2),
        (store_mul,":bone_radius_minus",":bone_radius",-1), #lower bound must be negative
        (store_add,":bone_radius_plus",":bone_radius",1), #must be upper bound +1 (in range calculations upper value is never reached)
        (val_add,":reach_of_attack",":bone_radius"), #and later when checking that reach, we will pad from the other side by checking bone_radius_minus to reach, instead of 0 to reach
        (store_add,":search_range",":reach_of_attack",50), #just in case let's scan for targets in an area larger than our reach, say +50 cm to search radius
        #common operations for all agents in the loop
        (try_begin), #count agents
            (agent_is_human,":triggering_agent"),
            (assign,":targets_struck",1), #one is already hit, that's our initial agent that fired the trigger
        (else_try),
            (assign,":targets_struck",0), #initial target is horse, we don't count them
        (try_end),
        (agent_set_slot,":triggering_agent",slot_agent_just_pierced_by,":attacker"), #mark him as already hit
        (agent_get_team,":attacker_team",":attacker"), #get attacker team for comparison with each agent in range
        #find valid targets
        (try_for_agents,":target",pos10,":search_range"), #search around spear tip (pos10)
            #(neq,":target",":triggering_agent"), #already covered by agent slot check below (we fill the slot somewhere above)
            (agent_slot_eq,":target",slot_agent_just_pierced_by,-1), #we need to prevent multiple pierces through 1 agent within 1 frame, so we only work on those not marked as already pierced (this is cleared every 0.1 sec, roughly every 6 frames, so still a high chance of being pierced by multiple targets)
            #(neq,":target",":attacker"), #covered by friendly fire check
            #(neq,":target",":attacker_horse"), #covered by friendly fire check
            #(agent_is_active,":target"), #not needed, try_for_agents only loops through active agents
            (agent_is_alive,":target"), #agent is not dead (dead agents have no bones and will break the engine when trying to check them)
            (neg|agent_is_wounded,":target"), #not wounded either (same problem as above)
            #set bones and friendly-fire check
            (try_begin), #human
                (agent_is_human,":target"),
                #human bones (there is 20):
                #0  = abdomen (lowest part of torso)
                #1  = thigh.L (left upper leg)
                #2  = calf.L (left lower leg)
                #3  = foot.L (left foot)
                #4  = thigh.R (right upper leg)
                #5  = calf.R (right lower leg)
                #6  = foot.R (right foot)
                #7  = spine (middle part of torso)
                #8  = thorax (upper part of torso)
                #9  = head
                #10 = shoulder.L (left shoulder)
                #11 = upperarm.L (left upper arm)
                #12 = forearm.L (left forearm)
                #13 = hand.L (left hand) - has no hitbox!
                #14 = item.L (base of item in left hand) - has no hitbox!
                #15 = shoulder.R (right shoulder)
                #16 = upperarm.R (right upper arm)
                #17 = forearm.R (right forearm)
                #18 = hand.R (right hand) - has no hitbox!
                #19 = item.R (base of item in right hand) - has no hitbox!
                (assign,":num_bones",19), #skip right-hand item bone, it's almost same pos as hand (this is just for performance, so no need to skip left-hand item, which is in the middle of bones)
                (agent_get_team,":target_team",":target"), #belongs to his own team
            (else_try), #horse
                #horse bones:
                #0  = abdomen (back half of torso)
                #1  = thorax (upper part of abdomen bone) - has no hitbox
                #2  = spine_2 (front half of torso)
                #3  = spine_3 (upper part of spine_2 bone) - has no hitbox
                #4  = neck (bottom part of neck)
                #5  = neck_2 (middle part of neck)
                #6  = neck_3 (upper part of neck)
                #7  = head
                #8  = uarm.L (above front-left leg, where neck meets torso) - has no hitbox
                #9  = forearm.L (upper part of front-left leg)
                #10 = f_foot.L (middle part of front-left leg)
                #11 = f_hoof.L (lower part of front-left leg)
                #12 = clavicle.L (hoof and below of front-left leg) - has no hitbox
                #13 = uarm.R (above front-right leg, where neck meets torso) - has no hitbox
                #14 = forearm.R (upper part of front-right leg)
                #15 = f_foot.R (middle part of front-right leg)
                #16 = f_hoof.R (lower part of front-right leg)
                #17 = clavicle.R (hoof and below of front-right leg) - has no hitbox
                #18 = thigh.L (upper part of back-left leg)
                #19 = calf.L (middle part of back-left leg)
                #20 = h_foot.L (lower part of back-left leg)
                #21 = h_hoof.L (hoof and below of back-left leg) - has no hitbox
                #22 = thigh.R (upper part of back-right leg)
                #23 = calf.R (middle part of back-right leg)
                #24 = h_foot.R (lower part of back-right leg)
                #25 = h_hoof.R (hoof and below of back-right leg) - has no hitbox
                #26 = tail1 (upper part of tail, where horse has living flesh)
                #27 = tail2 (lower part of tail, where horse has just hair)
                (assign,":num_bones",26), #skip both tail bones, but otherwise loop through all bones (even hooves, because lance is especially dangerous to horse's legs)
                (agent_get_rider,":rider",":target"),
                (ge,":rider",0), #with a rider
                (agent_get_team,":target_team",":rider"), #belongs to rider's team
            (else_try), #without a rider
                (agent_get_team,":target_team",":target"), #belongs to his own team
            (try_end),
            (this_or_next|teams_are_enemies,":attacker_team",":target_team"), #enemy
            (             eq,":target_team",7), #or neutral (we already excluded neutral horses that do have a rider)
            (try_for_range,":bone",0,":num_bones"),
                (agent_get_bone_position,pos12,":target",":bone",1),
                (position_transform_position_to_local,pos13,pos10,pos12), #need relative distance, so we can calculate distance to our weapon's attack vector
                (position_get_x,":pos13_x",pos13),
                (position_get_y,":pos13_y",pos13),
                (position_get_z,":pos13_z",pos13),
                (is_between,":pos13_x",":bone_radius_minus",":bone_radius_plus"), #center of bone not far from vector of attack, edge of bone should be in range
                (is_between,":pos13_z",":bone_radius_minus",":bone_radius_plus"), #center of bone not far from vector of attack, edge of bone should be in range
                (is_between,":pos13_y",":bone_radius_minus",":reach_of_attack"),  #center of bone not far from vector of attack, edge of bone should be in range
                (try_begin),
                    (eq,":num_bones",19), #it's human (trying not to repeat agent_is_human again)
                    (val_add,":targets_struck",1), #count player's human victims only
                (try_end),
                (assign,":num_bones",0), #break
            (try_end),
            (eq,":num_bones",0), #break was used in bone loop
            (agent_set_slot,":target",slot_agent_just_pierced_by,":attacker"), #mark him for piercing
        (try_end),
        (try_begin), #display score to player, for bragging rights
            (ge,":targets_struck",2), #don't show score for just 1 enemy hit, that's not an accomplishment
            (get_player_agent_no,":player_agent"),
            (eq,":attacker",":player_agent"),
            (try_begin), #personal best?
                (item_get_slot,":best_score",":attacker_weapon",slot_item_lance_pierce_best_multihit_score),
                (gt,":targets_struck",":best_score"),
                (str_store_item_name,s13,":attacker_weapon"),
                (display_message,"@Personal best for {s13}!",0xFEDC11), #(same color as "Head shot!" for bows)
                (item_set_slot,":attacker_weapon",slot_item_lance_pierce_best_multihit_score,":targets_struck"),
            (try_end),
            (assign,reg13,":targets_struck"),
            (display_message,"@Enemies pierced: {reg13}",0xA9A9A9), #(same color as "Shot difficulty: x.xx" for bows)
        (try_end),
        #execute agents
        (try_for_agents,":target",pos10,":search_range"), #limit search to spear tip again, for optimization
            (neq,":target",":triggering_agent"), #he's getting killed by trigger result, if we don't skip him here, he will get 2 notifications
            (agent_slot_eq,":target",slot_agent_just_pierced_by,":attacker"), #marked for death
            (agent_is_alive,":target"), #just to make sure he wasn't killed by something else yet
            (neg|agent_is_wounded,":target"), #same as above
            (agent_deliver_damage_to_agent,":attacker",":target",":damage",":attacker_weapon"), #to preserve blunt/lethal dmg setting, we set it to attacker's weapon, and deal same dmg as 1st target received
        (try_end),
        (set_trigger_result,":damage"), #stupid as it is, if we don't set the trigger result, the 1st agent hit survives
    ]
)
rndl_lance_penetration_system_on_timer = (0.1,0,0,
    [],
    [
        (try_for_agents,":agent"), #loop through all agents
            (agent_set_slot,":agent",slot_agent_just_pierced_by,-1), #clear their "I was hit by this guy" slot
        (try_end),
        #this doesn't cover spawning agents, they will have 0 in that slot for the first 0.1 sec of existing, but this simply makes them immune to being pierced for that short time if somebody is spawn camping - seems fair
    ]
)
rndl_lance_penetration_system_on_agent_killed_or_wounded = (ti_on_agent_killed_or_wounded,0,0,
    [],
    [
        #the whole purpose of this entire trigger is to detect that blunt weapon was used, then prevent the agent from dying and knock him unconscious instead
        (store_trigger_param_1,":dead_agent_no"),
        (store_trigger_param_2,":killer_agent_no"),
        #(store_trigger_param_3,":is_wounded"), #not needed, we don't have to check it, just set it
        (agent_slot_eq,":dead_agent_no",slot_agent_just_pierced_by,":killer_agent_no"), #make sure it was our scripted kill
        (agent_is_human,":killer_agent_no"), #there is a minimum chance that agent was killed by a normal non-scripted attack in less than 0.1 sec after spawning, and his slot still holds 0, but agent = 0 didn't kill him with couched lance, so we gotta check if agent isn't a horse that trampled him
        (agent_get_wielded_item,":wielded",":killer_agent_no",0), #0 = mainhand item, 1 = offhand item (bow is also mainhand, for 2-handed weapons returns -1 for offhand)
        (ge,":wielded",0), #weapon exists
        (item_get_thrust_damage_type,":damage_type",":wielded"), #cut = 0, pierce = 1, blunt = 2
        (eq,":damage_type",2),
        (set_trigger_result,2), #result = 2 means force fall unconscious (1 = force killed, 0 = no change)
    ]
)

#MOD END - lance piercing multiple targets

And then add definitions of those 3 triggers to:
- tournament_triggers
- village_attack_bandits
- village_raid
Like this:
Python:
    ("village_attack_bandits",mtf_battle_mode|mtf_synch_inventory,charge,"You lead your men to battle.",
        [
            (3,mtef_visitor_source|mtef_team_1,0,aif_start_alarmed,1,[]),
            (1,mtef_team_0|mtef_use_exact_number,0,aif_start_alarmed,7,[]),
            (1,mtef_visitor_source|mtef_team_0,0,aif_start_alarmed,1,[]),
        ],
        [
            #MOD BEGIN - lance piercing multiple targets
            rndl_lance_penetration_system_on_agent_hit,
            rndl_lance_penetration_system_on_timer,
            rndl_lance_penetration_system_on_agent_killed_or_wounded,
            #MOD END - lance piercing multiple targets
            common_battle_tab_press,
            common_battle_init_banner,
            (ti_question_answered,0,0,
            #the rest of the triggers continues below...

For lead_charge it's a bit more complicated, because (at least in my case) I had to integrate into (already modified) pre-existing ti_on_agent_killed_or_wounded.
This is how I would inject into vanilla one:
Python:
            (ti_on_agent_killed_or_wounded,0,0,
                [],
                [
                    (store_trigger_param_1,":dead_agent_no"),
                    (store_trigger_param_2,":killer_agent_no"),
                    (store_trigger_param_3,":is_wounded"),
                    (try_begin),
                        (ge,":dead_agent_no",0),
                        (neg|agent_is_ally,":dead_agent_no"),
                        (agent_is_human,":dead_agent_no"),
                        (agent_get_troop_id,":dead_agent_troop_id",":dead_agent_no"),
                        (party_add_members,"p_total_enemy_casualties",":dead_agent_troop_id",1), #addition_to_p_total_enemy_casualties
                        #!#MOD BEGIN - lance piercing multiple targets
                        #the whole purpose of this entire block is to detect that blunt weapon was used, then prevent the agent from dying and knock him unconscious instead
                        (try_begin),
                            (agent_slot_eq,":dead_agent_no",slot_agent_just_pierced_by,":killer_agent_no"), #make sure it was our scripted kill
                            #(agent_is_human,":killer_agent_no"), #already checked above
                            (agent_get_wielded_item,":wielded",":killer_agent_no",0), #0 = mainhand item, 1 = offhand item (bow is also mainhand, for 2-handed weapons returns -1 for offhand)
                            (ge,":wielded",0), #weapon exists
                            (item_get_thrust_damage_type,":damage_type",":wielded"), #cut = 0, pierce = 1, blunt = 2
                            (eq,":damage_type",2),
                            (assign,":is_wounded",1), #mark him as just knocked unconscious, not dead (for the rest of the code below)
                            (set_trigger_result,2), #same, but for the engine, and it will take effect only after the code below finishes doing its stuff
                        (try_end),
                        #!#MOD END - lance piercing multiple targets
                        (eq,":is_wounded",1),
                        (party_wound_members,"p_total_enemy_casualties",":dead_agent_troop_id",1),
                    (try_end),
                    (call_script,"script_apply_death_effect_on_courage_scores",":dead_agent_no",":killer_agent_no"),
                ]
            ),

This solves all of the problem listed above, and also has a minor feature of showing to player how well he had done - showing number of agents pierced in 1 hit, if there is 2 or more (same color as shot difficulty for bow), and also storing the high score separately for each lance item, then showing to player if he has broken his record for that lance (same color as "Head shot!" text for bow).
 
Last edited:
Upvote 0
Back
Top Bottom