local H = wesnoth.require "lua/helper.lua" local W = H.set_wml_action_metatable {} local AH = wesnoth.require "ai/lua/ai_helper.lua" local MAIUV = wesnoth.require "ai/micro_ais/micro_ai_unit_variables.lua" local function hunter_attack_weakest_adj_enemy(ai, hunter) -- Attack the enemy with the fewest hitpoints adjacent to 'hunter', if there is one -- Returns status of the attack: -- 'attacked': if a unit was attacked -- 'killed': if a unit was killed -- 'no_attack': if no unit was attacked if (hunter.attacks_left == 0) then return 'no_attack' end local min_hp, target = 9e99 for xa,ya in H.adjacent_tiles(hunter.x, hunter.y) do local enemy = wesnoth.get_unit(xa, ya) if enemy and wesnoth.is_enemy(enemy.side, wesnoth.current.side) then if (enemy.hitpoints < min_hp) then min_hp, target = enemy.hitpoints, enemy end end end if target then AH.checked_attack(ai, hunter, target) if target and target.valid then return 'attacked' else return 'killed' end end return 'no_attack' end local function get_hunter(cfg) local filter = cfg.filter or { id = cfg.id } local hunter = AH.get_units_with_moves { side = wesnoth.current.side, { "and", filter } }[1] return hunter end local ca_hunter = {} function ca_hunter:evaluation(ai, cfg) if get_hunter(cfg) then return cfg.ca_score end return 0 end function ca_hunter:execution(ai, cfg) -- Hunter does a random wander in area given by @cfg.hunting_ground until it finds -- and kills an enemy unit, then retreats to position given by @cfg.home_x/y -- for @cfg.rest_turns turns, or until fully healed local hunter = get_hunter(cfg) local hunter_vars = MAIUV.get_mai_unit_variables(hunter, cfg.ai_id) -- If hunting_status is not set for the hunter -> default behavior -> random wander if (not hunter_vars.hunting_status) then -- Hunter gets a new goal if none exist or on any move with 10% random chance local rand = math.random(10) if (not hunter_vars.goal_x) or (rand == 1) then -- 'locs' includes border hexes, but that does not matter here locs = AH.get_passable_locations((cfg.filter_location or {}), hunter) local rand = math.random(#locs) hunter_vars.goal_x, hunter_vars.goal_y = locs[rand][1], locs[rand][2] MAIUV.set_mai_unit_variables(hunter, cfg.ai_id, hunter_vars) end local reach_map = AH.get_reachable_unocc(hunter) -- Now find the one of these hexes that is closest to the goal local max_rating, best_hex = -9e99 reach_map:iter( function(x, y, v) -- Distance from goal is first rating local rating = - H.distance_between(x, y, hunter_vars.goal_x, hunter_vars.goal_y) -- Huge rating bonus if this is next to an enemy local enemy_hp = 500 for xa,ya in H.adjacent_tiles(x, y) do local enemy = wesnoth.get_unit(xa, ya) if enemy and wesnoth.is_enemy(enemy.side, wesnoth.current.side) then if (enemy.hitpoints < enemy_hp) then enemy_hp = enemy.hitpoints end end end rating = rating + 500 - enemy_hp -- prefer attack on weakest enemy if (rating > max_rating) then max_rating, best_hex = rating, { x, y } end end) if (best_hex[1] ~= hunter.x) or (best_hex[2] ~= hunter.y) then AH.checked_move(ai, hunter, best_hex[1], best_hex[2]) -- partial move only if (not hunter) or (not hunter.valid) then return end else -- If hunter did not move, we need to stop it (also delete the goal) AH.checked_stopunit_moves(ai, hunter) if (not hunter) or (not hunter.valid) then return end hunter_vars.goal_x, hunter_vars.goal_y = nil, nil MAIUV.set_mai_unit_variables(hunter, cfg.ai_id, hunter_vars) end -- If this gets the hunter to the goal, we delete the goal if (hunter.x == hunter_vars.goal_x) and (hunter.y == hunter_vars.goal_y) then hunter_vars.goal_x, hunter_vars.goal_y = nil, nil MAIUV.set_mai_unit_variables(hunter, cfg.ai_id, hunter_vars) end -- Finally, if the hunter ended up next to enemies, attack the weakest of those local attack_status = hunter_attack_weakest_adj_enemy(ai, hunter) -- If the enemy was killed, hunter returns home if hunter and hunter.valid and (attack_status == 'killed') then hunter_vars.goal_x, hunter_vars.goal_y = nil, nil hunter_vars.hunting_status = 'returning' MAIUV.set_mai_unit_variables(hunter, cfg.ai_id, hunter_vars) if cfg.show_messages then W.message { speaker = hunter.id, message = 'Now that I have eaten, I will go back home.' } end end return end -- If we got here, this means the hunter is either returning, or resting if (hunter_vars.hunting_status == 'returning') then goto_x, goto_y = wesnoth.find_vacant_tile(cfg.home_x, cfg.home_y) local next_hop = AH.next_hop(hunter, goto_x, goto_y) if next_hop then AH.movefull_stopunit(ai, hunter, next_hop) if (not hunter) or (not hunter.valid) then return end -- If there's an enemy on the 'home' hex and we got right next to it, attack that enemy if (H.distance_between(cfg.home_x, cfg.home_y, next_hop[1], next_hop[2]) == 1) then local enemy = wesnoth.get_unit(cfg.home_x, cfg.home_y) if enemy and wesnoth.is_enemy(enemy.side, hunter.side) then if cfg.show_messages then W.message { speaker = hunter.id, message = 'Get out of my home!' } end AH.checked_attack(ai, hunter, enemy) if (not hunter) or (not hunter.valid) then return end end end end -- We also attack the weakest adjacent enemy, if still possible hunter_attack_weakest_adj_enemy(ai, hunter) if (not hunter) or (not hunter.valid) then return end -- If the hunter got home, start the resting counter if (hunter.x == cfg.home_x) and (hunter.y == cfg.home_y) then hunter_vars.hunting_status = 'resting' hunter_vars.resting_until = wesnoth.current.turn + (cfg.rest_turns or 3) MAIUV.set_mai_unit_variables(hunter, cfg.ai_id, hunter_vars) if cfg.show_messages then W.message { speaker = hunter.id, message = 'I made it home - resting now until the end of Turn ' .. hunter_vars.resting_until .. ' or until fully healed.' } end end return end -- If we got here, the only remaining action should be resting if (hunter_vars.hunting_status == 'resting') then AH.checked_stopunit_moves(ai, hunter) if (not hunter) or (not hunter.valid) then return end -- We also attack the weakest adjacent enemy hunter_attack_weakest_adj_enemy(ai, hunter) if (not hunter) or (not hunter.valid) then return end -- If this is the last turn of resting, we remove the status and turn variable if (hunter.hitpoints >= hunter.max_hitpoints) and (hunter_vars.resting_until <= wesnoth.current.turn) then hunter_vars.hunting_status = nil hunter_vars.resting_until = nil MAIUV.set_mai_unit_variables(hunter, cfg.ai_id, hunter_vars) if cfg.show_messages then W.message { speaker = hunter.id, message = 'I am done resting. It is time to go hunting again next turn.' } end end return end -- In principle we should never get here, but just in case something got changed in WML: -- Reset variable, so that hunter goes hunting on next turn hunter_vars.hunting_status = nil MAIUV.set_mai_unit_variables(hunter, cfg.ai_id, hunter_vars) end return ca_hunter