瀏覽代碼

Monster skill use behavior, ranges, position lag fixes
- Updated monster skill use behavior so monsters use skills the same way and at the same rhythm as official servers (bugreport:009326), the changes include:
* Unified the "monster can't move by default" and the "monster trapped" code as it really should behave exactly the same
* Fixed a bug that caused the "monster skill use" routine to be called 20 times more often than it should in some situations
* When a monster attacks you and you run away from it, the monster will now check if it has a ranged skill on "attack" state before switching to chase state
* Monsters will now always do a normal attack before using "attack" state skills
* Fixed a bug that caused monsters to switch to idle mode and never use their chase skills when they get hit continuously
* Changed default for skillrange_from_weapon from 30 (all but player) to 0 (none); monsters will now use all skills at the skill range and not at their own attack range, if they get tanked from farther away than skill range, they won't use these skills
- Updated ranges to work as on official servers (bugreport:009326), the changes include:
* Implemented new functions "check_distance_client" and "distance_client", that instead of the server-sided square range system use the circular one that the client uses; these functions should be used for units that send their commands via the client (players mostly)
* Applied the new distance algorithm to some player-specific range checks, players will now have a circular attack range that reflects their attack range on the client; this makes it impossible to hack the client for more diagonal range
* Removed the arbitrary +1 range bonus at range checks; as monsters now react fluently, they won't need it anymore, that means a monster with for example 4 attack range will now only attack targets within a 9x9 area around it, the moment you step out of this area, the attacks will stop (if the monster can move it will follow you); as for players, the extra cell attack range when on a linear line to the target now is integrated into the distance algorithm, that means a player with attack range 4 can attack from 5 cells away when directly on line with a monster, but no longer diagonally as range is circular for players now
* Implemented a new "step action" feature to reflect official chase range behavior; when an attack or skill is used inside the attack range, it will act exactly as now, the skill is used once the signal from the client was received; however, if an attack or skill is used outside the attack range, the player should move into the chase area which is 1 cell inside the attack range border; the client actually sends us where to use the attack / skill from, but previously we just dropped that information the moment an attack request came; now instead of stopping the player instantly on an attack request, the player will continue moving to the target cell and then automatically use the command received earlier (it will be remembered); this change was absolutely necessary as the client sends the attack request slightly before attack range is reached, execution on official servers only takes place on every full cell moved; the new system copies this behavior
- Implemented an improved hit-lock system (bugreport:007460), the changes include:
* MVPs are no longer immune to being stopped by a hit unless they used Endure
* When hitting any unit, it will stop for its "dMotion" interval, exactly at the end of "dMotion" it will continue walking (official behavior); this helps getting more "move" packets to be displayed
* The unit that was hit will be immune to being stopped for another "dMotion" interval, this allows to slow down any units by hitting them frequently, but makes it almost impossible to completely stop them forever (depends a lot of ASPD and dMotion value); this does not affect special hit-lock properties (some skills and events should set delay anyway)
* The unit that was hit will no longer be "pulled" to the next cell; this caused a lot of position lag, it should be much better now
- Fixed Sightrasher missing its variable cast time in Renewal

Playtester 10 年之前
父節點
當前提交
2c86ee4356
共有 11 個文件被更改,包括 194 次插入48 次删除
  1. 3 3
      conf/battle/skill.conf
  2. 1 1
      db/re/skill_cast_db.txt
  3. 4 3
      src/config/core.h
  4. 3 3
      src/map/battle.c
  5. 1 1
      src/map/battle.h
  6. 2 1
      src/map/clif.c
  7. 13 14
      src/map/mob.c
  8. 32 0
      src/map/path.c
  9. 10 0
      src/map/path.h
  10. 122 21
      src/map/unit.c
  11. 3 1
      src/map/unit.h

+ 3 - 3
conf/battle/skill.conf

@@ -69,9 +69,9 @@ skill_out_range_consume: no
 skillrange_by_distance: 14
 
 // Should the equipped weapon's range override the skill's range defined in the skill_db for most weapon-based skills? (Note 3)
