---==================================================================================================================--- --- --- --- Original Author(s) : NLTP_ASHES, RavenAscendant --- --- Edited : N/A --- --- Date : 17/04/2023 --- --- License : Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) --- --- --- --- This script represents an API to make handling of helicopters easier. --- --- --- --- While all the functions are accessible outside of the script, only two are designed to be used externally : --- --- --- --- - heli_spawn(...) : --- --- A function you can use to create an helicopter at a location (default mode is idle); --- --- --- --- - heli_register(...) : --- --- A function you can use to give an order to the helicopter. --- --- --- --- Helicopters using this API have "modes", or sort of objectives, and they go as follows : --- --- --- --- - HELICOPTERS_MODES.IDLE : --- --- The helicopter will wait at its current position until it is put in anther mode; --- --- --- --- - HELICOPTERS_MODES.PROTECT_ACTOR : --- --- The helicopter will fly over the player, and react to attack orders; --- --- --- --- - HELICOPTERS_MODES.MOVE_TO_POINT : --- --- The helicopter will fly in a straight line to a given point in space; --- --- --- --- - HELICOPTERS_MODES.LAND_AT_POINT : --- --- The helicopter will fly high until it gets close to the landing spot, then it'll descend to land, and --- --- automatically switch to "idle" mode; --- --- --- --- - HELICOPTERS_MODES.LEAVE_AT_POINT : --- --- The helicopter will fly high until it gets close to its leaving spot, then it'll descend, and upon --- --- reaching the spot, it'll automatically get released and it'll be removed from the system. --- --- --- --- - HELICOPTERS_MODES.RELEASE : --- --- The helicopter will immediately get released. This isn't really meant to be used, and is more so here as a --- --- last resort in case it would be needed. Normally, you'd use HELICOPTERS_MODES.LEAVE_AT_POINT. --- --- --- ---==================================================================================================================--- -- --------------------------------------------------------------------------------------------------------------------- -- Constants, global variables and imported functions -- --------------------------------------------------------------------------------------------------------------------- HELICOPTERS_MODES = { IDLE = 1, PROTECT_ACTOR = 2, MOVE_TO_POINT = 3, LAND_AT_POINT = 4, LEAVE_AT_POINT = 5, RELEASE = 6 } -- Helicopter constants local CONST_HELICOPTER_UPDATE_FREQ = 5000 local CONST_HELICOPTER_FLIGHT_ALT = 100 local CONST_HELICOPTER_FLIGHT_ALT_SQR = 10000 local CONST_HELICOPTER_SPOT = "secondary_task_location" -- Helicopter variables local HELICOPTERS_TABLE = {} local helicopters_targets = {} local helicopters_sounds = { sound_object([[western_goods_misc\helicopter\heli_attack_1]]), sound_object([[western_goods_misc\helicopter\heli_attack_2]]), sound_object([[western_goods_misc\helicopter\heli_attack_3]]) } -- Imported functions local dbg_printf = western_goods_utils.dbg_printf local level_object_by_id = western_goods_utils.level_object_by_id -- --------------------------------------------------------------------------------------------------------------------- -- Functions -- --------------------------------------------------------------------------------------------------------------------- --- Function used to spawn an helicopter. The helicopter will automatically enter idle state. --- @param sec string --- @param pos vector --- @param lvid number --- @param gvid number --- @return cse_alife_object function heli_spawn(sec,pos,lvid,gvid) local se_heli = alife_create(sec,pos,lvid,gvid) if not se_heli then return nil end local visual = ini_sys:r_string_ex(sec,"visual") -- Required to spawn by script local data = utils_stpk.get_heli_data(se_heli) if (data) then data.visual_name = visual and visual ~="" and visual or [[dynamics\vehicles\mi2\veh_mi2_01]] data.motion_name = [[helicopter\aaa.anm]] data.startup_animation = "idle" data.skeleton_name = "idle" data.engine_sound = [[vehicles\helicopter\helicopter]] utils_stpk.set_heli_data(data,se_heli) se_heli.force_online = true heli_register(se_heli.id,HELICOPTERS_MODES.IDLE,nil) -- Execute at the next frame, once the heli has spawned in the world CreateTimeEvent("western_goods_heli_take_control", se_heli.id, 0, take_control, se_heli.id) else safe_release_manager.release(se_heli) return nil end dbg_printf("[WG] Helicopter | Successfully created (%s)", se_heli.id) return se_heli end --- Function used to give an order to the helicopter. --- Valid modes : See HELICOPTERS_MODES table. --- @param heli_id number --- @param mode nulber --- @param target_pos vector --- @return nil function heli_register(heli_id,mode,target_pos) if not heli_id then return end -- Get existing data (if there is any) local heli_data = HELICOPTERS_TABLE[heli_id] or {} -- Set the new data heli_data.mode = mode or HELICOPTERS_MODES.IDLE heli_data.last_mode = heli_data.last_mode or HELICOPTERS_MODES.IDLE heli_data.target_pos = target_pos or heli_data.target_pos heli_data.update_timer = heli_data.update_timer or time_global() heli_data.spot = heli_data.spot or false -- Add the helicopter to the refresh list HELICOPTERS_TABLE[heli_id] = heli_data -- Force the first update local heli_obj = level_object_by_id(heli_id) if heli_obj then heli_update(heli_obj,heli_data.mode,heli_data.last_mode,heli_data.target_pos) end dbg_printf("[WG] Helicopter | Registered order - %s - heli:%s", mode, heli_id) end --- Function used to remove an helicopter from the update table (use when the helicopter has been released from the level). --- @param heli_id number --- @return nil function heli_unregister(heli_id) if not heli_id then return end -- Clear the heli from the refresh table HELICOPTERS_TABLE[heli_id] = nil -- Remove the helicopter's marker on the PDA if level.map_has_object_spot(heli_id, CONST_HELICOPTER_SPOT) then level.map_remove_object_spot(heli_id, CONST_HELICOPTER_SPOT) end -- Delete the helicopter from the world if it exists alife_release_id(heli_id) dbg_printf("[WG] Helicopter | Successfully released (%s)", heli_id) end --- Function used to refresh all the helicopters. --- @return nil function refresh_helicopters() for id,_ in pairs(HELICOPTERS_TABLE) do local heli_se = alife_object(id) local heli_obj = heli_se and level_object_by_id(heli_se.id) -- If heli is gone or has been replaced by another object if (not heli_se) or heli_se:section_name() ~= "western_goods_helicopter" then HELICOPTERS_TABLE[id] = nil if level.map_has_object_spot(id, CONST_HELICOPTER_SPOT) ~= 0 then level.map_remove_object_spot(id, CONST_HELICOPTER_SPOT) end break end -- Update heli only if level object is available local mode = HELICOPTERS_TABLE[id].mode or HELICOPTERS_MODES.IDLE local last_mode = HELICOPTERS_TABLE[id].last_mode or HELICOPTERS_MODES.IDLE local target_pos = HELICOPTERS_TABLE[id].target_pos local update_timer = HELICOPTERS_TABLE[id].update_timer or time_global() + CONST_HELICOPTER_UPDATE_FREQ -- Continue only if the mode has changed, or the timer has ticked if mode == last_mode and update_timer > time_global() then break end -- If the heli is in combat, let it fight, and stop updating for now if (db.storage[heli_se.id] and db.storage[heli_se.id].combat and db.storage[heli_se.id].combat:update()) then break end -- Update the helicopter heli_update(heli_obj,mode,last_mode,target_pos) end end --- Function used to refresh an helicopter, this function is called by refresh_helicopters(). --- @param heli_obj game_object --- @param mode number --- @param target_pos vector --- @return nil function heli_update(heli_obj,mode,last_mode,target_pos) if not heli_obj then return end local updated = false -- Set the helicopter to idle on its current position if not mode or mode == HELICOPTERS_MODES.IDLE then -- Prevent the heli from spinning heli_obj:get_helicopter():SetDestPosition(heli_obj:position()) dbg_printf("[WG] Helicopter | Target updated - idling - heli:%s", heli_obj:id()) updated = true end -- Transition when taking off -- TODO find a way to fix heli going downwards when taking off sometimes --[[if not updated and last_mode == HELICOPTERS_MODES.IDLE and mode ~= HELICOPTERS_MODES.IDLE then -- Give the heli time to take off local intermediate_target = vector():set(heli_obj:position().x, heli_obj:position().y + 30, heli_obj:position().z) heli_obj:get_helicopter():SetDestPosition(intermediate_target) dbg_printf("[WG] Helicopter | Target updated - take_off - heli:%s", heli_obj:id()) updated = true end--]] -- Set the helicopter to protect the player if not updated and mode == HELICOPTERS_MODES.PROTECT_ACTOR then target_pos = db.actor:position() -- Go 50 meters above the player, somewhere in a 20m radius around him local inter_x = target_pos.x + math.random(-10,10) local inter_y = target_pos.y + CONST_HELICOPTER_FLIGHT_ALT local inter_z = target_pos.z + math.random(-10,10) local intermediate_target = vector():set(inter_x, inter_y, inter_z) -- Set the helicopter's destination heli_set_target(heli_obj,intermediate_target) dbg_printf("[WG] Helicopter | Target updated - protect actor - heli:%s target:%s", heli_obj:id(), intermediate_target) updated = true end -- Set the helicopter to move to a certain point if not updated and mode == HELICOPTERS_MODES.MOVE_TO_POINT then if not target_pos then return end -- Set the helicopter to go 50 meters above the target local intermediate_target = vector():set(target_pos.x, target_pos.y + CONST_HELICOPTER_FLIGHT_ALT, target_pos.z) -- Set the helicopter's destination heli_set_target(heli_obj,intermediate_target) dbg_printf("[WG] Helicopter | Target updated - move to point - heli:%s target:%s", heli_obj:id(), intermediate_target) updated = true end -- Set the helicopter to land at a certain point if not updated and mode == HELICOPTERS_MODES.LAND_AT_POINT then if not target_pos then return end local distance = western_goods_utils.get_distance_sqr(heli_obj:position(), target_pos) local approach_dist = CONST_HELICOPTER_FLIGHT_ALT_SQR + (CONST_HELICOPTER_FLIGHT_ALT_SQR/2) if distance > approach_dist then -- While the heli is 100m away from its destination, fly high local intermediate_target = vector():set(target_pos.x, target_pos.y+ CONST_HELICOPTER_FLIGHT_ALT, target_pos.z) heli_set_target(heli_obj,intermediate_target) dbg_printf("[WG] Helicopter | Target updated - land at point - heli:%s dist(sqr):%s target:%s", heli_obj:id(), distance, intermediate_target) elseif distance > 100 then -- Start lowering the altitude when we're closer than 10m heli_set_target(heli_obj,target_pos) dbg_printf("[WG] Helicopter | Target updated - land at point - heli:%s dist(sqr):%s target:%s", heli_obj:id(), distance, target_pos) else -- Make the helicopter idle if it's closer than 10m heli_register(heli_obj:id(),HELICOPTERS_MODES.IDLE) dbg_printf("[WG] Helicopter | Target updated - idling - heli:%s", heli_obj:id()) end updated = true end -- Set the helicopter to land a a certain point and get released if not updated and mode == HELICOPTERS_MODES.LEAVE_AT_POINT then if not target_pos then return end local distance = western_goods_utils.get_distance_sqr(heli_obj:position(), target_pos) local approach_dist = CONST_HELICOPTER_FLIGHT_ALT_SQR + (CONST_HELICOPTER_FLIGHT_ALT_SQR/2) if distance > approach_dist then -- While the heli is 100m away from its destination, fly high local intermediate_target = vector():set(target_pos.x, target_pos.y+ CONST_HELICOPTER_FLIGHT_ALT, target_pos.z) heli_set_target(heli_obj,intermediate_target) dbg_printf("[WG] Helicopter | Target updated - leave at point - heli:%s dist(sqr):%s target:%s", heli_obj:id(), distance, intermediate_target) elseif distance > 100 then -- Start lowering the altitude when we're closer than 10m heli_set_target(heli_obj,target_pos) dbg_printf("[WG] Helicopter | Target updated - leave at point - heli:%s dist(sqr):%s target:%s", heli_obj:id(), distance, target_pos) else -- Make the helicopter idle if it's closer than 10m heli_unregister(heli_obj:id()) dbg_printf("[WG] Helicopter | Target updated - unregister - heli:%s", heli_obj:id()) end updated = true end -- Set the helicopter to get deleted immediately if not updated and mode == HELICOPTERS_MODES.RELEASE then -- Forcefully release the helicopter heli_unregister(heli_obj:id()) dbg_printf("[WG] Helicopter | Target updated - release - heli:%s", heli_obj:id()) updated = true end -- Save the new state if the heli still exists (it could have been deleted during the update) if updated and HELICOPTERS_TABLE[heli_obj:id()] then HELICOPTERS_TABLE[heli_obj:id()].last_mode = mode HELICOPTERS_TABLE[heli_obj:id()].update_timer = time_global() + CONST_HELICOPTER_UPDATE_FREQ -- Refresh the helicopter's marker on the PDA if level.map_has_object_spot(heli_obj:id(), CONST_HELICOPTER_SPOT) == 0 then level.map_add_object_spot(heli_obj:id(), CONST_HELICOPTER_SPOT, "Helicopter") end elseif not updated then dbg_printf("![WG] ERROR | Helicopter | Failed to update helicopter %s", heli_obj:id()) end end --- Function used to set the point in space where the helicopter has to go. --- @param heli_obj game_object --- @param pos vector --- @return nil function heli_set_target(heli_obj,pos) if not heli_obj then return end heli_obj:get_helicopter():SetMaxVelocity(20) heli_obj:get_helicopter():GoPatrolByRoundPath(pos, 7, true) end --- Function used to know if an object is a valid target for the helicopter or not --- @param obj game_object --- @return boolean function valid_target(obj) if not obj then return end if not (IsStalker(obj) or IsMonster(obj)) then return false end if not (obj:is_entity_alive() and obj:alive()) then return false end if IsStalker(obj) and obj:character_community() == "killer" then return false end return true end --- Function used by the helicopter's IA to determine if an object is an enemy or not. --- @author RavenAscendant --- @param self_combat any --- @param obj game_object --- @return boolean function is_enemy(self_combat,obj) if not obj then return end return valid_target(obj) and helicopters_targets[obj:id()] end --- Function used to take over the helicopter's combat IA (to force the helicopter to target certain objects). --- @author RavenAscendant --- @param heli_id number --- @return boolean function take_control(heli_id) if not level_object_by_id(heli_id) then local se_obj = alife_object(heli_id) if not se_obj or se_obj:section_name() ~= "western_goods_helicopter" then dbg_printf("[WG] Helicopter | Cannot control missing or replaced (%s)", heli_id) end return false end local combat = db.storage[heli_id] and db.storage[heli_id].combat if combat then -- Take over is_enemy to control helicopter combat db.storage[heli_id].combat.is_enemy = is_enemy dbg_printf("[WG] Helicopter | Successfully controlled (%s)", heli_id) return true end end --- Function used to tell the helicopter's combat IA to enter combat state an attack the target the player is currently aiming at. --- @param dik DIK_keys --- @return nil function order_helicopter(dik) if dik ~= western_goods_mcm.get_config("heli_attack") then return end local target = level.get_target_obj() if target then for id,_ in pairs(HELICOPTERS_TABLE) do if HELICOPTERS_TABLE[id].mode == HELICOPTERS_MODES.PROTECT_ACTOR then local combat = db.storage[id] and db.storage[id].combat if combat and valid_target(target) then combat:set_enemy(target) helicopters_targets[target:id()] = true dbg_printf("[WG] Helicopter | Give order - Terminate enemy - heli:%s - target:%s(%s)", id, target:section(), target:id()) -- Send message in news feed local message = western_goods_utils.get_translation("st_wg_trader_act_1_task_2_heli_attack_" .. math.random(1,5)) western_goods_dialogs_manager.send_message(message, "Mercenary helicopter", "ui_inGame2_PD_DownToEarth", 0) -- Play sound effect local rnd_sound = math.random(1,size_table(helicopters_sounds)) play_sound_effect(helicopters_sounds[rnd_sound],helicopters_sounds) else dbg_printf("[WG] Helicopter | Give order - Invalid target - heli:%s - target:%s(%s)", id, target:section(), target:id()) end end end else dbg_printf("[WG] Helicopter | Give order - No target") end end --- Function used to play a sound effect in the player's ears. --- If a sound within 'sound_table' is already playing, the function will not play additional sounds. --- @param sound_obj sound_object --- @param sound_table table --- @return nil function play_sound_effect(sound_obj,sound_table) for _,sound in pairs(sound_table) do if sound and sound:playing() then return end end sound_obj:play(db.actor, 0, sound_object.s2d) end -- --------------------------------------------------------------------------------------------------------------------- -- Callbacks registration -- --------------------------------------------------------------------------------------------------------------------- --- Function used to register callbacks. --- @return nil function on_game_start() RegisterScriptCallback("actor_on_update", refresh_helicopters) RegisterScriptCallback("on_key_press",order_helicopter) RegisterScriptCallback("save_state", save_state) RegisterScriptCallback("load_state", load_state) end -- --------------------------------------------------------------------------------------------------------------------- -- Data persistence -- --------------------------------------------------------------------------------------------------------------------- --- Function used to store information in the save file. --- @param m_data table --- @return nil function save_state(m_data) dbg_printf("[WG] Helicopter | Saving helicopter table...") local HELICOPTERS_SAVE = {} copy_table(HELICOPTERS_SAVE, HELICOPTERS_TABLE) for id,_ in pairs(HELICOPTERS_SAVE) do -- Save each field HELICOPTERS_SAVE[id].update_timer = HELICOPTERS_SAVE[id].update_timer - time_global() -- Save the target position manually as this is an engine class local target_to_save = HELICOPTERS_SAVE[id].target_pos if target_to_save then HELICOPTERS_SAVE[id].target_pos = { x = target_to_save.x, y = target_to_save.y, z = target_to_save.z} end end m_data.heli_tbl = HELICOPTERS_SAVE dbg_printf("[WG] Helicopter | Saved helicopter table :\n%s", utils_data.print_table(HELICOPTERS_SAVE, false, true)) end --- Function used to load information stored in the save file. --- @param m_data table --- @return nil function load_state(m_data) dbg_printf("[WG] Helicopter | Loading helicopter table...") local HELICOPTERS_SAVE = m_data.heli_tbl or {} for id,_ in pairs(HELICOPTERS_SAVE) do HELICOPTERS_SAVE[id].update_timer = HELICOPTERS_SAVE[id].update_timer + time_global() -- Execute at the next frame, once the heli has spawned in the world CreateTimeEvent("western_goods_heli_take_control", id, 0, take_control, id) -- Load the target back into an engine class from the previously converted pos local target_to_load = HELICOPTERS_SAVE[id].target_pos if target_to_load then HELICOPTERS_SAVE[id].target_pos = vector():set(target_to_load.x,target_to_load.y,target_to_load.z) end end copy_table(HELICOPTERS_TABLE, HELICOPTERS_SAVE) dbg_printf("[WG] Helicopter | Loaded helicopter table :\n%s", utils_data.print_table(HELICOPTERS_TABLE, false, true)) end