Quellcode durchsuchen

Monster Ranges, Looters and Blind (#8936)

- Looters now have a loot range of 12 (configurable) that is no longer reduced by Blind
- Chase range is no longer increased when hitting a monster from outside its chase range 
  * Removed the "min_chase" custom solution
- Blind now reduces both aggro range and chase range to 1
- Chase range at the beginning of a chase is now only checked in Angry state (blind doesn't matter)
- Chase range (including blind) is now checked whenever the attack timer ends
  * The attack timer can no longer be stopped by pre-planning a chase
  * This will make monsters stop attacking when outside chase range at the end of the timer
- Monsters will now think about updating their chase already one cell before they reach the end of their path
  * This is not only official but also makes their movement and behavior smoother
  * If at this time no enemy is in chase range, the monster will drop its target but still finish moving
- A monster's chase path will now always go all the way next to the target, even if the monster has ranged attacks
  * Monsters will now check for each cell moved if target is in attack range and then immediately attack
  * If they still have attack delay they will stop and attack when ready (if in chase range at this point)
- Fixed a timer to delay movement during hit lock
  * Previously the timer didn't work at all and the whole mob AI had to be processed again
  * Now the monster will start moving immediately once the hit lock is gone
- Fixed an endless loop that occurred when we call mob_randomwalk while the mob still has a target
  * This happens because we pre-plan chases with timers but the state remains Idle until the chase starts
- Monsters now become active once a player is within 18 cells (AREA_SIZE+4)
- Slaves now teleport back to their master if they are more than 15 cells away (AREA_SIZE+1) 
  * This only applies to AI_ABR and AI_BIONIC unless "slave_stick_with_master" is enabled
- Improved code structure in unit.cpp: Bundling repeating code segments into new functions
- Fixes #8914
Playtester vor 3 Monaten
Ursprung
Commit
9f81f8d606
9 geänderte Dateien mit 220 neuen und 135 gelöschten Zeilen
  1. 14 10
      conf/battle/monster.conf
  2. 2 1
      src/map/battle.cpp
  3. 1 0
      src/map/battle.hpp
  4. 40 30
      src/map/mob.cpp
  5. 0 3
      src/map/mob.hpp
  6. 19 12
      src/map/status.cpp
  7. 1 1
      src/map/status.hpp
  8. 141 77
      src/map/unit.cpp
  9. 2 1
      src/map/unit.hpp

+ 14 - 10
conf/battle/monster.conf

@@ -19,9 +19,9 @@ monster_hp_rate: 100
 monster_max_aspd: 199
 
 // Defines various mob AI related settings. (Note 3)
-// 0x0001: When enabled mobs will update their target cell every few iterations
-//         (normally they never update their target cell until they reach it while
-//         chasing)
+// 0x0001: When enabled, mobs will update their target cell every x cells moved.
+//         (normally they never update their target cell until they are one cell
+//         before the end of their walkpath. x = monster_chase_refresh, see below)
 // 0x0002: Makes mob use their "rude attack" skill (usually warping away) if they
 //         are attacked and they can't attack back regardless of how they were
 //         attacked (eg: GrimTooth), otherwise, their "rude attack" is only activated
@@ -63,13 +63,14 @@ monster_ai: 0
 // How often should a monster rethink its chase?
 // 0: Every 100ms (MIN_MOBTHINKTIME)
 // 1: Every cell moved
-// x: Every x cells moved or at end of the chase path
-// 30 (max): Only at end of the chase path (official)
-// Regardless of this setting, a monster will always check for targets in attack
-// range. Decrease this value if you want to make monsters to be more reactive while
-// chasing. This also defines the maximum amount of cells monsters will move after
-// they lost their target (hide, no line of sight, etc.).
-monster_chase_refresh: 30
+// x: Every x cells moved or one cell before the end of the chase path
+// 32 (max): One cell before the end of the chase path (official)
+// Regardless of this setting, a monster will always check for targets in attack range.
+// Decrease this value if you want to make monsters be more reactive while chasing.
+// If you want monsters to update their target cell while chasing you also need to enable
+// monster_ai 0x0001, see above. Otherwise this only defines the maximum amount of cells
+// monsters will move after they lost their target (hide, no line of sight, etc.).
+monster_chase_refresh: 32
 
 // Should mobs be able to be warped (add as needed)?
 // 0: Disable.
@@ -98,6 +99,9 @@ chase_range_rate: 100
 // be increased by that number.
 monster_eye_range_bonus: 0
 
+// Range in which looters search for loot (eAthena - 10, official - 12, max - 32)
+loot_range: 12
+
 // Allow monsters to be aggresive and attack first? (Note 1)
 monster_active_enable: yes
 

+ 2 - 1
src/map/battle.cpp

@@ -11791,7 +11791,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,               30,     0,      MAX_MINCHASE,   },
+	{ "monster_chase_refresh",              &battle_config.mob_chase_refresh,               32,     0,      MAX_WALKPATH,   },
 	{ "mob_icewall_walk_block",             &battle_config.mob_icewall_walk_block,          75,     0,      255,            },
 	{ "boss_icewall_walk_block",            &battle_config.boss_icewall_walk_block,         0,      0,      255,            },
 	{ "snap_dodge",                         &battle_config.snap_dodge,                      0,      0,      1,              },
@@ -11943,6 +11943,7 @@ static const struct _battle_data {
 	{ "hom_delay_reset_vaporize",           &battle_config.hom_delay_reset_vaporize,        1,      0,      1,              },
 	{ "hom_delay_reset_warp",               &battle_config.hom_delay_reset_warp,            1,      0,      1,              },
 #endif
+	{ "loot_range",                         &battle_config.loot_range,                      12,     1,      MAX_WALKPATH,   },
 
 #include <custom/battle_config_init.inc>
 };

+ 1 - 0
src/map/battle.hpp

@@ -766,6 +766,7 @@ struct Battle_Config
 	int32 item_stacking;
 	int32 hom_delay_reset_vaporize;
 	int32 hom_delay_reset_warp;
+	int32 loot_range;
 
 #include <custom/battle_config_struct.inc>
 };

