Divergent/mods/Zone Customization Project/gamedata/scripts/bind_anomaly_field.script

640 lines
18 KiB
Plaintext

--[[
Tronex
set enable_debug to true, for debugging and map markers
----------------------------------------------------------
- Dynamic Anomalies
2019/6/14
used ini:
plugins\dynamic_anomalies.ltx
1. Script will read the list of anomalies and their position/types from the config (you can recorded custom pos for your anomalies)
2. Then spawn all anomalies on new game, then disable a random number of them.
3. When an enmission happen, anomalies will shuffle between off/on state (the dynamic factor)
----------------------------------------------------------
- The Pulse
A concept of electro-psy anomaly that forms in the sky and discharge into the ground like a thunderbolt, killing any stalkers nearby
--]]
local enable_debug = false
function print_debug(...)
if enable_debug then
printf(...)
end
end
-------------------------------
-- Dynamic anomalies
-------------------------------
local ini_ano
local dyn_ano_init = false
local current_level
local dyn_ano_chance = 35 -- [0 - 100] chance of activating a dynamic anomaly
local dyn_ano_safe_dist = 15 -- [m] don't activate anomalies within the safe distance to player
local dyn_ano_type = {} -- [type] = {}
local dyn_ano_info = {} -- [level][name] = info
local dyn_anomalies_dbg = {} -- [id] = name
dyn_anomalies = {} -- [level][id] = name
-- ZCP
if smr_amain_mcm.get_config("smr_enabled") then
dyn_ano_chance = smr_anomalies_mcm.get_config("dyn_ano_chance")
end
-- ZCP END
-- Prepare
function ini_settings()
if (dyn_ano_init) then return end
dyn_ano_init = true
ini_ano = ini_file("plugins\\dynamic_anomalies.ltx")
local n,m = 0,0
local result, id, value = "","",""
local name, info = "","",""
-- Gather anomaly types
n = ini_ano:line_count("categories") or 0
for i=0,n-1 do
result, id, value = ini_ano:r_line_ex("categories",i,"","")
-- ZCP
if smr_amain_mcm.get_config("smr_enabled") and (smr_anomalies_mcm.get_config(id) == false) then
smr_debug.get_log().info("anomalies/types", "skipping disabled anomaly type %s", id)
goto continue
end
-- ZCP END
dyn_ano_type[id] = {}
m = ini_ano:line_count(id) or 0
for ii=0,m-1 do
result, name, info = ini_ano:r_line_ex(id,ii,"","")
if name and info then
for j=1,tonumber(info) do
local size = #dyn_ano_type[id] + 1
dyn_ano_type[id][size] = name
print_debug("- Dynamic Anomalies | dyn_ano_type[%s][%s] = %s", id, size, name)
end
end
end
::continue::
end
-- Gather anomaly coordinates in all levels
n = ini_ano:line_count("levels") or 0
for i=0,n-1 do
result, id, value = ini_ano:r_line_ex("levels",i,"","")
m = ini_ano:line_count(id) or 0
dyn_ano_info[id] = {}
for ii=0,m-1 do
result, name, info = ini_ano:r_line_ex(id,ii,"","")
if name and info then
local t = str_explode(info,",")
if (#t == 6) and (t[1] ~= "NA") then
dyn_ano_info[id][name] = {
typ = t[1],
x = tonumber(t[2]),
y = tonumber(t[3]),
z = tonumber(t[4]),
lvl_id = tonumber(t[5]),
gm_id = tonumber(t[6]),
}
end
end
end
end
end
local marker_by_type = {
["electric"] = "anomaly_electric",
["chemical"] = "anomaly_chemical",
["thermal"] = "anomaly_thermal",
["gravitational"] = "anomaly_gravitational",
["radioactive"] = "anomaly_radioactive",
["disabled"] = "anomaly_disabled",
}
function add_marker(lvl, section, id, state)
if enable_debug then
ini_settings()
if lvl and dyn_ano_info[lvl] then
local name = dyn_anomalies_dbg[id]
if name then
local info = dyn_ano_info[lvl][name]
if info then
for k,v in pairs(marker_by_type) do
if (level.map_has_object_spot(id, v) ~= 0) then
level.map_remove_object_spot(id, v)
end
end
local typ = info.typ
local spot = marker_by_type[typ] or marker_by_type["gravitational"]
if (state == false) then
spot = marker_by_type["disabled"]
end
level.map_add_object_spot_ser(id, spot, "Name: " .. name .. " \\nType: " .. typ .. " \\nSection: " .. section)
else
print_debug("! Dynamic Anomalies | Marker - no info is found for name {%s}", name)
end
else
print_debug("! Dynamic Anomalies | Marker - no name is found for id (%s)", id)
end
else
print_debug("! Dynamic Anomalies | Marker - level %s is not stored in dyn_ano_info table", lvl)
end
end
end
-- Operation
function dyn_anomalies_spawn()
-- Iterate through all anomalies info for all levels
for lvl,v in pairs(dyn_ano_info) do
dyn_anomalies[lvl] = {}
for name,info in pairs(v) do
-- Get random anomaly section
local anom_type = dyn_ano_type[info.typ]
local section = anom_type and anom_type[math.random(#anom_type)]
if (not section) then
print_debug("! Dynamic Anomalies | Anomaly section not found for type: %s", info.typ)
return
end
-- Info check
if not (info.x and info.y and info.z and info.lvl_id and info.gm_id and true) then
print_debug("! Dynamic Anomalies | Anomaly {%s} has wrong or incomplete info", name)
return
end
-- Spawn
local se_obj = alife_create( section, vector():set(info.x , info.y , info.z), info.lvl_id, info.gm_id )
if ( not se_obj ) then
print_debug("! Dynamic Anomalies | Unable to spawn dynamic anomaly")
return
end
-- Set anomaly properties:
local data = utils_stpk.get_anom_zone_data( se_obj )
if ( not data ) then
print_debug("! Dynamic Anomalies | Unable to set dynamic anomaly properties" )
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 )
data.shapes[1].radius = 3
utils_stpk.set_anom_zone_data( data, se_obj )
-- Save data
dyn_anomalies[lvl][se_obj.id] = true
if enable_debug then
dyn_anomalies_dbg[se_obj.id] = name
end
print_debug("- Dynamic Anomalies | %s | Spawned anomaly [%s](%s){%s}", lvl, section, se_obj.id, name)
end
end
end
function dyn_anomalies_suffle()
for lvl,v in pairs(dyn_anomalies) do
for id, state in pairs(v) do
if (math.random(100) < dyn_ano_chance) then
dyn_anomalies[lvl][id] = true
print_debug("/ Dynamic Anomalies | Shuffle - dyn_anomalies[%s][%s] = %s", lvl, id, true)
else
dyn_anomalies[lvl][id] = false
print_debug("/ Dynamic Anomalies | Shuffle - dyn_anomalies[%s][%s] = %s", lvl, id, false)
end
end
end
end
function dyn_anomalies_update()
if (not dyn_anomalies[current_level]) then
print_debug("! Dynamic Anomalies | Can't update anomalies because current level (%s) has no anomalies recorded", current_level)
return true
end
local actor_pos = db.actor:position()
for id,state in pairs(dyn_anomalies[current_level]) do
local obj = level.object_by_id(id)
if obj then
if (actor_pos:distance_to(obj:position()) > dyn_ano_safe_dist) then
obj:enable_anomaly()
if (state == false) then
obj:disable_anomaly()
end
add_marker(current_level, obj:section(), id, state)
print_debug("- Dynamic Anomalies | %s | Anomaly (%s) is set to state: %s", current_level, id, state)
else
print_debug("! Dynamic Anomalies | %s | Anomaly (%s) is close to player, no process", current_level, id)
end
else
print_debug("! Dynamic Anomalies | %s | Couldn't get online object for id (%s)", current_level, id)
end
end
return true
end
function dyn_anomalies_refresh(force)
-- Prepare anomalies for the first time
if (not has_alife_info("dynamic_anomalies_spawned")) and is_empty(dyn_anomalies) then
give_info("dynamic_anomalies_spawned")
ini_settings()
dyn_anomalies_spawn()
dyn_anomalies_suffle()
-- shuffle state of all anomalies after emission
elseif force then
dyn_anomalies_suffle()
end
-- enable/disable online anomalies
-- NOTE: it's important to use timer because online objects don't register instantly after creating the server objects, so we need to wait for a bit.
-- Guess there's a delay in engine to set things up completely
local n = has_alife_info("dynamic_anomalies_spawned") and 1 or 10
CreateTimeEvent(0, "update_dynamic_anomalies", n, dyn_anomalies_update)
end
-------------------------------
-- Pulse anomalies
-------------------------------
local pAno_first = true
local pAno_tg = time_global()
local pAno_light = nil
local pAno_pfx = particles_object("generator\\generator_accum_thunderbolt")
local pAno_snd_close = sound_object("anomaly\\emi_blowout")
local pAno_snd_far = sound_object("anomaly\\emi_blowout_01")
local pAno_snd_distance = 150 -- [m] (max distance between player and anomaly where close sound effect can be heard)
local pAno_snd_delay = 5.5 -- [sec] (time delay between anomaly's spawn and sound effect)
local pAno_p_hit_distance = 10 -- [m] (max distance between player and anomaly to recieve psy damage)
local pAno_e_hit_distance = 20 -- [m] (max distance between player and anomaly to recieve shock damage)
local pAno_hit_delay = 11.5 -- [sec] (time delay between anomaly's spawn and player hit)
local pAno_article_distance = 50 -- [m] (max distance between player and anomaly to trigger related article )
local pAno_max_distance = 150 -- [m] (max distance between player and anomaly's spawn)
local pAno_delay = 2 * 60 * 1000 -- [millie sec] (smallest time delay between pulse anomalies to spawn)
local pAno_chance = {
["clear"] = 0,
["partly"] = 0,
["cloudy"] = 10,
["rain"] = 15,
["storm"] = 25,
["foggy"] = 0,
}
local pAno_maps = {
["k00_marsh"] = 0.5,
["k01_darkscape"] = 1,
["k02_trucks_cemetery"] = 1,
["l01_escape"] = 0.2,
["l02_garbage"] = 0.7,
["l03_agroprom"] = 0.5,
["l04_darkvalley"] = 0.5,
["l06_rostok"] = 1,
["l07_military"] = 0.5,
["l08_yantar"] = 0.7,
["l09_deadcity"] = 0.5,
["l10_red_forest"] = 1,
["jupiter"] = 1,
["pripyat"] = 1,
["zaton"] = 1,
["l13_generators"] = 1.5,
["l12_stancia_2"] = 1.5,
["l12_stancia"] = 1.5,
["l11_pripyat"] = 0.2,
["l10_radar"] = 1,
["y04_pole"] = 0.7,
}
local function pulse_anomaly_sound(sound_pos)
local distance = distance_2d(db.actor:position(), sound_pos)
local pAno_snd = (distance > pAno_snd_distance) and pAno_snd_far or pAno_snd_close
if pAno_snd and pAno_snd:playing() then
pAno_snd:stop()
end
if pAno_snd ~= nil then
pAno_snd:play_at_pos(db.actor, sound_pos)
pAno_snd.volume = 1
end
pAno_light:set_position(sound_pos)
pAno_light.enabled = true
pAno_light:update()
return true
end
local function pulse_anomaly_hit(particle_pos)
pAno_light.lanim_brightness = 0.2
pAno_light.volumetric_distance = 1
pAno_light.volumetric_intensity = 0.1
if GetEvent("current_safe_cover") then
return true
end
local hit_power = 0
local distance = distance_2d(db.actor:position(), particle_pos)
-- Article
if distance < pAno_article_distance then
SendScriptCallback("actor_on_interaction", "anomalies", nil, "pulse")
end
-- Psi hit
if distance < pAno_p_hit_distance then
hit_power = math.cos(distance * math.pi / pAno_p_hit_distance) + 1
local h = hit()
h.type = hit.telepatic
if (level_environment.is_actor_immune() or dialogs_yantar.actor_has_psi_helmet()) then
h.power = 0
else
h.power = surge_manager.SurgeManager:hit_power(hit_power, h.type)
end
h.impulse = 0
h.direction = VEC_Z
h.draftsman = db.actor
db.actor:hit(h)
level.remove_pp_effector(666)
level.add_pp_effector("psi_fade.ppe", 666, false)
level.set_pp_effector_factor(666,h.power)
end
-- Electric hit
if distance < pAno_e_hit_distance then
hit_power = math.cos(distance * math.pi / pAno_e_hit_distance) + 1
local h = hit()
h.type = hit.shock
if (level_environment.is_actor_immune()) then
h.power = 0
else
h.power = surge_manager.SurgeManager:hit_power(hit_power, h.type)
end
h.impulse = 0
h.direction = VEC_Z
h.draftsman = db.actor
db.actor:hit(h)
level.remove_pp_effector(667)
level.add_pp_effector("electro_fade.ppe", 667, false)
level.set_pp_effector_factor(667,h.power)
end
return true
end
local function pulse_anomaly_light()
pAno_light.lanim_brightness = 0.025
pAno_light.volumetric_distance = 0.25
pAno_light.volumetric_intensity = 0.05
pAno_light.enabled = false
return true
end
function pulse_anomaly_update()
local tg = time_global()
if pAno_first then
pAno_tg = tg + pAno_delay
pAno_first = false
return
end
if (pAno_light and pAno_light.enabled) then
pAno_light:update()
end
if bLevelUnderground or (tg < pAno_tg) then
return
end
pAno_tg = tg + pAno_delay
local lvl_factor = pAno_maps[level.name()] or 0
local wthr = level_weathers.get_weather_manager():get_curr_weather()
local weather_chance = pAno_chance[wthr] or 1
if (math.random(100) > (weather_chance * lvl_factor)) then
return
end
local pos = db.actor:position()
local angle_dec = math.random(0,359)
local angle_rad = math.rad(angle_dec)
local ano_distance = math.random(0,pAno_max_distance)
local pos_x = math.cos(angle_rad)*ano_distance
local pos_z = math.sin(angle_rad)*ano_distance
local particle_pos = vector():set(pos.x+pos_x, pos.y+60, pos.z+pos_z)
pAno_pfx:play_at_pos(particle_pos)
if (not pAno_light) then
--local color = fcolor()
--color:set(0,0,100,50)
pAno_light = script_light()
pAno_light.range = 100
--pAno_light.type = 0 --light_type.Direct)
--pAno_light:set_direction(vector():set(0,-1.5,0))
--pAno_light.shadow = true
pAno_light.lanim = "koster_01_electra"
pAno_light.lanim_brightness = 0.025
pAno_light.volumetric = true
pAno_light.volumetric_quality = 1
pAno_light.volumetric_distance = 0.25
pAno_light.volumetric_intensity = 0.05
--pAno_light.color = color
end
CreateTimeEvent(0, "pulse_anomaly_sound", pAno_snd_delay, pulse_anomaly_sound, particle_pos)
CreateTimeEvent(0, "pulse_anomaly_hit", pAno_hit_delay, pulse_anomaly_hit, particle_pos)
CreateTimeEvent(0, "pulse_anomaly_light", pAno_hit_delay + 0.5, pulse_anomaly_light)
end
-------------------------------
-- Callbacks
-------------------------------
local function actor_on_first_update()
current_level = level.name()
local enabled = ui_options.get("alife/general/dynamic_anomalies")
if enabled and (not IsTestMode()) then
dyn_anomalies_refresh()
end
end
local function actor_on_update()
-- ZCP
if smr_anomalies_mcm.get_config("pulse") then
pulse_anomaly_update()
end
end
local function actor_on_interaction(typ, obj, name)
if (typ == "anomalies") and (name == "emission_end") and ui_options.get("alife/general/dynamic_anomalies") then
dyn_anomalies_refresh(true)
end
end
local function save_state(m_data)
m_data.dyn_anomalies = dyn_anomalies
if enable_debug then
m_data.dyn_anomalies_dbg = dyn_anomalies_dbg
end
end
local function load_state(m_data)
dyn_anomalies = m_data.dyn_anomalies or {}
if enable_debug then
dyn_anomalies_dbg = m_data.dyn_anomalies_dbg or {}
end
end
local function anomaly_on_before_activate(zone, obj, flags)
if (not obj or not zone) then
return
end
if (IsStalker(obj) or IsMonster(obj)) then
if (not obj:alive()) then
flags.ret_value = false
end
return
end
if not (obj:clsid() == clsid.obj_bolt) then
flags.ret_value = false
end
end
function on_game_start()
RegisterScriptCallback("actor_on_first_update",actor_on_first_update)
RegisterScriptCallback("actor_on_update",actor_on_update)
RegisterScriptCallback("actor_on_interaction",actor_on_interaction)
RegisterScriptCallback("anomaly_on_before_activate",anomaly_on_before_activate)
RegisterScriptCallback("save_state",save_state)
RegisterScriptCallback("load_state",load_state)
end
-------------------------------
-- Anomaly field binder
-------------------------------
fields_by_names = {}
function bind(obj)
obj:bind_object(anomaly_field_binder(obj))
end
class "anomaly_field_binder" (object_binder)
function anomaly_field_binder:__init(obj) super(obj)
end
function anomaly_field_binder:reload(section)
object_binder.reload(self, section)
end
function anomaly_field_binder:reinit()
object_binder.reinit(self)
db.storage[self.object:id()] = {}
self.st = db.storage[self.object:id()]
end
function anomaly_field_binder:net_spawn(se_abstract)
if not object_binder.net_spawn(self, se_abstract) then
return false
end
db.add_zone(self.object)
db.add_obj(self.object)
fields_by_names[self.object:name()] = self
--[[
eDefaultRestrictorTypeNone = u8(0),
eDefaultRestrictorTypeOut = u8(1),
eDefaultRestrictorTypeIn = u8(2),
eRestrictorTypeNone = u8(3),
eRestrictorTypeIn = u8(4),
eRestrictorTypeOut = u8(5),
--]]
-- don't enable unless you realize that engine AI schemes to deal with anomalies is stupid and will not be supported
-- MAY CAUSE HUGE FPS DROP ON COP MAPS
--[[
if (get_console_cmd(1,"ai_die_in_anomaly") == true) then
-- It causes HUGE fps drop on COP maps which is why it was probably cut
local ignore = {
["zaton"] = true,
["jupiter"] = true,
["pripyat"] = true
}
if not (ignore[level.name()]) then
self.object:set_restrictor_type(3)
end
end
--]]
return true
end
function anomaly_field_binder:net_destroy()
db.del_zone( self.object )
db.del_obj(self.object)
db.storage[self.object:id()] = nil
fields_by_names[self.object:name()] = nil
object_binder.net_destroy(self)
end
function anomaly_field_binder:set_enable(bEnable)
if(bEnable) then
self.object:enable_anomaly()
else
self.object:disable_anomaly()
end
end
function anomaly_field_binder:update(delta)
object_binder.update(self, delta)
--[[ testing
local itr = function(id)
local obj = id and alife_object(id)
printf("%s touch_feel id=%s obj=%s",self.object:name(),id,obj and obj:name())
end
self.object:iterate_feel_touch(itr)
--]]
end
-- Standart function for save
function anomaly_field_binder:net_save_relevant()
return true
end