Divergent/mods/Dynamic Zone Transitions/gamedata/scripts/dynamic_zone_anomalies.script

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