-// NOTE: Skills affected by this option are those whose range in the skill_db are negative. Note that unless monster_ai&0x400 is
-// set, the range of all skills is 9 for monsters.
-skillrange_from_weapon: 30
+// NOTE: Skills affected by this option are those whose range in the skill_db are negative. By default always the skill range is used.
+// Note that if you want all monster skills to have a range of 9 you need to set monster_ai&0x400.
+skillrange_from_weapon: 0
 
 // Should a check on the caster's status be performed in all skill attacks?
 // When set to yes, meteors, storm gust and any other ground skills will have 

+ 1 - 1
db/re/skill_cast_db.txt

@@ -158,7 +158,7 @@
 //-- WZ_FIREPILLAR
 80,1920:1728:1536:1344:1152:960:768:576:384:192,1000,0,30000,600:800:1000:1200:1400:1600:1800:2000:2200:2400,0,480:432:384:336:288:240:192:144:96:48
 //-- WZ_SIGHTRASHER
-81,0,2000,0,500,0,0,80
+81,320,2000,0,500,0,0,80
 //-- WZ_METEOR
 83,9600,2000:3000:3000:4000:4000:5000:5000:6000:6000:7000,0,500,5000,0,2400
 //-- WZ_JUPITEL

+ 4 - 3
src/config/core.h

@@ -42,9 +42,10 @@
 //#define CELL_NOSTACK
 
 /// Uncomment to enable circular area checks.
-/// By default, all range checks in Aegis are of Square shapes, so a weapon range
-/// - of 10 allows you to attack from anywhere within a 21x21 area.
-/// Enabling this changes such checks to circular checks, which is more realistic,
+/// By default, most server-sided range checks in Aegis are of square shapes, so a monster
+/// with a range of 4 can attack anything within a 9x9 area.
+/// Client-sided range checks are, however, are always circular.
+/// Enabling this changes all checks to circular checks, which is more realistic,
 /// - but is not the official behaviour.
 //#define CIRCULAR_AREA
 

+ 3 - 3
src/map/battle.c

