Divergent/mods/Western Goods/gamedata/scripts/western_goods_helicopter.sc...

526 lines
24 KiB
Plaintext

---==================================================================================================================---
--- ---
--- 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