diff --git a/NetRedirect.dll b/NetRedirect.dll index 4139fedfab..59209c6fd7 100644 Binary files a/NetRedirect.dll and b/NetRedirect.dll differ diff --git a/XSTools.dll b/XSTools.dll index 1052402ba7..fd7c152036 100644 Binary files a/XSTools.dll and b/XSTools.dll differ diff --git a/control/config.txt b/control/config.txt index d75b469fd9..67f5508c65 100644 --- a/control/config.txt +++ b/control/config.txt @@ -72,9 +72,8 @@ attackAuto_notWhile_storageAuto 1 attackAuto_notWhile_buyAuto 1 attackAuto_notWhile_sellAuto 1 attackAuto_considerDamagedAggressive 0 -attackBeyondMaxDistance_waitForAgressive 1 attackDistance 1 -attackDistanceAuto 0 +attackDistanceAuto 1 attackMaxDistance 1 attackMaxRouteDistance 50 attackMaxRouteTime 4 @@ -83,8 +82,8 @@ attackMinPortalDistance 4 attackUseWeapon 1 attackNoGiveup 0 attackCanSnipe 0 -attackCheckLOS 0 -attackRouteMaxPathDistance 13 +attackCheckLOS 1 +attackRouteMaxPathDistance 20 attackLooters 0 attackLooters_dist 1 attackChangeTarget 1 @@ -92,6 +91,11 @@ aggressiveAntiKS 0 attackUpdateMonsterPos 1 +attackBeyondMaxDistance_waitForAgressive 1 +attackBeyondMaxDistance_sendAttackWhileWaiting 1 +attackSendAttackWithMove 1 +attackWaitApproachFinish 1 + autoMoveOnDeath 0 autoMoveOnDeath_x autoMoveOnDeath_y @@ -225,12 +229,16 @@ route_removeMissingPortals_NPC 1 route_removeMissingPortals 0 route_tryToGuessMissingPortalByDistance 1 route_reAddMissingPortals 1 +route_randomFactor 0 runFromTarget 0 runFromTarget_inAdvance 0 runFromTarget_dist 5 runFromTarget_minStep 7 runFromTarget_maxPathDistance 13 +runFromTarget_noAttackMethodFallback 0 +runFromTarget_noAttackMethodFallback_attackMaxDist 14 +runFromTarget_noAttackMethodFallback_minStep 8 saveMap saveMap_warpToBuyOrSell 1 @@ -386,22 +394,27 @@ mercenary_attackAuto_notWhile_storageAuto 1 mercenary_attackAuto_notWhile_buyAuto 1 mercenary_attackAuto_notWhile_sellAuto 1 mercenary_attackAuto_considerDamagedAggressive 0 -mercenary_attackBeyondMaxDistance_waitForAgressive 1 mercenary_attackAuto_onlyWhenSafe 0 mercenary_attackAuto_duringRandomWalk 0 mercenary_attackAuto_duringItemsTake 0 mercenary_attackDistance 1 mercenary_attackMaxDistance 1 -mercenary_attackDistanceAuto 0 +mercenary_attackDistanceAuto 1 mercenary_attackMaxRouteTime 4 mercenary_attackCanSnipe 0 mercenary_attackCheckLOS 1 mercenary_attackRouteMaxPathDistance 20 +mercenary_attackUseWeapon 1 mercenary_attackNoGiveup 0 mercenary_attackChangeTarget 1 mercenary_attack_dance_melee 0 mercenary_attack_dance_ranged 0 +mercenary_attackBeyondMaxDistance_waitForAgressive 1 +mercenary_attackBeyondMaxDistance_sendAttackWhileWaiting 1 +mercenary_attackSendAttackWithMove 1 +mercenary_attackWaitApproachFinish 1 + mercenary_lost_teleportToMaster_maxTries 6 mercenary_route_randomWalk_rescueWhenLost 0 @@ -413,13 +426,16 @@ mercenary_runFromTarget_inAdvance 0 mercenary_runFromTarget_dist 5 mercenary_runFromTarget_minStep 7 mercenary_runFromTarget_maxPathDistance 20 +mercenary_runFromTarget_noAttackMethodFallback 0 +mercenary_runFromTarget_noAttackMethodFallback_attackMaxDist 14 +mercenary_runFromTarget_noAttackMethodFallback_minStep 8 mercenary_followDistanceMax 12 mercenary_followDistanceMin 3 mercenary_moveNearWhenIdle 1 -mercenary_moveNearWhenIdle_minDistance 2 -mercenary_moveNearWhenIdle_maxDistance 12 +mercenary_moveNearWhenIdle_minDistance 3 +mercenary_moveNearWhenIdle_maxDistance 8 mercenary_idleWalkType 1 @@ -449,21 +465,26 @@ homunculus_attackAuto_notWhile_storageAuto 1 homunculus_attackAuto_notWhile_buyAuto 1 homunculus_attackAuto_notWhile_sellAuto 1 homunculus_attackAuto_considerDamagedAggressive 0 -homunculus_attackBeyondMaxDistance_waitForAgressive 1 homunculus_attackAuto_onlyWhenSafe 0 homunculus_attackAuto_duringRandomWalk 0 homunculus_attackAuto_duringItemsTake 0 homunculus_attackDistance 1 homunculus_attackMaxDistance 1 -homunculus_attackDistanceAuto 0 +homunculus_attackDistanceAuto 1 homunculus_attackMaxRouteTime 4 homunculus_attackCanSnipe 0 homunculus_attackCheckLOS 1 homunculus_attackRouteMaxPathDistance 20 +homunculus_attackUseWeapon 1 homunculus_attackNoGiveup 0 homunculus_attackChangeTarget 1 homunculus_attack_dance_melee 0 +homunculus_attackBeyondMaxDistance_waitForAgressive 1 +homunculus_attackBeyondMaxDistance_sendAttackWhileWaiting 1 +homunculus_attackSendAttackWithMove 1 +homunculus_attackWaitApproachFinish 1 + homunculus_lost_teleportToMaster_maxTries 6 homunculus_route_randomWalk_rescueWhenLost 0 @@ -474,13 +495,16 @@ homunculus_runFromTarget 0 homunculus_runFromTarget_dist 5 homunculus_runFromTarget_minStep 7 homunculus_runFromTarget_maxPathDistance 20 +homunculus_runFromTarget_noAttackMethodFallback 0 +homunculus_runFromTarget_noAttackMethodFallback_attackMaxDist 14 +homunculus_runFromTarget_noAttackMethodFallback_minStep 8 homunculus_followDistanceMax 12 homunculus_followDistanceMin 3 homunculus_moveNearWhenIdle 1 -homunculus_moveNearWhenIdle_minDistance 2 -homunculus_moveNearWhenIdle_maxDistance 12 +homunculus_moveNearWhenIdle_minDistance 3 +homunculus_moveNearWhenIdle_maxDistance 8 homunculus_idleWalkType 1 diff --git a/control/timeouts.txt b/control/timeouts.txt index 928ca2a1d5..e1f84d131e 100644 --- a/control/timeouts.txt +++ b/control/timeouts.txt @@ -37,6 +37,8 @@ ai_attack 1 ai_homunculus_attack 1 ai_mercenary_attack 1 +ai_attack_after_skill 0.5 + ai_homunculus_dance_attack_melee 0.2 ai_mercenary_dance_attack_melee 0.2 @@ -67,6 +69,11 @@ ai_attack_waitAfterKill 0.3 ai_homunculus_attack_waitAfterKill 0.3 ai_mercenary_attack_waitAfterKill 0.3 +# Every x seconds loop the attack logic routine (send move, attack, skill, avoid, etc) +ai_attack_main 0.1 +ai_homunculus_attack_main 0.1 +ai_mercenary_attack_main 0.1 + ai_attack_unstuck 2.75 ai_attack_unfail 12 diff --git a/plugins/NewAStarAvoid/NewAStarAvoid.pl b/plugins/NewAStarAvoid/NewAStarAvoid.pl new file mode 100644 index 0000000000..8a615c323d --- /dev/null +++ b/plugins/NewAStarAvoid/NewAStarAvoid.pl @@ -0,0 +1,672 @@ +package NewAStarAvoid; + +use strict; +use Globals; +use Settings; +use Misc; +use Plugins; +use Utils; +use Log qw(message debug error warning); +use Data::Dumper; + +Plugins::register('NewAStarAvoid', 'Enables smart pathing using the dynamic aspect of D* Lite pathfinding', \&onUnload); + +use constant { + PLUGIN_NAME => 'NewAStarAvoid', + ENABLE_MOVE => 1, + ENABLE_REMOVE => 1, +}; + +use constant { + ENABLE_AVOID_MONSTERS => 1, + ENABLE_AVOID_PLAYERS => 0, + ENABLE_AVOID_AREASPELLS => 0, + ENABLE_AVOID_PORTALS => 0, +}; + +my $hooks = Plugins::addHooks( + ['PathFindingReset', \&on_PathFindingReset], # Changes args + ['AI_pre/manual', \&on_AI_pre_manual], # Recalls routing + ['packet_mapChange', \&on_packet_mapChange], +); + +my $obstacle_hooks = Plugins::addHooks( + # Mobs + ['add_monster_list', \&on_add_monster_list], + ['monster_disappeared', \&on_monster_disappeared], + ['monster_moved', \&on_monster_moved], + + # Players + ['add_player_list', \&on_add_player_list], + ['player_disappeared', \&on_player_disappeared], + ['player_moved', \&on_player_moved], + + # Spells + ['packet_areaSpell', \&on_add_areaSpell_list], + ['packet_pre/area_spell_disappears', \&on_areaSpell_disappeared], + + # portals + ['add_portal_list', \&on_add_portal_list], + ['portal_disappeared', \&on_portal_disappeared], +); + +my $mobhooks = Plugins::addHooks( + ['checkMonsterAutoAttack', \&on_checkMonsterAutoAttack], +); + +sub onUnload { + Plugins::delHooks($hooks); + Plugins::delHooks($obstacle_hooks); + Plugins::delHooks($mobhooks); +} + +my %mob_nameID_obstacles = ( + 1368 => { # planta carnivora + weight => 2000, + dist => 12, + drop_target_near => 0, + drop_dest_near => 0, + } +); + +my %player_name_obstacles = ( + +); + +my %area_spell_type_obstacles = ( + +); + +my %portals_obstacles = ( + weight => 1000, + dist => 10, +); + +my %obstaclesList; + +my %removed_obstacle_still_in_list; + +my $mustRePath = 0; + +my $weight_limit = 65000; + +my $teleport_soon = 0; +my $teleport_soon_timeout; + +sub on_packet_mapChange { + undef %obstaclesList; + $mustRePath = 0; +} + +sub on_checkMonsterAutoAttack { + my (undef, $args) = @_; + + my $realMonsterPos = calcPosition($args->{monster}); + my $obstacle = is_there_an_obstacle_near_pos($realMonsterPos, 1); + if (defined $obstacle) { + debug "[avoidObstacle 2] Not picking target ".$args->{monster}." because there is an Obstacle outside the screen nearby.\n"; + $args->{return} = 0; + return; + } +} + +# 1 => target +# 2 => dest +sub is_there_an_obstacle_near_pos { + my ($pos, $type) = @_; + foreach my $obstacle_ID (keys %obstaclesList) { + my $obstacle = $obstaclesList{$obstacle_ID}; + + if (($type == 1 && $obstacle->{drop_target_near} == 1) || ($type == 2 && $obstacle->{drop_dest_near} == 1)) { + my $obstacle_last_pos = $obstacle->{pos_to}; + + my $dist = blockDistance($pos, $obstacle_last_pos); + my $min_dist = 13;#TODO config this + next unless ($dist <= $min_dist); + + return 1; + } + } + return undef; +} + +sub on_AI_pre_manual_drop_target_near_Obstacle { + my @obstacles = keys(%obstaclesList); + return unless (@obstacles > 0); + if ( + (AI::action eq "attack" && AI::args->{ID}) + || (AI::action eq "route" && AI::action (1) eq "attack" && AI::args->{attackID}) + || (AI::action eq "move" && AI::action (2) eq "attack" && AI::args->{attackID}) + ) { + my $args = AI::args; + my $ID; + my $ataqArgs; + if (AI::action eq "attack") { + $ID = $args->{ID}; + $ataqArgs = AI::args(0); + } else { + if (AI::action(1) eq "attack") { + $ataqArgs = AI::args(1); + + } elsif (AI::action(2) eq "attack") { + $ataqArgs = AI::args(2); + } + $ID = $args->{attackID}; + } + + my $target = Actor::get($ID); + return unless ($target); + my $target_is_aggressive = is_aggressive($target, undef, 0, 0); + + my $realMonsterPos = calcPosition($target); + + my $obstacle = is_there_an_obstacle_near_pos($realMonsterPos, 1); + + if (defined $obstacle) { + #$char->sendAttackStop; + if ($target_is_aggressive) { + warning "[avoidObstacle 3] Dropping agressive target ".$target." during attack because an Obstacle appeared near it.\n"; + $teleport_soon = 1; + $teleport_soon_timeout->{time} = time; + $teleport_soon_timeout->{timeout} = 0.8; + + } else { + warning "[avoidObstacle 4] Dropping target ".$target." before attack because an Obstacle appeared near it.\n"; + AI::dequeue while (AI::inQueue("attack")); + } + } + } +} + +sub on_AI_pre_manual_teleport_soon { + return unless ($teleport_soon == 1); + return unless (main::timeOut($teleport_soon_timeout)); + $teleport_soon = 0; + useTeleport(1); +} + +sub on_AI_pre_manual_drop_route_dest_near_Obstacle { + my @obstacles = keys(%obstaclesList); + return unless (@obstacles > 0); + + my $arg_i; + if (AI::is("route")) { + $arg_i = 0; + return if (AI::action (1) eq "attack"); + } elsif (AI::action eq "move" && AI::action (1) eq "route") { + $arg_i = 1; + return if (AI::action (2) eq "attack"); + } else { + return; + } + + + my $args = AI::args($arg_i); + my $task = get_task($args); + return unless (defined $task); + + return unless ($task->{isRandomWalk} || ($task->{isToLockMap} && $field->baseName eq $config{'lockMap'})); + + my $obstacle = is_there_an_obstacle_near_pos($task->{dest}{pos}, 2); + if (defined $obstacle) { + warning "[avoidObstacle 5] Dropping current route dest because an Obstacle appeared near it.\n"; + AI::clear("move", "route"); + } +} + +################################################### +######## Main obstacle management +################################################### + +sub add_obstacle { + my ($actor, $obstacle, $type) = @_; + + if (exists $removed_obstacle_still_in_list{$actor->{ID}}) { + warning "[".PLUGIN_NAME."] New obstacle $actor on location ".$actor->{pos_to}{x}." ".$actor->{pos_to}{y}." already exists in removed_obstacle_still_in_list, deleting from it and updating position.\n"; + delete $obstaclesList{$actor->{ID}}; + delete $removed_obstacle_still_in_list{$actor->{ID}}; + } + + warning "[".PLUGIN_NAME."] Adding obstacle $actor on location ".$actor->{pos_to}{x}." ".$actor->{pos_to}{y}.".\n"; + + my $weight_changes = create_changes_array($actor->{pos_to}, $obstacle); + + $obstaclesList{$actor->{ID}}{pos_to} = $actor->{pos_to}; + $obstaclesList{$actor->{ID}}{weight} = $weight_changes; + $obstaclesList{$actor->{ID}}{type} = $type; + $obstaclesList{$actor->{ID}}{name} = $actor->name; + if ($type eq 'monster') { + $obstaclesList{$actor->{ID}}{nameID} = $actor->{nameID}; + } + + define_extras($actor->{ID}, $obstacle); + + $mustRePath = 1; +} + +sub define_extras { + my ($ID, $obstacle) = @_; + + if (exists $obstacle->{drop_target_near} && defined $obstacle->{drop_target_near} && $obstacle->{drop_target_near} == 1) { + $obstaclesList{$ID}{drop_target_near} = 1; + } else { + $obstaclesList{$ID}{drop_target_near} = 0; + } + + if (exists $obstacle->{drop_dest_near} && defined $obstacle->{drop_dest_near} && $obstacle->{drop_dest_near} == 1) { + $obstaclesList{$ID}{drop_dest_near} = 1; + } else { + $obstaclesList{$ID}{drop_dest_near} = 0; + } +} + +sub move_obstacle { + my ($actor, $obstacle, $type) = @_; + + return unless (ENABLE_MOVE); + + warning "[".PLUGIN_NAME."] Moving obstacle $actor (from ".$actor->{pos}{x}." ".$actor->{pos}{y}." to ".$actor->{pos_to}{x}." ".$actor->{pos_to}{y}.").\n"; + + my $weight_changes = create_changes_array($actor->{pos_to}, $obstacle); + + $obstaclesList{$actor->{ID}}{pos_to} = $actor->{pos_to}; + $obstaclesList{$actor->{ID}}{weight} = $weight_changes; + + $mustRePath = 1; +} + +sub remove_obstacle { + my ($actor, $type, $reason) = @_; + + return unless (ENABLE_REMOVE); + + if (($type eq 'monster' || $type eq 'player') && $reason eq 'outofsight') { + $removed_obstacle_still_in_list{$actor->{ID}} = 1; + warning "[".PLUGIN_NAME."] Putting obstacle $actor from ".$actor->{pos_to}{x}." ".$actor->{pos_to}{y}." in to the removed_obstacle_still_in_list.\n"; + + } else { + warning "[".PLUGIN_NAME."] Removing obstacle $actor from ".$actor->{pos_to}{x}." ".$actor->{pos_to}{y}.".\n"; + delete $obstaclesList{$actor->{ID}}; + $mustRePath = 1; + } +} + +################################################### +######## Tecnical subs +################################################### + +sub on_AI_pre_manual { + on_AI_pre_manual_drop_target_near_Obstacle(); + on_AI_pre_manual_teleport_soon(); + on_AI_pre_manual_drop_route_dest_near_Obstacle(); + on_AI_pre_manual_removed_obstacle_still_in_list(); + on_AI_pre_manual_repath(); +} + +sub on_AI_pre_manual_removed_obstacle_still_in_list { + my @obstacles = keys(%removed_obstacle_still_in_list); + return unless (@obstacles > 0); + + #warning "[".PLUGIN_NAME."] removed_obstacle_still_in_list: ".(scalar @obstacles)."\n"; + + OBSTACLE: foreach my $obstacle_ID (@obstacles) { + my $obstacle = $obstaclesList{$obstacle_ID}; + + my $realMyPos = calcPosition($char); + + my $dist = blockDistance($realMyPos, $obstacle->{pos_to}); + my $sight = ($config{clientSight}-2); # 2 cell leeway? + + next OBSTACLE unless ($dist < $sight); + + my $target; + #LIST: foreach my $list ($playersList, $monstersList, $npcsList, $petsList, $portalsList, $slavesList, $elementalsList) { + + if ($obstacle->{type} eq 'monster') { + my $actor = $monstersList->getByID($obstacle_ID); + if ($actor) { + $target = $actor; + } + } elsif ($obstacle->{type} eq 'player') { + my $actor = $playersList->getByID($obstacle_ID); + if ($actor) { + $target = $actor; + } + } + + # Should never happen + if ($target) { + warning "[REMOVING TEST] wwwwttttffffff 1.\n"; + } else { + warning "[removed_obstacle_still_in_list] Removing obstacle ".$obstacle->{name}." (".$obstacle->{type}.") from ".$obstacle->{pos_to}{x}." ".$obstacle->{pos_to}{y}." we at ($realMyPos->{x} $realMyPos->{y}) dist:$dist, sight:$sight.\n"; + delete $obstaclesList{$obstacle_ID}; + delete $removed_obstacle_still_in_list{$obstacle_ID}; + $mustRePath = 1; + } + } +} + +sub on_AI_pre_manual_repath { + return unless ($mustRePath); + + my $arg_i; + my $arg_i2; + + if (AI::is("route")) { + $arg_i = 0; + if (AI::action (1) eq "attack") { + if (AI::action (2) eq "route") { + $arg_i2 = 2; + } elsif (AI::action (3) eq "route") { + $arg_i2 = 3; + } + } + } elsif (AI::is("move") && AI::action (1) eq "route") { + $arg_i = 1; + if (AI::action (2) eq "attack") { + if (AI::action (3) eq "route") { + $arg_i2 = 3; + } elsif (AI::action (4) eq "route") { + $arg_i2 = 4; + } + } + } else { + return; + } + + $mustRePath = 0; + + my $args = AI::args($arg_i); + my $task = get_task($args); + if (defined $task) { + if (scalar @{$task->{solution}} == 0) { + Log::warning "[test1] Route already reseted.\n"; + } else { + Log::warning "[test2] Reseting route.\n"; + $task->resetRoute; + } + } + + return unless (defined $arg_i2); + + my $args2 = AI::args($arg_i2); + my $task2 = get_task($args2); + if (defined $task2) { + if (scalar @{$task2->{solution}} == 0) { + Log::warning "[test3] Route second already reseted.\n"; + } else { + Log::warning "[test4] Reseting second route.\n"; + $task2->resetRoute; + } + } +} + +sub get_task { + my ($args) = @_; + if (UNIVERSAL::isa($args, 'Task::Route')) { + return $args; + } elsif (UNIVERSAL::isa($args, 'Task::MapRoute') && $args->getSubtask && UNIVERSAL::isa($args->getSubtask, 'Task::Route')) { + return $args->getSubtask; + } else { + return undef; + } +} + +sub on_PathFindingReset { + my (undef, $hookargs) = @_; + + return unless (exists $hookargs->{args}{getRoute} && $hookargs->{args}{getRoute} == 1); + + my @obstacles = keys(%obstaclesList); + + #warning "[".PLUGIN_NAME."] on_PathFindingReset before check, there are ".@obstacles." obstacles.\n"; + + return unless (@obstacles > 0); + + my $args = $hookargs->{args}; + + #Log::warning "[test] on_PathFindingReset: Using grided info for ".@obstacles." obstacles.\n"; + + $args->{customWeights} = 1; + $args->{secondWeightMap} = get_final_grid(); + + $args->{avoidWalls} = 1 unless (defined $args->{avoidWalls}); + $args->{weight_map} = \($args->{field}->{weightMap}) unless (defined $args->{weight_map}); + + $args->{randomFactor} = 0 unless (defined $args->{randomFactor}); + $args->{useManhattan} = 0 unless (defined $args->{useManhattan}); + + $args->{timeout} = 1500 unless ($args->{timeout}); + $args->{width} = $args->{field}{width} unless ($args->{width}); + $args->{height} = $args->{field}{height} unless ($args->{height}); + $args->{min_x} = 0 unless (defined $args->{min_x}); + $args->{max_x} = ($args->{width}-1) unless (defined $args->{max_x}); + $args->{min_y} = 0 unless (defined $args->{min_y}); + $args->{max_y} = ($args->{height}-1) unless (defined $args->{max_y}); + + $hookargs->{return} = 0; +} + +sub getOffset { + my ($x, $width, $y) = @_; + return (($y * $width) + $x); +} + +sub get_final_grid { + my $changes = sum_all_changes(); + return $changes; +} + +sub get_weight_for_block { + my ($ratio, $dist) = @_; + if ($dist == 0) { + $dist = 1; + } + my $weight = int($ratio/($dist*$dist)); + if ($weight >= $weight_limit) { + $weight = $weight_limit; + } + return $weight; +} + +sub create_changes_array { + my ($obstacle_pos, $obstacle) = @_; + + my %obstacle = %{$obstacle}; + + my $max_distance = $obstacle{dist}; + my $ratio = $obstacle{weight}; + + my @changes_array; + + my ($min_x, $min_y, $max_x, $max_y) = Utils::getSquareEdgesFromCoord($field, $obstacle_pos, $max_distance); + + my @y_range = ($min_y..$max_y); + my @x_range = ($min_x..$max_x); + + foreach my $y (@y_range) { + foreach my $x (@x_range) { + next unless ($field->isWalkable($x, $y)); + my $pos = { + x => $x, + y => $y + }; + + my $distance = adjustedBlockDistance($pos, $obstacle_pos); + my $delta_weight = get_weight_for_block($ratio, $distance); + #warning "[".PLUGIN_NAME."] $x $y ($distance) -> $delta_weight.\n"; + push(@changes_array, { + x => $x, + y => $y, + weight => $delta_weight + }); + } + } + + @changes_array = sort { $b->{weight} <=> $a->{weight} } @changes_array; + + return \@changes_array; +} + +sub sum_all_changes { + my %changes_hash; + + #warning "[".PLUGIN_NAME."] 1 obstaclesList: ". Data::Dumper::Dumper \%obstaclesList; + + foreach my $key (keys %obstaclesList) { + #warning "[".PLUGIN_NAME."] sum_all_avoid - testing obstacle at $obstaclesList{$key}{pos_to}{x} $obstaclesList{$key}{pos_to}{y}.\n"; + foreach my $change (@{$obstaclesList{$key}{weight}}) { + my $x = $change->{x}; + my $y = $change->{y}; + my $changed = $change->{weight}; + $changes_hash{$x}{$y} += $changed; + } + } + + my @rebuilt_array; + foreach my $x_keys (keys %changes_hash) { + foreach my $y_keys (keys %{$changes_hash{$x_keys}}) { + next if ($changes_hash{$x_keys}{$y_keys} == 0); + push(@rebuilt_array, { x => $x_keys, y => $y_keys, weight => $changes_hash{$x_keys}{$y_keys} }); + } + } + + #warning "[".PLUGIN_NAME."] 2 rebuilt: ". Data::Dumper::Dumper \@rebuilt_array; + + return \@rebuilt_array; +} + +################################################### +######## Player avoiding +################################################### + +sub on_add_player_list { + return unless (ENABLE_AVOID_PLAYERS); + my (undef, $args) = @_; + my $actor = $args; + + return unless (exists $player_name_obstacles{$actor->{name}}); + + my %obstacle = %{$player_name_obstacles{$actor->{name}}}; + + add_obstacle($actor, \%obstacle, 'player'); +} + +sub on_player_moved { + return unless (ENABLE_AVOID_PLAYERS); + my (undef, $args) = @_; + my $actor = $args; + + return unless (exists $obstaclesList{$actor->{ID}}); + + my %obstacle = %{$player_name_obstacles{$actor->{name}}}; + + move_obstacle($actor, \%obstacle, 'player'); +} + +sub on_player_disappeared { + return unless (ENABLE_AVOID_PLAYERS); + my (undef, $args) = @_; + my $actor = $args->{player}; + + return unless (exists $obstaclesList{$actor->{ID}}); + + remove_obstacle($actor, 'player'); +} + +################################################### +######## Mob avoiding +################################################### + +sub on_add_monster_list { + return unless (ENABLE_AVOID_MONSTERS); + my (undef, $args) = @_; + my $actor = $args; + + return unless (exists $mob_nameID_obstacles{$actor->{nameID}}); + + my %obstacle = %{$mob_nameID_obstacles{$actor->{nameID}}}; + + add_obstacle($actor, \%obstacle, 'monster'); +} + +sub on_monster_moved { + return unless (ENABLE_AVOID_MONSTERS); + my (undef, $args) = @_; + my $actor = $args; + + return unless (exists $obstaclesList{$actor->{ID}}); + + my %obstacle = %{$mob_nameID_obstacles{$actor->{nameID}}}; + + move_obstacle($actor, \%obstacle, 'monster'); +} + +sub on_monster_disappeared { + return unless (ENABLE_AVOID_MONSTERS); + my (undef, $args) = @_; + my $actor = $args->{monster}; + + return unless (exists $obstaclesList{$actor->{ID}}); + + my $reason; + if ($args->{type} == 0) { + $reason = 'outofsight'; + } else { + $reason = 'gone'; + } + message ("[on_monster_disappeared] $actor type $args->{type} | reason $reason\n", "route"); + remove_obstacle($actor, 'monster', $reason); +} + +################################################### +######## Spell avoiding +################################################### + +# TODO: Add fail flag check + +sub on_add_areaSpell_list { + return unless (ENABLE_AVOID_AREASPELLS); + my (undef, $args) = @_; + my $ID = $args->{ID}; + my $spell = $spells{$ID}; + + return unless (exists $area_spell_type_obstacles{$spell->{type}}); + + my %obstacle = %{$area_spell_type_obstacles{$spell->{type}}}; + + add_obstacle($spell, \%obstacle, 'spell'); +} + +sub on_areaSpell_disappeared { + return unless (ENABLE_AVOID_AREASPELLS); + my (undef, $args) = @_; + my $ID = $args->{ID}; + my $spell = $spells{$ID}; + + return unless (exists $obstaclesList{$spell->{ID}}); + + remove_obstacle($spell, 'spell'); +} + +################################################### +######## portals avoiding +################################################### + +sub on_add_portal_list { + return unless (ENABLE_AVOID_PORTALS); + my (undef, $args) = @_; + my $actor = $args; + + add_obstacle($actor, \%portals_obstacles, 'portal'); +} + +sub on_portal_disappeared { + return unless (ENABLE_AVOID_PORTALS); + my (undef, $args) = @_; + my $actor = $args->{portal}; + + #remove_obstacle($actor, 'portal'); +} + +return 1; \ No newline at end of file diff --git a/src/AI.pm b/src/AI.pm index 502d1978d6..5ef42a19cf 100644 --- a/src/AI.pm +++ b/src/AI.pm @@ -171,7 +171,22 @@ sub mapChanged { } sub findAction { - return binFind(\@ai_seq, $_[0]); + my $wanted_action = shift; + my $skip = shift; + if (!defined $skip) { + $skip = 0; + } + + foreach my $i (0..$#ai_seq) { + next unless ($ai_seq[$i] eq $wanted_action); + if ($skip) { + $skip--; + } else { + return $i; + } + } + + return undef; } sub inQueue { @@ -380,6 +395,7 @@ sub ai_slave_getAggressives { next if (!timeOut($monster->{attack_failedLOS}, $timeout{ai_attack_failedLOS}{timeout})); next if (!timeOut($monster->{$slave->{ai_attack_failed_timeout}}, $timeout{ai_attack_unfail}{timeout})); next if (!Misc::slave_checkMonsterCleanness($slave, $ID)); + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? my $pos = calcPosition($monster); next if (blockDistance($char->position, $pos) > ($config{$slave->{configPrefix}.'followDistanceMax'} + $config{$slave->{configPrefix}.'attackMaxDistance'})); diff --git a/src/AI/Attack.pm b/src/AI/Attack.pm index d684dd824d..127798949f 100644 --- a/src/AI/Attack.pm +++ b/src/AI/Attack.pm @@ -34,6 +34,10 @@ use Utils; use Utils::Benchmark; use Utils::PathFinding; +use constant { + MOVING_TO_ATTACK => 1, + ATTACKING => 2, +}; sub process { Benchmark::begin("ai_attack") if DEBUG; @@ -43,9 +47,11 @@ sub process { if (shouldAttack($action, $args)) { my $ID; my $ataqArgs; + my $stage; # 1 - moving to attack | 2 - attacking if (AI::action eq "attack") { $ID = $args->{ID}; $ataqArgs = AI::args(0); + $stage = ATTACKING; } else { if (AI::action(1) eq "attack") { $ataqArgs = AI::args(1); @@ -54,149 +60,136 @@ sub process { $ataqArgs = AI::args(2); } $ID = $args->{attackID}; + $stage = MOVING_TO_ATTACK; } if (targetGone($ataqArgs, $ID)) { finishAttacking($ataqArgs, $ID); return; } elsif (shouldGiveUp($ataqArgs, $ID)) { - giveUp($ataqArgs, $ID); + giveUp($ataqArgs, $ID, 0); return; } my $target = Actor::get($ID); - if ($target) { - my $party = $config{'attackAuto_party'} ? 1 : 0; - my $target_is_aggressive = is_aggressive($target, undef, 0, $party); - my @aggressives = ai_getAggressives(0, $party); - if ($config{attackChangeTarget} && !$target_is_aggressive && @aggressives) { - my $attackTarget = getBestTarget(\@aggressives, $config{attackCheckLOS}, $config{attackCanSnipe}); - if ($attackTarget) { - $char->sendAttackStop; - AI::dequeue while (AI::inQueue("attack")); - ai_setSuspend(0); - my $new_target = Actor::get($attackTarget); - warning TF("Your target is not aggressive: %s, changing target to aggressive: %s.\n", $target, $new_target), 'ai_attack'; - $char->attack($attackTarget); - AI::Attack::process(); - return; - } - } + unless ($target && $target->{type} ne 'Unknown') { + finishAttacking($ataqArgs, $ID); + return; } - } - - if (AI::action eq "attack" && AI::args->{suspended}) { - $args->{ai_attack_giveup}{time} += time - $args->{suspended}; - delete $args->{suspended}; - } - - if (AI::action eq "attack" && $args->{move_start}) { - # We've just finished moving to the monster. - # Don't count the time we spent on moving - $args->{ai_attack_giveup}{time} += time - $args->{move_start}; - undef $args->{unstuck}{time}; - undef $args->{move_start}; - - } elsif (AI::action eq "attack" && $args->{avoiding} && $args->{ID}) { - my $ID = $args->{ID}; - my $target = Actor::get($ID); - $args->{ai_attack_giveup}{time} = time; - undef $args->{avoiding}; - debug "Finished avoiding movement from target $target, updating ai_attack_giveup\n", "ai_attack"; - - } elsif (((AI::action eq "route" && AI::action(1) eq "attack") || (AI::action eq "move" && AI::action(2) eq "attack")) - && $args->{attackID} && timeOut($timeout{ai_attack_route_adjust})) { - # We're on route to the monster; check whether the monster has moved - my $ID = $args->{attackID}; - my $attackSeq = (AI::action eq "route") ? AI::args(1) : AI::args(2); - my $target = Actor::get($ID); - my $realMyPos = calcPosition($char); - my $realMonsterPos = calcPosition($target); - - if ( - $target->{type} ne 'Unknown' && - $attackSeq->{monsterPos} && - %{$attackSeq->{monsterPos}} && - $attackSeq->{monsterLastMoveTime} && - $attackSeq->{monsterLastMoveTime} != $target->{time_move} - ) { - # Monster has moved; stop moving and let the attack AI readjust route - debug "Target $target has moved since we started routing to it - Adjusting route\n", "ai_attack"; - AI::dequeue while (AI::is("move", "route")); - - $attackSeq->{ai_attack_giveup}{time} = time; - - } elsif ( - $target->{type} ne 'Unknown' && - $attackSeq->{monsterPos} && - %{$attackSeq->{monsterPos}} && - $attackSeq->{monsterLastMoveTime} && - $attackSeq->{attackMethod}{maxDistance} == 1 && - canReachMeleeAttack($realMyPos, $realMonsterPos) && - (blockDistance($realMyPos, $realMonsterPos) < 2 || !$config{attackCheckLOS} ||($config{attackCheckLOS} && blockDistance($realMyPos, $realMonsterPos) == 2 && $field->checkLOS($realMyPos, $realMonsterPos, $config{attackCanSnipe}))) - ) { - debug "Target $target is now reachable by melee attacks during routing to it.\n", "ai_attack"; - AI::dequeue while (AI::is("move", "route")); - - $attackSeq->{ai_attack_giveup}{time} = time; - + my $party = $config{'attackAuto_party'} ? 1 : 0; + my $target_is_aggressive = is_aggressive($target, undef, 0, $party); + my @aggressives = ai_getAggressives(0, $party); + if ($config{attackChangeTarget} && !$target_is_aggressive && @aggressives) { + my $attackTarget = getBestTarget(\@aggressives, $config{attackCheckLOS}, $config{attackCanSnipe}); + if ($attackTarget) { + $char->sendAttackStop; + AI::dequeue while (AI::inQueue("attack")); + ai_setSuspend(0); + my $new_target = Actor::get($attackTarget); + warning TF("Your target is not aggressive: %s, changing target to aggressive: %s.\n", $target, $new_target), 'ai_attack'; + $char->attack($attackTarget); + AI::Attack::process(); + return; + } } - $timeout{ai_attack_route_adjust}{time} = time; - } - - if (AI::action eq "attack" && timeOut($args->{attackMainTimeout}, 0.1)) { - $args->{attackMainTimeout} = time; - main(); - } - - # Check for hidden monsters - if (AI::inQueue("attack") && AI::is("move", "route", "attack")) { - my $ID = AI::args->{attackID}; - my $monster = $monsters{$ID}; - if (($monster->{statuses}->{EFFECTSTATE_BURROW} || $monster->{statuses}->{EFFECTSTATE_HIDING}) && - $config{avoidHiddenMonsters}) { - message TF("Dropping target %s - will not attack hidden monsters\n", $monster), 'ai_attack'; + my $cleanMonster = checkMonsterCleanness($ID); + if (!$cleanMonster) { + message TF("Dropping target %s - will not kill steal others\n", $target), 'ai_attack'; $char->sendAttackStop; - $monster->{ignore} = 1; - + $target->{ignore} = 1; AI::dequeue while (AI::inQueue("attack")); - if ($config{teleportAuto_dropTargetHidden}) { - message T("Teleport due to dropping hidden target\n"); + if ($config{teleportAuto_dropTargetKS}) { + message T("Teleport due to dropping attack target\n"), "teleport"; ai_useTeleport(1); } + return; } - } - - # Check for kill steal, mob-training and hiding while moving - if ((AI::is("move", "route") && $args->{attackID} && AI::inQueue("attack") - && timeOut($args->{movingWhileAttackingTimeout}, 0.2))) { - my $ID = AI::args->{attackID}; - my $monster = $monsters{$ID}; - - # Check for kill steal while moving - if ($monster && !Misc::checkMonsterCleanness($ID)) { - dropTargetWhileMoving(); + if ((my $control = mon_control($target->{name},$target->{nameID}))) { + if ($control->{attack_auto} == 3 && ($target->{dmgToYou} || $target->{missedYou} || $target->{dmgFromYou})) { + message TF("Dropping target - %s (%s) has been provoked\n", $target->{name}, $target->{binID}); + $char->sendAttackStop; + $target->{ignore} = 1; + AI::dequeue while (AI::inQueue("attack")); + return; + } } - - # Mob-training, stop attacking the monster if it is already aggressive - if ((my $control = mon_control($monster->{name},$monster->{nameID}))) { - if ($control->{attack_auto} == 3 - && ($monster->{dmgToYou} || $monster->{missedYou} || $monster->{dmgFromYou})) { - - message TF("Dropping target - %s (%s) has been provoked\n", $monster->{name}, $monster->{binID}); + + if ($stage == MOVING_TO_ATTACK) { + # Check for hidden monsters + if (($target->{statuses}->{EFFECTSTATE_BURROW} || $target->{statuses}->{EFFECTSTATE_HIDING}) && $config{avoidHiddenMonsters}) { + message TF("Dropping target %s - will not attack hidden monsters\n", $target), 'ai_attack'; $char->sendAttackStop; - $monster->{ignore} = 1; - # Right now, the queue is either - # move, route, attack - # -or- - # route, attack + $target->{ignore} = 1; + AI::dequeue while (AI::inQueue("attack")); + if ($config{teleportAuto_dropTargetHidden}) { + message T("Teleport due to dropping hidden target\n"); + ai_useTeleport(1); + } + return; + } + + # We're on route to the monster; check whether the monster has moved + if ($args->{attackID} && timeOut($timeout{ai_attack_route_adjust})) { + if ( + $target->{type} ne 'Unknown' && + $ataqArgs->{monsterLastMoveTime} && + $ataqArgs->{monsterLastMoveTime} != $target->{time_move} + ) { + if ( + ($args->{monsterLastMovePosTo}{x} == $target->{pos_to}{x} && $args->{monsterLastMovePosTo}{y} == $target->{pos_to}{y}) + ) { + $args->{monsterLastMoveTime} = $target->{time_move}; + $args->{monsterLastMovePosTo}{x} = $target->{pos_to}{x}; + $args->{monsterLastMovePosTo}{y} = $target->{pos_to}{y}; + } else { + # Monster has moved; stop moving and let the attack AI readjust route + debug "Target $target has moved since we started routing to it - Adjusting route\n", "ai_attack"; + AI::dequeue while (AI::is("move", "route")); + + $ataqArgs->{ai_attack_giveup}{time} = time; + $ataqArgs->{sentApproach} = 0; + undef $args->{unstuck}{time}; + undef $args->{avoiding}; + undef $args->{move_start}; + } + } else { + $timeout{ai_attack_route_adjust}{time} = time; + } } } + + if ($stage == ATTACKING) { + if (AI::args->{suspended}) { + $args->{ai_attack_giveup}{time} += time - $args->{suspended}; + delete $args->{suspended}; + + # We've just finished moving to the monster. + # Don't count the time we spent on moving + } elsif ($args->{move_start}) { + $args->{ai_attack_giveup}{time} += time - $args->{move_start}; + undef $args->{unstuck}{time}; + undef $args->{move_start}; + + } elsif ($args->{avoiding}) { + $args->{ai_attack_giveup}{time} = time; + undef $args->{avoiding}; + debug "Finished avoiding movement from target $target, updating ai_attack_giveup\n", "ai_attack"; + } + + if (timeOut($timeout{ai_attack_main})) { + if ($char->{sitting}) { + ai_setSuspend(0); + stand(); + } else { + main(); + } + $timeout{ai_attack_main}{time} = time; + } - $args->{movingWhileAttackingTimeout} = time; + } } Benchmark::end("ai_attack") if DEBUG; @@ -205,9 +198,9 @@ sub process { sub shouldAttack { my ($action, $args) = @_; return ( - ($action eq "attack" && $args->{ID}) - || ($action eq "route" && AI::action(1) eq "attack" && $args->{attackID}) - || ($action eq "move" && AI::action(2) eq "attack" && $args->{attackID}) + ($action eq "attack" && $args->{ID}) || + ($action eq "route" && AI::action(1) eq "attack" && $args->{attackID}) || + ($action eq "move" && AI::action(2) eq "attack" && $args->{attackID}) ); } @@ -217,9 +210,16 @@ sub shouldGiveUp { } sub giveUp { - my ($args, $ID) = @_; + my ($args, $ID, $LOS) = @_; my $target = Actor::get($ID); - $target->{attack_failed} = time if ($monsters{$ID}); + if ($monsters{$ID}) { + if ($LOS) { + $target->{attack_failedLOS} = time; + } else { + $target->{attack_failed} = time; + } + } + $target->{dmgFromYou} = 0; # Hack | TODO: Fix me AI::dequeue while (AI::inQueue("attack")); message T("Can't reach or damage target, dropping target\n"), "ai_attack"; if ($config{'teleportAuto_dropTarget'}) { @@ -230,7 +230,14 @@ sub giveUp { sub targetGone { my ($args, $ID) = @_; - return !$monsters{$args->{ID}} && (!$players{$args->{ID}} || $players{$args->{ID}}{dead}); + my $target = Actor::get($ID, 1); + unless ($target) { + return 1; + } + if (exists $target->{dead} && $target->{dead} == 1) { + return 1; + } + return 0; } sub finishAttacking { @@ -282,24 +289,52 @@ sub finishAttacking { $messageSender->sendStopSkillUse($char->{last_continuous_skill_used}) if $char->{last_skill_used_is_continuous}; Plugins::callHook('attack_end', {ID => $ID}) - } -sub dropTargetWhileMoving { - my $ID = AI::args->{attackID}; - my $target = Actor::get($ID); - message TF("Dropping target %s - will not kill steal others\n", $target), 'ai_attack'; - $char->sendAttackStop; - $target->{ignore} = 1; - - # Right now, the queue is either - # move, route, attack - # -or- - # route, attack - AI::dequeue while (AI::inQueue("attack")); - if ($config{teleportAuto_dropTargetKS}) { - message T("Teleport due to dropping attack target\n"); - ai_useTeleport(1); +sub find_kite_position { + my ($args, $inAdvance, $target, $realMyPos, $realMonsterPos, $noAttackMethodFallback_runFromTarget) = @_; + + my $maxDistance; + if (!$noAttackMethodFallback_runFromTarget && defined $args->{attackMethod}{type} && defined $args->{attackMethod}{maxDistance}) { + $maxDistance = $args->{attackMethod}{maxDistance}; + } elsif ($noAttackMethodFallback_runFromTarget) { + $maxDistance = $config{'runFromTarget_noAttackMethodFallback_attackMaxDist'}; + } else { + # Should never happen. + return 0; + } + + # We try to find a position to kite from at least runFromTarget_minStep away from the target but at maximun {attackMethod}{maxDistance} away from it + my $pos = meetingPosition($char, 1, $target, $maxDistance, ($noAttackMethodFallback_runFromTarget ? 2 : 1)); + if ($pos) { + if ($inAdvance) { + debug TF("[runFromTarget_inAdvance] %s kiting in advance (%d %d) to (%d %d), mob at (%d %d).\n", $char, $realMyPos->{x}, $realMyPos->{y}, $pos->{x}, $pos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + } elsif ($noAttackMethodFallback_runFromTarget) { + debug TF("[runFromTarget_noAttackMethodFallback] %s kiting in advance (%d %d) to (%d %d), mob at (%d %d).\n", $char, $realMyPos->{x}, $realMyPos->{y}, $pos->{x}, $pos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + } else { + debug TF("[runFromTarget] (attackmaxDistance %s) %s kiteing from (%d %d) to (%d %d), mob at (%d %d).\n", $maxDistance, $char, $realMyPos->{x}, $realMyPos->{y}, $pos->{x}, $pos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + } + $args->{avoiding} = 1; + $char->route( + undef, + @{$pos}{qw(x y)}, + noMapRoute => 1, + avoidWalls => 0, + randomFactor => 0, + useManhattan => 1, + runFromTarget => 1 + ); + return 1; + + } else { + if ($inAdvance) { + debug TF("[runFromTarget_inAdvance] %s no acceptable place to kite in advance from (%d %d), mob at (%d %d).\n", $char, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + } elsif ($noAttackMethodFallback_runFromTarget) { + debug TF("[runFromTarget_noAttackMethodFallback] %s no acceptable place to kite from (%d %d), mob at (%d %d).\n", $char, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + } else { + debug TF("[runFromTarget] %s no acceptable place to kite from (%d %d), mob at (%d %d).\n", $char, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + } + return 0; } } @@ -319,13 +354,22 @@ sub main { my $monsterPos = $target->{pos_to}; my $monsterDist = blockDistance($myPos, $monsterPos); - my ($realMyPos, $realMonsterPos, $realMonsterDist, $hitYou); - my $realMyPos = calcPosition($char); - my $realMonsterPos = calcPosition($target); + my $realMyPos = calcPosFromPathfinding($field, $char); + my $realMonsterPos = calcPosFromPathfinding($field, $target); + my $realMonsterDist = blockDistance($realMyPos, $realMonsterPos); - - my $cleanMonster = checkMonsterCleanness($ID); - + my $clientDist = getClientDist($realMyPos, $realMonsterPos); + + my $failed_to_attack_packet_recv = 0; + + if (!exists $args->{temporary_extra_range} || !defined $args->{temporary_extra_range}) { + $args->{temporary_extra_range} = 0; + } + + if (exists $target->{movetoattack_pos} && exists $char->{movetoattack_pos}) { + $failed_to_attack_packet_recv = 1; + $args->{temporary_extra_range} = 0; + } # If the damage numbers have changed, update the giveup time so we don't timeout if ($args->{dmgToYou_last} != $target->{dmgToYou} @@ -335,12 +379,15 @@ sub main { $args->{ai_attack_giveup}{time} = time; debug "Update attack giveup time\n", "ai_attack", 2; } - $hitYou = ($args->{dmgToYou_last} != $target->{dmgToYou} - || $args->{missedYou_last} != $target->{missedYou}); + + my $hitYou = ($args->{dmgToYou_last} != $target->{dmgToYou} || $args->{missedYou_last} != $target->{missedYou}); + my $youHitTarget = ($args->{dmgFromYou_last} != $target->{dmgFromYou}); + $args->{dmgToYou_last} = $target->{dmgToYou}; $args->{missedYou_last} = $target->{missedYou}; $args->{dmgFromYou_last} = $target->{dmgFromYou}; $args->{missedFromYou_last} = $target->{missedFromYou}; + $args->{lastSkillTime} = $char->{last_skill_time}; Benchmark::end("ai_attack (part 1.1)") if DEBUG; @@ -348,6 +395,7 @@ sub main { # Determine what combo skill to use delete $args->{attackMethod}; + my $i = 0; while (exists $config{"attackComboSlot_$i"}) { if (!$config{"attackComboSlot_$i"}) { @@ -389,13 +437,13 @@ sub main { # Determine what skill to use to attack if (!$args->{attackMethod}{type}) { if ($config{'attackUseWeapon'}) { + $args->{attackMethod}{type} = "weapon"; $args->{attackMethod}{distance} = $config{'attackDistance'}; $args->{attackMethod}{maxDistance} = $config{'attackMaxDistance'}; - $args->{attackMethod}{type} = "weapon"; } else { + undef $args->{attackMethod}{type}; $args->{attackMethod}{distance} = 1; $args->{attackMethod}{maxDistance} = 1; - undef $args->{attackMethod}{type}; } $i = 0; @@ -427,14 +475,11 @@ sub main { } $i++; } - - if ($config{'runFromTarget'} && $config{'runFromTarget_dist'} > $args->{attackMethod}{distance}) { - $args->{attackMethod}{distance} = $config{'runFromTarget_dist'}; - } } $args->{attackMethod}{maxDistance} ||= $config{attackMaxDistance}; $args->{attackMethod}{distance} ||= $config{attackDistance}; + if ($args->{attackMethod}{maxDistance} < $args->{attackMethod}{distance}) { $args->{attackMethod}{maxDistance} = $args->{attackMethod}{distance}; } @@ -442,175 +487,212 @@ sub main { Benchmark::end("ai_attack (part 1.2)") if DEBUG; Benchmark::end("ai_attack (part 1)") if DEBUG; + my $melee; + my $ranged; if (defined $args->{attackMethod}{type} && exists $args->{ai_attack_failed_give_up} && defined $args->{ai_attack_failed_give_up}{time}) { + debug "Deleting ai_attack_failed_give_up time.\n"; delete $args->{ai_attack_failed_give_up}{time}; - } + + } elsif ($args->{attackMethod}{maxDistance} == 1) { + $melee = 1; - if ($char->{sitting}) { - ai_setSuspend(0); - stand(); - - } elsif (!$cleanMonster) { - # Drop target if it's already attacked by someone else - message TF("Dropping target %s - will not kill steal others\n", $target), 'ai_attack'; - $char->sendMove(@{$realMyPos}{qw(x y)}); - AI::dequeue while (AI::inQueue("attack")); - if ($config{teleportAuto_dropTargetKS}) { - message T("Teleport due to dropping attack target\n"), "teleport"; - ai_useTeleport(1); + } elsif ($args->{attackMethod}{maxDistance} > 1) { + $ranged = 1; + } + + $args->{attackMethod}{maxDistance} += $args->{temporary_extra_range}; + + # -2: undefined attackMethod + # -1: No LOS + # 0: out of range + # 1: sucess + my $canAttack; + if (defined $args->{attackMethod}{type} && defined $args->{attackMethod}{maxDistance}) { + $canAttack = canAttack($field, $realMyPos, $realMonsterPos, $config{attackCanSnipe}, $args->{attackMethod}{maxDistance}, $config{clientSight}); + } else { + $canAttack = -2; + } + + my $range_type_string = ($melee ? "Melee" : ($ranged ? "Ranged" : "None")); + my $canAttack_fail_string = (($canAttack == -2) ? "No Method" : (($canAttack == -1) ? "No LOS" : (($canAttack == 0) ? "No Range" : "OK"))); + + # Here we check if the monster which we are waiting to get closer to us is in fact close enough + # If it is close enough delete the ai_attack_failed_waitForAgressive_give_up keys and loop attack logic + if ( + $config{"attackBeyondMaxDistance_waitForAgressive"} && + $target->{dmgFromYou} > 0 && + $canAttack == 1 && + exists $args->{ai_attack_failed_waitForAgressive_give_up} && + defined $args->{ai_attack_failed_waitForAgressive_give_up}{time} + ) { + debug "Deleting ai_attack_failed_waitForAgressive_give_up time.\n"; + delete $args->{ai_attack_failed_waitForAgressive_give_up}{time}; + } + + # Here we check if we have finished moving to the meeting position to attack our target, only checks this if attackWaitApproachFinish is set to 1 in config + # If so sets sentApproach to 0 + if ( + $config{"attackWaitApproachFinish"} && + ($canAttack == 0 || $canAttack == -1) && + $args->{sentApproach} + ) { + if (!timeOut($char->{time_move}, $char->{time_move_calc})) { + debug TF("[Out of Range - Still Approaching - Waiting] %s (%d %d), target %s (%d %d), distance %d, maxDistance %d, dmgFromYou %d.\n", $char, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $realMonsterDist, $args->{attackMethod}{maxDistance}, $target->{dmgFromYou}), 'ai_attack'; + return; + } else { + debug TF("[Out of Range - Ended Approaching] %s (%d %d), target %s (%d %d), distance %d, maxDistance %d, dmgFromYou %d.\n", $char, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $realMonsterDist, $args->{attackMethod}{maxDistance}, $target->{dmgFromYou}), 'ai_attack'; + $args->{sentApproach} = 0; } - - } elsif ($config{'runFromTarget'} && ($realMonsterDist < $config{'runFromTarget_dist'} || $hitYou)) { - my $cell = meetingPosition($char, 1, $target, $args->{attackMethod}{maxDistance}, 1); - if ($cell) { - debug TF("[runFromTarget] %s kiteing from (%d %d) to (%d %d), mob at (%d %d).\n", $char, $realMyPos->{x}, $realMyPos->{y}, $cell->{x}, $cell->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; - $args->{avoiding} = 1; - $char->route(undef, @{$cell}{qw(x y)}, noMapRoute => 1, avoidWalls => 0, runFromTarget => 1); + } + + my $found_action = 0; + my $failed_runFromTarget = 0; + my $hitTarget_when_not_possible = 0; + + # Here, if runFromTarget is active, we check if the target mob is closer to us than the minimun distance specified in runFromTarget_dist + # If so try to kite it + if ( + !$found_action && + $config{"runFromTarget"} && + $realMonsterDist < $config{"runFromTarget_dist"} + ) { + my $try_runFromTarget = find_kite_position($args, 0, $target, $realMyPos, $realMonsterPos, 0); + if ($try_runFromTarget) { + $found_action = 1; } else { - debug TF("%s no acceptable place to kite from (%d %d), mob at (%d %d).\n", $char, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + $failed_runFromTarget = 1; } - - if (!$cell) { - my $max = $args->{attackMethod}{maxDistance} + 4; - if ($max > 14) { - $max = 14; - } - $cell = meetingPosition($char, 1, $target, $max, 1); - if ($cell) { - debug TF("[runFromTarget] %s kiteing from (%d %d) to (%d %d), mob at (%d %d).\n", $char, $realMyPos->{x}, $realMyPos->{y}, $cell->{x}, $cell->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; - $args->{avoiding} = 1; - $char->route(undef, @{$cell}{qw(x y)}, noMapRoute => 1, avoidWalls => 0, runFromTarget => 1); - } else { - debug TF("%s no acceptable place to kite from (%d %d), mob at (%d %d).\n", $char, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; - } + } + + # Here, if runFromTarget is active, and we can't attack right now (eg. all skills in cooldown) we check if the target mob is closer to us than the minimun distance specified in runFromTarget_noAttackMethodFallback_minStep + # If so try to kite it using maxdistance of runFromTarget_noAttackMethodFallback_attackMaxDist + if ( + !$found_action && + $canAttack == -2 && + #$config{"runFromTarget"} && + $config{'runFromTarget_noAttackMethodFallback'} && + $realMonsterDist < $config{'runFromTarget_noAttackMethodFallback_minStep'} + ) { + my $try_runFromTarget = find_kite_position($args, 0, $target, $realMyPos, $realMonsterPos, 1); + if ($try_runFromTarget) { + $found_action = 1; } + } - - } elsif(!defined $args->{attackMethod}{type}) { + if ( + !$found_action && + $canAttack == -2 + ) { debug T("Can't determine a attackMethod (check attackUseWeapon and Skills blocks)\n"), "ai_attack"; $args->{ai_attack_failed_give_up}{timeout} = 6 if !$args->{ai_attack_failed_give_up}{timeout}; $args->{ai_attack_failed_give_up}{time} = time if !$args->{ai_attack_failed_give_up}{time}; if (timeOut($args->{ai_attack_failed_give_up})) { delete $args->{ai_attack_failed_give_up}{time}; - message T("Unable to determine a attackMethod (check attackUseWeapon and Skills blocks)\n"), "ai_attack"; - giveUp($args, $ID); + warning T("Unable to determine a attackMethod (check attackUseWeapon and Skills blocks), dropping target.\n"), "ai_attack"; + $found_action = 1; + giveUp($args, $ID, 0); } - - - } elsif ( - # We are out of range, but already hit enemy, should wait for him in a safe place instead of going after him - # Example at https://youtu.be/kTRk5Na1aCQ?t=25 in which this check did not exist, we tried getting closer intead of waiting and got hit - ($args->{attackMethod}{maxDistance} > 1 && $realMonsterDist > $args->{attackMethod}{maxDistance}) && - #(!$config{attackCheckLOS} || $field->checkLOS($realMyPos, $realMonsterPos, $config{attackCanSnipe})) && # Is this check needed? + } + + if ($canAttack == 0 && $youHitTarget) { + debug TF("[%s - %s] We were able to hit target even though it is out of range or LOS, accepting and continuing. (you %s (%d %d), target %s (%d %d) [(%d %d) -> (%d %d)], distance %d, maxDistance %d, dmgFromYou %d)\n", $canAttack_fail_string, $range_type_string, $char, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $target->{pos}{x}, $target->{pos}{y}, $target->{pos_to}{x}, $target->{pos_to}{y}, $realMonsterDist, $args->{attackMethod}{maxDistance}, $target->{dmgFromYou}), 'ai_attack'; + if ($clientDist > $args->{attackMethod}{maxDistance} && $clientDist <= ($args->{attackMethod}{maxDistance} + 1) && $args->{temporary_extra_range} == 0) { + debug TF("[%s] Probably extra range provided by the server due to chasing, increasing range by 1.\n", $canAttack_fail_string), 'ai_attack'; + $args->{temporary_extra_range} = 1; + $args->{attackMethod}{maxDistance} += $args->{temporary_extra_range}; + $canAttack = canAttack($field, $realMyPos, $realMonsterPos, $config{attackCanSnipe}, $args->{attackMethod}{maxDistance}, $config{clientSight}); + } else { + debug TF("[%s] Reason unknown, allowing once.\n", $canAttack_fail_string), 'ai_attack'; + $hitTarget_when_not_possible = 1; + } + if ( + $config{"attackBeyondMaxDistance_waitForAgressive"} && + exists $args->{ai_attack_failed_waitForAgressive_give_up} && + defined $args->{ai_attack_failed_waitForAgressive_give_up}{time} + ) { + debug "[Accepting] Deleting ai_attack_failed_waitForAgressive_give_up time.\n"; + delete $args->{ai_attack_failed_waitForAgressive_give_up}{time};; + } + } + + # Here we decide what to do when a mob we have already hit is no longer in range or we have no LOS to it + # We also check if we have waited too long for the monster which we are waiting to get closer to us to approach + # TODO: Maybe we should separate this into 2 sections, one for out of range and another for no LOS - low priority + if ( + !$found_action && $config{"attackBeyondMaxDistance_waitForAgressive"} && - $target->{dmgFromYou} > 0 + $target->{dmgFromYou} > 0 && + ($canAttack == 0 || $canAttack == -1) && + !$hitTarget_when_not_possible ) { $args->{ai_attack_failed_waitForAgressive_give_up}{timeout} = 6 if !$args->{ai_attack_failed_waitForAgressive_give_up}{timeout}; $args->{ai_attack_failed_waitForAgressive_give_up}{time} = time if !$args->{ai_attack_failed_waitForAgressive_give_up}{time}; - if (timeOut($args->{ai_attack_failed_waitForAgressive_give_up})) { delete $args->{ai_attack_failed_waitForAgressive_give_up}{time}; - message T("[Out of Range] Waited too long for target to get closer, dropping target\n"), "ai_attack"; - giveUp($args, $ID); + warning TF("[%s - %s] Waited too long for target to get closer, dropping target. (you %s (%d %d), target %s (%d %d) [(%d %d) -> (%d %d)], distance %d, maxDistance %d, dmgFromYou %d)\n", $canAttack_fail_string, $range_type_string, $char, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $target->{pos}{x}, $target->{pos}{y}, $target->{pos_to}{x}, $target->{pos_to}{y}, $realMonsterDist, $args->{attackMethod}{maxDistance}, $target->{dmgFromYou}), 'ai_attack'; + giveUp($args, $ID, 0); } else { - warning TF("[Out of Range - Waiting] %s (%d %d), target %s (%d %d), distance %d, maxDistance %d, dmgFromYou %d.\n", $char, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $realMonsterDist, $args->{attackMethod}{maxDistance}, $target->{dmgFromYou}), 'ai_attack'; + $messageSender->sendAction($ID, ($config{'tankMode'}) ? 0 : 7) if ($config{"attackBeyondMaxDistance_sendAttackWhileWaiting"}); + debug TF("[%s - %s - Waiting] %s (%d %d), target %s (%d %d) [(%d %d) -> (%d %d)], distance %d, maxDistance %d, dmgFromYou %d.\n", $canAttack_fail_string, $range_type_string, $char, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $target->{pos}{x}, $target->{pos}{y}, $target->{pos_to}{x}, $target->{pos_to}{y}, $realMonsterDist, $args->{attackMethod}{maxDistance}, $target->{dmgFromYou}), 'ai_attack'; } + $found_action = 1; + } - } elsif ( - # We are out of range - ($args->{attackMethod}{maxDistance} == 1 && !canReachMeleeAttack($realMyPos, $realMonsterPos)) || - ($args->{attackMethod}{maxDistance} > 1 && $realMonsterDist > $args->{attackMethod}{maxDistance}) + # Here we decide what to do with a mob which is out of range or we have no LOS to + if ( + !$found_action && + ($canAttack == 0 || $canAttack == -1) && + !$hitTarget_when_not_possible ) { - $args->{move_start} = time; - $args->{monsterPos} = {%{$monsterPos}}; - $args->{monsterLastMoveTime} = $target->{time_move}; + debug "Attack $char ($realMyPos->{x} $realMyPos->{y}) - target $target ($realMonsterPos->{x} $realMonsterPos->{y})\n"; + if ($canAttack == 0) { + debug "[Attack] [$range_type_string] [No range] Too far from us to attack, distance is $realMonsterDist, attack maxDistance is $args->{attackMethod}{maxDistance}\n", 'ai_attack'; - debug "Attack $char ($realMyPos->{x} $realMyPos->{y}) - target $target ($realMonsterPos->{x} $realMonsterPos->{y}) is too far from us to attack, distance is $realMonsterDist, attack maxDistance is $args->{attackMethod}{maxDistance}\n", 'ai_attack'; + } elsif ($canAttack == -1) { + debug "[Attack] [$range_type_string] [No LOS] No LOS from player to mob\n", 'ai_attack'; + } my $pos = meetingPosition($char, 1, $target, $args->{attackMethod}{maxDistance}); - my $result; - if ($pos) { debug "Attack $char ($realMyPos->{x} $realMyPos->{y}) - moving to meeting position ($pos->{x} $pos->{y})\n", 'ai_attack'; - $result = $char->route( + $args->{move_start} = time; + $args->{monsterLastMoveTime} = $target->{time_move}; + $args->{sentApproach} = 1; + + my $sendAttackWithMove = 0; + if ($config{"attackSendAttackWithMove"} && $args->{attackMethod}{type} eq "weapon") { + $sendAttackWithMove = 1; + } + + $char->route( undef, @{$pos}{qw(x y)}, maxRouteTime => $config{'attackMaxRouteTime'}, attackID => $ID, + sendAttackWithMove => $sendAttackWithMove, avoidWalls => 0, + randomFactor => 0, + useManhattan => 1, meetingSubRoute => 1, noMapRoute => 1 ); - - if (!$result) { - # Unable to calculate a route to target - $target->{attack_failed} = time; - AI::dequeue while (AI::inQueue("attack")); - message T("Unable to calculate a route to target, dropping target\n"), "ai_attack"; - if ($config{'teleportAuto_dropTarget'}) { - message T("Teleport due to dropping attack target\n"); - ai_useTeleport(1); - } - } else { - debug "Attack $char - successufully routing to $target\n", 'ai_attack'; - } - } else { - $target->{attack_failed} = time; - AI::dequeue while (AI::inQueue("attack")); - message TF("Unable to calculate a meetingPosition to target, dropping target. Check %s in config.txt\n", 'attackRouteMaxPathDistance'), "ai_attack"; - if ($config{'teleportAuto_dropTarget'}) { - message T("Teleport due to dropping attack target\n"); - ai_useTeleport(1); - } - } - - } elsif ( - # We are a ranged attacker in range without LOS - $args->{attackMethod}{maxDistance} > 1 && - $config{attackCheckLOS} && - !$field->checkLOS($realMyPos, $realMonsterPos, $config{attackCanSnipe}) - ) { - my $best_spot = meetingPosition($char, 1, $target, $args->{attackMethod}{maxDistance}); - - # Move to the closest spot - my $msg = TF("No LOS from %s (%d, %d) to target %s (%d, %d) (distance: %d)", $char, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $realMonsterDist); - if ($best_spot) { - message TF("%s; moving to (%s, %s)\n", $msg, $best_spot->{x}, $best_spot->{y}); - $char->route(undef, @{$best_spot}{qw(x y)}, noMapRoute => 1, avoidWalls => 0, LOSSubRoute => 1); } else { - warning TF("%s; no acceptable place to stand\n", $msg); - $target->{attack_failedLOS} = time; - AI::dequeue while (AI::inQueue("attack")); - } - - } elsif ( - # We are a melee attacker in range without LOS - $args->{attackMethod}{maxDistance} == 1 && - $config{attackCheckLOS} && - blockDistance($realMyPos, $realMonsterPos) == 2 && - !$field->checkLOS($realMyPos, $realMonsterPos, $config{attackCanSnipe}) - ) { - my $best_spot = meetingPosition($char, 1, $target, $args->{attackMethod}{maxDistance}); - - # Move to the closest spot - my $msg = TF("No LOS in melee from %s (%d, %d) to target %s (%d, %d) (distance: %d)", $char, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $realMonsterDist); - if ($best_spot) { - message TF("%s; moving to (%s, %s)\n", $msg, $best_spot->{x}, $best_spot->{y}); - $char->route(undef, @{$best_spot}{qw(x y)}, noMapRoute => 1, avoidWalls => 0, LOSSubRoute => 1); - } else { - warning TF("%s; no acceptable place to stand\n", $msg); - $target->{attack_failedLOS} = time; - AI::dequeue while (AI::inQueue("attack")); + message T("Unable to calculate a meetingPosition to target, dropping target\n"), "ai_attack"; + giveUp($args, $ID, 1); } + $found_action = 1; + } - } elsif ((!$config{'runFromTarget'} || $realMonsterDist >= $config{'runFromTarget_dist'}) - && (!$config{'tankMode'} || !$target->{dmgFromYou})) { + if ( + !$found_action && + (!$config{"runFromTarget"} || $realMonsterDist >= $config{"runFromTarget_dist"} || $failed_runFromTarget) && + (!$config{"tankMode"} || !$target->{dmgFromYou}) + ) { # Attack the target. In case of tanking, only attack if it hasn't been hit once. if (!$args->{firstAttack}) { $args->{firstAttack} = 1; - my $pos = "$myPos->{x},$myPos->{y}"; - debug "Ready to attack target (which is $realMonsterDist blocks away); we're at ($pos)\n", "ai_attack"; + debug "Ready to attack target $target ($realMonsterPos->{x} $realMonsterPos->{y}) ($realMonsterDist blocks away); we're at ($realMyPos->{x} $realMyPos->{y})\n", "ai_attack"; } $args->{unstuck}{time} = time if (!$args->{unstuck}{time}); @@ -624,44 +706,25 @@ sub main { $args->{unstuck}{count}++; } - if ($args->{attackMethod}{type} eq "weapon" && timeOut($timeout{ai_attack})) { + # Attack with weapon logic + if ($args->{attackMethod}{type} eq "weapon" && timeOut($timeout{ai_attack}) && timeOut($timeout{ai_attack_after_skill})) { if (Actor::Item::scanConfigAndCheck("attackEquip")) { #check if item needs to be equipped Actor::Item::scanConfigAndEquip("attackEquip"); } else { - $messageSender->sendAction($ID, - ($config{'tankMode'}) ? 0 : 7); + debug "[Attack] Sending attack target $target ($realMonsterPos->{x} $realMonsterPos->{y}) ($realMonsterDist blocks away); we're at ($realMyPos->{x} $realMyPos->{y})\n", "ai_attack"; + $messageSender->sendAction($ID, ($config{'tankMode'}) ? 0 : 7); $timeout{ai_attack}{time} = time; delete $args->{attackMethod}; - if ($config{'runFromTarget'} && $config{'runFromTarget_inAdvance'} && $realMonsterDist < $config{'runFromTarget_minStep'}) { - my $cell = meetingPosition($char, 1, $target, $args->{attackMethod}{maxDistance}, 1); - if ($cell) { - debug TF("%s kiting in advance (%d %d) to (%d %d), mob at (%d %d).\n", $char, $realMyPos->{x}, $realMyPos->{y}, $cell->{x}, $cell->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; - $args->{avoiding} = 1; - $char->move($cell->{x}, $cell->{y}, $ID); - } else { - debug TF("%s no acceptable place to kite in advance from (%d %d), mob at (%d %d).\n", $char, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; - } - } - } - } elsif ($args->{attackMethod}{type} eq "skill") { - # check if has LOS to use skill - if(!$field->checkLOS($realMyPos, $realMonsterPos, $config{attackCanSnipe})) { - my $best_spot = meetingPosition($char, 1, $target, $args->{attackMethod}{maxDistance}); - - # Move to the closest spot - my $msg = TF("No LOS in from %s (%d, %d) to target %s (%d, %d) (distance: %d)", $char, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $realMonsterDist); - if ($best_spot) { - message TF("%s; moving to (%s, %s)\n", $msg, $best_spot->{x}, $best_spot->{y}); - $char->route(undef, @{$best_spot}{qw(x y)}, noMapRoute => 1, avoidWalls => 0, LOSSubRoute => 1); - } else { - warning TF("%s; no acceptable place to stand\n", $msg); - $target->{attack_failedLOS} = time; - AI::dequeue while (AI::inQueue("attack")); + if ($config{"runFromTarget"} && $config{"runFromTarget_inAdvance"} && $realMonsterDist < $config{"runFromTarget_minStep"}) { + find_kite_position($args, 1, $target, $realMyPos, $realMonsterPos, 0); } } + $found_action = 1; + # Attack with skill logic + } elsif ($args->{attackMethod}{type} eq "skill") { my $slot = $args->{attackMethod}{skillSlot}; delete $args->{attackMethod}; @@ -683,8 +746,14 @@ sub main { ); $args->{monsterID} = $ID; my $skill_lvl = $config{"attackSkillSlot_${slot}_lvl"} || $char->getSkillLevel($skill); - debug "Auto-skill on monster ".getActorName($ID).": ".qq~$config{"attackSkillSlot_$slot"} (lvl $skill_lvl)\n~, "ai_attack"; + debug "[attackSkillSlot] Auto-skill on monster ".getActorName($ID).": ".qq~$config{"attackSkillSlot_$slot"} (lvl $skill_lvl)\n~, "ai_attack"; + # TODO: We sould probably add a runFromTarget_inAdvance logic here also, we could want to kite using skills, but only instant cast ones like double strafe I believe + $timeout{ai_attack_after_skill}{time} = time; + delete $args->{attackMethod}; + $found_action = 1; + + # Attack with combo logic } elsif ($args->{attackMethod}{type} eq "combo") { my $slot = $args->{attackMethod}{comboSlot}; my $isSelfSkill = $args->{attackMethod}{isSelfSkill}; @@ -704,13 +773,18 @@ sub main { $config{"attackComboSlot_${slot}_waitBeforeUse"}, ); $args->{monsterID} = $ID; + $found_action = 1; } - } elsif ($config{tankMode}) { + } + + if (!$found_action && $config{tankMode}) { if ($args->{dmgTo_last} != $target->{dmgTo}) { $args->{ai_attack_giveup}{time} = time; + $char->sendAttackStop; } $args->{dmgTo_last} = $target->{dmgTo}; + $found_action = 1; } Plugins::callHook('AI::Attack::main', {target => $target}) diff --git a/src/AI/CoreLogic.pm b/src/AI/CoreLogic.pm index e61f2af319..4f4fd918c9 100644 --- a/src/AI/CoreLogic.pm +++ b/src/AI/CoreLogic.pm @@ -633,6 +633,7 @@ sub processEscapeUnknownMaps { if ($config{route_escape_randomWalk} && !$skip) { #randomly search for portals... my ($randX, $randY); my $i = 500; + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? my $pos = calcPosition($char); do { if ((rand(2)+1)%2) { @@ -2345,7 +2346,10 @@ sub processRandomWalk_stopDuringSlaveAttack { my $slave = AI::SlaveManager::mustStopForAttack(); if (defined $slave) { message TF("%s started attacking during randomWalk - Stoping movement for it.\n", $slave), 'slave'; + # TODO: Since meetingposition takes into account the movement of the character + # we shoudl probably not stop it, just not send new move commands after the current one $char->sendAttackStop; + # TODO: This should probably just pause route instead of dequeuing it AI::dequeue() while (AI::is(qw/move route mapRoute/) && AI::args()->{isRandomWalk}); } } @@ -2889,7 +2893,7 @@ sub processAutoSkillUse { ##### PARTY-SKILL USE ##### sub processPartySkillUse { if (AI::isIdle || AI::is(qw(route mapRoute follow sitAuto take items_gather items_take attack move))){ - my $realMyPos = calcPosition($char); + my $realMyPos = calcPosFromPathfinding($field, $char); my %party_skill; PARTYSKILL: for (my $i = 0; exists $config{"partySkill_$i"}; $i++) { @@ -2923,11 +2927,11 @@ sub processPartySkillUse { ); my $party_skill_dist = $config{"partySkill_$i"."_dist"} || $config{partySkillDistance} || "0..8"; if (defined($config{"partySkill_$i"."_dist"}) && defined($config{"partySkill_$i"."_maxDist"})) { $party_skill_dist = $config{"partySkill_$i"."_dist"} . ".." . $config{"partySkill_$i"."_maxDist"};} - my $realActorPos = calcPosition($player); + my $realActorPos = calcPosFromPathfinding($field, $player); my $distance = blockDistance($realMyPos, $realActorPos); next unless ($party_skill{owner}{ID} eq $player->{ID} || inRange($distance, $party_skill_dist)); next unless (checkPlayerCondition("partySkill_$i"."_target", $ID)); - + $party_skill{ID} = $party_skill{skillObject}->getHandle; $party_skill{lvl} = $config{"partySkill_$i"."_lvl"} || $char->getSkillLevel($party_skill{skillObject}); $party_skill{target} = $player->{name}; @@ -2954,11 +2958,11 @@ sub processPartySkillUse { if ($char->{party}{joined} && $char->{party}{users}{$party_skill{targetID}} && $char->{party}{users}{$party_skill{targetID}}{hp}) { $hp_diff = $char->{party}{users}{$party_skill{targetID}}{hp_max} - $char->{party}{users}{$party_skill{targetID}}{hp}; - + } elsif ($char->has_mercenary && $party_skill{targetID} eq $char->{mercenary}{ID} && $char->{mercenary}{hp} && $char->{mercenary}{hp_max}) { $hp_diff = $char->{mercenary}{hp_max} - $char->{mercenary}{hp}; $modifier /= 2; - + } else { if ($players{$party_skill{targetID}}) { $hp_diff = -$players{$party_skill{targetID}}{deltaHp}; @@ -3211,23 +3215,32 @@ sub processAutoAttack { } my $control = mon_control($monster->{name}, $monster->{nameID}); - if (!AI::is(qw/sitAuto take items_gather items_take/) + next unless (!AI::is(qw/sitAuto take items_gather items_take/) && $config{'attackAuto'} >= 2 && ($control->{attack_auto} == 1 || $control->{attack_auto} == 3) && (!$config{'attackAuto_onlyWhenSafe'} || isSafe()) && !$ai_v{sitAuto_forcedBySitCommand} && $attackOnRoute >= 2 && !$monster->{dmgFromYou} - && ($control->{dist} eq '' || blockDistance($monster->{pos}, calcPosition($char)) <= $control->{dist}) - && timeOut($monster->{attack_failed}, $timeout{ai_attack_unfail}{timeout}) - && timeOut($monster->{attack_failedLOS}, $timeout{ai_attack_failedLOS}{timeout})) { - my %hookArgs; - $hookArgs{monster} = $monster; - $hookArgs{return} = 1; - Plugins::callHook('checkMonsterAutoAttack', \%hookArgs); - next if (!$hookArgs{return}); - push @cleanMonsters, $_; - } + ); + + my $myPos = calcPosition($char); + my $target_pos = calcPosition($monster); + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? + next unless ($control->{dist} eq '' || blockDistance($target_pos, $myPos) <= $control->{dist}); + + # TODO: Sometimes we had no LOS to attack mob and dropped it, but now it is following us and attacking us + # which means we now have LOS to is, it we should have a way to delete ai_attack_unfail and ai_attack_failedLOS + # timeouts in these cases. + next unless (timeOut($monster->{attack_failed}, $timeout{ai_attack_unfail}{timeout})); + next unless (timeOut($monster->{attack_failedLOS}, $timeout{ai_attack_failedLOS}{timeout})); + + my %hookArgs; + $hookArgs{monster} = $monster; + $hookArgs{return} = 1; + Plugins::callHook('checkMonsterAutoAttack', \%hookArgs); + next if (!$hookArgs{return}); + push @cleanMonsters, $_; } ### Step 2: Pick out the "best" monster ### @@ -3475,6 +3488,7 @@ sub processAutoTeleport { $timeout{ai_teleport_away}{time} = time; return; } elsif ($teleAuto < 0 && !$char->{dead}) { + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? my $pos = calcPosition($monsters{$_}); my $myPos = calcPosition($char); my $dist = blockDistance($pos, $myPos); diff --git a/src/AI/Slave.pm b/src/AI/Slave.pm index ed1c8e235f..eb8877cd99 100644 --- a/src/AI/Slave.pm +++ b/src/AI/Slave.pm @@ -239,7 +239,7 @@ sub processFollow { ) { $slave->clear('move', 'route'); if (!$field->canMove($slave->{pos_to}, $char->{pos_to})) { - $slave->route(undef, @{$char->{pos_to}}{qw(x y)}, noMapRoute => 1, avoidWalls => 0, isFollow => 1); + $slave->route(undef, @{$char->{pos_to}}{qw(x y)}, noMapRoute => 1, avoidWalls => 0, randomFactor => 0, useManhattan => 1, isFollow => 1); debug TF("%s follow route (distance: %d)\n", $slave, $slave->{master_dist}), 'slave'; } elsif (timeOut($slave->{move_retry}, 0.5)) { @@ -288,7 +288,7 @@ sub processIdleWalk { splice(@cells, $index, 1); } return unless ($walk_pos); - $slave->route(undef, @{$walk_pos}{qw(x y)}, attackOnRoute => 2, noMapRoute => 1, avoidWalls => 0, isIdleWalk => 1); + $slave->route(undef, @{$walk_pos}{qw(x y)}, attackOnRoute => 2, noMapRoute => 1, avoidWalls => 0, randomFactor => 0, useManhattan => 1, isIdleWalk => 1); debug TF("%s IdleWalk route\n", $slave), 'slave'; } } @@ -426,11 +426,12 @@ sub processAutoAttack { $attackOnRoute = 2; } - ### Step 1: Generate a list of all monsters that we are allowed to attack. ### - my @aggressives; - my @partyMonsters; - my @cleanMonsters; - my $myPos = calcPosition($slave); + ### Step 1: Generate a list of all monsters that we are allowed to attack. ### + my @aggressives; + my @partyMonsters; + my @cleanMonsters; + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? + my $myPos = calcPosition($slave); # List aggressive monsters my $party = $config{$slave->{configPrefix}.'attackAuto_party'} ? 1 : 0; @@ -444,10 +445,11 @@ sub processAutoAttack { # Never attack monsters that we failed to get LOS with next if (!timeOut($monster->{attack_failedLOS}, $timeout{ai_attack_failedLOS}{timeout})); - my $pos = calcPosition($monster); - my $master_pos = $char->position; - - next if (blockDistance($master_pos, $pos) > ($config{$slave->{configPrefix}.'followDistanceMax'} + $config{$slave->{configPrefix}.'attackMaxDistance'})); + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? + my $target_pos = calcPosition($monster); + my $master_pos = $char->position; + + next if (blockDistance($master_pos, $target_pos) > ($config{$slave->{configPrefix}.'followDistanceMax'} + $config{$slave->{configPrefix}.'attackMaxDistance'})); # List monsters that master and other slaves are attacking if ( @@ -495,9 +497,10 @@ sub processAutoAttack { if ($config{$slave->{configPrefix}.'attackAuto'} >= 2 && ($control->{attack_auto} == 1 || $control->{attack_auto} == 3) && $attackOnRoute >= 2 && $safe - && !positionNearPlayer($pos, $playerDist) && !positionNearPortal($pos, $portalDist) + && !positionNearPlayer($target_pos, $playerDist) && !positionNearPortal($target_pos, $portalDist) && !$monster->{dmgFromYou} - && timeOut($monster->{$slave->{ai_attack_failed_timeout}}, $timeout{ai_attack_unfail}{timeout})) { + && timeOut($monster->{$slave->{ai_attack_failed_timeout}}, $timeout{ai_attack_unfail}{timeout}) + ) { push @cleanMonsters, $_; } } diff --git a/src/AI/SlaveAttack.pm b/src/AI/SlaveAttack.pm index c02b97d54c..e72843614b 100644 --- a/src/AI/SlaveAttack.pm +++ b/src/AI/SlaveAttack.pm @@ -16,6 +16,11 @@ use Skill; use Utils; use Utils::PathFinding; +use constant { + MOVING_TO_ATTACK => 1, + ATTACKING => 2, +}; + use AI::Slave; use AI::Slave::Homunculus; use AI::Slave::Mercenary; @@ -25,16 +30,14 @@ use AI::Slave::Mercenary; sub process { my $slave = shift; - if ( - ($slave->action eq "attack" && $slave->args->{ID}) - || ($slave->action eq "route" && $slave->action (1) eq "attack" && $slave->args->{attackID}) - || ($slave->action eq "move" && $slave->action (2) eq "attack" && $slave->args->{attackID}) - ) { + if (shouldAttack($slave, $slave->action, $slave->args)) { my $ID; my $ataqArgs; + my $stage; # 1 - moving to attack | 2 - attacking if ($slave->action eq "attack") { $ID = $slave->args->{ID}; $ataqArgs = $slave->args(0); + $stage = ATTACKING; } else { if ($slave->action(1) eq "attack") { $ataqArgs = $slave->args(1); @@ -43,126 +46,169 @@ sub process { $ataqArgs = $slave->args(2); } $ID = $slave->args->{attackID}; + $stage = MOVING_TO_ATTACK; } if (targetGone($slave, $ataqArgs, $ID)) { finishAttacking($slave, $ataqArgs, $ID); return; } elsif (shouldGiveUp($slave, $ataqArgs, $ID)) { - giveUp($slave, $ataqArgs, $ID); + giveUp($slave, $ataqArgs, $ID, 0); return; } my $target = Actor::get($ID); - if ($target) { - my $party = $config{$slave->{configPrefix}.'attackAuto_party'} ? 1 : 0; - my $target_is_aggressive = is_aggressive_slave($slave, $target, undef, 0, $party); - my @aggressives = ai_slave_getAggressives($slave, 0, $party); - if ($config{$slave->{configPrefix}.'attackChangeTarget'} && !$target_is_aggressive && @aggressives) { - my $attackTarget = getBestTarget(\@aggressives, $config{$slave->{configPrefix}.'attackCheckLOS'}, $config{$slave->{configPrefix}.'attackCanSnipe'}); - if ($attackTarget) { - $slave->sendAttackStop; - $slave->dequeue while ($slave->inQueue("attack")); - $slave->setSuspend(0); - my $new_target = Actor::get($attackTarget); - warning TF("%s target is not aggressive: %s, changing target to aggressive: %s.\n", $slave, $target, $new_target), 'slave_attack'; - $slave->attack($attackTarget); - AI::SlaveAttack::process($slave); - return; - } + unless ($target && $target->{type} ne 'Unknown') { + finishAttacking($slave, $ataqArgs, $ID); + return; + } + my $party = $config{$slave->{configPrefix}.'attackAuto_party'} ? 1 : 0; + my $target_is_aggressive = is_aggressive_slave($slave, $target, undef, 0, $party); + my @aggressives = ai_slave_getAggressives($slave, 0, $party); + if ($config{$slave->{configPrefix}.'attackChangeTarget'} && !$target_is_aggressive && @aggressives) { + my $attackTarget = getBestTarget(\@aggressives, $config{$slave->{configPrefix}.'attackCheckLOS'}, $config{$slave->{configPrefix}.'attackCanSnipe'}); + if ($attackTarget) { + $slave->sendAttackStop; + $slave->dequeue while ($slave->inQueue("attack")); + $slave->setSuspend(0); + my $new_target = Actor::get($attackTarget); + warning TF("%s target is not aggressive: %s, changing target to aggressive: %s.\n", $slave, $target, $new_target), 'slave_attack'; + $slave->attack($attackTarget); + AI::SlaveAttack::process($slave); + return; } } - } - - if ($slave->action eq "attack" && $slave->args->{suspended}) { - $slave->args->{ai_attack_giveup}{time} += time - $slave->args->{suspended}; - delete $slave->args->{suspended}; - } - if ($slave->action eq "attack" && $slave->args->{move_start}) { - # We've just finished moving to the monster. - # Don't count the time we spent on moving - $slave->args->{ai_attack_giveup}{time} += time - $slave->args->{move_start}; - undef $slave->args->{unstuck}{time}; - undef $slave->args->{move_start}; - - } elsif ($slave->action eq "attack" && $slave->args->{avoiding} && $slave->args->{ID}) { - my $ID = $slave->args->{ID}; - my $target = Actor::get($ID); - $slave->args->{ai_attack_giveup}{time} = time; - undef $slave->args->{avoiding}; - debug "$slave finished avoiding movement from target $target, updating ai_attack_giveup\n", 'slave_attack'; - - } elsif ((($slave->action eq "route" && $slave->action (1) eq "attack") || ($slave->action eq "move" && $slave->action (2) eq "attack")) - && $slave->args->{attackID} && timeOut($timeout{$slave->{ai_route_adjust_timeout}})) { - # We're on route to the monster; check whether the monster has moved - my $ID = $slave->args->{attackID}; - my $attackSeq = ($slave->action eq "route") ? $slave->args (1) : $slave->args (2); - my $target = Actor::get($ID); - my $realMyPos = calcPosition($slave); - my $realMonsterPos = calcPosition($target); - - if ( - $target->{type} ne 'Unknown' && - $attackSeq->{monsterPos} && - %{$attackSeq->{monsterPos}} && - $attackSeq->{monsterLastMoveTime} && - $attackSeq->{monsterLastMoveTime} != $target->{time_move} - ) { - # Monster has moved; stop moving and let the attack AI readjust route - debug "$slave target $target has moved since we started routing to it - Adjusting route\n", 'slave_attack'; - $slave->dequeue while ($slave->is("move", "route")); - - $attackSeq->{ai_attack_giveup}{time} = time; - - } elsif ( - $target->{type} ne 'Unknown' && - $attackSeq->{monsterPos} && - %{$attackSeq->{monsterPos}} && - $attackSeq->{monsterLastMoveTime} && - $attackSeq->{attackMethod}{maxDistance} == 1 && - canReachMeleeAttack($realMyPos, $realMonsterPos) && - (blockDistance($realMyPos, $realMonsterPos) < 2 || !$config{$slave->{configPrefix}.'attackCheckLOS'} ||($config{$slave->{configPrefix}.'attackCheckLOS'} && blockDistance($realMyPos, $realMonsterPos) == 2 && $field->checkLOS($realMyPos, $realMonsterPos, $config{$slave->{configPrefix}.'attackCanSnipe'}))) - ) { - debug "$slave target $target is now reachable by melee attacks during routing to it.\n", 'slave_attack'; - $slave->dequeue while ($slave->is("move", "route")); + my $cleanMonster = slave_checkMonsterCleanness($slave, $ID); + if (!$cleanMonster) { + message TF("%s dropping target %s - will not kill steal others\n", $slave, $target), 'slave_attack'; + $slave->sendAttackStop; + $target->{slave_ignore} = 1; + $slave->dequeue while ($slave->inQueue("attack")); - $attackSeq->{ai_attack_giveup}{time} = time; + if ($config{$slave->{configPrefix}.'teleportAuto_dropTargetKS'}) { + message TF("Teleport due to dropping %s attack target\n", $slave), 'teleport'; + ai_useTeleport(1); + } + return; } + + if ($stage == MOVING_TO_ATTACK) { + # Check for hidden monsters + if (($target->{statuses}->{EFFECTSTATE_BURROW} || $target->{statuses}->{EFFECTSTATE_HIDING}) && $config{avoidHiddenMonsters}) { + message TF("Slave %s Dropping target %s - will not attack hidden monsters\n", $slave, $target), 'ai_attack'; + $slave->sendAttackStop; + $target->{ignore} = 1; - $timeout{$slave->{ai_route_adjust_timeout}}{time} = time; - } - - if ($slave->action eq "attack" && timeOut($slave->args->{attackMainTimeout}, 0.1)) { - $slave->args->{attackMainTimeout} = time; - main($slave); - } + $slave->dequeue while ($slave->inQueue("attack")); + if ($config{teleportAuto_dropTargetHidden}) { + message T("Teleport due to dropping hidden target\n"); + ai_useTeleport(1); + } + return; + } + + # We're on route to the monster; check whether the monster has moved + if ($slave->args->{attackID} && timeOut($timeout{$slave->{ai_route_adjust_timeout}})) { + my $reset = 0; + if ($target->{type} ne 'Unknown') { + # Monster has moved; stop moving and let the attack AI readjust route + if ( + $ataqArgs->{monsterLastMoveTime} && + $ataqArgs->{monsterLastMoveTime} != $target->{time_move} + ) { + if ( + ($slave->args->{monsterLastMovePosTo}{x} == $target->{pos_to}{x} && $slave->args->{monsterLastMovePosTo}{y} == $target->{pos_to}{y}) + ) { + $slave->args->{monsterLastMoveTime} = $target->{time_move}; + $slave->args->{monsterLastMovePosTo}{x} = $target->{pos_to}{x}; + $slave->args->{monsterLastMovePosTo}{y} = $target->{pos_to}{y}; + } else { + debug "$slave target $target has moved since we started routing to it - Adjusting route\n", 'slave_attack'; + $reset = 1; + } - # Check for kill steal while moving - if (($slave->is("move", "route") && $slave->args->{attackID} && $slave->inQueue("attack") - && timeOut($slave->args->{movingWhileAttackingTimeout}, 0.2))) { + # Master has moved; stop moving and let the attack AI readjust route + } elsif ( + $ataqArgs->{masterLastMoveTime} && + $ataqArgs->{masterLastMoveTime} != $char->{time_move} + ) { + if ( + ($slave->args->{masterLastMovePosTo}{x} == $char->{pos_to}{x} && $slave->args->{masterLastMovePosTo}{y} == $char->{pos_to}{y}) + ) { + $slave->args->{masterLastMoveTime} = $char->{time_move}; + $slave->args->{masterLastMovePosTo}{x} = $char->{pos_to}{x}; + $slave->args->{masterLastMovePosTo}{y} = $char->{pos_to}{y}; + } else { + debug "$slave master $char has moved since we started routing to target $target - Adjusting route\n", 'slave_attack'; + $reset = 1; + } + } + if ($reset) { + $slave->dequeue while ($slave->is("move", "route")); + $ataqArgs->{ai_attack_giveup}{time} = time; + $ataqArgs->{sentApproach} = 0; + undef $slave->args->{unstuck}{time}; + undef $slave->args->{avoiding}; + undef $slave->args->{move_start}; + } + } + + $timeout{$slave->{ai_route_adjust_timeout}}{time} = time; + } + } - my $ID = $slave->args->{attackID}; - my $monster = $monsters{$ID}; + if ($stage == ATTACKING) { + if ($slave->args->{suspended}) { + $slave->args->{ai_attack_giveup}{time} += time - $slave->args->{suspended}; + delete $slave->args->{suspended}; + + # We've just finished moving to the monster. + # Don't count the time we spent on moving + } elsif ($slave->args->{move_start}) { + $slave->args->{ai_attack_giveup}{time} += time - $slave->args->{move_start}; + undef $slave->args->{unstuck}{time}; + undef $slave->args->{move_start}; + + } elsif ($slave->args->{avoiding}) { + $slave->args->{ai_attack_giveup}{time} = time; + undef $slave->args->{avoiding}; + debug "$slave finished avoiding movement from target $target, updating ai_attack_giveup\n", 'slave_attack'; + } - # Check for kill steal while moving - if ($monster && !Misc::slave_checkMonsterCleanness($slave, $ID)) { - dropTargetWhileMoving($slave); + if (timeOut($timeout{$slave->{ai_attack_main}})) { + main($slave); + $timeout{$slave->{ai_attack_main}}{time} = time; + } } - - $slave->args->{movingWhileAttackingTimeout} = time; } } +sub shouldAttack { + my ($slave, $action, $args) = @_; + return ( + ($slave->action eq "attack" && $slave->args->{ID}) || + ($slave->action eq "route" && $slave->action (1) eq "attack" && $slave->args->{attackID}) || + ($slave->action eq "move" && $slave->action (2) eq "attack" && $slave->args->{attackID}) + ); +} + sub shouldGiveUp { my ($slave, $args, $ID) = @_; return !$config{$slave->{configPrefix}.'attackNoGiveup'} && (timeOut($args->{ai_attack_giveup}) || $args->{unstuck}{count} > 5) } sub giveUp { - my ($slave, $args, $ID) = @_; + my ($slave, $args, $ID, $LOS) = @_; my $target = Actor::get($ID); - $target->{$slave->{ai_attack_failed_timeout}} = time if $monsters{$ID}; + if ($monsters{$ID}) { + if ($LOS) { + $target->{attack_failedLOS} = time; + } else { + $target->{$slave->{ai_attack_failed_timeout}} = time; + } + } + $target->{dmgFromPlayer}{$slave->{ID}} = 0; # Hack | TODO: Fix me $slave->dequeue while ($slave->inQueue("attack")); message TF("%s can't reach or damage target, dropping target\n", $slave), 'slave_attack'; if ($config{$slave->{configPrefix}.'teleportAuto_dropTarget'}) { @@ -220,25 +266,52 @@ sub finishAttacking { ID => $ID, slave => $slave }) - } -sub dropTargetWhileMoving { - my $slave = shift; - my $ID = $slave->args->{attackID}; - my $target = Actor::get($ID); - message TF("%s dropping target %s - will not kill steal others\n", $slave, $target), 'slave_attack'; - $slave->sendAttackStop; - $target->{slave_ignore} = 1; - - # Right now, the queue is either - # move, route, attack - # -or- - # route, attack - $slave->dequeue while ($slave->inQueue("attack")); - if ($config{$slave->{configPrefix}.'teleportAuto_dropTargetKS'}) { - message TF("Teleport due to dropping %s attack target\n", $slave), 'teleport'; - ai_useTeleport(1); +sub find_kite_position { + my ($slave, $args, $inAdvance, $target, $realMyPos, $realMonsterPos, $noAttackMethodFallback_runFromTarget) = @_; + + my $maxDistance; + if (!$noAttackMethodFallback_runFromTarget && defined $args->{attackMethod}{type} && defined $args->{attackMethod}{maxDistance}) { + $maxDistance = $args->{attackMethod}{maxDistance}; + } elsif ($noAttackMethodFallback_runFromTarget) { + $maxDistance = $config{$slave->{configPrefix}.'runFromTarget_noAttackMethodFallback_attackMaxDist'}; + } else { + # Should never happen. + return 0; + } + + # We try to find a position to kite from at least runFromTarget_minStep away from the target but at maximun {attackMethod}{maxDistance} away from it + my $pos = meetingPosition($slave, 2, $target, $maxDistance, ($noAttackMethodFallback_runFromTarget ? 2 : 1)); + if ($pos) { + if ($inAdvance) { + debug TF("[%s] [runFromTarget_inAdvance] kiting in advance (%d %d) to (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $pos->{x}, $pos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + } elsif ($noAttackMethodFallback_runFromTarget) { + debug TF("[%s] [runFromTarget_noAttackMethodFallback] kiting in advance (%d %d) to (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $pos->{x}, $pos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + } else { + debug TF("[%s] [runFromTarget] (attackmaxDistance %s) kiteing from (%d %d) to (%d %d), mob at (%d %d).\n", $slave, $maxDistance, $realMyPos->{x}, $realMyPos->{y}, $pos->{x}, $pos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + } + $args->{avoiding} = 1; + $slave->route( + undef, + @{$pos}{qw(x y)}, + noMapRoute => 1, + avoidWalls => 0, + randomFactor => 0, + useManhattan => 1, + runFromTarget => 1 + ); + return 1; + + } else { + if ($inAdvance) { + debug TF("[%s] [runFromTarget_inAdvance] no acceptable place to kite in advance from (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + } elsif ($noAttackMethodFallback_runFromTarget) { + debug TF("[%s] [runFromTarget_noAttackMethodFallback] no acceptable place to kite from (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + } else { + debug TF("[%s] [runFromTarget] no acceptable place to kite from (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'ai_attack'; + } + return 0; } } @@ -255,13 +328,32 @@ sub main { my $monsterPos = $target->{pos_to}; my $monsterDist = blockDistance($myPos, $monsterPos); - my ($realMyPos, $realMonsterPos, $realMonsterDist, $hitYou); - my $realMyPos = calcPosition($slave); - my $realMonsterPos = calcPosition($target); + my $realMyPos = calcPosFromPathfinding($field, $slave); + my $realMonsterPos = calcPosFromPathfinding($field, $target); + my $realMonsterDist = blockDistance($realMyPos, $realMonsterPos); - - my $cleanMonster = slave_checkMonsterCleanness($slave, $ID); - + my $clientDist = getClientDist($realMyPos, $realMonsterPos); + + #my $realMasterPos = calcPosFromPathfinding($field, $char); + #my $realMasterDistToSlave = blockDistance($realMasterPos, $realMyPos); + #my $realMasterDistToTarget = blockDistance($realMasterPos, $realMonsterPos); + + if (!exists $args->{first_run}) { + $args->{first_run} = 1; + } elsif ($args->{first_run} == 1) { + $args->{first_run} = 0; + } + + #my $failed_to_attack_packet_recv = 0; + + if (!exists $args->{temporary_extra_range} || !defined $args->{temporary_extra_range}) { + $args->{temporary_extra_range} = 0; + } + + #if (exists $target->{movetoattack_pos} && exists $char->{movetoattack_pos}) { + # $failed_to_attack_packet_recv = 1; + # $args->{temporary_extra_range} = 0; + #} # If the damage numbers have changed, update the giveup time so we don't timeout if ($args->{dmgToYou_last} != $target->{dmgToPlayer}{$slave->{ID}} @@ -270,213 +362,282 @@ sub main { $args->{ai_attack_giveup}{time} = time; debug "Update slave attack giveup time\n", 'slave_attack', 2; } - $hitYou = ($args->{dmgToYou_last} != $target->{dmgToPlayer}{$slave->{ID}} - || $args->{missedYou_last} != $target->{missedToPlayer}{$slave->{ID}}); + + my $hitYou = ($args->{dmgToYou_last} != $target->{dmgToPlayer}{$slave->{ID}} || $args->{missedYou_last} != $target->{missedToPlayer}{$slave->{ID}}); + my $youHitTarget = ($args->{dmgFromYou_last} != $target->{dmgFromPlayer}{$slave->{ID}}); + + # Hack - TODO: Fix me - If the homunculus dies trying to kill a monster and is resurrected still next to that monster it will think that it is still hitting the mob, this avoids that behaviour + if ($youHitTarget && $args->{first_run}) { + $youHitTarget = 0; + } + $args->{dmgToYou_last} = $target->{dmgToPlayer}{$slave->{ID}}; $args->{missedYou_last} = $target->{missedToPlayer}{$slave->{ID}}; $args->{dmgFromYou_last} = $target->{dmgFromPlayer}{$slave->{ID}}; $args->{missedFromYou_last} = $target->{missedFromPlayer}{$slave->{ID}}; - - $args->{attackMethod}{type} = "weapon"; - + + delete $args->{attackMethod}; + # $target->{dmgFromPlayer}{$slave->{ID}} - $target->{dmgTo} + # $target->{dmgFromPlayer}{$slave->{ID}} - $target->{dmgFromYou} + ### attackSkillSlot begin for (my ($i, $prefix) = (0, 'attackSkillSlot_0'); $prefix = "attackSkillSlot_$i" and exists $config{$prefix}; $i++) { next unless $config{$prefix}; - if (checkSelfCondition($prefix) && checkMonsterCondition("${prefix}_target", $target)) { - my $skill = new Skill(auto => $config{$prefix}); - next unless $slave->checkSkillOwnership ($skill); - - next if $config{"${prefix}_maxUses"} && $target->{skillUses}{$skill->getHandle()} >= $config{"${prefix}_maxUses"}; - next if $config{"${prefix}_target"} && !existsInList($config{"${prefix}_target"}, $target->{name}); - - # Donno if $char->getSkillLevel is the right place to look at. - # my $lvl = $config{"${prefix}_lvl"} || $char->getSkillLevel($party_skill{skillObject}); - my $lvl = $config{"${prefix}_lvl"}; - my $maxCastTime = $config{"${prefix}_maxCastTime"}; - my $minCastTime = $config{"${prefix}_minCastTime"}; - debug "Slave attackSkillSlot on $target->{name} ($target->{binID}): ".$skill->getName()." (lvl $lvl)\n", "monsterSkill"; - my $skillTarget = $config{"${prefix}_isSelfSkill"} ? $slave : $target; - AI::ai_skillUse2($skill, $lvl, $maxCastTime, $minCastTime, $skillTarget, $prefix); - $ai_v{$prefix . "_time"} = time; - $ai_v{$prefix . "_target_time"}{$ID} = time; - last; - } + + next unless (checkSelfCondition($prefix)); + + next unless (checkMonsterCondition("${prefix}_target", $target)); + + my $skill = new Skill(auto => $config{$prefix}); + next unless $slave->checkSkillOwnership ($skill); + + next if $config{"${prefix}_maxUses"} && $target->{skillUses}{$skill->getHandle()} >= $config{"${prefix}_maxUses"}; + + next unless (!$config{"${prefix}_maxAttempts"} || $args->{attackSkillSlot_attempts}{$i} < $config{"${prefix}_maxAttempts"}); + + next unless (!$config{"${prefix}_monsters"} || existsInList($config{"${prefix}_monsters"}, $target->{'name'}) || existsInList($config{"${prefix}_monsters"}, $target->{nameID})); + + next unless (!$config{"${prefix}_notMonsters"} || !(existsInList($config{"${prefix}_notMonsters"}, $target->{'name'}) || existsInList($config{"${prefix}_notMonsters"}, $target->{nameID}))); + + next unless (!$config{"${prefix}_previousDamage"} || inRange($target->{dmgTo}, $config{"${prefix}_previousDamage"})); + + $args->{attackSkillSlot_attempts}{$i}++; + $args->{attackMethod}{distance} = $config{"${prefix}_dist"}; + $args->{attackMethod}{maxDistance} = $config{"${prefix}_maxDist"} || $config{"${prefix}_dist"}; + $args->{attackMethod}{type} = "skill"; + $args->{attackMethod}{skillSlot} = $i; + last; } ### attackSkillSlot end - - $args->{attackMethod}{maxDistance} = $config{$slave->{configPrefix}.'attackMaxDistance'}; - $args->{attackMethod}{distance} = ($config{$slave->{configPrefix}.'runFromTarget'} && $config{$slave->{configPrefix}.'runFromTarget_dist'} > $config{$slave->{configPrefix}.'attackDistance'}) ? $config{$slave->{configPrefix}.'runFromTarget_dist'} : $config{$slave->{configPrefix}.'attackDistance'}; + + if (!$args->{attackMethod}{type}) { + if ($config{$slave->{configPrefix}.'attackUseWeapon'}) { + $args->{attackMethod}{type} = "weapon"; + $args->{attackMethod}{distance} = $config{$slave->{configPrefix}.'attackDistance'}; + $args->{attackMethod}{maxDistance} = $config{$slave->{configPrefix}.'attackMaxDistance'}; + } else { + undef $args->{attackMethod}{type}; + $args->{attackMethod}{distance} = 1; + $args->{attackMethod}{maxDistance} = 1; + } + } + if ($args->{attackMethod}{maxDistance} < $args->{attackMethod}{distance}) { $args->{attackMethod}{maxDistance} = $args->{attackMethod}{distance}; } + my $melee; + my $ranged; if (defined $args->{attackMethod}{type} && exists $args->{ai_attack_failed_give_up} && defined $args->{ai_attack_failed_give_up}{time}) { + debug "[Slave $slave] Deleting ai_attack_failed_give_up time.\n"; delete $args->{ai_attack_failed_give_up}{time}; - } + + } elsif ($args->{attackMethod}{maxDistance} == 1) { + $melee = 1; - if (!$cleanMonster) { - # Drop target if it's already attacked by someone else - $target->{$slave->{ai_attack_failed_timeout}} = time if $monsters{$ID}; - message TF("%s dropping target %s - will not kill steal others\n", $slave, $target), 'slave_attack'; - $slave->sendMove ($realMyPos->{x}, $realMyPos->{y}); - $slave->dequeue while ($slave->inQueue("attack")); - if ($config{$slave->{configPrefix}.'teleportAuto_dropTargetKS'}) { - message TF("Teleport due to dropping %s attack target\n", $slave), 'teleport'; - ai_useTeleport(1); + } elsif ($args->{attackMethod}{maxDistance} > 1) { + $ranged = 1; + } + + #$args->{attackMethod}{maxDistance} += $args->{temporary_extra_range}; + + # -2: undefined attackMethod + # -1: No LOS + # 0: out of range + # 1: sucess + my $canAttack = -2; + if (defined $args->{attackMethod}{type} && defined $args->{attackMethod}{maxDistance}) { + $canAttack = canAttack($field, $realMyPos, $realMonsterPos, $config{$slave->{configPrefix}.'attackCanSnipe'}, $args->{attackMethod}{maxDistance}, $config{clientSight}); + } else { + $canAttack = -2; + } + + my $range_type_string = ($melee ? "Melee" : ($ranged ? "Ranged" : "None")); + my $canAttack_fail_string = (($canAttack == -2) ? "No Method" : (($canAttack == -1) ? "No LOS" : (($canAttack == 0) ? "No Range" : "OK"))); + + # Here we check if the monster which we are waiting to get closer to us is in fact close enough + # If it is close enough delete the ai_attack_failed_waitForAgressive_give_up keys and loop attack logic + if ( + $config{$slave->{configPrefix}."attackBeyondMaxDistance_waitForAgressive"} + && $target->{dmgFromPlayer}{$slave->{ID}} > 0 + && $canAttack == 1 + && exists $args->{ai_attack_failed_waitForAgressive_give_up} + && defined $args->{ai_attack_failed_waitForAgressive_give_up}{time} + ) { + debug "[Slave $slave] Deleting ai_attack_failed_waitForAgressive_give_up time.\n"; + delete $args->{ai_attack_failed_waitForAgressive_give_up}{time}; + } + + # Here we check if we have finished moving to the meeting position to attack our target, only checks this if attackWaitApproachFinish is set to 1 in config + # If so sets sentApproach to 0 + if ( + $config{$slave->{configPrefix}."attackWaitApproachFinish"} && + ($canAttack == 0 || $canAttack == -1) && + $args->{sentApproach} + ) { + if (!timeOut($slave->{time_move}, $slave->{time_move_calc})) { + debug TF("[Slave] [Out of Range - Still Approaching - Waiting] %s (%d %d), target %s (%d %d), distance %d, maxDistance %d.\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $realMonsterDist, $args->{attackMethod}{maxDistance}), 'ai_attack'; + return; + } else { + debug TF("[Slave] [Out of Range - Ended Approaching] %s (%d %d), target %s (%d %d), distance %d, maxDistance %d.\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $realMonsterDist, $args->{attackMethod}{maxDistance}), 'ai_attack'; + $args->{sentApproach} = 0; } + } + + my $found_action = 0; + my $failed_runFromTarget = 0; + my $hitTarget_when_not_possible = 0; - } elsif ($config{$slave->{configPrefix}.'runFromTarget'} && ($realMonsterDist < $config{$slave->{configPrefix}.'runFromTarget_dist'} || $hitYou)) { - my $cell = meetingPosition($slave, 2, $target, $args->{attackMethod}{maxDistance}, 1); - if ($cell) { - debug TF("[runFromTarget] %s kiteing from (%d %d) to (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $cell->{x}, $cell->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'slave_attack'; - $slave->args->{avoiding} = 1; - $slave->route(undef, @{$cell}{qw(x y)}, noMapRoute => 1, avoidWalls => 0, runFromTarget => 1); + # Here, if runFromTarget is active, we check if the target mob is closer to us than the minimun distance specified in runFromTarget_dist + # If so try to kite it + if ( + !$found_action && + $config{$slave->{configPrefix}."runFromTarget"} && + $realMonsterDist < $config{$slave->{configPrefix}."runFromTarget_dist"} + ) { + my $try_runFromTarget = find_kite_position($slave, $args, 0, $target, $realMyPos, $realMonsterPos, 0); + if ($try_runFromTarget) { + $found_action = 1; } else { - debug TF("%s no acceptable place to kite from (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'slave_attack'; + $failed_runFromTarget = 1; } - - if (!$cell) { - my $max = $args->{attackMethod}{maxDistance} + 4; - if ($max > 14) { - $max = 14; - } - $cell = meetingPosition($slave, 2, $target, $max, 1); - if ($cell) { - debug TF("[runFromTarget] %s kiteing from (%d %d) to (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $cell->{x}, $cell->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'slave_attack'; - $args->{avoiding} = 1; - $slave->route(undef, @{$cell}{qw(x y)}, noMapRoute => 1, avoidWalls => 0, runFromTarget => 1); - } else { - debug TF("%s no acceptable place to kite from (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'slave_attack'; - } + } + + # Here, if runFromTarget is active, and we can't attack right now (eg. all skills in cooldown) we check if the target mob is closer to us than the minimun distance specified in runFromTarget_noAttackMethodFallback_minStep + # If so try to kite it using maxdistance of runFromTarget_noAttackMethodFallback_attackMaxDist + if ( + !$found_action && + $canAttack == -2 && + #$config{"runFromTarget"} && + $config{$slave->{configPrefix}."runFromTarget_noAttackMethodFallback"} && + $realMonsterDist < $config{$slave->{configPrefix}."runFromTarget_noAttackMethodFallback_minStep"} + ) { + my $try_runFromTarget = find_kite_position($slave, $args, 0, $target, $realMyPos, $realMonsterPos, 1); + if ($try_runFromTarget) { + $found_action = 1; } + } - - } elsif(!defined $args->{attackMethod}{type}) { - debug T("Can't determine a attackMethod (check attackUseWeapon and Skills blocks)\n"), 'slave_attack'; + if ( + !$found_action && + $canAttack == -2 + ) { + debug T("[Slave $slave] Can't determine a attackMethod (check attackUseWeapon and Skills blocks)\n"), "ai_attack"; $args->{ai_attack_failed_give_up}{timeout} = 6 if !$args->{ai_attack_failed_give_up}{timeout}; $args->{ai_attack_failed_give_up}{time} = time if !$args->{ai_attack_failed_give_up}{time}; if (timeOut($args->{ai_attack_failed_give_up})) { delete $args->{ai_attack_failed_give_up}{time}; - message T("$slave unable to determine a attackMethod (check attackUseWeapon and Skills blocks)\n"), 'slave_attack'; - giveUp($slave, $args, $ID); + warning T("[$slave] Unable to determine a attackMethod (check attackUseWeapon and Skills blocks), dropping target.\n"), "ai_attack"; + $found_action = 1; + giveUp($args, $ID, 0); } - - - } elsif ( - # We are out of range, but already hit enemy, should wait for him in a safe place instead of going after him - # Example at https://youtu.be/kTRk5Na1aCQ?t=25 in which this check did not exist, we tried getting closer intead of waiting and got hit - ($args->{attackMethod}{maxDistance} > 1 && $realMonsterDist > $args->{attackMethod}{maxDistance}) && - #(!$config{$slave->{configPrefix}.'attackCheckLOS'} || $field->checkLOS($realMyPos, $realMonsterPos, $config{$slave->{configPrefix}.'attackCanSnipe'})) && # Is this check needed? + } + + if ($canAttack == 0 && $youHitTarget) { + debug TF("[%s] [%s - %s] We were able to hit target even though it is out of range or LOS, accepting and continuing. (you (%d %d), target %s (%d %d) [(%d %d) -> (%d %d)], distance %d, maxDistance %d)\n", $slave, $canAttack_fail_string, $range_type_string, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $target->{pos}{x}, $target->{pos}{y}, $target->{pos_to}{x}, $target->{pos_to}{y}, $realMonsterDist, $args->{attackMethod}{maxDistance}), 'ai_attack'; + if ($clientDist > $args->{attackMethod}{maxDistance} && $clientDist <= ($args->{attackMethod}{maxDistance} + 1) && $args->{temporary_extra_range} == 0) { + debug TF("[$canAttack_fail_string] Probably extra range provided by the server due to chasing, increasing range by 1.\n"), 'ai_attack'; + $args->{temporary_extra_range} = 1; + $args->{attackMethod}{maxDistance} += $args->{temporary_extra_range}; + $canAttack = canAttack($field, $realMyPos, $realMonsterPos, $config{$slave->{configPrefix}."attackCanSnipe"}, $args->{attackMethod}{maxDistance}, $config{clientSight}); + } else { + debug TF("[%s] [%s] Reason unknown, allowing once.\n", $slave, $canAttack_fail_string), 'ai_attack'; + $hitTarget_when_not_possible = 1; + } + if ( + $config{$slave->{configPrefix}."attackBeyondMaxDistance_waitForAgressive"} && + exists $args->{ai_attack_failed_waitForAgressive_give_up} && + defined $args->{ai_attack_failed_waitForAgressive_give_up}{time} + ) { + debug TF("[%s] [Accepting] Deleting ai_attack_failed_waitForAgressive_give_up time.\n", $slave), 'ai_attack'; + delete $args->{ai_attack_failed_waitForAgressive_give_up}{time};; + } + } + + # Here we decide what to do when a mob we have already hit is no longer in range or we have no LOS to it + # We also check if we have waited too long for the monster which we are waiting to get closer to us to approach + # TODO: Maybe we should separate this into 2 sections, one for out of range and another for no LOS - low priority + if ( + !$found_action && $config{$slave->{configPrefix}."attackBeyondMaxDistance_waitForAgressive"} && - $target->{dmgFromPlayer}{$slave->{ID}} > 0 + $target->{dmgFromPlayer}{$slave->{ID}} > 0 && + ($canAttack == 0 || $canAttack == -1) && + !$hitTarget_when_not_possible ) { $args->{ai_attack_failed_waitForAgressive_give_up}{timeout} = 6 if !$args->{ai_attack_failed_waitForAgressive_give_up}{timeout}; $args->{ai_attack_failed_waitForAgressive_give_up}{time} = time if !$args->{ai_attack_failed_waitForAgressive_give_up}{time}; - if (timeOut($args->{ai_attack_failed_waitForAgressive_give_up})) { delete $args->{ai_attack_failed_waitForAgressive_give_up}{time}; - message T("[Out of Range] $slave waited too long for target to get closer, dropping target\n"), 'slave_attack'; - giveUp($slave, $args, $ID); + warning TF("[%s] [%s - %s] Waited too long for target to get closer, dropping target. (you (%d %d), target %s (%d %d) [(%d %d) -> (%d %d)], distance %d, maxDistance %d)\n", $slave, $canAttack_fail_string, $range_type_string, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $target->{pos}{x}, $target->{pos}{y}, $target->{pos_to}{x}, $target->{pos_to}{y}, $realMonsterDist, $args->{attackMethod}{maxDistance}), 'ai_attack'; + giveUp($slave, $args, $ID, 0); } else { - warning TF("[Out of Range - Waiting] %s (%d %d), target %s (%d %d), distance %d, maxDistance %d, dmgFromYou %d.\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $realMonsterDist, $args->{attackMethod}{maxDistance}, $target->{dmgFromPlayer}{$slave->{ID}}), 'slave_attack'; + $slave->sendAttack($ID) if ($config{$slave->{configPrefix}."attackBeyondMaxDistance_sendAttackWhileWaiting"}); + debug TF("[%s] [%s - %s] [Waiting] (%d %d), target %s (%d %d) [(%d %d) -> (%d %d)], distance %d, maxDistance %d.\n", $slave, $canAttack_fail_string, $range_type_string, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $target->{pos}{x}, $target->{pos}{y}, $target->{pos_to}{x}, $target->{pos_to}{y}, $realMonsterDist, $args->{attackMethod}{maxDistance}), 'ai_attack'; } + $found_action = 1; + } - } elsif ( - # We are out of range - ($args->{attackMethod}{maxDistance} == 1 && !canReachMeleeAttack($realMyPos, $realMonsterPos)) || - ($args->{attackMethod}{maxDistance} > 1 && $realMonsterDist > $args->{attackMethod}{maxDistance}) + # Here we decide what to do with a mob which is out of range or we have no LOS to + if ( + !$found_action && + ($canAttack == 0 || $canAttack == -1) && + !$hitTarget_when_not_possible ) { - # The target monster moved; move to target - $args->{move_start} = time; - $args->{monsterPos} = {%{$monsterPos}}; - $args->{monsterLastMoveTime} = $target->{time_move}; + debug "Attack $slave ($realMyPos->{x} $realMyPos->{y}) - target $target ($realMonsterPos->{x} $realMonsterPos->{y})\n"; + if ($canAttack == 0) { + debug "[Slave $slave] [Attack] [$range_type_string] [No range] Too far from us to attack, distance is $realMonsterDist, attack maxDistance is $args->{attackMethod}{maxDistance}\n", 'ai_attack'; - debug "$slave target $target ($realMonsterPos->{x} $realMonsterPos->{y}) is too far from slave ($realMyPos->{x} $realMyPos->{y}) to attack, distance is $realMonsterDist, attack maxDistance is $args->{attackMethod}{maxDistance}\n", 'slave_attack'; + } elsif ($canAttack == -1) { + debug "[Slave $slave] [Attack] [$range_type_string] [No LOS] No LOS from player to mob\n", 'ai_attack'; + } my $pos = meetingPosition($slave, 2, $target, $args->{attackMethod}{maxDistance}); - my $result; - if ($pos) { - debug "Attack $slave ($realMyPos->{x} $realMyPos->{y}) - moving to meeting position ($pos->{x} $pos->{y})\n", 'slave_attack'; - - $result = $slave->route( + debug "Attack $slave ($realMyPos->{x} $realMyPos->{y}) - moving to meeting position ($pos->{x} $pos->{y})\n", 'ai_attack'; + + $args->{move_start} = time; + $args->{monsterLastMoveTime} = $target->{time_move}; + $args->{monsterLastMovePosTo}{x} = $target->{pos_to}{x}; + $args->{monsterLastMovePosTo}{y} = $target->{pos_to}{y}; + + $args->{masterLastMoveTime} = $char->{time_move}; + $args->{masterLastMovePosTo}{x} = $char->{pos_to}{x}; + $args->{masterLastMovePosTo}{y} = $char->{pos_to}{y}; + $args->{sentApproach} = 1; + + my $sendAttackWithMove = 0; + if ($config{$slave->{configPrefix}."attackSendAttackWithMove"} && $args->{attackMethod}{type} eq "weapon") { + $sendAttackWithMove = 1; + } + + $slave->route( undef, @{$pos}{qw(x y)}, maxRouteTime => $config{$slave->{configPrefix}.'attackMaxRouteTime'}, attackID => $ID, + sendAttackWithMove => $sendAttackWithMove, avoidWalls => 0, + randomFactor => 0, + useManhattan => 1, meetingSubRoute => 1, noMapRoute => 1 ); - - if (!$result) { - # Unable to calculate a route to target - $target->{$slave->{ai_attack_failed_timeout}} = time; - $slave->dequeue while ($slave->inQueue("attack")); - message TF("Unable to calculate a route to %s target, dropping target\n", $slave), 'slave_attack'; - if ($config{$slave->{configPrefix}.'teleportAuto_dropTarget'}) { - message TF("Teleport due to dropping %s attack target\n", $slave), 'teleport'; - ai_useTeleport(1); - } else { - debug "Attack $slave - successufully routing to $target\n", 'attack'; - } - } - } else { - $target->{$slave->{ai_attack_failed_timeout}} = time; - $slave->dequeue while ($slave->inQueue("attack")); - message TF("Unable to calculate a meetingPosition to target, dropping target. Check %s in config.txt\n", $config{$slave->{configPrefix}.'attackRouteMaxPathDistance'}), 'slave_attack'; - if ($config{$slave->{configPrefix}.'teleportAuto_dropTarget'}) { - message TF("Teleport due to dropping %s attack target\n", $slave), 'teleport'; - ai_useTeleport(1); - } - } - - } elsif ( - # We are a ranged attacker in range without LOS - $args->{attackMethod}{maxDistance} > 1 && - $config{$slave->{configPrefix}.'attackCheckLOS'} && - !$field->checkLOS($realMyPos, $realMonsterPos, $config{$slave->{configPrefix}.'attackCanSnipe'}) - ) { - my $best_spot = meetingPosition($slave, 2, $target, $args->{attackMethod}{maxDistance}); - - # Move to the closest spot - my $msg = TF("%s has no LOS from (%d, %d) to target %s (%d, %d) (distance: %d)", $slave, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $realMonsterDist); - if ($best_spot) { - message TF("%s; moving to (%d, %d)\n", $msg, $best_spot->{x}, $best_spot->{y}), 'slave_attack'; - $slave->route(undef, @{$best_spot}{qw(x y)}, noMapRoute => 1, avoidWalls => 0, LOSSubRoute => 1); - } else { - $target->{attack_failedLOS} = time; - warning TF("%s; no acceptable place to stand\n", $msg); - $slave->dequeue while ($slave->inQueue("attack")); - } - - } elsif ( - # We are a melee attacker in range without LOS - $args->{attackMethod}{maxDistance} == 1 && - $config{$slave->{configPrefix}.'attackCheckLOS'} && - blockDistance($realMyPos, $realMonsterPos) == 2 && - !$field->checkLOS($realMyPos, $realMonsterPos, $config{$slave->{configPrefix}.'attackCanSnipe'}) - ) { - my $best_spot = meetingPosition($slave, 2, $target, $args->{attackMethod}{maxDistance}); - - # Move to the closest spot - my $msg = TF("%s has no LOS in melee from (%d, %d) to target %s (%d, %d) (distance: %d)", $slave, $realMyPos->{x}, $realMyPos->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}, $realMonsterDist); - if ($best_spot) { - message TF("%s; moving to (%d, %d)\n", $msg, $best_spot->{x}, $best_spot->{y}), 'slave_attack'; - $slave->route(undef, @{$best_spot}{qw(x y)}, noMapRoute => 1, avoidWalls => 0, LOSSubRoute => 1); } else { - $target->{attack_failedLOS} = time; - warning TF("%s; no acceptable place to stand\n", $msg); - $slave->dequeue while ($slave->inQueue("attack")); + message T("[Slave $slave] Unable to calculate a meetingPosition to target, dropping target\n"), "ai_attack"; + giveUp($slave, $args, $ID, 1); } + $found_action = 1; + } - } elsif ((!$config{$slave->{configPrefix}.'runFromTarget'} || $realMonsterDist >= $config{$slave->{configPrefix}.'runFromTarget_dist'}) - && (!$config{$slave->{configPrefix}.'tankMode'} || !$target->{dmgFromPlayer}{$slave->{ID}})) { + if ( + !$found_action && + (!$config{$slave->{configPrefix}."runFromTarget"} || $realMonsterDist >= $config{$slave->{configPrefix}."runFromTarget_dist"} || $failed_runFromTarget) && + (!$config{$slave->{configPrefix}."tankMode"} || !$target->{dmgFromPlayer}{$slave->{ID}}) + ) { # Attack the target. In case of tanking, only attack if it hasn't been hit once. - if (!$slave->args->{firstAttack}) { - $slave->args->{firstAttack} = 1; - my $pos = "$myPos->{x},$myPos->{y}"; - debug "$slave is ready to attack target $target (which is $realMonsterDist blocks away); we're at ($pos)\n", 'slave_attack'; + if (!$args->{firstAttack}) { + $args->{firstAttack} = 1; + debug "[Slave $slave] Ready to attack target $target ($realMonsterPos->{x} $realMonsterPos->{y}) ($realMonsterDist blocks away); we're at ($realMyPos->{x} $realMyPos->{y})\n", "ai_attack"; } $args->{unstuck}{time} = time if (!$args->{unstuck}{time}); @@ -491,7 +652,7 @@ sub main { } if ($args->{attackMethod}{type} eq "weapon") { - if ($config{$slave->{configPrefix}.'attack_dance_melee'} && $args->{attackMethod}{distance} == 1) { + if ($config{$slave->{configPrefix}.'attack_dance_melee'} && $melee) { if (timeOut($timeout{$slave->{ai_dance_attack_melee_timeout}})) { my $cell = get_dance_position($slave, $target); debug TF("Slave %s will dance type %d from (%d, %d) to (%d, %d), target %s at (%d, %d).\n", $slave, $config{$slave->{configPrefix}.'attack_dance_melee'}, $realMyPos->{x}, $realMyPos->{y}, $cell->{x}, $cell->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}); @@ -500,52 +661,71 @@ sub main { $slave->sendAttack ($ID); $timeout{$slave->{ai_dance_attack_melee_timeout}}{time} = time; } - - } elsif ($config{$slave->{configPrefix}.'attack_dance_ranged'} && $args->{attackMethod}{distance} > 2) { + + } elsif ($config{$slave->{configPrefix}.'attack_dance_ranged'} && $ranged) { if (timeOut($timeout{$slave->{ai_dance_attack_ranged_timeout}})) { my $cell = get_dance_position($slave, $target); debug TF("Slave %s will range dance type %d from (%d, %d) to (%d, %d), target %s at (%d, %d).\n", $slave, $config{$slave->{configPrefix}.'attack_dance_ranged'}, $realMyPos->{x}, $realMyPos->{y}, $cell->{x}, $cell->{y}, $target, $realMonsterPos->{x}, $realMonsterPos->{y}); $slave->sendMove ($cell->{x}, $cell->{y}); $slave->sendMove ($realMyPos->{x},$realMyPos->{y}); $slave->sendAttack ($ID); - if ($config{$slave->{configPrefix}.'runFromTarget'} && $config{$slave->{configPrefix}.'runFromTarget_inAdvance'} && $realMonsterDist < $config{$slave->{configPrefix}.'runFromTarget_minStep'}) { - my $cell = meetingPosition($slave, 2, $target, $args->{attackMethod}{maxDistance}, 1); - if ($cell) { - debug TF("%s kiting in advance (%d %d) to (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $cell->{x}, $cell->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'slave_attack'; - $args->{avoiding} = 1; - $slave->sendMove($cell->{x}, $cell->{y}); - } else { - debug TF("%s no acceptable place to kite in advance from (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'slave_attack'; - } - } $timeout{$slave->{ai_dance_attack_ranged_timeout}}{time} = time; + + if ($config{$slave->{configPrefix}."runFromTarget"} && $config{$slave->{configPrefix}."runFromTarget_inAdvance"} && $realMonsterDist < $config{$slave->{configPrefix}.'runFromTarget_minStep'}) { + find_kite_position($slave, $args, 1, $target, $realMyPos, $realMonsterPos, 0); + } } } else { if (timeOut($timeout{$slave->{ai_attack_timeout}})) { $slave->sendAttack ($ID); - if ($config{$slave->{configPrefix}.'runFromTarget'} && $config{$slave->{configPrefix}.'runFromTarget_inAdvance'} && $realMonsterDist < $config{$slave->{configPrefix}.'runFromTarget_minStep'}) { - my $cell = meetingPosition($slave, 2, $target, $args->{attackMethod}{maxDistance}, 1); - if ($cell) { - debug TF("%s kiting in advance (%d %d) to (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $cell->{x}, $cell->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'slave_attack'; - $args->{avoiding} = 1; - $slave->sendMove($cell->{x}, $cell->{y}); - } else { - debug TF("%s no acceptable place to kite in advance from (%d %d), mob at (%d %d).\n", $slave, $realMyPos->{x}, $realMyPos->{y}, $realMonsterPos->{x}, $realMonsterPos->{y}), 'slave_attack'; - } - } $timeout{$slave->{ai_attack_timeout}}{time} = time; + + if ($config{$slave->{configPrefix}."runFromTarget"} && $config{$slave->{configPrefix}."runFromTarget_inAdvance"} && $realMonsterDist < $config{$slave->{configPrefix}.'runFromTarget_minStep'}) { + find_kite_position($slave, $args, 1, $target, $realMyPos, $realMonsterPos, 0); + } } } delete $args->{attackMethod}; + $found_action = 1; + + # Attack with skill logic + } elsif ($args->{attackMethod}{type} eq "skill") { + my $slot = $args->{attackMethod}{skillSlot}; + delete $args->{attackMethod}; + + $ai_v{"attackSkillSlot_${slot}_time"} = time; + $ai_v{"attackSkillSlot_${slot}_target_time"}{$ID} = time; + + ai_setSuspend(0); + my $skill = new Skill(auto => $config{"attackSkillSlot_$slot"}); + ai_skillUse2( + $skill, + $config{"attackSkillSlot_${slot}_lvl"},# || $char->getSkillLevel($skill),? + $config{"attackSkillSlot_${slot}_maxCastTime"}, + $config{"attackSkillSlot_${slot}_minCastTime"}, + $config{"attackSkillSlot_${slot}_isSelfSkill"} ? $slave : $target, + "attackSkillSlot_${slot}", + undef, + "attackSkill", + $config{"attackSkillSlot_${slot}_isStartSkill"} ? 1 : 0, + ); + $args->{monsterID} = $ID; + my $skill_lvl = $config{"attackSkillSlot_${slot}_lvl"};# || $char->getSkillLevel($skill);? + debug "[Slave $slave] [attackSkillSlot] Auto-skill on monster ".getActorName($ID).": ".qq~$config{"attackSkillSlot_$slot"} (lvl $skill_lvl)\n~, "ai_attack"; + # TODO: We sould probably add a runFromTarget_inAdvance logic here also, we could want to kite using skills, but only instant cast ones like double strafe I believe + $found_action = 1; } - } elsif ($config{$slave->{configPrefix}.'tankMode'}) { + } + + if ($config{$slave->{configPrefix}.'tankMode'}) { if ($args->{'dmgTo_last'} != $target->{dmgFromPlayer}{$slave->{ID}}) { $args->{'ai_attack_giveup'}{'time'} = time; $slave->sendAttackStop; } $args->{'dmgTo_last'} = $target->{dmgFromPlayer}{$slave->{ID}}; + $found_action = 1; } } diff --git a/src/AI/SlaveManager.pm b/src/AI/SlaveManager.pm index 87a5e3a5b0..4dde470568 100644 --- a/src/AI/SlaveManager.pm +++ b/src/AI/SlaveManager.pm @@ -27,6 +27,7 @@ sub addSlave { $actor->{ai_attack_auto_timeout} = 'ai_homunculus_attack_auto'; $actor->{ai_check_monster_auto} = 'ai_homunculus_check_monster_auto'; $actor->{ai_route_adjust_timeout} = 'ai_homunculus_route_adjust'; + $actor->{ai_attack_main} = 'ai_homunculus_attack_main'; $actor->{ai_standby_timeout} = 'ai_homunculus_standby'; $actor->{ai_dance_attack_melee_timeout} = 'ai_homunculus_dance_attack_melee'; $actor->{ai_attack_waitAfterKill_timeout} = 'ai_homunculus_attack_waitAfterKill'; @@ -43,6 +44,7 @@ sub addSlave { $actor->{ai_attack_auto_timeout} = 'ai_mercenary_attack_auto'; $actor->{ai_check_monster_auto} = 'ai_mercenary_check_monster_auto'; $actor->{ai_route_adjust_timeout} = 'ai_mercenary_route_adjust'; + $actor->{ai_attack_main} = 'ai_mercenary_attack_main'; $actor->{ai_standby_timeout} = 'ai_mercenary_standby'; $actor->{ai_dance_attack_melee_timeout} = 'ai_mercenary_dance_attack_melee'; $actor->{ai_dance_attack_ranged_timeout} = 'ai_mercenary_dance_attack_ranged'; diff --git a/src/Actor.pm b/src/Actor.pm index 83e66a0495..2775aca89e 100644 --- a/src/Actor.pm +++ b/src/Actor.pm @@ -116,7 +116,7 @@ sub new { # actor lists. If $ID is not in any of the actor lists, it will return # a new Actor::Unknown object. sub get { - my ($ID) = @_; + my ($ID, $retUndefwhenNotFound) = @_; assert(defined $ID, "ID must be provided to retrieve and Actor class") if DEBUG; if ($ID eq $accountID) { @@ -133,6 +133,9 @@ sub get { return $actor; } } + if ($retUndefwhenNotFound) { + return undef; + } return new Actor::Unknown($ID); } } @@ -455,7 +458,7 @@ sub verb { sub position { my ($self) = @_; - return calcPosition($self); + return calcPosFromPathfinding($field, $self); } ## @@ -823,7 +826,7 @@ sub route { y => $y, maxDistance => $args{maxRouteDistance}, maxTime => $args{maxRouteTime}, - map { $_ => $args{$_} } qw(distFromGoal pyDistFromGoal notifyUponArrival avoidWalls) + map { $_ => $args{$_} } qw(distFromGoal pyDistFromGoal notifyUponArrival avoidWalls randomFactor useManhattan) ); if ($map && !$args{noMapRoute}) { @@ -831,7 +834,7 @@ sub route { } else { $task = new Task::Route(field => $field, @params); } - $task->{$_} = $args{$_} for qw(attackID attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget); + $task->{$_} = $args{$_} for qw(attackID sendAttackWithMove attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget); $self->queue('route', $task); } @@ -918,7 +921,7 @@ sub processTask { sub sendAttackStop { my ($self) = @_; - $self->sendMove(@{calcPosition($self)}{qw(x y)}); + $self->sendMove(@{calcPosFromPathfinding($field, $self)}{qw(x y)}); } ## diff --git a/src/Actor/You.pm b/src/Actor/You.pm index 02fb784df0..827956c9ab 100644 --- a/src/Actor/You.pm +++ b/src/Actor/You.pm @@ -473,4 +473,9 @@ sub sendStopSkillUse { $messageSender->sendStopSkillUse($self->{last_continuous_skill_used}); } +sub sendAttack { + my ($self, $attackID) = @_; + $messageSender->sendAction($attackID, ($config{'tankMode'}) ? 0 : 7); +} + 1; diff --git a/src/Commands.pm b/src/Commands.pm index 89dba0e76e..eeac8efc1f 100644 --- a/src/Commands.pm +++ b/src/Commands.pm @@ -6265,11 +6265,6 @@ sub cmdUseSkill { } else { $target = { x => $x, y => $y }; } - # This was the code for choosing a random location when x and y are not given: - # my $pos = calcPosition($char); - # my @positions = calcRectArea($pos->{x}, $pos->{y}, int(rand 2) + 2, $field); - # $pos = $positions[rand(@positions)]; - # ($x, $y) = ($pos->{x}, $pos->{y}); } elsif ($cmd eq 'ss') { if (defined $args[0] && $args[0] eq 'start') { diff --git a/src/Field.pm b/src/Field.pm index 31be75953a..a2614c680c 100644 --- a/src/Field.pm +++ b/src/Field.pm @@ -249,48 +249,40 @@ sub getCellInfo { # boolean $Field->isWalkable(int x, int y) # # Check whether you can walk on ($x,$y) on this field. +# 1.73 microsec -> 798.04 nanosec p.o. sub isWalkable { my ($self, $x, $y) = @_; - return 0 if ($self->isOffMap($x, $y)); - my $offset = $self->getOffset($x, $y); - my $value = $self->getBlock($offset); - return ($value & TILE_WALK); + return PathFinding::checkTile($x, $y, TILE_WALK, $self->{width}, $self->{height}, \$self->{rawMap}); } ## # boolean $Field->isSnipable(int x, int y) # # Check whether you can snipe through ($x,$y) on this field. +# 1.73 microsec -> 798.04 nanosec p.o. sub isSnipable { my ($self, $x, $y) = @_; - return 0 if ($self->isOffMap($x, $y)); - my $offset = $self->getOffset($x, $y); - my $value = $self->getBlock($offset); - return ($value & TILE_SNIPE); + return PathFinding::checkTile($x, $y, TILE_SNIPE, $self->{width}, $self->{height}, \$self->{rawMap}); } ## # boolean $Field->isWater(int x, int y) # # Check whether there is water ($x,$y) on this field. +# 1.73 microsec -> 798.04 nanosec p.o. sub isWater { my ($self, $x, $y) = @_; - return 0 if ($self->isOffMap($x, $y)); - my $offset = $self->getOffset($x, $y); - my $value = $self->getBlock($offset); - return ($value & TILE_WATER); + return PathFinding::checkTile($x, $y, TILE_WATER, $self->{width}, $self->{height}, \$self->{rawMap}); } ## # boolean $Field->isCliff(int x, int y) # # Check whether cell ($x,$y) in a cliff on this field. +# 1.73 microsec -> 798.04 nanosec p.o. sub isCliff { my ($self, $x, $y) = @_; - return 0 if ($self->isOffMap($x, $y)); - my $offset = $self->getOffset($x, $y); - my $value = $self->getBlock($offset); - return ($value & TILE_CLIFF); + return PathFinding::checkTile($x, $y, TILE_CLIFF, $self->{width}, $self->{height}, \$self->{rawMap}); } sub getBlockWeight { @@ -326,7 +318,7 @@ sub closestWalkableSpot { my @current_distance = (1..$max_distance); foreach my $distance (@current_distance) { - my @blocks = Misc::calcRectArea($center{x}, $center{y}, $distance, $self); + my @blocks = $self->calcRectArea($center{x}, $center{y}, $distance); foreach my $block (@blocks) { next if (!$self->isWalkable($block->{x}, $block->{y})); return $block; @@ -336,134 +328,129 @@ sub closestWalkableSpot { return undef; } -sub checkLOS { - my ($self, $from, $to, $can_snipe) = @_; +sub getSquareEdgesFromCoord { + my ($self, $start, $radius) = @_; - # Simulate tracing a line to the location (modified Bresenham's algorithm) - my ($X0, $Y0, $X1, $Y1) = ($from->{x}, $from->{y}, $to->{x}, $to->{y}); + my @coords; + PathFinding::getSquareEdgesFromCoord($start->{x}, $start->{y}, $radius, $self->{width}, $self->{height}, \@coords); + return @coords; +} - my $steep; - my $posX = 1; - my $posY = 1; - if ($X1 - $X0 < 0) { - $posX = -1; - } - if ($Y1 - $Y0 < 0) { - $posY = -1; - } - if (abs($Y0 - $Y1) < abs($X0 - $X1)) { - $steep = 0; - } else { - $steep = 1; - } - if ($steep == 1) { - my $Yt = $Y0; - $Y0 = $X0; - $X0 = $Yt; - - $Yt = $Y1; - $Y1 = $X1; - $X1 = $Yt; - } - if ($X0 > $X1) { - my $Xt = $X0; - $X0 = $X1; - $X1 = $Xt; - - my $Yt = $Y0; - $Y0 = $Y1; - $Y1 = $Yt; - } - my $dX = $X1 - $X0; - my $dY = abs($Y1 - $Y0); - my $E = 0; - my $dE; - if ($dX) { - $dE = $dY / $dX; +## +# $Field->calcRectArea($x, $y, $radius) +# Returns: an array with position hashes. Each has contains an x and a y key. +# +# Creates a rectangle with center ($x,$y) and radius $radius, +# and returns a list of positions of the border of the rectangle. +# 8.9us -> 1.1us +sub calcRectArea { + my ($self, $x, $y, $radius) = @_; + + my @walkableBlocks; + PathFinding::calcRectArea($x, $y, $radius, TILE_WALK, $self->{width}, $self->{height}, \$self->{rawMap}, \@walkableBlocks); + return @walkableBlocks; +} + +# Bresenham's algorithm +# +# Used for checking if there are no obstacles in the direct line of sight of 2 actors +# Do not use for checking if you can walk between 2 cells, use checkPathFree for that +# +# Reference: hercules src\map\path.c path_search_long +# 27.2micros -> 1.2micros +sub checkLOS { + my ($self, $from, $to, $can_snipe) = @_; + + my $tile; + if ($can_snipe) { + $tile = TILE_WALK|TILE_SNIPE; } else { - # Delta X is 0, it only occures when $from is equal to $to - return 1; + $tile = TILE_WALK; } - my $stepY; - if ($Y0 < $Y1) { - $stepY = 1; + + return PathFinding::checkLOS($from->{x}, $from->{y}, $to->{x}, $to->{y}, $tile, $self->{width}, $self->{height}, \$self->{rawMap}); +} + +# Returns: +# -1: No LOS +# 0: out of range +# 1: sucess +# +# Reference: hercules src\map\battle.c battle_check_range +# 3.1micros -> 1.1micros +sub canAttack { + my ($self, $pos1, $pos2, $can_snipe, $range, $clientSight) = @_; + + my $tile; + if ($can_snipe) { + $tile = TILE_WALK|TILE_SNIPE; } else { - $stepY = -1; - } - my $Y = $Y0; - my $Erate = 0.99; - if (($posY == -1 && $posX == 1) || ($posY == 1 && $posX == -1)) { - $Erate = 0.01; - } - for (my $X=$X0;$X<=$X1;$X++) { - $E += $dE; - if ($steep == 1) { - if (!$self->isWalkable($Y, $X)) { - return 0 if (!$can_snipe); - return 0 if (!$self->isSnipable($Y, $X)) - } - } else { - if (!$self->isWalkable($X, $Y)) { - return 0 if (!$can_snipe); - return 0 if (!$self->isSnipable($X, $Y)) - } - } - if ($E >= $Erate) { - $Y += $stepY; - $E -= 1; - } + $tile = TILE_WALK; } - return 1; + + return PathFinding::canAttack($pos1->{x}, $pos1->{y}, $pos2->{x}, $pos2->{y}, $tile, $self->{width}, $self->{height}, $range, $clientSight, \$self->{rawMap}); } +# Used for checking if there are no obstacles in a given walking solution +# +# get_client_solution already does this in the A* algorithm itself, so there is no need to check solutions made by it +# get_client_easy_solution does not check for obstacles, so all solutions made by it *should* be checked when certainty is necessary +# +# Do not use for checking if you can attack between 2 cells, use checkLOS for that +# +# Reference: hercules src\map\path.c path_search - flag&1 +sub checkPathFree { + my ($self, $from, $to) = @_; + return PathFinding::checkPathFree($from->{x}, $from->{y}, $to->{x}, $to->{y}, TILE_WALK, $self->{width}, $self->{height}, \$self->{rawMap}); +} + +# Checks wheter you can send a move command from $from to $to +# +# Reference: hercules src\map\unit.c unit_walk_toxy +# +# Todo this should be used in a lot more places like Task::Route and Follow +# Can probably be moved to XS-cpp sub canMove { my ($self, $from, $to) = @_; - + + return 0 unless ($self->isWalkable($from->{x}, $from->{y})); + return 0 unless ($self->isWalkable($to->{x}, $to->{y})); + my $dist = blockDistance($from, $to); + + # This 17 is actually set at + # hercules conf\map\battle\client.conf max_walk_path (which is by default 17, can be higher) + # TODO: Change this 17 to a config key with default value 17 if ($dist > 17) { - return -1; + return 0; } - - my $LOS = $self->checkLOS($from, $to, 0); - if ($LOS) { + + # Actually uses CheckLos at rathena - TODO: check which is better, both work + # If there are no obstacles return success + if ($self->checkPathFree($from, $to)) { return 1; } - my $solution = []; - my ($min_pathfinding_x, $min_pathfinding_y, $max_pathfinding_x, $max_pathfinding_y) = Utils::getSquareEdgesFromCoord($self, $from, 20); - my $dist_path = new PathFinding( - field => $self, - start => $from, - dest => $to, - avoidWalls => 0, - min_x => $min_pathfinding_x, - max_x => $max_pathfinding_x, - min_y => $min_pathfinding_y, - max_y => $max_pathfinding_y - )->run($solution); - if ($dist_path > 14) { - return -2; + # If there are obstacles and the path is walkable the max solution dist acceptable is 14 (double check to save time) + if ($dist > 14) { + return 0; } - - return 1; -} -sub checkWallLength { - my ($self, $pos, $dx, $dy, $length) = @_; + # If there are obstacles and OFFICIAL_WALKPATH is defined (which is by default) then calculate a client pathfinding + my $solution = get_client_solution($self, $from, $to); + my $dist_path = scalar @{$solution}; - my $x = $pos->{x}; - my $y = $pos->{y}; - my $len = 0; + if ($dist_path == 0) { + return 0; + } - while (1) { - last if ($self->isOffMap($x, $y)); - $x += $dx; - $y += $dy; - $len++; - last unless (!$self->isWalkable($x, $y) && $len < $length); + # Pathfinding always returns the original cell in the solution, so remove 1 from it (or compare to 15 (14+1)) + #$dist_path -= 1; + if ($dist_path > 15) { + return 0; } - return (($len >= $length) ? 1 : 0); + return 1; } ## diff --git a/src/Interface/Wx.pm b/src/Interface/Wx.pm index adb6dc88f8..1299bf91d1 100644 --- a/src/Interface/Wx.pm +++ b/src/Interface/Wx.pm @@ -912,28 +912,68 @@ sub updateStatusBar { $setStatus->('aiText', $aiText); } +sub get_task { + my ($args) = @_; + if (UNIVERSAL::isa($args, 'Task::Route')) { + return $args; + } elsif (UNIVERSAL::isa($args, 'Task::MapRoute') && $args->getSubtask && UNIVERSAL::isa($args->getSubtask, 'Task::Route')) { + return $args->getSubtask; + } else { + return undef; + } +} + sub updateMapViewer { my $self = shift; my $map = $self->{mapViewer}; return unless ($map && $field && $char); my $myPos; + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? + # Maybe a new Wx mode? $myPos = calcPosition($char); $map->set($field->baseName, $myPos->{x}, $myPos->{y}, $field, $char->{look}); - my ($i, $args, $routeTask, $solution); - if ( - defined ($i = AI::findAction ('route')) && ($args = AI::args ($i)) && ( - ($routeTask = $args->getSubtask) && %{$routeTask} && ($solution = $routeTask->{solution}) && @$solution - || - $args->{dest} && $args->{dest}{pos} && ($solution = [{x => $args->{dest}{pos}{x}, y => $args->{dest}{pos}{y}}]) - ) - ) { - $map->setRoute ([@$solution]); - } else { + my ($i, $args, $routeTask, $solution, $set_route, $task); + $i = AI::findAction ('route'); + if (defined $i) { + $args = AI::args($i); + $task = get_task($args); + if (defined $task) { + if (scalar @{$task->{solution}} > 0) { + my $attack = $task->{attackID} ? 1 : 0; + $map->setRoute ([@{$task->{solution}}], $attack); + $set_route = 1; + } + } + } + if (!$set_route) { $map->setRoute; } + + undef $i; + undef $args; + undef $routeTask; + undef $solution; + undef $set_route; + undef $task; + + $i = AI::findAction ('route', 1); + if (defined $i) { + $args = AI::args($i); + $task = get_task($args); + if (defined $task) { + if (scalar @{$task->{solution}} > 0) { + my $attack = $task->{attackID} ? 1 : 0; + $map->setRoute2 ([@{$task->{solution}}], $attack); + $set_route = 1; + } + } + } + if (!$set_route) { + $map->setRoute2; + } $map->setPlayers ([values %players]); $map->setParty ([values %{$char->{party}{users}}]) if $char->{party}{joined} && $char->{party}{users}; diff --git a/src/Interface/Wx/MapViewer.pm b/src/Interface/Wx/MapViewer.pm index efaaffb050..b4c91dbda1 100644 --- a/src/Interface/Wx/MapViewer.pm +++ b/src/Interface/Wx/MapViewer.pm @@ -45,6 +45,7 @@ sub new { $self->{brush}{text} = new Wx::Brush(new Wx::Colour(0, 255, 0), wxSOLID); $self->{brush}{dest} = new Wx::Brush(new Wx::Colour(255, 110, 245), wxSOLID); + $self->{brush}{attackdest} = new Wx::Brush(new Wx::Colour(255, 80, 80), wxSOLID); $self->{brush}{party} = new Wx::Brush(new Wx::Colour(0, 0, 255), wxSOLID); $self->{textColor}{party} = new Wx::Colour (0, 0, 255); $self->{brush}{player} = new Wx::Brush(new Wx::Colour(0, 200, 0), wxSOLID); @@ -136,20 +137,30 @@ sub set { } } -# UNUSED -sub setDest { - my ($self, $x, $y) = @_; - $self->setRoute(defined $x ? [x => $x, y => $y] : undef); -} - sub setRoute { - my ($self, $solution) = @_; + my ($self, $solution, $attack) = @_; if (defined $solution) { $self->{route} = $solution; + $self->{routeAttack} = $attack; $self->{needUpdate} = 1; } elsif (defined $self->{route}) { undef $self->{route}; + undef $self->{routeAttack}; + $self->{needUpdate} = 1; + } +} + +sub setRoute2 { + my ($self, $solution, $attack) = @_; + + if (defined $solution) { + $self->{route2} = $solution; + $self->{routeAttack2} = $attack; + $self->{needUpdate} = 1; + } elsif (defined $self->{route2}) { + undef $self->{route2}; + undef $self->{routeAttack2}; $self->{needUpdate} = 1; } } @@ -722,13 +733,17 @@ sub _onPaint { } if ($self->{route} && @{$self->{route}}) { - $dc->SetBrush($self->{brush}{dest}); + if ($self->{routeAttack}) { + $dc->SetBrush($self->{brush}{attackdest}); + } else { + $dc->SetBrush($self->{brush}{dest}); + } if ($config{wx_map_route} == 2) { $dc->SetPen(wxRED_PEN); foreach my $pos (@{$self->{route}}) { ($x, $y) = $self->_posXYToView ($pos->{x}, $pos->{y}); - $dc->DrawEllipse($x - 1, $y - 1, 1, 1); + $dc->DrawEllipse($x - $actor_r, $y - $actor_r, $actor_d, $actor_d); } } elsif ($config{wx_map_route} == 1) { $dc->SetPen(wxWHITE_PEN); @@ -745,6 +760,34 @@ sub _onPaint { $dc->SetPen(wxBLACK_PEN); } + if ($self->{route2} && @{$self->{route2}}) { + if ($self->{routeAttack2}) { + $dc->SetBrush($self->{brush}{attackdest}); + } else { + $dc->SetBrush($self->{brush}{dest}); + } + + if ($config{wx_map_route} == 2) { + $dc->SetPen(wxRED_PEN); + foreach my $pos (@{$self->{route2}}) { + ($x, $y) = $self->_posXYToView ($pos->{x}, $pos->{y}); + $dc->DrawEllipse($x - $actor_r, $y - $actor_r, $actor_d, $actor_d); + } + } elsif ($config{wx_map_route} == 1) { + $dc->SetPen(wxWHITE_PEN); + my $i = 0; + for (grep {not $i++ % ($portal_d * 2)} reverse @{$self->{route2}}) { + ($x, $y) = $self->_posXYToView ($_->{x}, $_->{y}); + $dc->DrawEllipse($x - $portal_r, $y - $portal_r, $portal_d, $portal_d); + } + } else { + ($x, $y) = $self->_posXYToView ($self->{route2}[-1]{x}, $self->{route2}[-1]{y}); + $dc->DrawEllipse($x - $portal_r, $y - $portal_r, $portal_d, $portal_d); + } + + $dc->SetPen(wxBLACK_PEN); + } + if (!$self->{selfDot}) { my $file = File::Spec->catfile($Settings::maps_folder, "kore.png"); $self->{selfDot} = _loadImage($file) if (-f $file); diff --git a/src/Misc.pm b/src/Misc.pm index 4eef1a14bb..7b7256cd22 100644 --- a/src/Misc.pm +++ b/src/Misc.pm @@ -29,7 +29,6 @@ use Compress::Zlib; use base qw(Exporter); use utf8; use Math::Trig; -use Math::Trig qw/pi pi2 pip2 pip4/; use Globals; use Log qw(message warning error debug); @@ -70,8 +69,7 @@ our @EXPORT = ( visualDump/, # Field math - qw/calcRectArea - calcRectArea2 + qw/calcRectArea2 objectInsideSpell objectInsideCasting objectIsMovingTowards @@ -141,7 +139,6 @@ our @EXPORT = ( look lookAtPosition manualMove - canReachMeleeAttack meetingPosition objectAdded objectRemoved @@ -544,64 +541,6 @@ sub visualDump { ####################################### ####################################### -## -# calcRectArea($x, $y, $radius, $field) -# Returns: an array with position hashes. Each has contains an x and a y key. -# -# Creates a rectangle with center ($x,$y) and radius $radius, -# and returns a list of positions of the border of the rectangle. -sub calcRectArea { - my ($x, $y, $radius, $field) = @_; - my (%topLeft, %topRight, %bottomLeft, %bottomRight); - - sub capX { - return 0 if ($_[0] < 0); - return $_[1]->width - 1 if ($_[0] >= $_[1]->width); - return int $_[0]; - } - sub capY { - return 0 if ($_[0] < 0); - return $_[1]->height - 1 if ($_[0] >= $_[1]->height); - return int $_[0]; - } - - # Get the avoid area as a rectangle - $topLeft{x} = capX($x - $radius, $field); - $topLeft{y} = capY($y + $radius, $field); - $topRight{x} = capX($x + $radius, $field); - $topRight{y} = capY($y + $radius, $field); - $bottomLeft{x} = capX($x - $radius, $field); - $bottomLeft{y} = capY($y - $radius, $field); - $bottomRight{x} = capX($x + $radius, $field); - $bottomRight{y} = capY($y - $radius, $field); - - # Walk through the border of the rectangle - # Record the blocks that are walkable - my @walkableBlocks; - for (my $x = $topLeft{x}; $x <= $topRight{x}; $x++) { - if ($field->isWalkable($x, $topLeft{y})) { - push @walkableBlocks, {x => $x, y => $topLeft{y}}; - } - } - for (my $x = $bottomLeft{x}; $x <= $bottomRight{x}; $x++) { - if ($field->isWalkable($x, $bottomLeft{y})) { - push @walkableBlocks, {x => $x, y => $bottomLeft{y}}; - } - } - for (my $y = $bottomLeft{y} + 1; $y < $topLeft{y}; $y++) { - if ($field->isWalkable($topLeft{x}, $y)) { - push @walkableBlocks, {x => $topLeft{x}, y => $y}; - } - } - for (my $y = $bottomRight{y} + 1; $y < $topRight{y}; $y++) { - if ($field->isWalkable($topRight{x}, $y)) { - push @walkableBlocks, {x => $topRight{x}, y => $y}; - } - } - - return @walkableBlocks; -} - ## # calcRectArea2($x, $y, $radius, $minRange) # Returns: an array with position hashes. Each has contains an x and a y key. @@ -648,7 +587,8 @@ sub objectInsideSpell { sub objectInsideCasting { my ($monster) = @_; - + + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? my $monsterPos = calcPosition($monster); foreach my $caster (@$playersList, @$slavesList) { @@ -739,6 +679,7 @@ sub get_dance_position { my ($slave, $target) = @_; my ($dy, $dx); + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? my $my_pos = calcPosition($slave); my $target_pos = calcPosition($target); @@ -2538,127 +2479,28 @@ sub manualMove { main::ai_route($field->baseName, $char->{pos_to}{x} + $dx, $char->{pos_to}{y} + $dy); } -sub canReachMeleeAttack { - my ($actor_pos, $target_pos) = @_; - - my ($diag, $orto) = Utils::specifiedBlockDistance($actor_pos, $target_pos); - - if (($diag == 0 && $orto <= 2) || ($diag <= 1 && $orto == 0)) { - return 1; - } else { - return 0; - } -} - ## # meetingPosition(actor, actorType, target_actor, attackMaxDistance, runFromTargetActive) # actor: current object. # actorType: 1 - char | 2 - slave # target_actor: actor to meet. # attackMaxDistance: attack distance based on attack method. -# runFromTargetActive: Wheter meetingPosition was called by a runFromTarget check +# runFromTargetActive: Wheter meetingPosition was called by a runFromTarget check, if 2 then use runFromTarget_noAttackMethodFallback values # # Returns: the position where the character should go to meet a moving monster. sub meetingPosition { my ($actor, $actorType, $target, $attackMaxDistance, $runFromTargetActive) = @_; - # Actor was going from 'pos' to 'pos_to' in the last movement - my %myPos; - $myPos{x} = $actor->{pos}{x}; - $myPos{y} = $actor->{pos}{y}; - my %myPosTo; - $myPosTo{x} = $actor->{pos_to}{x}; - $myPosTo{y} = $actor->{pos_to}{y}; + if ($attackMaxDistance < 1) { + error "attackMaxDistance must be positive ($attackMaxDistance).\n"; + return; + } my $mySpeed = ($actor->{walk_speed} || 0.12); my $timeSinceActorMoved = time - $actor->{time_move}; - # Calculate the time actor will need to finish moving from pos to pos_to - my $timeActorFinishMove = calcTime(\%myPos, \%myPosTo, $mySpeed); - - my $realMyPos; - # Actor has finished moving - if ($timeSinceActorMoved >= $timeActorFinishMove) { - $realMyPos->{x} = $myPosTo{x}; - $realMyPos->{y} = $myPosTo{y}; - # Actor is currently moving - } else { - ($realMyPos, undef) = calcPosFromTime(\%myPos, \%myPosTo, $mySpeed, $timeSinceActorMoved); - } - - # Target was going from 'pos' to 'pos_to' in the last movement - my %targetPos; - $targetPos{x} = $target->{pos}{x}; - $targetPos{y} = $target->{pos}{y}; - my %targetPosTo; - $targetPosTo{x} = $target->{pos_to}{x}; - $targetPosTo{y} = $target->{pos_to}{y}; - - my $targetSpeed = ($target->{walk_speed} || 0.12); - my $timeSinceTargetMoved = time - $target->{time_move}; - - # Calculate the time target will need to finish moving from pos to pos_to - my $timeTargetFinishMove = calcTime(\%targetPos, \%targetPosTo, $targetSpeed); - - my $target_moving; - my $realTargetPos; - my $targetTotalSteps; - my $targetCurrentStep; - # Target has finished moving - if ($timeSinceTargetMoved >= $timeTargetFinishMove) { - $target_moving = 0; - $realTargetPos->{x} = $targetPosTo{x}; - $realTargetPos->{y} = $targetPosTo{y}; - - # Target is currently moving - } else { - $target_moving = 1; - ($realTargetPos, $targetCurrentStep) = calcPosFromTime(\%targetPos, \%targetPosTo, $targetSpeed, $timeSinceTargetMoved); - $targetTotalSteps = countSteps(\%targetPos, \%targetPosTo); - } - - my @target_pos_to_check; - my $timeForTargetToGetToStep; - my %targetPosInStep; - my $myDistToTargetPosInStep; - - # Target started moving from %targetPos to %targetPosTo and has not finished moving yet, it is currently at $realTargetPos, here we calculate every block still in its path and the time to reach them - if ($target_moving) { - my $steps_count = 0; - foreach my $currentStep ($targetCurrentStep..$targetTotalSteps) { - # Calculate the steps - %targetPosInStep = moveAlong(\%targetPos, \%targetPosTo, $currentStep); - - # Calculate time to walk for target - if ($steps_count == 0) { - $timeForTargetToGetToStep = 0; - } else { - $timeForTargetToGetToStep = calcTime(\%targetPos, \%targetPosInStep, $targetSpeed) - $timeSinceTargetMoved; - } - - $myDistToTargetPosInStep = blockDistance($realMyPos, \%targetPosInStep); - - $target_pos_to_check[$steps_count] = { - targetPosInStep => { - x => $targetPosInStep{x}, - y => $targetPosInStep{y} - }, - timeForTargetToGetToStep => $timeForTargetToGetToStep, - myDistToTargetPosInStep => $myDistToTargetPosInStep - }; - } continue { - $steps_count++; - } - - # Target has finished moving and is at %targetPosTo - } else { - $myDistToTargetPosInStep = blockDistance($realMyPos, $realTargetPos); - $target_pos_to_check[0] = { - targetPosInStep => $realTargetPos, - timeForTargetToGetToStep => 0, - myDistToTargetPosInStep => $myDistToTargetPosInStep - }; - } + my $my_solution; + my $timeActorFinishMove; my $attackRouteMaxPathDistance; my $attackCanSnipe; @@ -2676,7 +2518,11 @@ sub meetingPosition { $runFromTarget_maxPathDistance = $config{runFromTarget_maxPathDistance} || 13; $runFromTarget = $config{runFromTarget}; $runFromTarget_dist = $config{runFromTarget_dist}; - $runFromTarget_minStep = $config{runFromTarget_minStep}; + if ($runFromTargetActive == 2) { + $runFromTarget_minStep = $config{runFromTarget_noAttackMethodFallback_minStep}; + } else { + $runFromTarget_minStep = $config{runFromTarget_minStep}; + } $followDistanceMax = $config{followDistanceMax}; $attackCanSnipe = $config{attackCanSnipe}; if ($config{follow}) { @@ -2687,73 +2533,115 @@ sub meetingPosition { } } if ($master) { - $masterPos = calcPosition($master); + $masterPos = 1; } } + # If the actor is the character then we should have already saved the time calc and solution at Receive.pm::character_moves + $my_solution = $char->{solution}; + $timeActorFinishMove = $char->{time_move_calc}; + # actor is a slave } elsif ($actorType == 2) { $attackRouteMaxPathDistance = $config{$actor->{configPrefix}.'attackRouteMaxPathDistance'} || 20; $runFromTarget_maxPathDistance = $config{$actor->{configPrefix}.'runFromTarget_maxPathDistance'} || 20; $runFromTarget = $config{$actor->{configPrefix}.'runFromTarget'}; $runFromTarget_dist = $config{$actor->{configPrefix}.'runFromTarget_dist'}; - $runFromTarget_minStep = $config{$actor->{configPrefix}.'runFromTarget_minStep'}; + if ($runFromTargetActive == 2) { + $runFromTarget_minStep = $config{$actor->{configPrefix}.'runFromTarget_noAttackMethodFallback_minStep'}; + } else { + $runFromTarget_minStep = $config{$actor->{configPrefix}.'runFromTarget_minStep'}; + } $followDistanceMax = $config{$actor->{configPrefix}.'followDistanceMax'}; $attackCanSnipe = $config{$actor->{configPrefix}.'attackCanSnipe'}; $master = $char; - $masterPos = calcPosition($char); + $masterPos = 1; + + $my_solution = get_solution($field, $actor->{pos}, $actor->{pos_to}); + $timeActorFinishMove = calcTimeFromSolution($my_solution, $mySpeed); + } + + my $realMyPos; + # Actor has finished moving and is at PosTo + if ($timeSinceActorMoved >= $timeActorFinishMove) { + $realMyPos = $actor->{pos_to}; + + # Actor is currently moving + } else { + my $steps_walked = calcStepsWalkedFromTimeAndSolution($my_solution, $mySpeed, $timeSinceActorMoved); + $realMyPos = $my_solution->[$steps_walked]; + } + + # Should never happen + unless ($field->isWalkable($realMyPos->{x}, $realMyPos->{y})) { + $realMyPos = $field->closestWalkableSpot($realMyPos, 1); + } + + my $targetSpeed = ($target->{walk_speed} || 0.12); + my $timeSinceTargetMoved = time - $target->{time_move}; + + my $target_solution = get_solution($field, $target->{pos}, $target->{pos_to}); + + # Calculate the time target will need to finish moving from pos to pos_to + my $timeTargetFinishMove = calcTimeFromSolution($target_solution, $targetSpeed); + + my $realTargetPos; + my $targetTotalSteps; + my $targetCurrentStep; + + my @target_pos_to_check; + + # Target has finished moving + if ($timeSinceTargetMoved >= $timeTargetFinishMove) { + $realTargetPos = $target->{pos_to}; + $target_pos_to_check[0] = { + targetPosInStep => $realTargetPos + }; + + # Target is currently moving + } else { + $targetTotalSteps = $#{$target_solution}; + $targetCurrentStep = calcStepsWalkedFromTimeAndSolution($target_solution, $targetSpeed, $timeSinceTargetMoved); + $realTargetPos = $target_solution->[$targetCurrentStep]; + + my $steps_count = 0; + foreach my $currentStep ($targetCurrentStep..$targetTotalSteps) { + $target_pos_to_check[$steps_count] = { + targetPosInStep => $target_solution->[$currentStep] + }; + } continue { + $steps_count++; + } } my $master_moving; + my $master_solution; my $timeSinceMasterMoved; my $realMasterPos; - my %masterPos; - my %masterPosTo; my $masterSpeed; if ($masterPos) { - $masterPos{x} = $master->{pos}{x}; - $masterPos{y} = $master->{pos}{y}; - $masterPosTo{x} = $master->{pos_to}{x}; - $masterPosTo{y} = $master->{pos_to}{y}; - $masterSpeed = ($master->{walk_speed} || 0.12); $timeSinceMasterMoved = time - $master->{time_move}; + $master_solution = get_solution($field, $master->{pos}, $master->{pos_to}); + # Calculate the time master will need to finish moving from pos to pos_to - my $timeMasterFinishMove = calcTime(\%masterPos, \%masterPosTo, $masterSpeed); + my $timeMasterFinishMove = calcTimeFromSolution($master_solution, $masterSpeed); # master has finished moving if ($timeSinceMasterMoved >= $timeMasterFinishMove) { $master_moving = 0; - $realMasterPos->{x} = $masterPosTo{x}; - $realMasterPos->{y} = $masterPosTo{y}; + $realMasterPos = $master->{pos_to}; + # master is currently moving } else { $master_moving = 1; - ($realMasterPos, undef) = calcPosFromTime(\%masterPos, \%masterPosTo, $masterSpeed, $timeSinceMasterMoved); } } - my $melee; - my $ranged; - if ($attackMaxDistance == 1) { - $melee = 1; - - } elsif ($attackMaxDistance > 1) { - $ranged = 1; - - } else { - error "attackMaxDistance must be positive ($attackMaxDistance).\n"; - return; - } - my $min_destination_dist = 1; - my $max_destination_dist; - if ($ranged && $runFromTarget) { - $min_destination_dist = $runFromTarget_dist; - if ($runFromTargetActive) { - $min_destination_dist = $runFromTarget_minStep; - } + if ($runFromTarget) { + $min_destination_dist = $runFromTarget_minStep; } my $max_path_dist; @@ -2762,112 +2650,102 @@ sub meetingPosition { } else { $max_path_dist = $attackRouteMaxPathDistance; } - - # We should not stray further than $attackMaxDistance - if ($melee) { - $max_destination_dist = 2; # we can atack from a distance of 2 on ortogonal only cells - } else { - $max_destination_dist = $attackMaxDistance; - } - - my $max_pathfinding_dist = $max_destination_dist; - - unless ($field->isWalkable($realMyPos->{x}, $realMyPos->{y})) { - my $new_pos = $field->closestWalkableSpot($realMyPos, 1); - $realMyPos->{x} = $new_pos->{x}; - $realMyPos->{y} = $new_pos->{y}; + # Add 1 here to account for pos from solution so we don't have to do it multiple times later + $max_path_dist += 1; + + my %allspots; + my @blocks = calcRectArea2($realMyPos->{x}, $realMyPos->{y}, $max_path_dist, 0); + foreach my $spot (@blocks) { + $allspots{$spot->{x}}{$spot->{y}} = 1; } my $best_spot; + my $best_targetPosInStep; + my $best_dist_to_target; my $best_time; - foreach my $possible_target_pos (@target_pos_to_check) { - if ($possible_target_pos->{myDistToTargetPosInStep} >= $max_pathfinding_dist) { - $max_pathfinding_dist = $possible_target_pos->{myDistToTargetPosInStep} + 1; - } - # TODO: This algorithm is now a lot smarter than runFromTarget, maybe port it here + foreach my $x_spot (sort keys %allspots) { + foreach my $y_spot (sort keys %{$allspots{$x_spot}}) { + my $spot; + $spot->{x} = $x_spot; + $spot->{y} = $y_spot; - my ($min_pathfinding_x, $min_pathfinding_y, $max_pathfinding_x, $max_pathfinding_y) = Utils::getSquareEdgesFromCoord($field, $possible_target_pos->{targetPosInStep}, $max_pathfinding_dist); - # TODO: Check if this reverse is actually any good here - foreach my $distance (reverse ($min_destination_dist..$max_destination_dist)) { + next unless ($spot->{x} != $realMyPos->{x} || $spot->{y} != $realMyPos->{y}); - my @blocks = calcRectArea($possible_target_pos->{targetPosInStep}{x}, $possible_target_pos->{targetPosInStep}{y}, $distance, $field); + # Is this spot acceptable? - SPOT: foreach my $spot (@blocks) { - next unless ($spot->{x} != $realMyPos->{x} || $spot->{y} != $realMyPos->{y}); + # 1. It must be walkable. + next unless ($field->isWalkable($spot->{x}, $spot->{y})); - # Is this spot acceptable? + # 2. It must not be close to a portal. + next if (positionNearPortal($spot, $config{'attackMinPortalDistance'})); - # 1. It must be walkable - next unless ($field->isWalkable($spot->{x}, $spot->{y})); + my $time_actor_to_get_to_spot; - my $dist_to_target = blockDistance($spot, $possible_target_pos->{targetPosInStep}); - next unless ($dist_to_target <= $attackMaxDistance); - next unless ($dist_to_target >= $min_destination_dist); + my $solution = get_solution($field, $realMyPos, $spot); + + # 3. It must be reachable. + next if (scalar @{$solution} == 0); + + # 4. It must have at max $max_path_dist of route distance to it from our current position. + next if (scalar @{$solution} > $max_path_dist); - next if (positionNearPortal($spot, $config{'attackMinPortalDistance'})); + $time_actor_to_get_to_spot = calcTimeFromSolution($solution, $mySpeed); - # 2. It must be within $followDistanceMax of MasterPos, if we have a master. - if ($realMasterPos) { - if ($master_moving) { - my $masterPos_inTime; - my $totalTime = $timeSinceMasterMoved + $possible_target_pos->{timeForTargetToGetToStep}; - ($masterPos_inTime, undef) = calcPosFromTime(\%masterPos, \%masterPosTo, $masterSpeed, $totalTime); - next unless (blockDistance($spot, $masterPos_inTime) <= $followDistanceMax); - } else { - next unless (blockDistance($spot, $realMasterPos) <= $followDistanceMax); - } - } - # 3. It must have LOS to the target ($possible_target_pos->{targetPosInStep}) if that is active and we are ranged or must be reacheable from melee - if ($ranged) { - next unless ($field->checkLOS($spot, $possible_target_pos->{targetPosInStep}, $attackCanSnipe)); - } elsif ($melee) { - next unless (canReachMeleeAttack($spot, $possible_target_pos->{targetPosInStep})); - if (blockDistance($spot, $possible_target_pos->{targetPosInStep}) == 2) { - next unless ($field->checkLOS($spot, $possible_target_pos->{targetPosInStep}, $attackCanSnipe)); - } - } + my $total_time = ($timeSinceTargetMoved+$time_actor_to_get_to_spot); + my $temp_targetCurrentStep = calcStepsWalkedFromTimeAndSolution($target_solution, $targetSpeed, $total_time); + # Position target would be at if it doesn't change route (and is not following us) + my $targetPosInStep = $target_solution->[$temp_targetCurrentStep]; - # 4. The route should not exceed at any point $max_pathfinding_dist distance from the target. - my $solution = []; - my $dist = new PathFinding( - field => $field, - start => $realMyPos, - dest => $spot, - avoidWalls => 0, - min_x => $min_pathfinding_x, - max_x => $max_pathfinding_x, - min_y => $min_pathfinding_y, - max_y => $max_pathfinding_y - )->run($solution); - - # 5. It must be reachable and have at max $max_path_dist of route distance to it from our current position. - next unless ($dist >= 0 && $dist <= $max_path_dist); - - my $time_actor_to_get_to_spot = calcTime($realMyPos, $spot, $mySpeed); - my $time_actor_will_have_to_wait_in_spot_for_target_to_be_at_targetPosInStep; - - if ($time_actor_to_get_to_spot >= $possible_target_pos->{timeForTargetToGetToStep}) { - $time_actor_will_have_to_wait_in_spot_for_target_to_be_at_targetPosInStep = 0; + # 5. It must not be the same position the target will be in + next unless ($spot->{x} != $targetPosInStep->{x} || $spot->{y} != $targetPosInStep->{y}); + + # 6. We must be able to attack the target from this spot + next unless (canAttack($field, $spot, $targetPosInStep, $attackCanSnipe, $attackMaxDistance, $config{clientSight}) == 1); + + # 7. It must not be too close to the target if we have runfromtarget set + # TODO: Maybe we should assume the target will keep following us after it reaches its destination and take that into consideration when runfromtarget is set + my $dist_to_target = blockDistance($spot, $targetPosInStep); + next unless ($dist_to_target >= $min_destination_dist); + + # 8. It must be within $followDistanceMax of MasterPos, if we have a master. + if ($realMasterPos) { + my $masterPosNow; + if ($master_moving) { + my $totalTime = $timeSinceMasterMoved + $time_actor_to_get_to_spot; + my $master_CurrentStep = calcStepsWalkedFromTimeAndSolution($master_solution, $masterSpeed, $totalTime); + $masterPosNow = $master_solution->[$master_CurrentStep]; } else { - $time_actor_will_have_to_wait_in_spot_for_target_to_be_at_targetPosInStep = $possible_target_pos->{timeForTargetToGetToStep} - $time_actor_to_get_to_spot; + $masterPosNow = $realMasterPos; } + next unless ($spot->{x} != $masterPosNow->{x} || $spot->{y} != $masterPosNow->{y}); + next unless (blockDistance($spot, $masterPosNow) <= $followDistanceMax); + next unless (blockDistance($targetPosInStep, $masterPosNow) <= $followDistanceMax); + } - my $time_target_to_get_to_spot = calcTime($realTargetPos, $spot, $targetSpeed); - next if ($runFromTargetActive && $time_actor_to_get_to_spot > $time_target_to_get_to_spot); - - my $sum_time = $time_actor_to_get_to_spot + $time_actor_will_have_to_wait_in_spot_for_target_to_be_at_targetPosInStep; - - if (!defined($best_time) || $sum_time < $best_time) { - $best_time = $sum_time; - $best_spot = $spot; + # 8. We must be able to get to the spot before our target + # TODO: Fix me. The target does not need to get to the spot, but to at least 2 cells away to be able to attack us, so take that into account + if ($runFromTargetActive) { + my $time_target_to_get_to_spot = calcTimeFromPathfinding($field, $realTargetPos, $spot, $targetSpeed); + if ($time_actor_to_get_to_spot > $time_target_to_get_to_spot) { + next; } } + + # We then choose the spot which takes the least amount of time to reach + # TODO: Maybe this is not the best idea when runfromtarget is set + if (!defined($best_time) || $time_actor_to_get_to_spot < $best_time) { + $best_time = $time_actor_to_get_to_spot; + $best_spot = $spot; + $best_targetPosInStep = $targetPosInStep; + $best_dist_to_target = $dist_to_target; + } } } - if ($best_spot) { + if (defined $best_spot) { + debug "[meetingPosition] Best spot is $best_spot->{x} $best_spot->{y}, mob will be at $best_targetPosInStep->{x} $best_targetPosInStep->{y}, dist $best_dist_to_target, it will take $best_time seconds to get there.\n"; return $best_spot; } } @@ -3448,7 +3326,7 @@ sub updateDamageTables { $monster->{atkMiss}++; } else { if ($config{attackUpdateMonsterPos} && ($monster->{pos}{x} != $monster->{pos_to}{x} || $monster->{pos}{y} != $monster->{pos_to}{y})) { - my $new_monster_pos = calcPosition($monster); + my $new_monster_pos = calcPosFromPathfinding($field, $monster); $monster->{pos} = $new_monster_pos; $monster->{pos_to} = $new_monster_pos; $monster->{time_move} = time; @@ -3476,117 +3354,6 @@ sub updateDamageTables { } -=pod - } elsif ($targetID eq $accountID) { - if ((my $monster = $monstersList->getByID($sourceID))) { - # Monster attacks you - $monster->{dmgFrom} += $damage; - $monster->{dmgToYou} += $damage; - if ($damage == 0) { - $monster->{missedYou}++; - } - $monster->{attackedYou}++ unless ( - scalar(keys %{$monster->{dmgFromPlayer}}) || - scalar(keys %{$monster->{dmgToPlayer}}) || - $monster->{missedFromPlayer} || - $monster->{missedToPlayer} - ); - $monster->{target} = $targetID; - - if (AI::state == 2) { - my $teleport = 0; - if (mon_control($monster->{name},$monster->{nameID})->{teleport_auto} == 2 && $damage){ - message TF("Teleporting due to attack from %s\n", - $monster->{name}), "teleport"; - $teleport = 1; - - } elsif ($config{teleportAuto_deadly} && $damage >= $char->{hp} - && !$char->statusActive('EFST_ILLUSION')) { - message TF("Next %d dmg could kill you. Teleporting...\n", - $damage), "teleport"; - $teleport = 1; - - } elsif ($config{teleportAuto_maxDmg} && $damage >= $config{teleportAuto_maxDmg} - && !$char->statusActive('EFST_ILLUSION') - && !($config{teleportAuto_maxDmgInLock} && $field->baseName eq $config{lockMap})) { - message TF("%s hit you for more than %d dmg. Teleporting...\n", - $monster->{name}, $config{teleportAuto_maxDmg}), "teleport"; - $teleport = 1; - - } elsif ($config{teleportAuto_maxDmgInLock} && $field->baseName eq $config{lockMap} - && $damage >= $config{teleportAuto_maxDmgInLock} - && !$char->statusActive('EFST_ILLUSION')) { - message TF("%s hit you for more than %d dmg in lockMap. Teleporting...\n", - $monster->{name}, $config{teleportAuto_maxDmgInLock}), "teleport"; - $teleport = 1; - - } elsif (AI::inQueue("sitAuto") && $config{teleportAuto_attackedWhenSitting} - && $damage > 0) { - message TF("%s attacks you while you are sitting. Teleporting...\n", - $monster->{name}), "teleport"; - $teleport = 1; - - } elsif ($config{teleportAuto_totalDmg} - && $monster->{dmgToYou} >= $config{teleportAuto_totalDmg} - && !$char->statusActive('EFST_ILLUSION') - && !($config{teleportAuto_totalDmgInLock} && $field->baseName eq $config{lockMap})) { - message TF("%s hit you for a total of more than %d dmg. Teleporting...\n", - $monster->{name}, $config{teleportAuto_totalDmg}), "teleport"; - $teleport = 1; - - } elsif ($config{teleportAuto_totalDmgInLock} && $field->baseName eq $config{lockMap} - && $monster->{dmgToYou} >= $config{teleportAuto_totalDmgInLock} - && !$char->statusActive('EFST_ILLUSION')) { - message TF("%s hit you for a total of more than %d dmg in lockMap. Teleporting...\n", - $monster->{name}, $config{teleportAuto_totalDmgInLock}), "teleport"; - $teleport = 1; - - } elsif ($config{teleportAuto_hp} && percent_hp($char) <= $config{teleportAuto_hp}) { - message TF("%s hit you when your HP is too low. Teleporting...\n", - $monster->{name}), "teleport"; - $teleport = 1; - - } elsif ($config{attackChangeTarget} && ((AI::action eq "route" && AI::action(1) eq "attack") || (AI::action eq "move" && AI::action(2) eq "attack")) - && AI::args->{attackID} && AI::args()->{attackID} ne $sourceID) { - my $attackTarget = Actor::get(AI::args->{attackID}); - my $attackSeq = (AI::action eq "route") ? AI::args(1) : AI::args(2); - if (!$attackTarget->{dmgToYou} && !$attackTarget->{dmgFromYou} && distance($monster->{pos_to}, calcPosition($char)) <= $attackSeq->{attackMethod}{distance}) { - my $ignore = 0; - # Don't attack ignored monsters - if ((my $control = mon_control($monster->{name},$monster->{nameID}))) { - $ignore = 1 if ( ($control->{attack_auto} == -1) - || ($control->{attack_lvl} ne "" && $control->{attack_lvl} > $char->{lv}) - || ($control->{attack_jlvl} ne "" && $control->{attack_jlvl} > $char->{lv_job}) - || ($control->{attack_hp} ne "" && $control->{attack_hp} > $char->{hp}) - || ($control->{attack_sp} ne "" && $control->{attack_sp} > $char->{sp}) - || ($control->{attack_auto} == 3 && ($monster->{dmgToYou} || $monster->{missedYou} || $monster->{dmgFromYou})) - ); - } - if (!$ignore) { - # Change target to closer aggressive monster - message TF("Change target to aggressive : %s (%s)\n", $monster->name, $monster->{binID}); - stopAttack(); - AI::dequeue; - AI::dequeue if (AI::action eq "route"); - AI::dequeue; - attack($sourceID); - } - } - - } elsif (AI::action eq "attack" && mon_control($monster->{name},$monster->{nameID})->{attack_auto} == 3 - && ($monster->{dmgToYou} || $monster->{missedYou} || $monster->{dmgFromYou})) { - - # Mob-training, stop attacking the monster if it has been attacking you - message TF("%s (%s) has been provoked, searching another monster\n", $monster->{name}, $monster->{binID}); - stopAttack(); - AI::dequeue(); - } - - ai_useTeleport(1) if ($teleport); - } - } -=cut - } elsif ((my $monster = $monstersList->getByID($sourceID))) { if (my $player = ($accountID eq $targetID && $char) || $playersList->getByID($targetID) || $slavesList->getByID($targetID)) { # Monster attacks player or slave @@ -3924,6 +3691,7 @@ sub getBestTarget { my $playerDist = $config{'attackMinPlayerDistance'} || 1; my @noLOSMonsters; + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? my $myPos = calcPosition($char); my ($highestPri, $smallestDist, $bestTarget); @@ -3931,6 +3699,7 @@ sub getBestTarget { foreach (@{$possibleTargets}) { my $monster = $monsters{$_}; + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? my $pos = calcPosition($monster); next if (positionNearPlayer($pos, $playerDist) || positionNearPortal($pos, $portalDist) @@ -3972,6 +3741,7 @@ sub getBestTarget { # more time and CPU resources, so, we use rough solution with priority and distance comparison my $monster = $monsters{$_}; + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? my $pos = calcPosition($monster); my $name = lc $monster->{name}; my $dist = adjustedBlockDistance($myPos, $pos); @@ -4597,6 +4367,8 @@ sub checkSelfCondition { return 0 if ($config{$prefix."_whenIdle"} && !AI::isIdle); return 0 if ($config{$prefix."_whenNotIdle"} && AI::isIdle); + + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here in these checks? # *_manualAI 0 = auto only # *_manualAI 1 = manual only @@ -5150,6 +4922,8 @@ sub checkPlayerCondition { sub checkMonsterCondition { my ($prefix, $monster) = @_; + + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime in these checks? if ($config{$prefix . "_hp"}) { if($config{$prefix . "_hp"} =~ /(\d+)%$/) { diff --git a/src/Network/Receive.pm b/src/Network/Receive.pm index 8f373d33c0..68ca387976 100644 --- a/src/Network/Receive.pm +++ b/src/Network/Receive.pm @@ -1267,6 +1267,8 @@ sub map_loaded { message(TF("Your Coordinates: %s, %s\n", $char->{pos}{x}, $char->{pos}{y}), undef, 1); $char->{time_move} = 0; $char->{time_move_calc} = 0; + $char->{solution} = []; + push(@{$char->{solution}}, { x => $char->{pos}{x}, y => $char->{pos}{y} }); # set initial status from data received from the char server (seems needed on eA, dunno about kRO)} if ($masterServer->{private}){ setStatus($char, $char->{opt1}, $char->{opt2}, $char->{option}); } @@ -2072,6 +2074,7 @@ sub actor_display { # too many packets in prontera and cause server lag). As a side effect, you won't be able to "see" actors # beyond clientSight. if ($config{clientSight}) { + # TODO: Is there any situation where we should use calcPosFromPathfinding or calcPosFromTime here? my $realMyPos = calcPosition($char); my $realActorPos = calcPosition($actor); my $realActorDist = blockDistance($realMyPos, $realActorPos); @@ -2884,9 +2887,9 @@ sub reconstruct_minimap_indicator { # 0ba4 .24B .B .W .W .W .W .W .W .W .W .W .W .W .L .L .L .L .eL .eL .W .W (ZC_PROPERTY_HOMUN4) sub homunculus_property { my ($self, $args) = @_; - + return 0 unless enforce_homun_state(); - + my $slave = $char->{homunculus}; $slave->{name} = bytesToString($args->{name}); @@ -2901,10 +2904,14 @@ sub homunculus_property { # TODO: we do this for homunculus, mercenary and our char... make 1 function and pass actor and attack_range? # or make function in Actor class if ($config{homunculus_attackDistanceAuto} && exists $slave->{attack_range}) { - configModify('homunculus_attackDistance', $slave->{attack_range}, 1) if ($config{homunculus_attackDistanceAuto} > $slave->{attack_range}); - configModify('homunculus_attackMaxDistance', $slave->{attack_range}, 1) if ($config{homunculus_attackMaxDistance} != $slave->{attack_range}); - message TF("Autodetected attackDistance for homunculus = %s\n", $config{homunculus_attackDistanceAuto}), "success"; - message TF("Autodetected homunculus_attackMaxDistance for homunculus = %s\n", $config{homunculus_attackMaxDistance}), "success"; + if($config{homunculus_attackDistance} > $slave->{attack_range}) { # decrease attack range if necessary + configModify('homunculus_attackDistance', $slave->{attack_range}, 1); + message TF("Autodetected attackDistance for homunculus = %s\n", $config{homunculus_attackDistance}), "success"; + } + if ($config{homunculus_attackMaxDistance} != $slave->{attack_range}) { # set max distance using information coming from the server + configModify('homunculus_attackMaxDistance', $slave->{attack_range}, 1); + message TF("Autodetected homunculus_attackMaxDistance for homunculus = %s\n", $config{homunculus_attackMaxDistance}), "success"; + } } } @@ -3013,7 +3020,7 @@ sub homunculus_info { debug "[Homunculus] We received a homunculus_info packet while our homunculus is dead, assume it was resurrected.\n"; $char->{homunculus_info}{dead} = 0; } - + $char->{homunculus} = Actor::get($args->{ID}) if ($char->{homunculus}{ID} ne $args->{ID}); $char->{homunculus}{map} = $field->baseName; unless ($char->{slaves}{$char->{homunculus}{ID}}) { @@ -5474,7 +5481,12 @@ sub character_moves { my $dist = blockDistance($char->{pos}, $char->{pos_to}); debug "You're moving from ($char->{pos}{x}, $char->{pos}{y}) to ($char->{pos_to}{x}, $char->{pos_to}{y}) - distance $dist\n", "parseMsg_move"; $char->{time_move} = time; - $char->{time_move_calc} = calcTime($char->{pos}, $char->{pos_to}, ($char->{walk_speed} || 0.12)); + + my $speed = ($char->{walk_speed} || 0.12); + my $my_solution = get_solution($field, $char->{pos}, $char->{pos_to}); + my $time = calcTimeFromSolution($my_solution, $speed); + $char->{solution} = $my_solution; + $char->{time_move_calc} = $time; # Correct the direction in which we're looking my (%vec, $degree); @@ -7189,6 +7201,8 @@ sub map_change { $char->{pos_to} = {%coords}; $char->{time_move} = 0; $char->{time_move_calc} = 0; + $char->{solution} = []; + push(@{$char->{solution}}, { x => $char->{pos}{x}, y => $char->{pos}{y} }); message TF("Map Change: %s (%s, %s)\n", $args->{map}, $char->{pos}{x}, $char->{pos}{y}), "connection"; if ($net->version == 1) { ai_clientSuspend(0, $timeout{'ai_clientSuspend'}{'timeout'}); @@ -7241,6 +7255,8 @@ sub map_changed { $char->{pos_to} = {%coords}; $char->{time_move} = 0; $char->{time_move_calc} = 0; + $char->{solution} = []; + push(@{$char->{solution}}, { x => $char->{pos}{x}, y => $char->{pos}{y} }); undef $conState_tries; main::initMapChangeVars(); @@ -10189,10 +10205,14 @@ sub attack_range { $char->{attack_range} = $type; if ($config{attackDistanceAuto}) { - configModify('attackDistance', $type, 1) if ($config{attackDistance} > $type); - configModify('attackMaxDistance', $type, 1) if ($config{attackMaxDistance} != $type); - message TF("Autodetected attackDistance = %s\n", $config{attackDistance}), "success"; - message TF("Autodetected attackMaxDistance = %s\n", $config{attackMaxDistance}), "success"; + if($config{attackDistance} > $type) { # decrease attack range if necessary + configModify('attackDistance', $type, 1); + message TF("Autodetected attackDistance = %s\n", $config{attackDistance}), "success"; + } + if ($config{attackMaxDistance} != $type) { # set max distance using information coming from the server + configModify('attackMaxDistance', $type, 1) if ($config{attackMaxDistance} != $type); + message TF("Autodetected attackMaxDistance = %s\n", $config{attackMaxDistance}), "success"; + } } } @@ -11076,10 +11096,14 @@ sub mercenary_init { # ST0's counterpart for ST kRO, since it attempts to support all servers # TODO: we do this for homunculus, mercenary and our char... make 1 function and pass actor and attack_range? if ($config{mercenary_attackDistanceAuto} && exists $slave->{attack_range}) { - configModify('mercenary_attackDistance', $slave->{attack_range}, 1) if ($config{mercenary_attackDistance} > $slave->{attack_range}); - configModify('mercenary_attackMaxDistance', $slave->{attack_range}, 1) if ($config{mercenary_attackMaxDistance} != $slave->{attack_range}); - message TF("Autodetected attackDistance for mercenary = %s\n", $config{mercenary_attackDistance}), "success"; - message TF("Autodetected attackMaxDistance for mercenary = %s\n", $config{mercenary_attackMaxDistance}), "success"; + if($config{mercenary_attackDistance} > $slave->{attack_range}) { # decrease attack range if necessary + configModify('mercenary_attackDistance', $slave->{attack_range}, 1); + message TF("Autodetected attackDistance for mercenary = %s\n", $config{mercenary_attackDistance}), "success"; + } + if ($config{mercenary_attackMaxDistance} != $slave->{attack_range}) { # set max distance using information coming from the server + configModify('mercenary_attackMaxDistance', $slave->{attack_range}, 1); + message TF("Autodetected mercenary_attackMaxDistance for mercenary = %s\n", $config{mercenary_attackMaxDistance}), "success"; + } } } @@ -11108,16 +11132,14 @@ sub monster_ranged_attack { my $monster = $monstersList->getByID($ID); if ($monster) { - $monster->{pos} = {%coords1}; - $monster->{pos_to} = {%coords1}; - $monster->{time_move} = time; - $monster->{time_move_calc} = 0; + $monster->{movetoattack_pos} = {%coords1}; + $monster->{movetoattack_time} = time; } - $char->{pos} = {%coords2}; - $char->{pos_to} = {%coords2}; - $char->{time_move} = time; - $char->{time_move_calc} = 0; - debug "Received Failed to attack target - you: $coords2{x},$coords2{y} - monster: $coords1{x},$coords1{y} - range $range\n", "parseMsg_move", 2; + $char->{movetoattack_pos} = {%coords2}; + $char->{movetoattack_time} = time; + debug "Received Failed to attack target - you: $coords2{x},$coords2{y} - monster: $coords1{x},$coords1{y} - range $range\n", "parseMsg_move"; + + Plugins::callHook('monster_ranged_attack', {ID => $ID}); } sub mvp_item { @@ -11775,13 +11797,13 @@ sub skill_use_failed { Plugins::callHook('packet_skillfail', \%hookArgs); warning(TF("Skill %s failed: %s (error number %s)\n", Skill->new(idn => $args->{skillID})->getName(), $errorMessage, $args->{cause}), "skill") if ($hookArgs{warn}); - + # Ressurect Homunculus failed - which means we have no dead homunculus if ($args->{skillID} == 247 && $args->{cause} == 0) { debug "[Homunculus] Ressurect Homunculus failed - which means we have no dead homunculus.\n"; $char->{homunculus_info}{dead} = 0; } - + # Call Homunculus failed - which means we have no vaporized homunculus if ($args->{skillID} == 243) { if ($args->{cause} == 0) { diff --git a/src/Network/Receive/ServerType0.pm b/src/Network/Receive/ServerType0.pm index 801dca4a1d..eca03d4ac8 100644 --- a/src/Network/Receive/ServerType0.pm +++ b/src/Network/Receive/ServerType0.pm @@ -1398,11 +1398,13 @@ sub skill_used_no_damage { $args->{sourceID} eq $accountID or $args->{sourceID} eq $args->{targetID}; countCastOn($args->{sourceID}, $args->{targetID}, $args->{skillID}); if ($args->{sourceID} eq $accountID) { - my $pos = calcPosition($char); + my $pos = calcPosFromPathfinding($field, $char); %{$char->{pos}} = %{$pos}; %{$char->{pos_to}} = %{$pos}; $char->{time_move} = 0; $char->{time_move_calc} = 0; + $char->{solution} = []; + push(@{$char->{solution}}, { x => $char->{pos}{x}, y => $char->{pos}{y} }); } # Resolve source and target names diff --git a/src/Network/Receive/kRO/Sakexe_0.pm b/src/Network/Receive/kRO/Sakexe_0.pm index 49d0a1d5fd..5f13ab1013 100644 --- a/src/Network/Receive/kRO/Sakexe_0.pm +++ b/src/Network/Receive/kRO/Sakexe_0.pm @@ -1376,11 +1376,13 @@ sub skill_used_no_damage { $args->{sourceID} eq $accountID or $args->{sourceID} eq $args->{targetID}; countCastOn($args->{sourceID}, $args->{targetID}, $args->{skillID}); if ($args->{sourceID} eq $accountID) { - my $pos = calcPosition($char); + my $pos = calcPosFromPathfinding($field, $char); %{$char->{pos}} = %{$pos}; %{$char->{pos_to}} = %{$pos}; $char->{time_move} = 0; $char->{time_move_calc} = 0; + $char->{solution} = []; + push(@{$char->{solution}}, { x => $char->{pos}{x}, y => $char->{pos}{y} }); } # Resolve source and target names diff --git a/src/Task/MapRoute.pm b/src/Task/MapRoute.pm index a35f0b2aac..830f104af6 100644 --- a/src/Task/MapRoute.pm +++ b/src/Task/MapRoute.pm @@ -89,7 +89,7 @@ sub new { ArgumentException->throw(error => "Task::MapRoute: Invalid arguments."); } - my $allowed = new Set(qw(maxDistance maxTime distFromGoal pyDistFromGoal avoidWalls notifyUponArrival attackID attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget)); + my $allowed = new Set(qw(maxDistance maxTime distFromGoal pyDistFromGoal avoidWalls randomFactor useManhattan notifyUponArrival attackID attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget)); foreach my $key (keys %args) { if ($allowed->has($key) && defined $args{$key}) { $self->{$key} = $args{$key}; @@ -103,7 +103,15 @@ sub new { $self->{dest}{pos}{y} = $args{y}; if ($config{'route_avoidWalls'}) { $self->{avoidWalls} = 1 if (!defined $self->{avoidWalls}); - } else {$self->{avoidWalls} = 0;} + } else { + $self->{avoidWalls} = 0; + } + if ($config{'route_randomFactor'}) { + $self->{randomFactor} = $config{'route_randomFactor'} if (!defined $self->{randomFactor}); + } else { + $self->{randomFactor} = 0; + } + $self->{useManhattan} = 0 if (!defined $self->{useManhattan}); # Watch for map change events. Pass a weak reference to ourselves in order # to avoid circular references (memory leaks). @@ -163,6 +171,10 @@ sub iterate { my $min_npc_dist = 8; my $max_npc_dist = 10; my $dist_to_npc = blockDistance($self->{actor}{pos}, $self->{mapSolution}[0]{pos}); + + if (!exists $self->{mapSolution}[0]{retry} || !defined $self->{mapSolution}[0]{retry}) { + $self->{mapSolution}[0]{retry} = 0; + } # If current solution has conversation steps specified if ( $self->{substage} eq 'Waiting for Warp' ) { @@ -176,7 +188,8 @@ sub iterate { warning TF("NPC error: %s.\n", $self->{mapSolution}[0]{error}), "route" if (exists $self->{mapSolution}[0]{error}); if ($self->{mapSolution}[0]{retry} < ($config{route_maxNpcTries} || 5)) { - warning "Retrying for the ".$self->{mapSolution}[0]{retry}." time...\n", "route"; + $self->{mapSolution}[0]{retry}++; + warning "Retrying for the ".$self->{mapSolution}[0]{retry}."th time...\n", "route"; delete $self->{mapSolution}[0]{error}; } else { @@ -252,6 +265,8 @@ sub iterate { maxTime => $self->{maxTime}, distFromGoal => $min_npc_dist, avoidWalls => $self->{avoidWalls}, + randomFactor => $self->{randomFactor}, + useManhattan => $self->{useManhattan}, solution => \@solution ); $self->setSubtask($task); @@ -287,11 +302,13 @@ sub iterate { field => $field, maxTime => $self->{maxTime}, avoidWalls => $self->{avoidWalls}, + randomFactor => $self->{randomFactor}, + useManhattan => $self->{useManhattan}, distFromGoal => $self->{distFromGoal}, pyDistFromGoal => $self->{pyDistFromGoal}, solution => \@solution ); - $task->{$_} = $self->{$_} for qw(attackID attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget); + $task->{$_} = $self->{$_} for qw(attackID sendAttackWithMove attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget); $self->setSubtask($task); $self->{mapSolution}[0]{routed} = 1; @@ -385,9 +402,9 @@ sub iterate { solution => \@solution ); $params{$_} = $self->{guess_portal}{pos}{$_} for qw(x y); - $params{$_} = $self->{$_} for qw(actor maxTime avoidWalls); + $params{$_} = $self->{$_} for qw(actor maxTime avoidWalls randomFactor useManhattan); my $task = new Task::Route(%params); - $task->{$_} = $self->{$_} for qw(attackID attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget); + $task->{$_} = $self->{$_} for qw(attackID sendAttackWithMove attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget); $self->setSubtask($task); } } @@ -493,9 +510,11 @@ sub iterate { field => $field, maxTime => $self->{maxTime}, avoidWalls => $self->{avoidWalls}, + randomFactor => $self->{randomFactor}, + useManhattan => $self->{useManhattan}, solution => \@solution ); - $task->{$_} = $self->{$_} for qw(attackID attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget); + $task->{$_} = $self->{$_} for qw(attackID sendAttackWithMove attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget); $self->setSubtask($task); } else { @@ -564,10 +583,12 @@ sub subtaskDone { field => $field, maxTime => $self->{maxTime}, avoidWalls => $self->{avoidWalls}, + randomFactor => $self->{randomFactor}, + useManhattan => $self->{useManhattan}, distFromGoal => $self->{distFromGoal}, pyDistFromGoal => $self->{pyDistFromGoal} ); - $task->{$_} = $self->{$_} for qw(attackID attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget); + $task->{$_} = $self->{$_} for qw(attackID sendAttackWithMove attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget); $self->setSubtask($task); } } diff --git a/src/Task/Move.pm b/src/Task/Move.pm index 3f23edf9ed..b2706a89ee 100644 --- a/src/Task/Move.pm +++ b/src/Task/Move.pm @@ -158,6 +158,10 @@ sub iterate { $self->{retry}{count}++; debug "Move $self->{actor} (to $self->{x} $self->{y}) - trying ($self->{retry}{count})\n", "move"; $self->{actor}->sendMove(@{$self}{qw(x y)}); + if ($self->{sendAttack}) { + debug "[Test Move Attack Buffer] Sending attack with move.\n"; + $self->{actor}->sendAttack($self->{attackID}); + } $self->{retry}{time} = time; } } diff --git a/src/Task/Route.pm b/src/Task/Route.pm index 3b32b7131f..afb99f96b3 100644 --- a/src/Task/Route.pm +++ b/src/Task/Route.pm @@ -35,7 +35,7 @@ use Network; use Field; use Translation qw(T TF); use Misc; -use Utils qw(timeOut adjustedBlockDistance distance blockDistance calcPosition); +use Utils qw(timeOut adjustedBlockDistance distance blockDistance calcPosFromPathfinding); use Utils::Exceptions; use Utils::Set; use Utils::PathFinding; @@ -107,7 +107,7 @@ sub new { ArgumentException->throw(error => "Invalid Coordinates argument."); } - my $allowed = new Set(qw(maxDistance maxTime distFromGoal pyDistFromGoal avoidWalls notifyUponArrival attackID attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget)); + my $allowed = new Set(qw(maxDistance maxTime distFromGoal pyDistFromGoal avoidWalls randomFactor useManhattan notifyUponArrival attackID sendAttackWithMove attackOnRoute noSitAuto LOSSubRoute meetingSubRoute isRandomWalk isFollow isIdleWalk isSlaveRescue isMoveNearSlave isEscape isItemTake isItemGather isDeath isToLockMap runFromTarget)); foreach my $key (keys %args) { if ($allowed->has($key) && defined($args{$key})) { $self->{$key} = $args{$key}; @@ -131,6 +131,18 @@ sub new { } else { $self->{avoidWalls} = 0; } + + if ($config{$self->{actor}{configPrefix}.'route_randomFactor'}) { + if (!defined $self->{randomFactor}) { + $self->{randomFactor} = $config{$self->{actor}{configPrefix}.'route_randomFactor'}; + } + } else { + $self->{randomFactor} = 0; + } + if (!defined $self->{useManhattan}) { + $self->{useManhattan} = 0; + } + $self->{solution} = []; $self->{stage} = NOT_INITIALIZED; @@ -198,14 +210,16 @@ sub iterate { } elsif ($self->{stage} == CALCULATE_ROUTE) { my $pos = $self->{actor}{pos}; my $pos_to = $self->{actor}{pos_to}; - + + debug "Route $self->{actor}: Calculating. Your pos ($pos->{x} $pos->{y}). Your pos_to ($pos_to->{x} $pos_to->{y}).\n", "route"; + my $begin = time; if (!$self->{meetingSubRoute} && !$self->{LOSSubRoute} && $pos_to->{x} == $self->{dest}{pos}{x} && $pos_to->{y} == $self->{dest}{pos}{y}) { debug "Route $self->{actor}: Current position and destination are the same.\n", "route"; $self->setDone(); - - } elsif ($self->getRoute($self->{solution}, $self->{dest}{map}, $pos, $self->{dest}{pos}, $self->{avoidWalls}, 1)) { + + } elsif ($self->getRoute($self->{solution}, $self->{dest}{map}, $pos, $self->{dest}{pos}, $self->{avoidWalls}, $self->{randomFactor}, $self->{useManhattan}, 1)) { $self->{stage} = ROUTE_SOLUTION_READY; @{$self->{last_pos}}{qw(x y)} = @{$pos}{qw(x y)}; @@ -214,13 +228,11 @@ sub iterate { $self->{confirmed_correct_vector} = 0; debug "Route $self->{actor} Solution Ready! Found path on ".$self->{dest}{map}->baseName." from ".$pos->{x}." ".$pos->{y}." to ".$self->{dest}{pos}{x}." ".$self->{dest}{pos}{y}.". Size: ".@{$self->{solution}}." steps.\n", "route"; + + # Changed in pathfinding.xs + #unshift(@{$self->{solution}}, { x => $pos->{x}, y => $pos->{y}}); - unshift(@{$self->{solution}}, { x => $pos->{x}, y => $pos->{y}}); - - if (time - $begin < 0.01) { - # Optimization: immediately go to the next stage if we spent neglible time in this step. - $self->iterate(); - } + $self->iterate(); } else { debug "Something's wrong; there is no path from " . $self->{dest}{map}->baseName . "($pos->{x},$pos->{y}) to " . $self->{dest}{map}->baseName . "($self->{dest}{pos}{x},$self->{dest}{pos}{y}).\n", "debug"; @@ -279,8 +291,7 @@ sub iterate { if (@{$self->{solution}} == 0) { debug "Route $self->{actor}: DistFromGoal|pyDistFromGoal trimmed all solution steps.\n", "route"; $self->setDone(); - } elsif (time - $begin < 0.01) { - # Optimization: immediately go to the next stage if we spent neglible time in this step. + } else { $self->iterate(); } @@ -300,9 +311,9 @@ sub iterate { # $actor->{pos_to} is the position the character moved TO in the last move packet received @{$current_pos_to}{qw(x y)} = @{$self->{actor}{pos_to}}{qw(x y)}; - - my $current_calc_pos = calcPosition($self->{actor}); - + + my $current_calc_pos = calcPosFromPathfinding($field, $self->{actor}); + if ($current_calc_pos->{x} == $solution->[$#{$solution}]{x} && $current_calc_pos->{y} == $solution->[$#{$solution}]{y}) { # Actor position is the destination; we've arrived at the destination if ($self->{notifyUponArrival}) { @@ -403,6 +414,8 @@ sub iterate { } my $stepsleft = @{$solution}; + + $self->{lastStep} = 0; if ($stepsleft == 0) { # No more points to cover; we've arrived at the destination @@ -445,15 +458,11 @@ sub iterate { # If we still have more points to cover, walk to next point if ($self->{step_index} >= $stepsleft) { $self->{step_index} = $stepsleft - 1; + $self->{lastStep} = 1; } @{$self->{next_pos}}{qw(x y)} = @{$solution->[$self->{step_index}]}{qw(x y)}; $self->{time_step} = time; - my $task = new Task::Move( - actor => $self->{actor}, - x => $self->{next_pos}{x}, - y => $self->{next_pos}{y} - ); - $self->setSubtask($task); + $self->setMove(); } } else { @@ -488,6 +497,7 @@ sub iterate { # If there are less steps to cover than the step size move to the last step (the destination). if ($self->{step_index} >= $stepsleft) { $self->{step_index} = $stepsleft - 1; + $self->{lastStep} = 1; } # Here maybe we should also use pos_to (in the form of best_pos_to_step) to decide the next step index, as it can make the routing way more responsive @@ -521,18 +531,8 @@ sub iterate { @{$self->{last_pos_to}}{qw(x y)} = @{$current_pos_to}{qw(x y)}; debug "Route $self->{actor} - next step moving to ($self->{next_pos}{x}, $self->{next_pos}{y}), index $self->{step_index}, $stepsleft steps left\n", "route"; - - my $task = new Task::Move( - actor => $self->{actor}, - x => $self->{next_pos}{x}, - y => $self->{next_pos}{y} - ); - $self->setSubtask($task); - - if (time - $begin < 0.01) { - # Optimization: immediately begin moving, if we spent neglible time in this step. - $self->iterate(); - } + + $self->setMove(); } } $self->{route_out_time} = time; @@ -543,6 +543,26 @@ sub iterate { } } +sub setMove { + my ($self) = @_; + + my $task = new Task::Move( + actor => $self->{actor}, + x => $self->{next_pos}{x}, + y => $self->{next_pos}{y} + ); + + if ($self->{lastStep} == 1 && $self->{attackID} && $self->{sendAttackWithMove}) { + $task->{sendAttack} = 1; + $task->{attackID} = $self->{attackID}; + } else { + $task->{sendAttack} = 0; + } + + $self->setSubtask($task); + $self->iterate(); +} + sub resetRoute { my ($self) = @_; $self->{solution} = []; @@ -568,7 +588,7 @@ sub resetRoute { # This function is a convenience wrapper function for the stuff # in Utils/PathFinding.pm sub getRoute { - my ($class, $solution, $field, $start, $dest, $avoidWalls, $self_call) = @_; + my ($class, $solution, $field, $start, $dest, $avoidWalls, $randomFactor, $useManhattan, $self_call) = @_; assertClass($field, 'Field') if DEBUG; if (!defined $dest->{x} || $dest->{y} eq '') { @{$solution} = () if ($solution); @@ -595,6 +615,8 @@ sub getRoute { $plugin_args{dest} = $closest_dest; $plugin_args{field} = $field; $plugin_args{avoidWalls} = $avoidWalls; + $plugin_args{randomFactor} = $randomFactor; + $plugin_args{useManhattan} = $useManhattan; $plugin_args{return} = 0; Plugins::callHook('getRoute' => \%plugin_args); @@ -611,7 +633,10 @@ sub getRoute { start => $closest_start, dest => $closest_dest, field => $field, - avoidWalls => $avoidWalls + avoidWalls => $avoidWalls, + randomFactor => $randomFactor, + useManhattan => $useManhattan, + getRoute => 1 ); return undef if (!$pathfinding); diff --git a/src/Utils.pm b/src/Utils.pm index b5bebfeb51..d4085f9afd 100644 --- a/src/Utils.pm +++ b/src/Utils.pm @@ -38,9 +38,12 @@ our @EXPORT = ( @{$Utils::DataStructures::EXPORT_TAGS{all}}, # Math - qw(calcPosFromTime calcPosition fieldAreaCorrectEdges getSquareEdgesFromCoord calcStepsWalkedFromTimeAndRoute calcTimeFromRoute calcTime checkMovementDirection countSteps distance + qw(get_client_solution get_client_easy_solution get_solution calcPosFromPathfinding calcTimeFromPathfinding calcStepsWalkedFromTimeAndSolution calcTimeFromSolution + calcPosFromTime calcTime calcPosition + checkMovementDirection + distance blockDistance specifiedBlockDistance adjustedBlockDistance getClientDist canAttack intToSignedInt intToSignedShort - blockDistance specifiedBlockDistance adjustedBlockDistance getVector moveAlong moveAlongVector + getVector moveAlong moveAlongVector normalize vectorToDegree max min round ceil), # OS-specific qw(checkLaunchedApp launchApp launchScript), @@ -55,7 +58,10 @@ our @EXPORT = ( our %strings; our %quarks; - +use constant { + MOVE_COST => 10, + MOVE_DIAGONAL_COST => 14, +}; ################################ ################################ @@ -63,90 +69,69 @@ our %quarks; ################################ ################################ - ## -# calcPosFromTime(pos, pos_to, speed, time) +# get_client_solution(field, pos, pos_to) # -# Returns: the position where an actor moving from $pos to $pos_to with -# the speed $speed will be in $time amount of time. -# Walls are not considered. -sub calcPosFromTime { - my ($pos, $pos_to, $speed, $time) = @_; - my $posX = $$pos{x}; - my $posY = $$pos{y}; - my $pos_toX = $$pos_to{x}; - my $pos_toY = $$pos_to{y}; - my $stepType = 0; # 1 - vertical or horizontal; 2 - diagonal - my $s = 0; # step - - my $time_needed_ortogonal = $speed; - my $time_needed_diagonal = sqrt(2) * $speed; - - my %result; - $result{x} = $pos_toX; - $result{y} = $pos_toY; +# Returns: the walking path from $pos to $pos_to using the A* pathfinding +# +# Reference: hercules src\map\path.c path_search +sub get_client_solution { + my ($field, $pos, $pos_to) = @_; - if (!$speed) { - return %result; - } - while (1) { - $s++; - $stepType = 0; - if ($posX < $pos_toX) { - $posX++; - $stepType++; - } - if ($posX > $pos_toX) { - $posX--; - $stepType++; - } - if ($posY < $pos_toY) { - $posY++; - $stepType++; - } - if ($posY > $pos_toY) { - $posY--; - $stepType++; - } + my $solution = []; - if ($stepType == 2) { - $time -= $time_needed_diagonal; - } elsif ($stepType == 1) { - $time -= $time_needed_ortogonal; - } else { - $s--; - last; - } - if ($time < 0) { - $s--; - last; - } + # Optimization so we don't need to call the Pathfinding just to get this cell + if ($pos->{x} == $pos_to->{x} && $pos->{y} == $pos_to->{y}) { + push(@{$solution}, { x => $pos->{x}, y => $pos->{y} }); + return $solution; } - %result = moveAlong($pos, $pos_to, $s); - return (\%result, $s); + # Game client uses the same A* Pathfinding as openkore but uses and inadmissible heuristic (Manhattan distance) + # To better simulate the client pathfinding we tell openkore's pathfinding to use the same Manhattan heuristic + # We also deactivate any custom pathfinding weights (randomFactor, avoidWalls, customWeights) + # TODO: This 35 probably should be something dynamic like (max(abs(pos_x-posto_x),abs(pos_y-posto_y))) + my ($min_pathfinding_x, $min_pathfinding_y, $max_pathfinding_x, $max_pathfinding_y) = $field->getSquareEdgesFromCoord($pos, 35); + my $dist_path = new PathFinding( + field => $field, + start => $pos, + dest => $pos_to, + avoidWalls => 0, + randomFactor => 0, + useManhattan => 1, + min_x => $min_pathfinding_x, + max_x => $max_pathfinding_x, + min_y => $min_pathfinding_y, + max_y => $max_pathfinding_y + )->run($solution); + return $solution; } ## -# calcTime(pos, pos_to, speed) +# get_client_easy_solution(pos, pos_to) # -# Returns: time to move from $pos to $pos_to with $speed speed. -# Walls are not considered. -sub calcTime { - my ($pos, $pos_to, $speed) = @_; +# Returns: the walking path from $pos to $pos_to using "easy path search" +# +# Walls and pathfinding are not considered. +# Obstacles should be checked using Field::checkPathFree +# +# Reference: hercules src\map\path.c path_search - flag&1 +sub get_client_easy_solution { + my ($pos, $pos_to) = @_; + my $posX = $$pos{x}; my $posY = $$pos{y}; my $pos_toX = $$pos_to{x}; my $pos_toY = $$pos_to{y}; - my $stepType = 0; # 1 - vertical or horizontal; 2 - diagonal - my $time = 0; - my $time_needed_ortogonal = $speed; - my $time_needed_diagonal = sqrt(2) * $speed; + # 1 - vertical or horizontal + # 2 - diagonal + my $stepType = 0; - return if (!$speed); # Make sure $speed actually has a non-zero value... + my $solution = []; + + while (1) { + push(@{$solution}, { x => $posX, y => $posY }); - while ($posX ne $pos_toX || $posY ne $pos_toY) { $stepType = 0; if ($posX < $pos_toX) { $posX++; @@ -164,89 +149,145 @@ sub calcTime { $posY--; $stepType++; } + if ($stepType == 2) { - $time += $time_needed_diagonal; + next; } elsif ($stepType == 1) { - $time += $time_needed_ortogonal; + next; } + + # $stepType == 0 then $pos == $pos_to + last; } - return $time; + + return $solution; } ## -# calcPosition(object, [extra_time, float]) -# object: $char (yourself), or a value in %monsters or %players. -# float: If set to 1, return coordinates as floating point. -# Returns: reference to a position hash. -# -# The position information server that the server sends indicates a motion: -# it says that an object is walking from A to B, and that it will arrive at B shortly. -# This function calculates the current position of $object based on the motion information. -# -# If $extra_time is given, this function will calculate where $object will be -# after $extra_time seconds. +# get_solution(field, pos, pos_to) # -# Example: -# my $pos; -# $pos = calcPosition($char); -# print "You are currently at: $pos->{x}, $pos->{y}\n"; +# Returns: the walking path from $pos to $pos_to # -# $pos = calcPosition($monsters{$ID}); -# # Calculate where the player will be after 2 seconds -# $pos = calcPosition($players{$ID}, 2); -sub calcPosition { - my ($object, $extra_time, $float) = @_; - my $time_needed = $object->{time_move_calc}; - my $elasped = time - $object->{time_move} + $extra_time; +# Wrapper that checks if an easy solution is possible, if it is then return easy solution, if it isn't then return client solution +sub get_solution { + my ($field, $pos, $pos_to) = @_; - if ($elasped >= $time_needed || !$time_needed) { - return $object->{pos_to}; - } else { - my (%vec, %result, $dist); - my $pos = $object->{pos}; - my $pos_to = $object->{pos_to}; + # If there are no obstacles between Pos and PosTo use calcPosFromTime to save time. + if ($field->checkPathFree($pos, $pos_to)) { + return get_client_easy_solution($pos, $pos_to); - getVector(\%vec, $pos_to, $pos); - $dist = (distance($pos, $pos_to) - 1) * ($elasped / $time_needed); - moveAlongVector(\%result, $pos, \%vec, $dist); - $result{x} = int sprintf("%.0f", $result{x}) if (!$float); - $result{y} = int sprintf("%.0f", $result{y}) if (!$float); - return \%result; + # If there are obstacles then use the client pathfinding to solve it + } else { + return get_client_solution($field, $pos, $pos_to); } } -sub fieldAreaCorrectEdges { - my ($field, $x1, $y1, $x2, $y2) = @_; +# Currently the go-to function to get the position of a given actor on critical ocasions (eg. Attack logic) +sub calcPosFromPathfinding { + my ($field, $actor, $extra_time) = @_; + my $speed = ($actor->{walk_speed} || 0.12); + my $time = time - $actor->{time_move} + $extra_time; - if ($x1 < 0) { - $x1 = 0; + # If Pos and PosTo are the same return Pos + if ($actor->{pos}{x} == $actor->{pos_to}{x} && $actor->{pos}{y} == $actor->{pos_to}{y}) { + return $actor->{pos}; } - if ($y1 < 0) { - $y1 = 0; - } + my $solution; - if ($x2 >= $field->width) { - $x2 = $field->width-1; + # If the actor is the character then we should have already saved the time calc and solution at Receive.pm::character_moves + if (UNIVERSAL::isa($actor, "Actor::You")) { + if ($time >= $actor->{time_move_calc}) { + return $actor->{pos_to}; + } + $solution = $actor->{solution}; + + } else { + $solution = get_solution($field, $actor->{pos}, $actor->{pos_to}); } - if ($y2 >= $field->height) { - $y2 = $field->height-1; + my $steps_walked = calcStepsWalkedFromTimeAndSolution($solution, $speed, $time); + + my $pos = $solution->[$steps_walked]; + + return $pos; +} + +# Wrapper for calcTimeFromSolution so you don't need to call get_client_solution and calcTimeFromSolution when you only need the time +# Used in Misc::meetingPosition to calculate if the target would have time to catch-up with the character when running away from it +sub calcTimeFromPathfinding { + my ($field, $pos, $pos_to, $speed) = @_; + + if ($pos->{x} == $pos_to->{x} && $pos->{y} == $pos_to->{y}) { + return 0; } - return ($x1, $y1, $x2, $y2); + my $solution = get_solution($field, $pos, $pos_to); + + my $summed_time = calcTimeFromSolution($solution, $speed); + return $summed_time; } -sub getSquareEdgesFromCoord { - my ($field, $start, $dist_from_center) = @_; +# Returns: +# -1: No LOS +# 0: out of range +# 1: sucess +# +# Reference: hercules src\map\battle.c battle_check_range +sub canAttack { + my ($field, $pos1, $pos2, $attackCanSnipe, $range, $clientSight) = @_; + + my $distance = blockDistance($pos1, $pos2); + return 1 if ($distance < 2); + + # hercules conf\map\battle\client.conf area_size + # Here the check is done against area_size (which is by default 14, can be higher, eg. 22 in OathRO) + # Openkore clientSight should be area_size+1 (by default 15) + return 0 if ($distance >= $clientSight); - my ($min_x, $min_y, $max_x, $max_y) = fieldAreaCorrectEdges($field, ($start->{x} - $dist_from_center), ($start->{y} - $dist_from_center), ($start->{x} + $dist_from_center), ($start->{y} + $dist_from_center)); + my $client_distance = getClientDist($pos1, $pos2); + return 0 unless ($client_distance <= $range); - return ($min_x, $min_y, $max_x, $max_y); + return -1 unless ($field->checkLOS($pos1, $pos2, $attackCanSnipe)); + + return 1; +} + +# Only God and gravity developers know why this is done this way, but I tested in the client and it works 100% of the time +# Probably done this way because the client actually calculates distance in 3D and takes in consideration height ou the GAT file +# To save processing time the server just removes some distance (0.0625 in Hercules and 0.1 in rathena) to compensate +# Bound to fail sometimes as the server itself will fail in some cases +# This actually makes it so that openkore can target, in very specific cases, targets a bit further away than the client can (very large height diference) +# Reference: rathena src\map\path.c distance_client +sub getClientDist { + my ($pos1, $pos2) = @_; + my $xD = abs($pos1->{x} - $pos2->{x}); + my $yD = abs($pos1->{y} - $pos2->{y}); + my $temp_dist = sqrt(($xD*$xD) + ($yD*$yD)); + $temp_dist -= 0.1; + $temp_dist = 0 if($temp_dist < 0); + $temp_dist = int($temp_dist); + return $temp_dist } ## -# calcStepsWalkedFromTimeAndRoute(solution, speed, time_elapsed) +# blockDistance(pos1, pos2) +# pos1, pos2: references to position hash tables. +# Returns: the distance in number of blocks (integer). +# +# Calculates the distance in number of blocks between pos1 and pos2. +# This is used for e.g. weapon range calculation. +# +# Reference: hercules src\map\path.c distance +sub blockDistance { + my ($pos1, $pos2) = @_; + + return max(abs($pos1->{x} - $pos2->{x}), + abs($pos1->{y} - $pos2->{y})); +} + +## +# calcStepsWalkedFromTimeAndSolution(solution, speed, time_elapsed) # solution: Reference to an array in which the solution is stored. It will contain hashes of x and y coordinates from the start to the end of the path, the first array element should be the current position. # speed: The actor speed in blocks / second. # time_elapsed: The amount of time that has passed since movement started. @@ -255,106 +296,213 @@ sub getSquareEdgesFromCoord { # # Example: # my $steps_walked; -# $steps_walked = calcStepsWalkedFromTimeAndRoute($solution, $speed, $time_elapsed) +# $steps_walked = calcStepsWalkedFromTimeAndSolution($solution, $speed, $time_elapsed) # print "You are currently at: $solution->[$steps_walked]{x} $solution->[$steps_walked]{y}\n"; -sub calcStepsWalkedFromTimeAndRoute { - my ($solution, $speed, $time_elapsed) = @_; +sub calcStepsWalkedFromTimeAndSolution { + my ($solution, $speed, $time_elapsed) = @_; - my $stepType = 0; # 1 - vertical or horizontal; 2 - diagonal - my $current_step = 0; # step + my $stepType = 0; # 1 - vertical or horizontal; 2 - diagonal + my $current_step = 0; # step my %current_pos = ( x => $solution->[0]{x}, y => $solution->[0]{y} ); - my %next_pos; + my %next_pos; my @steps = @{$solution}[1..$#{$solution}]; - my $dist = @steps; + my $dist = @steps; - my $time_needed_ortogonal = $speed; - my $time_needed_diagonal = sqrt(2) * $speed; - my $time_needed; + my $time_needed_ortogonal = $speed; + my $time_needed_diagonal = $speed * (MOVE_DIAGONAL_COST / MOVE_COST); + my $time_needed; - while ($current_step < $dist) { - %next_pos = ( x => $steps[$current_step]{x}, y => $steps[$current_step]{y} ); + while ($current_step < $dist) { + %next_pos = ( x => $steps[$current_step]{x}, y => $steps[$current_step]{y} ); - $stepType = 0; + $stepType = 0; - if ($current_pos{x} != $next_pos{x}) { - $stepType++; - } + if ($current_pos{x} != $next_pos{x}) { + $stepType++; + } - if ($current_pos{y} != $next_pos{y}) { - $stepType++; - } + if ($current_pos{y} != $next_pos{y}) { + $stepType++; + } - if ($stepType == 2) { - $time_needed = $time_needed_diagonal; - } elsif ($stepType == 1) { - $time_needed = $time_needed_ortogonal; - } + if ($stepType == 2) { + $time_needed = $time_needed_diagonal; + } elsif ($stepType == 1) { + $time_needed = $time_needed_ortogonal; + } - if ($time_elapsed > $time_needed) { - $time_elapsed -= $time_needed; - %current_pos = %next_pos; - $current_step++; - } else { - last; - } - } + if ($time_elapsed > $time_needed) { + $time_elapsed -= $time_needed; + %current_pos = %next_pos; + $current_step++; + } else { + last; + } + } - return $current_step; + return $current_step; } ## -# calcTimeFromRoute(solution, speed) +# calcTimeFromSolution(solution, speed) # solution: Reference to an array in which the solution is stored. It will contain hashes of x and y coordinates from the start to the end of the path, the first array element should be the current position. # speed: The actor speed in blocks / second. # -# Returns the amount of seconds to walk the given route with the given speed. -sub calcTimeFromRoute { - my ($solution, $speed) = @_; +# Returns the amount of seconds to walk the given Solution with the given speed. +sub calcTimeFromSolution { + my ($solution, $speed) = @_; - my $stepType = 0; # 1 - vertical or horizontal; 2 - diagonal - my $current_step = 0; # step + my $stepType = 0; # 1 - vertical or horizontal; 2 - diagonal + my $current_step = 0; # step my %current_pos = ( x => $solution->[0]{x}, y => $solution->[0]{y} ); - my %next_pos; + my %next_pos; my @steps = @{$solution}[1..$#{$solution}]; - my $dist = @steps; + my $dist = @steps; - my $time_needed_ortogonal = $speed; - my $time_needed_diagonal = sqrt(2) * $speed; - my $time_needed; + my $time_needed_ortogonal = $speed; + my $time_needed_diagonal = $speed * (MOVE_DIAGONAL_COST / MOVE_COST); + my $time_needed; my $summed_time = 0; - while ($current_step < $dist) { - %next_pos = ( x => $steps[$current_step]{x}, y => $steps[$current_step]{y} ); + while ($current_step < $dist) { + %next_pos = ( x => $steps[$current_step]{x}, y => $steps[$current_step]{y} ); - $stepType = 0; + $stepType = 0; - if ($current_pos{x} != $next_pos{x}) { - $stepType++; - } + if ($current_pos{x} != $next_pos{x}) { + $stepType++; + } - if ($current_pos{y} != $next_pos{y}) { - $stepType++; - } + if ($current_pos{y} != $next_pos{y}) { + $stepType++; + } - if ($stepType == 2) { - $time_needed = $time_needed_diagonal; - } elsif ($stepType == 1) { - $time_needed = $time_needed_ortogonal; - } + if ($stepType == 2) { + $time_needed = $time_needed_diagonal; + } elsif ($stepType == 1) { + $time_needed = $time_needed_ortogonal; + } $summed_time += $time_needed; %current_pos = %next_pos; - $current_step++; - } + $current_step++; + } + + return $summed_time; +} + +## +# calcPosFromTime(pos, pos_to, speed, time) +# +# Returns: the position where an actor moving from $pos to $pos_to with +# the speed $speed will be in $time amount of time. +# +# Walls and pathfinding are not considered. +sub calcPosFromTime { + my ($pos, $pos_to, $speed, $time) = @_; + + # If Pos and PosTo are the same return Pos + if ($pos->{x} == $pos_to->{x} && $pos->{y} == $pos_to->{y}) { + return $pos; + } + + my $solution = get_client_easy_solution($pos, $pos_to); + my $steps_walked = calcStepsWalkedFromTimeAndSolution($solution, $speed, $time); + my $pos = $solution->[$steps_walked]; + return $pos; +} + +## +# calcTime(pos, pos_to, speed) +# +# Returns: time to move from $pos to $pos_to with $speed speed. +# +# Walls and pathfinding are not considered. +sub calcTime { + my ($pos, $pos_to, $speed) = @_; - return $summed_time; + if ($pos->{x} == $pos_to->{x} && $pos->{y} == $pos_to->{y}) { + return 0; + } + + my $solution = get_client_easy_solution($pos, $pos_to); + my $summed_time = calcTimeFromSolution($solution, $speed); + return $summed_time; +} + +## +# calcPosition(object, [extra_time, float]) +# object: $char (yourself), or a value in %monsters or %players. +# float: If set to 1, return coordinates as floating point. +# Returns: reference to a position hash. +# +# Walls and pathfinding are not considered. +# +# The position information server that the server sends indicates a motion: +# it says that an object is walking from A to B, and that it will arrive at B shortly. +# This function calculates the current position of $object based on the motion information. +# +# If $extra_time is given, this function will calculate where $object will be +# after $extra_time seconds. +# +# Example: +# my $pos; +# $pos = calcPosition($char); +# print "You are currently at: $pos->{x}, $pos->{y}\n"; +# +# $pos = calcPosition($monsters{$ID}); +# # Calculate where the player will be after 2 seconds +# $pos = calcPosition($players{$ID}, 2); +sub calcPosition { + my ($object, $extra_time, $float) = @_; + my $time_needed = $object->{time_move_calc}; + my $elasped = time - $object->{time_move} + $extra_time; + + if ($elasped >= $time_needed || !$time_needed) { + return $object->{pos_to}; + } else { + my (%vec, %result, $dist); + my $pos = $object->{pos}; + my $pos_to = $object->{pos_to}; + + getVector(\%vec, $pos_to, $pos); + $dist = (distance($pos, $pos_to) - 1) * ($elasped / $time_needed); + moveAlongVector(\%result, $pos, \%vec, $dist); + $result{x} = int sprintf("%.0f", $result{x}) if (!$float); + $result{y} = int sprintf("%.0f", $result{y}) if (!$float); + return \%result; + } +} + +# Only God and gravity developers know why this is done this way, but I tested in the client and it works 100% of the time +# +# Reference: hercules src\map\path.c distance_client +# 956ns -> 618ns +sub getClientDist { + my ($pos1, $pos2) = @_; + return PathFinding::getClientDist($pos1->{x}, $pos1->{y}, $pos2->{x}, $pos2->{y}); +} + +## +# blockDistance(pos1, pos2) +# pos1, pos2: references to position hash tables. +# Returns: the distance in number of blocks (integer). +# +# Calculates the distance in number of blocks between pos1 and pos2. +# This is used for e.g. weapon range calculation. +# +# Reference: hercules src\map\path.c distance +# 650ns -> 580ns +sub blockDistance { + my ($pos1, $pos2) = @_; + return PathFinding::blockDistance($pos1->{x}, $pos1->{y}, $pos2->{x}, $pos2->{y}); } ## @@ -381,36 +529,6 @@ sub checkMovementDirection { (($obj1ToObj2Degree - $movementDegree) % 360) <= $fuzziness; } -## -# countSteps(pos, pos_to) -# -# Returns: the number of steps from $pos to $pos_to. -# Walls are not considered. -sub countSteps { - my ($pos, $pos_to) = @_; - my $posX = $$pos{x}; - my $posY = $$pos{y}; - my $pos_toX = $$pos_to{x}; - my $pos_toY = $$pos_to{y}; - my $s = 0; # steps - while ($posX ne $pos_toX || $posY ne $pos_toY) { - $s++; - if ($posX < $pos_toX) { - $posX++; - } - if ($posX > $pos_toX) { - $posX--; - } - if ($posY < $pos_toY) { - $posY++; - } - if ($posY > $pos_toY) { - $posY--; - } - } - return $s; -} - ## # distance(r_hash1, r_hash2) # pos1, pos2: references to position hash tables. @@ -469,20 +587,6 @@ sub intToSignedShort { } } -## -# blockDistance(pos1, pos2) -# pos1, pos2: references to position hash tables. -# Returns: the distance in number of blocks (integer). -# -# Calculates the distance in number of blocks between pos1 and pos2. -# This is used for e.g. weapon range calculation. -sub blockDistance { - my ($pos1, $pos2) = @_; - - return max(abs($pos1->{x} - $pos2->{x}), - abs($pos1->{y} - $pos2->{y})); -} - ## # specifiedBlockDistance(pos1, pos2) # pos1, pos2: references to position hash tables. @@ -1145,8 +1249,11 @@ sub makeCoordsDir { # Another 1 byte or 2*4 bits are reserved for a clientside feature: # x0+=sx0*0.0625-0.5 and y0+=sy0*0.0625-0.5 # Note: if sx0/sy0 is 8, this will respectively add 0 to x0/y0 +# Reference: hercules src\map\clif.c // client-side: x0+=sx0*0.0625-0.5 and y0+=sy0*0.0625-0.5 # # ex. walk packet (4 + 4 + 10 + 10 + 10 + 10 = 48 bits = 6 bytes = a6) +# +# TODO: Maybe aegis, athena, cronus, brathena or other emulators actually use this sx0/sy0 argument and we don't know sub makeCoordsFromTo { my ($r_hashFrom, $r_hashTo, $rawCoords) = @_; unShiftPack(\$rawCoords, undef, 4); # seems to be returning 8 (always?) diff --git a/src/Utils/PathFinding.pm b/src/Utils/PathFinding.pm index 40e0b0eee1..37d52df7dd 100644 --- a/src/Utils/PathFinding.pm +++ b/src/Utils/PathFinding.pm @@ -75,11 +75,13 @@ sub new { # Optional arguments: # `l # - timeout: the number of milliseconds to run each step for, defaults to 1500 -# - avoidWalls: of walls should be avoided during pathing, defaults to 1 +# - avoidWalls: if walls should be avoided during pathing, defaults to 1 # - min_x: limits the map in a certain minimum x coordinate, defaults to 0 # - max_x: limits the map in a certain maximum x coordinate, defaults to width-1 # - min_y: limits the map in a certain minimum y coordinate, defaults to 0 # - max_y: limits the map in a certain maximum y coordinate, defaults to height-1 +# - customWeights: if secondWeightMap should be used during pathing, defaults to 0 +# - secondWeightMap: An array of hashes containing 3 keys, 'x', 'y' and 'weight', for all the cells which had their weight changed, 'weight' is the weight of the cell, defaults to undef # `l` sub reset { my $class = shift; @@ -102,11 +104,18 @@ sub reset { $hookArgs{return} = 1; Plugins::callHook('PathFindingReset', \%hookArgs); if ($hookArgs{return}) { + $args{avoidWalls} = 1 unless (defined $args{avoidWalls}); $args{weight_map} = \($args{field}->{weightMap}) unless (defined $args{weight_map}); + + $args{customWeights} = 0 unless (defined $args{customWeights}); + $args{secondWeightMap} = undef unless (defined $args{secondWeightMap}); + + $args{randomFactor} = 0 unless (defined $args{randomFactor}); + $args{useManhattan} = 0 unless (defined $args{useManhattan}); + $args{width} = $args{field}{width} unless (defined $args{width}); $args{height} = $args{field}{height} unless (defined $args{height}); $args{timeout} = 1500 unless (defined $args{timeout}); - $args{avoidWalls} = 1 unless (defined $args{avoidWalls}); $args{min_x} = 0 unless (defined $args{min_x}); $args{max_x} = ($args{width}-1) unless (defined $args{max_x}); $args{min_y} = 0 unless (defined $args{min_y}); @@ -114,9 +123,13 @@ sub reset { } return $class->_reset( - $args{weight_map}, - $args{avoidWalls}, - $args{width}, + $args{weight_map}, + $args{avoidWalls}, + $args{customWeights}, + $args{secondWeightMap}, + $args{randomFactor}, + $args{useManhattan}, + $args{width}, $args{height}, $args{start}{x}, $args{start}{y}, @@ -133,7 +146,7 @@ sub reset { ## # $PathFinding->run(solution_array) -# solution_array: Reference to an array in which the solution is stored. It will contain hashes of x and y coordinates from the start to the end of the path. +# solution_array: Reference to an array in which the solution is stored. It will contain hashes of x and y coordinates from the start to the end of the path, including the starting pos # Returns: # -3 when pathfinding is not yet complete. # -2 when Pathfinding->reset was not called. diff --git a/src/auto/XSTools/PathFinding/PathFinding.xs b/src/auto/XSTools/PathFinding/PathFinding.xs index 06a3bd100c..eda9dfba51 100644 --- a/src/auto/XSTools/PathFinding/PathFinding.xs +++ b/src/auto/XSTools/PathFinding/PathFinding.xs @@ -1,4 +1,5 @@ #include +#include #include "EXTERN.h" #include "perl.h" #include "XSUB.h" @@ -19,10 +20,14 @@ PathFinding_create() void -PathFinding__reset(session, weight_map, avoidWalls, width, height, startx, starty, destx, desty, time_max, min_x, max_x, min_y, max_y) +PathFinding__reset(session, weight_map, avoidWalls, customWeights, secondWeightMap, randomFactor, useManhattan, width, height, startx, starty, destx, desty, time_max, min_x, max_x, min_y, max_y) PathFinding session - SV *weight_map + SV * weight_map SV * avoidWalls + SV * customWeights + SV * secondWeightMap + SV * randomFactor + SV * useManhattan SV * width SV * height SV * startx @@ -34,161 +39,324 @@ PathFinding__reset(session, weight_map, avoidWalls, width, height, startx, start SV * max_x SV * min_y SV * max_y - + PREINIT: char *weight_map_data = NULL; - + CODE: - + /* If the object was already initiated, clean map memory */ if (session->initialized) { free_currentMap(session); session->initialized = 0; } - + /* If the path has already been calculated on this object, clean openlist memory */ if (session->run) { free_openList(session); session->run = 0; } - + /* Check for any missing arguments */ - if (!session || !weight_map || !avoidWalls || !width || !height || !startx || !starty || !destx || !desty || !time_max || !min_x || !max_x || !min_y || !max_y) { + if (!session || !weight_map || !avoidWalls || !customWeights || !secondWeightMap || !randomFactor || !useManhattan || !width || !height || !startx || !starty || !destx || !desty || !time_max || !min_x || !max_x || !min_y || !max_y) { printf("[pathfinding reset error] missing argument\n"); XSRETURN_NO; } - + /* Check for any bad arguments */ if (SvROK(avoidWalls) || SvTYPE(avoidWalls) >= SVt_PVAV || !SvOK(avoidWalls)) { printf("[pathfinding reset error] bad avoidWalls argument\n"); XSRETURN_NO; } - + + if (SvROK(customWeights) || SvTYPE(customWeights) >= SVt_PVAV || !SvOK(customWeights)) { + printf("[pathfinding reset error] bad customWeights argument\n"); + XSRETURN_NO; + } + + if (SvROK(randomFactor) || SvTYPE(randomFactor) >= SVt_PVAV || !SvOK(randomFactor)) { + printf("[pathfinding reset error] bad randomFactor argument\n"); + XSRETURN_NO; + } + + if (SvROK(useManhattan) || SvTYPE(useManhattan) >= SVt_PVAV || !SvOK(useManhattan)) { + printf("[pathfinding reset error] bad useManhattan argument\n"); + XSRETURN_NO; + } + if (SvROK(width) || SvTYPE(width) >= SVt_PVAV || !SvOK(width)) { printf("[pathfinding reset error] bad width argument\n"); XSRETURN_NO; } - + if (SvROK(height) || SvTYPE(height) >= SVt_PVAV || !SvOK(height)) { printf("[pathfinding reset error] bad height argument\n"); XSRETURN_NO; } - + if (SvROK(startx) || SvTYPE(startx) >= SVt_PVAV || !SvOK(startx)) { printf("[pathfinding reset error] bad startx argument\n"); XSRETURN_NO; } - + if (SvROK(starty) || SvTYPE(starty) >= SVt_PVAV || !SvOK(starty)) { printf("[pathfinding reset error] bad starty argument\n"); XSRETURN_NO; } - + if (SvROK(destx) || SvTYPE(destx) >= SVt_PVAV || !SvOK(destx)) { printf("[pathfinding reset error] bad destx argument\n"); XSRETURN_NO; } - + if (SvROK(desty) || SvTYPE(desty) >= SVt_PVAV || !SvOK(desty)) { printf("[pathfinding reset error] bad desty argument\n"); XSRETURN_NO; } - + if (SvROK(time_max) || SvTYPE(time_max) >= SVt_PVAV || !SvOK(time_max)) { printf("[pathfinding reset error] bad time_max argument\n"); XSRETURN_NO; } - + if (!SvROK(weight_map) || !SvOK(weight_map)) { printf("[pathfinding reset error] bad weight_map argument\n"); XSRETURN_NO; } - + if (SvROK(min_x) || SvTYPE(min_x) >= SVt_PVAV || !SvOK(min_x)) { printf("[pathfinding reset error] bad min_x argument\n"); XSRETURN_NO; } - + if (SvROK(max_x) || SvTYPE(max_x) >= SVt_PVAV || !SvOK(max_x)) { printf("[pathfinding reset error] bad max_x argument\n"); XSRETURN_NO; } - + if (SvROK(min_y) || SvTYPE(min_y) >= SVt_PVAV || !SvOK(min_y)) { printf("[pathfinding reset error] bad min_y argument\n"); XSRETURN_NO; } - + if (SvROK(max_y) || SvTYPE(max_y) >= SVt_PVAV || !SvOK(max_y)) { printf("[pathfinding reset error] bad max_y argument\n"); XSRETURN_NO; } - + /* Get the weight_map data */ weight_map_data = (char *) SvPV_nolen (SvRV (weight_map)); session->map_base_weight = weight_map_data; - + session->width = (int) SvUV (width); session->height = (int) SvUV (height); - + session->startX = (int) SvUV (startx); session->startY = (int) SvUV (starty); session->endX = (int) SvUV (destx); session->endY = (int) SvUV (desty); - + session->min_x = (int) SvUV (min_x); session->max_x = (int) SvUV (max_x); session->min_y = (int) SvUV (min_y); session->max_y = (int) SvUV (max_y); - + + srand(time(0)); + session->randomFactor = (unsigned int) SvUV (randomFactor); + session->useManhattan = (unsigned short) SvUV (useManhattan); + // Min and max check if (session->min_x >= session->width || session->min_y >= session->height || session->min_x < 0 || session->min_y < 0) { printf("[pathfinding reset error] Minimum coordinates %d %d are out of the map (size: %d x %d).\n", session->min_x, session->min_y, session->width, session->height); XSRETURN_NO; } - + if (session->max_x >= session->width || session->max_y >= session->height || session->max_x < 0 || session->max_y < 0) { printf("[pathfinding reset error] Maximum coordinates %d %d are out of the map (size: %d x %d).\n", session->max_x, session->max_y, session->width, session->height); XSRETURN_NO; } - + // Start check if (session->startX >= session->width || session->startY >= session->height || session->startX < 0 || session->startY < 0) { printf("[pathfinding reset error] Start coordinate %d %d is out of the map (size: %d x %d).\n", session->startX, session->startY, session->width, session->height); XSRETURN_NO; } - + if (session->map_base_weight[((session->startY * session->width) + session->startX)] == -1) { printf("[pathfinding reset error] Start coordinate %d %d is not a walkable cell.\n", session->startX, session->startY); XSRETURN_NO; } - + if (session->startX > session->max_x || session->startY > session->max_y || session->startX < session->min_x || session->startY < session->min_y) { printf("[pathfinding reset error] Start coordinate %d %d is out of the minimum and maximum coordinates (size: %d .. %d x %d .. %d).\n", session->startX, session->startY, session->min_x, session->max_x, session->min_y, session->max_y); XSRETURN_NO; } - + // End check if (session->endX >= session->width || session->endY >= session->height || session->endX < 0 || session->endY < 0) { printf("[pathfinding reset error] End coordinate %d %d is out of the map (size: %d x %d).\n", session->endX, session->endY, session->width, session->height); XSRETURN_NO; } - + if (session->map_base_weight[((session->endY * session->width) + session->endX)] == -1) { printf("[pathfinding reset error] End coordinate %d %d is not a walkable cell.\n", session->endX, session->endY); XSRETURN_NO; } - + if (session->endX > session->max_x || session->endY > session->max_y || session->endX < session->min_x || session->endY < session->min_y) { printf("[pathfinding reset error] End coordinate %d %d is out of the minimum and maximum coordinates (size: %d .. %d x %d .. %d).\n", session->endX, session->endY, session->min_x, session->max_x, session->min_y, session->max_y); XSRETURN_NO; } - + session->avoidWalls = (unsigned short) SvUV (avoidWalls); + session->customWeights = (unsigned short) SvUV (customWeights); session->time_max = (unsigned int) SvUV (time_max); - + CalcPath_init(session); + if (session->customWeights) { + /* secondWeightMap should be a reference to an array */ + if (!SvROK(secondWeightMap)) { + printf("[pathfinding reset error] secondWeightMap is not a reference\n"); + XSRETURN_NO; + } + + if (SvTYPE(SvRV(secondWeightMap)) != SVt_PVAV) { + printf("[pathfinding reset error] secondWeightMap is not an array reference\n"); + XSRETURN_NO; + } + + if (!SvOK(secondWeightMap)) { + printf("[pathfinding reset error] secondWeightMap is not defined\n"); + XSRETURN_NO; + } + + AV *deref_secondWeightMap; + I32 array_len; + + deref_secondWeightMap = (AV *) SvRV (secondWeightMap); + array_len = av_len (deref_secondWeightMap); + + if (array_len == -1) { + printf("[pathfinding reset error] secondWeightMap has no members\n"); + XSRETURN_NO; + } + + SV **fetched; + HV *hash; + + SV **ref_x; + SV **ref_y; + SV **ref_weight; + + IV x; + IV y; + + I32 index; + + for (index = 0; index <= array_len; index++) { + fetched = av_fetch (deref_secondWeightMap, index, 0); + + if (!SvROK(*fetched)) { + printf("[pathfinding reset error] [secondWeightMap] member of array is not a reference\n"); + XSRETURN_NO; + } + + if (SvTYPE(SvRV(*fetched)) != SVt_PVHV) { + printf("[pathfinding reset error] [secondWeightMap] member of array is not a reference to a hash\n"); + XSRETURN_NO; + } + + if (!SvOK(*fetched)) { + printf("[pathfinding reset error] [secondWeightMap] member of array is not defined\n"); + XSRETURN_NO; + } + + hash = (HV*) SvRV(*fetched); + + if (!hv_exists(hash, "x", 1)) { + printf("[pathfinding reset error] [secondWeightMap] member of array does not contain the key 'x'\n"); + XSRETURN_NO; + } + + ref_x = hv_fetch(hash, "x", 1, 0); + + if (SvROK(*ref_x)) { + printf("[pathfinding reset error] [secondWeightMap] member of array 'x' key is a reference\n"); + XSRETURN_NO; + } + + if (SvTYPE(*ref_x) >= SVt_PVAV) { + printf("[pathfinding reset error] [secondWeightMap] member of array 'x' key is not a scalar\n"); + XSRETURN_NO; + } + + if (!SvOK(*ref_x)) { + printf("[pathfinding reset error] [secondWeightMap] member of array 'x' key is not defined\n"); + XSRETURN_NO; + } + + x = SvIV(*ref_x); + + if (!hv_exists(hash, "y", 1)) { + printf("[pathfinding reset error] [secondWeightMap] member of array does not contain the key 'y'\n"); + XSRETURN_NO; + } + + ref_y = hv_fetch(hash, "y", 1, 0); + + if (SvROK(*ref_y)) { + printf("[pathfinding reset error] [secondWeightMap] member of array 'y' key is a reference\n"); + XSRETURN_NO; + } + + if (SvTYPE(*ref_y) >= SVt_PVAV) { + printf("[pathfinding reset error] [secondWeightMap] member of array 'y' key is not a scalar\n"); + XSRETURN_NO; + } + + if (!SvOK(*ref_y)) { + printf("[pathfinding reset error] [secondWeightMap] member of array 'y' key is not defined\n"); + XSRETURN_NO; + } + + y = SvIV(*ref_y); + + if (!hv_exists(hash, "weight", 6)) { + printf("[pathfinding reset error] [secondWeightMap] member of array does not contain the key 'weight'\n"); + XSRETURN_NO; + } + + ref_weight = hv_fetch(hash, "weight", 6, 0); + + if (SvROK(*ref_weight)) { + printf("[pathfinding reset error] [secondWeightMap] member of array 'weight' key is a reference\n"); + XSRETURN_NO; + } + + if (SvTYPE(*ref_weight) >= SVt_PVAV) { + printf("[pathfinding reset error] [secondWeightMap] member of array 'weight' key is not a scalar\n"); + XSRETURN_NO; + } + + if (!SvOK(*ref_weight)) { + printf("[pathfinding reset error] [secondWeightMap] member of array 'weight' key is not defined\n"); + XSRETURN_NO; + } + + unsigned int weight = SvIV(*ref_weight); + + long current = (y * session->width) + x; + + session->second_weight_map[current] = weight; + } + } else { + if (SvOK(secondWeightMap)) { + printf("[pathfinding reset error] secondWeightMap is defined while customWeights is 0\n"); + XSRETURN_NO; + } + } + int PathFinding_run(session, solution_array) PathFinding session @@ -196,61 +364,63 @@ PathFinding_run(session, solution_array) PREINIT: int status; CODE: - + /* Check for any missing arguments */ if (!session || !solution_array) { printf("[pathfinding run error] missing argument\n"); XSRETURN_NO; } - + /* solution_array should be a reference to an array */ if (!SvROK(solution_array)) { printf("[pathfinding run error] solution_array is not a reference\n"); XSRETURN_NO; } - + if (SvTYPE(SvRV(solution_array)) != SVt_PVAV) { printf("[pathfinding run error] solution_array is not an array reference\n"); XSRETURN_NO; } - + if (!SvOK(solution_array)) { printf("[pathfinding run error] solution_array is not defined\n"); XSRETURN_NO; } status = CalcPath_pathStep (session); - + if (status < 0) { RETVAL = status; } else { AV *array; - int size; + long size; - size = session->solution_size; + size = (session->solution_size + 1); array = (AV *) SvRV (solution_array); - if (av_len (array) > size) - av_clear (array); - - av_extend (array, session->solution_size); - + av_clear (array); + av_extend (array, size); + Node currentNode = session->currentMap[(session->endY * session->width) + session->endX]; + long current = session->solution_size; - while (currentNode.x != session->startX || currentNode.y != session->startY) + while (1) { HV * rh = (HV *)sv_2mortal((SV *)newHV()); hv_store(rh, "x", 1, newSViv(currentNode.x), 0); hv_store(rh, "y", 1, newSViv(currentNode.y), 0); - - av_unshift(array, 1); - av_store(array, 0, newRV((SV *)rh)); - - currentNode = session->currentMap[currentNode.predecessor]; + av_store(array, current, newRV((SV *)rh)); + + if (current == 0) { + break; + } else { + currentNode = session->currentMap[currentNode.predecessor]; + current--; + } } - + RETVAL = size; } OUTPUT: @@ -264,11 +434,11 @@ PathFinding_runcount(session) CODE: status = CalcPath_pathStep (session); - + if (status < 0) { RETVAL = status; } else { - RETVAL = (int) session->solution_size; + RETVAL = (long) session->solution_size; } OUTPUT: RETVAL @@ -280,3 +450,346 @@ PathFinding_DESTROY(session) session = (PathFinding) 0; /* shut up compiler warning */ CODE: CalcPath_destroy (session); + +int +PathFinding_checkTile(ix, iy, itile, iwidth, iheight, rawMap) + SV * ix + SV * iy + SV * itile + SV * iwidth + SV * iheight + SV * rawMap + + CODE: + int x = (int) SvUV (ix); + int y = (int) SvUV (iy); + int tile = (int) SvUV (itile); + int width = (int) SvUV (iwidth); + int height = (int) SvUV (iheight); + + char * rawMap_data = (char *) SvPVbyte_nolen (SvRV (rawMap)); + + RETVAL = checkTile_inner(x, y, tile, width, height, rawMap_data); + + OUTPUT: + RETVAL + +int +PathFinding_checkLOS(istart_x, istart_y, iend_x, iend_y, itile, iwidth, iheight, rawMap) + SV * istart_x + SV * istart_y + SV * iend_x + SV * iend_y + SV * itile + SV * iwidth + SV * iheight + SV * rawMap + + CODE: + int start_x = (int) SvUV (istart_x); + int start_y = (int) SvUV (istart_y); + int end_x = (int) SvUV (iend_x); + int end_y = (int) SvUV (iend_y); + int tile = (int) SvUV (itile); + int width = (int) SvUV (iwidth); + int height = (int) SvUV (iheight); + + char * rawMap_data = (char *) SvPVbyte_nolen (SvRV (rawMap)); + + RETVAL = checkLOS_inner(start_x, start_y, end_x, end_y, tile, width, height, rawMap_data); + + OUTPUT: + RETVAL + +int +PathFinding_canAttack(istart_x, istart_y, iend_x, iend_y, itile, iwidth, iheight, irange, iclientSight, rawMap) + SV * istart_x + SV * istart_y + SV * iend_x + SV * iend_y + SV * itile + SV * iwidth + SV * iheight + SV * irange + SV * iclientSight + SV * rawMap + + CODE: + int start_x = (int) SvUV (istart_x); + int start_y = (int) SvUV (istart_y); + int end_x = (int) SvUV (iend_x); + int end_y = (int) SvUV (iend_y); + int tile = (int) SvUV (itile); + int width = (int) SvUV (iwidth); + int height = (int) SvUV (iheight); + int range = (int) SvUV (irange); + int clientSight = (int) SvUV (iclientSight); + + char * rawMap_data = (char *) SvPVbyte_nolen (SvRV (rawMap)); + + RETVAL = canAttack_inner(start_x, start_y, end_x, end_y, tile, width, height, range, clientSight, rawMap_data); + + OUTPUT: + RETVAL + +void +PathFinding_calcRectArea(i_x, i_y, iradius, itile, iwidth, iheight, rawMap, solution_array) + SV * i_x + SV * i_y + SV * iradius + SV * itile + SV * iwidth + SV * iheight + SV * rawMap + SV * solution_array + + CODE: + int x = (int) SvUV (i_x); + int y = (int) SvUV (i_y); + int radius = (int) SvUV (iradius); + int tile = (int) SvUV (itile); + int width = (int) SvUV (iwidth); + int height = (int) SvUV (iheight); + + char * rawMap_data = (char *) SvPVbyte_nolen (SvRV (rawMap)); + + int * limits = getSquareEdgesFromCoord_inner(x, y, radius, width, height); + int min_x = limits[0]; + int min_y = limits[1]; + int max_x = limits[2]; + int max_y = limits[3]; + + AV *array; + array = (AV *) SvRV (solution_array); + av_clear (array); + + int offset; + + int value; + + int size; + + x = min_x; + y = min_y; + offset = (y * width) + x; + size = 0; + + while (x < max_x) { + value = rawMap_data[offset]; + if (value & tile) { + av_extend (array, (size+1)); + HV * rh = (HV *)sv_2mortal((SV *)newHV()); + + hv_store(rh, "x", 1, newSViv(x), 0); + hv_store(rh, "y", 1, newSViv(y), 0); + + av_store(array, size, newRV((SV *)rh)); + size++; + } + offset++; + x++; + } + + while (y < max_y) { + value = rawMap_data[offset]; + if (value & tile) { + av_extend (array, (size+1)); + HV * rh = (HV *)sv_2mortal((SV *)newHV()); + + hv_store(rh, "x", 1, newSViv(x), 0); + hv_store(rh, "y", 1, newSViv(y), 0); + + av_store(array, size, newRV((SV *)rh)); + size++; + } + offset += width; + y++; + } + + while (x > min_x) { + value = rawMap_data[offset]; + if (value & tile) { + av_extend (array, (size+1)); + HV * rh = (HV *)sv_2mortal((SV *)newHV()); + + hv_store(rh, "x", 1, newSViv(x), 0); + hv_store(rh, "y", 1, newSViv(y), 0); + + av_store(array, size, newRV((SV *)rh)); + size++; + } + offset--; + x--; + } + + while (y > min_y) { + value = rawMap_data[offset]; + if (value & tile) { + av_extend (array, (size+1)); + HV * rh = (HV *)sv_2mortal((SV *)newHV()); + + hv_store(rh, "x", 1, newSViv(x), 0); + hv_store(rh, "y", 1, newSViv(y), 0); + + av_store(array, size, newRV((SV *)rh)); + size++; + } + offset -= width; + y--; + } + +int +PathFinding_checkPathFree(istart_x, istart_y, iend_x, iend_y, itile, iwidth, iheight, rawMap) + SV * istart_x + SV * istart_y + SV * iend_x + SV * iend_y + SV * itile + SV * iwidth + SV * iheight + SV * rawMap + + CODE: + int start_x = (int) SvUV (istart_x); + int start_y = (int) SvUV (istart_y); + int end_x = (int) SvUV (iend_x); + int end_y = (int) SvUV (iend_y); + int tile = (int) SvUV (itile); + int width = (int) SvUV (iwidth); + int height = (int) SvUV (iheight); + + char * rawMap_data = (char *) SvPVbyte_nolen (SvRV (rawMap)); + + RETVAL = checkPathFree_inner(start_x, start_y, end_x, end_y, tile, width, height, rawMap_data); + + OUTPUT: + RETVAL + +void +PathFinding_getSquareEdgesFromCoord(i_x, i_y, iradius, iwidth, iheight, solution_array) + SV * i_x + SV * i_y + SV * iradius + SV * iwidth + SV * iheight + SV * solution_array + + CODE: + int x = (int) SvUV (i_x); + int y = (int) SvUV (i_y); + int radius = (int) SvUV (iradius); + int width = (int) SvUV (iwidth); + int height = (int) SvUV (iheight); + + int * limits = getSquareEdgesFromCoord_inner(x, y, radius, width, height); + + AV *array; + array = (AV *) SvRV (solution_array); + av_clear (array); + av_extend (array, 4); + + av_store(array, 0, newSViv(limits[0])); + av_store(array, 1, newSViv(limits[1])); + av_store(array, 2, newSViv(limits[2])); + av_store(array, 3, newSViv(limits[3])); + +int +PathFinding_blockDistance(istart_x, istart_y, iend_x, iend_y) + SV * istart_x + SV * istart_y + SV * iend_x + SV * iend_y + + CODE: + int start_x = (int) SvUV (istart_x); + int start_y = (int) SvUV (istart_y); + int end_x = (int) SvUV (iend_x); + int end_y = (int) SvUV (iend_y); + + RETVAL = blockDistance_inner(start_x, start_y, end_x, end_y); + + OUTPUT: + RETVAL + +int +PathFinding_getClientDist(istart_x, istart_y, iend_x, iend_y) + SV * istart_x + SV * istart_y + SV * iend_x + SV * iend_y + + CODE: + int start_x = (int) SvUV (istart_x); + int start_y = (int) SvUV (istart_y); + int end_x = (int) SvUV (iend_x); + int end_y = (int) SvUV (iend_y); + + RETVAL = getClientDist_inner(start_x, start_y, end_x, end_y); + + OUTPUT: + RETVAL + +int +PathFinding_get_client_easy_solution(istart_x, istart_y, iend_x, iend_y, solution_array) + SV * istart_x + SV * istart_y + SV * iend_x + SV * iend_y + SV * solution_array + + CODE: + int start_x = (int) SvUV (istart_x); + int start_y = (int) SvUV (istart_y); + int end_x = (int) SvUV (iend_x); + int end_y = (int) SvUV (iend_y); + + int size = blockDistance_inner(start_x, start_y, end_x, end_y); + + AV *array; + array = (AV *) SvRV (solution_array); + av_clear (array); + av_extend (array, size); + + int stepType; + int g = 0; + int i = 0; + + while (1) { + HV * rh = (HV *)sv_2mortal((SV *)newHV()); + + hv_store(rh, "x", 1, newSViv(start_x), 0); + hv_store(rh, "y", 1, newSViv(start_y), 0); + hv_store(rh, "g", 1, newSViv(g), 0); + + av_store(array, i, newRV((SV *)rh)); + i++; + + stepType = 0; + if (start_x < end_x) { + start_x++; + stepType++; + } else if (start_x > end_x) { + start_x--; + stepType++; + } + if (start_y < end_y) { + start_y++; + stepType++; + } else if (start_y > end_y) { + start_y--; + stepType++; + } + + if (stepType == 1) { + g += 10; + } else if (stepType == 2) { + g += 14; + } else if (stepType == 0) { + break; + } + } + + RETVAL = 1; + + OUTPUT: + RETVAL diff --git a/src/auto/XSTools/PathFinding/algorithm.cpp b/src/auto/XSTools/PathFinding/algorithm.cpp index d8887ebc5c..ec5c9d03ef 100644 --- a/src/auto/XSTools/PathFinding/algorithm.cpp +++ b/src/auto/XSTools/PathFinding/algorithm.cpp @@ -36,10 +36,10 @@ CalcPath_new () CalcPath_session *session; session = (CalcPath_session*) malloc (sizeof (CalcPath_session)); - + session->initialized = 0; session->run = 0; - + return session; } @@ -51,21 +51,24 @@ CalcPath_init (CalcPath_session *session) // Allocate enough memory in currentMap to hold all nodes in the map // Here we use calloc instead of malloc (calloc sets all memory allocated to 0's) so all uninitialized cells have whichlist set to NONE session->currentMap = (Node*) calloc(session->height * session->width, sizeof(Node)); - - unsigned long goalAdress = (session->endY * session->width) + session->endX; + if (session->customWeights) { + session->second_weight_map = (unsigned int*) calloc(session->height * session->width, sizeof(unsigned int)); + } + + long goalAdress = (session->endY * session->width) + session->endX; Node* goal = &session->currentMap[goalAdress]; goal->x = session->endX; goal->y = session->endY; goal->nodeAdress = goalAdress; - - unsigned long startAdress = (session->startY * session->width) + session->startX; + + long startAdress = (session->startY * session->width) + session->startX; Node* start = &session->currentMap[startAdress]; start->x = session->startX; start->y = session->startY; start->nodeAdress = startAdress; - start->h = heuristic_cost_estimate(start->x, start->y, goal->x, goal->y); + start->h = heuristic_cost_estimate(start->x, start->y, goal->x, goal->y, session->useManhattan); start->f = start->h; - + session->initialized = 1; } @@ -77,51 +80,52 @@ CalcPath_pathStep (CalcPath_session *session) printf("[pathfinding run error] You must call 'reset' before 'run'.\n"); return -2; } - + Node* start = &session->currentMap[((session->startY * session->width) + session->startX)]; Node* goal = &session->currentMap[((session->endY * session->width) + session->endX)]; - + if (!session->run) { session->run = 1; session->openListSize = 0; // Allocate enough memory in openList to hold the adress of all nodes in the map - session->openList = (unsigned long*) malloc((session->height * session->width) * sizeof(unsigned long)); - + session->openList = (long*) malloc((session->height * session->width) * sizeof(long)); + // To initialize the pathfinding add only the start node to openList openListAdd (session, start); } - + // If the start node and goal node are the same return a valid path with length 0 if (goal->nodeAdress == start->nodeAdress) { session->solution_size = 0; return 1; } - + Node* currentNode; Node* neighborNode; - + short i; - + // All possible directions the character can move (in order: north, south, east, west, northeast, southeast, southwest, northwest) short i_x[8] = {0, 0, 1, -1, 1, 1, -1, -1}; short i_y[8] = {1, -1, 0, 0, 1, -1, -1, 1}; - + int neighbor_x; int neighbor_y; - unsigned long neighbor_adress; + long neighbor_adress; unsigned long distanceFromCurrent; - + unsigned int c_randomFactor; + unsigned int g_score = 0; - + unsigned long timeout = (unsigned long) GetTickCount(); int loop = 0; - + while (1) { // If the openList is empty no path exists if (session->openListSize == 0) { return -1; } - + // Every 100th loop check if we have ran out if time loop++; if (loop == 100) { @@ -131,7 +135,7 @@ CalcPath_pathStep (CalcPath_session *session) } else loop = 0; } - + // Set currentNode to the top node in openList, and remove it from openList. currentNode = openListGetLowest (session); @@ -141,7 +145,7 @@ CalcPath_pathStep (CalcPath_session *session) reconstruct_path(session, goal, start); return 1; } - + // Loop between all neighbors for (i = 0; i <= 7; i++) { @@ -158,14 +162,14 @@ CalcPath_pathStep (CalcPath_session *session) if (session->map_base_weight[neighbor_adress] == -1) { continue; } - + neighborNode = &session->currentMap[neighbor_adress]; - + // If a neighbor is in closedList ignore it, it has already been expanded and has its lowest possible g_score if (neighborNode->whichlist == CLOSED) { continue; } - + // First 4 neighbors in the list are in a ortogonal path and the last 4 are in a diagonal path from currentNode. if (i >= 4) { // If neighborNode has a diagonal path from currentNode then we can only move to it if both ortogonal composite nodes are walkable. (example: To move to the northeast both north and east must be walkable) @@ -178,15 +182,24 @@ CalcPath_pathStep (CalcPath_session *session) // We use 10 for ortogonal movement weight distanceFromCurrent = 10; } - + // If avoidWalls is true we add weight to cells near walls to disencourage the algorithm to move to them. if (session->avoidWalls) { distanceFromCurrent += session->map_base_weight[neighbor_adress]; } - + + if (session->customWeights) { + distanceFromCurrent += session->second_weight_map[neighbor_adress]; + } + + if (session->randomFactor) { + c_randomFactor = rand() % session->randomFactor; + distanceFromCurrent += c_randomFactor; + } + // g_score is the summed weight of all nodes from start node to neighborNode, which is the g_score of currentNode + the weight to move from currentNode to neighborNode. g_score = currentNode->g + distanceFromCurrent; - + // If neighborNode is not in openList neither in closedList it has not been reached yet, initialize it and add it to openList if (neighborNode->whichlist == NONE) { neighborNode->x = neighbor_x; @@ -194,10 +207,10 @@ CalcPath_pathStep (CalcPath_session *session) neighborNode->nodeAdress = neighbor_adress; neighborNode->predecessor = currentNode->nodeAdress; neighborNode->g = g_score; - neighborNode->h = heuristic_cost_estimate(neighborNode->x, neighborNode->y, session->endX, session->endY); + neighborNode->h = heuristic_cost_estimate(neighborNode->x, neighborNode->y, session->endX, session->endY, session->useManhattan); neighborNode->f = neighborNode->g + neighborNode->h; openListAdd (session, neighborNode); - + // If neighborNode is in a list it has to be in openList, since we cannot access nodes in closedList. } else { // Check if we have found a shorter path to neighborNode, if so update it to have currentNode as its predecessor. @@ -214,15 +227,24 @@ CalcPath_pathStep (CalcPath_session *session) return -1; } -// The heuristic used is diagonal distance. +// The heuristic used is diagonal distance, unless specified to use manhattan (to mimic client) int -heuristic_cost_estimate (int currentX, int currentY, int goalX, int goalY) +heuristic_cost_estimate (int currentX, int currentY, int goalX, int goalY, bool useManhattan) { - int xDistance = abs(currentX - goalX); - int yDistance = abs(currentY - goalY); - - int hScore = (10 * (xDistance + yDistance)) - (6 * ((xDistance > yDistance) ? yDistance : xDistance)); - + int xDistance = currentX - goalX; + int yDistance = currentY - goalY; + if (xDistance < 0) xDistance = -xDistance; + if (yDistance < 0) yDistance = -yDistance; + + // # Game client uses the inadmissible (overestimating) heuristic of Manhattan distance + // #define heuristic(currentX, currentY, goalX, goalY) (10 * (xDistance + yDistance)) // Manhattan distance + int hScore; + if (useManhattan == 1) { + hScore = (10 * (xDistance + yDistance)); + } else { + hScore = (10 * (xDistance + yDistance)) - (6 * ((xDistance > yDistance) ? yDistance : xDistance)); + } + return hScore; } @@ -231,7 +253,7 @@ void reconstruct_path(CalcPath_session *session, Node* goal, Node* start) { Node* currentNode = goal; - + session->solution_size = 0; while (currentNode->nodeAdress != start->nodeAdress) { @@ -251,38 +273,38 @@ openListAdd (CalcPath_session *session, Node* currentNode) // Save in currentNode its index in openList currentNode->openListIndex = session->openListSize; currentNode->whichlist = OPEN; - + // Defines openList[index] to currentNode adress session->openList[currentNode->openListIndex] = currentNode->nodeAdress; - + // Increses openListSize by 1, since we just added a new member session->openListSize++; - + long parentIndex = (long)floor((currentNode->openListIndex - 1) / 2); Node* parentNode; - + // Repeat while currentNode still has a parent node, otherwise currentNode is the top node in the heap while (parentIndex >= 0) { - + parentNode = &session->currentMap[session->openList[parentIndex]]; - + // If parent node is bigger than currentNode, exchange their positions if (parentNode->f > currentNode->f) { // Changes the node adress of openList[currentNode->openListIndex] (which is 'currentNode') to that of openList[parentIndex] (which is the current parent of 'currentNode') session->openList[currentNode->openListIndex] = session->openList[parentIndex]; - + // Changes openListIndex of the current parent of 'currentNode' to that of 'currentNode' since they exchanged positions parentNode->openListIndex = currentNode->openListIndex; - + // Changes the node adress of openList[parentIndex] (which is the current parent of 'currentNode') to that of openList[currentNode->openListIndex] (which is 'currentNode') session->openList[parentIndex] = currentNode->nodeAdress; - + // Changes openListIndex of 'currentNode' to that of the current parent of 'currentNode' since they exchanged positions currentNode->openListIndex = parentIndex; - + // Updates parentIndex to that of the current parent of 'currentNode' parentIndex = (long)floor((currentNode->openListIndex - 1) / 2); - + } else { break; } @@ -294,29 +316,29 @@ reajustOpenListItem (CalcPath_session *session, Node* currentNode) { long parentIndex = (long)floor((currentNode->openListIndex - 1) / 2); Node* parentNode; - + // Repeat while currentNode still has a parent node, otherwise currentNode is the top node in the heap while (parentIndex >= 0) { - + parentNode = &session->currentMap[session->openList[parentIndex]]; - + // If parent node is bigger than currentNode, exchange their positions if (parentNode->f > currentNode->f) { // Changes the node adress of openList[currentNode->openListIndex] (which is 'currentNode') to that of openList[parentIndex] (which is the current parent of 'currentNode') session->openList[currentNode->openListIndex] = session->openList[parentIndex]; - + // Changes openListIndex of the current parent of 'currentNode' to that of 'currentNode' since they exchanged positions parentNode->openListIndex = currentNode->openListIndex; - + // Changes the node adress of openList[parentIndex] (which is the current parent of 'currentNode') to that of openList[currentNode->openListIndex] (which is 'currentNode') session->openList[parentIndex] = currentNode->nodeAdress; - + // Changes openListIndex of 'currentNode' to that of the current parent of 'currentNode' since they exchanged positions currentNode->openListIndex = parentIndex; - + // Updates parentIndex to that of the current parent of 'currentNode' parentIndex = (long)floor((currentNode->openListIndex - 1) / 2); - + } else { break; } @@ -327,78 +349,78 @@ Node* openListGetLowest (CalcPath_session *session) { session->openListSize--; - + Node* lowestNode = &session->currentMap[session->openList[0]]; - + // Since it was decreaased, but the node was not removed yet, session->openListSize is now also the index of the last node in openList // We move the last node in openList to this position and adjust it down as necessary session->openList[lowestNode->openListIndex] = session->openList[session->openListSize]; - + Node* movedNode; - + // Saves in movedNode that it now is the top node in openList movedNode = &session->currentMap[session->openList[lowestNode->openListIndex]]; movedNode->openListIndex = lowestNode->openListIndex; - + // Saves in lowestNode that it is no longer in openList lowestNode->whichlist = CLOSED; lowestNode->openListIndex = 0; - + long smallerChildIndex; Node* smallerChildNode; - + long rightChildIndex = 2 * movedNode->openListIndex + 2; Node* rightChildNode; - + long leftChildIndex = 2 * movedNode->openListIndex + 1; Node* leftChildNode; - + long lastIndex = session->openListSize-1; - + while (leftChildIndex <= lastIndex) { //There are 2 children if (rightChildIndex <= lastIndex) { - + rightChildNode = &session->currentMap[session->openList[rightChildIndex]]; leftChildNode = &session->currentMap[session->openList[leftChildIndex]]; - + if (rightChildNode->f > leftChildNode->f) { smallerChildIndex = leftChildIndex; } else { smallerChildIndex = rightChildIndex; } - + //There is 1 children } else { smallerChildIndex = leftChildIndex; } - + smallerChildNode = &session->currentMap[session->openList[smallerChildIndex]]; - + if (movedNode->f > smallerChildNode->f) { - + // Changes the node adress of openList[movedNode->openListIndex] (which is 'movedNode') to that of openList[smallerChildIndex] (which is the current child of 'movedNode') session->openList[movedNode->openListIndex] = smallerChildNode->nodeAdress; - + // Changes openListIndex of the current child of 'movedNode' to that of 'movedNode' since they exchanged positions smallerChildNode->openListIndex = movedNode->openListIndex; - + // Changes the node adress of openList[smallerChildIndex] (which is the current child of 'movedNode') to that of openList[movedNode->openListIndex] (which is 'movedNode') session->openList[smallerChildIndex] = movedNode->nodeAdress; - + // Changes openListIndex of 'movedNode' to that of the current child of 'movedNode' since they exchanged positions movedNode->openListIndex = smallerChildIndex; - + // Updates rightChildIndex and leftChildIndex to those of the current children of 'movedNode' rightChildIndex = 2 * movedNode->openListIndex + 2; leftChildIndex = 2 * movedNode->openListIndex + 1; - + } else { break; } } - + return lowestNode; } @@ -407,6 +429,9 @@ void free_currentMap (CalcPath_session *session) { free(session->currentMap); + if (session->customWeights) { + free(session->second_weight_map); + } } // Frees the memory allocated by openList @@ -422,6 +447,9 @@ CalcPath_destroy (CalcPath_session *session) { if (session->initialized) { free(session->currentMap); + if (session->customWeights) { + free(session->second_weight_map); + } } if (session->run) { free(session->openList); @@ -429,6 +457,237 @@ CalcPath_destroy (CalcPath_session *session) free(session); } +int +checkTile_inner(int start_x, int start_y, int tile, int width, int height, char * rawMap_data) { + if (start_x < 0 || start_x >= width || start_y < 0 || start_y >= height) { + return 0; + } + int offset; + + int value; + + offset = (start_y * width) + start_x; + value = rawMap_data[offset]; + if (!(value & tile)) { + return 0; + } + return 1; +} + +int +checkLOS_inner(int start_x, int start_y, int end_x, int end_y, int tile, int width, int height, char * rawMap_data) { + if (start_x < 0 || start_x >= width || start_y < 0 || start_y >= height) { + return 0; + } + if (end_x < 0 || end_x >= width || end_y < 0 || end_y >= height) { + return 0; + } + int dx; + int dy; + int wx; + int wy; + int weight; + + int offset; + + int value; + + int temp; + dx = end_x - start_x; + if (dx < 0) { + temp = start_x; + start_x = end_x; + end_x = temp; + + temp = start_y; + start_y = end_y; + end_y = temp; + + dx = -dx; + } + dy = end_y - start_y; + + int absdy; + if (dy >= 0) { + absdy = dy; + } else { + absdy = -dy; + } + + if (dx > absdy) { + weight = dx; + } else { + weight = absdy; + } + offset = (start_y * width) + start_x; + + wx = 0; + wy = 0; + while (start_x != end_x || start_y != end_y) { + wx += dx; + wy += dy; + if (wx >= weight) { + wx -= weight; + start_x++; + offset++; + } + if (wy >= weight) { + wy -= weight; + start_y++; + offset += width; + } else if (wy < 0) { + wy += weight; + start_y--; + offset -= width; + } + value = rawMap_data[offset]; + if (!(value & tile)) { + return 0; + } + } + return 1; +} + +int +canAttack_inner(int start_x, int start_y, int end_x, int end_y, int tile, int width, int height, int range, int clientSight, char * rawMap_data) { + int distance = blockDistance_inner(start_x, start_y, end_x, end_y); + if (distance < 2) { + return 1; + } + if (distance >= clientSight) { + return 0; + } + + int client_distance = getClientDist_inner(start_x, start_y, end_x, end_y); + if (client_distance > range) { + return 0; + } + if (!checkLOS_inner(start_x, start_y, end_x, end_y, tile, width, height, rawMap_data)) { + return -1 ; + } + + return 1; +} + +int +checkPathFree_inner(int start_x, int start_y, int end_x, int end_y, int tile, int width, int height, char * rawMap_data) { + int offset; + + int value; + + int stepX; + int stepY; + + offset = (start_y * width) + start_x; + value = rawMap_data[offset]; + + if (!(value & tile)) { + return 0; + } + + while (1) { + + stepX = 0; + stepY = 0; + + if (start_x < end_x) { + start_x++; + stepX++; + } else if (start_x > end_x) { + start_x--; + stepX--; + } + if (start_y < end_y) { + start_y++; + stepY += width; + } else if (start_y > end_y) { + start_y--; + stepY -= width; + } + + if (stepX != 0 && stepY != 0) { + value = rawMap_data[(offset + stepX)]; + if (!(value & tile)) { + return 0; + } + value = rawMap_data[(offset + stepY)]; + if (!(value & tile)) { + return 0; + } + } + + offset += (stepX + stepY); + value = rawMap_data[offset]; + + if (!(value & tile)) { + return 0; + } + + if (stepX == 0 && stepY == 0) { + return 1; + } + } +} + +int * +getSquareEdgesFromCoord_inner (int x, int y, int radius, int width, int height) +{ + static int limits[4]; + + // min_x + limits[0] = (x - radius); + if (limits[0] < 0) { + limits[0] = 0; + } + + // min_y + limits[1] = (y - radius); + if (limits[1] < 0) { + limits[1] = 0; + } + + // max_x + limits[2] = (x + radius); + if (limits[2] >= width) { + limits[2] = width-1; + } + + // max_y + limits[3] = (y + radius); + if (limits[3] >= height) { + limits[3] = height-1; + } + + return limits; +} + +int +blockDistance_inner (int start_x, int start_y, int end_x, int end_y) +{ + int dx = start_x - end_x; + int dy = start_y - end_y; + if (dx < 0) dx = -dx; + if (dy < 0) dy = -dy; + return dx > dy ? dx : dy; +} + +int +getClientDist_inner (int start_x, int start_y, int end_x, int end_y) +{ + int dx = start_x - end_x; + int dy = start_y - end_y; + + double temp_dist = sqrt((double)(dx*dx + dy*dy)); + + temp_dist -= 0.1; + + if (temp_dist < 0) { + temp_dist = 0; + } + + return ((int)temp_dist); +} + #ifdef __cplusplus } #endif /* __cplusplus */ \ No newline at end of file diff --git a/src/auto/XSTools/PathFinding/algorithm.h b/src/auto/XSTools/PathFinding/algorithm.h index d8d78a4e48..d5e11ac591 100644 --- a/src/auto/XSTools/PathFinding/algorithm.h +++ b/src/auto/XSTools/PathFinding/algorithm.h @@ -8,14 +8,14 @@ extern "C" { typedef struct { int x; int y; - - unsigned long nodeAdress; - - unsigned int predecessor; - - unsigned int whichlist; + + long nodeAdress; + + long predecessor; + + unsigned short whichlist; long openListIndex; - + unsigned long g; unsigned long h; unsigned long f; @@ -23,32 +23,39 @@ typedef struct { typedef struct { bool avoidWalls; - + const char *map_base_weight; + + bool customWeights; + unsigned int *second_weight_map; + + unsigned int randomFactor; + + bool useManhattan; + unsigned long time_max; - + int width; int height; - + int min_x; int max_x; int min_y; int max_y; - + int startX; int startY; int endX; int endY; - - int solution_size; + + unsigned long solution_size; int initialized; int run; - + long openListSize; - - const char *map_base_weight; + Node *currentMap; - - unsigned long *openList; + + long *openList; } CalcPath_session; CalcPath_session *CalcPath_new (); @@ -57,7 +64,7 @@ void CalcPath_init (CalcPath_session *session); int CalcPath_pathStep (CalcPath_session *session); -int heuristic_cost_estimate(int currentX, int currentY, int goalX, int goalY); +int heuristic_cost_estimate(int currentX, int currentY, int goalX, int goalY, bool useManhattan); void reconstruct_path(CalcPath_session *session, Node* goal, Node* start); @@ -73,6 +80,20 @@ void free_openList (CalcPath_session *session); void CalcPath_destroy (CalcPath_session *session); +int checkTile_inner (int start_x, int start_y, int tile, int width, int height, char * rawMap_data); + +int checkLOS_inner (int start_x, int start_y, int end_x, int end_y, int tile, int width, int height, char * rawMap_data); + +int canAttack_inner (int start_x, int start_y, int end_x, int end_y, int tile, int width, int height, int range, int clientSight, char * rawMap_data); + +int checkPathFree_inner (int start_x, int start_y, int end_x, int end_y, int tile, int width, int height, char * rawMap_data); + +int * getSquareEdgesFromCoord_inner (int x, int y, int radius, int width, int height); + +int blockDistance_inner (int start_x, int start_y, int end_x, int end_y); + +int getClientDist_inner (int start_x, int start_y, int end_x, int end_y); + #ifdef __cplusplus } #endif /* __cplusplus */