OSP Kit Combat Fancy Damage Systems

Users who are viewing this thread

xenoargh

Grandmaster Knight
This is pretty basic and crude, but I thought I'd post it and get this ball rolling.

In version 1.134 of the Warband engine, the trigger condition ti_on_agent_hit was introduced.  This is a very big deal; it means that we can pretty much design whatever damage systems we'd like, making really huge changes to combat systems.  For example, if you want to do an ultra-detailed damage sim for a really hardcore historical mod, including critical hits, weapons breaking (this trigger is the most efficient way to handle that) and special damage for special situations (like, say, slipping a dagger through some poor knight's visor while he's prone, or, for that matter, knocking people prone or stunned occasionally, like how I did it with the Shield Bash code) well, this is by far the way to do these things.

In short, this is a Big Deal :smile:

I'll certainly be back to this and exploring how fancy I can make it without it performing really badly when I'm back around again.  It's one of the things that I really wanted to play with when Blood and Steel's core combat balance was designed, and now that it exists, I think that everybody should look at it.  It will allow people to customize combat in ways that simply weren't possible before.

So, here's some very basic code to get people's brains moving.  I haven't had time to flesh this out with a lot of details yet, but it's functional and it demonstrates some basic uses of this- a "natural soak" or "toughness" stat (as opposed to armor soak) and an "auto-dodge" (critical hits could use the same idea but with the opposite treatment of damage, obviously).


In module_mission_templates:

Code:
#Damage simulation code example by xenoargh
#Makes Ironflesh reduce damage (i.e., it's actually worth putting points into in SP)
#Makes Athletics also give an 'auto-dodge' or 'roll with punch' that reduces damage to a minimum
common_damage_system = (ti_on_agent_hit, 0, 0, [],
[
	(store_trigger_param_1, ":agent"),
	(store_trigger_param_2, ":attacker"),	
	(store_trigger_param_3, ":damage"),
	(store_trigger_param_3, ":orig_damage"),
	(agent_get_troop_id, ":troop", ":agent"),
	(agent_get_troop_id, ":attacker_troop", ":attacker"),	
	(store_skill_level, ":ironflesh",  "skl_ironflesh", ":troop"),
	(store_skill_level, ":athletics",  "skl_athletics", ":troop"),	
	#Damage is lowered 5% per point of Ironflesh.
	(try_begin),
		(gt, ":ironflesh", 0),
		(val_mul, ":ironflesh", 5),
		(assign, ":mod_damage", ":damage"),		
		(val_mul, ":mod_damage", ":ironflesh"),
		(val_div, ":mod_damage", 100),#Rounded here, but whatever.
		(val_sub, ":damage", ":mod_damage"),
	(try_end),
	#Damage is avoided 5% per point of Athletics
	(try_begin),
		(gt, ":athletics", 0),	
		(val_mul, ":athletics", 5),
		(store_random_in_range, ":random_no", 1, 100),
		(try_begin),
			(le, ":random_no", ":athletics"),
			(assign, ":damage", 1),
			(try_begin),
				(eq, ":troop", "trp_player"),
				(display_message, "@You dodge the attack!"),
			(else_try),
				(eq, ":attacker_troop", "trp_player"),
				(display_message, "@{He/She} dodges the attack!"),
			(try_end),			
		(try_end),
	(try_end),	

	(store_sub, ":diff_damage", ":damage", ":orig_damage"),
	(val_mul, ":diff_damage", -1),
	(store_agent_hit_points, ":hitpoints" , ":agent", 1),
	(val_add, ":hitpoints", ":diff_damage"),
	(agent_set_hit_points,":agent",":hitpoints",1),
])

