781 lines
30 KiB
Plaintext
781 lines
30 KiB
Plaintext
--[[
|
|
DYNAMIC ZONE - Anomalies Spawn Module
|
|
|
|
Original Author(s)
|
|
VodoXleb <vodoxlebushek>
|
|
Singustromo <singustromo at disroot.org>
|
|
|
|
Edited by
|
|
|
|
License
|
|
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
|
|
(https://creativecommons.org/licenses/by-nc-sa/4.0)
|
|
|
|
Synopsis
|
|
Main module responsible for spawning anomalies on transitions that are
|
|
blocked. Anomalies are spawned randomly in a given radius around the
|
|
teleport space restrictor.
|
|
Additionally we also spawn anomalies on predefined coordinates due
|
|
to missing level vertex id's on some map spots.
|
|
|
|
Anomalies are only being spawned for the current level as we handle over
|
|
20 of those per blocked transition in order to not cap the
|
|
a-life limit with heavily modded games.
|
|
|
|
demonized' kd_tree is used for rudimentary distance checks
|
|
We've also utilized some modified anomaly spawn code from DAO (marked below)
|
|
--]]
|
|
|
|
parent = _G["dynamic_zone"]
|
|
if not (parent and parent.VERSION and parent.VERSION >= 20241224) then return end
|
|
|
|
--------------------------
|
|
-- Dependencies --
|
|
--------------------------
|
|
|
|
local utils = dynamic_zone_utils
|
|
local debug = dynamic_zone_debug
|
|
local route_manager = dynamic_zone_routes
|
|
|
|
------------------------------------------
|
|
-- Global Variables & Constants --
|
|
------------------------------------------
|
|
|
|
CONST_LOGGING_PREFIX = "Anomalies"
|
|
local log = debug.log_register("info", CONST_LOGGING_PREFIX)
|
|
local log_warn = debug.log_register("warning", CONST_LOGGING_PREFIX)
|
|
local log_error = debug.log_register("error", CONST_LOGGING_PREFIX)
|
|
|
|
CONST_MDATA_KEY_USED_TRANSITION = "last_taken_transition"
|
|
CONST_STRING_THEME_ID = "st_dynzone_$NAME_theme"
|
|
|
|
CONST_ANOMALY_FALLBACK_SIZE_MIN = 2 -- fallback values
|
|
CONST_ANOMALY_FALLBACK_SIZE_MAX = 4
|
|
CONST_FIELD_SIZE_MIN = 30 -- static spawn at transition center
|
|
CONST_FIELD_SIZE_MAX = 40
|
|
|
|
CONST_SAFEZONE_RADIUS = 10
|
|
CONST_GENERATE_NARROW_FACTOR = 0.6
|
|
CONST_ABORT_CREATION_DELAY_MS = 250
|
|
|
|
CONST_BASE_INI_DIR = "plugins\\dynamic_zone\\anomalies"
|
|
ini_transition_params = ini_file_ex(CONST_BASE_INI_DIR .. "\\transition_settings.ltx")
|
|
ini_static_spawns = ini_file_ex(CONST_BASE_INI_DIR .. "\\static_spawns.ltx")
|
|
|
|
--------------------------------------
|
|
-- Anomaly Spawn Settings --
|
|
--------------------------------------
|
|
|
|
-- Contains spawn parameters for transitions - also the defaults
|
|
spawn_parameters = {
|
|
defaults = { -- Needs to contain every possible key (failsafe)
|
|
rootpos = false,
|
|
narrow = false,
|
|
count = 19,
|
|
max_height_offset = 0,
|
|
spread_radius = 20,
|
|
generate_max_tries = 64,
|
|
min_anomaly_proximity = 1,
|
|
static_spawn_ratio = 0.4
|
|
},
|
|
}
|
|
|
|
anomaly_themes = {
|
|
[1] = {
|
|
name = "thermal",
|
|
anomaly_field_types = {
|
|
"zone_field_thermal_strong",
|
|
"zone_field_thermal_average",
|
|
},
|
|
anomaly_mines_types = {
|
|
"zone_mine_thermal_strong",
|
|
"zone_mine_thermal_average",
|
|
"zone_mine_thermal_weak",
|
|
},
|
|
},
|
|
[2] = {
|
|
name = "acidic",
|
|
anomaly_field_types = {
|
|
"zone_field_acidic_strong",
|
|
"zone_field_acidic_average",
|
|
},
|
|
anomaly_mines_types = {
|
|
"zone_mine_acidic_strong",
|
|
"zone_mine_acidic_average",
|
|
"zone_mine_acidic_weak",
|
|
"zone_mine_chemical_strong",
|
|
"zone_mine_chemical_average",
|
|
"zone_mine_chemical_weak",
|
|
},
|
|
},
|
|
[3] = {
|
|
name = "electric",
|
|
anomaly_field_types = {
|
|
"zone_field_psychic_strong",
|
|
"zone_field_psychic_average",
|
|
},
|
|
anomaly_mines_types = {
|
|
"zone_mine_electric_strong",
|
|
"zone_mine_electric_average",
|
|
"zone_mine_electric_weak",
|
|
},
|
|
},
|
|
[4] = {
|
|
name = "gravitational",
|
|
anomaly_field_types = {
|
|
"zone_field_radioactive_strong",
|
|
"zone_field_radioactive_average",
|
|
},
|
|
anomaly_mines_types = {
|
|
"zone_mine_gravitational_strong",
|
|
"zone_mine_gravitational_average",
|
|
"zone_mine_gravitational_weak",
|
|
},
|
|
},
|
|
}
|
|
|
|
-- TODO: Use table from DAO/Arrival, if available
|
|
-- defining anomaly radii for most in-game anomalies
|
|
anomaly_radii = {
|
|
zone_radioactive = {min = 4, max = 6},
|
|
zone_radioactive_weak = {min = 4, max = 6},
|
|
zone_radioactive_average = {min = 4, max = 6},
|
|
zone_radioactive_strong = {min = 4, max = 6},
|
|
|
|
zone_mine_acid = {min = 2, max = 3},
|
|
zone_mine_acidic_weak = {min = 2, max = 3},
|
|
zone_mine_acidic_average = {min = 2, max = 3},
|
|
zone_mine_acidic_strong = {min = 2, max = 3},
|
|
|
|
zone_mine_blast = {min = 2, max = 3},
|
|
zone_mine_umbra = {min = 2, max = 3},
|
|
|
|
zone_mine_electra = {min = 2, max = 3},
|
|
zone_mine_electric_weak = {min = 2, max = 3},
|
|
zone_mine_electric_average = {min = 2, max = 3},
|
|
zone_mine_electric_strong = {min = 2, max = 3},
|
|
|
|
zone_mine_flash = {min = 3, max = 3},
|
|
zone_mine_ghost = {min = 2, max = 3},
|
|
zone_mine_gold = {min = 2, max = 3},
|
|
zone_mine_thorn = {min = 2, max = 3},
|
|
zone_mine_seed = {min = 3, max = 3},
|
|
zone_mine_shatterpoint = {min = 6, max = 8},
|
|
|
|
zone_mine_gravitational_weak = {min = 2, max = 3},
|
|
zone_mine_gravitational_average = {min = 3, max = 5},
|
|
zone_mine_gravitational_strong = {min = 4, max = 6},
|
|
|
|
zone_mine_sloth = {min = 3, max = 4},
|
|
zone_mine_mefistotel = {min = 3, max = 4},
|
|
zone_mine_net = {min = 2, max = 3},
|
|
zone_mine_point = {min = 2, max = 3},
|
|
zone_mine_cdf = {min = 2, max = 3},
|
|
zone_mine_sphere = {min = 4, max = 5},
|
|
zone_mine_springboard = {min = 4, max = 6},
|
|
|
|
zone_mine_thermal_weak = {min = 1, max = 2},
|
|
zone_mine_thermal_average = {min = 1, max = 2},
|
|
zone_mine_thermal_strong = {min = 1, max = 2},
|
|
zone_mine_zharka = {min = 1, max = 2},
|
|
|
|
zone_mine_vapour = {min = 1, max = 2},
|
|
zone_mine_vortex = {min = 3, max = 5},
|
|
}
|
|
|
|
------------------------------
|
|
-- Global Getters --
|
|
------------------------------
|
|
|
|
-- Getter for the coresponding string identifier determined by anomaly_themes[id].name
|
|
-- @param theme_id (index from anomaly_theme table)
|
|
-- @returns string (identifier used in xml)
|
|
function get_theme_string(theme_id)
|
|
if not utils.valid_type{ caller = "get_theme_string",
|
|
"int", theme_id } then return end
|
|
|
|
utils.assert(anomaly_themes[theme_id], "Undefined theme with ID #%s!", theme_id)
|
|
local theme_name = anomaly_themes[theme_id] and anomaly_themes[theme_id].name
|
|
|
|
local string_id = CONST_STRING_THEME_ID
|
|
return (theme_name) and string_id:gsub("%$NAME", theme_name)
|
|
end
|
|
|
|
-- Ensures that we always have usable spawn parameters
|
|
-- Uses defaults section as a fallback
|
|
-- @param transition_name
|
|
-- @returns table
|
|
function get_spawn_parameters(transition_name)
|
|
if not utils.valid_type{ caller = "get_spawn_parameters",
|
|
"str", transition_name } then return end
|
|
|
|
local defaults = spawn_parameters.defaults
|
|
|
|
if (utils.is_table_empty(spawn_parameters[transition_name])) then
|
|
log("No parameters defined for transition '%s', using defaults.", transition_name or "N/A")
|
|
return defaults
|
|
end
|
|
|
|
return spawn_parameters[transition_name]
|
|
end
|
|
|
|
-- Returns the root position for anomaly spawns of the given transition
|
|
-- @param transition_name
|
|
-- @returns pos (copy) or nil
|
|
function get_rootposition(transition_name)
|
|
if not utils.valid_type{ caller = "get_rootposition",
|
|
"str", transition_name } then return end
|
|
|
|
local params = get_spawn_parameters(transition_name)
|
|
local pos = params and params.pos
|
|
return pos and vector():set(pos.x, pos.y, pos.z)
|
|
end
|
|
|
|
-----------------------------
|
|
-- Configuration --
|
|
-----------------------------
|
|
|
|
function collect_spawn_parameters(section)
|
|
local settings, target = ini_transition_params:collect_section(section)
|
|
|
|
spawn_parameters[section] = {}
|
|
target = spawn_parameters[section]
|
|
|
|
for key, value in pairs(settings) do
|
|
if (key == "rootpos") then
|
|
target[key] = utils.string_to_vector(value) or false
|
|
goto continue
|
|
end
|
|
|
|
local value_bool = utils.string_to_bool(value)
|
|
target[key] = (type(value_bool) == "bool") and value_bool
|
|
or tonumber(value) or value
|
|
|
|
:: continue ::
|
|
end
|
|
end
|
|
|
|
-- Returns the appropriate radius, to ensure we default to fallback values, if necessary
|
|
-- @param anomaly_type section of the anomaly
|
|
-- @returns table || nil
|
|
function get_anomaly_radii(anomaly_type)
|
|
if (not anomaly_type) then return end
|
|
|
|
-- Fields share the same radii in our use case
|
|
if string.find(anomaly_type, "^zone_field_") then
|
|
return {min = CONST_FIELD_SIZE_MIN,
|
|
max = CONST_FIELD_SIZE_MAX}
|
|
end
|
|
|
|
return anomaly_radii[anomaly_type]
|
|
or {min = CONST_ANOMALY_FALLBACK_SIZE_MIN, max = CONST_ANOMALY_FALLBACK_SIZE_MAX}
|
|
end
|
|
|
|
----------------------------
|
|
-- Monkey patches --
|
|
----------------------------
|
|
|
|
SRTeleportMsgBoxOk = ui_sr_teleport.msg_box_ui.OnMsgOk
|
|
|
|
-- We gotta save the last used transition for Anomaly safe Zone
|
|
function ui_sr_teleport.msg_box_ui.OnMsgOk(self)
|
|
local transition_name, m_data, data = self.name
|
|
|
|
local route = route_manager.get_route_by_transition(transition_name)
|
|
if (utils.assert_failed(route) or not route.blocked) then goto teleport end
|
|
|
|
m_data = alife_storage_manager.get_state()
|
|
data = m_data and m_data[parent.data_key]
|
|
|
|
data[CONST_MDATA_KEY_USED_TRANSITION] = transition_name
|
|
log("Saved '%s' as last used transition", data[CONST_MDATA_KEY_USED_TRANSITION])
|
|
|
|
:: teleport ::
|
|
return SRTeleportMsgBoxOk(self)
|
|
end
|
|
|
|
-------------------------
|
|
-- Callbacks --
|
|
-------------------------
|
|
|
|
function on_game_start()
|
|
RegisterScriptCallback("dynzone_changed_block_state", dynzone_changed_block_state)
|
|
RegisterScriptCallback("on_level_changing", release_all_anomalies)
|
|
|
|
ini_transition_params:section_for_each(collect_spawn_parameters)
|
|
end
|
|
|
|
-- Called in main script by actor_on_first_update because it
|
|
-- should be executed after routes have been updated
|
|
-- Spawns anomalies on the current level for blocked transitions
|
|
-- Only does so, if spawned_anomalies of the transition is empty
|
|
-- TODO: encapsulate from main script (e.g. new Callback)
|
|
function spawn_on_current_level(force)
|
|
for route_id, route in route_manager.iterate_routes() do
|
|
if not (route.blocked or force) then goto next_route end
|
|
|
|
spawn_for_route_on_current_level(route_id)
|
|
:: next_route ::
|
|
end
|
|
end
|
|
|
|
-- Sets the Anomaly Theme, Spawns and Despawns anomalies accordingly
|
|
-- We check the spawned_anomalies attribute to
|
|
-- determine if that transition is on current level
|
|
function dynzone_changed_block_state(previously_blocked, new_blocked)
|
|
for route_id, _ in pairs(previously_blocked) do
|
|
route_manager.set_route_property(route_id, "anomaly_theme", 0)
|
|
|
|
for section, attributes in route_manager.iterate_transitions(route_id) do
|
|
if (not utils.is_table_empty(attributes.spawned_anomalies)) then
|
|
release_all_from_transition(section)
|
|
end
|
|
end
|
|
end
|
|
|
|
for route_id, _ in pairs(new_blocked) do
|
|
set_route_theme(route_id) -- We need to set it beforehand for e.g. the news
|
|
spawn_for_route_on_current_level(route_id)
|
|
end
|
|
end
|
|
|
|
-- Called 'on_level_changing'
|
|
function release_all_anomalies()
|
|
if (utils.debug_level_loaded()) then return end
|
|
|
|
log("Releasing anomalies on '%s'", utils.get_mapname())
|
|
for route_id, route in route_manager.iterate_routes() do
|
|
for section, attributes in route_manager.iterate_transitions(route_id) do
|
|
if (not utils.is_table_empty(attributes.spawned_anomalies)) then
|
|
release_all_from_transition(section)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
------------------------------
|
|
-- Main functions --
|
|
------------------------------
|
|
|
|
-- Sets a specific or random theme for any given route
|
|
-- @param gizmo transition-section || route-id
|
|
-- @param theme_id Optional theme id (randomized, if invalid)
|
|
function set_route_theme(gizmo, theme_id)
|
|
local theme = (theme_id and anomaly_themes[theme_id]) and theme_id
|
|
or math.random(1, #anomaly_themes)
|
|
|
|
local route_id = (route_manager.route_exists(gizmo)) and gizmo
|
|
or route_manager.get_route_by_transition(gizmo).id
|
|
|
|
if not utils.valid_type{ caller = "set_route_theme",
|
|
"int", route_id, "int", theme } then return end
|
|
|
|
route_manager.set_route_property(route_id, "anomaly_theme", theme)
|
|
|
|
if (route_manager.get_route_property(route_id, "anomaly_theme") ~= theme) then
|
|
log_warn("Unable to set route theme for route #%s", route_id)
|
|
end -- Just a sanity check
|
|
end
|
|
|
|
function spawn_for_route_on_current_level(route_id)
|
|
if not utils.valid_type{ caller = "spawn_for_route_on_current_level",
|
|
"int", route_id } then return end
|
|
|
|
local actor_level = utils.get_mapname()
|
|
|
|
if (utils.is_underground(actor_level)) then
|
|
log("Loaded underground level %s, will not execute.", actor_level)
|
|
return
|
|
end
|
|
|
|
local route = route_manager.get_route(route_id)
|
|
local m_data, safe_zone = alife_storage_manager.get_state()
|
|
local data = m_data and m_data[parent.data_key]
|
|
local last_used_transition = data[CONST_MDATA_KEY_USED_TRANSITION]
|
|
|
|
if (utils.table_has(route.members, last_used_transition)) then
|
|
data[CONST_MDATA_KEY_USED_TRANSITION] = nil
|
|
safe_zone = true
|
|
end
|
|
|
|
for section, attributes in route_manager.iterate_transitions(route_id) do
|
|
if (not utils.is_table_empty(attributes.spawned_anomalies)) then
|
|
log_warn("[%s] Already spawned anomalies for '%s'", route_id, section)
|
|
goto continue
|
|
end
|
|
|
|
local se_obj = section and get_story_se_object(section)
|
|
if utils.assert_failed(se_obj, "No object with the ID #%s exists!", id) then
|
|
goto continue
|
|
end
|
|
|
|
-- We need to check, if transition is on same level
|
|
local transition_level = utils.get_mapname(se_obj)
|
|
if (actor_level ~= transition_level) then goto continue end
|
|
|
|
populate_transition(section, safe_zone)
|
|
|
|
-- Just a logging check
|
|
local spawned = route_manager.get_transition_property(section, "spawned_anomalies")
|
|
log("Spawned %s anomalies for %s\n# %s",
|
|
size_table(spawned), section, table.concat(spawned, ", "))
|
|
|
|
:: continue ::
|
|
end
|
|
end
|
|
|
|
-- Spawns anomalies on a transition (static from LTX and dynamically)
|
|
-- Also reads transition specific spawn parameters from the INI
|
|
function populate_transition(section, safe_zone_around_player)
|
|
if not utils.valid_type{ caller = "populate_transition",
|
|
"str", section } then return end
|
|
|
|
local route_id = route_manager.get_route_id(section)
|
|
local theme_id = route_id and route_manager.get_route_property(route_id, "anomaly_theme")
|
|
|
|
local anomaly_theme = anomaly_themes[theme_id]
|
|
if utils.assert_failed(anomaly_theme and type(anomaly_theme) == "table") then
|
|
return
|
|
end
|
|
|
|
local parameters = get_spawn_parameters(section)
|
|
if utils.assert_failed(parameters) then return end
|
|
|
|
local rpos = parameters.rootpos
|
|
log("Spawning %s dynamic anomalies for '%s'%s (Theme: %s)",
|
|
parameters.count +1, section,
|
|
rpos and string.format(" @(%.1f, %.1f, %.1f)", rpos.x, rpos.y, rpos.z) or "",
|
|
game.translate_string(get_theme_string(theme_id)))
|
|
|
|
local transition_field_types = anomaly_theme.anomaly_field_types
|
|
local guarenteed_spawn_type = transition_field_types[math.random(1, #transition_field_types)]
|
|
spawn_at_transition(section, guarenteed_spawn_type, true, nil, parameters)
|
|
|
|
local position_data = kd_tree.buildTreeVectors()
|
|
if (utils.assert_failed(position_data)) then return end
|
|
|
|
local types = anomaly_theme.anomaly_mines_types
|
|
local count = parameters.count
|
|
while (count > 0) do
|
|
local anomaly_type = types[math.random(1, #types)]
|
|
utils.assert(anomaly_type)
|
|
|
|
spawn_at_transition(section, anomaly_type, false, position_data, parameters)
|
|
count = count -1
|
|
end
|
|
|
|
-- We now spawn static anomalies which fill the gaps on the map that have no level id's
|
|
populate_transition_with_static_spawns(section, anomaly_theme, position_data, parameters)
|
|
|
|
if (not safe_zone_around_player) then return end
|
|
local actor_pos, transition = db.actor:position(), route_manager.get_transition(section)
|
|
|
|
log("Establishing Safe-Zone around player for transition '%s' @(%.1f, %.1f, %.1f)",
|
|
section, actor_pos.x, actor_pos.y, actor_pos.z)
|
|
--[[
|
|
TODO: Only use one tree and directly attach Obj-ID's to position data
|
|
|
|
Gotta build a new tree, otherwise we won't have their object-id as reference
|
|
Returns table with elements like this: { { x = a, y = b, z = c, data = d}, distance }
|
|
--]]
|
|
local pos_tree = kd_tree.buildTreeSeObjectIds(transition.spawned_anomalies)
|
|
local anomalies_near = pos_tree:nearestAll(actor_pos)
|
|
if utils.assert_failed(not utils.is_table_empty(anomalies_near)) then
|
|
log_error("Position Tree returned no object references")
|
|
return
|
|
end
|
|
|
|
for _, anomaly_info in pairs(anomalies_near) do
|
|
local obj_id, distance = anomaly_info[1].data, anomaly_info[2]
|
|
if (distance > CONST_SAFEZONE_RADIUS) then break end -- Sorted by distance
|
|
|
|
local se_obj = alife_object(obj_id)
|
|
if (utils.assert_failed(se_obj, "Got no server object with ID #%s", obj_id)) then
|
|
goto continue
|
|
end
|
|
|
|
local anomaly_name = se_obj:section_name()
|
|
if (anomaly_name == guarenteed_spawn_type) then goto continue end
|
|
|
|
log("Releasing previously spawned Anomaly '%s' with ID #%s (Distance: %.2f)",
|
|
anomaly_name, obj_id, distance)
|
|
|
|
local id_idx = utils.index_of(transition.spawned_anomalies, obj_id)
|
|
if utils.assert_failed(id_idx) then
|
|
log_error("[#%s] Unable to find Obj-ID #%s in {%s}",
|
|
route_id, obj_id, unpack(transition.spawned_anomalies, ", "))
|
|
goto continue
|
|
end
|
|
|
|
table.remove(transition.spawned_anomalies, id_idx)
|
|
alife_release_id(obj_id)
|
|
|
|
:: continue ::
|
|
end
|
|
end
|
|
|
|
-- Randomly spawns anomalies from a predefined pool in the ini
|
|
-- @param transition_name section of target transition
|
|
-- @param anomaly_theme pointer to the theme (anomaly_themes[theme_id])
|
|
function populate_transition_with_static_spawns(transition_name, anomaly_theme, position_data, parameters)
|
|
if not utils.valid_type{ caller = "populate_transition_with_static_spawns",
|
|
"str", transition_name, "tbl", anomaly_theme, "tbl", parameters } then return end
|
|
|
|
if (not ini_static_spawns:section_exist(transition_name)) then
|
|
log("Transition '%s' has no predefined static spawns, skipping.", transition_name)
|
|
return
|
|
end
|
|
|
|
local min_anomaly_proximity = parameters.min_anomaly_proximity
|
|
local spawn_percent = parameters.static_spawn_ratio
|
|
local transition_spawns = ini_static_spawns:collect_section(transition_name)
|
|
local types = anomaly_theme.anomaly_mines_types
|
|
|
|
_g.shuffle_table(transition_spawns)
|
|
local shuffled_spawns = {}
|
|
for _, v in pairs(transition_spawns) do -- reindex for next step
|
|
shuffled_spawns[#shuffled_spawns +1] = v
|
|
end
|
|
|
|
local remaining_spawns = _g.round(#shuffled_spawns * spawn_percent)
|
|
log("Trying to populate transition %s with %s static spawns", transition_name, remaining_spawns)
|
|
|
|
local spawned = route_manager.get_transition_property(transition_name, "spawned_anomalies")
|
|
for index in utils.random_numbered_sequence(1, #shuffled_spawns) do
|
|
if (remaining_spawns < 1) then break end
|
|
local position_raw = shuffled_spawns[index]
|
|
|
|
local data = position_raw and utils.string_to_posdata(position_raw)
|
|
if (not data) then goto continue end
|
|
|
|
local anomaly_type, pos = types[math.random(1, #types)], data.pos
|
|
|
|
if (not posdata_distance_check(position_data, pos, anomaly_type, min_anomaly_proximity)) then
|
|
goto continue
|
|
end
|
|
|
|
local anomaly_id = drx_da_spawn_anomaly(anomaly_type, pos, data.lvid, data.gvid)
|
|
if (anomaly_id) then
|
|
spawned[#spawned +1] = anomaly_id
|
|
remaining_spawns = remaining_spawns -1
|
|
end
|
|
|
|
:: continue ::
|
|
end
|
|
end
|
|
|
|
-- @param transition_name section of target transition
|
|
-- @param anomaly_type section of anomaly
|
|
-- @param spawn_at_center boolean
|
|
-- @param position_data kd_tree; used when spawn_at_center = false (optional)
|
|
-- @param parameters needed when spawn_at_center = false
|
|
function spawn_at_transition(transition_name, anomaly_type, spawn_at_center, position_data, parameters)
|
|
-- TODO: Create corresponding function in utils
|
|
local id = transition_name and get_story_object_id(transition_name)
|
|
local transition_obj = id and alife_object(id)
|
|
if (not transition_obj) then
|
|
log_error("Can't create dynamic anomaly for %s, the specified transition does not exist.", transition_name)
|
|
return
|
|
end
|
|
|
|
local pos = (parameters) and parameters.rootpos or transition_obj.position
|
|
if (not spawn_at_center) then
|
|
pos = generate_valid_position(transition_obj, anomaly_type, parameters, position_data)
|
|
if (not pos) then
|
|
log_error("Failed to generate position. Aborting.")
|
|
return
|
|
end
|
|
end
|
|
|
|
local lvid = level.vertex_id(pos)
|
|
local anomaly_id = drx_da_spawn_anomaly(anomaly_type,
|
|
pos, lvid, transition_obj.m_game_vertex_id)
|
|
|
|
if (not anomaly_id) then return end
|
|
local spawned = route_manager.get_transition_property(transition_name, "spawned_anomalies")
|
|
spawned[#spawned +1] = anomaly_id
|
|
|
|
return true
|
|
end
|
|
|
|
-- Releases all spawned anomalies tied to any given transition
|
|
function release_all_from_transition(transition_name)
|
|
if not utils.valid_type{ caller = "release_all_from_transition",
|
|
"str", transition_name } then return end
|
|
|
|
local transition = route_manager.get_transition(transition_name)
|
|
local spawned_anomalies = transition and transition.spawned_anomalies
|
|
if (utils.assert_failed(spawned_anomalies)) then return end
|
|
|
|
local released = {}
|
|
for index, id in pairs(spawned_anomalies) do
|
|
if (alife_release_id(id)) then
|
|
released[#released +1] = id
|
|
else
|
|
log_error("Unable to release anomaly with ID #%s", id)
|
|
end
|
|
end
|
|
transition.spawned_anomalies = {} -- make sure no old references remain
|
|
|
|
log("Released the following %s IDs for %s\n# %s",
|
|
size_table(released), transition_name, table.concat(released, ", "))
|
|
end
|
|
|
|
function posdata_distance_check(position_data, pos, anomaly_type, minimum_proximity)
|
|
if not utils.valid_type{ caller = "posdata_distance_check",
|
|
"tbl", position_data, "usr", pos, "str", anomaly_type, "number", minimum_proximity } then return end
|
|
|
|
if (not position_data) then
|
|
return true
|
|
elseif (not position_data.root) then
|
|
position_data:insertAndRebuild{x = pos.x, y = pos.y, z = pos.z}
|
|
return true
|
|
end
|
|
|
|
local nearest = position_data:nearest(pos)
|
|
if not (nearest and nearest[1] and nearest[1][2]) then
|
|
log("Can't check position data (%.2f, %.2f, %.2f)", pos.x, pos.y, pos.z)
|
|
position_data:insertAndRebuild{x = pos.x, y = pos.y, z = pos.z}
|
|
return true
|
|
end
|
|
|
|
local radii = get_anomaly_radii(anomaly_type)
|
|
local anomaly_radius = _g.round((radii.min + radii.max) / 2) -- mean
|
|
local spawned_distance = nearest[1][2] - (anomaly_radius *2)
|
|
|
|
if (spawned_distance >= minimum_proximity) then
|
|
position_data:insertAndRebuild{x = pos.x, y = pos.y, z = pos.z}
|
|
return true
|
|
end
|
|
|
|
log("Position invalid. Too close. (Distance %s < %s) @(%.2f, %.2f, %.2f)",
|
|
spawned_distance, minimum_proximity, pos.x, pos.y, pos.z)
|
|
end
|
|
|
|
-- ANOMALY SPAWN FUNCTIONS
|
|
-- THE FOLLOWING LINES ARE TAKEN FROM drx_da_main.script
|
|
-- FROM DYNAMIC ANOMALIES OVERHAUL (DAO) (UPDATE 25)
|
|
-- CREDITS TO TheMrDemonized, DoctorX, Eugenium, Barry Bogs, Lucy, Grok, Jurkonov, CrimsonVirus
|
|
--
|
|
-- THESE FUNCTIONS HAVE BEEN ADAPTED FOR USE IN THIS ADDON BY THE MOD AUTHORS
|
|
|
|
-- Spawns one anomaly of any valid given type
|
|
-- @param anomaly_type (anomaly section)
|
|
-- @param pos, lvid, gvid (self-explanatory)
|
|
-- @returns se_obj_id
|
|
function drx_da_spawn_anomaly(anomaly_type, pos, lvid, gvid)
|
|
if not (anomaly_type and ini_sys:section_exist(anomaly_type)) then
|
|
log_error("Anomaly type '%s' does not exist! Aborting.", anomaly_type)
|
|
return
|
|
end
|
|
|
|
local se_obj = alife():create(anomaly_type, pos, lvid, gvid)
|
|
if (not se_obj) then
|
|
log_error("[%s] Unable to spawn anomaly.", anomaly_type)
|
|
return
|
|
end
|
|
|
|
local function abort_creation(se_obj_id, anomaly_type)
|
|
utils.timed_call(CONST_ABORT_CREATION_DELAY_MS, function()
|
|
local obj = alife_object(se_obj_id)
|
|
if obj then
|
|
log_error("[%s] Anomaly '%s' failed to spawn correctly, releasing.",
|
|
se_obj_id, anomaly_type)
|
|
alife_release(obj)
|
|
end
|
|
|
|
return true
|
|
end)
|
|
end
|
|
|
|
local data = utils_stpk.get_anom_zone_data(se_obj) -- get anomaly properties
|
|
if (not data) then
|
|
abort_creation(se_obj.id, anomaly_type)
|
|
return
|
|
end
|
|
|
|
data.shapes[1] = {}
|
|
data.shapes[1].shtype = 0
|
|
data.shapes[1].offset = vector():set(0, 0, 0) -- Leave for compatibility with CoC 1.4.22, delete later
|
|
data.shapes[1].center = vector():set(0, 0, 0)
|
|
|
|
local radii = get_anomaly_radii(anomaly_type)
|
|
data.shapes[1].radius = math.random(radii.min, radii.max)
|
|
utils_stpk.set_anom_zone_data(data, se_obj)
|
|
|
|
return se_obj.id
|
|
end
|
|
|
|
-- Generates a random valid position from a vertex in radius around a given transition
|
|
-- @param se_obj game object of the transition space restrictor
|
|
-- @param anomaly_type section of the anomaly to be spawned
|
|
-- @param spawn_params table containing the spawn parameters
|
|
-- @param position_data (optional)
|
|
function generate_valid_position(se_obj, anomaly_type, spawn_params, position_data)
|
|
if not utils.valid_type{ caller = "generate_valid_position",
|
|
"usr", se_obj, "str", anomaly_type, "tbl", spawn_params } then return end
|
|
|
|
local random = math.random
|
|
|
|
local level_vertex_id = level.vertex_id
|
|
local level_vertex_position = level.vertex_position
|
|
|
|
local rootpos = spawn_params.rootpos or se_obj.position
|
|
local num_tries = spawn_params.generate_max_tries
|
|
local spread_radius = spawn_params.spread_radius
|
|
local minimum_proximity = spawn_params.min_anomaly_proximity
|
|
|
|
local max_offset = {x = spread_radius, y = spread_radius, z = spread_radius}
|
|
if (spawn_params.narrow) then
|
|
for k, v in pairs(max_offset) do
|
|
max_offset[k] = math.floor(max_offset[k] * CONST_GENERATE_NARROW_FACTOR)
|
|
end
|
|
end
|
|
|
|
local pos = vector():set(0, 0, 0)
|
|
|
|
-- In the following, we randomly offset the position to
|
|
-- get a valid level vertex id we can spawn an anomaly on
|
|
while (num_tries > 0) do
|
|
local offset_x = max_offset.x * random()
|
|
local pos_x = (random() <= 0.5) and (rootpos.x +offset_x)
|
|
or (rootpos.x -offset_x)
|
|
|
|
local offset_y = max_offset.y * random()
|
|
local pos_y = (random() <= 0.5) and (rootpos.y +offset_y)
|
|
or (rootpos.y -offset_y)
|
|
|
|
local offset_z = max_offset.z * random()
|
|
local pos_z = (random() <= 0.5) and (rootpos.z +offset_z)
|
|
or (rootpos.z -offset_z)
|
|
|
|
-- Set anomaly position at vertex and check if valid:
|
|
pos = vector():set(pos_x, pos_y, pos_z)
|
|
|
|
local check_distance -- interpreter would complain bc of goto statements
|
|
|
|
pos = utils.get_closest_vertex_pos(pos)
|
|
if (not pos) then goto next_try end
|
|
|
|
check_distance = pos:distance_to(rootpos)
|
|
if (check_distance > spread_radius) then
|
|
log_error("Distance to root position is too high (%s)", check_distance)
|
|
goto next_try
|
|
end
|
|
|
|
if (posdata_distance_check(position_data, pos, anomaly_type, minimum_proximity)) then
|
|
break
|
|
end
|
|
|
|
:: next_try ::
|
|
num_tries = (num_tries - 1)
|
|
end
|
|
|
|
if (num_tries <= 0) then
|
|
log_error("Unable to generate valid lvid position, aborting.")
|
|
return
|
|
end
|
|
|
|
return pos
|
|
end
|