+ 40 - 30
src/map/mob.cpp

@@ -43,7 +43,7 @@
 
 using namespace rathena;
 
-#define ACTIVE_AI_RANGE 2	//Distance added on top of 'AREA_SIZE' at which mobs enter active AI mode.
+#define ACTIVE_AI_RANGE 4	//Distance added on top of 'AREA_SIZE' at which mobs enter active AI mode.
 
 const t_tick MOB_MAX_DELAY = 24 * 3600 * 1000;
 #define RUDE_ATTACKED_COUNT 1	//After how many rude-attacks should the skill be used?
@@ -1031,7 +1031,6 @@ int32 mob_linksearch(struct block_list *bl,va_list ap)
 		md->last_linktime = tick;
 		if( mob_can_reach(md,target,md->db->range2) ){	// Reachability judging
 			md->target_id = target->id;
-			md->min_chase=md->db->range3;
 			return 1;
 		}
 	}
@@ -1282,7 +1281,6 @@ int32 mob_target(struct mob_data *md,struct block_list *bl,int32 dist)
 	// When an angry monster is provoked, it will switch to retaliate AI
 	if (md->state.provoke_flag && md->state.aggressive)
 		md->state.aggressive = 0;
-	md->min_chase = cap_value(dist + md->db->range3 - md->status.rhw.range, md->db->range3, MAX_MINCHASE);
 	return 0;
 }
 
@@ -1335,7 +1333,6 @@ static int32 mob_ai_sub_hard_activesearch(struct block_list *bl,va_list ap)
 #endif
 		(*target) = bl;
 		md->target_id=bl->id;
-		md->min_chase = cap_value(dist + md->db->range3 - md->status.rhw.range, md->db->range3, MAX_MINCHASE);
 		return 1;
 
 	}
@@ -1364,7 +1361,6 @@ static int32 mob_ai_sub_hard_changechase(struct block_list *bl,va_list ap)
 	{
 		(*target) = bl;
 		md->target_id=bl->id;
-		md->min_chase= md->db->range3;
 	}
 	return 1;
 }
@@ -1399,7 +1395,7 @@ static int32 mob_ai_sub_hard_lootsearch(struct block_list *bl,va_list ap)
 	target = va_arg(ap,struct block_list**);
 
 	dist = distance_bl(&md->bl, bl);
