Added New Mods and Profiles Folders

This is a complete rebuild of the modpack, with all new mods and updates for 1.5.3 of Anomaly.
This commit is contained in:
2025-01-14 05:07:53 -05:00
parent 85c665b107
commit 376b4b9689
21217 changed files with 546254 additions and 0 deletions
@@ -0,0 +1,487 @@
-- Generic panic scheme for arbitrary panic states
-- Checks db.storage[<npc_id>].aoe_panic to evaluate
-- Used by AOE classes, hence the name, but can be used for any arbitrary danger
-- Based on axr_stalker_panic
local enable_debug = false
local print_tip = function(s, ...)
local f = print_tip or printf
if enable_debug then
return f("AOE Panic: " .. s, ...)
end
end
-- Check if dir2 is to the left of dir1
local function angle_left_xz(dir1, dir2)
local dir1 = vector():set(dir1.x, 0, dir1.z)
local dir2 = vector():set(dir2.x, 0, dir2.z)
local dir_res = vector():set(VEC_ZERO):crossproduct(dir1, dir2)
return dir_res.y <= 0
end
actid = 188121
evaid = 188121
-- Default panic check function, you can use this when checking need to panic or define your own
function can_panic(npc)
return npc
and IsStalker(npc)
and npc:id() ~= AC_ID
and npc.alive and npc:alive()
and not (
character_community(npc) == "zombied"
or npc:section() == "actor_visual_stalker"
)
end
function npc_init_aoe_tables(obj_id)
if obj_id == AC_ID then return end
if not db.storage[obj_id] then
print_tip("npc %s storage not exists", obj_id)
return
end
if db.storage[obj_id].aoe_panic == nil then db.storage[obj_id].aoe_panic = false end
if db.storage[obj_id].active_aoe_panic == nil then db.storage[obj_id].active_aoe_panic = {} end
-- print_tip("npc %s aoe panic init", obj_id)
return true
end
function npc_update_panic_state(obj_id)
if not npc_init_aoe_tables(obj_id) then return end
db.storage[obj_id].aoe_panic = size_table(db.storage[obj_id].active_aoe_panic) > 0
print_tip("npc %s panic %s", obj_id, db.storage[obj_id].aoe_panic)
end
function npc_add_aoe_panic(obj_id, AOE_class, time, danger_dir, danger_pos, panic_sound_interval, target_pos, npc_state)
if not npc_init_aoe_tables(obj_id) then return end
if panic_sound_interval == nil then
panic_sound_interval = true
end
local lifetime = time_global() + (time or 0) * 1000
if db.storage[obj_id].active_aoe_panic[AOE_class] then
if time and not db.storage[obj_id].active_aoe_panic[AOE_class].time then
npc_remove_aoe_panic_deferred(obj_id, AOE_class)
end
db.storage[obj_id].active_aoe_panic[AOE_class].time = time and lifetime
db.storage[obj_id].danger_dir = danger_dir
db.storage[obj_id].danger_pos = danger_pos
db.storage[obj_id].target_pos = target_pos
db.storage[obj_id].npc_state = npc_state
db.storage[obj_id].panic_sound_interval = panic_sound_interval == true and 2 or panic_sound_interval
return
end
db.storage[obj_id].active_aoe_panic[AOE_class] = {
time = time and lifetime
}
db.storage[obj_id].danger_dir = danger_dir
db.storage[obj_id].danger_pos = danger_pos
db.storage[obj_id].target_pos = target_pos
db.storage[obj_id].npc_state = npc_state
db.storage[obj_id].panic_sound_interval = panic_sound_interval == true and 2 or panic_sound_interval
npc_update_panic_state(obj_id)
if time then npc_remove_aoe_panic_deferred(obj_id, AOE_class) end
end
function npc_remove_aoe_panic(obj_id, AOE_class, force)
if not npc_init_aoe_tables(obj_id) then return end
if not db.storage[obj_id].active_aoe_panic[AOE_class] then return end
if force
or not db.storage[obj_id].active_aoe_panic[AOE_class].time
or (
db.storage[obj_id].active_aoe_panic[AOE_class].time
and time_global() > db.storage[obj_id].active_aoe_panic[AOE_class].time
) then
db.storage[obj_id].active_aoe_panic[AOE_class] = nil
npc_update_panic_state(obj_id)
return true
end
end
-- This will remove aoe panic when time is elapsed, checking on each game update
function npc_remove_aoe_panic_deferred(obj_id, AOE_class)
if not npc_init_aoe_tables(obj_id) then return end
if not db.storage[obj_id].active_aoe_panic[AOE_class] then return end
AddUniqueCall(function()
return npc_remove_aoe_panic(obj_id, AOE_class)
end)
end
class "evaluator_stalker_aoe_panic" (property_evaluator)
function evaluator_stalker_aoe_panic:__init(npc,name,storage) super (nil, name)
self.st = storage
self.st.stage = 0
end
function evaluator_stalker_aoe_panic:evaluate()
--utils_data.debug_write("eva_panic")
local npc = self.object
if not npc:alive() then
return false
end
if IsWounded(npc) then
return false
end
local st = db.storage[npc:id()]
if st and st.aoe_panic then
return true
end
return false
end
class "action_stalker_aoe_panic" (action_base)
function action_stalker_aoe_panic:__init (npc,name,storage) super (nil,name)
self.st = storage
end
function action_stalker_aoe_panic:initialize()
action_base.initialize(self)
local npc = self.object
self.movement_type = npc:movement_type()
self.body_state = npc:body_state()
self.mental_state = npc:mental_state()
self.path = npc:path_type()
npc:set_desired_position()
npc:set_desired_direction()
db.storage[npc:id()].panicked = true
self.first_update = true
-- Minus value means instant reaction, the movement would still be inert a bit
self.panic_threshold = math.random(100, 300)
self.panic_time = time_global()
end
function action_stalker_aoe_panic:validate(vid, force)
local npc = self.object
return vid and vid < 4294967295 and vid > 0 -- Is existing lvid
and npc:accessible(vid) -- Accessible by npc
and vid ~= npc:level_vertex_id() -- Not current npc lvid
and force or (
not db.used_level_vertex_ids[vid] -- Not taken by another entity
and math.abs(level.vertex_position(vid).y - npc:position().y) < 2 -- Not higher or lower from npc position than this threshold so npcs wont run vertically
)
end
function action_stalker_aoe_panic:lmove(vid, force)
-- Return if not valid
if not self:validate(vid, force) then
return
end
local npc = self.object
local st = self.st
if (st.vid) then
db.used_level_vertex_ids[st.vid] = nil
st.vid = nil
end
db.used_level_vertex_ids[vid] = npc:id()
npc:set_dest_level_vertex_id(vid)
st.vid = vid
return vid
end
function action_stalker_aoe_panic:try_go_away()
local npc = self.object
local id = npc:id()
local base_point = npc:level_vertex_id()
local tries = 5
if db.storage[id].target_pos then
local lvid = level.vertex_id(db.storage[id].target_pos)
return self:lmove(lvid, true)
end
for i = 1, self.initial_dir and tries * 2 or tries do
-- If cant find lvid in initial direction after <tries> tries - invalidate it
if i > tries then self.initial_dir = nil end
-- Check lvid in npc direction
-- If has information about danger - run away from it
-- If already run towards initial direction - continue in same direction until invalidation
local dir = npc:direction()
local danger_dir = db.storage[id].danger_dir
danger_dir = danger_dir and vector():set(danger_dir.x, dir.y, danger_dir.z):normalize()
local danger_pos = db.storage[id].danger_pos
-- Case 1: danger_dir and danger_pos are known, run away from danger in about 90 degrees from it
if danger_dir and danger_pos then
local npc_to_danger = vector():set(npc:position()):sub(danger_pos):normalize()
if angle_left_xz(danger_dir, npc_to_danger) then
away_dir = vector_rotate_y(danger_dir, 90)
else
away_dir = vector_rotate_y(danger_dir, -90)
end
print_tip("npc %s has danger_dir and pos", npc:name())
-- demonized_geometry_ray.VisualizeRay(npc:position():add(vector():set(0, 1, 0)), vector():mad(npc:position():add(vector():set(0, 1, 0)), away_dir, 3), nil, 300)
-- Case 2: danger_pos is known, maximize distance away from it
elseif danger_pos then
local npc_to_danger = vector():set(npc:position()):sub(danger_pos):normalize()
away_dir = vector_rotate_y(npc_to_danger, random_choice(-20, 20))
print_tip("npc %s has danger_pos", npc:name())
-- demonized_geometry_ray.VisualizeRay(npc:position():add(vector():set(0, 1, 0)), vector():mad(npc:position():add(vector():set(0, 1, 0)), away_dir, 3), nil, 300)
-- Case 3: danger_dir is known, choose random 90 degree direction
elseif danger_dir then
away_dir = vector_rotate_y(danger_dir, random_choice(90, -90))
print_tip("npc %s has danger_dir", npc:name())
-- demonized_geometry_ray.VisualizeRay(npc:position():add(vector():set(0, 1, 0)), vector():mad(npc:position():add(vector():set(0, 1, 0)), away_dir, 3), nil, 300)
-- Case 4: no information, just run somewhere
else
away_dir = vector_rotate_y(dir, random_choice(120, -120))
end
-- Randomize direction a bit
away_dir = vector_rotate_y(away_dir, random_float(-30, 30))
-- Set new lvid and direction
local dir = self.initial_dir and vector_rotate_y(self.initial_dir, random_float(-30, 30)) or away_dir
for radius = 10, 1, -1 do
local lvid = level.vertex_in_direction(base_point, dir, radius)
if self:validate(lvid) then
-- If found lvid - invalidate dangers, next update pick in initial direction or randomly
if not self.initial_dir then
db.storage[id].danger_dir = nil
db.storage[id].danger_pos = nil
end
self.initial_dir = level.vertex_position(lvid):sub(npc:position()):normalize()
-- demonized_geometry_ray.VisualizeRay(npc:position():add(vector():set(0, 1, 0)), vector():mad(npc:position():add(vector():set(0, 1, 0)), self.initial_dir, 3), nil, 300)
return self:lmove(lvid)
end
end
end
end
function action_stalker_aoe_panic:execute()
--utils_data.debug_write(strformat("action_stalker_aoe_panic:execute start"))
action_base.execute(self)
-- Check reaction time first
if time_global() - self.panic_time < self.panic_threshold then
print_tip("npc hasn't reacted yet, pt %s, pthr %s", time_global() - self.panic_time, self.panic_threshold)
return
end
local npc = self.object
local id = npc:id()
--printf("enemy = %s",enemy and enemy:name())
-- ensure and enforce path type
if (npc:path_type() ~= game_object.level_path) then
npc:set_path_type(game_object.level_path)
end
-- Set panic state
local bw = npc:active_item()
local new_state = db.storage[id].npc_state or (bw and "sprint" or "panic")
local lvid
-- Find new lvid to reach
if self.st.vid then
if (npc:level_vertex_id() == self.st.vid) then
print_tip("npc reached lvid %s, switch", self.st.vid)
lvid = self:try_go_away()
print_tip("npc switched to new lvid %s", lvid)
else
npc:set_dest_level_vertex_id(self.st.vid)
lvid = self.st.vid
-- print_tip("npc trying to reach lvid %s", lvid)
end
else
lvid = self:try_go_away()
print_tip("npc init new lvid %s", lvid)
end
-- Set sight in direction of new lvid and confirm the state
--[[
npc:set_sight(look.direction, lvid and level.vertex_position(lvid):sub(npc:position()):normalize() or npc:direction())
state_mgr.set_state(npc, new_state, nil, nil, {
look_position = lvid and level.vertex_position(lvid) or npc:position(),
look_object = lvid,
look_dir = lvid and level.vertex_position(lvid):sub(npc:position()):normalize() or npc:direction(),
}, {
fast_set = true,
animation = false,
})
--]]
-- npc:set_sight(look.direction, lvid and level.vertex_position(lvid):sub(npc:position()):normalize() or npc:direction())
state_mgr.set_state(npc, new_state, nil, nil, nil, {
fast_set = true,
animation = false,
})
if db.storage[id].panic_sound_interval then
local tg = time_global()
local ptg = db.storage[id].panic_sound_tg or 0
local interval = db.storage[id].panic_sound_interval * 1000
if tg - ptg > interval then
local t = {"panic_monster", "panic_human"}
xr_sound.set_sound_play(id, t[math.random(#t)])
db.storage[id].panic_sound_tg = tg + interval
end
end
-- First update force movement
if self.first_update then
npc:clear_animations()
npc:movement_enabled(true)
npc:set_movement_type(move.run)
npc:set_body_state(move.standing)
npc:set_mental_state(anim.panic)
self.first_update = false
end
end
function action_stalker_aoe_panic:finalize()
action_base.finalize(self)
self.first_update = true
self.initial_dir = nil
if (self.st.vid) then
db.used_level_vertex_ids[self.st.vid] = nil
end
self.st.vid = nil
db.storage[self.object:id()].panicked = nil
self.panic_time = time_global()
local npc = self.object
npc:clear_animations()
npc:movement_enabled(true)
npc:set_movement_type(self.movement_type)
npc:set_body_state(self.body_state)
npc:set_mental_state(self.mental_state)
-- npc:set_path_type(self.path)
end
function setup_generic_scheme(npc,ini,scheme,section,stype,temp)
local st = xr_logic.assign_storage_and_bind(npc,ini,"stalker_aoe_panic",section,temp)
end
function add_to_binder(npc,ini,scheme,section,storage,temp)
if not npc then return end
local manager = npc:motivation_action_manager()
if not manager then return end
if not npc:alive() or npc:section() == "actor_visual_stalker" then
manager:add_evaluator(evaid,property_evaluator_const(false))
temp.needs_configured = false
return
end
local evaluator = evaluator_stalker_aoe_panic(npc,"eva_stalker_aoe_panic",storage)
temp.action = action_stalker_aoe_panic(npc,"act_stalker_aoe_panic",storage)
if not evaluator or not temp.action then return end
manager:add_evaluator(evaid,evaluator)
temp.action:add_precondition(world_property(stalker_ids.property_alive,true))
-- temp.action:add_precondition(world_property(stalker_ids.property_danger, false))
temp.action:add_precondition(world_property(evaid,true))
temp.action:add_effect(world_property(evaid,false))
manager:add_action(actid,temp.action)
--xr_logic.subscribe_action_for_events(npc, storage, temp.action)
end
function configure_actions(npc,ini,scheme,section,stype,temp)
if not npc then return end
local manager = npc:motivation_action_manager()
if not manager or not temp.action then return end
temp.action:add_precondition(world_property(xr_evaluators_id.sidor_wounded_base,false))
temp.action:add_precondition(world_property(xr_evaluators_id.wounded_exist,false))
-- if (_G.schemes["rx_ff"]) then
-- temp.action:add_precondition(world_property(rx_ff.evaid,false))
-- end
if (_G.schemes["gl"]) then
temp.action:add_precondition(world_property(rx_gl.evid_gl_reload,false))
end
-- if (_G.schemes["facer"]) then
-- temp.action:add_precondition(world_property(xrs_facer.evid_facer,false))
-- temp.action:add_precondition(world_property(xrs_facer.evid_steal_up_facer,false))
-- end
local action
local p = {xr_danger.actid, stalker_ids.action_combat_planner, stalker_ids.action_danger_planner, xr_actions_id.state_mgr + 2, xr_actions_id.alife}
for i=1,#p do
--printf("ACTION_ALIFE_ID(demonized_stalker_aoe_panic.configure_actions): " .. tostring(p[i]))
action = manager:action(p[i])
if (action) then
action:add_precondition(world_property(evaid,false))
else
printf("axr_panic: no action id p[%s]",i)
end
end
end
function disable_generic_scheme(npc,scheme,stype)
local st = db.storage[npc:id()][scheme]
if st then
st.enabled = false
end
end
function npc_add_precondition(action)
if not action then return end
action:add_precondition(world_property(evaid,false))
end
LoadScheme("demonized_stalker_aoe_panic", "stalker_aoe_panic", modules.stype_stalker)
-- Disable aggroing on panic
function on_enemy_eval(obj, enemy, flags)
if not IsStalker(obj) then return end
local id = obj:id()
if id == AC_ID then return end
if db.storage[id] and db.storage[id].aoe_panic then
flags.override = true
flags.ret_value = false
end
end
function on_game_start()
RegisterScriptCallback("on_enemy_eval", on_enemy_eval)
end
-- Disable talk
process_npc_usability = xr_meet.process_npc_usability
xr_meet.process_npc_usability = function(npc, ...)
local id = npc:id()
if db.storage[id] and db.storage[id].aoe_panic then
npc:disable_talk()
return
end
return process_npc_usability(npc, ...)
end
-- Disable npc evaluation
evaluator_contact_evaluate = xr_meet.evaluator_contact.evaluate
xr_meet.evaluator_contact.evaluate = function(self, ...)
local id = self.object:id()
if db.storage[id] and db.storage[id].aoe_panic then
return false
end
return evaluator_contact_evaluate(self, ...)
end
@@ -0,0 +1,560 @@
local squads_t = {}
local flee_smarts = {}
local smoke_parts = {}
------------------------------------ things to change ---------------------------------
-- power mults for stalker its (rank*comm); for mutant only last table
local rank_to_val = {
["novice"] = 0.6, ["trainee"] = 0.6, ["experienced"] = 0.9, ["professional"] = 0.9,
["veteran"] = 1.15, ["expert"] = 1.15, ["master"] = 1.4, ["legend"] = 1.4,
}
local comm_to_val = {
["zombied"] = 0.7, ["bandit"] = 0.8, ["renegade"] = 0.8, ["stalker"] = 0.9, ["csky"] = 0.9, ["ecolog"] = 0.9,
["dolg"] = 1, ["freedom"] = 1, ["army"] = 1.1, ["killer"] = 1.2, ["greh"] = 1.2, ["isg"] = 1.3, ["monolith"] = 1.3,
}
local mutant_to_val = {
[clsid["tushkano_s"]] = 0.1, [clsid["zombie_s"]] = 0.3, [clsid["dog_s"]] = 0.3, [clsid["flesh_s"]] = 0.5, [clsid["fracture_s"]] = 0.5, [clsid["boar_s"]] = 0.75, [clsid["pseudodog_s"]] = 0.75,
[clsid["cat_s"]] = 0.75, [clsid["snork_s"]] = 0.75, ["SM_KARLIK"] = 0.75, [clsid["poltergeist_s"]] = 1, ["SM_LURKER"] = 1.5, [clsid["psy_dog_s"]] = 2,
["SM_PSYSUCKER"] = 2.5, [clsid["bloodsucker_s"]] = 2.5, [clsid["burer_s"]] = 2.5, [clsid["chimera_s"]] = 3.5, [clsid["controller_s"]] = 4, [clsid["gigant_s"]] = 5,
}
-- ignore list for comms that do not flee
local ignore_flee = {
["zombied"] = true, ["monolith"] = true,
}
-- chance to use smoke grenade (every npc in squad is checked, 1 is 100%)
local smoke_ranks = {
["novice"] = 0.01, ["trainee"] = 0.03, ["experienced"] = 0.1, ["professional"] = 0.15,
["veteran"] = 0.25, ["expert"] = 0.3, ["master"] = 0.4, ["legend"] = 0.4,
}
---------------------------------------------------------------------------------------
-- start flee if (friends_power * threshold_mult < enemies_power)
local threshold_mult = npc_fleeing_mcm.get_config("threshold_mult")
-- powers multipliers (goes to friends_power and enemies_power)
local enemies_pwr_mult = 1 --npc_fleeing_mcm.get_config("enemies_pwr_mult")
local friends_pwr_mult = 1 --npc_fleeing_mcm.get_config("friends_pwr_mult")
local companion_pwr_mult = npc_fleeing_mcm.get_config("companion_pwr")
-- iterate through enemies/friends radius when NOT fleeing, then if enemy power is greater - start flee
local enemies_radius = npc_fleeing_mcm.get_config("enemies_radius")
local friends_radius = npc_fleeing_mcm.get_config("friends_radius")
-- iterate through enemies/friends radius when fleeing, then if enemy power is less - cancel flee
local enemies_upd_radius = npc_fleeing_mcm.get_config("enemies_upd_radius")
local friends_upd_radius = npc_fleeing_mcm.get_config("friends_upd_radius")
-- some other stuff
local start_flee_time = npc_fleeing_mcm.get_config("start_flee_time") -- guaranteed flee time after it starts (cant be cancelled)
local flee_protection_time = npc_fleeing_mcm.get_config("flee_protection_time") -- seconds between new flee (set 0 for instant flees one after another when possible)
local cancel_on_hit = npc_fleeing_mcm.get_config("cancel_on_hit") -- cancel flee on hit (only after "start_flee_time")
local hits_to_cancel = npc_fleeing_mcm.get_config("hits_to_cancel") -- amount of hits needed to cancel flee
local flee_smart_min_dist = npc_fleeing_mcm.get_config("flee_smart_min_dist") -- pick smart to flee that at least further than this value
local cover_time = npc_fleeing_mcm.get_config("cover_time")
local smoke_enabled = npc_fleeing_mcm.get_config("smoke_enabled")
-- do not touch for now
local flee_time = 600
local start_flee_check = false -- delay for first update, because somehow they dont see friends for a few first seconds
local flee_dbg = npc_fleeing_mcm.get_config("flee_dbg")
local print_lists_dbg = false
local immortal_npcs = false
function squad_on_update(squad)
-- small delay for loading
if not start_flee_check then
return
end
-- do only for online squads
if not (squad.online) then
squads_t[squad.id] = nil
return
end
-- do only for simulation stalkers
if (ignore_flee[squad.player_id]) or (string.find(squad.player_id, "monster")) or (not string.find(squad:section_name(), "sim_squad")) then
return
end
-- do only if not surge (and cancel flee)
if xr_conditions.surge_started() then
cancel_flee(squad, squads_t[squad.id] and squads_t[squad.id].fleeing, "surge started")
return
end
-- check update timer (2 sec per squad)
if squads_t[squad.id] and squads_t[squad.id].tmr and ( time_global() - squads_t[squad.id].tmr < 2000 ) then
return
end
-- store/update the squad properties (timer, powers and enemy)
squads_t[squad.id] = squads_t[squad.id] or {}
squads_t[squad.id].tmr = time_global()
-- if fleeing - update flee state and return
if squads_t[squad.id].fleeing then
update_flee(squad)
pr2("~----------------------------------upd")
return
end
-- flee to another smart
start_flee(squad)
pr2("~-------------------------------end")
end
function update_flee(squad)
-- start updates only after flee started + start_flee_time
if squads_t[squad.id].start_updates_tmr and ( time_global() - squads_t[squad.id].start_updates_tmr < 1000 * start_flee_time ) then
return
end
squads_t[squad.id].start_updates_tmr = nil
-- cancel flee if at least one squad member is close to smart target
local smart = squads_t[squad.id].smart_id and alife_object(squads_t[squad.id].smart_id)
for m in squad:squad_members() do
local npc = level.object_by_id(m.id)
if valid_obj(npc) and smart and npc:position():distance_to(smart.position) < 15 then
cancel_flee(squad, true, "smart reached")
return
end
end
-- cancel flee if power is greater than enemies (to fight weak enemies on the way)
local enemy_pwr, friend_pwr, be_squad_id = get_powers(squad, enemies_upd_radius, friends_upd_radius)
if be_squad_id and friend_pwr * threshold_mult >= enemy_pwr then
cancel_flee(squad, true, "weak enemies met", enemy_pwr, friend_pwr)
return
end
-- make nearest npc in squad to cover the rest
local function stop_covering(obj_id, squad_id)
local cvr_npc = level.object_by_id(obj_id)
if valid_obj(cvr_npc, true) and squads_t[squad_id] then
squads_t[squad_id].npc_cover_id = nil
end
return true
end
local cover_id = squads_t[squad.id].npc_cover_id
local cover_npc = cover_id and level.object_by_id(cover_id)
if valid_obj(cover_npc, true) then
CreateTimeEvent("flee_cover_e" .. cover_id, "flee_cover_a" .. cover_id, cover_time, stop_covering, cover_id, squad.id)
demonized_stalker_aoe_panic.npc_remove_aoe_panic(cover_id, "npc_flee" .. cover_id, true)
end
-- change flee state between panic and assault based on distance to best enemy
local enemy = be_squad_id and alife_object(be_squad_id)
for m in squad:squad_members() do
local npc = level.object_by_id(m.id)
if valid_obj(npc) and (not cover_id or cover_id ~= m.id) then
local state = enemy and npc:position():distance_to(enemy.position) < 40 and "panic" or "assault"
demonized_stalker_aoe_panic.npc_add_aoe_panic(npc:id(), "npc_flee" .. npc:id(), flee_time, nil, enemy and enemy.position, false, smart and smart.position, state)
end
end
end
function start_flee(squad)
-- protection from another instant flee
if squads_t[squad.id].no_flee_time and (time_global() - squads_t[squad.id].no_flee_time < 1000 * flee_protection_time) then
return
end
squads_t[squad.id].no_flee_time = nil
-- get powers and calculate if enemies are greater
local enemy_pwr, friend_pwr, be_squad_id = get_powers(squad, enemies_radius, friends_radius)
if (not be_squad_id) or (friend_pwr * threshold_mult >= enemy_pwr) then return end
-- get smart to flee
local smart_name = get_smart_to_flee(squad, be_squad_id)
local smart = smart_name and SIMBOARD.smarts_by_names[smart_name]
if not smart then return end
-- set this smart as assigned target and store
squad:specific_update(smart.id)
squads_t[squad.id].smart_id = smart.id
-- start panic
local enemy = alife_object(be_squad_id)
for m in squad:squad_members() do
local npc = level.object_by_id(m.id)
if valid_obj(npc) then
pr2("~ -------------- start panic")
local state = enemy and npc:position():distance_to(enemy.position) < 80 and "panic" or "assault"
demonized_stalker_aoe_panic.npc_add_aoe_panic(npc:id(), "npc_flee" .. npc:id(), flee_time, nil, nil, false, smart.position, state)
end
end
pr("- id: %s || sec: %s", squad.id, squad:section_name())
pr("$ flee to %s || en_pwr: %s || fr_pwr: %s", smart_name, enemy_pwr, friend_pwr)
pr("~-------------------------------------------")
squads_t[squad.id].fleeing = true
squads_t[squad.id].start_updates_tmr = time_global()
squads_t[squad.id].hits = 0
-- get last npc that will cover squad
local nearest_id = get_nearest_squad_member_id(squad, smart)
if nearest_id and cover_time > 0 and squad:npc_count() > 1 then
squads_t[squad.id].npc_cover_id = nearest_id
end
-- create smoke on farthest npc position
local farthest_id = get_farthest_squad_member_id(squad, smart)
local far_npc = farthest_id and level.object_by_id(farthest_id)
if smoke_enabled and far_npc then
for m in squad:squad_members() do
local npc = level.object_by_id(m.id)
local npc_rank = valid_obj(npc) and ranks.get_obj_rank_name(npc)
if npc_rank and smoke_ranks[npc_rank] and smoke_ranks[npc_rank] >= math.random() then
smoke_parts[npc:id()] = particles_object("explosions\\explosion_dym")
smoke_parts[npc:id()]:play_at_pos(far_npc:position())
break
end
end
end
end
function cancel_flee(squad, is_fleeing, print_msg, pwr1, pwr2)
if not (squads_t[squad.id] and is_fleeing) then return end
local squad_npcs = {}
for m in squad:squad_members() do
local npc = level.object_by_id(m.id)
if valid_obj(npc) then
squad_npcs[npc:id()] = true
demonized_stalker_aoe_panic.npc_remove_aoe_panic(npc:id(), "npc_flee" .. npc:id(), true)
end
end
pr("- id: %s || sec: %s", squad.id, squad:section_name())
pr("$ stopped fleeing, %s %s %s", print_msg, pwr1 and ("|| en_pwr: " .. pwr1) or "", pwr2 and (" || fr_pwr: " .. pwr2) or "")
pr("~-------------------------------------------")
squads_t[squad.id].fleeing = nil
squads_t[squad.id].no_flee_time = time_global()
squads_t[squad.id].hits = nil
squads_t[squad.id].npc_cover_id = nil
-- test (delete npc from body)
--[[
for id, t in pairs(db.storage) do
if t.corpse_already_selected and squad_npcs[t.corpse_already_selected] then
db.storage[id].corpse_already_selected = nil
end
end
--]]
-- test (delete body from npc)
for id, _ in pairs(squad_npcs) do
if db.storage[id] and db.storage[id].corpse_detection then
db.storage[id].corpse_detection.selected_corpse_id = nil
end
end
end
function get_powers(squad, enemy_radius, friend_radius)
local enemies_t = {}
local friends_t = {}
local highest_enemy_id
local highest_enemy_power = 0
-- iterate through memory of each npc in squad
for m in squad:squad_members() do
local npc = level.object_by_id(m.id)
if valid_obj(npc) and npc.memory_visible_objects and npc:memory_visible_objects() then
-- add self
if npc:has_info("npcx_is_companion") then
friends_t[npc:id()] = friends_t[npc:id()] or get_obj_power(npc, true) or 0
else
friends_t[npc:id()] = friends_t[npc:id()] or get_obj_power(npc) or 0
end
pr2("~ ------------1")
for o in npc:memory_visible_objects() do
local obj = o and o:object()
if valid_obj(obj, true) and obj:position():distance_to(npc:position()) < enemy_radius then
local obj_power = get_obj_power(obj) or 0
-- get all enemy powers
if npc:relation(obj) >= game_object.enemy then
enemies_t[obj:id()] = enemies_t[obj:id()] or obj_power
-- save best enemy
if obj:id() ~= 0 and obj_power > highest_enemy_power then
highest_enemy_power = obj_power
local enemy_squad = get_object_squad(obj)
highest_enemy_id = enemy_squad and enemy_squad.id or highest_enemy_id
end
-- get all friend powers (with reduced range)
elseif obj:position():distance_to(npc:position()) < friend_radius then
friends_t[obj:id()] = friends_t[obj:id()] or obj_power
end
end
end
end
end
pr2("~ ------------2")
if not highest_enemy_id then return end
pr2("~ ------------3")
-- calc total powers
local enemies_power = 0
pr2(" enemies list:")
for obj_id, pwr in pairs(enemies_t) do
pr2(" enemy: %s || pwr: %s", level.object_by_id(obj_id):name(), pwr)
enemies_power = enemies_power + pwr
end
pr2("~ ------------4")
local friends_power = 0
pr2(" friends list:")
for obj_id, pwr in pairs(friends_t) do
pr2(" friend: %s || pwr: %s", level.object_by_id(obj_id):name(), pwr)
friends_power = friends_power + pwr
end
pr2("~ ------------5")
return enemies_power * enemies_pwr_mult, friends_power * friends_pwr_mult, highest_enemy_id
end
function get_obj_power(npc, is_comp)
local power = 1
-- if stalker
if IsStalker(npc) then
-- rank and community power
local rank = ranks.get_obj_rank_name(npc)
local comm = npc:character_community()
power = power * (rank and rank_to_val[rank] or 0.5) * (comm and comm_to_val[comm] or 0.7)
-- if companion
if is_comp then
power = power * companion_pwr_mult
end
-- if mutant
elseif IsMonster(npc) then
local kind = ini_sys:r_string_ex(npc:section(), "kind")
if kind and mutant_to_val[kind] then
power = power * mutant_to_val[kind]
elseif mutant_to_val[npc:clsid()] then
power = power * mutant_to_val[npc:clsid()]
else
power = power * 0.3
end
end
return power
end
function get_smart_to_flee(squad, be_squad_id)
local enemy_squad = alife_object(be_squad_id)
if not (enemy_squad and flee_smarts[squad.player_id]) then return end
local nearest_dist = 9999
local nearest_smart_name
for smart_name, _ in pairs(flee_smarts[squad.player_id]) do
local smart = SIMBOARD.smarts_by_names[smart_name]
if smart then
-- get dist from squad to this smart
local squad_dist = squad.position:distance_to(smart.position)
-- get dist from enemy to this smart
local enemy_dist = enemy_squad.position:distance_to(smart.position)
-- check if squad is closer to smart than enemy, pick smart that is "flee_smart_min_dist" meters away but nearest of those
if squad_dist and enemy_dist and (enemy_dist - squad_dist) > 0 and (squad_dist > flee_smart_min_dist) and (squad_dist < nearest_dist) then
nearest_dist = squad_dist
nearest_smart_name = smart_name
end
end
end
return nearest_smart_name
end
function get_nearest_squad_member_id(squad, smart)
local dist = 999
local nearest_id
for m in squad:squad_members() do
local npc = level.object_by_id(m.id)
if valid_obj(npc) and (npc:position():distance_to(smart.position) < dist) then
dist = npc:position():distance_to(smart.position)
nearest_id = npc:id()
end
end
return nearest_id
end
function get_farthest_squad_member_id(squad, smart)
local dist = 0
local farthest_id
for m in squad:squad_members() do
local npc = level.object_by_id(m.id)
if valid_obj(npc) and (npc:position():distance_to(smart.position) > dist) then
dist = npc:position():distance_to(smart.position)
farthest_id = npc:id()
end
end
return farthest_id
end
function get_obj_level_name(se_obj)
local lid = se_obj and game_graph():vertex(se_obj.m_game_vertex_id):level_id()
return lid and alife():level_name(lid)
end
function actor_on_first_update()
local comms = { "bandit", "renegade", "stalker", "csky", "ecolog", "dolg", "freedom", "army", "killer", "greh", "isg", "monolith" }
local def_props_t = { ["all"] = true, ["base"] = true, ["surge"] = true, ["territory"] = true }
for name, smart in pairs(SIMBOARD.smarts_by_names) do
local smart_level_name = get_obj_level_name(smart)
-- smart on current level
if smart_level_name == level.name() then
for i = 1, #comms do
local comm = comms[i]
-- validate if smart can receive squads
if (not smart.disabled) and (smart.max_population and smart.max_population > 0) then
-- check if smart can receive this community
local comm_prop = smart.props and smart.props[comm] and smart.props[comm] > 0
local def_prop = false
for k, v in pairs(def_props_t) do
if smart.props and smart.props[k] and smart.props[k] > 0 then
def_prop = true
break
end
end
-- store this smart name for this community
if comm_prop or def_prop then
flee_smarts[comm] = flee_smarts[comm] or {}
flee_smarts[comm][name] = true
end
end
end
end
end
-- small delay for loading
CreateTimeEvent("dly_sq_upd_e", "dly_sq_upd_a", 5, function()
start_flee_check = true
return true
end)
end
function valid_obj(obj, cls)
if not obj then return end
local class_check = (not cls) or IsStalker(obj) or IsMonster(obj)
return class_check and obj.alive and obj:alive() and (not IsWounded(obj))
end
function server_entity_on_unregister(se_obj, typ)
-- remove squad
if squads_t[se_obj.id] then
squads_t[se_obj.id] = nil
end
end
function pr(...)
if not flee_dbg then return end
printf(...)
end
function pr2(...)
if not print_lists_dbg then return end
printf(...)
end
function npc_on_before_hit(npc, s_hit, bone_id, flags)
local squad = get_object_squad(npc)
if cancel_on_hit and squad and squad.commander_id and squads_t[squad.id] and squads_t[squad.id].fleeing and (not squads_t[squad.id].start_updates_tmr) and squads_t[squad.id].hits then
squads_t[squad.id].hits = squads_t[squad.id].hits + 1
if squads_t[squad.id].hits >= hits_to_cancel then
cancel_flee(squad, true, "got hitted")
end
end
if not immortal_npcs then return end
if not (s_hit.draftsman and s_hit.draftsman:id() == 0) then
flags.ret_value = false
return
end
s_hit.power = 1000
end
function on_option_change()
threshold_mult = npc_fleeing_mcm.get_config("threshold_mult")
-- enemies_pwr_mult = npc_fleeing_mcm.get_config("enemies_pwr_mult")
-- friends_pwr_mult = npc_fleeing_mcm.get_config("friends_pwr_mult")
enemies_radius = npc_fleeing_mcm.get_config("enemies_radius")
friends_radius = npc_fleeing_mcm.get_config("friends_radius")
enemies_upd_radius = npc_fleeing_mcm.get_config("enemies_upd_radius")
friends_upd_radius = npc_fleeing_mcm.get_config("friends_upd_radius")
start_flee_time = npc_fleeing_mcm.get_config("start_flee_time")
flee_protection_time = npc_fleeing_mcm.get_config("flee_protection_time")
cancel_on_hit = npc_fleeing_mcm.get_config("cancel_on_hit")
hits_to_cancel = npc_fleeing_mcm.get_config("hits_to_cancel")
flee_smart_min_dist = npc_fleeing_mcm.get_config("flee_smart_min_dist")
cover_time = npc_fleeing_mcm.get_config("cover_time")
smoke_enabled = npc_fleeing_mcm.get_config("smoke_enabled")
flee_dbg = npc_fleeing_mcm.get_config("flee_dbg")
end
function on_game_start()
RegisterScriptCallback("squad_on_update", squad_on_update)
RegisterScriptCallback("actor_on_first_update", actor_on_first_update)
RegisterScriptCallback("server_entity_on_unregister", server_entity_on_unregister)
RegisterScriptCallback("npc_on_before_hit", npc_on_before_hit)
RegisterScriptCallback("monster_on_before_hit", npc_on_before_hit)
RegisterScriptCallback("on_option_change", on_option_change)
end
@@ -0,0 +1,74 @@
-- If you don't use MCM, change your defaults from here.
local defaults = {
-- power
["threshold_mult"] = 1.25,
["companion_pwr"] = 5,
-- ["enemies_pwr_mult"] = 1,
-- ["friends_pwr_mult"] = 1,
-- distance before flee
["enemies_radius"] = 125,
["friends_radius"] = 70,
-- distance after flee
["enemies_upd_radius"] = 35,
["friends_upd_radius"] = 35,
-- others
["start_flee_time"] = 7,
["flee_protection_time"] = 12,
["cancel_on_hit"] = true,
["hits_to_cancel"] = 3,
["flee_smart_min_dist"] = 90,
["cover_time"] = 12,
["smoke_enabled"] = true,
["flee_dbg"] = false,
}
function get_config(key)
if ui_mcm then return ui_mcm.get("npc_flee/"..key) else return defaults[key] end
end
function on_mcm_load()
op = { id= "npc_flee",sh=true ,gr={
{ id= "title",type= "slide",link= "ui_options_slider_player",text="ui_mcm_npc_flee_title",size= {512,50},spacing= 20 },
{ id= "title" ,type= "desc" ,text= "ui_mcm_npc_flee_header_power" ,align= "l" ,clr= {255,60,190,190} },
{id = "threshold_mult", type = "track", val = 2, min = 1.15, max = 2, step = 0.05, def = 1.25},
{id = "companion_pwr", type = "track", val = 2, min = 1, max = 50, step = 0.5, def = 5},
-- {id = "enemies_pwr_mult", type = "track", val = 2, min = 0.25, max = 3, step = 0.25, def = 1},
-- {id = "friends_pwr_mult", type = "track", val = 2, min = 0.25, max = 3, step = 0.25, def = 1},
{id="divider", type="line" },
{ id= "title" ,type= "desc" ,text= "ui_mcm_npc_flee_header_dist_before" ,align= "l" ,clr= {255,60,190,190} },
{id = "enemies_radius", type = "track", val = 2, min = 30, max = 240, step = 5, def = 125},
{id = "friends_radius", type = "track", val = 2, min = 30, max = 240, step = 5, def = 70},
{id="divider", type="line" },
{ id= "title" ,type= "desc" ,text= "ui_mcm_npc_flee_header_dist_after" ,align= "l" ,clr= {255,60,190,190} },
{id = "enemies_upd_radius", type = "track", val = 2, min = 10, max = 100, step = 5, def = 35},
{id = "friends_upd_radius", type = "track", val = 2, min = 10, max = 100, step = 5, def = 35},
{id="divider", type="line" },
{ id= "title" ,type= "desc" ,text= "ui_mcm_npc_flee_header_others" ,align= "l" ,clr= {255,60,190,190} },
{id = "start_flee_time", type = "track", val = 2, min = 0, max = 20, step = 1, def = 7},
{id = "flee_protection_time", type = "track", val = 2, min = 0, max = 30, step = 1, def = 12},
{id = "cancel_on_hit", type = "check", val = 1, def = true},
{id = "hits_to_cancel", type = "track", val = 2, min = 1, max = 7, step = 1, def = 3},
{id = "flee_smart_min_dist", type = "track", val = 2, min = 50, max = 200, step = 5, def = 90},
{id = "cover_time", type = "track", val = 2, min = 0, max = 25, step = 1, def = 12},
{id = "smoke_enabled", type = "check", val = 1, def = true},
{id="divider", type="line" },
{id = "flee_dbg", type = "check", val = 1, def = false},
}
}
return op
end