Skip to content

Commit

Permalink
NPC_RUN, Monster Skill Interval (rathena#8302)
Browse files Browse the repository at this point in the history
- Monsters will now properly run away when using NPC_RUN (fixes rathena#7941)
- Introduced a new function unit_get_walkpath_time that returns the time the unit needs to walk its current walkpath
  * This is now used for NPC_RUN and random walking
- Fixed an issue with mismatching timer warnings when a monster casts NPC_RUN multiple times in row

- Monsters will now always attempt to use non-berserk-state skills once per second (fixes rathena#1700)
  * This completely replaces the "ugly" solution to use a walk_count for idle, walk and chase skills
  * This interval is now a lot more accurate and no longer influenced by external factors such as canact delay
  * This interval is now also used for lazy monsters rather than MIN_MOBTHINKTIME*10 so that MIN_MOBTHINKTIME can be reduced without having to worry about skills being cast more often
  * Angry skills no longer replace the normal attack and now follow the once per second rule; they will always first be attempted at the end of the walk delay after a normal attack
- The special follow-up attack skill monsters use when you move out of their attack range, now is only used when they are in Angry state
  * Also fixed a bug that this was checked every 100ms until the monster used a skill, instead of just once per second

- Monsters now can use chase skills even before they start moving (assuming one second has already passed since last skill check)
- Removed "hack" to make monsters cast chase skills when trapped in icewall
  * This was solved by implementing checking for chase skills before starting to move

- Monsters will now receive an aMotion walk delay after having used a skill
- A monster that could not walk randomly because of walk delay will now walk immediately once the walk delay expires
- Using angry or berserk skills will now set a monster's attack delay
  • Loading branch information
Playtester authored May 8, 2024
1 parent bcb3469 commit ffe40de
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 59 deletions.
4 changes: 2 additions & 2 deletions conf/battle/skill.conf
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ min_skill_delay_limit: 100

// This delay is the min 'can't walk delay' of all skills.
// NOTE: Do not set this too low, if a character starts moving too soon after
// doing a skill, the client will not update this, and the player/mob will
// appear to "teleport" afterwards.
// doing a skill, the client will not update this, and the player will appear
// to "teleport" afterwards. Monsters use AttackMotion instead.
default_walk_delay: 300

// Completely disable skill delay of the following types (Note 3)
Expand Down
82 changes: 48 additions & 34 deletions src/map/mob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ using namespace rathena;

#define ACTIVE_AI_RANGE 2 //Distance added on top of 'AREA_SIZE' at which mobs enter active AI mode.

#define IDLE_SKILL_INTERVAL 10 //Active idle skills should be triggered every 1 second (1000/MIN_MOBTHINKTIME)

const t_tick MOB_MAX_DELAY = 24 * 3600 * 1000;
#define RUDE_ATTACKED_COUNT 1 //After how many rude-attacks should the skill be used?

Expand Down Expand Up @@ -1165,6 +1163,7 @@ int mob_spawn (struct mob_data *md)
md->dmgtick = tick - 5000;
md->last_pcneartime = 0;
md->last_canmove = tick;
md->last_skillcheck = 0;

t_tick c = tick - MOB_MAX_DELAY;

Expand Down Expand Up @@ -1522,14 +1521,17 @@ int mob_unlocktarget(struct mob_data *md, t_tick tick)
break;
}
// Idle skill.
if (!(++md->ud.walk_count%IDLE_SKILL_INTERVAL) && mobskill_use(md, tick, -1))
if (DIFF_TICK(tick, md->last_skillcheck) >= MOB_SKILL_INTERVAL && mobskill_use(md, tick, -1))
break;
//Random walk.
if (!md->master_id &&
DIFF_TICK(md->next_walktime, tick) <= 0 &&
!mob_randomwalk(md,tick))
//Delay next random walk when this one failed.
md->next_walktime = tick+rnd()%1000;
if (md->next_walktime < md->ud.canmove_tick)
md->next_walktime = md->ud.canmove_tick;
else
md->next_walktime = tick+rnd()%1000;
break;
default:
mob_stop_attack(md);
Expand Down Expand Up @@ -1563,8 +1565,7 @@ int mob_unlocktarget(struct mob_data *md, t_tick tick)
int mob_randomwalk(struct mob_data *md,t_tick tick)
{
const int d=7;
int i,c,r,rdir,dx,dy,max;
int speed;
int i,r,rdir,dx,dy,max;

nullpo_ret(md);

Expand Down Expand Up @@ -1648,16 +1649,9 @@ int mob_randomwalk(struct mob_data *md,t_tick tick)
}
return 0;
}
speed=status_get_speed(&md->bl);
for(i=c=0;i<md->ud.walkpath.path_len;i++){ // The next walk start time is calculated.
if( direction_diagonal( md->ud.walkpath.path[i] ) )
c+=speed*MOVE_DIAGONAL_COST/MOVE_COST;
else
c+=speed;
}
md->state.skillstate=MSS_WALK;
md->move_fail_count=0;
md->next_walktime = tick+rnd()%1000+MIN_RANDOMWALKTIME+c;
md->next_walktime = tick+rnd()%1000+MIN_RANDOMWALKTIME + unit_get_walkpath_time(md->bl);
return 1;
}