Example of use (for people who aren't familiar with using custom triggers):
Code:
  (
    "back_alley_kill_local_merchant",mtf_arena_fight,-1,
    "You enter the back alley",
    [
      (0,mtef_visitor_source|mtef_team_0,af_override_horse,aif_start_alarmed,1,[]),
      (3,mtef_visitor_source|mtef_team_1,af_override_horse,aif_start_alarmed,1,[]),
    ],
    [
      common_inventory_not_available,
	  #Realistic Weather
	  common_weather_system,
	  #Ammunition System
	  common_ammunition_system,	
	#New Damage System	
	common_damage_system,	  
	  #Fade in
		common_shield_bash,
		order_weapon_type_triggers,			  
	  common_fade_from_black,		  
      (ti_tab_pressed, 0, 0, [(display_message,"str_cannot_leave_now")], []),
      (ti_before_mission_start, 0, 0, [], [(call_script, "script_change_banners_and_chest")]),

      (0, 0, ti_once, [],
       [
         (call_script, "script_music_set_situation_with_culture", mtf_sit_ambushed),
         ]),

      (0, 0, ti_once, [
          (store_mission_timer_a,":cur_time"),
          (ge,":cur_time",1), 
          (assign, ":merchant_hp", 0),
          (assign, ":player_hp", 0),
          (assign, ":merchant_hp", 0),
          (assign, ":merchant_agent", -1),
          (assign, ":player_agent", -1),
          (try_for_agents, ":agent_no"),
            (agent_get_troop_id, ":troop_id", ":agent_no"),
            (try_begin),
              (eq, ":troop_id", "trp_local_merchant"),
              (store_agent_hit_points, ":merchant_hp", ":agent_no"),
              (assign, ":merchant_agent", ":agent_no"),
            (else_try),
              (eq, ":troop_id", "trp_player"),
              (store_agent_hit_points, ":player_hp",":agent_no"),
              (assign, ":player_agent", ":agent_no"),
            (try_end),
          (try_end),
          (ge, ":player_agent", 0),
          (ge, ":merchant_agent", 0),
          (agent_is_alive, ":player_agent"),
          (agent_is_alive, ":merchant_agent"),
          (is_between, ":merchant_hp", 1, 30),
          (gt, ":player_hp", 50),
          (start_mission_conversation, "trp_local_merchant"),
          ], []),
      
      (1, 4, ti_once, [(assign, ":not_alive", 0),
                       (try_begin),
                         (call_script, "script_cf_troop_agent_is_alive", "trp_local_merchant"),
                       (else_try),
                         (assign, ":not_alive", 1),
                       (try_end),
                       (this_or_next|main_hero_fallen),
                       (eq, ":not_alive", 1)],
       [
           (try_begin),
             (main_hero_fallen),
             (call_script, "script_fail_quest", "qst_kill_local_merchant"),
           (else_try),
             (call_script, "script_change_player_relation_with_center", "$current_town", -4),
             (call_script, "script_succeed_quest", "qst_kill_local_merchant"),
           (try_end),
	  (set_rain, 0, 0),
	  #(set_fog_distance, 0),		   
           (finish_mission),
           ]),
    ],
  ),
 
Here's a fancier version, with random knockback / knockdown, critical hits for missiles.  It has been tested, and works.

This is more advanced stuff, please read the comments before attempting to copy-pasta this code, it has a dependency.

Code:
common_damage_system = (ti_on_agent_hit, 0, 0, [],
[
	(store_trigger_param_1, ":agent"),
	(store_trigger_param_2, ":attacker"),	
	(store_trigger_param_3, ":damage"),
	(store_trigger_param_3, ":orig_damage"),
	(agent_get_troop_id, ":troop", ":agent"),
	(agent_get_troop_id, ":attacker_troop", ":attacker"),	

	#Damage is lowered 5% per point of Ironflesh.
	(try_begin),
		(agent_is_human, ":agent"),#stop if not human
		(store_skill_level, ":ironflesh",  "skl_ironflesh", ":troop"),
		(gt, ":ironflesh", 0),
		(val_mul, ":ironflesh", 5),
		(assign, ":mod_damage", ":damage"),		
		(val_mul, ":mod_damage", ":ironflesh"),
		(val_div, ":mod_damage", 100),#Rounded here, but whatever.
		(val_sub, ":damage", ":mod_damage"),
	(try_end),
	#Damage is avoided 5% per point of Athletics
	(try_begin),
		(agent_is_human, ":agent"),#stop if not human
		(store_skill_level, ":athletics",  "skl_athletics", ":troop"),
		(gt, ":athletics", 0),	
		(val_mul, ":athletics", 5),
		(store_random_in_range, ":random_no", 1, 100),
		(try_begin),
			(le, ":random_no", ":athletics"),
			(assign, ":damage", 1),
			(try_begin),
				(eq, ":troop", "trp_player"),
				(display_message, "@You dodge the attack!"),
			(else_try),
				(eq, ":attacker_troop", "trp_player"),
				(display_message, "@{He/She} dodges the attack!"),
			(try_end),			
		(try_end),
	(try_end),	
	#Critical hits for missiles
	(try_begin),
		(is_between, reg0, "itm_missile_weapons_start", "itm_missile_weapons_end"),#special def in Blood and Steel source
		(store_random_in_range, ":random_no", 1, 100),
		(try_begin),
			(le, ":random_no", 5),
			(val_min, ":damage", 20),
		(try_end),
	(try_end),	
	#Random knockdown, with STR used to test
	(try_begin),
		(store_attribute_level,":strength",":attacker_troop",ca_strength),
		(store_attribute_level,":enemy_strength",":troop",ca_strength),
		(store_sub, ":strength_test", ":strength", ":enemy_strength"),
		(val_min, ":strength_test", 2),#2 percent knockdown, for weaker characters.
		(store_random_in_range, ":random", 1, 100),
		(try_begin),
			(neg|agent_is_ally,":agent"),#don't knock down allies
			(agent_is_human, ":agent"),#stop if not human			
			(agent_is_active,":agent"),#stop accidental CTD
			(agent_is_alive,":agent"),#same diff
			(agent_get_horse, ":horse", ":agent"),
			(eq, ":horse", -1),#don't knock down riders, maybe write some "lose yer horse" code later	
			(le, ":random", ":strength_test"),
			(agent_set_animation, ":agent","anim_shield_strike"),#see module_animations from Blood and Steel source for this anim definition
		(try_end),
	(try_end),
	(store_sub, ":diff_damage", ":damage", ":orig_damage"),
	(val_mul, ":diff_damage", -1),
	(store_agent_hit_points, ":hitpoints" , ":agent", 1),
	(val_add, ":hitpoints", ":diff_damage"),
	(agent_set_hit_points,":agent",":hitpoints",1),
])

Oh, and one other thing:  agent_deliver_damage_to_agent triggers this code.  That has some pretty cool implications.
 
Nice.. this gives me a few ideas for more advanced combat mechanics.

For example: give the player a X% chance (dependant on weapon, using item slots) to deflect/parry a blow when a key is held down (e.g is readying attack and X key is pressed). This move could also disarm the enemy agent. Hmm.  :razz:
 
Right- amongst other things, this makes implementing sword-breakers and such a snap. 

Oh, wait, and bucklers can actually do their RL jobs, instead of being functionally useless in this engine. 

Oh, and spears can suddenly do a lot more damage to horses, so phalanxes and pikemen will suddenly work, without tricks.

And... heh.  Basically, this is one of the biggest things to happen to combat mechanics since Warband's release, frankly.
 
Yeah. The Barfight mod can now actually put those stools (among other things) to proper use with stuns etc, lol  :razz:
 
Nice. Since I started playing Oblivion (one-two weeks ago) I hate how I need 50 arrows and/or 50 melee hits to kill someone. I've never wanted to mod Oblivion to fix this. But I got an idea from that game; I liked the methods of skill increasing (which I'll implement in TBS for Warband - doing something a lot makes your skill increase) and also I want to have "talents" for every weapon type (in Warband, that is). The weapon proficiencies exist, but with this trigger one could make the damage increase a bit with higher weapon proficiencies.

