---- AI Additions ---- ---- Rulix aka Bak ---- ---- 27.07.2016 function printf(s, ...) -- rx_utils.printf("com:"..s,...) end ASSERTX(rx_ai.rx_ini:section_exist("combat"),"no section [combat] in rx_ini") local run_on_hit = rx_utils.read_from_ini(rx_ai.rx_ini,"combat","run_on_hit",false,0) --local set_inertia_time_5_10,set_inertia_time_15 local md_enabled = rx_utils.read_from_ini(rx_ai.rx_ini,"combat","monster_defend",false,0) local md_max_dist = 12*12 local md_walk_dist = 9*9 local md_run_dist = 5*5 local md_min_rank = 0.2/rx_utils.rank_unit local md_monsters = { -- actor = 0, -- stalker = 0, bloodsucker = math.pi*0.3, boar = 0, burer = 0, cat = 0, chimera = math.pi*0.1, -- controller = 0, dog = math.pi*0.2, flesh = 0, fracture = 0, giant = 0, -- poltergeist = 0, pseudodog = 0, psy_dog = math.pi*0.8, psy_dog_phantom = 0, snork = math.pi*0.1, tushkano = 0, zombie = 0, } local t = {} for name,cls_id in pairs(rx_utils.creatures_clslist) do t[cls_id] = md_monsters[name] or nil end md_monsters = t t = nil local function suitable_target(npc,target) return target:alive() and md_monsters[target:clsid()] and npc:position():distance_to_sqr(target:position()) < md_max_dist --npc:max_ignore_monster_distance() end class "evaluator_monster_defend" (property_evaluator) function evaluator_monster_defend:__init(npc,storage,name) super (nil,name) self.st = storage self.st.check_time = 0 rx_ai.subscribe_for_events(npc,self) end function evaluator_monster_defend:evaluate() local npc = self.object -- if npc:critically_wounded() then -- return false -- end local be = npc:best_enemy() if not (be and npc:best_weapon()) then return false end local target = self.st.target and (db.storage[self.st.target] and db.storage[self.st.target].object or level.object_by_id(self.st.target)) if target and suitable_target(npc,target) and (npc:see(target) or target:see(npc)) then return true else self.st.target = nil end if self.st.check_time > time_global() then return false end self.st.check_time = time_global()+700 if npc:see(be) and suitable_target(npc,be) then self.st.target = be:id() return true end return false end function evaluator_monster_defend:hit_callback(amount,_,who) local npc = self.object if who and amount > 2 and suitable_target(npc,who) then self.st.target = who:id() end end class "action_monster_defend" (planner_action) function action_monster_defend:__init (npc,storage,action_name,planner) super (nil,action_name) self.st = storage self.evaluator_ready_to_kill = planner:evaluator(stalker_ids.property_ready_to_kill) self.rand_dif = math.random(-30,30)/100 self.ddr = math.random() < 0.5 and 1 or -1 end function action_monster_defend:initialize() action_base.initialize(self) local npc = self.object npc:set_desired_position() npc:set_desired_direction() npc:set_mental_state(anim.danger) npc:set_path_type(game_object.level_path) npc:set_dest_level_vertex_id(npc:level_vertex_id()) end function action_monster_defend:execute() action_base.execute(self) local npc = self.object local target = self.st.target and (db.storage[self.st.target] and db.storage[self.st.target].object or level.object_by_id(self.st.target)) if not target then return end if npc:path_type() ~= game_object.level_path then npc:set_path_type(game_object.level_path) end local wpn = npc:best_weapon() if not wpn then return end if self.evaluator_ready_to_kill:evaluate() ~= true and wpn:get_ammo_in_magazine() ~= 0 then -- заклинило? if self.weapon_fix_time then if self.weapon_fix_time < time_global() then self.st.target = nil self.st.check_time = time_global()+5000 return end else self.weapon_fix_time = time_global()+4000 end else self.weapon_fix_time = nil end if npc:id()%2 == 0 and wpn:get_ammo_in_magazine() == 0 and not npc:path_completed() then npc:set_sight(look.path_dir,nil,0) npc:set_item(object.aim1,wpn) elseif not npc:see(target) then npc:set_sight(target,true,false) npc:set_item(object.aim1,wpn) else local fire_point = rx_utils.safe_bone_pos(target,"bip01_neck") -- target:center() rx_utils.safe_bone_pos(target,"bip01_neck") target:bone_position(rx_utils.get_fire_bone(target:section())) -- fire_point.y = fire_point.y+0.15 npc:set_sight(look.direction,fire_point:sub(npc:bone_position("bip01_neck"))) -- npc:set_sight(look.point,fire_point,true) local magsize = math.min(rx_utils.get_mag_size(wpn:section()),50) local min_queue = math.floor(magsize/5) local max_queue = math.ceil(magsize/3) npc:set_item(object.fire1,wpn,math.random(min_queue,max_queue),700) end local dist = npc:position():distance_to_sqr(target:position()) local target_be = target:get_enemy() if not (target_be and target_be:id() == npc:id()) then if dist < md_walk_dist then npc:set_movement_type(move.walk) local vertex = self:get_dodge_vertex(npc:level_vertex_id(),target:position(),0,1) if vertex then npc:set_dest_level_vertex_id(vertex) end end return end if dist < md_run_dist then npc:set_movement_type(move.run) npc:set_body_state(move.standing) elseif dist < md_walk_dist then npc:set_movement_type(move.walk) else return end local vertex = self:get_dodge_vertex(npc:level_vertex_id(),target:position(),md_monsters[target:clsid()]) if not vertex then self.ddr = -self.ddr self.st.target = nil self.st.check_time = time_global()+4000 return end npc:set_dest_level_vertex_id(vertex) end function action_monster_defend:finalize() action_base.finalize(self) self.weapon_fix_time = nil end local _H_step = math.pi/6 function action_monster_defend:get_dodge_vertex(lvid,pos,imod,tc) local npc = self.object local npc_pos = npc:position() local dir = vector():sub(npc:position(),pos) local dH,dP,res = dir:getH()+self.rand_dif,dir:getP() local found,mod imod = imod or 0 for i=0,(tc or 5) do mod = imod > 0 and imod+i*_H_step or imod-i*_H_step if mod > 2.6 then break end res = npc:vertex_in_direction(lvid,vector():setHP(dH+mod*self.ddr,dP),4) if (lvid ~= res and npc:accessible(res) and level.vertex_position(res):distance_to_sqr(npc_pos) > 8) then found = true break end end return found and res end class "action_take_cover" (action_base) function action_take_cover:__init (npc,storage,action_name,action) super (nil,action_name) self.st = storage self.inherited = action end function action_take_cover:initialize() self.inherited:initialize() local npc = self.object if math.random() > npc.health-0.15 then npc:set_movement_type(move.run) end local wpn = npc:best_weapon() self.max_queue = wpn and math.ceil(rx_utils.get_mag_size(wpn:section())/5) or 5 rx_ai.subscribe_for_events(npc,self) end function action_take_cover:execute() self.inherited:execute() local npc = self.object if self.use_crouch == true then npc:set_body_state(move.crouch) end local enemy = npc:best_enemy() if enemy and npc:see(enemy) then local npc_pos,enemy_pos = npc:position(),enemy:position() if math.abs(npc_pos.y - enemy_pos.y) > 2 then local int = npc_pos:distance_to(enemy_pos)*26 npc:set_item(object.fire1,npc:best_weapon(),math.random(1,self.max_queue),int < 400 and 400 or int > 2500 and 2500 or int) end end end function action_take_cover:hit_callback(amount) local npc = self.object npc:set_movement_type(move.run) if self.use_crouch == nil then self.use_crouch = math.random() < 0.35 local wpn = npc:best_weapon() self.max_queue = wpn and math.ceil(rx_utils.get_mag_size(wpn:section())/3) or 10 end end function action_take_cover:finalize() self.inherited:finalize() self.use_crouch = nil rx_ai.unsubscribe_from_events(self.object:id(),self) end class "evaluator_sudden_attack" (property_evaluator) function evaluator_sudden_attack:__init(npc,storage,name) super (nil,name) self.st = storage -- rx_ai.subscribe_for_events(npc,self) end function evaluator_sudden_attack:evaluate() local npc = self.object local be = npc:best_enemy() if not be then return true end if self.st.enable_panic then return false end local dist = npc:position():distance_to(be:position()) if dist > 150 then self.storage:set_property(stalker_ids.property_use_suddenness,false) return true end return true end function evaluator_sudden_attack:hit_callback(amount,_,who) if who and amount > 2 then self.storage:set_property(stalker_ids.property_use_suddenness,false) end end class "evaluator_low_cover" (property_evaluator) function evaluator_low_cover:__init(npc,storage,name) super (nil,name) self.st = storage end function evaluator_low_cover:evaluate() local npc = self.object local be = npc:best_enemy() if not be then return false end if not self.storage:property(stalker_ids.property_in_cover) then return false end if not npc:best_weapon() then return false end local npc_pos,enemy_pos = npc:position(),npc:memory_position(be) local cover = npc:best_cover(npc_pos, enemy_pos, 1, 5, 150) if not cover then return false end local cover_pos = cover:position() if npc_pos:distance_to(cover_pos) > 0.1 then return false end local lvid = cover:level_vertex_id() local dir = vector():sub(enemy_pos,cover_pos) local low = level.low_cover_in_direction(lvid,dir) local high = level.high_cover_in_direction(lvid,dir) return (low+0.2 < high) end ---------------------------------------------------------------------------------------------------------------------- -- BINDER ---------------------------------------------------------------------------------------------------------------------- evid_monster_defend = rx_ai.base_id+45 evid_sudden_attack = evid_monster_defend+1 actid_monster_defend = stalker_ids.action_get_distance function add_combat(npc,st) local storage = rx_ai.get_storage(npc:id(),"combat_add") local manager = npc:motivation_action_manager() local combat_action = manager:action(stalker_ids.action_combat_planner) local combat_action_planner = cast_planner(combat_action) -- MONSTER DEFEND if md_enabled and npc:character_rank() >= md_min_rank then combat_action_planner:add_evaluator(evid_monster_defend,evaluator_monster_defend(npc,storage,"eva_monster_defend")) local new_action = action_monster_defend(npc,storage,"monster_defend",combat_action_planner) new_action:add_precondition(world_property(stalker_ids.property_critically_wounded,false)) new_action:add_precondition(world_property(stalker_ids.property_danger_grenade,false)) new_action:add_precondition(world_property(stalker_ids.property_use_suddenness,false)) -- new_action:add_precondition(world_property(stalker_ids.property_ready_to_kill,true)) new_action:add_precondition(world_property(stalker_ids.property_inside_anomaly+3,false)) -- eWorldPropertyInSmartCover new_action:add_precondition(world_property(stalker_ids.property_panic,false)) new_action:add_precondition(world_property(stalker_ids.property_pure_enemy+2,false)) -- eWorldPropertyEnemyWounded new_action:add_precondition(world_property(stalker_ids.property_pure_enemy+5,false)) -- eWorldPropertyPlayerOnThePath -- new_action:add_precondition(world_property(stalker_ids.property_critically_wounded+4,false)) -- eWorldPropertyTooFarToKillEnemy -- new_action:add_precondition(world_property(stalker_ids.property_pure_enemy,true)) new_action:add_precondition(world_property(evid_monster_defend,true)) new_action:add_effect(world_property(stalker_ids.property_pure_enemy,false)) new_action:add_effect(world_property(evid_monster_defend,false)) -- добавлять новые экшены в комбат планнер запрещено -- придётся пока выкидывать лишние combat_action_planner:remove_action(stalker_ids.action_get_distance) -- не используется combat_action_planner:add_action(stalker_ids.action_get_distance,new_action) end -- TAKE COVER if run_on_hit then local old_action = combat_action_planner:action(stalker_ids.action_take_cover) old_action:remove_effect(stalker_ids.property_in_cover) old_action:remove_effect(stalker_ids.property_looked_out) old_action:remove_effect(stalker_ids.property_position_holded) old_action:remove_effect(stalker_ids.property_enemy_detoured) -- combat_action_planner:remove_action(stalker_ids.action_take_cover) local new_action = action_take_cover(npc,storage,"monster_defend",old_action) new_action:add_precondition(world_property(stalker_ids.property_critically_wounded,false)) new_action:add_precondition(world_property(stalker_ids.property_danger_grenade,false)) new_action:add_precondition(world_property(stalker_ids.property_use_suddenness,false)) new_action:add_precondition(world_property(stalker_ids.property_item_to_kill,true)) new_action:add_precondition(world_property(stalker_ids.property_item_can_kill,true)) new_action:add_precondition(world_property(stalker_ids.property_ready_to_kill,true)) new_action:add_precondition(world_property(stalker_ids.property_in_cover,false)) new_action:add_precondition(world_property(stalker_ids.property_inside_anomaly+3,false)) -- eWorldPropertyInSmartCover new_action:add_precondition(world_property(stalker_ids.property_pure_enemy+2,false)) -- eWorldPropertyEnemyWounded new_action:add_precondition(world_property(stalker_ids.property_pure_enemy+5,false)) -- eWorldPropertyPlayerOnThePath new_action:add_effect(world_property(stalker_ids.property_in_cover,true)) new_action:add_effect(world_property(stalker_ids.property_looked_out,false)) new_action:add_effect(world_property(stalker_ids.property_position_holded,false)) new_action:add_effect(world_property(stalker_ids.property_enemy_detoured,false)) combat_action_planner:add_action(stalker_ids.action_take_cover,new_action) end -- SUDDEN ATTACK -- combat_action_planner:add_evaluator(evid_sudden_attack,evaluator_sudden_attack(npc,storage,"eva_sudden_attack")) -- local old_action = combat_action_planner:action(stalker_ids.action_sudden_attack) -- old_action:add_precondition(world_property(evid_sudden_attack,true)) -- LOW COVER -- combat_action_planner:remove_evaluator(stalker_ids.property_inside_anomaly+2) -- eWorldPropertyLowCover -- combat_action_planner:add_evaluator(stalker_ids.property_inside_anomaly+2,evaluator_low_cover(npc,storage,"eva_low_cover")) -- eWorldPropertyLowCover -- if not set_inertia_time_15 then -- local danger_action_planner = cast_planner(manager:action(stalker_ids.action_danger_planner)) -- local danger_unknown_planner = cast_planner(danger_action_planner:action(stalker_ids.action_danger_unknown_planner)) -- set_inertia_time_15 = danger_unknown_planner:action(stalker_ids.action_danger_unknown_look_around).initialize -- 15000 -- local danger_indirection_planner = cast_planner(danger_action_planner:action(stalker_ids.action_danger_in_direction_planner)) -- set_inertia_time_5_10 = danger_indirection_planner:action(stalker_ids.action_danger_in_direction_hold_position).initialize -- 5000-10000 -- end -- local low_cover_action = combat_action_planner:action(stalker_ids.action_kill_if_enemy_critically_wounded+8) -- local low_cover_planner = cast_planner(low_cover_action) -- st.lowcover_hold_position_act = low_cover_planner:action(stalker_ids.action_hold_position) end ---------------------------------------------------------------------------------------------------------------------- -- UPDATE ---------------------------------------------------------------------------------------------------------------------- function combat_planner_update(npc,st) if not st.combat_planner then st.combat_planner = cast_planner(st.planner:action(stalker_ids.action_combat_planner)) end local action_id = st.combat_planner:current_action_id() -- фикс набегания без оружия if action_id == stalker_ids.action_sudden_attack and not npc:active_item() then npc:set_item(object.idle,npc:best_weapon()) --rx_utils.get_weapon(npc) end if action_id ~= st.combat_planner_action_id then st.combat_planner_action_id = action_id st.combat_planner_fix_timer = nil if action_id == stalker_ids.action_get_ready_to_kill or action_id == stalker_ids.action_get_ready_to_kill+1 then st.combat_planner_fix_timer = time_global()+3300 -- elseif action_id == stalker_ids.action_look_out then -- st.combat_planner_fix_timer = time_global()+15000 -- elseif action_id == stalker_ids.action_critically_wounded then -- st.combat_planner_fix_timer = time_global()+5000 -- elseif action_id == stalker_ids.action_kill_if_enemy_critically_wounded+8 then -- eWorldOperatorLowCover -- set_inertia_time_5_10(st.lowcover_hold_position_act) elseif action_id == stalker_ids.action_search_enemy then if math.random() < 0.3 then npc:set_movement_type(move.run) end end elseif st.combat_planner_fix_timer and st.combat_planner_fix_timer < time_global() then st.combat_planner_fix_timer = nil if action_id == stalker_ids.action_get_ready_to_kill then -- фикс зависания оружия в strap -- фикс зависания, если в инвентаре нет патронов и оружие заклинило local wpn = npc:active_item() if not npc:weapon_unstrapped() then npc:drop_item(wpn) elseif wpn and wpn:get_ammo_in_magazine() == wpn:get_ammo_total() then wpn:unload_magazine() end elseif action_id == stalker_ids.action_get_ready_to_kill+1 then -- eWorldOperatorGetReadyToDetour -- фикс зависания, если в инвентаре нет патронов local wpn = npc:active_item() local aminmag = wpn and wpn:get_ammo_in_magazine() if wpn and aminmag == wpn:get_ammo_total() then local magsize = rx_utils.get_mag_size(wpn:section()) wpn:set_ammo_elapsed(aminmag < magsize and magsize or aminmag+10) end -- elseif action_id == stalker_ids.action_look_out then -- -- фикс зависания, если враг далеко -- local enemy = npc:best_enemy() -- local dist = npc:position():distance_to(npc:memory_position(enemy)) -- npc:enable_memory_object(enemy,false) -- elseif action_id == stalker_ids.action_critically_wounded then end end end