-	if (mob_can_reach(md, bl, md->db->range3) && (
+	if (mob_can_reach(md, bl, battle_config.loot_range) && (
 		(*target) == nullptr ||
 		(battle_config.monster_loot_search_type && md->target_id > bl->id) ||
 		(!battle_config.monster_loot_search_type && !check_distance_bl(&md->bl, *target, dist)) // New target closer than previous one.
@@ -1407,7 +1403,6 @@ static int32 mob_ai_sub_hard_lootsearch(struct block_list *bl,va_list ap)
 	{
 		(*target) = bl;
 		md->target_id = bl->id;
-		md->min_chase = md->db->range3;
 	}
 	else if (!battle_config.monster_loot_search_type)
 		mob_stop_walking(md, 1); // Stop walking immediately if item is no longer on the ground.
@@ -1460,14 +1455,12 @@ static int32 mob_ai_sub_hard_slavemob(struct mob_data *md,t_tick tick)
 
 	if(status_has_mode(&md->status,MD_CANMOVE))
 	{	//If the mob can move, follow around. [Check by Skotlex]
-		int32 old_dist = md->master_dist;
-
 		// Distance with between slave and master is measured.
 		md->master_dist = distance_bl(&md->bl, bl);
 
 		if (battle_config.slave_stick_with_master || md->special_state.ai == AI_ABR || md->special_state.ai == AI_BIONIC) {
-			// Since the master was in near immediately before, teleport is carried out and it pursues.
-			if (bl->m != md->bl.m || (old_dist < 10 && md->master_dist > 18) || md->master_dist > MAX_MINCHASE) {
+			// Teleport to master if further away than 15 cells (official value for AI_ABR and AI_BIONIC)
+			if (bl->m != md->bl.m || md->master_dist > AREA_SIZE+1) {
 				md->master_dist = 0;
 				unit_warp(&md->bl, bl->m, bl->x, bl->y, CLR_TELEPORT);
 				return 1;
@@ -1526,7 +1519,6 @@ static int32 mob_ai_sub_hard_slavemob(struct mob_data *md,t_tick tick)
 			}
 			if (tbl && status_check_skilluse(&md->bl, tbl, 0, 0)) {
 				md->target_id=tbl->id;
-				md->min_chase = cap_value(distance_bl(&md->bl, tbl) + md->db->range3 - md->status.rhw.range, md->db->range3, MAX_MINCHASE);
 				return 1;
 			}
 		}
@@ -1620,6 +1612,9 @@ int32 mob_randomwalk(struct mob_data *md,t_tick tick)
 	   !status_has_mode(&md->status,MD_CANMOVE))
 		return 0;
 
+	// Make sure the monster has no target anymore, otherwise the walkpath will check for it
+	md->ud.target_to = 0;
+
 	r=rnd();
 	rdir=rnd()%4; // Randomize direction in which we iterate to prevent monster cluttering up in one corner
 	dx=r%(d*2+1)-d;
@@ -1723,8 +1718,8 @@ int32 mob_warpchase(struct mob_data *md, struct block_list *target)
 static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
 {
 	struct block_list *tbl = nullptr, *abl = nullptr;
-	int32 mode;
-	int32 view_range, can_move;
+	bool can_move;
+	int32 view_range, mode;
 
 	if(md->bl.prev == nullptr || md->status.hp == 0)
 		return false;
@@ -1748,7 +1743,7 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
 	}
 
 	if (md->sc.getSCE(SC_BLIND))
-		view_range = 3;
+		view_range = 1;
 	else
 		view_range = md->db->range2;
 	mode = status_get_mode(&md->bl);
@@ -1762,7 +1757,6 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
 		tbl = map_id2bl(md->target_id);
 		if (!tbl || tbl->m != md->bl.m ||
 			(md->ud.attacktimer == INVALID_TIMER && !status_check_skilluse(&md->bl, tbl, 0, 0)) ||
-			(md->ud.walktimer != INVALID_TIMER && !(battle_config.mob_ai&0x1) && !check_distance_bl(&md->bl, tbl, md->min_chase)) ||
 			(
 				tbl->type == BL_PC &&
 				((((TBL_PC*)tbl)->state.gangsterparadise && !(mode&MD_STATUSIMMUNE)) ||
@@ -1789,7 +1783,7 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
 						|| md->sc.getSCE(SC__MANHOLE) // Not yet confirmed if boss will teleport once it can't reach target.
 						|| md->walktoxy_fail_count > 0)
 					)
-					|| !mob_can_reach(md, tbl, md->min_chase)
+					|| !mob_can_reach(md, tbl, md->db->range3)
 				)
 			&&  md->state.attacked_count++ >= RUDE_ATTACKED_COUNT
 			&&  !mobskill_use(md, tick, MSC_RUDEATTACKED) // If can't rude Attack
@@ -1804,7 +1798,7 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
 		{
 			int32 dist;
 			if( md->bl.m != abl->m || abl->prev == nullptr
-				|| (dist = distance_bl(&md->bl, abl)) >= MAX_MINCHASE // Attacker longer than visual area
+				|| (dist = distance_bl(&md->bl, abl)) > AREA_SIZE // Attacker longer than visual area
 				|| battle_check_target(&md->bl, abl, BCT_ENEMY) <= 0 // Attacker is not enemy of mob
 				|| (battle_config.mob_ai&0x2 && !status_check_skilluse(&md->bl, abl, 0, 0)) // Cannot normal attack back to Attacker
 				|| (!battle_check_range(&md->bl, abl, md->status.rhw.range) // Not on Melee Range and ...
@@ -1839,7 +1833,6 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
 				md->target_id = md->attacked_id; // set target
 				if (md->state.attacked_count)
 					md->state.attacked_count--; //Should we reset rude attack count?
-				md->min_chase = cap_value(dist + md->db->range3 - md->status.rhw.range, md->db->range3, MAX_MINCHASE);
 				tbl = abl; //Set the new target
 			}
 		}
@@ -1861,7 +1854,7 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
 	if (!tbl && can_move && mode&MD_LOOTER && md->lootitems && DIFF_TICK(tick, md->ud.canact_tick) > 0 &&
 		(md->lootitem_count < LOOTITEM_SIZE || battle_config.monster_loot_type != 1))
 	{	// Scan area for items to loot, avoid trying to loot if the mob is full and can't consume the items.
-		map_foreachinshootrange (mob_ai_sub_hard_lootsearch, &md->bl, view_range, BL_ITEM, md, &tbl);
+		map_foreachinshootrange (mob_ai_sub_hard_lootsearch, &md->bl, battle_config.loot_range, BL_ITEM, md, &tbl);
 	}
 
 	if ((mode&MD_AGGRESSIVE && (!tbl || slave_lost_target)) || md->state.skillstate == MSS_FOLLOW)
@@ -1992,7 +1985,6 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
 					tbl = battle_getenemy(&md->bl, DEFAULT_ENEMY_TYPE(md), search_size);
 					if (tbl != nullptr) {
 						md->target_id = tbl->id;
-						md->min_chase = md->db->range3;
 					}
 				}
 			}
@@ -2006,13 +1998,10 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
 				md->ud.attackabletime = tick + md->status.adelay;
 			}
 		}
+		//Target still in attack range, no need to chase the target
 		return true;
 	}
 
-	//Target still in attack range, no need to chase the target
-	if(battle_check_range(&md->bl, tbl, md->status.rhw.range))
-		return true;
-
 	//Only update target cell / drop target after having moved at least "mob_chase_refresh" cells
 	if(md->ud.walktimer != INVALID_TIMER && (!can_move || md->ud.walkpath.path_pos <= battle_config.mob_chase_refresh)
 		&& mob_is_chasing(md->state.skillstate))
@@ -2037,18 +2026,39 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
 		return true;
 	}
 