Expand Down Expand Up @@ -1917,8 +1911,23 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
return true;
}

//Attempt to attack.
//At this point we know the target is attackable, we just gotta check if the range matches.
// At this point we know the target is attackable, attempt to attack

// Monsters in angry state, after having used a normal attack, will always attempt a skill
if (md->ud.walktimer == INVALID_TIMER && md->state.skillstate == MSS_ANGRY && md->ud.skill_id == 0)
{
// Only use skill if able to walk on next tick and not attempted a skill the last second
if (DIFF_TICK(md->ud.canmove_tick, tick) <= MIN_MOBTHINKTIME && DIFF_TICK(tick, md->last_skillcheck) >= MOB_SKILL_INTERVAL){
if (mobskill_use(md, tick, -1)) {
// After the monster used an angry skill, it will not attack for aDelay
// Setting the delay here because not all monster skill use situations will cause an attack delay
md->ud.attackabletime = tick + md->status.adelay;
return true;
}
}
}

// Normal attack / berserk skill is only used when target is in range
if (battle_check_range(&md->bl, tbl, md->status.rhw.range) && !(md->sc.option&OPTION_HIDE))
{ //Target within range and able to use normal attack, engage
if (md->ud.target != tbl->id || md->ud.attacktimer == INVALID_TIMER)
Expand All @@ -1940,16 +1949,6 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
return true;
}

//Monsters in berserk state, unable to use normal attacks, will always attempt a skill
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
if (mobskill_use(md, tick, -1))
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;
Expand All @@ -1971,7 +1970,7 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick tick)
else {
// Use idle skill but keep target for now
md->state.skillstate = MSS_IDLE;
if (!(++md->ud.walk_count%IDLE_SKILL_INTERVAL))
if (DIFF_TICK(tick, md->last_skillcheck) >= MOB_SKILL_INTERVAL)
mobskill_use(md, tick, -1);
}
}
Expand All @@ -1986,10 +1985,24 @@ static bool mob_ai_sub_hard(struct mob_data *md, t_tick 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) ||
!unit_walktobl(&md->bl, tbl, md->status.rhw.range, 2))
mob_unlocktarget(md,tick);
if (!mob_can_reach(md, tbl, md->min_chase)) {
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
// Skills during movement are handled in the walktobl routine
if (md->ud.walktimer == INVALID_TIMER
&& DIFF_TICK(md->ud.canmove_tick, tick) <= MIN_MOBTHINKTIME
&& DIFF_TICK(tick, md->last_skillcheck) >= MOB_SKILL_INTERVAL) {
md->state.skillstate = md->state.aggressive ? MSS_FOLLOW : MSS_RUSH;
if (mobskill_use(md, tick, -1))
return true;
}

if(!unit_walktobl(&md->bl, tbl, md->status.rhw.range, 2))
mob_unlocktarget(md, tick);

return true;
}
Expand Down Expand Up @@ -2063,7 +2076,7 @@ static int mob_ai_sub_lazy(struct mob_data *md, va_list args)
//Clean the spotted log
mob_clean_spotted(md);

