Divergent/mods/Jabbers Soulslike Gamemode/gamedata/scripts/soulslike.script

955 lines
28 KiB
Plaintext
Raw Normal View History

--[[
Jabbers
05APR2023
Jabbers' Soulslike Anomaly Mod
--]]
local version = "0.28-beta"
local tools_list = {
["itm_basickit"] = true,
["itm_advancedkit"] = true,
["itm_expertkit"] = true,
["itm_drugkit"] = true,
["itm_ammokit"] = true,
["itm_gunsmith_toolkit"] = true,
["itm_artefactskit"] = true,
}
local scenario_logic = nil
RELATIONS = {
FRIENDS = 1000,
BUDDIES = 500,
NEUTRALS = 0,
ENEMIES = -1000
}
SCENARIOS = {
Default = 1,
RFDetectorStash = 2,
HiddenStash = 3,
NoLoss = 4,
}
hit_type_to_str = {
[hit.light_burn] = "Light Burn",
[hit.burn] = "Burn",
[hit.strike] = "Strike",
[hit.shock] = "Shock",
[hit.wound] = "Wound",
[hit.radiation] = "Radiation",
[hit.telepatic] = "Telepatic",
[hit.chemical_burn] = "Chemical Burn",
[hit.explosion] = "Explosion",
[hit.fire_wound] = "Fire",
}
entity_type = {
Stalker = 1,
Monster = 2,
Anomaly = 3,
Self = 4,
Other = 4
}
----------------------------------------
-- Classes
----------------------------------------
MAX_HIT_POOL_COUNT = 30
MAX_HIT_TIME = 30000
local hit_queue = soulslike_classes.TimedQueue(MAX_HIT_POOL_COUNT, MAX_HIT_TIME)
----------------------------------------
-- Helpers
----------------------------------------
function try(func, ...)
local status, error_or_result = pcall(func, ...)
if not status then
soulslike.error(error_or_result)
return false
else
return error_or_result
end
end
function math.clamp(x, min, max)
if x < min then return min end
if x > max then return max end
return x
end
function table.get_length(T)
local count = 0
for _ in pairs(T) do count = count + 1 end
return count
end
function table.has_value(tab, val)
for _, value in ipairs(tab) do
if value == val then
return true
end
end
return false
end
----------------------------------------
-- DEBUG
----------------------------------------
local function print_table (tbl, indent)
if not indent then
indent = 0
end
utils_data.debug_write(string.rep(" ", indent) .. "{")
indent = indent + 2
if (type(tbl) == "userdata") then
utils_data.debug_write("<userdata>,\n")
else
for k, v in pairs(tbl) do
local toprint = string.rep(" ", indent)
if (type(k) == "number") then
toprint = toprint .. "[" .. k .. "] = "
elseif (type(k) == "string") then
toprint = toprint .. k .. " = "
end
if (type(v) == "number") then
utils_data.debug_write(toprint .. v .. ",")
elseif (type(v) == "string") then
utils_data.debug_write(toprint .. "\"" .. v .. "\",")
elseif (type(v) == "table") then
utils_data.debug_write(toprint)
print_table(v, indent + 2)
else
utils_data.debug_write(toprint .. "\"" .. tostring(v) .. "\",")
end
end
end
utils_data.debug_write(string.rep(" ", indent-2) .. "}")
end
local function log(log_type, output)
if not output then
utils_data.debug_write("[Soulslike] "..log_type..": ".."(nil)")
elseif (type(output) == "table") then
utils_data.debug_write("[Soulslike] "..log_type..": ")
print_table(output)
else
utils_data.debug_write("[Soulslike] "..log_type..": "..output)
end
end
function debug(output)
log("DEBUG", output)
end
function info(output)
log("INFO ", output)
end
function warn(output)
log("WARN ", output)
end
function error(output)
log("ERROR", output)
end
function debug_tip(text, delay)
debug(text)
if not soulslike_mcm.show_debug_tips() then
return
end
local text = "[Soulslike] "..tostring(text)
if not db.actor then
return
end
local ico = "ui_inGame2_Dengi_otdani"
local text_color = utils_xml.get_color("pda_white")
text = text_color .. text
if delay == nil then
delay = 6000
end
news_manager.send_tip(db.actor, text, nil, ico, delay)
end
----------------------------------------
-- Globals
----------------------------------------
function _G.IsSoulslikeMode()
return not IsHardcoreMode() and axr_main.config and axr_main.config:r_value("character_creation","new_game_soulslike_mode",1) == true or alife_storage_manager.get_state().enable_soulslike_mode == true
end
function _G.IsToolkit(o, s)
if not (s) then
s = o and o:section()
end
return tools_list[s]
end
----------------------------------------
-- Helper Functions
----------------------------------------
function get_soulslike_state()
local game_state = alife_storage_manager.get_state()
if not game_state.soulslike then
game_state.soulslike = {
created_stashes = {},
spawn_location = {
level = nil,
position = {
x = nil,
y = nil,
z = nil,
},
angle = {
x = nil,
y = nil,
z = nil,
},
level_vertex_id = nil,
game_vertex_id = nil,
},
note_message_data = {},
hidden_stashes = {}
}
end
-- Backward save compatibility
if not game_state.soulslike.note_message_data then
game_state.soulslike.note_message_data = {}
end
if not game_state.soulslike.hidden_stashes then
game_state.soulslike.hidden_stashes = {}
end
if not game_state.soulslike.created_stashes then
game_state.soulslike.created_stashes = {}
end
if not game_state.soulslike.spawn_location then
game_state.soulslike.spawn_location = {
level = nil,
position = {
x = nil,
y = nil,
z = nil,
},
angle = {
x = nil,
y = nil,
z = nil,
},
level_vertex_id = nil,
game_vertex_id = nil,
}
end
return game_state.soulslike
end
function set_spawn(show_message)
local se_actor = alife():actor()
local state = get_soulslike_state()
state.spawn_location.level = level.name()
state.spawn_location.position.x = se_actor.position.x
state.spawn_location.position.y = se_actor.position.y
state.spawn_location.position.z = se_actor.position.z
state.spawn_location.angle.x = se_actor.angle.x
state.spawn_location.angle.y = se_actor.angle.y
state.spawn_location.angle.z = se_actor.angle.z
state.spawn_location.level_vertex_id = se_actor.m_level_vertex_id
state.spawn_location.game_vertex_id = se_actor.m_game_vertex_id
debug("Saved spawn location data:")
if show_message then
local str = game.translate_string("st_soulslike_spawn_location_set")
actor_menu.set_msg(1, str, 4)
end
end
function find_closest_enemy()
local enemy = nil
local enemy_dist = 150
local sim = alife()
local gg = game_graph()
local level_name = level.name()
if not sim then return end
if not gg then return end
for i=1,65534 do
local se_obj = sim:object(i)
if se_obj and (level_name == sim:level_name(gg:vertex(se_obj.m_game_vertex_id):level_id())) then
local cls = se_obj:clsid()
local sec = se_obj:section_name()
local is_valid = false
if IsStalker(nil,cls) and string.find(sec,"sim_default_") and se_obj:alive() then
local comm = se_obj:community()
if (comm ~= "trader") and (comm ~= "zombied") then
is_valid = true
end
elseif IsMonster(nil,cls) then
is_valid = true
end
if is_valid then
local dist = se_obj.position:distance_to_sqr(db.actor:position())
if enemy_dist > dist then
if IsStalker(nil,cls) then
local comm = se_obj:community()
debug("Found enemy "..se_obj:name().." from the "..comm.." community "..tostring(dist).." meters away.")
enemy = se_obj
elseif IsMonster(nil,cls) then
enemy = se_obj
end
end
end
end
end
return enemy
end
function find_closest_enemy_mutant()
local enemy = nil
local enemy_dist = 75
local sim = alife()
local gg = game_graph()
local level_name = level.name()
if not sim then return end
if not gg then return end
for i=1,65534 do
local se_obj = sim:object(i)
if se_obj and (level_name == sim:level_name(gg:vertex(se_obj.m_game_vertex_id):level_id())) then
local cls = se_obj:clsid()
if IsMonster(nil,cls) then
local dist = se_obj.position:distance_to_sqr(db.actor:position())
if enemy_dist > dist then
if IsMonster(nil,cls) then
enemy = se_obj
end
end
end
end
end
return enemy
end
function find_closest_enemy_stalker()
local friend = nil
local friend_dist = 100000
for i=1, #db.OnlineStalkers do
local id = db.OnlineStalkers[i]
local npc = db.storage[id] and db.storage[id].object or level.object_by_id(id)
if npc then
local dist = npc:position():distance_to_sqr(db.actor:position())
local is_friend = npc:general_goodwill(db.actor) <= RELATIONS.ENEMIES
if npc:alive() and is_friend and friend_dist > dist and (not get_object_story_id(id)) then
local comm = npc:character_community()
debug("Found friend "..npc:name().." from the "..comm.." community "..tostring(dist).." meters away.")
friend = npc
friend_dist = dist
end
end
end
return friend
end
function find_closest_friendly_stalker()
local friend = nil
local friend_dist = 100000
for i=1, #db.OnlineStalkers do
local id = db.OnlineStalkers[i]
local npc = db.storage[id] and db.storage[id].object or level.object_by_id(id)
if npc then
local dist = npc:position():distance_to_sqr(db.actor:position())
local is_friend = npc:general_goodwill(db.actor) >= RELATIONS.ENEMIES
if npc:alive() and is_friend and friend_dist > dist and (not get_object_story_id(id)) then
local comm = npc:character_community()
debug("Found friend "..npc:name().." from the "..comm.." community "..tostring(dist).." meters away.")
friend = npc
friend_dist = dist
end
end
end
return friend
end
function force_save(type)
--if game isn't already paused, then force a pause here
local force_pause
if not (device():is_paused()) then
device():pause(true)
force_pause = true
end
local Y, M, D, h
Y, M, D, h = game.get_game_time():get(Y, M, D, h)
local m = level.get_time_minutes()
if m < 10 then
m = ("0"..m)
end
local comm = utils_xml.get_special_txt(db.actor:character_community())
local map = utils_xml.get_special_txt(level.name())
local date = string.format("%d.%d.%d %d-%d", D, M, Y, h, m)
local file_name = "soulslike_"..comm.." - "..map.." "..date.." - "..type
exec_console_cmd("save ".. file_name)
if (force_pause) then
device():pause(false)
end
end
----------------------------------------
-- Dream Callbacks
----------------------------------------
function wakeup_callback()
debug("wakeup_callback")
xr_effects.enable_ui(db.actor, nil)
exec_console_cmd("snd_volume_music "..tostring(_G.mus_vol))
exec_console_cmd("snd_volume_eff "..tostring(_G.amb_vol))
_G.amb_vol = 0
_G.mus_vol = 0
disable_info("tutorial_sleep")
disable_info("actor_is_sleeping")
disable_info("sleep_active")
debug("Looking for scenario logic.")
if scenario_logic then
debug("Completing scenario.")
scenario_logic:OnComplete()
else
error("No logic state")
end
local data = get_soulslike_state()
scenario_logic:destroy()
scenario_logic = nil
data.logic_state = nil
end
function dream_callback()
debug("dream_callback")
level.add_cam_effector("camera_effects\\sleep.anm", 10, false, "soulslike.wakeup_callback")
local hours = math.random(6,14)
level.change_game_time(0,hours,0)
db.actor.power = 1
SendScriptCallback("actor_on_sleep", hours)
end
----------------------------------------
-- Game Callbacks
----------------------------------------
local function actor_on_before_death(who, flags)
if not IsSoulslikeMode() then
return
end
-- Pretty sure this fixes arena fights, still need to test
if has_alife_info("bar_arena_fight") then
debug('Actor was in an arena fight, ignoring death.')
return
end
debug('Actor died')
game_statistics.increment_statistic("deaths")
hit_queue:Invalidate()
scenario_logic = soulslike_scenario_logic_factory.create_new(hit_queue:Values())
if scenario_logic then
scenario_logic:OnDeath()
flags.ret_value = false
end
hit_queue:Clear()
end
local function on_before_save_input(flags, type, text)
if not IsSoulslikeMode() then
return
end
-- No hardcore save setting, allow saving
if not soulslike_mcm.is_hardcore_save_enabled() then
return
end
-- Hardcore save is enabled, but we still want to save at campfires
-- We just return to let the regular saving work.
if soulslike_mcm.override_campfire_hardcore_saves() then
return
end
-- All other scenarios flow through here and we just disallow saving
if not level_weathers.valid_levels[level.name()] then
return
end
debug('User tried to save')
local str = game.translate_string("st_save_only_when_sleeping")
actor_menu.set_msg(1, str, 4)
exec_console_cmd("main_menu off")
flags.ret = true
end
local function try_send_inventory_examined_message(stash_id)
local data = get_soulslike_state()
local stash_data = data.created_stashes[stash_id]
if stash_data and not stash_data.examine then
local lost_items = stash_data.lost_items
debug('Stash not yet examined.')
debug(lost_items)
if #lost_items > 0 then
local msg = "You examine your belongings and find that you were missing the following items: "
local item_groups = {}
for _, sec in pairs(lost_items) do
if not item_groups[sec] then
item_groups[sec] = {
section = sec,
count = 1
}
else
item_groups[sec].count = item_groups[sec].count + 1
end
end
local item_names = {}
for _, group in pairs(item_groups) do
local inv_name = ui_item.get_sec_name(group.section)
if group.count > 1 then
table.insert(item_names, tostring(group.count).." x "..inv_name)
else
table.insert(item_names, inv_name)
end
end
msg = msg..table.concat(item_names, ", ")
local ui_sender = news_manager.tips_icons['default']
db.actor:give_game_news("", msg, ui_sender, 0, 20000)
stash_data.examine = true
end
end
end
local function actor_on_item_take_from_box(box,obj)
if not IsSoulslikeMode() then
return
end
local state = get_soulslike_state()
local id = box:id()
if (box:section() == "inv_backpack") then
if (box:is_inv_box_empty()) then
hide_hud_inventory()
local se_obj = alife_object(id)
if se_obj then
alife_release(se_obj)
try_send_inventory_examined_message(id)
state.created_stashes[id] = nil
end
end
end
end
local function actor_on_stash_remove(data)
if not IsSoulslikeMode() then
return
end
local state = get_soulslike_state()
if state.created_stashes[data.stash_id] then
data.cancel = true
try_send_inventory_examined_message(state.stash_id)
end
end
local function on_console_execute(name, ...)
if not IsSoulslikeMode() then
return
end
if(name == "save") then
debug(name)
local extraArgs = {...}
if extraArgs then
local last_save_file_name = table.concat(extraArgs," ")
debug(last_save_file_name)
if soulslike_mcm.is_hardcore_save_enabled() then
last_save_file_name = string.lower(last_save_file_name)
debug("Don't delete: ".. last_save_file_name)
local uuid = get_soulslike_state().uuid
local fs = getFS()
if not fs then return end
local flist = fs:file_list_open_ex("$game_saves$",bit_or(FS.FS_ListFiles,FS.FS_RootOnly),"*.scoc")
local f_cnt = flist:Size()
for it=0, f_cnt-1 do
local file = flist:GetAt(it)
local file_name = string.sub(file:NameFull(), 0, (string.len(file:NameFull()) - string.len(".scoc")))
local scoc_path = fs:update_path('$game_saves$', '')..file_name..".scoc"
local scop_path = fs:update_path('$game_saves$', '')..file_name..".scop"
local dds_path = fs:update_path('$game_saves$', '')..file_name..".dds"
local f = io.open(scoc_path,"rb")
if f then
local data = f:read("*all")
f:close()
if (data) then
local decoded = alife_storage_manager.decode(data)
local d_soulslike = decoded and decoded.soulslike
if (d_soulslike and (d_soulslike.uuid == uuid)) then
debug("/ Soulslike mode | file: "..file_name)
file_name = string.lower(file_name)
if file_name ~= last_save_file_name then
debug("~ Soulslike mode | delete save file: "..file_name)
local scoc_path_bak = fs:update_path('$game_saves$', '').."soulslike-backup/"..file_name..".scoc"
local scop_path_bak = fs:update_path('$game_saves$', '').."soulslike-backup/"..file_name..".scop"
local dds_path_bak = fs:update_path('$game_saves$', '').."soulslike-backup/"..file_name..".dds"
fs:file_copy(scoc_path, scoc_path_bak)
fs:file_copy(scop_path, scop_path_bak)
fs:file_copy(dds_path, dds_path_bak)
ui_load_dialog.delete_save_game(file_name)
end
end
end
end
end
end
end
end
end
local function physic_object_on_use_callback(box, who)
local data = get_soulslike_state()
if not IsInvbox(box) or not data.hidden_stashes or not data.hidden_stashes[box:id()] then
return
end
local id = box:id()
local stash_data = data.hidden_stashes[id]
local stash_id = stash_data.stash_id
local se_obj = alife_object(stash_id)
local stash = level.object_by_id(stash_id)
if not stash then
warn("Not expected, unable to find stash id linked to "..tostring(id)..". Please save and reload near the stash to see if it solves the issue.")
return
end
local sim = alife();
if not sim then return end
if se_obj and not se_obj.online then
se_obj:switch_online()
end
sim:set_switch_online(stash_id,true)
sim:set_switch_offline(stash_id,false)
try_send_inventory_examined_message(stash_id)
local function transfer_item(temp,item)
debug("Transfering item "..item:section())
stash:transfer_item(item, box)
end
if stash_data.radio_id then
debug("Clearing radio stash "..tostring(stash_data.radio_id))
item_radio.clear_stash(stash_data.radio_level, stash_data.radio_id)
end
debug("Transfering items from hidden stash "..tostring(id).." to static stash "..tostring(stash_id))
stash:iterate_inventory_box(transfer_item)
alife_release(stash)
debug("Removing PDA marker "..tostring(id))
level.map_remove_object_spot(id , "secondary_task_location")
data.hidden_stashes[id] = nil
end
local function load_state(data)
if not IsSoulslikeMode() then
return
end
local data = get_soulslike_state()
if not data.logic_state then
debug("No logic state")
elseif data.logic_state and not data.logic_state.scenario_id then
warn("Logic state exists without scenario id")
elseif data.logic_state and data.logic_state.scenario_id then
-- Reinitialize the last scenario using the saved logic state.
scenario_logic = soulslike_scenario_logic_factory.create_by_id(data.logic_state.scenario_id, data.logic_state)
end
end
local function save_state(data)
if not IsSoulslikeMode() then
return
end
local data = get_soulslike_state()
if not data.spawn_location.level and level.name() ~= 'fake_start' then
set_spawn(false)
end
if scenario_logic then
-- Save the logic state for the current scenario so we can restore it on load
-- This is for the purposes of ChangeLevel which unloads the scripts and then
-- reloads them, so we need to be able to reinitialize the scenario as it was
-- before changing levels
data.logic_state = scenario_logic.logic_state;
end
end
local function on_level_changing()
if not IsSoulslikeMode() then
return
end
debug("On on_level_changing load.")
local data = get_soulslike_state()
if not data.spawn_location.level and level.name() ~= 'fake_start' then
set_spawn(false)
end
end
local function on_game_load()
if not IsSoulslikeMode() then
return
end
debug("On game load.")
local data = get_soulslike_state()
local sim = alife()
if sim then
debug("Updating position and status of hidden stashes.")
for box_id, value in pairs(data.hidden_stashes) do
local se_hidden_stash = alife_object(value.stash_id)
local se_box = alife_object(box_id)
if not se_hidden_stash then
debug("Unable to find hidden stash "..tostring(value.stash_id))
elseif not se_box then
debug("Unable to find box id "..tostring(box_id))
else
debug("Moving hidden stash"..tostring(value.stash_id))
sim:teleport_object(value.stash_id, se_box.m_game_vertex_id, se_box.m_level_vertex_id, se_box.position)
debug("Setting hidden stash online")
if not se_box.online then
se_box:switch_online()
end
for i=1,65534 do
local se_obj = sim:object(i)
if (se_obj and se_obj.parent_id == id) then
debug("Moving "..se_obj.section.." id:"..tostring(value.stash_id))
sim:teleport_object(value.stash_id, se_box.m_game_vertex_id, se_box.m_level_vertex_id, se_box.position)
if se_obj then
if not se_obj.online then
debug("Setting "..se_obj.section.." online")
se_obj:switch_online()
end
end
end
end
end
end
end
-- Write out an identifier so we can use it later
-- to identify other saves with the same ID if the
-- user is playing with Hardcore Saves enabled.
if not data.uuid then
data.uuid = GAME_VERSION .. "_" .. tostring(math.random(100)) .. tostring(math.random()) .. tostring(math.random(1000))
end
debug("Looking for scenario logic.")
if scenario_logic then
debug("Calling scenario respawn.")
scenario_logic:OnRespawn()
end
end
local function actor_on_sleep(hours)
if not IsSoulslikeMode() then
return
end
soulslike.debug('actor_on_sleep')
-- Only force save if we aren't running a scenario
if not scenario_logic then
soulslike.force_save("sleep")
end
end
local detour_actor_on_before_hit = nil
local function actor_on_before_hit(shit, bone_id, flags)
if not IsSoulslikeMode() then
if detour_actor_on_before_hit then
detour_actor_on_before_hit(shit, bone_id, flags)
end
return
end
local damage = shit.power
if grok_actor_damage_balancer and grok_actor_damage_balancer.damage then
--debug("Using damage from Grok's Damage Balancer")
damage = grok_actor_damage_balancer.damage
end
--debug("Player hit:")
--debug("shit.type: "..tostring(hit_type_to_str[shit.type]))
--debug("shit.power: "..tostring(damage))
--debug("shit.impulse: "..tostring(shit.impulse))
--debug("shit.draftsman: "..(shit and shit.draftsman:name() or "<nil>"))
if shit.draftsman then
--debug("shit.draftsman: "..(shit and shit.draftsman:name() or "<nil>"))
end
local health = db.actor.health
local is_fatal = shit.power >= health
hit_queue:Enqueue({
type = shit.type,
power = damage,
is_fatal = is_fatal,
time = time_global(),
draftsman_id = shit.draftsman and shit.draftsman:id() or nil,
})
--debug("is_fatal: "..tostring(is_fatal))
end
local function npc_on_net_spawn(npc, se_obj)
if not IsSoulslikeMode() then
return
end
local state = get_soulslike_state()
if npc and IsStalker(npc) and state.tracked_ambushers and state.tracked_ambushers[npc:id()] then
debug("Setting up ambushers "..tostring(npc:id()))
local ambusher_state = state.tracked_ambushers[npc:id()]
local position = vector():set(ambusher_state.position.x, ambusher_state.position.y, ambusher_state.position.z)
npc:set_mental_state(ambusher_state.mental_state)
npc:set_body_state(ambusher_state.body_state)
npc:set_movement_type(ambusher_state.movement_type)
npc:set_sight(ambusher_state.sight_type, nil, 0)
npc:set_desired_position(position)
npc:set_desired_direction()
if soulslike_mcm.debug_squad_spawns() then
level.map_add_object_spot_ser(npc:id(), "secondary_task_location", "DEBUG: Ambush NPC")
end
state.tracked_ambushers[npc:id()] = nil
end
end
function on_game_start()
debug('Version: '..version)
RegisterScriptCallback("actor_on_stash_remove", actor_on_stash_remove)
RegisterScriptCallback("actor_on_item_take_from_box", actor_on_item_take_from_box)
RegisterScriptCallback("actor_on_before_death", actor_on_before_death)
RegisterScriptCallback("on_before_save_input", on_before_save_input)
RegisterScriptCallback("save_state", save_state)
RegisterScriptCallback("load_state", load_state)
RegisterScriptCallback("on_level_changing", on_level_changing)
RegisterScriptCallback("on_game_load", on_game_load)
RegisterScriptCallback("actor_on_sleep", actor_on_sleep)
RegisterScriptCallback("on_console_execute", on_console_execute)
RegisterScriptCallback("actor_on_before_hit", actor_on_before_hit)
RegisterScriptCallback("physic_object_on_use_callback", physic_object_on_use_callback)
--RegisterScriptCallback("npc_on_net_spawn", npc_on_net_spawn)
end