Divergent/mods/Companion Disguise Patch Fix/gamedata/scripts/gameplay_disguise.script

1165 lines
39 KiB
Plaintext

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