-- =================================================================================== --[[ Gameplay Addon: NPC Friendly Fire / War Response and Actor Surrender -- =================================================================================== Author: dEmergence (dEmergence @ ModDB) Version: 0.3.0 Updated: 20241207 Standalone version: https://www.moddb.com/mods/stalker-anomaly/addons/npc-friendly-fire-response-and-actor-surrender Originally created by dEmergence for 'Dynamic Faction Relations Customizer' https://www.moddb.com/mods/stalker-anomaly/addons/dynamic-faction-relations-customizer-dfrc-v10 License: Feel free to reuse any of my code. Just add credit where credit is due. -- ================================================================================--]] ------------------------------------------------------------ -- Global Settings ------------------------------------------------------------ local dfrc = dynamic_faction_relations_customizer local dfrc_has_feature = { ["affs"] = false } if dfrc then dfrc_has_feature["affs"] = dfrc.features and dfrc.features["affs"] or false end settings = {} settings["enable_debug_log"] = false settings["faction_dissatisfaction_limit"] = 100 --settings["enable_goodwill_reset_on_friendly_fire"] = true settings["enable_goodwill_regeneration"] = true settings["min_goodwill_regainable_after_reset"] = 0 settings["max_goodwill_regainable_after_reset"] = 0.5 settings["max_goodwill_regainable_after_friendly_fire"] = 0.5 -- manual settings: settings["disable_hud_bar"] = false factions_table = game_relations.factions_table is_factions_enemies = game_relations.is_factions_enemies valid_factions = {} for i = 1, #factions_table do valid_factions[factions_table[i]] = true end ------------------------------------------------------------ -- Initialization ------------------------------------------------------------ dfrc_tgr = {} dfrc_tgr.npc_map = {} dfrc_tgr.last_hit_time = 0 -- Area Scanning Parameters WITNESS_DISTANCE = 1600 GOSSIP_DISTANCE = 4900 MAX_OBSERVER_DISTANCE = 10000 local AREA_STATS = {} local min_scan_time_age = 3000 local max_scan_time_age = 300000 -- NPC Updates happen roughly every 1000 ms that means this value is applied once per second! local base_grudge_value_mod = 0.5 local aggravation_factor = 1 local calming_factor = 0.25 -- Timing Parameters local min_hit_time_delay = 600000 local min_regen_time_delay = 450000 -- General Update Rate local tg_update = 0 local tg_update_step = 996 --[ms] set to be a little off to avoid stacking up the same timeslot as other functions -- Regeneration Update Rate local tg_update_tgr = 0 local tg_update_step_tgr = tg_update_step * 60 * 4 --* every 4 minutes ( = 24 min in game ) -- News Message Handler Parameters local msg_duration = 10 local msg_cooldown_min_time = 7000 local msg_cooldown_time = {} local spammer_tracker = { ["WasHit"] = 0, ["SeenHit"] = -1, ["SeenActorHitWeapon"] = 0, ["SeenActorWeapon"] = 0, ["SeenActorInitSurrender"] = 0, ["AcknowledgedActorSurrender"] = 0, ["SeenActorUnSurrender"] = 0, ["ReportingAttack"] = 0 } local spammer_tracker_max = { ["WasHit"] = 3, ["SeenHit"] = 3, ["SeenActorHitWeapon"] = 3, ["SeenActorWeapon"] = 3, ["SeenActorInitSurrender"] = 3, ["AcknowledgedActorSurrender"] = 3, ["SeenActorUnSurrender"] = 3, ["ReportingAttack"] = 3 } -- HUD local show_hud_bar = false -- Pause Aggravation on active GUIs (e.g. inventory open) local pause_aggravation_evaluation = false local pause_aggravation_evaluation_arena = false function setup_tgr_cache_comm(faction) dfrc_tgr[faction] = {} dfrc_tgr[faction].npcs = {} dfrc_tgr[faction].hit_v = 0 dfrc_tgr[faction].last_hit_time = 0 dfrc_tgr[faction].regen_enabled = not is_factions_enemies(faction,string.gsub( get_object_community(db.actor), "actor_", "" )) dfrc_tgr[faction].regen_active = false dfrc_tgr[faction].regen_rate = 0 dfrc_tgr[faction].regen_cycle = 0 dfrc_tgr[faction].regen_peak = 0.02 -- default regen_peak dfrc_tgr[faction].last_regen_time = 0 dfrc_tgr[faction].last_regen_time_hr = 0 dfrc_tgr[faction].last_reset_time = 0 dfrc_tgr[faction].surrender_enabled = not is_factions_enemies(faction,string.gsub( get_object_community(db.actor), "actor_", "" )) end function setup_tgr_cache_npc(faction,id) dfrc_tgr[faction].npcs[id] = {} dfrc_tgr[faction].npcs[id].last_hit_time = 0 end ------------------------------------------------------------ -- Utility ------------------------------------------------------------ -- Debug Output function d_printf(debugtxt) if settings["enable_debug_log"] then printf(tostring( debugtxt )) end end local function load_settings() if dfrc then d_printf("DFRC.TGR: Loading settings from DFRC") if dynamic_faction_relations_customizer.settings then --settings["enable_debug_log"] = dynamic_faction_relations_customizer.settings["enable_debug_log"] or false --d_printf("* settings[enable_debug_log] == "..tostring(settings["enable_debug_log"])) settings["faction_dissatisfaction_limit"] = dynamic_faction_relations_customizer.settings["faction_dissatisfaction_limit"] or 100 d_printf("* settings[faction_dissatisfaction_limit] == "..tostring(settings["faction_dissatisfaction_limit"])) -- if dynamic_faction_relations_customizer.settings["enable_goodwill_reset_on_friendly_fire"] ~= nil then -- settings["enable_goodwill_reset_on_friendly_fire"] = dynamic_faction_relations_customizer.settings["enable_goodwill_reset_on_friendly_fire"] -- end settings["min_goodwill_regainable_after_reset"] = dynamic_faction_relations_customizer.settings["min_goodwill_regainable_after_reset"] or dynamic_faction_relations_customizer.settings["min_keep_goodwill_percent_on_reset"] or 0 d_printf("* settings[min_goodwill_regainable_after_reset] == "..tostring(settings["min_goodwill_regainable_after_reset"])) settings["max_goodwill_regainable_after_reset"] = dynamic_faction_relations_customizer.settings["max_goodwill_regainable_after_reset"] or dynamic_faction_relations_customizer.settings["max_keep_goodwill_percent_on_reset"] or 0.5 d_printf("* settings[max_goodwill_regainable_after_reset] == "..tostring(settings["max_goodwill_regainable_after_reset"])) settings["max_goodwill_regainable_after_friendly_fire"] = dynamic_faction_relations_customizer.settings["max_goodwill_regainable_after_friendly_fire"] or 0.5 d_printf("* settings[max_goodwill_regainable_after_friendly_fire] == "..tostring(settings["max_goodwill_regainable_after_friendly_fire"])) else d_printf("! Unable to retrieve settings. This addon requires at least Dynamic Faction Relations Customizer version 2.2.4 to apply custom settings.") end end end function get_object_levelname(obj) if not (obj) then return "nil" end local gvid = obj.m_game_vertex_id or obj.game_vertex_id and obj:game_vertex_id() if not (gvid) then return "nil" end local gg = game_graph() if (gg:valid_vertex_id(gvid)) then cvertex = gg:vertex(gvid) return alife():level_name(cvertex:level_id()) end return "nil" end function distance_to_xz_sqr(a, b) return math.pow(b.x - a.x, 2) + math.pow(b.z - a.z, 2) end function is_within_x_distance(npc_1, npc_2, dist_type, dist_custom) if not (npc_1 and npc_2) then d_printf("DFRC.TGR: is_within_x_distance(npc_1,npc_2,..): nil object was passed",nil,"param_error") return false end local npc_1_level_name = tostring(get_object_levelname(npc_1)) local npc_2_level_name = tostring(get_object_levelname(npc_2)) if ( npc_1_level_name and npc_1_level_name == npc_2_level_name ) then local dist = distance_to_xz_sqr(npc_1:position(), npc_2:position()) local dist_xz = MAX_OBSERVER_DISTANCE if dist_type then if dist_type == "WITNESS" then dist_xz = WITNESS_DISTANCE elseif dist_type == "GOSSIP" then dist_xz = GOSSIP_DISTANCE elseif dist_type == "HEARSAY" then dist_xz = MAX_OBSERVER_DISTANCE elseif dist_type == "CUSTOM" then if dist_custom then dist_xz = tonumber(dist_custom) or MAX_OBSERVER_DISTANCE end end end return (dist < dist_xz) else -- npc's are not in same level return false end end function is_within_observer_distance(npc_1, npc_2) return is_within_x_distance(npc_1, npc_2, "HEARSAY") end -- setup and age check for AREA_STATS tables function scan_area_cooldown(obj_comm) local tg = time_global() if not AREA_STATS[obj_comm] then AREA_STATS[obj_comm] = {} end if not AREA_STATS[obj_comm].last_scan_time then AREA_STATS[obj_comm].last_scan_time = 0 --AREA_STATS[obj_comm].num_enemies = 0 end if (AREA_STATS[obj_comm].last_scan_time + min_scan_time_age) > tg then return true elseif (AREA_STATS[obj_comm].last_scan_time + max_scan_time_age) < tg then AREA_STATS[obj_comm].hostile = false -- clear enemy num memory end AREA_STATS[obj_comm].num_enemies = 0 -- clear enemy num memory AREA_STATS.last_comm = obj_comm return false end function scan_area_for_enemies(obj, obj_comm) if scan_area_cooldown(obj_comm) then return end local actor = db.actor local tg = time_global() for i=1, #db.OnlineStalkers do local npc = level.object_by_id(db.OnlineStalkers[i]) if npc and IsStalker(npc,npc:clsid()) and npc:alive() then local npc_id = npc:id() if npc_id ~= obj:id() and npc_id ~= AC_ID and not npc:has_info("npcx_is_companion") then if is_within_observer_distance(actor, npc) then local npc_comm = get_object_community(npc) if is_factions_enemies(npc_comm, obj_comm) then AREA_STATS[obj_comm].num_enemies = AREA_STATS[obj_comm].num_enemies + 1 end end end end end AREA_STATS[obj_comm].hostile = ( AREA_STATS[obj_comm].hostile or (AREA_STATS[obj_comm].num_enemies > 0) ) d_printf("DFRC.TGR: scan_area_for_enemies - comm "..obj_comm.." num_enemies "..AREA_STATS[obj_comm].num_enemies) AREA_STATS[obj_comm].last_scan_time = tg end function scan_area_and_alert_nearby_npcs(obj, obj_comm, wpn, silent, silent_hear_dist) if scan_area_cooldown(obj_comm) then return end local actor = db.actor local tg = time_global() local dist_type = silent and "CUSTOM" or "HEARSAY" local reporting_npcs = {} for i=1, #db.OnlineStalkers do local npc = level.object_by_id(db.OnlineStalkers[i]) if npc and IsStalker(npc,npc:clsid()) and npc:alive() then local npc_id = npc:id() if npc_id ~= obj:id() and npc_id ~= AC_ID and not npc:has_info("npcx_is_companion") then if is_within_x_distance(actor, npc, dist_type, silent_hear_dist) then local npc_comm = get_object_community(npc) if is_factions_enemies(npc_comm, obj_comm) then AREA_STATS[obj_comm].num_enemies = AREA_STATS[obj_comm].num_enemies + 1 elseif wpn then if not dfrc_tgr[obj_comm].npcs[npc_id] then setup_tgr_cache_npc(obj_comm,npc_id) d_printf("DFRC.TGR: scan_area_and_alert_nearby_npcs - Setup Cache for NPC id: "..tostring(npc_id)..", comm: "..tostring(npc_comm)..", goodwill(db.actor): "..tostring(npc:goodwill(db.actor))) end dfrc_tgr.npc_map[npc_id] = obj_comm -- used to map neutral npc to attacked faction if npc:see(obj) or npc:see(db.actor) then dfrc_tgr[obj_comm].npcs[npc_id].witness = true if npc:see(db.actor) then set_npc_hostile(npc, npc_id, npc_comm) reporting_npcs[#reporting_npcs + 1] = npc end end dfrc_tgr[obj_comm].watches_out_for = wpn:section() end end end end end if #reporting_npcs > 0 then local sender = reporting_npcs[math.random(#reporting_npcs)] if sender then SeenHit(sender, npc_comm) end end AREA_STATS[obj_comm].hostile = ( AREA_STATS[obj_comm].hostile or (AREA_STATS[obj_comm].num_enemies > 0) ) d_printf("DFRC.TGR: scan_area - comm "..obj_comm.." num_enemies "..AREA_STATS[obj_comm].num_enemies) AREA_STATS[obj_comm].last_scan_time = tg end function is_valid_affected_faction(faction) -- keep this as function in case you want extra conditions to apply at a later stage return valid_factions[faction] or false end ------------------------------------------------------------ -- Callbacks NPC Aggravation ------------------------------------------------------------ function npc_on_hit_callback(npc,amount,local_direction,who,bone_index) --if not settings["enable_goodwill_reset_on_friendly_fire"] then return end if not (npc and who) then return end if who:id() ~= AC_ID then return end -- shooter must be actor -- if pause_aggravation_evaluation_arena then return end -- disable evaluation during arena battle -- collect victim npc data local npc_id = npc:id() local npc_comm = get_object_community(npc) local actor_comm = get_actor_true_community() -- string.gsub( get_object_community(db.actor), "actor_", "" ) --if not dfrc_tgr.last_comm_hit_by_actor then dfrc_tgr.last_comm_hit_by_actor = npc_comm dfrc_tgr.last_hit_time = time_global() --end -- skip if faction cannot be affected -- if not is_valid_affected_faction(npc_comm) then return end -- skip if faction is already enemy to actor if is_factions_enemies(npc_comm, actor_comm) then return end --if goodwill_reset_not_applicable(npc_comm) and not dfrc_tgr[npc_comm] then return end -- check if npc is in combat and player was not seen by any other non-hostile npc -- 03-NOV-2024: Fixed missing check where get_object_community('false') would be called local witness = gameplay_silent_kills.anybody_see(db.actor) local witness_comm = witness and get_object_community(witness) local actor_seen = witness and not is_factions_enemies(npc_comm, witness_comm) if not npc:see(db.actor) and not actor_seen then if state_mgr.is_npc_in_combat(npc) then d_printf("#DFRC.TGR: Ignored HIT NPC "..npc_id.." comm = "..npc_comm.." due to being in combat state.") return end local be = npc:best_enemy() if be then if be:id() ~= AC_ID and IsStalker(be) then d_printf("#DFRC.TGR: Ignored HIT NPC "..npc_id.." comm = "..npc_comm.." due to being in combat: best_enemy = "..tostring(be:id())) return end end end -- collecting incident parameters local wpn = db.actor:active_item() if not IsWeapon(wpn) then return end -- no point going further if "just" H2H local silent = wpn and gameplay_silent_kills.valid_silent_weapon(wpn) or false local silent_hear_dist = wpn and gameplay_silent_kills.silent_weapon_hear_range(wpn) or 10 -- setup cache entries if not dfrc_tgr[npc_comm] then setup_tgr_cache_comm(npc_comm) end if not dfrc_tgr[npc_comm].npcs[npc_id] then setup_tgr_cache_npc(npc_comm,npc_id) dfrc_tgr.npc_map[npc_id] = npc_comm d_printf("DFRC.TGR: npc_on_hit_callback - Setup Cache for NPC id: "..tostring(npc_id)..", comm: "..tostring(npc_comm)..", goodwill(db.actor): "..tostring(npc:goodwill(db.actor))) end -- scan area for enemies and neutrals to npc scan_area_and_alert_nearby_npcs(npc, npc_comm, wpn, silent, silent_hear_dist) local num_nearby_enemies = AREA_STATS[npc_comm].num_enemies local num_alerted_npcs = math.max(#dfrc_tgr[npc_comm].npcs, 1) local enemy_factor = ( num_nearby_enemies * 2 ) + 1 local grudge = (num_nearby_enemies > 0) and (amount / enemy_factor) or amount if npc:see(db.actor) then grudge = grudge * 2 set_npc_hostile(npc, npc_id, npc_comm) end -- set faction specific data dfrc_tgr[npc_comm].last_hit_time = time_global() dfrc_tgr[npc_comm].watches_out_for = wpn:section() dfrc_tgr[npc_comm].hit_v = dfrc_tgr[npc_comm].hit_v + grudge if num_nearby_enemies == 0 and not silent then dfrc_tgr[npc_comm].regen_active = false -- even if they cannot prove it was you their suspicion is enough to temporarily halt regeneration WasHit(npc, npc_comm, num_alerted_npcs) end -- set npc data dfrc_tgr[npc_comm].npcs[npc_id].victim = true if npc:see(db.actor) and not dfrc_tgr[npc_comm].npcs[npc_id].witness then dfrc_tgr[npc_comm].npcs[npc_id].witness = true end -- if not msg_cooldown then >>msg: "Ouch!", "Ack...", "What the..." d_printf("~DFRC.TGR: HIT NPC "..npc_id.." comm = "..npc_comm.." new hit_v = "..tostring(dfrc_tgr[npc_comm].hit_v)..", alive = "..tostring(npc:alive())..", witness = "..tostring(dfrc_tgr[npc_comm].npcs[npc_id].witness)..", ttr = "..tostring(dfrc_tgr[npc_comm].npcs[npc_id].time_to_report)..", goodwill(db.actor): "..tostring(npc:goodwill(db.actor))) end function npc_on_update(npc) --if not settings["enable_goodwill_reset_on_friendly_fire"] then return end if not npc then return end if pause_aggravation_evaluation then return end -- disable evaluation if GUI is open if pause_aggravation_evaluation_arena then return end -- disable evaluation during arena battle local actor_comm = get_actor_true_community() --string.gsub( get_object_community(db.actor), "actor_", "" ) local npc_id = npc:id() local npc_comm = get_object_community(npc) -- if not is_valid_affected_faction(npc_comm) then return end local npc_hostile = (npc:relation(db.actor) >= game_object.enemy) local target_comm = dfrc_tgr.npc_map[npc_id] if npc:alive() and not target_comm and npc_hostile then if not dfrc_tgr[npc_comm] then return end -- map and cache newly 'triggered' npc dfrc_tgr.npc_map[npc_id] = npc_comm target_comm = npc_comm if not dfrc_tgr[target_comm].npcs[npc_id] then setup_tgr_cache_npc(target_comm,npc_id) dfrc_tgr.npc_map[npc_id] = target_comm d_printf("DFRC.TGR: npc_on_update_callback - Setup Cache for NPC id: "..tostring(npc_id)..", comm: "..tostring(npc_comm)..", goodwill(db.actor): "..tostring(npc:goodwill(db.actor))..", hostile = "..tostring(npc_hostile)) end end if not npc:alive() then if dfrc_tgr[target_comm] then dfrc_tgr[target_comm].npcs[npc_id] = nil end if dfrc_tgr.npc_map[npc_id] then dfrc_tgr.npc_map[npc_id] = nil end return end if not dfrc_tgr[target_comm] then return end -- show Aggravation HUD bar if aggravation > 0 show_hud_bar = (dfrc_tgr[target_comm].hit_v > 0) local tg = time_global() -- if actor did generally hit an enemy of the aggravated faction recently this flag will be set local actor_vs_comm = (dfrc_tgr.last_hit_time + 240000 > tg) and dfrc_tgr.last_comm_hit_by_actor local actor_is_helping = actor_vs_comm and dfrc_tgr[target_comm].actor_is_helping or actor_vs_comm and is_factions_enemies(actor_vs_comm, target_comm) or nil dfrc_tgr[target_comm].actor_is_helping = actor_is_helping if dfrc_tgr[target_comm].npcs[npc_id] then -- rescan area for enemies scan_area_for_enemies(npc, target_comm) local enemies_nearby = (AREA_STATS[target_comm].num_enemies > 0) local is_enemy_comm = is_factions_enemies(actor_comm, target_comm) if not is_aggravation_limit_reached(target_comm) and enemies_nearby then -- aggravation is only possible through hits during enemy encounters if npc_hostile and not is_enemy_comm then -- there are more important matters at hand so we will set this npc neutral and exit set_npc_neutral(npc, npc_id, target_comm, false) end -- exit return end local npc_flags = dfrc_tgr[target_comm].npcs[npc_id] -- No enemies around, lets take a look at what the actor is up to local hostile_area = AREA_STATS[target_comm].hostile local num_alerted_npcs = math.max(#dfrc_tgr[target_comm].npcs, 1) local hit_timeout = ( dfrc_tgr[target_comm].last_hit_time + ( 100000 / ( hostile_area and 2 or 1 ) ) ) < tg local incident_timeout = ( dfrc_tgr[target_comm].last_hit_time + ( 600000 / ( hostile_area and 3 or 1 ) ) ) < tg -- some cleanup of npc flags if applicable: if dfrc_tgr[target_comm].npcs[npc_id].surrender_ack then if dfrc_tgr[target_comm].npcs[npc_id].surrender_ack + 600000 < tg then dfrc_tgr[target_comm].npcs[npc_id].surrender_ack = nil end end if dfrc_tgr[target_comm].npcs[npc_id].last_report then if dfrc_tgr[target_comm].npcs[npc_id].last_report + 600000 < tg then dfrc_tgr[target_comm].npcs[npc_id].last_report = nil end end local wpn = db.actor:active_item() -- calming is faster if area is flagged as hostile local cm = (actor_is_helping or incident_timeout) and 3 or hit_timeout and 2 or 1 if npc:see(db.actor) then if not IsWeapon(wpn) then -- reduce aggravation and suspicion unless surrender meachnics are disabled for whichever reason (enemy faction, fake-out) if not dfrc_tgr[target_comm].surrender_enabled and incident_timeout then -- reset surrender enabled if incident has timed out dfrc_tgr[target_comm].surrender_enabled = not is_enemy_comm end if dfrc_tgr[target_comm].surrender_enabled then if npc_hostile or npc_flags.witness then if IsMoveState('mcCrouch') and not IsMoveState('mcAnyMove') then -- actor surrenders if npc_flags.surrender_ack then if npc_flags.time_to_sack then dfrc_tgr[target_comm].npcs[npc_id].time_to_sack = nil AcknowledgedActorSurrender(npc, target_comm) end elseif npc_flags.time_to_sack then local actor_critically_wounded = (db.actor.health < 0.4 and db.actor.bleeding > 0.5) if npc_flags.time_to_sack < tg or actor_critically_wounded then if actor_critically_wounded then d_printf("~DFRC.TGR: npc_on_update_callback - actor is spared due to injuries") end -- acknowledge surrender and set hostile to neutral set_npc_neutral(npc, npc_id, target_comm, tg) -- victim can set all registered witness npcs neutral -- also triggered if actor badly wounded if npc_flags.victim or actor_critically_wounded then set_all_neutral_in_faction(target_comm, tg) end end else -- >>msg: "That's right, stay down!" if npc_flags.witness and is_aggravation_limit_reached(target_comm) then SeenActorInitSurrender(npc, target_comm) end set_surrender_time(npc_id, target_comm, ( 1 / cm ) ) end calm_down(target_comm, (2 * cm / num_alerted_npcs)) else calm_down(target_comm, (1 * cm / num_alerted_npcs)) end elseif hit_timeout then calm_down(target_comm, (1 * cm / num_alerted_npcs)) end end else -- Compare active weapon with cached type -- Update 0.3: added 'game.actor_weapon_lowered()' checks if dfrc_tgr[target_comm].watches_out_for then local aggr_mult = game.actor_weapon_lowered() and 0.25 or 1 if dfrc_tgr[target_comm].watches_out_for == wpn:section() then aggr_mult = aggr_mult * 2 if not npc_flags.witness then dfrc_tgr[target_comm].npcs[npc_id].witness = true SeenActorHitWeapon(npc, target_comm) end if not hostile_area then aggr_mult = aggr_mult * 2 if npc_flags.victim and not hit_timeout then aggr_mult = aggr_mult * 2 set_npc_hostile(npc, npc_id, target_comm) end end else if not hostile_area then if npc_flags.witness then if npc_flags.victim and not hit_timeout then aggr_mult = aggr_mult * 2 end else if not npc_hostile then SeenActorWeapon(npc, target_comm) end end end end if not hostile_area and dfrc_tgr[target_comm].surrender_enabled and (npc_flags.surrender_ack or npc_flags.time_to_sack) and not game.actor_weapon_lowered() then aggr_mult = aggr_mult * 2 -- surrender option is temporarily forfeit if player is caught with weapon before incident timeout dfrc_tgr[target_comm].last_hit_time = tg -- using this to negate hit_timeout dfrc_tgr[target_comm].surrender_enabled = false dfrc_tgr[target_comm].npcs[npc_id].surrender_ack = nil dfrc_tgr[target_comm].npcs[npc_id].time_to_sack = nil SeenActorUnSurrender(npc, target_comm) end -- aggravate if aggr_mult > 0 then aggravate(target_comm, ( aggr_mult / cm / num_alerted_npcs )) end end -- enough is enough! if is_aggravation_limit_reached(target_comm) and dfrc_tgr[target_comm].npcs[npc_id].witness then set_npc_hostile(npc, npc_id, target_comm) set_npc_ttr(npc_id, target_comm, num_alerted_npcs) end end elseif hit_timeout and (dfrc_tgr[target_comm].npcs[npc_id].surrender_ack or not npc_flags.witness) and not npc_hostile then calm_down(target_comm, (cm / num_alerted_npcs)) end -- regardless of npc seeing actor -- will not trigger unless player carries weapon or surrender is disabled since is_aggravation_limit_reached() condition is used: if npc_flags.witness and npc_flags.time_to_report and is_aggravation_limit_reached(target_comm) then if npc_flags.time_to_sack then return end -- giving the surrender timer a chance if npc_flags.time_to_report > tg then return end -- RESET so it can be reused dfrc_tgr[target_comm].npcs[npc_id].time_to_report = nil if not npc_flags.last_report then -- >>msg: "You're going down!" / "I need some help over here!" local reporting_npcs = {} -- report will set all registered npcs to witness (but not yet hostile) -- random npc will be selected for report for k,v in pairs(dfrc_tgr[target_comm].npcs) do if not dfrc_tgr[target_comm].npcs[k].witness then dfrc_tgr[target_comm].npcs[k].witness = true local n = get_object_by_id(k) or nil if n then set_npc_hostile(n, npc_id, target_comm) reporting_npcs[#reporting_npcs + 1] = n end end end if #reporting_npcs > 0 then local sender = reporting_npcs[math.random(#reporting_npcs)] if sender then ReportingAttack(sender, target_comm) end end dfrc_tgr[target_comm].npcs[npc_id].last_report = tg if goodwill_reset_not_applicable(target_comm) then return end -- escalate to afd if dfrc installed and afd enabled if dfrc then if dfrc.setup_actor_faction_dissatisfation then -- Modify AFD tracking value if target_comm == actor_comm and feature is enabled local afd_enabled = false if dynamic_faction_relations_customizer.settings then afd_enabled = dynamic_faction_relations_customizer.settings["enable_actor_faction_dissatisfaction"] else afd_enabled = dynamic_faction_relations_customizer.enable_actor_faction_dissatisfaction end if afd_enabled and target_comm == actor_comm then if not dynamic_faction_relations_customizer.dfrc_afd then dfrc.setup_actor_faction_dissatisfation() end if not dynamic_faction_relations_customizer.dfrc_afd.value then dynamic_faction_relations_customizer.dfrc_afd.value = 0 end dynamic_faction_relations_customizer.dfrc_afd.value = dynamic_faction_relations_customizer.dfrc_afd.value + dfrc_tgr[target_comm].hit_v dynamic_faction_relations_customizer.dfrc_afd.threshold = math.max(dfrc_tgr[target_comm].hit_v,dynamic_faction_relations_customizer.dfrc_afd.threshold) end end end -- execute reset execute_temp_goodwill_reset(target_comm) else for i=1, #db.OnlineStalkers do local npc_a = level.object_by_id(db.OnlineStalkers[i]) if npc_a and IsStalker(npc_a,npc_a:clsid()) and npc_a:alive() then local npc_a_id = npc_a:id() if npc_a_id ~= npc_id and npc_a_id ~= AC_ID and not npc_a:has_info("npcx_is_companion") then if is_within_observer_distance(npc, npc_a) then local npc_comm = get_object_community(npc_a) if not is_factions_enemies(npc_comm, target_comm) then if not dfrc_tgr[target_comm].npcs[npc_a_id] then setup_tgr_cache_npc(target_comm,npc_a_id) d_printf("DFRC.TGR: trigger_nearby_npcs - Setup Cache for NPC id: "..tostring(npc_a_id)..", comm: "..tostring(target_comm)..", goodwill(db.actor): "..tostring(npc_a:goodwill(db.actor))) end dfrc_tgr.npc_map[npc_a_id] = target_comm -- used to map neutral npc to attacked faction dfrc_tgr[target_comm].npcs[npc_a_id].witness = true if npc_a:see(db.actor) then set_npc_hostile(npc_a, npc_a_id, target_comm) end end end end end end end end end end ------------------------------------------------------------ -- aggravation and calming ------------------------------------------------------------ function aggravate(target_comm, multiplier) dfrc_tgr[target_comm].hit_v = dfrc_tgr[target_comm].hit_v + (base_grudge_value_mod * aggravation_factor * multiplier) if dfrc_tgr[target_comm].hit_v >= settings["faction_dissatisfaction_limit"] then d_printf("!DFRC.TGR: MAXIMUM Aggravation reached - comm = "..target_comm) else d_printf("~DFRC.TGR: AGGRAVATING - comm = "..target_comm) end end function calm_down(target_comm, multiplier) if dfrc_tgr[target_comm].hit_v > 0 then dfrc_tgr[target_comm].hit_v = math.max(dfrc_tgr[target_comm].hit_v - (base_grudge_value_mod * calming_factor * multiplier), 0) if dfrc_tgr[target_comm].hit_v == 0 then d_printf("-DFRC.TGR: Aggravation has reached 0 - comm = "..target_comm) show_hud_bar = false else d_printf("#DFRC.TGR: CALMING DOWN - comm = "..target_comm) end end end function is_aggravation_limit_reached(target_comm) if not (dfrc_tgr[target_comm] and settings["faction_dissatisfaction_limit"]) then return false end if not dfrc_tgr[target_comm].hit_v then return false end return ( dfrc_tgr[target_comm].hit_v >= settings["faction_dissatisfaction_limit"] ) end function set_all_neutral_in_faction(target_comm, s_ack) for i=1, #db.OnlineStalkers do local npc = level.object_by_id(db.OnlineStalkers[i]) if npc and IsStalker(npc,npc:clsid()) and npc:alive() then local npc_id = npc:id() local npc_comm = get_object_community(npc) if npc_id ~= AC_ID and not npc:has_info("npcx_is_companion") then if not is_factions_enemies(npc_comm, target_comm) then set_npc_neutral(npc, npc_id, target_comm, s_ack) end end end end end function set_npc_neutral(npc, npc_id, target_comm, s_ack) if (npc:relation(db.actor) >= game_object.enemy) then -- add option to store old goodwill later? local old_goodwill = npc:goodwill(db.actor) npc:force_set_goodwill(-700, db.actor) if dfrc_tgr[target_comm].npcs[npc_id] then dfrc_tgr[target_comm].npcs[npc_id].time_to_report = nil dfrc_tgr[target_comm].npcs[npc_id].surrender_ack = s_ack end local hit_v_str = nil if dfrc_tgr[target_comm] then if dfrc_tgr[target_comm].hit_v then hit_v_str = string.format("%.3f", dfrc_tgr[target_comm].hit_v) end end d_printf("#DFRC.TGR: SET NPC NEUTRAL id = "..tostring(npc_id)..", comm = "..tostring(target_comm)..", hit_v = "..tostring(hit_v_str)..", old_goodwill(db.actor): "..tostring(old_goodwill)) end end function set_npc_hostile(npc, npc_id, target_comm) if (npc:relation(db.actor) < game_object.enemy) then -- add option to store old goodwill later? local old_goodwill = npc:goodwill(db.actor) npc:force_set_goodwill(-3000, db.actor) local hit_v_str = nil if dfrc_tgr[target_comm] then if dfrc_tgr[target_comm].hit_v then hit_v_str = string.format("%.3f", dfrc_tgr[target_comm].hit_v) end end d_printf("!DFRC.TGR: SET NPC HOSTILE id = "..tostring(npc_id)..", comm = "..tostring(target_comm)..", hit_v = "..tostring(hit_v_str)..", old_goodwill(db.actor): "..tostring(old_goodwill)) end end function set_surrender_time(npc_id, target_comm, multiplier) dfrc_tgr[target_comm].npcs[npc_id].time_to_sack = time_global() + math.floor( 1000 * math.random(2,5) * clamp(multiplier * math.max( ( dfrc_tgr[target_comm].hit_v / 20 ), 1 ), 0.25, 5) ) local hit_v_str = string.format("%.3f", dfrc_tgr[target_comm].hit_v) local tta_str = tostring(dfrc_tgr[target_comm].npcs[npc_id].time_to_sack) d_printf("!DFRC.TGR: SURRENDERING to NPC "..npc_id..", comm = "..target_comm..", hit_v = "..hit_v_str..", tta = "..tta_str) end function set_npc_ttr(npc_id, target_comm, multiplier) local hit_v_str = string.format("%.3f", dfrc_tgr[target_comm].hit_v) local ttr_str = tostring(dfrc_tgr[target_comm].npcs[npc_id].time_to_report) -- if dfrc_tgr[target_comm].npcs[npc_id].last_report then -- d_printf("*DFRC.TGR: Incident was already reported a short while ago. TTR will not be set for WITNESS NPC "..npc_id..", comm = "..target_comm..", hit_v = "..hit_v_str) -- return -- end if not dfrc_tgr[target_comm].npcs[npc_id].time_to_report then dfrc_tgr[target_comm].npcs[npc_id].time_to_report = time_global() + math.floor( 1000 * math.random(4,9) * clamp(multiplier, 1, 5) ) d_printf("!DFRC.TGR: SET TTR for WITNESS NPC "..npc_id.." comm = "..target_comm.." hit_v = "..hit_v_str..", ttr = "..ttr_str) else d_printf("*DFRC.TGR: TTR already set for WITNESS NPC "..npc_id..", comm = "..target_comm..", hit_v = "..hit_v_str..", ttr = "..ttr_str) end end ------------------------------------------------------------ -- Faction Goodwill Reset and Regeneration ------------------------------------------------------------ -- determine if a reset could be applied function goodwill_reset_not_applicable(target_comm) if relation_registry.community_goodwill(target_comm, AC_ID) <= 0 then return true end if not dfrc_tgr[target_comm] then return false end local tg = time_global() if dfrc_tgr[target_comm].last_reset_time > 0 and ( dfrc_tgr[target_comm].last_reset_time + 600000 ) > tg then -- last reset was less than an ingame hour ago return true else return false end end function execute_temp_goodwill_reset(faction) if not dfrc_tgr[faction] then d_printf("!DFRC.TGR: Cannot execute Goodwill Reset on faction "..tostring(faction).." - Cache was not properly set up") return end local current_goodwill = relation_registry.community_goodwill(faction, AC_ID) local min_keep_goodwill_pct = math.min(settings["min_goodwill_regainable_after_reset"],settings["max_goodwill_regainable_after_friendly_fire"]) local max_keep_goodwill_pct = math.max(settings["min_goodwill_regainable_after_reset"],settings["max_goodwill_regainable_after_friendly_fire"]) d_printf("!DFRC.TGR: min_keep_goodwill_pct = "..tostring(min_keep_goodwill_pct).." max_keep_goodwill_pct = "..tostring(max_keep_goodwill_pct)) if max_keep_goodwill_pct > settings["max_goodwill_regainable_after_friendly_fire"] then -- in case of switched min / max max_keep_goodwill_pct = settings["max_goodwill_regainable_after_friendly_fire"] d_printf("!DFRC.TGR: corrected max_keep_goodwill_pct = "..tostring(max_keep_goodwill_pct)) end local keep_goodwill_pct = random_float(min_keep_goodwill_pct,max_keep_goodwill_pct) d_printf("!DFRC.TGR: final keep_goodwill_pct = "..tostring(keep_goodwill_pct)) if not dfrc_tgr[faction].max_v then dfrc_tgr[faction].max_v = math.floor(current_goodwill * keep_goodwill_pct) else dfrc_tgr[faction].max_v = math.floor(dfrc_tgr[faction].max_v * keep_goodwill_pct) dfrc_tgr[faction].regen_rate = 0 dfrc_tgr[faction].regen_cycle = 0 end d_printf("!DFRC.TGR: Goodwill Reset Triggered - keep_gw% = "..string.format("%.2f",keep_goodwill_pct)..", new max_v = "..tostring(dfrc_tgr[faction].max_v)) relation_registry.set_community_goodwill( faction, AC_ID, 0 ) if dfrc_tgr[faction].max_v == 0 then d_printf("!DFRC.TGR: Goodwill cannot be regenerated.") dfrc_tgr[faction].regen_enabled = false else dfrc_tgr[faction].regen_enabled = not is_factions_enemies(faction,string.gsub( get_object_community(db.actor), "actor_", "" )) dfrc_tgr[faction].last_reset_time = time_global() end end -- called by change_factions_community_num function change_goodwill_regen_peak(faction, value) if not settings["enable_goodwill_regeneration"] then return end if not dfrc_tgr[faction] then return end if not dfrc_tgr[faction].regen_enabled then return end if value > 10 then dfrc_tgr[faction].regen_active = true dfrc_tgr[faction].regen_peak = math.min(dfrc_tgr[faction].regen_peak + 0.005, 0.05) dfrc_tgr[faction].hit_v = math.max(dfrc_tgr[faction].hit_v - value, 0) elseif value < -10 or dfrc_tgr[faction].hit_v > 75 then dfrc_tgr[faction].regen_peak = math.max(dfrc_tgr[faction].regen_peak - 0.005, 0) end d_printf("#DFRC.TGR: faction = "..faction..", new regen_peak = "..string.format("%.3f",dfrc_tgr[faction].regen_peak)) end function regenerate_goodwill_with_factions() if not settings["enable_goodwill_regeneration"] then return end --factions_table = game_relations.factions_table if not factions_table then d_printf("! ERROR in regenerate_goodwill_with_factions: no faction table found.") return end local tg = time_global() for i=1, #factions_table do if dfrc_tgr[factions_table[i]] then if dfrc_tgr[factions_table[i]].regen_enabled and dfrc_tgr[factions_table[i]].max_v then local last_hit_time = dfrc_tgr[factions_table[i]].last_hit_time local max_value = dfrc_tgr[factions_table[i]].max_v if max_value then if not dfrc_tgr[factions_table[i]].regen_active then if ((last_hit_time + min_hit_time_delay) < tg) then dfrc_tgr[factions_table[i]].regen_active = true end else if (dfrc_tgr[factions_table[i]].last_regen_time + min_regen_time_delay) < tg then local last_regen_time_hr = dfrc_tgr[factions_table[i]].last_regen_time_hr local current_hr = utils_obj.time_spent_in_zone(3) local hrs_passed = (last_regen_time_hr > 0) and (current_hr - last_regen_time_hr) or 1 if hrs_passed == 0 then hrs_passed = 1 end dfrc_tgr[factions_table[i]].last_regen_time_hr = current_hr dfrc_tgr[factions_table[i]].last_regen_time = tg -- increase rate on first and then after 3 iterations total local regen_cycle = dfrc_tgr[factions_table[i]].regen_cycle dfrc_tgr[factions_table[i]].regen_cycle = (regen_cycle < 3) and (regen_cycle + 1) or 1 if dfrc_tgr[factions_table[i]].regen_cycle == 1 then dfrc_tgr[factions_table[i]].regen_rate = math.min(dfrc_tgr[factions_table[i]].regen_rate + 0.005, dfrc_tgr[factions_table[i]].regen_peak) -- making sure that if this dropped to 0 that it will be restored to 0.05 in the next cycle. if dfrc_tgr[factions_table[i]].regen_peak == 0 then dfrc_tgr[factions_table[i]].regen_peak = 0.005 end end if dfrc_tgr[factions_table[i]].regen_rate > 0 then local regen_value = math.min(max_value * dfrc_tgr[factions_table[i]].regen_rate, 1) * hrs_passed local current_value = relation_registry.community_goodwill(factions_table[i], AC_ID) local new_value = math.min(current_value + regen_value, max_value) if new_value ~= current_value then relation_registry.set_community_goodwill( factions_table[i], AC_ID, new_value) end if ( (current_value + regen_value) > max_value ) then if #dfrc_tgr[factions_table[i]].npcs == 0 then dfrc_tgr[factions_table[i]] = nil end d_printf("#DFRC.TGR: Goodwill with "..factions_table[i].." fully regenerated.") else d_printf("#DFRC.TGR: Goodwill with "..factions_table[i].." regenerated to "..new_value) end end end end end end end end end ------------------------------------------------------------ -- Callbacks General ------------------------------------------------------------ local function save_state(m_data) if (USE_MARSHAL) then printdbg("# SAVING: Gameplay - Actor Friendly Fire and Surrender Stats") m_data.dfrc_tgr = dfrc_tgr end end local function load_state(m_data) if (USE_MARSHAL) then printdbg("# LOADING: DFRC Persistent Data") if m_data.dfrc_tgr then dfrc_tgr = m_data.dfrc_tgr end end end local function on_before_level_changing() -- purge npc lists from dfrc_tgr if outdated --if not settings["enable_goodwill_reset_on_friendly_fire"] then return end factions_table = game_relations.factions_table if not factions_table then d_printf("! ERROR in regenerate_goodwill_with_factions: no faction table found.") return end show_hud_bar = false -- TODO: -- * if npcs is not empty still has any npc with an active ttr flag: execute reset end local function actor_on_update() hud_update() if not settings["enable_goodwill_regeneration"] then return end local tg = time_global() if tg < tg_update then return end tg_update = tg + tg_update_step if tg > tg_update_tgr then regenerate_goodwill_with_factions() tg_update_tgr = tg + tg_update_step_tgr end end local function on_option_change() load_settings() end local function on_game_load() load_settings() show_hud_bar = false end local function GUI_on_show(name) --if (name ~= "UIInventory") then return end -- we don't pause for anything but the inventory screen pause_aggravation_evaluation = true end local function GUI_on_hide() pause_aggravation_evaluation = false end function on_game_start() RegisterScriptCallback("GUI_on_show",GUI_on_show) RegisterScriptCallback("GUI_on_hide",GUI_on_hide) RegisterScriptCallback("on_before_level_changing",on_before_level_changing) RegisterScriptCallback("npc_on_update",npc_on_update) --RegisterScriptCallback("on_enemy_eval", on_enemy_eval) RegisterScriptCallback("actor_on_update",actor_on_update) RegisterScriptCallback("npc_on_hit_callback",npc_on_hit_callback) RegisterScriptCallback("save_state",save_state) RegisterScriptCallback("load_state",load_state) RegisterScriptCallback("on_option_change", on_option_change) RegisterScriptCallback("on_game_load", on_game_load) -- Patch game_relations function if "Dynamic Faction Relation Customizer" is not installed: if not dfrc_has_feature["affs"] then ORIGINAL_GR_CFCN = game_relations.change_factions_community_num function game_relations.change_factions_community_num(faction_name, obj_id, delta, source) if(faction_name~=nil) and (faction_name~="none") and (obj_id ~= nil) and (delta ~= nil) then -- adapt regen_peak (if TGR is enabled) if obj_id == AC_ID then change_goodwill_regen_peak(faction_name, delta) end ORIGINAL_GR_CFCN(faction_name, obj_id, delta, source) end end -- full replacement of original function (fixes vanilla bug as well) function game_relations.reset_goodwill(faction_1 , faction_2) -- Reset the goodwill for actors of both factions if ui_options.get("alife/general/war_goodwill_reset") then d_printf("DFRC.TGR game_relations.reset_goodwill called: "..faction_1.." and "..faction_2) if faction_1 == faction_2 then d_printf("* Factions are identical...? No change.") return end local actor_comm = get_actor_true_community() local target_comm if actor_comm == faction_1 then target_comm = faction_2 elseif actor_comm == faction_2 then target_comm = faction_1 end if target_comm then local current_goodwill = relation_registry.community_goodwill(target_comm, AC_ID) if current_goodwill <= 0 then d_printf("* Goodwill is zero or negative. No change.") return end local min_keep_goodwill = current_goodwill * math.min(settings["min_goodwill_regainable_after_reset"],settings["max_goodwill_regainable_after_reset"]) local max_keep_goodwill = current_goodwill * math.max(settings["min_goodwill_regainable_after_reset"],settings["max_goodwill_regainable_after_reset"]) local new_goodwill = math.floor(math.random(min_keep_goodwill, max_keep_goodwill)) d_printf("* Actor_Comm = "..actor_comm..", Target_Comm = "..target_comm..", old_goodwill_value = "..current_goodwill..", rnd_keep% = "..string.format("%.2f",(new_goodwill/current_goodwill))..", new_goodwill = "..new_goodwill) -- will be empty when not enabled so separate check for feature enabled is not needed if dfrc_tgr[target_comm] then if current_goodwill < dfrc_tgr[target_comm].max_v then local current_max_v = dfrc_tgr[target_comm].max_v local min_keep_max_v = current_max_v * settings["min_goodwill_regainable_after_reset"] local max_keep_max_v = current_max_v * settings["max_goodwill_regainable_after_reset"] local new_max_v = math.floor(math.random(min_keep_max_v, max_keep_max_v)) gameplay_surrender_friendly_fire.dfrc_tgr[target_comm].max_v = new_max_v d_printf("* Actor_Comm = "..actor_comm..", Target_Comm = "..target_comm..", old_max_v_value = "..current_max_v..", rnd_keep% = "..string.format("%.2f",(min_keep_max_v/max_keep_max_v))..", new_max_v = "..new_max_v) end dfrc_tgr[target_comm].regen_rate = 0 dfrc_tgr[target_comm].regen_cycle = 0 else setup_tgr_cache_comm(target_comm) dfrc_tgr[target_comm].max_v = new_goodwill end -- this overrides is_enemy of setup if current_goodwill > 100 then dfrc_tgr[target_comm].regen_enabled = true end dfrc_tgr[faction].regen_peak = 0.01 -- default regen_peak for war response relation_registry.set_community_goodwill( target_comm, AC_ID, 0 ) end end end end -- Patch Arena Enter/Exit to disable aggravation checks ORIGINAL_XRFX_ARENA_ENTER = xr_effects.bar_arena_teleport function xr_effects.bar_arena_teleport(actor, npc) pause_aggravation_evaluation_arena = true end ORIGINAL_XRFX_ARENA_EXIT = xr_effects.bar_arena_teleport_2 function xr_effects.bar_arena_teleport_2(actor, npc) pause_aggravation_evaluation_arena = false end end ------------------------------------------------------------ -- News Extension ------------------------------------------------------------ function is_spam(msg_type_or_id) -- if npc id is passed then return false if not spammer_tracker[msg_type_or_id] then return false end -- increment tracker spammer_tracker[msg_type_or_id] = spammer_tracker[msg_type_or_id] + 1 -- check if anything other than 1 (not spam) local ret_v = (spammer_tracker[msg_type_or_id] ~= 1) if spammer_tracker[msg_type_or_id] > spammer_tracker_max[msg_type_or_id] then spammer_tracker[msg_type_or_id] = 0 end return ret_v end function active_message_cooldown(msg_type_or_id) local tg = time_global() local active_cooldown = false if msg_cooldown_time[msg_type_or_id] then active_cooldown = ( msg_cooldown_time[msg_type_or_id] + msg_cooldown_min_time > tg ) end if not active_cooldown then active_cooldown = is_spam(msg_type_or_id) end return active_cooldown end function set_message_cooldown(msg_type_or_id) local tg = time_global() msg_cooldown_time[msg_type_or_id] = tg end -- On NPC hit by friendly fire (NPC sees actor) function WasHit(sender, victim_comm, witnesses) if not sender then return end local msg_type = "WasHit" local sender_id = sender:id() if active_message_cooldown(sender_id) or active_message_cooldown(msg_type) then return end d_printf(">>> AFFS Dyn News: "..msg_type.." - call") set_message_cooldown(sender_id) set_message_cooldown(msg_type) local Se = strformat("%s, %s",sender:character_name(),dynamic_news_helper.GetCommunityDescription(sender,6)) local str_tbl = { [1] = "st_dyn_news_res_death_stalker_24", [2] = "st_dyn_news_comm_isg_12", [3] = "st_dyn_news_comm_actor_14", [4] = "st_dyn_news_comm_freedom_14", [5] = "dm_help_11", [6] = "axr_phrase_npc_start_wounded", } -- "st_dyn_news_res_death_stalker_24" What the fuck... -- "st_dyn_news_comm_isg_12" Taking fire, need assistance! -- "st_dyn_news_comm_actor_14" Help! -- "st_dyn_news_comm_freedom_14" Shit, help! -- "dm_help_11" Medkit! Quick! -- "axr_phrase_npc_start_wounded" Help... me. if witnesses > 1 then str_tbl = { [1] = "st_dyn_news_comm_renegade_12", [2] = "st_dyn_news_comm_actor_11", [3] = "st_dyn_news_comm_army_npc_11", [4] = "st_dyn_news_comm_isg_14", [5] = "dm_help_8", [6] = "axr_phrase_npc_start_wounded", } end -- "st_dyn_news_comm_renegade_12" We need some fucking help! -- "st_dyn_news_comm_actor_11" We need help! -- "st_dyn_news_comm_army_npc_11" Requesting support! -- "st_dyn_news_comm_isg_14" Requesting backup, now! -- "dm_help_8" Screw this place! -- "axr_phrase_npc_start_wounded" Help... me. local msg = game.translate_string(str_tbl[math.random(#str_tbl)]) local instance = dynamic_news_manager.get_dynamic_news() if not instance then dynamic_news_helper.send_tip(msg,Se, nil, msg_duration,victim_comm,"beep_1","gr") else instance:PushToChannel(victim_comm, {Mg=msg,Ic=victim_comm,Snd="beep_1",Se=Se,It="gr"}) end d_printf(">>> msg: '"..msg.."'") return end -- On NPC hit by friendly fire (Another NPC sees actor) function SeenHit(sender, victim_comm) if not sender then return end local msg_type = "SeenHit" local sender_id = sender:id() if active_message_cooldown(sender_id) or active_message_cooldown(msg_type) then return end d_printf(">>> AFFS Dyn News: "..msg_type.." - call") set_message_cooldown(sender_id) set_message_cooldown(msg_type) local Se = strformat("%s, %s",sender:character_name(),dynamic_news_helper.GetCommunityDescription(sender,6)) local str_tbl = { [1] = "st_dyn_news_res_fake_death_stalker_14", [2] = "st_dyn_news_res_fake_death_stalker_96", [3] = "st_dyn_news_res_fake_death_stalker_115", [4] = "st_dyn_news_res_fake_death_mutant_49", [5] = "dm_dialog_companion_question_answer_1", [6] = "dm_dialog_companion_final_words_bad_alone_2", } -- "st_dyn_news_res_fake_death_stalker_14 Damn. -- "st_dyn_news_res_fake_death_stalker_96" Uh, what happened here? -- "st_dyn_news_res_fake_death_stalker_115" Shit, I heard that one from here! -- "st_dyn_news_res_fake_death_mutant_49" What the hell is going on...? -- "dm_dialog_companion_question_answer_1" What is it? -- "dm_dialog_companion_final_words_bad_alone_2" Woah, man! What the hell is your problem? local msg = game.translate_string(str_tbl[math.random(#str_tbl)]) local instance = dynamic_news_manager.get_dynamic_news() if not instance then dynamic_news_helper.send_tip(msg,Se, nil, msg_duration,victim_comm,"beep_1","gr") else instance:PushToChannel(victim_comm, {Mg=msg,Ic=victim_comm,Snd="beep_1",Se=Se,It="gr"}) end d_printf(">>> msg: '"..msg.."'") return end -- NPC has seen weapon that was saved in .watches_out_for function SeenActorHitWeapon(sender, victim_comm) if not sender then return end local msg_type = "SeenActorHitWeapon" local sender_id = sender:id() if active_message_cooldown(sender_id) or active_message_cooldown(msg_type) then return end d_printf(">>> AFFS Dyn News: "..msg_type.." - call") set_message_cooldown(sender_id) set_message_cooldown(msg_type) local Se = strformat("%s, %s",sender:character_name(),dynamic_news_helper.GetCommunityDescription(sender,6)) local str_tbl = { [1] = "st_dyn_news_res_fake_death_stalker_101", [2] = "st_dyn_news_res_artefact_6", [3] = "st_dyn_news_builder_ending_12", [4] = "st_dyn_news_res_enemy_activity_stalker_1", [5] = "st_surrender_victim_answer_esc_m_trader_bounty_3", [6] = "dm_dialog_companion_final_words_bad_alone_2", } -- "st_dyn_news_res_fake_death_stalker_101" Well, well! What do we have here? -- "st_dyn_news_res_artefact_6" Son of a bitch... -- "st_dyn_news_builder_ending_12" I'd stay away if I were you. -- "st_dyn_news_res_enemy_activity_stalker_1" Someone is looking for trouble. -- "st_surrender_victim_answer_esc_m_trader_bounty_3" Fuck you! -- "dm_dialog_companion_final_words_bad_alone_2" Woah, man! What the hell is your problem? local msg = game.translate_string(str_tbl[math.random(#str_tbl)]) -- local instance = dynamic_news_manager.get_dynamic_news() -- if not instance then dynamic_news_helper.send_tip(msg,Se, nil, msg_duration,victim_comm,"beep_1","gr") -- else -- instance:PushToChannel(victim_comm, {Mg=msg,Ic=victim_comm,Snd="beep_1",Se=Se,It="gr"}) -- end d_printf(">>> msg: '"..msg.."'") return end -- On NPC seeing actor armed while under suspicion function SeenActorWeapon(sender, victim_comm) if not sender then return end local msg_type = "SeenActorWeapon" local sender_id = sender:id() if active_message_cooldown(sender_id) or active_message_cooldown(msg_type) then return end d_printf(">>> AFFS Dyn News: "..msg_type.." - call") set_message_cooldown(sender_id) set_message_cooldown(msg_type) local Se = strformat("%s, %s",sender:character_name(),dynamic_news_helper.GetCommunityDescription(sender,6)) local str_tbl = { [1] = "st_dyn_news_res_fake_death_stalker_8", [2] = "st_dyn_news_builder_ending_8", [3] = "st_dyn_news_surge_builder_end_bandit_5", [4] = "st_dyn_news_res_fake_death_stalker_102", [5] = "st_surrender_victim_answer_zat_b7_bandit_boss_sultan_bounty_5", [6] = "dm_dialog_companion_actor_reply_to_join_4", } -- "st_dyn_news_res_fake_death_stalker_8" Yeeeah... --> SurrenderInit -- "st_dyn_news_builder_ending_8" Watch out. -- "st_dyn_news_surge_builder_end_bandit_5" Better hide your ass -- "st_dyn_news_res_fake_death_stalker_102" Well, well... --> SurrenderInit -- "st_surrender_victim_answer_zat_b7_bandit_boss_sultan_bounty_5" ... -- "dm_dialog_companion_actor_reply_to_join_4" Get lost! local msg = game.translate_string(str_tbl[math.random(#str_tbl)]) -- local instance = dynamic_news_manager.get_dynamic_news() -- if not instance then dynamic_news_helper.send_tip(msg,Se, nil, msg_duration,victim_comm,"beep_1","gr") -- else -- instance:PushToChannel(victim_comm, {Mg=msg,Ic=victim_comm,Snd="beep_1",Se=Se,It="gr"}) -- end d_printf(">>> msg: '"..msg.."'") return end -- On NPC seeing actor initiating surrender function SeenActorInitSurrender(sender, victim_comm) if not sender then return end local msg_type = "SeenActorInitSurrender" local sender_id = sender:id() if active_message_cooldown(sender_id) or active_message_cooldown(msg_type) then return end d_printf(">>> AFFS Dyn News: "..msg_type.." - call") set_message_cooldown(sender_id) set_message_cooldown(msg_type) local Se = strformat("%s, %s",sender:character_name(),dynamic_news_helper.GetCommunityDescription(sender,6)) local str_tbl = { [1] = "st_dyn_news_res_fake_death_stalker_78", [2] = "st_dyn_news_res_fake_death_mutant_49", [3] = "st_dyn_news_gossip_hostage_4", [4] = "st_dyn_news_gossip_kill_wounded_8", [5] = "st_surrender_victim_answer_esc_m_trader_bounty_3", [6] = "dm_dialog_companion_actor_reply_to_join_2", [7] = "st_dyn_news_res_fake_death_stalker_83", } -- "st_dyn_news_res_fake_death_stalker_78" So, has anyone had time to get his stuff or not? -- "st_dyn_news_res_fake_death_mutant_49" What the hell is going on...? -- "st_dyn_news_gossip_hostage_4" I don't think your friends are coming for you! -- "st_dyn_news_gossip_kill_wounded_8" Surrender, asshole! We know you are wounded! -- "st_surrender_victim_answer_esc_m_trader_bounty_3" Fuck you! -- "dm_dialog_companion_actor_reply_to_join_2" Fine. But if you die, I'm taking your shit. -- "st_dyn_news_res_fake_death_stalker_83" That's how it is. local msg = game.translate_string(str_tbl[math.random(#str_tbl)]) -- local instance = dynamic_news_manager.get_dynamic_news() -- if not instance then dynamic_news_helper.send_tip(msg,Se, nil, msg_duration,victim_comm,"beep_1","gr") -- else -- instance:PushToChannel(victim_comm, {Mg=msg,Ic=victim_comm,Snd="beep_1",Se=Se,It="gr"}) -- end d_printf(">>> msg: '"..msg.."'") return end -- On NPC ackknowledging actor surrender function AcknowledgedActorSurrender(sender, victim_comm) if not sender then return end local msg_type = "AcknowledgedActorSurrender" local sender_id = sender:id() if active_message_cooldown(sender_id) or active_message_cooldown(msg_type) then return end d_printf(">>> AFFS Dyn News: "..msg_type.." - call") set_message_cooldown(sender_id) set_message_cooldown(msg_type) local Se = strformat("%s, %s",sender:character_name(),dynamic_news_helper.GetCommunityDescription(sender,6)) local str_tbl = { [1] = "st_dyn_news_res_enemy_activity_mutant_1_8", [2] = "st_dyn_news_res_fake_death_stalker_53", [3] = "st_dyn_news_res_enemy_activity_stalker_9", [4] = "st_dyn_news_res_enemy_activity_stalker_3", [5] = "st_surrender_actor_1", [6] = "st_surrender_actor_2", [7] = "st_surrender_victim_answer_5", [8] = "dm_dialog_companion_actor_reply_to_join_1", [9] = "axr_phrase_actor_give_command_ignore_combat", } -- "st_dyn_news_res_enemy_activity_mutant_1_8" Carry on, son. --> AcknowledgedActorSurrender -- "st_dyn_news_res_fake_death_stalker_53" I bet my ass this ain't gonna be no good... -- "st_dyn_news_res_enemy_activity_stalker_9" If he tries something stupid, open fire at once. --> AcknowledgedActorSurrender -- "st_dyn_news_res_enemy_activity_stalker_3" If he tries something funny, he's gonna pay for it. --> AcknowledgedActorSurrender -- "st_surrender_actor_1" You'll live. -- "st_surrender_actor_2" Good luck... -- "st_surrender_victim_answer_5" Go fuck yourself! -- "dm_dialog_companion_actor_reply_to_join_1" Just focus on where you aim with those guns, then. -- "axr_phrase_actor_give_command_ignore_combat" Avoid shooting at anyone for now. local msg = game.translate_string(str_tbl[math.random(#str_tbl)]) local instance = dynamic_news_manager.get_dynamic_news() if not instance then dynamic_news_helper.send_tip(msg,Se, nil, msg_duration,victim_comm,"beep_1","gr") else instance:PushToChannel(victim_comm, {Mg=msg,Ic=victim_comm,Snd="beep_1",Se=Se,It="gr"}) end d_printf(">>> msg: '"..msg.."'") return end -- On NPC seeing actor armed again in non-hostile environment function SeenActorUnSurrender(sender, victim_comm) if not sender then return end local msg_type = "SeenActorUnSurrender" local sender_id = sender:id() if active_message_cooldown(sender_id) or active_message_cooldown(msg_type) then return end d_printf(">>> AFFS Dyn News: "..msg_type.." - call") set_message_cooldown(sender_id) set_message_cooldown(msg_type) local Se = strformat("%s, %s",sender:character_name(),dynamic_news_helper.GetCommunityDescription(sender,6)) local str_tbl = { [1] = "st_dyn_news_res_artefact_9", [2] = "st_dyn_news_gossip_kill_wounded_4", [3] = "st_dyn_news_res_death_stalker_33", [4] = "st_dyn_news_res_fake_death_stalker_1", [5] = "st_surrender_actor_3", [6] = "dm_dialog_companion_final_words_bad_2", [7] = "stalker_stitch_join_eidolon_1", } -- "st_dyn_news_res_artefact_9" You serious?! --> Unsurrender -- "st_dyn_news_gossip_kill_wounded_4" Time to put him down for good. --> Unsurrender -- "st_dyn_news_res_death_stalker_33" Here we go again... -- "st_dyn_news_res_fake_death_stalker_1" What in hell?! -- "st_surrender_actor_3" Time to end you. -- "dm_dialog_companion_final_words_bad_2" Kill this asshole! -- "stalker_stitch_join_eidolon_1" All right, bring it on! local msg = game.translate_string(str_tbl[math.random(#str_tbl)]) -- local instance = dynamic_news_manager.get_dynamic_news() -- if not instance then dynamic_news_helper.send_tip(msg,Se, nil, msg_duration,victim_comm,"beep_1","gr") -- else -- instance:PushToChannel(victim_comm, {Mg=msg,Ic=victim_comm,Snd="beep_1",Se=Se,It="gr"}) -- end d_printf(">>> msg: '"..msg.."'") return end -- On witness reporting before Goodwill Reset function ReportingAttack(sender, victim_comm) if not sender then return end local msg_type = "ReportingAttack" local sender_id = sender:id() if active_message_cooldown(sender_id) or active_message_cooldown(msg_type) then return end d_printf(">>> AFFS Dyn News: "..msg_type.." - call") set_message_cooldown(sender_id) set_message_cooldown(msg_type) local Se = strformat("%s, %s",sender:character_name(),dynamic_news_helper.GetCommunityDescription(sender,6)) local str_tbl = { [1] = "st_dyn_news_surge_builder_end_killer_4", [2] = "st_dyn_news_res_artefact_12", [3] = "st_dyn_news_res_fake_death_mutant_42", [4] = "st_dyn_news_res_fake_death_stalker_55", [5] = "st_dyn_news_res_found_stash_1", [6] = "st_dyn_news_res_fake_death_stalker_40", [7] = "st_dyn_news_res_fake_death_stalker_50", [8] = "st_dyn_news_res_fake_death_stalker_75", [9] = "st_dyn_news_res_fake_death_stalker_86", } -- "st_dyn_news_surge_builder_end_killer_4" You've been warned -- "st_dyn_news_res_artefact_12" You probably just made yourself several enemies. -- "st_dyn_news_res_fake_death_mutant_42" Well, fuck that guy! -- "st_dyn_news_res_fake_death_stalker_55" I had a bad feeling about that guy. Good thing I wasn't nearby. -- "st_dyn_news_res_found_stash_1" Well, sucks to be you. -- "st_dyn_news_res_fake_death_stalker_40" He screwed up... -- "st_dyn_news_res_fake_death_stalker_50" I'm gonna put that fucking asshole in a coffin! -- "st_dyn_news_res_fake_death_stalker_75" Sad to see this happening... -- "st_dyn_news_res_fake_death_stalker_86" That's the end of you, scum. local msg = game.translate_string(str_tbl[math.random(#str_tbl)]) -- local instance = dynamic_news_manager.get_dynamic_news() -- if not instance then dynamic_news_helper.send_tip(msg,Se, nil, msg_duration,victim_comm,"beep_1","gr") -- else -- instance:PushToChannel(victim_comm, {Mg=msg,Ic=victim_comm,Snd="beep_1",Se=Se,It="gr"}) -- end d_printf(">>> msg: '"..msg.."'") return end ------------------------------------------------------------ -- Helpers for HUD ------------------------------------------------------------ function currently_aggravating() if not AREA_STATS.last_comm then return end local faction = AREA_STATS.last_comm if not faction then return end if not dfrc_tgr[faction] then return end if not dfrc_tgr[faction].hit_v then return end if not dfrc_tgr[faction].hit_v == 0 then return end return true end function get_current_aggravation_value() local faction = AREA_STATS.last_comm if not faction then return 0 end if not dfrc_tgr[faction] then return 0 end local factor = 1 if settings["faction_dissatisfaction_limit"] > 100 then factor = 100 / settings["faction_dissatisfaction_limit"] end local val = math.floor(dfrc_tgr[faction].hit_v * factor) return val end ------------------------------------------------------------ -- HUD -- (adapted from gameplay_disguise) ------------------------------------------------------------ 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_aggravation") --local hud_d = hud:GetCustomStatic("bar_disguise") local wnd if (not currently_aggravating()) or (db.actor:is_talking()) or not show_hud_bar or settings["disable_hud_bar"] then if (hud_d ~= nil) then hud:RemoveCustomStatic("bar_aggravation") --hud:RemoveCustomStatic("bar_disguise") hud_d = nil end return end if (hud_d == nil) then hud:AddCustomStatic("bar_aggravation",true) --hud:AddCustomStatic("bar_disguise",true) hud_d = hud:GetCustomStatic("bar_aggravation") --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 = get_current_aggravation_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