--[[ - Created by Tronex - Disguise system - 2018/10/1 - created - 2018/11/20 - fixed a potential bug related to saving dates (user_data) - 2023/5/17 - Edit by Catspaw to fix companions gaining suspicion or not letting you patch - Allows you to disguise by using outfits of other factions to hide your true identity. You can walk among enemies now. - The disguise system is tied to a suspicion-meter, and its calculated by many different factors (10 in total). - These factors are affected by your disguise, behaviour and other small details. - Once NPCs see you, they will examine you and build their own memory and suspicion based on mentioned factors - When the suspicion level of some NPC reach a breaking point, you will get exposed like a little bitch - Factors are controllable in-game, technical values are controllable via "configs\plugins\disguise.ltx" --]] ------------------------------------------------------------ -- Control ------------------------------------------------------------ -- Technical local enable_debug = false local d_ini = ini_file("plugins\\disguise.ltx") local clamp = clamp local ctime_to = utils_data.CTime_to_table local ctime_from = utils_data.CTime_from_table local time_f = 6 local start_record = nil -- don't touch local pos_diff = 0 -- actor position difference every (update_step) seconds local default_comm -- your true faction local is_disguised = false -- boolean: is actor disguised? local highest_suspicion = {value = 0, id = false} -- suspicion meter, stores the highest suspicion level detected among NPCs local update_step = 2000 -- update actor every 2 secs local spike = 0 -- suspicion spike happens when actor do a sudden action -- Controls local usual_memory_limit = d_ini:r_float_ex("diguise_controls","usual_memory_limit") or 10 -- real seconds, memory limit of natural NPCs local enemy_memory_limit = d_ini:r_float_ex("diguise_controls","enemy_memory_limit") or 30 -- real seconds, memory limit of enemy NPCs local memory_multi = d_ini:r_float_ex("diguise_controls","memory_multi") or 10 -- when actor's disguise gets exposed, npcs memory will be multiplied by this number local break_point = d_ini:r_float_ex("diguise_controls","break_point") or 90 -- suspicion limit. When the suspicion level goes above it, you will get exposed local inventory_specs = {} -- inventory parameters ( weight , num of big items , num of all items ), exceeding these limits will increase suspicion inventory_specs[1] = d_ini:r_float_ex("diguise_controls","inventory_limit_weight") or 25 inventory_specs[2] = d_ini:r_float_ex("diguise_controls","inventory_limit_big") or 4 inventory_specs[3] = d_ini:r_float_ex("diguise_controls","inventory_limit_num") or 40 local npc_awareness = { -- by NPC's visual ["v_0"] = d_ini:r_float_ex("npc_awareness","npc_awareness_type_0") or 1, ["v_1"] = d_ini:r_float_ex("npc_awareness","npc_awareness_type_1") or 1, ["v_2"] = d_ini:r_float_ex("npc_awareness","npc_awareness_type_2") or 1, ["v_3"] = d_ini:r_float_ex("npc_awareness","npc_awareness_type_3") or 1.1, ["v_4"] = d_ini:r_float_ex("npc_awareness","npc_awareness_type_4") or 1.2, ["v_5"] = d_ini:r_float_ex("npc_awareness","npc_awareness_type_5") or 1.3, ["special"] = d_ini:r_float_ex("npc_awareness","npc_awareness_special") or 1.4 } local relation_hit = d_ini:r_float_ex("relation_impact","relation_hit") or -75 -- Amount of relation hit by disguise fail (between your faction and npc's faction) local reputation_hit = d_ini:r_float_ex("relation_impact","reputation_hit") or -100 -- Amount of reputation hit by disguise fail local goodwill_hit = d_ini:r_float_ex("relation_impact","goodwill_hit") or -50 -- Amount of goodwill hit by disguise fail (between your faction and npc's faction) -- Memories local disguise_equipment = {} -- stores current equipment local disguise_actor = {} -- stores current examinations of actor's equipment by other NPCs local npcs_memory = {} -- stores last time npcs have saw you ------------------------------------------------------------ -- Control (Options) ------------------------------------------------------------ local is_enabled = true -- To enable/disable the feature local factor = { ["active_item"] = true, ["active_item_factor"] = 1, ["weapon"] = true, ["weapon_factor"] = 1, ["outfit"] = true, ["outfit_factor"] = 1, ["helmet"] = true, ["helmet_factor"] = 1, ["backpack"] = true, ["backpack_factor"] = 1, ["inventory"] = true, ["inventory_factor"] = 1, ["speed"] = true, ["speed_factor"] = 1, ["distance"] = true, ["distance_factor"] = 5, -- [meters] suspicion will raise when the distance between actor and npc goes below this value ["stay_time"] = true, ["stay_time_factor"] = 20, -- [real seconds] suspicion will raise after stay_time exceed this value } function update_feature_state(old_state, new_state) if (not default_comm) or (not db.actor) then return end if old_state and (not new_state) then -- if the feature has been disabled local curr_comm = character_community(db.actor):sub(7) if (curr_comm ~= default_comm) then set_msg("ui_st_disguse_disabled",default_comm) set_comm(default_comm) end elseif (not old_state) and new_state then -- if the feature has been enabled end end function toggle_feature(val) local was_enabled = is_enabled if val and (not is_enabled) then is_enabled = true update_feature_state(was_enabled, is_enabled) elseif (not val) and is_enabled then is_enabled = false update_feature_state(was_enabled, is_enabled) end end function update_settings() local was_enabled = is_enabled is_enabled = ui_options.get("gameplay/disguise/state") update_feature_state(was_enabled, is_enabled) factor.active_item = ui_options.get("gameplay/disguise/active_item") factor.active_item_factor = ui_options.get("gameplay/disguise/active_item_factor") factor.weapon = ui_options.get("gameplay/disguise/weapon") factor.weapon_factor = ui_options.get("gameplay/disguise/weapon_factor") factor.outfit = ui_options.get("gameplay/disguise/outfit") factor.outfit_factor = ui_options.get("gameplay/disguise/outfit_factor") factor.helmet = ui_options.get("gameplay/disguise/helmet") factor.helmet_factor = ui_options.get("gameplay/disguise/helmet_factor") factor.backpack = ui_options.get("gameplay/disguise/backpack") factor.backpack_factor = ui_options.get("gameplay/disguise/backpack_factor") factor.inventory = ui_options.get("gameplay/disguise/inventory") factor.inventory_factor = ui_options.get("gameplay/disguise/inventory_factor") factor.speed = ui_options.get("gameplay/disguise/speed") factor.speed_factor = ui_options.get("gameplay/disguise/speed_factor") factor.distance = ui_options.get("gameplay/disguise/distance") factor.distance_factor = ui_options.get("gameplay/disguise/distance_factor") factor.stay_time = ui_options.get("gameplay/disguise/stay_time") factor.stay_time_factor = ui_options.get("gameplay/disguise/stay_time_factor") end ------------------------------------------------------------ -- Utilities ------------------------------------------------------------ local blacklisted_factions = { ["monolith"] = true, ["greh"] = true, ["isg"] = true, --["renegade"] = true, } local possible_factions = { ["army"] = true, ["bandit"] = true, ["csky"] = true, ["dolg"] = true, ["ecolog"] = true, ["freedom"] = true, ["killer"] = true, ["monolith"] = true, ["stalker"] = true, ["renegade"] = true, ["greh"] = true, ["isg"] = true, } function update_default(comm, now) if not (comm and possible_factions[comm]) then printe("! ERROR update default community | community (%s) doesn't exist", tostring(comm)) callstack() return end default_comm = comm alife_storage_manager.get_state().default_faction = comm if now or (not is_disguised) then db.actor:set_character_community("actor_"..comm, 0, 0) end end function set_comm(comm) if not (comm and possible_factions[comm]) then printe("! ERROR update community | community (%s) doesn't exist", tostring(comm)) callstack() return end --debug_memory() db.actor:set_character_community("actor_" .. comm, 0, 0) is_disguised = default_comm and (comm ~= default_comm) and true or false end function get_default_comm() local def = default_comm or alife_storage_manager.get_state().default_faction or character_community(db.actor):sub(7) or "stalker" return def end function is_actor_disguised() return is_disguised end local tg_susp1 = 2000 -- [ms] timer for suspicion cooldown when undisguised local tg_susp1_step = 20000 local tg_susp2 = 0 -- [ms] timer for suspicion cooldown when npc not seeing you local tg_susp2_step = 2000 local susp_cool = 5 -- suspicion cool-down step. visuals only function moniter_highest_suspicion(tg) tg = tg or time_global() -- if not disguised, reduce slowly if (not is_disguised) and (tg > tg_susp1) then tg_susp1 = tg + tg_susp1_step highest_suspicion.value = highest_suspicion.value - susp_cool elseif highest_suspicion.id and (tg > tg_susp2) then tg_susp2 = tg + tg_susp2_step local id = highest_suspicion.id local npc = db.storage[id] and db.storage[id].object or level.object_by_id(id) if npc and IsStalker(npc) and npc:alive() then -- if npc doesn't see the player, reduce if (not npc:see( db.actor )) then highest_suspicion.value = highest_suspicion.value - susp_cool end else -- in case NPC doesn't exist, not stalker or dead highest_suspicion.id = false highest_suspicion.value = 0 end end -- Clear aware NPC if no suspicion happens highest_suspicion.value = clamp(highest_suspicion.value, 0, 100) if (highest_suspicion.value == 0) then highest_suspicion.id = false end if enable_debug then printf("% Disguise highest_suspicion = %s", highest_suspicion.value) end end local tg_memo = 60000 -- update relation list every 60 sec local tg_memo_step = 60000 -- when disguise is not active, do a cool down every 20 sec function moniter_memories(tg) tg = tg or timer_global() if (tg < tg_memo) then return end tg_memo = tg + tg_memo_step local curr_time = game.get_game_time() local time_passed, hs_clean for k,v in pairs(npcs_memory) do if v and v.last_seen then local last_seen = ctime_from(v.last_seen) time_passed = curr_time:diffSec(last_seen) if (time_passed > v.memo) then npcs_memory[k] = nil if (k == highest_suspicion.id) then highest_suspicion.id = false highest_suspicion.value = 0 end end else npcs_memory[k] = nil if (k == highest_suspicion.id) then highest_suspicion.id = false highest_suspicion.value = 0 end end end end -- ========================================================================= function is_companion_squad_member(npc) --[[ Added by Catspaw for Companion Disguise and Patch Fix To merge into any other mod's gameplay_disguise.script: 1. Copy this function, including comment block, into your copy of the script 2. In the anybody_see() function below, find this line: if (npc and IsStalker(npc,npc:clsid()) and npc:alive()) then Change to: if (npc and IsStalker(npc,npc:clsid()) and npc:alive()) and not is_companion_squad_member(npc) then 3. Then, a little further down, in the npc_on_update() function: if npc:see(db.actor) then Change to: if npc:see(db.actor) and not is_companion_squad_member(npc) then That's it--you're done. --]] local npc_squad = get_object_squad(npc) return npc_squad and axr_companions.companion_squads[npc_squad.id] ~= nil end -- ========================================================================= function anybody_see(t) local actor = db.actor for i=1, #db.OnlineStalkers do local st = db.storage[db.OnlineStalkers[i]] local npc = st and st.object or level.object_by_id(db.OnlineStalkers[i]) if (npc and IsStalker(npc,npc:clsid()) and npc:alive()) and not is_companion_squad_member(npc) then if t then if t[npc:community()] and (npc:see(actor)) then return true end elseif (npc:see(actor)) then return true end end end return false end function anybody_remember() local time_passed for k,v in pairs(npcs_memory) do if v and v.last_seen then local last_seen = ctime_from(v.last_seen) if last_seen then time_passed = game.get_game_time():diffSec(last_seen) if (time_passed < v.memo) then return true else npcs_memory[k] = nil end end else npcs_memory[k] = nil end end return false end local disguise_zones = { -- don't measure suspicion when player is inside these zones "bar_arena_surge_hide_a1", "bar_arena_ug_surge_hide_a1", } function inside_disguise_zone() --if has_alife_info("bar_arena_fight") then --return true --end for i=1,#disguise_zones do if (db.actor_inside_zones[disguise_zones[i]]) then return true end end return false end function set_msg(str, comm, comm2) local txt = strformat( game.translate_string(str) , comm and game.translate_string(comm) or "" , comm2 and game.translate_string(comm2) or "" ) actor_menu.set_msg(1, txt,10) end function get_outfit_comm(section) local new_comm = default_comm local outfit_comm = ini_sys:r_string_ex(section,"community") if outfit_comm and (outfit_comm ~= "") then --and possible_factions[outfit_comm] then new_comm = outfit_comm end return new_comm end function get_patch(faction, only_sec) local patch = faction .. "_patch" if ini_sys:section_exist(patch) then return only_sec and patch or db.actor:object(patch) end return end function clear_patch(id) se_save_var(id, nil, "unpatched", nil) end function delet_memory() empty_table(disguise_actor) empty_table(npcs_memory) end function time_to_str(tim) local str = tostring(tim.h) .. ":" .. tostring(tim.m) .. ":" .. tostring(tim.s) return str end local last_min_scan function increment_disguise_statistic(curr_time) last_min_scan = last_min_scan or curr_time if (curr_time:diffSec(last_min_scan) > 60) then game_statistics.increment_statistic("minutes_disguised") last_min_scan = curr_time end end function debug_disguise(npc, m, t) local id = npc:id() printf("% Disguise | npc seeing you | id: %s - comm: %s - name: %s", id, character_community(npc), npc:character_name()) printf("% Disguise | [%s] memory | awareness: %s - memory: %s - first_seen: %s - last_seen: %s", id, m.awareness, m.memo, time_to_str(m.first_seen), time_to_str(m.last_seen)) printf("% Disguise | [%s] on equipment | active item: %s - weapon: %s - outfit: %s - helmet: %s - backpack: %s - inv: %s ", id, t.active_item, t.weapon, t.outfit, t.helmet, t.backpack, t.inventory) printf("% Disguise | [%s] on behaviour | speed: %s - distance: %s - stay_time: %s", id, t.speed, t.distance, t.stay_time) printf("% Disguise | [%s] suspicion | spike: %s - result: %s", id, spike, m.suspicion) printf("--==============================================================") end function debug_memory() local time_passed for k,v in pairs(npcs_memory) do if v then local last_seen = ctime_from(v.last_seen) if last_seen then time_passed = game.get_game_time():diffSec(last_seen) if (time_passed < v.memo) then printf("% Disguise debug_memoery | [%s] still remembers you (%s sec left)",k, (v.memo - time_passed)) else printf("% Disguise debug_memoery | [%s] forgot you",k) end end end end end ------------------------------------------------------------ -- NPC Logic ------------------------------------------------------------ local need_inv_scan = true local update_step_seconds = update_step/1000 local function npc_on_update(npc) if (not is_enabled) or (not default_comm) or (not db.actor) then return end local id = npc:id() --printf("-NPC Update | id: %s - comm: %s - name: %s", id, comm, npc:character_name()) if npc:see(db.actor) and not is_companion_squad_member(npc) then -- Ignore in Arena fight if inside_disguise_zone() then return end local comm = character_community(npc) local curr_comm = character_community(db.actor):sub(7) local curr_time = game.get_game_time() local moment = ctime_to(curr_time) -- time table local pos = npc:position() local sec = npc:section() -- Set up NPC's memory if (not npcs_memory[id]) then npcs_memory[id] = {} npcs_memory[id].is_enem = game_relations.is_factions_enemies(default_comm,comm) --or (npc:relation(db.actor) < game_object.enemy) npcs_memory[id].memo = npcs_memory[id].is_enem and (enemy_memory_limit * time_f) or (usual_memory_limit * time_f) npcs_memory[id].memo = math.floor(npcs_memory[id].memo * (math.random(80,120) / 100)) npcs_memory[id].awareness = get_npc_awareness(sec) end npcs_memory[id].first_seen = npcs_memory[id].first_seen or moment npcs_memory[id].last_seen = moment if is_disguised and (not game_relations.is_factions_enemies(curr_comm,comm)) then -- Set up and calculate NPC's suspicion local thoughts_on_actor = {} thoughts_on_actor.active_item = examinate_active_item() -- 0~100 thoughts_on_actor.weapon = examinate_weapon() -- 0~100 thoughts_on_actor.outfit = examinate_outfit() -- 0~100 thoughts_on_actor.helmet = examinate_helmet() -- 0~100 thoughts_on_actor.backpack = examinate_backpack() -- 0~100 thoughts_on_actor.inventory = examinate_inventory() -- 0~100 thoughts_on_actor.speed = examinate_speed() -- meters/sec thoughts_on_actor.distance = examinate_distance(pos) -- meters thoughts_on_actor.stay_time = examinate_stay_time(npcs_memory[id].first_seen, npcs_memory[id].last_seen) -- 0~1 npcs_memory[id].suspicion = calculate_npc_suspicion(npc, id, thoughts_on_actor, npcs_memory[id].awareness) -- if NPC's suspicion is bigger than highest suspicion, tag him if (npcs_memory[id].suspicion > highest_suspicion.value) then highest_suspicion.value = npcs_memory[id].suspicion highest_suspicion.id = id -- if tagged NPC has less suspicion now, reduce the highest suspicion elseif (id == highest_suspicion.id) and (npcs_memory[id].suspicion < highest_suspicion.value) then highest_suspicion.value = highest_suspicion.value - susp_cool end -- if the highest suspicion has reach the limit, expose the actor if (highest_suspicion.value > break_point) then expose_actor(npc, comm, npcs_memory[id].is_enem) end -- Statistic increment_disguise_statistic(curr_time) if enable_debug then debug_disguise(npc,npcs_memory[id],thoughts_on_actor) end end -- NPC will forget you after enough time elseif npcs_memory[id] then npcs_memory[id].first_seen = nil local last_seen = ctime_from(npcs_memory[id].last_seen) local time_passes = game.get_game_time():diffSec(last_seen) if (time_passes > npcs_memory[id].memo) then npcs_memory[id] = nil end end end local function npc_on_death_callback(npc) -- delete memory on npc death local id = npc:id() if id and npcs_memory[id] then npcs_memory[id] = nil end end function get_npc_awareness(sec) sec = sec or "_0" local awareness = 1 if string.find(sec,"_0") then awareness = npc_awareness["v_0"] elseif string.find(sec,"_1") then awareness = npc_awareness["v_1"] elseif string.find(sec,"_2") then awareness = npc_awareness["v_2"] elseif string.find(sec,"_3") then awareness = npc_awareness["v_3"] elseif string.find(sec,"_4") then awareness = npc_awareness["v_4"] elseif string.find(sec,"_5") then awareness = npc_awareness["v_5"] end return awareness end function examinate_active_item() -- { id , condition , factor } if (not factor.active_item) then return 0 end local active_item = db.actor:active_item() local section = active_item and active_item:section() or false if (not active_item) then disguise_actor["active_item"] = {} return 100 end if (not disguise_actor["active_item"]) then disguise_actor["active_item"] = {} end -- no need to check again if nothing changed if disguise_actor["active_item"].id and (disguise_actor["active_item"].id == active_item:id()) and (disguise_actor["active_item"].condition == active_item:condition()) then return disguise_actor["active_item"].factor end -- Calculate factor disguise_actor["active_item"].condition = active_item:condition() local factor = 100 if active_item then if IsWeapon(active_item) then factor = 80 * disguise_actor["active_item"].condition elseif string.find(section,"detector") then factor = 60 elseif IsGrenade(active_item) then factor = 30 end end factor = math.floor(clamp(factor,20,99)) disguise_actor["active_item"].factor = factor return factor end function examinate_weapon() -- { id , condition , factor} if (not factor.weapon) then return 0 end local weapon = db.actor:item_in_slot(3) or db.actor:item_in_slot(2) local section = weapon and weapon:section() or false if (not weapon) then disguise_actor["weapon"] = {} return 80 -- carrying weapon is not necessary end if (not disguise_actor["weapon"]) then disguise_actor["weapon"] = {} end -- no need to check again if nothing changed if disguise_actor["weapon"].id and (disguise_actor["weapon"].id == weapon:id()) and (disguise_actor["weapon"].condition == weapon:condition()) then return disguise_actor["weapon"].factor end -- Calculate factor disguise_actor["weapon"].condition = weapon:condition() local factor = disguise_actor["weapon"].condition * 100 factor = math.floor(clamp(factor,50,99)) disguise_actor["weapon"].factor = factor return factor end function examinate_outfit() -- { id , condition , pre-helmet , factor} if (not factor.outfit) then return 0 end local outfit = db.actor:item_in_slot(7) local section = outfit and outfit:section() or false if (not outfit) then return 1 end if (not disguise_actor["outfit"]) then disguise_actor["outfit"] = {} end -- no need to check again if nothing changed if disguise_actor["outfit"].id and (disguise_actor["outfit"].id == outfit:id()) and (disguise_actor["outfit"].condition == outfit:condition()) then return disguise_actor["outfit"].factor end -- Calculate factor disguise_actor["outfit"].pre_helmet = (ini_sys:r_bool_ex(section, "helmet_avaliable") == false) disguise_actor["outfit"].condition = outfit:condition() local factor = disguise_actor["outfit"].condition * 100 factor = math.floor(clamp(factor,1,99)) disguise_actor["outfit"].factor = factor return factor end function examinate_helmet() -- { id , condition , factor} if (not factor.helmet) then return 0 end local helmet = db.actor:item_in_slot(12) local section = helmet and helmet:section() or false if disguise_actor["outfit"] and disguise_actor["outfit"].pre_helmet then return disguise_actor["outfit"].factor end if (not helmet) then disguise_actor["helmet"] = {} return 1 end if (not disguise_actor["helmet"]) then disguise_actor["helmet"] = {} end -- no need to check again if nothing changed if disguise_actor["helmet"].id and (disguise_actor["helmet"].id == helmet:id()) and (disguise_actor["helmet"].condition == helmet:condition()) then return disguise_actor["helmet"].factor end -- Calculate factor disguise_actor["helmet"].condition = helmet:condition() local factor = disguise_actor["helmet"].condition * 100 factor = math.floor(clamp(factor,1,99)) disguise_actor["helmet"].factor = factor return factor end function examinate_backpack() -- { id , condition , factor} if (not factor.backpack) then return 0 end local backpack = db.actor:item_in_slot(13) local section = backpack and backpack:section() or false if (not backpack) then disguise_actor["backpack"] = {} return 60 -- backpack is not necessary end if (not disguise_actor["backpack"]) then disguise_actor["backpack"] = {} end -- no need to check again if nothing changed if disguise_actor["backpack"].id and (disguise_actor["backpack"].id == backpack:id()) and (disguise_actor["backpack"].condition == backpack:condition()) then return disguise_actor["backpack"].factor end need_inv_scan = true -- inventory need a rescan after backpack changes -- Calculate factor disguise_actor["backpack"].condition = backpack:condition() local con = disguise_actor["backpack"].condition * 100 local carry_weight = ini_sys:r_float_ex(section, "additional_inventory_weight") local cw_f = carry_weight and carry_weight > 30 and 30 or 1 -- big backpacks are more suspicious local factor = con - cw_f factor = math.floor(clamp(factor,50,99)) disguise_actor["backpack"].factor = factor return factor end function examinate_inventory() -- { num , num_big, weight , factor } if (not factor.inventory) then return 0 end if (not disguise_actor["inventory"]) then disguise_actor["inventory"] = {} end -- No need to check again if nothing changed if (not need_inv_scan) and (disguise_actor["inventory"].num) and (disguise_actor["inventory"].num_big) and (disguise_actor["inventory"].weight) then return disguise_actor["inventory"].factor end -- Collect total inv weight and items number local total_weight = 0 local total_num = 0 local total_num_big = 0 local function iterate(temp, obj) local sec = obj:section() local weight = ini_sys:r_float_ex(sec,"weight") or 0 local grid_width = ini_sys:r_float_ex(sec,"inv_grid_width") or 1 local grid_height = ini_sys:r_float_ex(sec,"inv_grid_height") or 1 total_num = total_num + 1 total_weight = total_weight + weight total_num_big = (grid_width >= 2) and (grid_height >=2) and (total_num_big + 1) or total_num_big end db.actor:iterate_inventory(iterate,nil) need_inv_scan = false disguise_actor["inventory"].weight = total_weight disguise_actor["inventory"].num = total_num disguise_actor["inventory"].num_big = total_num_big local factor = 100 local is_messy = false if (total_weight > inventory_specs[1]) then factor = factor - (total_weight - inventory_specs[1]) is_messy = true end if (total_num_big > inventory_specs[2]) then factor = factor - (total_num_big - inventory_specs[2]) * 5 is_messy = true end if (total_num > inventory_specs[3]) then factor = factor - (total_num - inventory_specs[3]) is_messy = true end if is_messy and (not (disguise_actor["backpack"] and disguise_actor["backpack"].id)) then -- no backpack factor = factor - 50 end factor = math.floor(clamp(factor,30,99)) disguise_actor["inventory"].factor = factor return factor end function examinate_speed() if (not factor.speed) then return 1 end local speed = (pos_diff/update_step_seconds) -- [meter/second] speed = (speed > 1) and (speed < 10) and math.floor(speed) or 1 local result = (9 + speed) / 10 return result * factor.speed_factor end function examinate_distance(pos) if (not factor.distance) or (not pos) then return 1 end local distance = db.actor:position():distance_to(pos) -- meters local factor = factor.distance_factor distance = distance > 1 and distance or 1 -- make sure its not small fraction local result = (distance < factor) and factor/distance or 1 return result end function examinate_stay_time(first_seen, last_seen) if (not factor.stay_time) then return 1 end first_seen = ctime_from(first_seen) last_seen = ctime_from(last_seen) local stay_time = last_seen:diffSec(first_seen) local factor = factor.stay_time_factor stay_time = stay_time / time_f -- real time local result = (stay_time > factor) and (stay_time / factor) or 1 return result end function calculate_npc_suspicion(npc, id, t, awareness) local be = npc:best_enemy() local fraction = (factor.active_item and 1 or 0) + (factor.weapon and 1 or 0) + (factor.outfit and 1 or 0) + (factor.helmet and 1 or 0) + (factor.backpack and 1 or 0) + (factor.inventory and 1 or 0) local item_factor if fraction > 0 then itm_factor = ( (t.active_item * factor.active_item_factor) + (t.weapon * factor.weapon_factor) + (t.outfit * factor.outfit_factor) + (t.helmet * factor.helmet_factor) + (t.backpack * factor.backpack_factor) + (t.inventory * factor.inventory_factor) ) / fraction itm_factor = 100 - itm_factor else -- everything is disabled itm_factor = 1 end -- Formula: ( npc's awareness x player's behaviour x player's equipment ) + player's sudden action - npc's current danger local suspicion = (awareness * ( t.stay_time * t.distance * t.speed ) * itm_factor) + ((game_achievements.has_achievement("unforeseen_guest")) and clamp(spike-15,0,spike) or spike) - (be and (be:id() ~= AC_ID) and 50 or 0) if enable_debug then printf("% Disguise calc | fraction [%s] = active_item [%s] + weapon [%s] + outfit [%s] + helmet [%s] + backpack [%s] + inventory [%s]", fraction, (factor.active_item and 1 or 0), (factor.weapon and 1 or 0), (factor.outfit and 1 or 0), (factor.helmet and 1 or 0), (factor.backpack and 1 or 0), (factor.inventory and 1 or 0)) printf("% Disguise calc | itm_factor [%s] = active_item [%s]x[%s] + weapon [%s]x[%s] + outfit [%s]x[%s] + helmet [%s]x[%s] + backpack [%s]x[%s] + inventory [%s]x[%s] / fraction [%s]", itm_factor, t.active_item, factor.active_item_factor, t.weapon, factor.weapon_factor, t.outfit, factor.outfit_factor, t.helmet, factor.helmet_factor, t.backpack, factor.backpack_factor, t.inventory, factor.inventory_factor, fraction) printf("% Disguise calc | suspicion [%s] = (awareness [%s] x ( t.stay_time [%s] x t.distance [%s] x t.speed [%s]) x itm_factor [%s]) + spike [%s] - best enemy [%s]", suspicion, awareness , t.stay_time , t.distance , t.speed ,itm_factor,((game_achievements.has_achievement("unforeseen_guest")) and clamp(spike-15,0,spike) or spike), (be and (be:id() ~= AC_ID) and 50 or 0)) printf("-------------------------------------------------------------") end return math.floor(clamp(suspicion,1,100)) end ------------------------------------------------------------ -- Callbacks ------------------------------------------------------------ function try_to_disguise(new_comm, naked, no_patch) local curr_comm = character_community(db.actor):sub(7) --printf("/ Disguise | current comm: %s - new comm: %s", curr_comm, new_comm) if (new_comm == curr_comm) then return end if no_patch then set_msg("ui_st_disguse_patch_torn", default_comm) set_comm(default_comm) elseif (new_comm == default_comm) then set_msg("ui_st_disguse_back_to_default", default_comm) set_comm(default_comm) elseif blacklisted_factions[new_comm] and IsStoryMode() then set_msg("ui_st_disguse_blacklisted_faction", new_comm, curr_comm) elseif anybody_see() then set_msg("ui_st_disguse_someone_saw", curr_comm) elseif anybody_remember() then set_msg("ui_st_disguse_someone_remember", curr_comm) else if naked then set_msg("ui_st_disguse_naked", default_comm) set_comm(default_comm) elseif start_record then set_msg("ui_st_disguse_on", new_comm) set_comm(new_comm) end end end function expose_actor(npc, comm, is_enem) set_msg("st_disguse_actor_exposed",default_comm) set_comm(default_comm) -- News if npc and (not inside_disguise_zone()) then local str = "" if is_enem then str = "st_news_disguse_enemy_expose_" else str = "st_news_disguse_natural_expose_" if (comm ~= default_comm) then if (ui_options.get("alife/general/dynamic_relations") == true) and (not game_relations.is_faction_unaffected(comm)) and (not game_relations.is_faction_unaffected(default_comm)) and (not game_relations.is_faction_pair_unaffected(comm, default_comm)) then game_relations.change_faction_relations(comm, default_comm, relation_hit) end game_relations.change_factions_community_num(comm, default_comm, goodwill_hit) game_statistics.increment_reputation(reputation_hit) end end local msg = game.translate_string(str .. tostring(math.random(1,4))) local comm_name = ", " .. game.translate_string("st_dyn_news_comm_" .. comm .. "_6") local se = strformat("%s%s", npc:character_name(), string.find(comm_name,"_6") and comm_name or "") dynamic_news_helper.send_tip(msg,se,2,nil,npc:character_icon(),nil,"npc") end -- long-term memory for k,v in pairs(npcs_memory) do if v then v.memo = v.memo * memory_multi end end is_disguised = nil end local last_update = 2000 local old_pos local function actor_on_first_update() -- Set up basic things default_comm = alife_storage_manager.get_state().default_faction or character_community(db.actor):sub(7) or "stalker" printdbg("- Default community: %s",default_comm) time_f = level.get_time_factor() -- Control feature by its options update_settings() end local function actor_on_update() -- Basic conditions if (not is_enabled) or (not default_comm) or (not db.actor) then return end -- Less hooks local tg = time_global() if (tg < last_update) then return end last_update = tg + update_step -- 2 sec -- Update suspicion moniter_highest_suspicion(tg) -- Update NPCs memories moniter_memories(tg) -- Reset suspicion spike spike = 0 -- Update disguise bar hud_update() -- not in arena fight if (not inside_disguise_zone()) then -- Speed calculation local curr_pos = db.actor:position() pos_diff = curr_pos:distance_to(old_pos or curr_pos) old_pos = curr_pos --printf("-Actor Speed = " .. tostring(pos_diff/update_step_seconds)) -- Scan equipment for changes for i=1,13 do if (i ~= 10) and (i ~= 11) then local item = db.actor:item_in_slot(i) local id = item and item:id() or 0 if id and id ~= disguise_equipment[i] then spike = 15 disguise_equipment[i] = id if (i == 7) then local comm local state = se_load_var(id, nil, "unpatched") if (id == AC_ID) or state then comm = default_comm else comm = get_outfit_comm(item:section()) end try_to_disguise( comm , (id == AC_ID) , state ) end end end end end end local function actor_on_item_take(obj) need_inv_scan = true end local function actor_on_item_drop(obj) need_inv_scan = true spike = 15 end local function actor_on_weapon_zoom_in(obj) spike = 30 end local function on_before_level_changing() highest_suspicion = {value = 0, id = false} printdbg("~ gameplay_disguise | cleaned suspicion") end local function on_key_press(key) start_record = true UnregisterScriptCallback("on_key_press",on_key_press) end local function save_state(m_data) --m_data.disguise_default_comm = default_comm m_data.disguise_state = is_disguised m_data.disguise_highest_suspicion = highest_suspicion m_data.disguise_npcs_memory = npcs_memory end local function load_state(m_data) --default_comm = m_data.disguise_default_comm is_disguised = m_data.disguise_state highest_suspicion = m_data.disguise_highest_suspicion or highest_suspicion npcs_memory = m_data.disguise_npcs_memory or {} end function on_game_start() RegisterScriptCallback("save_state",save_state) RegisterScriptCallback("load_state",load_state) RegisterScriptCallback("actor_on_first_update",actor_on_first_update) RegisterScriptCallback("actor_on_update",actor_on_update) RegisterScriptCallback("npc_on_update",npc_on_update) RegisterScriptCallback("npc_on_death_callback", npc_on_death_callback) RegisterScriptCallback("on_before_level_changing",on_before_level_changing) RegisterScriptCallback("actor_on_item_take",actor_on_item_take) RegisterScriptCallback("actor_on_item_drop",actor_on_item_drop) RegisterScriptCallback("actor_on_weapon_zoom_in",actor_on_weapon_zoom_in) RegisterScriptCallback("on_key_press",on_key_press) RegisterScriptCallback("on_option_change",update_settings) end ------------------------------------------------------------ -- Item Menu ------------------------------------------------------------ function menu_patch(obj) -- display option for tear patch local p = obj:parent() if not (p and p:id() == AC_ID) then return end local section = obj:section() local comm = ini_sys:r_string_ex(section,"community") if comm and (comm ~= "") and possible_factions[comm] then local id = obj:id() local obj_patch = get_patch(comm) -- if patch is torn from this outfit, and there's an available patch in inventory local state = se_load_var(id, obj:name(), "unpatched") if state and obj_patch then local str = game.translate_string("st_item_attach_patch") local str_patch = ui_item.get_sec_name(get_patch(comm,true)) return strformat(str, str_patch) -- if patch is not torn from this outfit elseif (state == nil) then local str = game.translate_string("st_item_tear_patch") local str_patch = ui_item.get_sec_name(get_patch(comm,true)) return strformat(str, str_patch) end end return end function menu_patch_action(obj) local section = obj:section() local comm = ini_sys:r_string_ex(section,"community") if comm and (comm ~= "") and possible_factions[comm] then local id = obj:id() local obj_patch = get_patch(comm) -- if patch is torn from this outfit, and there's an available patch in inventory local state = se_load_var(id, obj:name(), "unpatched") if state and obj_patch then se_save_var( id, obj:name(), "unpatched", nil ) alife_release(obj_patch) actor_effects.play_item_fx("disguise_tear_patch") local str = game.translate_string("ui_st_disguse_patch_attach_now") local str_patch = ui_item.get_sec_name(get_patch(comm,true)) disguise_equipment[7] = 0 --actor_menu.set_msg(1, strformat(str,str_patch), 10 ) if enable_debug then printf("/ Disguise | attached patch to %s", section) end -- if patch is not torn from this outfit elseif (state == nil) then se_save_var( id, obj:name(), "unpatched", true ) actor_effects.play_item_fx("disguise_tear_patch") local str = game.translate_string("ui_st_disguse_patch_torn_now") local str_patch = ui_item.get_sec_name(get_patch(comm,true)) disguise_equipment[7] = 0 --actor_menu.set_msg(1, strformat(str,str_patch), 10 ) if enable_debug then printf("/ Disguise | torn patch of %s", section) end end end end ------------------------------------------------------------ -- HUD ------------------------------------------------------------ local h_tmr = 0 local h_a = "=" -- bar shape (character) local h_bar = 15 -- total bars local h_val = 1 -- suspicion-meter local h_step = 10 -- step size for suspicion changes local h_time = 500 -- update step [ms] function hud_update() local hud = get_hud() local hud_d = hud:GetCustomStatic("bar_disguise") local wnd if (not is_disguised) or (db.actor:is_talking()) then if (hud_d ~= nil) then hud:RemoveCustomStatic("bar_disguise") hud_d = nil end return end if (hud_d == nil) then hud:AddCustomStatic("bar_disguise",true) hud_d = hud:GetCustomStatic("bar_disguise") wnd = hud_d:wnd() if (wnd ~= nil) then wnd:SetAutoDelete(true) end end -- Update time local tg = time_global() if h_tmr >= tg then return else h_tmr = tg + h_time end if (hud_d ~= nil) then wnd = hud_d:wnd() -- Sliding bar local val = highest_suspicion.value if (val > h_val) and (val - h_val > h_step) then h_val = h_val + h_step elseif (val < h_val) and (h_val - val > h_step) then h_val = h_val - h_step end h_val = clamp(h_val,0,100) -- Color local str = hud_val_to_str(h_val) local green = math.floor(255 * ((100 - h_val)/100)) local red = math.floor(255 * (h_val/100)) wnd:TextControl():SetTextST(str) wnd:TextControl():SetTextColor(GetARGB(255, red, green, 50)) end end function hud_val_to_str(val) local str = "" local bars = (val > (h_step/2)) and math.floor((val/100)*h_bar) or 0 for i=1,bars do str = str .. h_a end return str end