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