if(DIFF_TICK(tick,md->last_thinktime)< 10*MIN_MOBTHINKTIME)
if(DIFF_TICK(tick,md->last_thinktime) < MOB_SKILL_INTERVAL)
return 0;

md->last_thinktime=tick;
Expand Down Expand Up @@ -3752,8 +3765,9 @@ int mobskill_use(struct mob_data *md, t_tick tick, int event, int64 damage)
if (!battle_config.mob_skill_rate || md->ud.skilltimer != INVALID_TIMER || ms.empty() || status_has_mode(&md->status,MD_NOCAST))
return 0;

if (event == -1 && DIFF_TICK(md->ud.canact_tick, tick) > 0)
return 0; //Skill act delay only affects non-event skills.
// Monsters check their non-attack-state skills once per second, but we ignore this for events for now
if (event == -1)
md->last_skillcheck = tick;

//Pick a starting position and loop from that.
i = battle_config.mob_ai&0x100?rnd()%ms.size():0;
Expand Down
5 changes: 4 additions & 1 deletion src/map/mob.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const t_tick MIN_MOBLINKTIME = 1000;
//Min time between random walks
const t_tick MIN_RANDOMWALKTIME = 4000;

// How often a monster will check for using a skill on non-attack states (in ms)
const t_tick MOB_SKILL_INTERVAL = 1000;

//Distance that slaves should keep from their master.
#define MOB_SLAVEDISTANCE 2

Expand Down Expand Up @@ -359,7 +362,7 @@ struct mob_data {
int areanpc_id; //Required in OnTouchNPC (to avoid multiple area touchs)
int bg_id; // BattleGround System

t_tick next_walktime,last_thinktime,last_linktime,last_pcneartime,dmgtick,last_canmove;
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;
Expand Down
27 changes: 23 additions & 4 deletions src/map/skill.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9938,7 +9938,15 @@ int skill_castend_nodamage_id (struct block_list *src, struct block_list *bl, ui
if (tbl) {
md->state.can_escape = 1;
mob_unlocktarget(md, tick);
unit_escape(src, tbl, skill_lv > 1 ? skill_lv : AREA_SIZE, 2); // Send distance in skill level > 1
// Official distance is 7, if level > 1, distance = level
t_tick time = unit_escape(src, tbl, skill_lv > 1 ? skill_lv : 7, 2);

if (time) {
// Need to set state here as it's not set otherwise
md->state.skillstate = MSS_WALK;
// Set AI to inactive for the duration of this movement
md->last_thinktime = tick + time;
}
}
}
break;
Expand Down Expand Up @@ -13421,8 +13429,14 @@ TIMER_FUNC(skill_castend_id){
}
}
}
if (skill_get_state(ud->skill_id) != ST_MOVE_ENABLE)
unit_set_walkdelay(src, tick, battle_config.default_walk_delay+skill_get_walkdelay(ud->skill_id, ud->skill_lv), 1);
if (skill_get_state(ud->skill_id) != ST_MOVE_ENABLE) {
// When monsters used a skill they won't walk for amotion, this does not apply to players
// This is also important for monster skill usage behavior
if (src->type == BL_MOB)
unit_set_walkdelay(src, tick, max((int)status_get_amotion(src), skill_get_walkdelay(ud->skill_id, ud->skill_lv)), 1);
else
unit_set_walkdelay(src, tick, battle_config.default_walk_delay + skill_get_walkdelay(ud->skill_id, ud->skill_lv), 1);
}

