Jump to content
  • 0

Mob attacks - how do you determine when the damage number appears?


ckx_

Question


  • Group:  Members
  • Topic Count:  5
  • Topics Per Day:  0.01
  • Content Count:  10
  • Reputation:   0
  • Joined:  06/29/22
  • Last Seen:  

After you call clif_damage on the server, display of animations and damages seems to be in the client's hands, unless I've missed something. So my question is:

With any given attack animation, how does one determine what frame of the attack should trigger the client to do a flinch animation for the target, & display the damage number? For example, if you spawn a Willow, you might notice that it deals damage around the time the animation begins, while the willow is still winding up. In contrast, if you spawn a Condor, you'll notice that damage does not happen until nearly the end of the animation.

I took a look in Act Editor to see if there were any relevant properties, but I didn't find anything. Any ideas? Thanks.

 

Link to comment
Share on other sites

4 answers to this question

Recommended Posts

  • 1

  • Group:  Members
  • Topic Count:  16
  • Topics Per Day:  0.00
  • Content Count:  665
  • Reputation:   673
  • Joined:  11/12/12
  • Last Seen:  

Heya,

This topic is actually very complex and it has tons of exceptions. It also depends of the client version since they do have different behaviors.

  • When the clif_damage packet is sent, the important value is "sdelay", "amotion", "animation motion" or "attack motion". This is the duration of the attack animation shown by the monster.
  • This value is defined in your mob db file under AttackMotion.
  • The AttackMotion is the speed at which the mob displays its attack animation and it can go between 0 and 500. It works as you think it does (ish):
    • An AttackMotion motion of 100 would be 100 * AttackMotion / 500 = 1/5th of the normal duration. Meanwhile 600 AttackMotion would be capped to 500, giving you the full attack animation duration.
  • Now you would think the animation duration is given by the AttackMotion, but that's just bullshit. That's not the case at all. The damage number shown isn't related to that.
  • The real animation duration that the client reads is given by the Act file itself by the action index 16. If you take Willow as an example, it has 7 frames at 100 ms each, so it has a total duration of 700 ms.
  • Now there's another special "feature" going on here. The damage number will be shown at "frames count - 1". So in the case of Condor, the damage animation will be shown at frame 7. So while the animation duration is indeed 9 x 75 = 675 ms, the damage number will be shown at 7 x 75 = 525 ms. To be clear, it is shown the moment the frame appears and not when it ends, so that's why it's not 8 x 75 but 7 x 75.
  • As for Condor, there is yet again another special "feature" going on. The damage number will be shown earlier if the "atk" sound is triggered. In the case of Willow, the attack sound is at frame 3, so while the animation duration is 700 ms, the damage will actually be shown at 3 x 100 = 300 ms.
  • Now that's why the attack isn't displayed at the end of the animation as you'd expect it to be.
  • The character stops moving on the client once the damage number is shown.

More on the server-side of things:

  • The damage will be inflicted after the AttackMotion timer server-side. Once AttackMotion runs out, the player movement will be stopped, that position will be saved and then a clif_updatestatus will be sent for the new HP value.
  • So as you can imagine, there is absolutely no way that the position in the client and the position on the server would be the same considering all this. The server can't know the Act file true damage animation duration vs attack animation duration.
  • Overall, quite the blunder Gravity made, yet again.

And that's just what I've found out through the years. I'm sure there is plenty more weirdness going on there, like how player ranged melee attacks are 4/3 of the AttackMotion on some clients, creating even more position lag issues. Anyway, to fix your issue, change the "atk" sound and it will work out, but fixing that for all mobs doesn't seem very realistic to me.

Edit: Please keep in mind most of the above is only related to monsters; players have different behavior in regards to AttackMotion.

Edited by Tokei
  • Upvote 1
Link to comment
Share on other sites

  • 0

  • Group:  Members
  • Topic Count:  5
  • Topics Per Day:  0.01
  • Content Count:  10
  • Reputation:   0
  • Joined:  06/29/22
  • Last Seen:  

Thank you for the detailed breakdown. This is exactly what I was looking for. I had noticed the behavior of the damage sound file, but still found it inconsistent; the rest of your post clarifies it greatly.

8 hours ago, Tokei said:

 

More on the server-side of things:

  • The damage will be inflicted after the AttackMotion timer server-side. Once AttackMotion runs out, the player movement will be stopped, that position will be saved and then a clif_updatestatus will be sent for the new HP value.
  • So as you can imagine, there is absolutely no way that the position in the client and the position on the server would be the same considering all this. The server can't know the Act file true damage animation duration vs attack animation duration.
  • Overall, quite the blunder Gravity made, yet again.

 

I've done a little work on that front for my server (currently still in development). I added an "AmotionActive" property to monsters and skills, an int value that gets used by battle_delay_damage to determine when _sub should get called. It is used to calculate the percentage of an AttackMotion where the damage should be "active", i.e. at what point in the attack animation should HP be deducted from a player.

The vanilla timer for delayed damage looks like this:

add_timer(tick+amotion, battle_delay_damage_sub, 0, (intptr_t)dat);

My modified call looks more like this (omitted safety checks for brevity):

