From ffe40def4ab753b428f4425cdd946a56c7ae947a Mon Sep 17 00:00:00 2001 From: Playtester <3785983+Playtester@users.noreply.github.com> Date: Wed, 8 May 2024 09:02:48 +0200 Subject: [PATCH] NPC_RUN, Monster Skill Interval (#8302) - Monsters will now properly run away when using NPC_RUN (fixes #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 #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 --- conf/battle/skill.conf | 4 +-- src/map/mob.cpp | 82 ++++++++++++++++++++++++------------------ src/map/mob.hpp | 5 ++- src/map/skill.cpp | 27 +++++++++++--- src/map/skill.hpp | 4 --- src/map/unit.cpp | 61 ++++++++++++++++++++++++------- src/map/unit.hpp | 4 +-- 7 files changed, 128 insertions(+), 59 deletions(-) diff --git a/conf/battle/skill.conf b/conf/battle/skill.conf index ff43256c270..5e2f3eb842f 100644 --- a/conf/battle/skill.conf +++ b/conf/battle/skill.conf @@ -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) diff --git a/src/map/mob.cpp b/src/map/mob.cpp index 5692a206076..510fa28331f 100644 --- a/src/map/mob.cpp +++ b/src/map/mob.cpp @@ -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? @@ -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; @@ -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); @@ -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); @@ -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;iud.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; } @@ -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) @@ -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; @@ -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); } } @@ -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; } @@ -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; @@ -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; diff --git a/src/map/mob.hpp b/src/map/mob.hpp index f2d1fa72417..a89e45a79b7 100644 --- a/src/map/mob.hpp +++ b/src/map/mob.hpp @@ -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 @@ -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; diff --git a/src/map/skill.cpp b/src/map/skill.cpp index a5227a2afcc..c8b9c1d7cb4 100755 --- a/src/map/skill.cpp +++ b/src/map/skill.cpp @@ -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; @@ -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", @@ -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); diff --git a/src/map/skill.hpp b/src/map/skill.hpp index cead3935170..424a049cf01 100644 --- a/src/map/skill.hpp +++ b/src/map/skill.hpp @@ -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; diff --git a/src/map/unit.cpp b/src/map/unit.cpp index 16d202c2abb..36c4368fdd1 100644 --- a/src/map/unit.cpp +++ b/src/map/unit.cpp @@ -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; } @@ -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) @@ -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) @@ -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); @@ -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; } /** @@ -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; @@ -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) diff --git a/src/map/unit.hpp b/src/map/unit.hpp index cfd9326156f..dd991e2eb81 100644 --- a/src/map/unit.hpp +++ b/src/map/unit.hpp @@ -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 ; @@ -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);