-- OPTIONS -- Dont set this to 0 -- Visibility will be reduce by this value each update visibility_reduce_coefficient = 0.02 -- Visibility will start to reduce after this amount of updates -- Each update is 200ms visibility_reduce_tick_threshold = 6 -- Smoke field registry smoke_fields = {} function spawn_smoke_field(pos, radius, lifetime) local f = SmokeField(pos, radius, lifetime) smoke_fields[#smoke_fields + 1] = f return f end function update_smoke_fields() local tg = time_global() for i = #smoke_fields, 1, -1 do local f = smoke_fields[i] if tg > f:getEndTime() then table.remove(smoke_fields, i) printf("%s: SmokeField destroy", f.init_time) end end end -- Smoke field class class "SmokeField" function SmokeField:__init(pos, radius, lifetime) self.position = vector():set(pos) self.radius = radius self.lifetime = lifetime self.init_time = time_global() -- Adjust position by radius up self.position.y = self.position.y + 1 printf("%s: SmokeField spawn", self.init_time) end function SmokeField:getEndTime() return self.init_time + self.lifetime * 1000 end function bind(obj) obj:bind_object(smoke_grenade_binder(obj)) end class "smoke_grenade_binder" (object_binder) function smoke_grenade_binder:__init(obj) super(obj) self.sec = self.object:section() -- Flag of fused self.fused = false self.time_event_done = false -- Get fuse time self.fuse_time = SYS_GetParam(2, self.sec, "destroy_time", 0) / 1000 -- Smoke params self.smoke_radius = SYS_GetParam(2, self.sec, "smoke_radius", 0) self.smoke_lifetime = SYS_GetParam(2, self.sec, "smoke_lifetime", 0) -- Fastcall to collision detection check self.object:set_fastcall(self.fastcall, self) end function smoke_grenade_binder:fastcall() local obj = self.object local id = obj:id() -- If object is in someone hands, dont check it if obj:parent() then if self.position then self.position = nil end self.fused = nil return end if self.fused == nil then printf("%s: check for fuse", obj:name()) self.fused = not alife_object(obj:id()) end if not self.fused then return end self.position = obj:position() if not self.time_event_done then self.time_event_done = true printf("%s: fused %s", obj:name(), self.fused) CreateTimeEvent(obj:name(), "spawn_smoke_field", self.fuse_time, function() printf("%s: spawn smoke field at %s", obj:name(), self.position) spawn_smoke_field(self.position, self.smoke_radius, self.smoke_lifetime) return true end) end end function actor_on_first_update() AddUniqueCall(update_smoke_fields) end function on_game_start() RegisterScriptCallback("actor_on_first_update", actor_on_first_update) end function get_npc_pos(npc) return utils_obj.safe_bone_pos(npc, "bip01_spine") or npc:position():add(vector():set(0, 0.4, 0)) end if AddScriptCallback then AddScriptCallback("npc_behind_smoke") end function is_smoke_field_between_npcs(field, npc, who) -- Check if field is between npcs local npc_pos = get_npc_pos(npc) local who_pos = get_npc_pos(who) local field_to_npc = vector():set(field.position):sub(npc_pos) local field_to_who = vector():set(field.position):sub(who_pos) if field_to_npc:dotproduct(field_to_who) > 0 then -- Vectors are in the same direction, field is not in between -- printf("check %s against %s, field is not in between", npc:name(), who:name()) return false end -- Construct invisibility cone local apex = npc_pos local pos = vector():set(who_pos):sub(apex):normalize() local axis_direction = vector():set(field_to_npc):normalize() local dot = pos:dotproduct(axis_direction) local cos_half_angle do local height = apex:distance_to(field.position) local half_angle = math.atan(field.radius / height) cos_half_angle = math.cos(half_angle) end -- Check if inside invisibility cone local res = dot >= cos_half_angle -- Visualize -- local function lerpVec(v1, v2, f) -- return vector():lerp(v1, v2, f) -- end -- local t = {} -- t[1] = vector_rotate_y(axis_direction, 90) -- t[2] = vector():set(t[1]):invert() -- t[3] = vector():crossproduct(axis_direction, t[1]) -- t[4] = vector():set(t[3]):invert() -- t[5] = lerpVec(t[1], t[3], 0.5) -- t[6] = vector():set(t[5]):invert() -- t[7] = lerpVec(t[2], t[3], 0.5) -- t[8] = vector():set(t[7]):invert() -- for i, v in ipairs(t) do -- local v1 = vector():mad(field.position, v, field.radius) -- demonized_geometry_ray.VisualizeRay( -- npc_pos, -- vector():mad(npc_pos, vector():set(v1):sub(npc_pos):normalize(), vector():set(v1):sub(npc_pos):magnitude() + 10), -- nil, -- 199 -- ) -- end -- if res then -- print_tip("check %s against %s, %s is in invisibility cone", npc:name(), who:name(), who:name()) -- end return res end function IsValidMonster(o) local c = o:clsid() return c == clsid.burer or c == clsid.burer_s or c == clsid.controller or c == clsid.controller_s end local vis_decrease_ticks = {} get_visible_value = visual_memory_manager.get_visible_value visual_memory_manager.get_visible_value = function(npc,who,time_delta,time_quant,luminocity,velocity_factor,velocity,distance,object_distance,always_visible_distance) local res = get_visible_value(npc,who,time_delta,time_quant,luminocity,velocity_factor,velocity,distance,object_distance,always_visible_distance) if #smoke_fields > 0 and npc and who and (IsStalker(npc) or IsValidMonster(npc)) and (who:id() == AC_ID or IsStalker(who) or IsMonster(who)) then local under_field = false for i = 1, #smoke_fields do local f = smoke_fields[i] if is_smoke_field_between_npcs(f, npc, who) then under_field = true -- Add an update threshold local npc_name = npc:name() local who_name = who:name() vis_decrease_ticks[npc_name] = vis_decrease_ticks[npc_name] or {} vis_decrease_ticks[npc_name][who_name] = (vis_decrease_ticks[npc_name][who_name] or 0) + 1 if vis_decrease_ticks[npc_name][who_name] >= visibility_reduce_tick_threshold then -- print_tip("check %s against %s, %s is in invisibility cone, start reducing visibility %s/%s", npc:name(), who:name(), who:name(), vis_decrease_ticks[npc_name][who_name], visibility_reduce_tick_threshold) res = -visibility_reduce_coefficient else -- print_tip("check %s against %s, %s is in invisibility cone, under threshold %s/%s", npc:name(), who:name(), who:name(), vis_decrease_ticks[npc_name][who_name], visibility_reduce_tick_threshold) end -- Send callback -- Npc is the one who tries to see enemy behind smoke -- Who is the enemy behind smoke if AddScriptCallback then SendScriptCallback("npc_behind_smoke", npc, who) end end end if not under_field then vis_decrease_ticks[npc:name()] = vis_decrease_ticks[npc:name()] or {} vis_decrease_ticks[npc:name()][who:name()] = 0 end elseif npc and who then vis_decrease_ticks[npc:name()] = vis_decrease_ticks[npc:name()] or {} vis_decrease_ticks[npc:name()][who:name()] = 0 end return res end