-	if (md->ud.walktimer != INVALID_TIMER && md->ud.target == tbl->id &&
+	// Officially we can stop here if monster is still chasing its target.
+	// If setting to update the chase path is set, we continue if current target tile not within attack range.
+	// In this case, we should also stop updating the chase path when target no longer in chase range.
+	if (md->ud.walktimer != INVALID_TIMER && md->ud.target_to == tbl->id &&
 		(
 			!(battle_config.mob_ai&0x1) ||
-			check_distance_blxy(tbl, md->ud.to_x, md->ud.to_y, md->status.rhw.range)
-	)) //Current target tile is still within attack range.
+			check_distance_blxy(tbl, md->ud.to_x, md->ud.to_y, md->status.rhw.range) ||
+			!check_distance_bl(&md->bl, tbl, md->db->range3)
+	))
 		return true;
 
-	//Follow up if possible.
-	if (!mob_can_reach(md, tbl, md->min_chase)) {
+	// Follow up if possible.
+
+	// Officially when a monster cannot act, move or attack, the AI isn't processed at all.
+	// The attacked ID remains unprocessed, so that once the monster can act again it will process it at this point.
+	// Our code works quite different from that and we try to plan a chase ahead of time.
+	// If we would go ahead now and initiate a chase, it will kill the attack timer which is not good because we
+	// need to check for visibility at the end of it for monsters because that's when they actually drop target.
+	// However, we only process a monster's AI every 100ms instead of every 20ms like on official servers, so
+	// without planning a chase, a monster is easy to hitlock.
+	// For now we prevent processing this part of the code if an attack timer is still running and the monster
+	// cannot move yet.
+	if (md->ud.walktimer == INVALID_TIMER
+		&& md->ud.attacktimer != INVALID_TIMER
+		&& DIFF_TICK(md->ud.canmove_tick, tick) > 0)
+		return true;
+
+	// Monsters in Angry state only start chasing when target is in chase range
+	if (md->state.skillstate == MSS_ANGRY && !mob_can_reach(md, tbl, md->db->range3)) {
 		mob_unlocktarget(md, tick);
 		return true;
 	}
+
 	// Monsters can use chase skills before starting to walk
 	// So we need to change the state and check for a skill here already
 	// But only use skill if able to walk on next tick and not attempted a skill the last second

+ 0 - 3
src/map/mob.hpp

@@ -30,8 +30,6 @@ struct guardian_data;
 #define MAX_MOB_DROP_TOTAL (MAX_MOB_DROP+MAX_MOB_DROP_ADD)
 #define MAX_MVP_DROP_TOTAL (MAX_MVP_DROP+MAX_MVP_DROP_ADD)
 
-#define MAX_MINCHASE 30	//Max minimum chase value to use for mobs.
-
 //Min time between AI executions
 const t_tick MIN_MOBTHINKTIME = 100;
 //Min time before mobs do a check to call nearby friends for help (or for slaves to support their master)
@@ -370,7 +368,6 @@ struct mob_data {
 	t_tick next_walktime,last_thinktime,last_linktime,last_pcneartime,dmgtick,last_canmove,last_skillcheck;
 	short move_fail_count;
 	short lootitem_count;
-	short min_chase;
 	unsigned char walktoxy_fail_count; //Pathfinding succeeds but the actual walking failed (e.g. Icewall lock)
 
 	int32 deletetimer;

+ 19 - 12
src/map/status.cpp

@@ -1699,7 +1699,7 @@ int32 status_damage(struct block_list *src,struct block_list *target,int64 dhp,
 		unit_remove_map(target,CLR_DEAD);
 	else { // Some death states that would normally be handled by unit_remove_map
 		unit_stop_attack(target);
-		unit_stop_walking(target,1);
+		unit_stop_walking(target,USW_FIXPOS|USW_RELEASE_TARGET);
 		unit_skillcastcancel(target,0);
 		clif_clearunit_area( *target, CLR_DEAD );
 		skill_unit_move(target,gettick(),4);
@@ -2239,16 +2239,17 @@ bool status_check_skilluse(struct block_list *src, struct block_list *target, ui
  * Checks whether the src can see the target
  * @param src:	Object using skill on target [PC|MOB|PET|HOM|MER|ELEM]
  * @param target: Object being targeted by src [PC|MOB|HOM|MER|ELEM]
- * @return src can see (1) or target is invisible (0)
+ * @param checkblind: Whether blind condition should be considered (sets view range to 1)
+ * @return src can see (true) or target is invisible (false)
  * @author [Skotlex]
  */
-int32 status_check_visibility(struct block_list *src, struct block_list *target)
+bool status_check_visibility(block_list* src, block_list* target, bool checkblind)
 {
 	int32 view_range;
 	status_change* tsc = status_get_sc(target);
 	switch (src->type) {
 		case BL_MOB:
-			view_range = ((TBL_MOB*)src)->min_chase;
+			view_range = ((TBL_MOB*)src)->db->range3;
 			break;
 		case BL_PET:
 			view_range = ((TBL_PET*)src)->db->range2;
@@ -2257,11 +2258,17 @@ int32 status_check_visibility(struct block_list *src, struct block_list *target)
 			view_range = AREA_SIZE;
 	}
 
+	if (checkblind) {
+		status_change* sc = status_get_sc(src);
+		if (sc != nullptr && sc->getSCE(SC_BLIND) != nullptr)
+			view_range = 1;
+	}
+
 	if (src->m != target->m || !check_distance_bl(src, target, view_range))
-		return 0;
+		return false;
 
 	if ( src->type == BL_NPC) // NPCs don't care for the rest
-		return 1;
+		return true;
 
 	if (tsc) {
 		bool is_boss = (status_get_class_(src) == CLASS_BOSS);
@@ -2272,24 +2279,24 @@ int32 status_check_visibility(struct block_list *src, struct block_list *target)
 					map_session_data *tsd = (TBL_PC*)target;
 
 					if (((tsc->option&(OPTION_HIDE|OPTION_CLOAK|OPTION_CHASEWALK)) || tsc->getSCE(SC_CAMOUFLAGE) || tsc->getSCE(SC_STEALTHFIELD) || tsc->getSCE(SC_SUHIDE)) && !is_boss && (tsd->special_state.perfect_hiding || !is_detector))
-						return 0;
+						return false;
 					if ((tsc->getSCE(SC_CLOAKINGEXCEED) || tsc->getSCE(SC_NEWMOON)) && !is_boss && ((tsd && tsd->special_state.perfect_hiding) || is_detector))
-						return 0;
+						return false;
 					if (tsc->getSCE(SC__FEINTBOMB) && !is_boss && !is_detector)
-						return 0;
+						return false;
 				}
 				break;
 			case BL_ELEM:
 				if (tsc->getSCE(SC_ELEMENTAL_VEIL) && !is_boss && !is_detector)
-					return 0;
+					return false;
 				break;
 			default:
 				if (((tsc->option&(OPTION_HIDE|OPTION_CLOAK|OPTION_CHASEWALK)) || tsc->getSCE(SC_CAMOUFLAGE) || tsc->getSCE(SC_STEALTHFIELD) || tsc->getSCE(SC_SUHIDE)) && !is_boss && !is_detector)
-					return 0;
+					return false;
 		}
 	}
 
-	return 1;
+	return true;
 }
 
 /**

+ 1 - 1
src/map/status.hpp

@@ -3632,7 +3632,7 @@ void status_calc_state(struct block_list *bl, status_change *sc, std::bitset<SCS
 void status_calc_slave_mode(mob_data& md);
 
 bool status_check_skilluse(struct block_list *src, struct block_list *target, uint16 skill_id, int32 flag);
-int32 status_check_visibility(struct block_list *src, struct block_list *target);
+bool status_check_visibility(block_list* src, block_list* target, bool checkblind);
 
 int32 status_change_spread(block_list *src, block_list *bl);
 

+ 141 - 77
src/map/unit.cpp

@@ -78,6 +78,111 @@ struct unit_data* unit_bl2ud(struct block_list *bl)
 	}
 }
 
+/**
+ * Updates chase depending on situation:
+ * If target in attack range -> attack
+ * If target out of sight -> drop target
+ * Otherwise update chase path
+ * @param bl: Moving bl
+ * @param tick: Current tick
+ * @param fullcheck: If false, only check for attack, don't drop target or update chase path
+ * @return Whether the chase path was updated (true) or current movement can continue (false)
+ */
+bool unit_update_chase(block_list& bl, t_tick tick, bool fullcheck) {
+	unit_data* ud = unit_bl2ud(&bl);
+
+	if (ud == nullptr)
+		return true;
+
+	block_list* tbl = nullptr;
+	if (ud->target_to != 0)
+		tbl = map_id2bl(ud->target_to);
+
+	// Reached destination, start attacking
+	if (tbl != nullptr && tbl->m == bl.m && check_distance_bl(&bl, tbl, ud->chaserange) && status_check_visibility(&bl, tbl, false)) {
+		ud->to_x = bl.x;
+		ud->to_y = bl.y;
+		ud->target_to = 0;
+		// Aegis uses one before every attack, we should
+		// only need this one for syncing purposes.
+		clif_fixpos(bl);
+		if (ud->state.attack_continue)
+			unit_attack(&bl, tbl->id, ud->state.attack_continue);
+		return true;
+	}
+	// Cancel chase
+	else if (tbl == nullptr || (fullcheck && !status_check_visibility(&bl, tbl, (bl.type == BL_MOB)))) {
+		ud->to_x = bl.x;
+		ud->to_y = bl.y;
+
+		if (tbl != nullptr && bl.type == BL_MOB) {
+			mob_data& md = reinterpret_cast<mob_data&>(bl);
+			if (mob_warpchase(&md, tbl))
+				return true;
+			// Make sure monsters properly unlock their target, but still continue movement
+			mob_unlocktarget(&md, tick);
+			return false;
+		}
+
+		ud->target_to = 0;
+		return true;
+	}
+	// Update chase path
+	else if (fullcheck && ud->walkpath.path_pos > 0) {
+		unit_walktobl(&bl, tbl, ud->chaserange, ud->state.walk_easy | (ud->state.attack_continue ? 2 : 0));
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Handles everything that happens when movement to the next cell is initiated
+ * @param bl: Moving bl
+ * @param sendMove: Whether move packet should be sent or not
+ * @param tick: Current tick
+ * @return Whether movement was initialized (true) or not (false)
+ */
+bool unit_walktoxy_nextcell(block_list& bl, bool sendMove, t_tick tick) {
+	unit_data* ud = unit_bl2ud(&bl);
+
+	if (ud == nullptr)
+		return true;
+
+	int32 speed;
+
+	// Reached end of walkpath
+	if (ud->walkpath.path_pos >= ud->walkpath.path_len)
+		return false;
+
+	if (direction_diagonal(ud->walkpath.path[ud->walkpath.path_pos]))
+		speed = status_get_speed(&bl) * MOVE_DIAGONAL_COST / MOVE_COST;
+	else
+		speed = status_get_speed(&bl);
+
+	// Monsters check if their target is in range each cell
+	if (bl.type == BL_MOB && ud->target_to != 0) {
+		int16 tx = ud->to_x;
+		int16 ty = ud->to_y;
+		// Monsters update their chase path one cell before reaching their final destination
+		if (unit_update_chase(bl, tick, (ud->walkpath.path_pos == ud->walkpath.path_len - 1)))
+			return true;
+		// Continue moving, restore to_x and to_y
+		ud->to_x = tx;
+		ud->to_y = ty;
+	}
+
+	// Make sure there is no active walktimer
+	if (ud->walktimer != INVALID_TIMER) {
+		delete_timer(ud->walktimer, unit_walktoxy_timer);
+		ud->walktimer = INVALID_TIMER;
+	}
+	ud->walktimer = add_timer(tick + speed, unit_walktoxy_timer, bl.id, speed);
+	if (sendMove)
+		clif_move(*ud);
+	return true;
+}
+
 /**
  * Tells a unit to walk to a specific coordinate
  * @param bl: Unit to walk [ALL]
@@ -108,7 +213,8 @@ int32 unit_walktoxy_sub(struct block_list *bl)
 
 	int32 i;
 
-	if (ud->target_to && ud->chaserange>1) {
+	// Monsters always target an adjacent tile even if ranged, no need to shorten the path
+	if (ud->target_to != 0 && ud->chaserange > 1 && bl->type != BL_MOB) {
 		// Generally speaking, the walk path is already to an adjacent tile
 		// so we only need to shorten the path if the range is greater than 1.
 		// Trim the last part of the path to account for range,
@@ -116,7 +222,7 @@ int32 unit_walktoxy_sub(struct block_list *bl)
 		for (i = (ud->chaserange*10)-10; i > 0 && ud->walkpath.path_len>1;) {
 			ud->walkpath.path_len--;
 			enum directions dir = ud->walkpath.path[ud->walkpath.path_len];
-			if (direction_diagonal(dir) && bl->type != BL_MOB)
+			if (direction_diagonal(dir))
 				i -= MOVE_COST * 2; //When chasing, units will target a diamond-shaped area in range [Playtester]
 			else
 				i -= MOVE_COST;
@@ -140,21 +246,7 @@ int32 unit_walktoxy_sub(struct block_list *bl)
 		unit_refresh( bl, true );
 	}
 #endif
-	clif_move( *ud );
-
-	if(ud->walkpath.path_pos>=ud->walkpath.path_len)
-		i = -1;
-	else if( direction_diagonal( ud->walkpath.path[ud->walkpath.path_pos] ) )
-		i = status_get_speed(bl)*MOVE_DIAGONAL_COST/MOVE_COST;
-	else
-		i = status_get_speed(bl);
-	if( i > 0 ){
-		if( ud->walktimer != INVALID_TIMER ){
-			delete_timer( ud->walktimer, unit_walktoxy_timer );
-			ud->walktimer = INVALID_TIMER;
-		}
-		ud->walktimer = add_timer(gettick()+i,unit_walktoxy_timer,bl->id,i);
-	}
+	unit_walktoxy_nextcell(*bl, true, gettick());
 
 	return 1;
 }
@@ -320,7 +412,7 @@ TIMER_FUNC(unit_step_timer){
 	} else {
 		//If a player has target_id set and target is in range, attempt attack
 		struct block_list *tbl = map_id2bl(target_id);
-		if (!tbl || !status_check_visibility(bl, tbl)) {
+		if (tbl == nullptr || !status_check_visibility(bl, tbl, false)) {
 			return 0;
 		}
 		if(ud->stepskill_id == 0) {
@@ -540,8 +632,6 @@ static TIMER_FUNC(unit_walktoxy_timer)
 					return 0; // Warped
 			} else
 				md->areanpc_id = 0;
-			if (md->min_chase > md->db->range3)
-				md->min_chase--;
 			// Walk skills are triggered regardless of target due to the idle-walk mob state.
 			// But avoid triggering on stop-walk calls.
 			// Monsters use walk/chase skills every second, but we only get here every "speed" ms
@@ -614,53 +704,14 @@ static TIMER_FUNC(unit_walktoxy_timer)
 
 	ud->walkpath.path_pos++;
 
-	if(ud->walkpath.path_pos >= ud->walkpath.path_len)
-		speed = -1;
-	else if( direction_diagonal( ud->walkpath.path[ud->walkpath.path_pos] ) )
-		speed = status_get_speed(bl)*MOVE_DIAGONAL_COST/MOVE_COST;
-	else
-		speed = status_get_speed(bl);
-
-	if(speed > 0) {
-		// For some reason sometimes the walk timer is not empty here
-		// TODO: Need to check why (e.g. when the monster spams NPC_RUN)
-		if (ud->walktimer != INVALID_TIMER) {
-			delete_timer(ud->walktimer, unit_walktoxy_timer);
-			ud->walktimer = INVALID_TIMER;
-		}
-		ud->walktimer = add_timer(tick+speed,unit_walktoxy_timer,id,speed);
-		if( md && DIFF_TICK(tick,md->dmgtick) < 3000 ) // Not required not damaged recently
-			clif_move( *ud );
+	if(unit_walktoxy_nextcell(*bl, (md != nullptr && DIFF_TICK(tick, md->dmgtick) < 3000), tick)) {
+		// Nothing else needs to be done
 	} else if(ud->state.running) { // Keep trying to run.
 		if (!(unit_run(bl, nullptr, SC_RUN) || unit_run(bl, sd, SC_WUGDASH)) )
 			ud->state.running = 0;
 	} 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.
-			ud->to_x = bl->x;
-			ud->to_y = bl->y;
-
-			if (tbl && bl->type == BL_MOB && mob_warpchase((TBL_MOB*)bl, tbl) )
-				return 0;
-
-			ud->target_to = 0;
-
-			return 0;
-		}
-		if (tbl->m == bl->m && check_distance_bl(bl, tbl, ud->chaserange)) { // Reached destination.
-			if (ud->state.attack_continue) {
-				// Aegis uses one before every attack, we should
-				// only need this one for syncing purposes. [Skotlex]
-				ud->target_to = 0;
-				clif_fixpos( *bl );
-				unit_attack(bl, tbl->id, ud->state.attack_continue);
-			}
-		} else { // Update chase-path
-			unit_walktobl(bl, tbl, ud->chaserange, ud->state.walk_easy|(ud->state.attack_continue?2:0));
-
-			return 0;
-		}
+		unit_update_chase(*bl, tick, true);
 	} else { // Stopped walking. Update to_x and to_y to current location [Skotlex]
 		ud->to_x = bl->x;
 		ud->to_y = bl->y;
@@ -840,7 +891,7 @@ static TIMER_FUNC(unit_walktobl_sub){
 	struct block_list *bl = map_id2bl(id);
 	struct unit_data *ud = bl?unit_bl2ud(bl):nullptr;
 
-	if (ud && ud->walktimer == INVALID_TIMER && ud->target && ud->target == data) {
+	if (ud != nullptr && ud->walktimer == INVALID_TIMER && ud->target_to != 0 && ud->target_to == data) {
 		if (DIFF_TICK(ud->canmove_tick, tick) > 0) // Keep waiting?
 			add_timer(ud->canmove_tick+1, unit_walktobl_sub, id, data);
 		else if (unit_can_move(bl)) {
@@ -908,7 +959,7 @@ int32 unit_walktobl(struct block_list *bl, struct block_list *tbl, int32 range,
 	}
 
 	if(DIFF_TICK(ud->canmove_tick, gettick()) > 0) { // Can't move, wait a bit before invoking the movement.
-		add_timer(ud->canmove_tick+1, unit_walktobl_sub, bl->id, ud->target);
+		add_timer(ud->canmove_tick+1, unit_walktobl_sub, bl->id, ud->target_to);
 		return 1;
 	}
 
@@ -1482,12 +1533,7 @@ void unit_stop_walking_soon(struct block_list& bl)
 /**
  * Stops a unit from walking
  * @param bl: Object to stop walking
- * @param type: Options
- *	USW_FIXPOS: Issue a fixpos packet afterwards
- *	USW_MOVE_ONCE: Force the unit to move one cell if it hasn't yet
- *	USW_MOVE_FULL_CELL: Enable moving to the next cell when unit was already half-way there
- *		(may cause on-touch/place side-effects, such as a scripted map change)
- *	USW_FORCE_STOP: Force stop moving, even if walktimer is currently INVALID_TIMER
+ * @param type: Options, see e_unit_stop_walking
  * @return Success(1); Failed(0);
  */
 int32 unit_stop_walking(struct block_list *bl,int32 type)
@@ -1533,6 +1579,9 @@ int32 unit_stop_walking(struct block_list *bl,int32 type)
 	ud->to_x = bl->x;
 	ud->to_y = bl->y;
 
+	if (type&USW_RELEASE_TARGET)
+		ud->target_to = 0;
+
 	if(bl->type == BL_PET && type&~USW_ALL)
 		ud->canmove_tick = gettick() + (type>>8);
 
@@ -1702,8 +1751,8 @@ int32 unit_set_walkdelay(struct block_list *bl, t_tick tick, t_tick delay, int32
 			else {
 				unit_stop_walking(bl,4);
 
-				if(ud->target)
-					add_timer(ud->canmove_tick+1, unit_walktobl_sub, bl->id, ud->target);
+				if(ud->target_to != 0)
+					add_timer(ud->canmove_tick+1, unit_walktobl_sub, bl->id, ud->target_to);
 			}
 		}
 	}
@@ -2140,7 +2189,6 @@ int32 unit_skilluse_id2(struct block_list *src, int32 target_id, uint16 skill_id
 
 					md->target_id = src->id;
 					md->state.aggressive = status_has_mode(tstatus,MD_ANGRY)?1:0;
-					md->min_chase = md->db->range3;
 					break;
 				case MSS_IDLE:
 				case MSS_WALK:
@@ -2149,7 +2197,6 @@ int32 unit_skilluse_id2(struct block_list *src, int32 target_id, uint16 skill_id
 
 					md->target_id = src->id;
 					md->state.aggressive = status_has_mode(tstatus,MD_ANGRY)?1:0;
-					md->min_chase = md->db->range3;
 					break;
 			}
 		}
@@ -2972,8 +3019,25 @@ static TIMER_FUNC(unit_attack_timer){
 
 	bl = map_id2bl(id);
 
-	if(bl && unit_attack_timer_sub(bl, tid, tick) == 0)
-		unit_unattackable(bl);
+	if (bl != nullptr) {
+		// Monsters have a special visibility check at the end of their attack delay
+		// We don't want this to trigger on direct calls of this function
+		if (bl->type == BL_MOB && tid != INVALID_TIMER) {
+			unit_data* ud = unit_bl2ud(bl);
+			if (ud == nullptr)
+				return 0;
+			block_list* tbl = map_id2bl(ud->target);
+			if (tbl == nullptr)
+				return 0;
+			if (!status_check_visibility(bl, tbl, true)) {
+				unit_unattackable(bl);
+				return 0;
+			}
+		}
+		// Execute attack
+		if (unit_attack_timer_sub(bl, tid, tick) == 0)
+			unit_unattackable(bl);
+	}
 
 	return 0;
 }
@@ -3193,7 +3257,7 @@ int32 unit_remove_map_(struct block_list *bl, clr_type clrtype, const char* file
 	map_freeblock_lock();
 
 	if (ud->walktimer != INVALID_TIMER)
-		unit_stop_walking(bl,0);
+		unit_stop_walking(bl,USW_RELEASE_TARGET);
 
 	if (clrtype == CLR_DEAD)
 		ud->state.blockedmove = true;

+ 2 - 1
src/map/unit.hpp

@@ -101,7 +101,8 @@ enum e_unit_stop_walking {
 	USW_MOVE_ONCE = 0x2, /// Force the unit to move one cell if it hasn't yet
 	USW_MOVE_FULL_CELL = 0x4, /// Enable moving to the next cell when unit was already half-way there (may cause on-touch/place side-effects, such as a scripted map change)
 	USW_FORCE_STOP = 0x8, /// Force stop moving, even if walktimer is currently INVALID_TIMER
-	USW_ALL = 0xf,
+	USW_RELEASE_TARGET = 0x10, /// Release chase target
+	USW_ALL = 0x1f,
 };
 
 // PC, MOB, PET