-- MCM function load_defaults() local t = {} local op = blood_pool_mcm.op for i, v in ipairs(op.gr) do if v.def ~= nil then t[v.id] = v.def end end return t end settings = load_defaults() function load_settings() settings = load_defaults() if ui_mcm then for k, v in pairs(settings) do settings[k] = ui_mcm.get("blood_pool/" .. k) end end return settings end -- UTILS local function vecSimilar(v1, v2, eps) eps = eps or 0.4 local abs = math.abs return abs(v1.x - v2.x) < eps and abs(v1.y - v2.y) < eps and abs(v1.z - v2.z) < eps end local enable_debug = false local function printf(...) if enable_debug then _G.printf(...) end end pool_starts_in = 5 blood_textures_amount = 24 pool_size_mult = { -- clsid -- [clsid.boar_s] = boar, -- [clsid.flesh_s] = flesh, -- [clsid.burer_s] = burer, -- [clsid.chimera_s] = chimera, -- [clsid.script_stalker] = human, -- [clsid.bloodsucker_s] = sucker, -- [clsid.dog_s] = dog, -- [clsid.pseudodog_s] = dog, -- [clsid.psy_dog_s] = dog, -- [clsid.psy_dog_phantom_s] = dog, -- [clsid.cat_s] = cat, -- [clsid.rat] = rat, -- [clsid.rat_s] = rat, -- [clsid.controller_s] = controller, -- [clsid.fracture_s] = fracture, -- [clsid.poltergeist_s] = poltergeist, -- [clsid.gigant_s] = gigant, -- [clsid.zombie_s] = zombie, -- [clsid.snork_s] = snork, -- [clsid.tushkano_s] = tushkano, -- [136] = karlik, -- [120] = sucker, -- psysucker -- [124] = chimera, -- lurker -- string find these patterns bloodsucker = 1.2, psysucker = 1.2, boar = 1.5, dog = 1.0, flesh = 1.25, burer = 1.25, cat = 0.8, rat = 0.3, chimera = 2.0, bibliotekar = 2.5, lurker = 1.25, karlik = 0.8, controller = 1.0, fracture = 1.1, gigant = 2.0, giant = 2.0, zombi = 1.0, snork = 1.0, tushkan = 0.5, } boneList = { "bip01_spine", "bip01_spine1", "spine", "spine1", } function getBloodPoolPos(obj) for i, v in ipairs(boneList) do if obj:get_bone_id(v) ~= 65535 then -- printf("found bone %s for obj %s", v, obj:section()) return utils_obj.safe_bone_pos(obj, v) end end return obj:position():add(vector():set(0, 0.5, 0)) end -- saved_pool stores information about npc which can leak blood pool -- alreadyLeakedPools stores information about leaked pools grouped by npc saved_pool = {} alreadyLeakedPools = {} -- on npc death -> time event and after 3 sec save npc bone position function start_blood_pool(id) local obj = level.object_by_id(id) if not obj then return end local pos = getBloodPoolPos(obj) saved_pool[id] = {} saved_pool[id].pos = pos saved_pool[id].texture = "blood_pool_texture_" .. math.random(1, blood_textures_amount) saved_pool[id].moved = false saved_pool[id].size_mult = 1 if IsMonster(obj) then for k, v in pairs(pool_size_mult) do if obj:section():find(k) then saved_pool[id].size_mult = v break end end end local ttl = settings.pool_lifetime local function draw_pool(size, npc_id, i, size_i) -- if npc with this id doesnt exist delete key -- or size reached max - delete key before drawing the last decal local npc = level.object_by_id(npc_id) if not npc or i >= settings.num_of_updates then saved_pool[npc_id] = nil CreateTimeEvent("remove_blood_pool", npc_id, ttl, function() alreadyLeakedPools[npc_id] = nil return true end) return true end -- if key is nil then dont do anything if not saved_pool[npc_id] then return true end local function genNextEvent() -- reduce lifetime of current decal for the entire pool to disappear together ttl = ttl - settings.update_freq -- Generate next time event size_i = size_i + 1 i = i + 1 CreateTimeEvent("xcv_pool_e", "xcv_pool_" .. i .. "_" .. id .. "_a", settings.update_freq, draw_pool, size, id, i, size_i) end local function delayRemoveCurrentPool() CreateTimeEvent("remove_blood_pool_from_pos", npc_id .. vec_to_str(saved_pool[npc_id].pos), ttl, function(p) printf("removing pool %s, %s", npc_id, p) if alreadyLeakedPools[npc_id] then if alreadyLeakedPools[npc_id][p] then printf("found vec to remove %s, %s", npc_id, p) end alreadyLeakedPools[npc_id][p] = nil end return true end, saved_pool[npc_id].pos) end -- if pos changed then refresh size local pos = getBloodPoolPos(npc) if saved_pool[npc_id].moved then saved_pool[npc_id].texture = "blood_pool_texture_" .. math.random(1, blood_textures_amount) if vecSimilar(saved_pool[npc_id].pos, pos, 0.01) then printf("body %s settled", npc_id) saved_pool[npc_id].moved = false saved_pool[npc_id].pos = pos size_i = 1 else printf("body %s still moving", npc_id) size_i = settings.num_of_updates * 0.08 saved_pool[npc_id].pos = pos delayRemoveCurrentPool() -- Uncomment to prevent blood trails from dragging -- saved_pool[npc_id].pos = pos -- saved_pool[npc_id].texture = "blood_pool_texture_" .. math.random(1, blood_textures_amount) -- genNextEvent() -- return true end else if not vecSimilar(saved_pool[npc_id].pos, pos) then printf("body %s is moved", npc_id) saved_pool[npc_id].moved = true delayRemoveCurrentPool() saved_pool[npc_id].pos = pos genNextEvent() return true end end -- draw decal local vec_pos = vector():set( saved_pool[npc_id].pos.x, saved_pool[npc_id].pos.y + 1, saved_pool[npc_id].pos.z ) local vec_dir = vector():set(0, -0.5, 0) size = (size_i / settings.num_of_updates) * saved_pool[npc_id].size_mult * settings.max_pool_size wallmarks_manager():place(vec_dir, vec_pos, 8, size, saved_pool[npc_id].texture, npc, ttl, false) -- Save leaked pool alreadyLeakedPools[npc_id] = alreadyLeakedPools[npc_id] or {} alreadyLeakedPools[npc_id][saved_pool[npc_id].pos] = size genNextEvent() return true end -- i is overall loop counter to draw a pool, size_i is used to determine pool size and can be resetted to 1 if body is moved local i = 1 local size_i = 1 local size = (size_i / settings.num_of_updates) * settings.max_pool_size CreateTimeEvent("xcv_pool_e", "xcv_pool_" .. i .. "_" .. id .. "_a", pool_starts_in, draw_pool, size, id, i, size_i) return true end function on_death_callback(npc) -- give body 3 sec to drop and save pos after CreateTimeEvent("xcv_body_drop_e", "xcv_body_drop_" .. npc:id() .. "_a", 3, start_blood_pool, npc:id()) end function actor_on_before_death() CreateTimeEvent("blood_pool_check_actor", 0, 1, function() if db.actor and not db.actor:alive() then printf("actor died, spawn pool") on_death_callback(db.actor) end return true end) end -- Blood steps local stepCount = {} local flipFlops = {} stepMaxCount = 10 size = 0.17 triggerDistance = 0.7^2 texture = "blood_step_" function checkNearBloodPool(obj) local pos = obj:position() for npc_id, t in pairs(alreadyLeakedPools) do for poolPos, size in pairs(t) do if poolPos:distance_to_sqr(pos) < triggerDistance then stepCount[obj:id()] = stepMaxCount printf("%s near blood pool", obj:name()) return true end end end end function npc_on_update(npc) checkNearBloodPool(npc) end function actor_on_update() checkNearBloodPool(db.actor) end function getHeading(dir) local heading = dir:getH() * 57.2958 -- Fix East -90deg to 270deg if heading < 0 then heading = 360 + heading end -- Clamp heading = clamp(math.floor(heading), 0, 359) -- Count only certain headings: 0, 2, 4, 6... heading = math.floor(heading / 2) * 2 printf("heading %s", heading) return heading end function placeBloodStep(id) local obj = level.object_by_id(id) if not obj then return end if not (stepCount[id] and stepCount[id] > 0) then return end flipFlops[id] = not flipFlops[id] local pos = id == AC_ID and device().cam_pos or utils_obj.safe_bone_pos(obj, flipFlops[id] and "bip01_r_foot" or "bip01_l_foot") local dir = id == AC_ID and device().cam_dir or obj:direction() local heading = getHeading(dir) if id == AC_ID then local v = vector():set(0, 0, 1) dir = vector_rotate_y(v, heading) local distanceBetweenSteps = 0.13 pos = pos:mad(vector_rotate_y(dir, flipFlops[id] and -90 or 90), distanceBetweenSteps) end -- Rotate 90 degrees left to align with textures heading = (heading + 90) % 360 stepCount[id] = stepCount[id] - 1 printf("placing bloodstep for %s", obj:name()) wallmarks_manager():place(vector():set(0, -0.7, 0), pos, 100, size, texture .. (flipFlops[id] and "r_" or "l_") .. heading, obj, settings.pool_lifetime, false) end function on_key_press(key) if enable_debug then if key == DIK_keys.DIK_LSHIFT then stepCount[AC_ID] = 10 placeBloodStep(AC_ID) end end end function actor_on_footstep() placeBloodStep(AC_ID) end function npc_on_foot_step(obj,power,b_play,b_on_ground,b_hud_view) placeBloodStep(obj:id()) end if z_npc_footsteps then -- 1.5.2 check if AddScriptCallback then RegisterScriptCallback("npc_on_foot_step", npc_on_foot_step) end og_npc_on_foot_step = z_npc_footsteps.npc_on_foot_step z_npc_footsteps.npc_on_foot_step = function(binder,obj,power,b_play,b_on_ground,b_hud_view) if og_npc_on_foot_step then og_npc_on_foot_step(binder,obj,power,b_play,b_on_ground,b_hud_view) end npc_on_foot_step(obj,power,b_play,b_on_ground,b_hud_view) end else net_spawn = xr_motivator.motivator_binder.net_spawn xr_motivator.motivator_binder.net_spawn = function(self, se_abstract) if not object_binder.net_spawn(self, se_abstract) then return false end local se_obj = alife_object(self.object:id()) if not (se_obj) then return false end local npc = self.object if npc:alive() then npc:set_callback(callback.on_foot_step, self.npc_on_foot_step, self) end return net_spawn(self, se_abstract) end net_destroy = xr_motivator.motivator_binder.net_destroy xr_motivator.motivator_binder.net_destroy = function(self) self.object:set_callback(callback.on_foot_step,nil) net_destroy(self) end -- 1.5.2 check if AddScriptCallback then AddScriptCallback("npc_on_foot_step") RegisterScriptCallback("npc_on_foot_step", npc_on_foot_step) end function xr_motivator.motivator_binder:npc_on_foot_step(obj,power,b_play,b_on_ground,b_hud_view) -- 1.5.2 check if AddScriptCallback then SendScriptCallback("npc_on_foot_step", obj,power,b_play,b_on_ground,b_hud_view) else npc_on_foot_step(obj,power,b_play,b_on_ground,b_hud_view) end end end function on_game_start() RegisterScriptCallback("actor_on_first_update", load_settings) RegisterScriptCallback("on_option_change", load_settings) RegisterScriptCallback("actor_on_before_death", actor_on_before_death) RegisterScriptCallback("npc_on_death_callback", on_death_callback) RegisterScriptCallback("monster_on_death_callback", on_death_callback) RegisterScriptCallback("actor_on_update", actor_on_update) RegisterScriptCallback("actor_on_footstep", actor_on_footstep) RegisterScriptCallback("npc_on_update", npc_on_update) RegisterScriptCallback("on_key_press", on_key_press) end