@@ -7425,7 +7425,7 @@ bool battle_check_range(struct block_list *src, struct block_list *bl, int range
 #ifndef CIRCULAR_AREA
 	if( src->type == BL_PC ) { // Range for players' attacks and skills should always have a circular check. [Angezerus]
 		int dx = src->x - bl->x, dy = src->y - bl->y;
-		if( !check_distance(dx, dy, range) )
+		if( !check_distance_client(dx, dy, range) )
 			return false;
 	} else
 #endif
@@ -7467,7 +7467,7 @@ static const struct _battle_data {
 	{ "skill_add_range",                    &battle_config.skill_add_range,                 0,      0,      INT_MAX,        },
 	{ "skill_out_range_consume",            &battle_config.skill_out_range_consume,         1,      0,      1,              },
 	{ "skillrange_by_distance",             &battle_config.skillrange_by_distance,          ~BL_PC, BL_NUL, BL_ALL,         },
-	{ "skillrange_from_weapon",             &battle_config.use_weapon_skill_range,          ~BL_PC, BL_NUL, BL_ALL,         },
+	{ "skillrange_from_weapon",             &battle_config.use_weapon_skill_range,          BL_NUL, BL_NUL, BL_ALL,         },
 	{ "player_damage_delay_rate",           &battle_config.pc_damage_delay_rate,            100,    0,      INT_MAX,        },
 	{ "defunit_not_enemy",                  &battle_config.defnotenemy,                     0,      0,      1,              },
 	{ "gvg_traps_target_all",               &battle_config.vs_traps_bctall,                 BL_PC,  BL_NUL, BL_ALL,         },
@@ -7744,7 +7744,6 @@ static const struct _battle_data {
 	{ "berserk_cancels_buffs",              &battle_config.berserk_cancels_buffs,           0,      0,      1,              },
 	{ "debuff_on_logout",                   &battle_config.debuff_on_logout,                1|2,    0,      1|2,            },
 	{ "monster_ai",                         &battle_config.mob_ai,                          0x000,  0x000,  0x77F,          },
-	{ "monster_chase_refresh",              &battle_config.mob_chase_refresh,               1,      0,      30,             },
 	{ "hom_setting",                        &battle_config.hom_setting,                     0xFFFF, 0x0000, 0xFFFF,         },
 	{ "dynamic_mobs",                       &battle_config.dynamic_mobs,                    1,      0,      1,              },
 	{ "mob_remove_damaged",                 &battle_config.mob_remove_damaged,              1,      0,      1,              },
@@ -7901,6 +7900,7 @@ static const struct _battle_data {
 	{ "arrow_shower_knockback",             &battle_config.arrow_shower_knockback,          1,      0,      1,              },
 	{ "devotion_rdamage_skill_only",        &battle_config.devotion_rdamage_skill_only,     1,      0,      1,              },
 	{ "max_extended_aspd",                  &battle_config.max_extended_aspd,               193,    100,    199,            },
+	{ "monster_chase_refresh",              &battle_config.mob_chase_refresh,               1,      0,      30,             }
 };
 
 #ifndef STATS_OPT_OUT

+ 1 - 1
src/map/battle.h

@@ -402,7 +402,6 @@ extern struct Battle_Config
 	int berserk_cancels_buffs; // [Aru]
 	int debuff_on_logout; // Removes a few "official" negative Scs on logout. [Skotlex]
 	int mob_ai; //Configures various mob_ai settings to make them smarter or dumber(official). [Skotlex]
-	int mob_chase_refresh; //How often a monster should refresh its chase [Playtester]
 	int hom_setting; //Configures various homunc settings which make them behave unlike normal characters.. [Skotlex]
 	int dynamic_mobs; // Dynamic Mobs [Wizputer] - battle_athena flag implemented by [random]
 	int mob_remove_damaged; // Dynamic Mobs - Remove mobs even if damaged [Wizputer]
@@ -571,6 +570,7 @@ extern struct Battle_Config
 	int arrow_shower_knockback;
 	int devotion_rdamage_skill_only;
 	int max_extended_aspd;
+	int mob_chase_refresh; //How often a monster should refresh its chase [Playtester]
 } battle_config;
 
 void do_init_battle(void);

+ 2 - 1
src/map/clif.c

@@ -10306,7 +10306,8 @@ void clif_parse_ActionRequest_sub(struct map_session_data *sd, int action_type,
 		 sd->sc.data[SC__MANHOLE] ))
 		return;
 
-	pc_stop_walking(sd, 1);
+	if(action_type != 0x00 && action_type != 0x07)
+		pc_stop_walking(sd, 1);
 	pc_stop_attack(sd);
 
 	if(target_id<0 && -target_id == sd->bl.id) // for disguises [Valaris]

+ 13 - 14
src/map/mob.c

@@ -1649,26 +1649,24 @@ static bool mob_ai_sub_hard(struct mob_data *md, unsigned int tick)
 	}
 
 	//Out of range...