if(battle_config.skill_log && battle_config.skill_log&src->type)
ShowInfo("Type %d, ID %d skill castend id [id =%d, lv=%d, target ID %d]\n",
Expand Down Expand Up @@ -13636,7 +13650,12 @@ TIMER_FUNC(skill_castend_pos){
// break;
// }
// }
unit_set_walkdelay(src, tick, battle_config.default_walk_delay+skill_get_walkdelay(ud->skill_id, ud->skill_lv), 1);
// When monsters used a skill they won't walk for amotion, this does not apply to players
// This is also important for monster skill usage behavior
if (src->type == BL_MOB)
unit_set_walkdelay(src, tick, max((int)status_get_amotion(src), skill_get_walkdelay(ud->skill_id, ud->skill_lv)), 1);
else
unit_set_walkdelay(src, tick, battle_config.default_walk_delay + skill_get_walkdelay(ud->skill_id, ud->skill_lv), 1);
map_freeblock_lock();
skill_castend_pos2(src,ud->skillx,ud->skilly,ud->skill_id,ud->skill_lv,tick,0);

Expand Down
4 changes: 0 additions & 4 deletions src/map/skill.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,6 @@ enum e_skill_unit_flag : uint8 {
UF_MAX,
};

/// Walk intervals at which chase-skills are attempted to be triggered.
/// If you change this, make sure it's an odd value (for icewall block behavior).
#define WALK_SKILL_INTERVAL 5

/// Time that's added to canact delay on castbegin and substracted on castend
/// This is to prevent hackers from sending a skill packet after cast but before a timer triggers castend
const t_tick SECURITY_CASTTIME = 100;
Expand Down
61 changes: 49 additions & 12 deletions src/map/unit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -448,11 +448,10 @@ static TIMER_FUNC(unit_walktoxy_timer)
//Needs to be done here so that rudeattack skills are invoked
md->walktoxy_fail_count++;
clif_fixpos(bl);
//Monsters in this situation first use a chase skill, then unlock target and then use an idle skill
if (!(++ud->walk_count%WALK_SKILL_INTERVAL))
mobskill_use(md, tick, -1);
// Monsters in this situation will unlock target and then attempt an idle skill
// When they start chasing again, they will check for a chase skill before returning here
mob_unlocktarget(md, tick);
if (!(++ud->walk_count%WALK_SKILL_INTERVAL))
if (DIFF_TICK(tick, md->last_skillcheck) >= MOB_SKILL_INTERVAL)
mobskill_use(md, tick, -1);
return 0;
}
Expand All @@ -463,7 +462,6 @@ static TIMER_FUNC(unit_walktoxy_timer)
x += dx;
y += dy;
map_moveblock(bl, x, y, tick);
ud->walk_count++; // Walked cell counter, to be used for walk-triggered skills. [Skotlex]

if (bl->x != x || bl->y != y || ud->walktimer != INVALID_TIMER)
return 0; // map_moveblock has altered the object beyond what we expected (moved/warped it)
Expand Down Expand Up @@ -542,8 +540,11 @@ static TIMER_FUNC(unit_walktoxy_timer)
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
// To make sure we check one skill per second on average, we substract half the speed as ms
if(!ud->state.force_walk && tid != INVALID_TIMER &&
!(ud->walk_count%WALK_SKILL_INTERVAL) &&
DIFF_TICK(tick, md->last_skillcheck) > MOB_SKILL_INTERVAL - md->status.speed / 2 &&
DIFF_TICK(tick, md->last_thinktime) > 0 &&
map[bl->m].users > 0 &&
mobskill_use(md, tick, -1)) {
if (!(ud->skill_id == NPC_SELFDESTRUCTION && ud->skilltimer != INVALID_TIMER)
Expand Down Expand Up @@ -616,6 +617,12 @@ static TIMER_FUNC(unit_walktoxy_timer)
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);
Expand Down Expand Up @@ -1004,22 +1011,47 @@ bool unit_run(struct block_list *bl, map_session_data *sd, enum sc_type type)
return true;
}

/**
* Returns duration of an object's current walkpath
* @param bl: Object that is moving
* @return Duration of the walkpath
*/
t_tick unit_get_walkpath_time(struct block_list& bl)
{
t_tick time = 0;
unsigned short speed = status_get_speed(&bl);
struct unit_data* ud = unit_bl2ud(&bl);

// The next walk start time is calculated.
for (uint8 i = 0; i < ud->walkpath.path_len; i++) {
if (direction_diagonal(ud->walkpath.path[i]))
time += speed * MOVE_DIAGONAL_COST / MOVE_COST;
else
time += speed;
}

return time;
}

/**
* Makes unit attempt to run away from target using hard paths
* @param bl: Object that is running away from target
* @param target: Target
* @param dist: How far bl should run
* @param flag: unit_walktoxy flag
* @return 1: Success 0: Fail
* @return The duration the unit will run (0 on fail)
*/
int unit_escape(struct block_list *bl, struct block_list *target, short dist, uint8 flag)
t_tick unit_escape(struct block_list *bl, struct block_list *target, short dist, uint8 flag)
{
uint8 dir = map_calc_dir(target, bl->x, bl->y);

while( dist > 0 && map_getcell(bl->m, bl->x + dist*dirx[dir], bl->y + dist*diry[dir], CELL_CHKNOREACH) )
dist--;

return ( dist > 0 && unit_walktoxy(bl, bl->x + dist*dirx[dir], bl->y + dist*diry[dir], flag) );
if (dist > 0 && unit_walktoxy(bl, bl->x + dist * dirx[dir], bl->y + dist * diry[dir], flag))
return unit_get_walkpath_time(*bl);

return 0;
}

/**
Expand Down Expand Up @@ -2784,10 +2816,14 @@ static int unit_attack_timer_sub(struct block_list* src, int tid, t_tick tick)
unit_stop_walking(src,1);

if(md) {
//First attack is always a normal attack
if(md->state.skillstate == MSS_ANGRY || md->state.skillstate == MSS_BERSERK) {
if (mobskill_use(md,tick,-1))
// Berserk skills can replace normal attacks except for the first attack
// If this is the first attack, the state is not Berserk yet, so the skill check is skipped
if(md->state.skillstate == MSS_BERSERK) {
if (mobskill_use(md, tick, -1)) {
// Setting the delay here because not all monster skill use situations will cause an attack delay
ud->attackabletime = tick + sstatus->adelay;
return 1;
}
}
// Set mob's ANGRY/BERSERK states.
md->state.skillstate = md->state.aggressive?MSS_ANGRY:MSS_BERSERK;
Expand Down Expand Up @@ -2818,6 +2854,7 @@ static int unit_attack_timer_sub(struct block_list* src, int tid, t_tick tick)
return 1;

ud->attackabletime = tick + sstatus->adelay;
ud->skill_id = 0;

// You can't move if you can't attack neither.
if (src->type&battle_config.attack_walk_delay)
Expand Down
4 changes: 2 additions & 2 deletions src/map/unit.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ struct unit_data {
t_tick canmove_tick;
bool immune_attack; ///< Whether the unit is immune to attacks
uint8 dir;
unsigned char walk_count;
unsigned char target_count;
struct s_udState {
unsigned change_walk_target : 1 ;
Expand Down Expand Up @@ -122,7 +121,8 @@ bool unit_can_move(struct block_list *bl);
int unit_is_walking(struct block_list *bl);
int unit_set_walkdelay(struct block_list *bl, t_tick tick, t_tick delay, int type);

int unit_escape(struct block_list *bl, struct block_list *target, short dist, uint8 flag = 0);
t_tick unit_get_walkpath_time(struct block_list& bl);
t_tick unit_escape(struct block_list *bl, struct block_list *target, short dist, uint8 flag = 0);

// Instant unit changes
bool unit_movepos(struct block_list *bl, short dst_x, short dst_y, int easy, bool checkpath);
Expand Down

0 comments on commit ffe40de

Please sign in to comment.