And there could be no more death-from-arrows-in-the-arm in MP. (Yesterday, I got shot by an arrow in the achilles tendon. I lost almost 1/2 of my full life. When in an other respawn I got shot in the right side of the chest, I lost the same amount of health. Is that proper? I don't think so!)

EDIT: I think that the damage reduction % should be decreased a bit, now 10 Ironflesh and 10 Athletics render you invulnerable?
 
Thanks for the initiative xenoargh...it definitely has the brain juices flowing!

cmpxchg8b said:
set_trigger_result shouldn't have any effect whatsoever, the game doesn't check it after executing the trigger.
same goes for pos0.
If this is the case and the agent_deliver_damage_to_agent operation triggers the ti_on_agent_hit, wouldn't that then recursively call the trigger if one were to try and increase the damage output of a given hit? (Or am I asking a foolish question and missing something?)

Damage reduction could be handled by giving HP back to the agent, still, though I suppose.
 
ti_on_agent_hit is called before the damage is actually incurred to the agent. The damage as a parameter also differs from the in-game damage notification - I suspect one rounds and the other (when passed to the trigger) truncates. In the case of damage increase, I would simply use agent_set_hit_points with reduction based on the actual damage. Also, have you tried using agent_delivered_damage_to_agent with 0 as the last flag? Does that still cause the trigger to fire? If so, is the value of reg0 -1?
 
Great Idea..
I never know before that this trigger have an output to modify the final damage.
BTW, refer to header_operations_py, I thing store_skill_level works to troops, not to agents, so we should use :
(store_skill_level, ":ironflesh",  "skl_ironflesh", ":troop"),
(store_skill_level, ":athletics",  "skl_athletics", ":troop"),

Edit:
The modified damage doesn't work. Even the damage delivered is different from the initial damage taken from trigger param. May be the trigger param is basic damage that 's not modified yet by bonus speed etc. I tried to assign the initial damage to reg2 and modified damage to reg3 and put this debug message at the end :
(display_message, "@DEBUG : Damage delivered to {s1} amount {reg2} and taken as {reg3}"),
The game's damage notification displayed below the debug message even sometime different from reg2, but it's related to reg2 instead of reg3.
 
D'oh!  My bad about the skill thing.  Corrected  :lol:

Hrmm.  So, how different is the damage input?  I thought the output worked, and mitigation before factoring in speed bonus is still going to cause the total result to get mitigated... and ofc we can turn speed bonus off. 

Guess I'm going to need to test this further and make sure that inputs and outputs make sense.  I know that the stun is working.

EDIT: I think that the damage reduction % should be decreased a bit, now 10 Ironflesh and 10 Athletics render you invulnerable?
No, one's flat reduction, one's avoidance.  So, at best, you have a 50% chance to avoid all damage and if you do take damage, you take 50% of normal.  It's still one heck of a buff, but you'd have to have 30 AGI and 30 STR, so in most mods, it's not likely to happen a lot, and if somebody wants to ignore INT / CHR, any good mod will punish them there.  Anyhow, it may be horribly OP, but it's OK, it's just an example.

Anyhow, more after I've tested things a bit.  Maybe I need to assign the result to reg0 instead (works that way with a lot of triggers).
 
OK, couple of things:

1. set_trigger_result does absolutely nothing.
2.  agent_deliver_damage_to_agent definitely calls this trigger.  Putting it anywhere in the loop causes an endless loop, lol.
3.  Reg0 is also worthless.

Wow.  I feel rather stupid.  Apparently this isn't a function call that expects us to return a value and operates after all the gamecode (which would have been the logical way to do this, imo, given the business case).  Oh well.

We can use agent_set_hit_points to give the difference, which is no big deal.  But the displayed damage for people who leave that (hardcoded) feature on will be fubar'd- you'll take 50 damage, but at 50% reduction, you've been handed 25 hitpoints before the damage actually happens... which is pretty disappointing.

Anyhow, more when I've verified that that approach works.
 
OK, I have verified the set_agent_hit_points method.  The script works as advertised, see changes above.

If you want to verify that it does, indeed, work correctly, add this:

Code:
	(try_begin),
		(eq, ":attacker_troop", "trp_player"),
		(assign, ":damage", 200),
	(try_end),
	(try_begin),
		(eq, ":troop", "trp_player"),
		(val_div, ":damage", 10),
	(try_end),

Just before the new ending code:
Code:
	(store_sub, ":diff_damage", ":damage", ":orig_damage"),
	(val_mul, ":diff_damage", -1),
	(store_agent_hit_points, ":hitpoints" , ":agent", 1),
	(val_add, ":hitpoints", ":diff_damage"),
	(agent_set_hit_points,":agent",":hitpoints",1),
Voila, your hero can kill fully-armored knights with punches and blows from giant mauls barely do damage (in Blood and Steel, with this and Heroic Health, you become practically invulnerable).  In short... it works.  Finally.

Oh, and yes, pos0 is absolute.  If you were hoping we'd finally get access to hitbox results... sorry.
 
Do you know what we can finally do with this? Two words-GAUNTLET WEAPONS! Yes, all those wrist blades, patas, brass knuckles and more can all be done now with two simple item checks!
 
Dual-wielding, in general, can also now be done, and single bladed gauntlets... heck, even katars that actually work. 

"Shield" that is a weapon mesh, custom animation on strike, some extra random damage... meh, basically, this trigger fixes a lot of stuff, now that it works.

I'll go ahead and do that gauntlet stuff, and spears vs. horses (since that's something that's never ever worked right, imo).  I'll let somebody else figure out dragging people off of horses with polearms (yup) and the other crazy stuff that's possible with this.
 
Yes, it works perfectly that way. One more suggestion from me, for troop ID that being played by player is not always trp_player (eg : on custom battle or on rubik's custom commander battles), I think we can use :
(get_player_agent_no":player_agent"),
at beginning and use it as comparation :
(eq, ":troop", "trp_player"), -> (eq, ":agent", ":player_agent"),
(eq, ":attacker_troop", "trp_player"), -> (eq, ":attacker", ":player_agent"),


xenoargh said:
Oh, and yes, pos0 is absolute.  If you were hoping we'd finally get access to hitbox results... sorry.
I hope we'll get hitbox access on the next patch.
 
Glad to hear it's working for you, I was pretty embarrassed that it didn't work right out of the box  :oops:

OK, gotta go party like it's 2011.  I'll do the other stuff I want to get done (gauntlets, spears-vs-horses, oh and stabbing daggers) tomorrow.  It's worth holding off my release another day or two to get the kinks out.
 
Back
Top Bottom