Synchronize Damage Feature (#8305)
- Added a new monster stat "ClientAttackMotion" to mob_db.yml which is the time from when a monster attacks until which the damage shows on the client at 1x speed - Added a new config synchronize_damage; when set to "yes", the client will display the damage of normal monster attacks at the exact time it is applied on the server, removing position lag (fixes #259) Special thanks to all people who worked together to make this possible. Co-authored-by: aleos, Lemongrass3110, Atemo
This commit is contained in:
parent
023263df7d
commit
b4ae40d401
@ -130,10 +130,19 @@ equip_self_break_rate: 100
|
|||||||
// This affects the behaviour of skills like acid terror and meltdown
|
// This affects the behaviour of skills like acid terror and meltdown
|
||||||
equip_skill_break_rate: 100
|
equip_skill_break_rate: 100
|
||||||
|
|
||||||
// Do weapon attacks have a attack speed delay before actual damage is applied? (Note 1)
|
// Should damage have a delay before it is applied? (Note 1)
|
||||||
// NOTE: The official setting is yes, even thought it degrades performance a bit.
|
// Some skills might not have a delay by default regardless of this setting.
|
||||||
|
// The official setting is yes, even thought it degrades performance a bit.
|
||||||
delay_battle_damage: yes
|
delay_battle_damage: yes
|
||||||
|
|
||||||
|
// Should the damage timing be synchronized between the client and server? (Note 1)
|
||||||
|
// This is not official behavior, but it should remove the position lag after being hit by a monster.
|
||||||
|
// This setting only affects normal monster attacks and takes priority over "delay_battle_damage".
|
||||||
|
// Many skills show their damage immediately, so setting "delay_battle_damage" to "no" at the same
|
||||||
|
// time might improve the experience further, but will not work for all skills.
|
||||||
|
// Tired of Dark Illusion hitting you 5 seconds too late? Then turn this on.
|
||||||
|
synchronize_damage: no
|
||||||
|
|
||||||
// Are arrows/ammo consumed when used on a bow/gun?
|
// Are arrows/ammo consumed when used on a bow/gun?
|
||||||
// 0 = No
|
// 0 = No
|
||||||
// 1 = Yes
|
// 1 = Yes
|
||||||
|
@ -56,6 +56,7 @@
|
|||||||
# WalkSpeed Walk speed. (Default: DEFAULT_WALK_SPEED)
|
# WalkSpeed Walk speed. (Default: DEFAULT_WALK_SPEED)
|
||||||
# AttackDelay Attack speed. (Default: 0)
|
# AttackDelay Attack speed. (Default: 0)
|
||||||
# AttackMotion Attack animation speed. (Default: 0)
|
# AttackMotion Attack animation speed. (Default: 0)
|
||||||
|
# ClientAttackMotion Client attack speed. (Default: AttackMotion)
|
||||||
# DamageMotion Damage animation speed. (Default: 0)
|
# DamageMotion Damage animation speed. (Default: 0)
|
||||||
# DamageTaken Rate at which the monster will receive incoming damage. (Default: 100)
|
# DamageTaken Rate at which the monster will receive incoming damage. (Default: 100)
|
||||||
# Ai Aegis monster type AI behavior. (Default: 06)
|
# Ai Aegis monster type AI behavior. (Default: 06)
|
||||||
@ -77,7 +78,7 @@
|
|||||||
|
|
||||||
Header:
|
Header:
|
||||||
Type: MOB_DB
|
Type: MOB_DB
|
||||||
Version: 3
|
Version: 4
|
||||||
|
|
||||||
#Body:
|
#Body:
|
||||||
# eAthena Dev Team
|
# eAthena Dev Team
|
||||||
|
@ -56,6 +56,7 @@
|
|||||||
# WalkSpeed Walk speed. (Default: DEFAULT_WALK_SPEED)
|
# WalkSpeed Walk speed. (Default: DEFAULT_WALK_SPEED)
|
||||||
# AttackDelay Attack speed. (Default: 0)
|
# AttackDelay Attack speed. (Default: 0)
|
||||||
# AttackMotion Attack animation speed. (Default: 0)
|
# AttackMotion Attack animation speed. (Default: 0)
|
||||||
|
# ClientAttackMotion Client attack speed. (Default: AttackMotion)
|
||||||
# DamageMotion Damage animation speed. (Default: 0)
|
# DamageMotion Damage animation speed. (Default: 0)
|
||||||
# DamageTaken Rate at which the monster will receive incoming damage. (Default: 100)
|
# DamageTaken Rate at which the monster will receive incoming damage. (Default: 100)
|
||||||
# Ai Aegis monster type AI behavior. (Default: 06)
|
# Ai Aegis monster type AI behavior. (Default: 06)
|
||||||
@ -77,7 +78,7 @@
|
|||||||
|
|
||||||
Header:
|
Header:
|
||||||
Type: MOB_DB
|
Type: MOB_DB
|
||||||
Version: 3
|
Version: 4
|
||||||
|
|
||||||
Footer:
|
Footer:
|
||||||
Imports:
|
Imports:
|
||||||
|
File diff suppressed because it is too large
Load Diff
2399
db/re/mob_db.yml
2399
db/re/mob_db.yml
File diff suppressed because it is too large
Load Diff
@ -204,6 +204,18 @@ AttackMotion: Attack animation motion. Low value means monster's attack will be
|
|||||||
|
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
|
ClientAttackMotion: The time from the start of a normal attack until the damage frame shows on client. At the same time you also get stopped on the client.
|
||||||
|
This value is only needed if you use the "synchronize_damage" feature (battle/battle.conf).
|
||||||
|
|
||||||
|
If you created a custom sprite, you want to set this value to the timing of the damage frame in your *.act file.
|
||||||
|
In Act Editor you can set the damage frame by setting the frame sound to "atk". If you don't define a damage frame, it will default to the second to last
|
||||||
|
frame. Also keep in mind that the Act Editor displays slightly inaccurate speed. Every 25ms in Act Editor is 24ms in reality.
|
||||||
|
|
||||||
|
Example: Drops has a animation speed of 24ms per frame and the 13th frame (frame #12) is the damage frame. This means the damage shows after 12 frames.
|
||||||
|
That's why Drops has a ClientAttackMotion of 24*12 = 288.
|
||||||
|
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
DamageMotion: Damage animation motion, same as aMotion but used to display the "I am hit" animation. Coincidentally, this same value is used to determine how long it is before the monster/player can move again. Endure is dMotion = 0, obviously.
|
DamageMotion: Damage animation motion, same as aMotion but used to display the "I am hit" animation. Coincidentally, this same value is used to determine how long it is before the monster/player can move again. Endure is dMotion = 0, obviously.
|
||||||
|
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
# WalkSpeed Walk speed. (Default: DEFAULT_WALK_SPEED)
|
# WalkSpeed Walk speed. (Default: DEFAULT_WALK_SPEED)
|
||||||
# AttackDelay Attack speed. (Default: 0)
|
# AttackDelay Attack speed. (Default: 0)
|
||||||
# AttackMotion Attack animation speed. (Default: 0)
|
# AttackMotion Attack animation speed. (Default: 0)
|
||||||
|
# ClientAttackMotion Client attack speed. (Default: AttackMotion)
|
||||||
# DamageMotion Damage animation speed. (Default: 0)
|
# DamageMotion Damage animation speed. (Default: 0)
|
||||||
# DamageTaken Rate at which the monster will receive incoming damage. (Default: 100)
|
# DamageTaken Rate at which the monster will receive incoming damage. (Default: 100)
|
||||||
# Ai Aegis monster type AI behavior. (Default: 06)
|
# Ai Aegis monster type AI behavior. (Default: 06)
|
||||||
|
@ -393,7 +393,20 @@ int battle_delay_damage(t_tick tick, int amotion, struct block_list *src, struct
|
|||||||
damage = 0;
|
damage = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( !battle_config.delay_battle_damage || amotion <= 1 ) {
|
// The client refuses to display animations slower than 1x speed
|
||||||
|
// So we need to shorten AttackMotion to be in-sync with the client in this case
|
||||||
|
if (battle_config.synchronize_damage && skill_id == 0 && src->type == BL_MOB && amotion > status_get_clientamotion(src))
|
||||||
|
amotion = status_get_clientamotion(src);
|
||||||
|
// Check for delay battle damage config
|
||||||
|
else if (!battle_config.delay_battle_damage)
|
||||||
|
amotion = 1;
|
||||||
|
// Aegis places a damage-delay cap of 1 sec to non player attacks
|
||||||
|
// We only want to apply this cap if damage was not synchronized
|
||||||
|
else if (src->type != BL_PC && amotion > 1000)
|
||||||
|
amotion = 1000;
|
||||||
|
|
||||||
|
// Skip creation of timer
|
||||||
|
if (amotion <= 1) {
|
||||||
//Deal damage
|
//Deal damage
|
||||||
battle_damage(src, target, damage, ddelay, skill_lv, skill_id, dmg_lv, attack_type, additional_effects, gettick(), isspdamage);
|
battle_damage(src, target, damage, ddelay, skill_lv, skill_id, dmg_lv, attack_type, additional_effects, gettick(), isspdamage);
|
||||||
return 0;
|
return 0;
|
||||||
@ -411,8 +424,6 @@ int battle_delay_damage(t_tick tick, int amotion, struct block_list *src, struct
|
|||||||
dat->additional_effects = additional_effects;
|
dat->additional_effects = additional_effects;
|
||||||
dat->src_type = src->type;
|
dat->src_type = src->type;
|
||||||
dat->isspdamage = isspdamage;
|
dat->isspdamage = isspdamage;
|
||||||
if (src->type != BL_PC && amotion > 1000)
|
|
||||||
amotion = 1000; //Aegis places a damage-delay cap of 1 sec to non player attacks. [Skotlex]
|
|
||||||
|
|
||||||
if( src->type == BL_PC )
|
if( src->type == BL_PC )
|
||||||
((TBL_PC*)src)->delayed_damage++;
|
((TBL_PC*)src)->delayed_damage++;
|
||||||
@ -11505,6 +11516,7 @@ static const struct _battle_data {
|
|||||||
#else
|
#else
|
||||||
{ "feature.instance_allow_reconnect", &battle_config.instance_allow_reconnect, 0, 0, 1, },
|
{ "feature.instance_allow_reconnect", &battle_config.instance_allow_reconnect, 0, 0, 1, },
|
||||||
#endif
|
#endif
|
||||||
|
{ "synchronize_damage", &battle_config.synchronize_damage, 0, 0, 1, },
|
||||||
|
|
||||||
#include <custom/battle_config_init.inc>
|
#include <custom/battle_config_init.inc>
|
||||||
};
|
};
|
||||||
|
@ -755,6 +755,7 @@ struct Battle_Config
|
|||||||
int feature_stylist;
|
int feature_stylist;
|
||||||
int feature_banking_state_enforce;
|
int feature_banking_state_enforce;
|
||||||
int instance_allow_reconnect;
|
int instance_allow_reconnect;
|
||||||
|
int synchronize_damage;
|
||||||
|
|
||||||
#include <custom/battle_config_struct.inc>
|
#include <custom/battle_config_struct.inc>
|
||||||
};
|
};
|
||||||
|
@ -5177,6 +5177,8 @@ static int clif_hallucination_damage()
|
|||||||
return (rnd() % 32767);
|
return (rnd() % 32767);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define DEFAULT_ANIMATION_SPEED 432
|
||||||
|
|
||||||
/// Sends a 'damage' packet (src performs action on dst)
|
/// Sends a 'damage' packet (src performs action on dst)
|
||||||
/// 008a <src ID>.L <dst ID>.L <server tick>.L <src speed>.L <dst speed>.L <damage>.W <div>.W <type>.B <damage2>.W (ZC_NOTIFY_ACT)
|
/// 008a <src ID>.L <dst ID>.L <server tick>.L <src speed>.L <dst speed>.L <damage>.W <div>.W <type>.B <damage2>.W (ZC_NOTIFY_ACT)
|
||||||
/// 02e1 <src ID>.L <dst ID>.L <server tick>.L <src speed>.L <dst speed>.L <damage>.L <div>.W <type>.B <damage2>.L (ZC_NOTIFY_ACT2)
|
/// 02e1 <src ID>.L <dst ID>.L <server tick>.L <src speed>.L <dst speed>.L <damage>.L <div>.W <type>.B <damage2>.L (ZC_NOTIFY_ACT2)
|
||||||
@ -5226,6 +5228,29 @@ int clif_damage(struct block_list* src, struct block_list* dst, t_tick tick, int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate what sdelay to send to the client so it applies damage at the same time as the server
|
||||||
|
if (battle_config.synchronize_damage && src->type == BL_MOB) {
|
||||||
|
// When a clif_damage packet is sent to the client it will also send "sdelay" (amotion) as value.
|
||||||
|
// The client however does not interpret this value as AttackMotion but incorrectly as an inverted
|
||||||
|
// animation speed modifier, with 432 standing for 1x animation speed.
|
||||||
|
// The client will ignore all values above 432, but lower values will speed up the animation.
|
||||||
|
// 216 for example means play the animation at double the speed. 108 is quadruple speed.
|
||||||
|
// Each monster has an attack animation and may define the frame in the attack animation on which
|
||||||
|
// it displays the damage and makes the target flinch / stop. If the damage frame is undefined,
|
||||||
|
// it instead displays the damage / flinch / stop at the beginning of the second to last frame.
|
||||||
|
// We define the time after which the damage frame shows at 1x speed as clientamotion.
|
||||||
|
uint16 clientamotion = std::max((uint16)1, status_get_clientamotion(src));
|
||||||
|
|
||||||
|
// Knowing when the damage frame happens in the animation allows us to synchronize the timing
|
||||||
|
// between client and server using the formula below.
|
||||||
|
sdelay = sdelay * DEFAULT_ANIMATION_SPEED / clientamotion;
|
||||||
|
|
||||||
|
// Hint: If amotion is larger than clientamotion this results in a value above 432 which makes the
|
||||||
|
// client display the attack at 1x speed. In this case we need to shorten the delay damage timer
|
||||||
|
// on the server to clientamotion ms instead (see battle_delay_damage).
|
||||||
|
sdelay = std::min(sdelay, DEFAULT_ANIMATION_SPEED);
|
||||||
|
}
|
||||||
|
|
||||||
WBUFW(buf,0) = cmd;
|
WBUFW(buf,0) = cmd;
|
||||||
WBUFL(buf,2) = src->id;
|
WBUFL(buf,2) = src->id;
|
||||||
WBUFL(buf,6) = dst->id;
|
WBUFL(buf,6) = dst->id;
|
||||||
|
@ -4461,6 +4461,7 @@ s_mob_db::s_mob_db()
|
|||||||
status.speed = DEFAULT_WALK_SPEED;
|
status.speed = DEFAULT_WALK_SPEED;
|
||||||
status.adelay = cap_value(0, battle_config.monster_max_aspd * 2, 4000);
|
status.adelay = cap_value(0, battle_config.monster_max_aspd * 2, 4000);
|
||||||
status.amotion = cap_value(0, battle_config.monster_max_aspd, 2000);
|
status.amotion = cap_value(0, battle_config.monster_max_aspd, 2000);
|
||||||
|
status.clientamotion = cap_value(status.amotion, 1, USHRT_MAX);
|
||||||
status.mode = static_cast<e_mode>(MONSTER_TYPE_06);
|
status.mode = static_cast<e_mode>(MONSTER_TYPE_06);
|
||||||
|
|
||||||
vd.class_ = id;
|
vd.class_ = id;
|
||||||
@ -4884,6 +4885,18 @@ uint64 MobDatabase::parseBodyNode(const ryml::NodeRef& node) {
|
|||||||
mob->status.amotion = cap_value(speed, battle_config.monster_max_aspd, 2000);
|
mob->status.amotion = cap_value(speed, battle_config.monster_max_aspd, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this->nodeExists(node, "ClientAttackMotion")) {
|
||||||
|
uint16 speed;
|
||||||
|
|
||||||
|
if (!this->asUInt16(node, "ClientAttackMotion", speed))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
mob->status.clientamotion = cap_value(speed, 1, USHRT_MAX);
|
||||||
|
} else {
|
||||||
|
if (!exists)
|
||||||
|
mob->status.clientamotion = cap_value(mob->status.amotion, 1, USHRT_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
if (this->nodeExists(node, "DamageMotion")) {
|
if (this->nodeExists(node, "DamageMotion")) {
|
||||||
uint16 speed;
|
uint16 speed;
|
||||||
|
|
||||||
|
@ -280,7 +280,7 @@ private:
|
|||||||
bool parseDropNode(std::string nodeName, const ryml::NodeRef& node, uint8 max, s_mob_drop *drops);
|
bool parseDropNode(std::string nodeName, const ryml::NodeRef& node, uint8 max, s_mob_drop *drops);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
MobDatabase() : TypesafeCachedYamlDatabase("MOB_DB", 3, 1) {
|
MobDatabase() : TypesafeCachedYamlDatabase("MOB_DB", 4, 1) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3172,7 +3172,7 @@ struct status_data {
|
|||||||
#endif
|
#endif
|
||||||
matk_min, matk_max,
|
matk_min, matk_max,
|
||||||
speed,
|
speed,
|
||||||
amotion, adelay, dmotion;
|
amotion, clientamotion, adelay, dmotion;
|
||||||
int mode;
|
int mode;
|
||||||
short
|
short
|
||||||
hit, flee, cri, flee2,
|
hit, flee, cri, flee2,
|
||||||
@ -3392,6 +3392,7 @@ defType status_get_def(struct block_list *bl);
|
|||||||
unsigned short status_get_speed(struct block_list *bl);
|
unsigned short status_get_speed(struct block_list *bl);
|
||||||
#define status_get_adelay(bl) status_get_status_data(bl)->adelay
|
#define status_get_adelay(bl) status_get_status_data(bl)->adelay
|
||||||
#define status_get_amotion(bl) status_get_status_data(bl)->amotion
|
#define status_get_amotion(bl) status_get_status_data(bl)->amotion
|
||||||
|
#define status_get_clientamotion(bl) status_get_status_data(bl)->clientamotion
|
||||||
#define status_get_dmotion(bl) status_get_status_data(bl)->dmotion
|
#define status_get_dmotion(bl) status_get_status_data(bl)->dmotion
|
||||||
#define status_get_patk(bl) status_get_status_data(bl)->patk
|
#define status_get_patk(bl) status_get_status_data(bl)->patk
|
||||||
#define status_get_smatk(bl) status_get_status_data(bl)->smatk
|
#define status_get_smatk(bl) status_get_status_data(bl)->smatk
|
||||||
|
Loading…
x
Reference in New Issue
Block a user