-	if (!(mode&MD_CANMOVE))
-	{	//Can't chase. Attempt an idle skill before unlocking.
-		if (md->ud.target != tbl->id || md->ud.attacktimer == INVALID_TIMER) 
-		{ //Only use skill if no more attack delay left
-			md->state.skillstate = MSS_IDLE;
-			if (!mobskill_use(md, tick, -1))
+	if (!(mode&MD_CANMOVE) || (!can_move && DIFF_TICK(tick, md->ud.canmove_tick) > 0))
+	{	//Can't chase. Immobile and trapped mobs should unlock target and use an idle skill on next interval.
+		if ((md->ud.target != tbl->id || md->ud.attacktimer == INVALID_TIMER)) 
+		{ //Only unlock target to use idle skill if no more attack left
+			md->ud.walk_count = (md->ud.walk_count+1)%250;
+			if (!(md->ud.walk_count%IDLE_SKILL_INTERVAL))
 				mob_unlocktarget(md,tick);
 		}
 		return true;
 	}
 
-	if (!can_move)
-	{	//Stuck. Attempt an idle skill
-		if (md->ud.target != tbl->id || md->ud.attacktimer == INVALID_TIMER) 
-		{ //Only use skill if no more attack delay left
-			md->state.skillstate = MSS_IDLE;
-			if (!(++md->ud.walk_count%IDLE_SKILL_INTERVAL))
-				mobskill_use(md, tick, -1);
+	//Before a monster starts to chase a target, it will check if it has a ranged "attack" skill to use on it.
+	if(md->ud.walktimer == INVALID_TIMER && (md->state.skillstate == MSS_BERSERK || md->state.skillstate == MSS_ANGRY)) 
+	{
+		if (DIFF_TICK(md->ud.canmove_tick, tick) <= MIN_MOBTHINKTIME && DIFF_TICK(md->ud.canact_tick, tick) < -MIN_MOBTHINKTIME*IDLE_SKILL_INTERVAL) 
+		{ //Only use skill if able to walk on next tick and not used a skill the last second
+			mobskill_use(md, tick, -1);
 		}
-		return true;
 	}
 
 	if (md->ud.walktimer != INVALID_TIMER && md->ud.target == tbl->id &&
@@ -1683,6 +1681,7 @@ static bool mob_ai_sub_hard(struct mob_data *md, unsigned int tick)
 		return true;
 
 	//Follow up if possible.
+	//Hint: Chase skills are handled in the walktobl routine
 	if(!mob_can_reach(md, tbl, md->min_chase, MSS_RUSH) ||
 		!unit_walktobl(&md->bl, tbl, md->status.rhw.range, 2))
 		mob_unlocktarget(md,tick);

+ 32 - 0
src/map/path.c

@@ -14,6 +14,7 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <math.h>
 
 #define SET_OPEN 0
 #define SET_CLOSED 1
@@ -465,3 +466,34 @@ unsigned int distance(int dx, int dy)
 	return (dx<dy?dy:dx);
 #endif
 }
+
+/**
+ * The client uses a circular distance instead of the square one. The circular distance
+ * is only used by units sending their attack commands via the client (not monsters).
+ * @param dx: Horizontal distance
+ * @param dy: Vertical distance
+ * @param distance: Distance to check against
+ * @return Within distance(1); Not within distance(0);
+ */
+int check_distance_client(int dx, int dy, int distance)
+{
+	return (distance_client(dx,dy) <= distance);
+}
+
+/**
+ * The client uses a circular distance instead of the square one. The circular distance
+ * is only used by units sending their attack commands via the client (not monsters).
+ * @param dx: Horizontal distance
+ * @param dy: Vertical distance
+ * @return Circular distance
+ */
+unsigned int distance_client(int dx, int dy)
+{
+	double temp_dist = sqrt((double)(dx*dx + dy*dy));
+
+	//Bonus factor used by client
+	//This affects even horizontal/vertical lines so they are one cell longer than expected
+	temp_dist -= 0.625;
+
+	return ((int)temp_dist);
+}

+ 10 - 0
src/map/path.h

@@ -32,6 +32,14 @@ struct path_interface *path;
 #define distance_blxy(bl, x1, y1) distance((bl)->x-(x1), (bl)->y-(y1))
 #define distance_xy(x0, y0, x1, y1) distance((x0)-(x1), (y0)-(y1))
 
+#define check_distance_client_bl(bl1, bl2, distance) check_distance_client((bl1)->x - (bl2)->x, (bl1)->y - (bl2)->y, distance)
+#define check_distance_client_blxy(bl, x1, y1, distance) check_distance_client((bl)->x-(x1), (bl)->y-(y1), distance)
+#define check_distance_client_xy(x0, y0, x1, y1, distance) check_distance_client((x0)-(x1), (y0)-(y1), distance)
+
+#define distance_client_bl(bl1, bl2) distance_client((bl1)->x - (bl2)->x, (bl1)->y - (bl2)->y)
+#define distance_client_blxy(bl, x1, y1) distance_client((bl)->x-(x1), (bl)->y-(y1))
+#define distance_client_xy(x0, y0, x1, y1) distance_client((x0)-(x1), (y0)-(y1))
+
 // calculates destination cell for knockback
 int path_blownpos(int16 m,int16 x0,int16 y0,int16 dx,int16 dy,int count);
 
@@ -44,5 +52,7 @@ bool path_search_long(struct shootpath_data *spd,int16 m,int16 x0,int16 y0,int16
 // distance related functions
 int check_distance(int dx, int dy, int distance);
 unsigned int distance(int dx, int dy);
+int check_distance_client(int dx, int dy, int distance);
+unsigned int distance_client(int dx, int dy);
 
 #endif /* _PATH_H_ */

+ 122 - 21
src/map/unit.c

@@ -355,6 +355,61 @@ static int unit_walktoxy_timer(int tid, unsigned int tick, int id, intptr_t data
 	if(tid == INVALID_TIMER) // A directly invoked timer is from battle_stop_walking, therefore the rest is irrelevant.
 		return 0;
 
+	//If stepaction is set then we remembered a client request that should be executed on the next step
+	//Execute request now if target is in attack range
+	if (ud->stepaction && ud->target_to) {
+		//Delay stepactions by half a step (so they are executed at full step)
+		if(ud->walkpath.path[ud->walkpath.path_pos]&1)
+			i = status_get_speed(bl)*14/20;
+		else
+			i = status_get_speed(bl)/2;
+		if(ud->stepskill_id && skill_get_inf(ud->stepskill_id) & INF_GROUND_SKILL) {
+			//Ground skill, create imaginary target
+			struct block_list tbl;
+			struct map_data *md = &map[bl->m];			
+			tbl.type = BL_NUL;
+			tbl.m = bl->m;
+			//Convert target_to back to map coordinates
+			tbl.x = ud->target_to%md->xs;
+			tbl.y = ud->target_to/md->xs;
+			if (battle_check_range(bl, &tbl, ud->chaserange)) {
+				//Execute ground skill
+				ud->stepaction = false;
+				ud->target_to = 0;
+				unit_stop_walking(bl, 1);
+				//TODO: Delay skill use
+				unit_skilluse_pos(bl, tbl.x, tbl.y, ud->stepskill_id, ud->stepskill_lv);
+				return 0;
+			}
+		} else {
+			//If a player has target_to set and target is in range, attempt attack
+			struct block_list *tbl = map_id2bl(ud->target_to);
+			if (!tbl || !status_check_visibility(bl, tbl)) {
+				ud->target_to = 0;
+			}
+			if (battle_check_range(bl, tbl, ud->chaserange)) {
+				// Close enough to attempt an attack
+				if(ud->stepskill_id == 0) {
+					//Execute normal attack
+					ud->stepaction = false;
+					ud->target = ud->target_to;
+					ud->target_to = 0;
+					unit_stop_walking(bl, 1);
+					ud->attacktimer=add_timer(tick+i,unit_attack_timer,bl->id,0);
+					return 0;
+				} else {
+					//Execute non-ground skill
+					ud->stepaction = false;
+					ud->target_to = 0;
+					unit_stop_walking(bl, 1);
+					//TODO: Delay skill use
+					unit_skilluse_id(bl, tbl->id, ud->stepskill_id, ud->stepskill_lv);
+					return 0;
+				}
+			}
+		}
+	}
+
 	if(ud->state.change_walk_target)
 		return unit_walktoxy_sub(bl);
 
@@ -375,7 +430,7 @@ static int unit_walktoxy_timer(int tid, unsigned int tick, int id, intptr_t data
 		// Keep trying to run.
 		if ( !(unit_run(bl) || unit_wugdash(bl,sd)) )
 			ud->state.running = 0;
-	} else if (ud->target_to) {
+	} else if (!ud->stepaction && ud->target_to) {
 		// Update target trajectory.
 		struct block_list *tbl = map_id2bl(ud->target_to);
 		if (!tbl || !status_check_visibility(bl, tbl)) { // Cancel chase.
@@ -1300,10 +1355,6 @@ int unit_set_walkdelay(struct block_list *bl, unsigned int tick, int delay, int
 	if (delay <= 0 || !ud)
 		return 0;
 
-	// /MvP mobs have no walk delay
-	if( bl->type == BL_MOB && (((TBL_MOB*)bl)->status.mode&MD_BOSS) )
-		return 0;
-
 	if (type) {
 		if (DIFF_TICK(ud->canmove_tick, tick+delay) > 0)
 			return 0;
@@ -1311,6 +1362,9 @@ int unit_set_walkdelay(struct block_list *bl, unsigned int tick, int delay, int
 		// Don't set walk delays when already trapped.
 		if (!unit_can_move(bl))
 			return 0;
+		//Immune to being stopped for double the flinch time
+		if (DIFF_TICK(ud->canmove_tick, tick-delay) > 0)
+			return 0;
 	}
 
 	ud->canmove_tick = tick + delay;
@@ -1323,7 +1377,7 @@ int unit_set_walkdelay(struct block_list *bl, unsigned int tick, int delay, int
 			if(ud->state.running)
 				add_timer(ud->canmove_tick, unit_resume_running, bl->id, (intptr_t)ud);
 			else {
-				unit_stop_walking(bl,2|4);
+				unit_stop_walking(bl,4);
 
 				if(ud->target)
 					add_timer(ud->canmove_tick+1, unit_walktobl_sub, bl->id, ud->target);
@@ -1554,6 +1608,19 @@ int unit_skilluse_id2(struct block_list *src, int target_id, uint16 skill_id, ui
 	else
 		range = skill_get_range2(src, skill_id, skill_lv); // Skill cast distance from database
 
+	// Remember the skill request from the client while walking to the next cell
+	if(src->type == BL_PC && ud->walktimer != INVALID_TIMER && !battle_check_range(src, target, range-1)) {
+		ud->stepaction = true;
+		ud->target_to = target_id;
+		ud->chaserange = range;
+		ud->stepskill_id = skill_id;
+		ud->stepskill_lv = skill_lv;
+		return 0; // Attacking will be handled by unit_walktoxy_timer in this case
+	} else {
+		// To make sure a failed stepaction is not remembered any longer
+		ud->stepaction = false;
+	}
+
 	// Check range when not using skill on yourself or is a combo-skill during attack
 	// (these are supposed to always have the same range as your attack)
 	if( src->id != target_id && (!combo || ud->attacktimer == INVALID_TIMER) ) {
@@ -1858,10 +1925,25 @@ int unit_skilluse_pos2( struct block_list *src, short skill_x, short skill_y, ui
 	else
 		range = skill_get_range2(src, skill_id, skill_lv); // Skill cast distance from database
 
+	// Remember the skill request from the client while walking to the next cell
+	if(src->type == BL_PC && ud->walktimer != INVALID_TIMER && !battle_check_range(src, &bl, range-1)) {
+		struct map_data *md = &map[src->m];
+		// Convert coordinates to target_to so we can use it as target later
+		ud->stepaction = true;
+		ud->target_to = (skill_x + skill_y*md->xs);
+		ud->chaserange = range;
+		ud->stepskill_id = skill_id;
+		ud->stepskill_lv = skill_lv;
+		return 0; // Attacking will be handled by unit_walktoxy_timer in this case
+	} else {
+		// To make sure a failed stepaction is not remembered any longer
+		ud->stepaction = false;
+	}
+
 	if( skill_get_state(ud->skill_id) == ST_MOVE_ENABLE ) {
 		if( !unit_can_reach_bl(src, &bl, range + 1, 1, NULL, NULL) )
 			return 0; // Walk-path check failed.
-	}else if( !battle_check_range(src, &bl, range + 1) )
+	}else if( !battle_check_range(src, &bl, range) )
 		return 0; // Arrow-path check failed.
 
 	unit_stop_attack(src);
@@ -2013,6 +2095,7 @@ int unit_attack(struct block_list *src,int target_id,int continuous)
 {
 	struct block_list *target;
 	struct unit_data  *ud;
+	int range;
 
 	nullpo_ret(ud = unit_bl2ud(src));
 
@@ -2049,17 +2132,28 @@ int unit_attack(struct block_list *src,int target_id,int continuous)
 	ud->state.attack_continue = continuous;
 	unit_set_target(ud, target_id);
 
+	range = status_get_range(src);
+
 	if (continuous) // If you're to attack continously, set to auto-chase character
-		ud->chaserange = status_get_range(src);
+		ud->chaserange = range;
 
 	// Just change target/type. [Skotlex]
 	if(ud->attacktimer != INVALID_TIMER)
 		return 0;
 
-	// Set Mob's ANGRY/BERSERK states.
-	if(src->type == BL_MOB)
-		((TBL_MOB*)src)->state.skillstate = ((TBL_MOB*)src)->state.aggressive?MSS_ANGRY:MSS_BERSERK;
-
+	// Remember the attack request from the client while walking to the next cell
+	if(src->type == BL_PC && ud->walktimer != INVALID_TIMER && !battle_check_range(src, target, range-1)) {
+		ud->stepaction = true;
+		ud->target_to = ud->target;
+		ud->chaserange = range;
+		ud->stepskill_id = 0;
+		ud->stepskill_lv = 0;
+		return 0; // Attacking will be handled by unit_walktoxy_timer in this case
+	} else {
+		// To make sure a failed stepaction is not remembered any longer
+		ud->stepaction = false;
+	}
+	
 	if(DIFF_TICK(ud->attackabletime, gettick()) > 0) // Do attack next time it is possible. [Skotlex]
 		ud->attacktimer=add_timer(ud->attackabletime,unit_attack_timer,src->id,0);
 	else // Attack NOW.
@@ -2323,17 +2417,18 @@ static int unit_attack_timer_sub(struct block_list* src, int tid, unsigned int t
 	}
 
 	sstatus = status_get_status_data(src);
-	range = sstatus->rhw.range + 1;
+	range = sstatus->rhw.range;
 
 	if( unit_is_walking(target) )
 		range++; // Extra range when chasing
 
-	if( !check_distance_bl(src,target,range) ) { // Chase if required.
-		if(sd)
-			clif_movetoattack(sd,target);
-		else if(ud->state.attack_continue)
-			unit_walktobl(src,target,ud->chaserange,ud->state.walk_easy|2);
-
+	if(sd && !check_distance_client_bl(src,target,range)) {
+		// Player tries to attack but target is too far, notify client
+		clif_movetoattack(sd,target);
+		return 1;
+	} else if(md && !check_distance_bl(src,target,range)) {
+		// Monster: Chase if required
+		unit_walktobl(src,target,ud->chaserange,ud->state.walk_easy|2);
 		return 1;
 	}
 
@@ -2362,8 +2457,14 @@ static int unit_attack_timer_sub(struct block_list* src, int tid, unsigned int t
 			unit_stop_walking(src,1);
 
 		if(md) {
-			if (mobskill_use(md,tick,-1))
-				return 1;
+			//First attack is always a normal attack
+			if(md->state.skillstate == MSS_ANGRY || md->state.skillstate == MSS_BERSERK) {
+				if (mobskill_use(md,tick,-1))
+					return 1;
+			} else {
+				// Set mob's ANGRY/BERSERK states.
+				md->state.skillstate = md->state.aggressive?MSS_ANGRY:MSS_BERSERK;
+			}
 
 			if (sstatus->mode&MD_ASSIST && DIFF_TICK(md->last_linktime, tick) < MIN_MOBLINKTIME) { 
 				// Link monsters nearby [Skotlex]

+ 3 - 1
src/map/unit.h

@@ -33,7 +33,9 @@ struct unit_data {
 	int   target_to;
 	int   attacktimer;
 	int   walktimer;
-	int	chaserange;
+	int   chaserange;
+	bool  stepaction; //Action should be executed on step [Playtester]
+	uint16 stepskill_id,stepskill_lv; //Remembers skill that should be casted on step [Playtester]
 	unsigned int attackabletime;
 	unsigned int canact_tick;
 	unsigned int canmove_tick;