int dmgdelay = amotion;
if (dmgdelay > 0) {
    int a_active;
    if (src->type==BL_MOB && !skill_id) {
    // Mob normals are handled on a case by case basis 
    a_active = ((TBL_MOB*)src)->db->amotion_active;
} else if (skill_id > 0) {
    a_active = skill_get_amotionactive(skill_id);
} else {
    // General cases get amotion reduced by the default amotion active value.
    a_active = AMOTION_ACTIVE;
}
dmgdelay = (a_active * dmgdelay)/100;
add_timer(tick+dmgdelay, battle_delay_damage_sub, 0, (intptr_t)dat);

So if Willow's full amotion is 700, and I set its AmotionActive property to 33, that will put the battle_delay_damage_sub timer at tick+231 (33% of 700), causing the HP to be subtracted at roughly that point in the animation. This is fairly simple, and allows me to specify an arbitrary point of an amotion where damage is actually dealt—It works well to eliminate "laggy" feeling damage (whether server-side damage is too early, or too late compared to the animation). I've also modified some of the flow around when clif_damage gets called to make things feel more responsive for this work.

That's all good,  but up until now I've been restricted by my inability to define at what point the client displays the hitstun/damage, so I've just been doing my best to match up the server-side damage delays with the flinches defined by Gravity; but with this newfound knowledge, I'll have control of both ends of the equation, and be able to create more responsive feeling combat where things deal damage when they actually hit you.

Thanks a bunch, and thanks for the tooling that makes this stuff simple.

Edited by ckx_
wow, this forum's formatting quirks are numerous.
Link to comment
Share on other sites

  • 0

  • Group:  Members
  • Topic Count:  5
  • Topics Per Day:  0.01
  • Content Count:  10
  • Reputation:   0
  • Joined:  06/29/22
  • Last Seen:  

I made a custom Act Editor script to assist in the trivial cases where setting SoundId to "atk" is enough. I figured I'd post it here to save anyone else a few moments, if they ever decide to take up the task of more accurate feeling damage timings:

using System;
using ErrorManager;
using GRF.FileFormats.ActFormat;
using GRF.Image;

namespace Scripts {
    public class Script : IActScript {
		public object DisplayName {
			get { return "Sound ID Replication"; }
		}
		
		public string Group {
			get { return "Custom Scripts"; }
		}
		
		public string InputGesture {
			get { return "Ctrl-Alt-Shift-A"; }
		}
		
		public string Image {
			get { return "settings.png"; }
		}
		
		public void Execute(Act act, int selectedActionIndex, int selectedFrameIndex, int[] selectedLayerIndexes) {
			if (act == null) return;
			string errorString = string.Empty;
			try {
				act.Commands.Begin();
				System.Collections.Generic.List<int> soundIds = new System.Collections.Generic.List<int>();
				foreach (var frame in act[selectedActionIndex].Frames) {
					soundIds.Add(frame.SoundId);
				}
				int start_index = selectedActionIndex;
				for (int i = selectedActionIndex; (i%8)!=0;i++) {
					start_index = i-7;
				}
				int end_index = start_index+7;
				for (int i = start_index; i < end_index+1; i++) {
					int actionIndex = i;
					if (actionIndex == selectedActionIndex) {
						continue;
					}
					GRF.FileFormats.ActFormat.Action action = act[actionIndex];
					if (action.NumberOfFrames != soundIds.Count) {
						errorString += "Frame count mismatch on action index " +actionIndex+ ". Expected " + soundIds.Count + 
							" frames, but got " + action.NumberOfFrames + "." + System.Environment.NewLine;
						continue;
					}
					for (int j = 0; j < action.Frames.Count; j++) {
						int frameIndex = j;
						act.Commands.SetSoundId(actionIndex, frameIndex, soundIds[frameIndex]);
					}
				}
				if (errorString != string.Empty) {
					System.Windows.Forms.MessageBox.Show(errorString, "Frame count mismatch", 
						System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Exclamation);
				} 
			}
			catch (Exception err) {
				act.Commands.CancelEdit();
				ErrorHandler.HandleException(err, ErrorLevel.Warning);
			}
			finally {
				act.Commands.End();
				act.InvalidateVisual();
				act.InvalidateSpriteVisual();
			}
		}
		
		public bool CanExecute(Act act, int selectedActionIndex, int selectedFrameIndex, int[] selectedLayerIndexes) {
			return act != null;
		}
	}
}

The intention is to setup your selected Action Index as your "base" index, and run the script. It will replicate the SoundIds for each frame over to the other relevant action indices—If a frame count mismatch occurs between the base index and another action index (relatively rare, but happens), it skips all mismatched indices and throws a message, so you can handle 'em manually afterwards.

I am not backing up the act here, as I have my own backup flow going on, so you might want to re-add the act backup command from the sample script if you use this.

EDIT: Generalized script to make it work on any action type, not just attack actions at indices 16~23.

 

Edited by ckx_
Link to comment
Share on other sites

  • 0

  • Group:  Members
  • Topic Count:  5
  • Topics Per Day:  0.01
  • Content Count:  10
  • Reputation:   0
  • Joined:  06/29/22
  • Last Seen:  

On 10/3/2023 at 11:34 AM, Tokei said:

Edit: Please keep in mind most of the above is only related to monsters; players have different behavior in regards to AttackMotion.

If you ever find the time & inclination, some pointers on what to do for player attacks would be appreciated, too; I've figured it out server-side, but have not yet started investigation on the clientside. Thank you.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Answer this question...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...