929 lines
32 KiB
Plaintext
929 lines
32 KiB
Plaintext
|
local gc = game.translate_string
|
||
|
local rand = math.random
|
||
|
local send_tip = news_manager.send_tip
|
||
|
local get_config = arti_lootboxes_mcm.get_config
|
||
|
|
||
|
-- test to see if scaling to 1000 helps or not
|
||
|
local NORMALIZE_1000 = true
|
||
|
|
||
|
-- importer for lootbox -> loot pool
|
||
|
ini_contents = ini_file("items\\lootboxes\\box_contents\\importer.ltx")
|
||
|
-- importer for loot pool -> contents
|
||
|
ini_pools = ini_file("items\\lootboxes\\item_pools\\importer.ltx")
|
||
|
ini_uniques = ini_file("items\\lootboxes\\unique_contents\\importer.ltx")
|
||
|
ini_lootbox = ini_file("items\\items\\items_lootbox.ltx")
|
||
|
|
||
|
ini_tools = ini_file("items\\settings\\lootbox_tools.ltx")
|
||
|
|
||
|
-- custom loaded contents
|
||
|
contents_custom = {}
|
||
|
pools_custom = {}
|
||
|
-- cached maps, these will get hydrated as more loot is rolled
|
||
|
local pool_cache = {}
|
||
|
local contents_cache = {}
|
||
|
-- map box groups to boxes
|
||
|
local variant_map = {}
|
||
|
-- track boxes that contain uniques, as well as what uniques have been seen so far
|
||
|
local uniques_cache = {}
|
||
|
-- store all lootbox contents
|
||
|
local lootbox_contents = {}
|
||
|
-- stolen from treasure_manager
|
||
|
local item_prop_table = { cond_r = {30,70} , cond_ct = "part" , cond_cr = {0.5,0.75,1} }
|
||
|
|
||
|
map_tiers = {}
|
||
|
|
||
|
local function save_state(mdata)
|
||
|
mdata.lootbox_contents = lootbox_contents
|
||
|
end
|
||
|
|
||
|
local function load_state(mdata)
|
||
|
lootbox_contents = mdata.lootbox_contents or {}
|
||
|
if not lootbox_contents.uniques then
|
||
|
lootbox_contents.uniques = {}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- unregister boxes that are deleted
|
||
|
local function server_entity_on_unregister(se_obj)
|
||
|
if lootbox_contents[se_obj.id] then
|
||
|
lootbox_contents[se_obj.id] = nil
|
||
|
end
|
||
|
if not lootbox_contents.uniques then
|
||
|
lootbox_contents.uniques = {}
|
||
|
end
|
||
|
-- if a unique box is deleted, add it back to the pool
|
||
|
if se_obj and lootbox_contents.uniques[se_obj.id] then
|
||
|
lootbox_contents.uniques[se_obj.id] = nil
|
||
|
if not uniques_cache[se_obj:section_name()] then return end
|
||
|
local unique = lootbox_contents.uniques[se_obj.id]
|
||
|
if unique and se_obj:section_name() then
|
||
|
uniques_cache[se_obj:section_name()][unique] = true
|
||
|
end
|
||
|
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function print_dbg(text, ...)
|
||
|
if get_config("debug") then
|
||
|
printf("lootboxes | %s | " .. text, time_global(), ...)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function get_loot_string(id)
|
||
|
return lootbox_contents[id]
|
||
|
end
|
||
|
|
||
|
-- rand() against these odds
|
||
|
function roll(odds)
|
||
|
if not odds then
|
||
|
callstack()
|
||
|
return false
|
||
|
end
|
||
|
if NORMALIZE_1000 then
|
||
|
odds = math.ceil(1000*tonumber(odds))
|
||
|
return rand(1000) <= odds
|
||
|
else
|
||
|
return rand() <= tonumber(odds)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- roll a cond between min and 2x min, clamped at 99
|
||
|
function roll_cond(min)
|
||
|
return rand(min, clamp(2*min, 0, 99))
|
||
|
end
|
||
|
|
||
|
-- return random entry from integer-indexed table
|
||
|
function random_entry(t)
|
||
|
t = t or {}
|
||
|
return t[rand(#t)]
|
||
|
end
|
||
|
|
||
|
-- convert a comma-separated string of sections and numbers into a table
|
||
|
function parse_lootstring(str)
|
||
|
local temp = str_explode(str, ",")
|
||
|
local t = {}
|
||
|
for i=1,#temp-1, 2 do
|
||
|
table_upsert(t, temp[i], temp[i+1])
|
||
|
end
|
||
|
return t
|
||
|
end
|
||
|
|
||
|
-- convert a table into a comma-separated string of sections and numbers
|
||
|
function create_lootstring(template)
|
||
|
str = ""
|
||
|
for k,v in pairs(template) do
|
||
|
str = str .. k .. "," .. v .. ","
|
||
|
end
|
||
|
return str
|
||
|
end
|
||
|
|
||
|
-- str_explode but they get parsed into numbers
|
||
|
function str_explode_num(str, sep, plain)
|
||
|
if not (str and sep) then
|
||
|
printe("!ERROR str_explode | missing parameter str = %s, sep = %s",str,sep)
|
||
|
callstack()
|
||
|
end
|
||
|
if not (sep ~= "" and str:find(sep,1,plain)) then
|
||
|
return { tonumber(str) }
|
||
|
end
|
||
|
local t = {}
|
||
|
local size = 0
|
||
|
for s in str:gsplit(sep,plain) do
|
||
|
size = size + 1
|
||
|
t[size] = tonumber(trim(s))
|
||
|
end
|
||
|
return t
|
||
|
end
|
||
|
|
||
|
function table_upsert(t, k, v)
|
||
|
if type(v) == "number" then
|
||
|
t[k] = t[k] and t[k] + v or v
|
||
|
else
|
||
|
t[k] = v
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- coerce the input into the section, or nil
|
||
|
local function get_sec(obj)
|
||
|
if not obj then return end
|
||
|
return type(obj) == "string" and obj or (type(obj.id) == "function" and obj:section() or nil)
|
||
|
end
|
||
|
|
||
|
local not_wpn = {
|
||
|
["WP_BINOC"] = true,
|
||
|
["WP_SCOPE"] = true,
|
||
|
["WP_SILEN"] = true,
|
||
|
["WP_GLAUN"] = true
|
||
|
}
|
||
|
|
||
|
function sec_is_weapon(section)
|
||
|
local class = SYS_GetParam(0, section, "class")
|
||
|
if string.find(section, "wpn_") then
|
||
|
return not_wpn[class] == nil
|
||
|
else return false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function is_box(obj, sec)
|
||
|
sec = obj and obj:section() or sec
|
||
|
return SYS_GetParam(0, sec, "open_with") ~= nil
|
||
|
end
|
||
|
|
||
|
function is_tool(obj, sec)
|
||
|
sec = obj and obj:section() or sec
|
||
|
return ini_tools:section_exist(sec)
|
||
|
end
|
||
|
|
||
|
function init()
|
||
|
local ini_map = ini_file("items\\settings\\lootbox_map_distribution.ltx")
|
||
|
ini_map:section_for_each(function(map)
|
||
|
if not map_tiers[map] then map_tiers[map] = {} end
|
||
|
print_dbg("processing map %s", map)
|
||
|
local line_count = ini_map:line_count(map) or 0
|
||
|
local ref = map_tiers[map]
|
||
|
for i=0,line_count-1 do
|
||
|
local drop_chance = {}
|
||
|
local junk1, group, chance = ini_map:r_line_ex(map, i, "", "")
|
||
|
drop_chance.group = group
|
||
|
drop_chance.chance = chance or 0.5
|
||
|
print_dbg("adding box group %s to map %s, weight %s", group, map, chance)
|
||
|
ref[i + 1] = drop_chance
|
||
|
end
|
||
|
end)
|
||
|
local ini_stalker = ini_file("items\\settings\\lootbox_stalker_distribution.ltx")
|
||
|
ini_stalker:section_for_each(function(faction)
|
||
|
if not map_tiers[faction] then map_tiers[faction] = {} end
|
||
|
print_dbg("processing faction %s", faction)
|
||
|
local line_count = ini_stalker:line_count(faction) or 0
|
||
|
local ref = map_tiers[faction]
|
||
|
for i=0,line_count-1 do
|
||
|
local drop_chance = {}
|
||
|
local junk1, items, chance = ini_stalker:r_line_ex(faction, i, "", "")
|
||
|
drop_chance.items = items
|
||
|
drop_chance.chance = chance or 0.5
|
||
|
ref[i + 1] = drop_chance
|
||
|
end
|
||
|
end)
|
||
|
|
||
|
ini_lootbox:section_for_each(function(section)
|
||
|
local group = ini_lootbox:r_string_ex(section, "box_group") or "low"
|
||
|
if group then
|
||
|
if not variant_map[group] then variant_map[group] = {} end
|
||
|
table.insert(variant_map[group], section)
|
||
|
end
|
||
|
end)
|
||
|
init_uniques()
|
||
|
end
|
||
|
|
||
|
function init_uniques()
|
||
|
-- hydrate the box uniques that have not been seen so far
|
||
|
-- mechanism: initially add all sections to this cache
|
||
|
-- when a unique box rolls, add an entry in uniques child table associating box id to what unique section exactly
|
||
|
-- when that box is opened (or deleted), drop the entry
|
||
|
-- opening also marks that entry as being unable to drop again
|
||
|
ini_uniques:section_for_each(function(section)
|
||
|
if not lootbox_contents.uniques then lootbox_contents.uniques = {} end
|
||
|
if lootbox_contents.uniques[section] then return end
|
||
|
local box = ini_uniques:r_string_ex(section, "box")
|
||
|
if not uniques_cache[box] then uniques_cache[box] = {} end
|
||
|
uniques_cache[box][section] = true
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
-- Weapon data is stored as weapon_section,flags_ammotype_cond
|
||
|
function append_weapon(template, section, min, add_ammo)
|
||
|
template = template or {}
|
||
|
if not sec_is_weapon(section) then
|
||
|
print_dbg("Section %s is not weapon! Returning only 1" , section)
|
||
|
table_upsert(template, section, 1)
|
||
|
return
|
||
|
end
|
||
|
if string.find(section, "knife") or string.find(section, "axe") then
|
||
|
print_dbg("Appending melee weapon")
|
||
|
table_upsert(template, section, "0_0_"..roll_cond(min))
|
||
|
return
|
||
|
end
|
||
|
local loot_str = ""
|
||
|
local flag = 0
|
||
|
|
||
|
if (ini_sys:r_float_ex(section,"scope_status")) then
|
||
|
flag = flag + 1
|
||
|
end
|
||
|
|
||
|
if (ini_sys:r_float_ex(section,"grenade_launcher_status")) then
|
||
|
flag = flag + 2
|
||
|
end
|
||
|
|
||
|
if (ini_sys:r_float_ex(section,"silencer_status")) then
|
||
|
flag = flag + 4
|
||
|
end
|
||
|
|
||
|
flag = rand(0,flag)
|
||
|
|
||
|
ammos = parse_list(ini_sys,section,"ammo_class")
|
||
|
ct = ammos and #ammos
|
||
|
ammo_type = ammos and ct and rand(0,ct-1) or 0
|
||
|
ammo_section = ammo_type and ammos[ammo_type+1]
|
||
|
|
||
|
local condition = roll_cond(min)
|
||
|
print_dbg("Appending weapon of type %s", section)
|
||
|
table_upsert(template, section, flag.. "_"..ammo_type.."_"..condition)
|
||
|
if add_ammo and ammos and ct and ct > 0 then
|
||
|
table_upsert(template, ammos[1], rand(3))
|
||
|
end
|
||
|
return template
|
||
|
end
|
||
|
|
||
|
|
||
|
-- on the chance that all maxes don't sum up, cut after this many iterations
|
||
|
local MAX_ITER = 10000
|
||
|
-- build a loot template to be substituted with items
|
||
|
-- loot template is table mapping item pools = quant of items
|
||
|
function build_template(template, section, max, bias)
|
||
|
template = template or {}
|
||
|
if ini_contents:section_exist(section) then
|
||
|
-- cache the table
|
||
|
if not contents_cache[section] then
|
||
|
n = ini_contents:line_count(section)
|
||
|
local pool = {}
|
||
|
for i=0,n-1 do
|
||
|
local content = {}
|
||
|
_, id, value = ini_contents:r_line_ex(section,i,"","")
|
||
|
print_dbg("Adding %s of %s", value, id)
|
||
|
values = str_explode_num(value, ",")
|
||
|
content.section = id
|
||
|
content.size = values[1]
|
||
|
content.limit = values[2]
|
||
|
content.chance = values[3]
|
||
|
table.insert(pool, content)
|
||
|
end
|
||
|
if contents_custom[section] and not is_empty(contents_custom[section]) then
|
||
|
print_dbg("Adding %s custom entries for section %s", #contents_custom[section], section)
|
||
|
for k,v in pairs(contents_custom[section]) do
|
||
|
table.insert(pool, v)
|
||
|
end
|
||
|
end
|
||
|
contents_cache[section] = pool
|
||
|
end
|
||
|
-- build
|
||
|
max = (max and max > 0) and max or 10
|
||
|
bias = bias or 1
|
||
|
bias = bias * get_config("lootquality")
|
||
|
print_dbg("Adding %s items, bias %s", max, bias)
|
||
|
content_pool = contents_cache[section]
|
||
|
iters = 0
|
||
|
while max > 0 do
|
||
|
-- pick a random entry from the pool
|
||
|
if iters > MAX_ITER then break end
|
||
|
item = random_entry(content_pool)
|
||
|
print_dbg("Rolled item %s, chance is %s", item.section, item.chance)
|
||
|
local curr_amt = template[item.section] or 0
|
||
|
if curr_amt < item.limit and item.size <= max and roll(item.chance * bias) then
|
||
|
table_upsert(template, item.section, 1)
|
||
|
max = clamp(max - item.size, 0, max)
|
||
|
print_dbg("Added %s, (size %s), %s left", item.section, item.size, max)
|
||
|
end
|
||
|
iters = iters + 1
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- sub item pools for actual items in content template
|
||
|
function process_template(template, box_section)
|
||
|
local processed = {}
|
||
|
for sec,v in pairs(template) do
|
||
|
for j=1,v do
|
||
|
local id, value = random_item_entry(sec)
|
||
|
print_dbg("Adding %s amount of %s to lootbox",value, id)
|
||
|
if sec_is_weapon(id) then
|
||
|
local condition = SYS_GetParam(2, box_section, "weapon_condition") or rand(20, 80)
|
||
|
append_weapon(processed, id, condition)
|
||
|
elseif item_device.device_npc_pda[id] then
|
||
|
if not processed[id] then
|
||
|
local schmuck_id = pda_custom.cache_local_schmuck()
|
||
|
print_dbg("Adding %s's pda to the loot", schmuck_id)
|
||
|
if schmuck_id ~= 0 then
|
||
|
table_upsert(processed, id, schmuck_id)
|
||
|
end
|
||
|
end
|
||
|
else
|
||
|
local max_multiuse = SYS_GetParam(1, box_section, "multiuse_full") or false
|
||
|
local max_uses = SYS_GetParam(2, id, "max_uses") or 1
|
||
|
if not max_multiuse and max_uses > 1 then
|
||
|
id = id .. "__" .. rand(max_uses)
|
||
|
end
|
||
|
table_upsert(processed, id, value)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
return processed
|
||
|
end
|
||
|
|
||
|
-- select a random item from the item pool
|
||
|
function random_item_entry(section)
|
||
|
print_dbg("Adding item from item pool %s", section)
|
||
|
if ini_pools:section_exist(section) then
|
||
|
if not pool_cache[section] then
|
||
|
n = ini_pools:line_count(section)
|
||
|
local contents = {}
|
||
|
for i=0,n-1 do
|
||
|
local ref = {}
|
||
|
_, id, value = ini_pools:r_line_ex(section,i,"","")
|
||
|
if not value or value == "" then value = 1 end
|
||
|
id = str_explode(id, ",")
|
||
|
ref.section = id[1]
|
||
|
ref.amount = id[2] or 1
|
||
|
ref.chance = value
|
||
|
contents[#contents + 1] = ref
|
||
|
end
|
||
|
if pools_custom[section] and not is_empty(pools_custom[section]) then
|
||
|
print_dbg("Adding %s custom entries for section %s", #pools_custom[section], section)
|
||
|
|
||
|
for k,v in pairs(pools_custom[section]) do
|
||
|
table.insert(contents, v)
|
||
|
end
|
||
|
end
|
||
|
pool_cache[section] = contents
|
||
|
-- end
|
||
|
end
|
||
|
|
||
|
local item_pool = pool_cache[section]
|
||
|
local selection = nil
|
||
|
while not selection do
|
||
|
local item = random_entry(item_pool)
|
||
|
if roll(item.chance) then
|
||
|
selection = item
|
||
|
end
|
||
|
end
|
||
|
return selection.section, selection.amount
|
||
|
else
|
||
|
printf("!!Content section %s not found!!", section)
|
||
|
return "duct_tape",1
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- attempt to populate a unique. if it fails, we can provide normal contents
|
||
|
function try_populate_unique(box_id, box_section)
|
||
|
u = uniques_cache[box_section]
|
||
|
if not u or next(u) == nil then return "" end
|
||
|
if rand(10) ~= 10 then return "" end
|
||
|
local unique_sec = random_key_table(u)
|
||
|
local unique_contents = ini_uniques:r_string_ex(unique_sec, "contents")
|
||
|
print_dbg("Putting custom contents %s into box %s", unique_contents, box_section)
|
||
|
|
||
|
if not lootbox_contents.uniques then lootbox_contents.uniques = {} end
|
||
|
lootbox_contents.uniques[box_id] = unique_sec
|
||
|
lootbox_contents[box_id] = unique_contents
|
||
|
if (box_section == "lootbox_4") then
|
||
|
lootbox_contents.spooky = box_id
|
||
|
end
|
||
|
|
||
|
uniques_cache[box_section][unique_sec] = nil
|
||
|
return unique_contents
|
||
|
end
|
||
|
|
||
|
function populate_lootbox(box_id, box_section)
|
||
|
if not lootbox_contents[box_id] then
|
||
|
print_dbg("Populating lootbox loot for %s of type: %s",box_id, box_section)
|
||
|
local loot_type = SYS_GetParam(0,box_section,"loot_type")
|
||
|
print_dbg("Loot type is %s", loot_type)
|
||
|
-- populate lore box, 10% chance
|
||
|
local loot_str = try_populate_unique(box_id, box_section)
|
||
|
if loot_str ~= "" then
|
||
|
-- no action required
|
||
|
elseif loot_type == "weapon" then
|
||
|
-- spawn the weapon, roll for condition + attachments + spare ammos
|
||
|
local weapon_tbl = {}
|
||
|
local loot_section = SYS_GetParam(0, box_section, "contents")
|
||
|
local section, value = random_item_entry(loot_section, true)
|
||
|
local condition = SYS_GetParam(2, box_section, "weapon_condition") or rand(20, 80)
|
||
|
append_weapon(weapon_tbl, section, condition, rand(2) == 1)
|
||
|
loot_str = loot_str .. create_lootstring(weapon_tbl)
|
||
|
elseif loot_type == "grab" then
|
||
|
local box_template = {}
|
||
|
-- grab a template, iterate through the pairs of items and populate bsaed on loot
|
||
|
local loot_sec = SYS_GetParam(0, box_section, "contents")
|
||
|
local min, max = unpack(str_explode_num(SYS_GetParam(0, box_section, "items_range"), ","))
|
||
|
local to_roll = rand(min, max)
|
||
|
|
||
|
local b_min, b_max = unpack(str_explode_num(SYS_GetParam(0, box_section, "bias_range"), ",")) or 1, 2
|
||
|
-- bias skews the drop rate for rare stuff up if less items spawn
|
||
|
local bias = b_min + ( (b_max - b_min) * clamp(max - to_roll, 0, 999) / clamp(max - min, 1, 999) )
|
||
|
build_template(box_template, loot_sec, to_roll, bias)
|
||
|
local loot_template = process_template(box_template, box_section)
|
||
|
loot_str = loot_str .. create_lootstring(loot_template)
|
||
|
end
|
||
|
|
||
|
lootbox_contents[box_id] = loot_str
|
||
|
print_dbg("Lootbox final contents for %s: %s", box_id, loot_str)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function actor_on_item_take_from_box(box,itm)
|
||
|
local id = box:id()
|
||
|
if lootbox_contents[id] == true then
|
||
|
lootbox_contents[id] = nil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function spook_player(obj)
|
||
|
if lootbox_contents.spooky == obj:id() then
|
||
|
send_tip(db.actor, gc("st_spooky_"..rand(4)), nil, "swiss_knife", 6000)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
function on_item_drag_dropped(from, to, slot_from, slot_to)
|
||
|
if not (slot_from == EDDListType.iActorBag and slot_to == EDDListType.iActorBag) then
|
||
|
return
|
||
|
end
|
||
|
try_open_box(to, from)
|
||
|
end
|
||
|
|
||
|
function check_open_compatibility(box, tool)
|
||
|
box = get_sec(box)
|
||
|
tool = get_sec(tool)
|
||
|
local open_type = ini_tools:r_string_ex(tool, "open_type") or "nope"
|
||
|
-- not a box
|
||
|
local box_open_type = parse_list(ini_lootbox, box, "open_with", true)
|
||
|
print_dbg("Checking tool %s (open type %s) against box %s", tool, open_type, box)
|
||
|
return box_open_type[open_type]
|
||
|
end
|
||
|
|
||
|
-- check if actor can even open the box
|
||
|
-- returns a table of result and reason (reason is passed to actor)
|
||
|
-- preconds need the same!
|
||
|
function check_open_box(box, tool)
|
||
|
-- coerce into section
|
||
|
box_sec = get_sec(box)
|
||
|
tool_sec = get_sec(tool)
|
||
|
-- not a tool
|
||
|
if not check_open_compatibility(box_sec, tool_sec) then
|
||
|
return {
|
||
|
result = false,
|
||
|
reason = "st_incompatible"}
|
||
|
end
|
||
|
local precond = ini_tools:r_string_ex(tool_sec, "precondition") or "arti_lootboxes.no_precond"
|
||
|
precond = str_explode(precond,"%.")
|
||
|
if _G[precond[1]] and _G[precond[1]][precond[2]] then
|
||
|
return _G[precond[1]][precond[2]](box, tool)
|
||
|
else
|
||
|
return {
|
||
|
result = false,
|
||
|
reason = "st_incompatible"}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- attempt to use the tool on the box and open it
|
||
|
function try_open_box(box, tool)
|
||
|
-- tool/box validation here
|
||
|
if not is_box(box) or not is_tool(tool) then return false end
|
||
|
local res = check_open_box(box, tool)
|
||
|
if res.result then
|
||
|
open_lootbox(box, tool)
|
||
|
post_open_box(box, tool)
|
||
|
else
|
||
|
-- tool not compatible/insufficient
|
||
|
send_tip(db.actor, gc(res.reason), nil, "swiss_knife", 6000)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- check postcondition
|
||
|
function post_open_box(box, tool)
|
||
|
tool_sec = get_sec(tool)
|
||
|
local postcond = ini_tools:r_string_ex(tool_sec, "postcondition") or "arti_lootboxes.no_precond"
|
||
|
postcond = str_explode(postcond,"%.")
|
||
|
if _G[postcond[1]] and _G[postcond[1]][postcond[2]] then
|
||
|
_G[postcond[1]][postcond[2]](box, tool)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function no_precond()
|
||
|
return {
|
||
|
result = true
|
||
|
}
|
||
|
end
|
||
|
|
||
|
-- for patchers
|
||
|
function get_difficulty(box, tool)
|
||
|
box = get_sec(box)
|
||
|
tool = get_sec(tool)
|
||
|
local diff = SYS_GetParam(2, box, "difficulty") or 1
|
||
|
if db.actor:object("lockpick_set") then diff = clamp(diff - 1, 1, 10) end
|
||
|
return diff
|
||
|
end
|
||
|
|
||
|
-- lockpicks, check if difficulty amount of picks are present
|
||
|
function precond_lockpick(box, tool)
|
||
|
box_sec = get_sec(box)
|
||
|
tool_sec = get_sec(tool)
|
||
|
local difficulty = get_difficulty(box_sec, tool_sec)
|
||
|
local amt = 0
|
||
|
local function search(temp, item)
|
||
|
if item:section() == tool_sec then
|
||
|
amt = amt + 1
|
||
|
end
|
||
|
end
|
||
|
db.actor:iterate_inventory(search)
|
||
|
if amt == 0 then return {
|
||
|
result = false,
|
||
|
reason = "st_no_picks"
|
||
|
} end
|
||
|
amt = amt > 10 and 10 or amt
|
||
|
if difficulty > amt then return {
|
||
|
result = false,
|
||
|
reason = "st_cant_unlock"}
|
||
|
else
|
||
|
return {result = true}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- consume X picks
|
||
|
function use_lockpick(box, tool)
|
||
|
box_sec = get_sec(box)
|
||
|
tool_sec = get_sec(tool)
|
||
|
local difficulty = get_difficulty(box_sec, tool_sec)
|
||
|
local amt = 0
|
||
|
local function search(temp, item)
|
||
|
if item:section() == tool_sec and amt < difficulty then
|
||
|
alife_release_id(item:id())
|
||
|
amt = amt + 1
|
||
|
end
|
||
|
end
|
||
|
db.actor:iterate_inventory(search)
|
||
|
end
|
||
|
|
||
|
function use_axe(box, tool)
|
||
|
-- smesh
|
||
|
if (tool:condition() > 0.25) then
|
||
|
tool:set_condition(clamp(tool:condition() - 0.25, 0, 1))
|
||
|
else
|
||
|
-- blin, axe broke
|
||
|
send_tip(db.actor, gc("st_rip_axe"), nil, "swiss_knife", 6000)
|
||
|
alife_release_id(tool:id())
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function precond_snapgun(box, tool)
|
||
|
box_sec = get_sec(box)
|
||
|
tool_sec = get_sec(tool)
|
||
|
local difficulty = get_difficulty(box_sec, tool_sec)
|
||
|
local power = ini_tools:r_float_ex(tool_sec, "open_power")
|
||
|
if difficulty > power then
|
||
|
return {
|
||
|
result = false,
|
||
|
reason = "st_cant_unlock_snapgun"
|
||
|
}
|
||
|
else
|
||
|
return {
|
||
|
result = true
|
||
|
}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function release_tool(box, tool)
|
||
|
alife_release_id(tool:id())
|
||
|
end
|
||
|
|
||
|
function str_pick(box)
|
||
|
local tool = db.actor:object("lockpick")
|
||
|
if tool and check_open_compatibility(box, "lockpick") then
|
||
|
return "st_unlock_pick"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function do_pick(box)
|
||
|
try_open_box(box, db.actor:object("lockpick"))
|
||
|
end
|
||
|
|
||
|
function str_snap(box)
|
||
|
local tool = db.actor:object("snapgun")
|
||
|
if tool and check_open_compatibility(box, tool) then
|
||
|
return "st_unlock_snap"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function do_snap(box)
|
||
|
try_open_box(box, db.actor:object("snapgun"))
|
||
|
end
|
||
|
|
||
|
function str_coin(box)
|
||
|
local tool = db.actor:object("arcade_tokens")
|
||
|
if tool and check_open_box(box, tool).result then
|
||
|
return "st_unlock_coin"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function do_coin(box)
|
||
|
try_open_box(box, db.actor:object("arcade_tokens"))
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Weapon data is stored as weapon_section,flags_ammotype_cond
|
||
|
function give_weapon(weapon, attachment_data)
|
||
|
if weapon == "wpn_toz194" then weapon = "wpn_wincheaster1300" end
|
||
|
local table = str_explode(attachment_data, "_")
|
||
|
local se_obj = alife_create(weapon,db.actor:position(),db.actor:level_vertex_id(),db.actor:game_vertex_id(),db.actor:id(),false)
|
||
|
local data = utils_stpk.get_weapon_data(se_obj)
|
||
|
local cond = tonumber(table[3]) or 75
|
||
|
if (data) then
|
||
|
data.condition = (cond/100)
|
||
|
data.addon_flags = tonumber(table[1])
|
||
|
data.ammo_type = tonumber(table[2])
|
||
|
utils_stpk.set_weapon_data(data, se_obj)
|
||
|
end
|
||
|
alife():register(se_obj)
|
||
|
print_dbg("Granting weapon of type %s", weapon)
|
||
|
local name = SYS_GetParam(0, weapon, "inv_name")
|
||
|
local quality = clamp(math.floor(cond / 25), 0, 3)
|
||
|
local message = gc("st_weapon_"..quality).." ".. gc(name) .. "\\n"
|
||
|
-- send_tip(db.actor, message, nil, "swiss_knife", 6000)
|
||
|
return message
|
||
|
|
||
|
end
|
||
|
|
||
|
function open_lootbox(box, tool)
|
||
|
local id = box:id()
|
||
|
if not (lootbox_contents[id]) then
|
||
|
print_dbg("Lootbox contents not found. Populating.")
|
||
|
populate_lootbox(id, box:section())
|
||
|
end
|
||
|
-- play cool animation, with cool sound
|
||
|
if get_config("animation") then
|
||
|
tool_sec = get_sec(tool)
|
||
|
local sound = ini_tools:r_string_ex(tool_sec, "sound") or "Plastic_Small_Pick"
|
||
|
local delay = ini_tools:r_float_ex(tool_sec, "duration") or 5
|
||
|
|
||
|
CreateTimeEvent("lootbox", "disable_ui", 0, function()
|
||
|
xr_effects.disable_ui_only(db.actor, nil)
|
||
|
return true
|
||
|
end)
|
||
|
utils_obj.play_sound(sound)
|
||
|
print_dbg("Start open box with id "..id)
|
||
|
RemoveTimeEvent("lootbox","box_open " .. id)
|
||
|
CreateTimeEvent("lootbox","box_open " .. id, delay,
|
||
|
function(id, give)
|
||
|
xr_effects.enable_ui_lite(db.actor, nil)
|
||
|
open_lootbox_timer(id, give)
|
||
|
return true
|
||
|
end, id, true)
|
||
|
else
|
||
|
RemoveTimeEvent("lootbox","box_open " .. id)
|
||
|
CreateTimeEvent("lootbox","box_open " .. id, 0.1,
|
||
|
function(id, give)
|
||
|
open_lootbox_timer(id, give)
|
||
|
return true
|
||
|
end, id, true)
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
function open_lootbox_timer(id, give)
|
||
|
if not lootbox_contents[id] then return false end
|
||
|
print_dbg("Opening box with id "..id)
|
||
|
local spawned_items = parse_lootstring(lootbox_contents[id])
|
||
|
local str = gc("st_lootbox_get") .. ":\\n"
|
||
|
-- give loot
|
||
|
for section, quantity in pairs(spawned_items) do
|
||
|
print_dbg("Creating %s of %s", quantity, section)
|
||
|
if section == "spooky" then
|
||
|
alife_create("m_poltergeist_normal_flame", db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id())
|
||
|
send_tip(db.actor, gc("st_spooky_free"), nil, "swiss_knife", 6000)
|
||
|
lootbox_contents.spooky = nil
|
||
|
elseif section == "money" then
|
||
|
local money_amt = tonumber(quantity) or 5000
|
||
|
db.actor:give_money(money_amt)
|
||
|
str = str ..money_amt .. " ".. gc("st_money") .. "\\n"
|
||
|
elseif sec_is_weapon(section) then
|
||
|
str = str .. give_weapon(section, quantity)
|
||
|
elseif item_device.device_npc_pda[section] then
|
||
|
local se_itm = alife_create_item(section, db.actor, item_prop_table)
|
||
|
local id = se_itm and se_itm.id
|
||
|
-- quantity will be the id of the schmuck this pda belongs to
|
||
|
pda_custom.register_pda(tonumber(quantity), section, id)
|
||
|
else
|
||
|
if section == "leatherman" then section = "leatherman_tool" end
|
||
|
local amt = tonumber(quantity) or 1
|
||
|
for j=1,amt do
|
||
|
local se_itm = alife_create_item(section, db.actor, item_prop_table)
|
||
|
end
|
||
|
if string.find(section, "__") then section = str_explode(section, "__")[1] end
|
||
|
local item_name = ui_item.get_sec_name(section) or "of something"
|
||
|
if string.find(section, "ammo") then
|
||
|
local str_ammos = amt > 1 and "st_loot_ammos" or "st_loot_ammo"
|
||
|
str = str.. amt.. " ".. gc(str_ammos) .. " "..gc(item_name).. "\\n"
|
||
|
|
||
|
else
|
||
|
str = str.. amt.. " "..gc(item_name).. "\\n"
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
send_tip(db.actor, str, nil, "swiss_knife", 6000)
|
||
|
if give then
|
||
|
lootbox_contents[id] = nil
|
||
|
-- manage uniques - once player opens, remove unique from appearing again
|
||
|
if not lootbox_contents.uniques then lootbox_contents.uniques = {} end
|
||
|
if lootbox_contents.uniques[id] then
|
||
|
local unique_sec = lootbox_contents.uniques[id]
|
||
|
lootbox_contents.uniques[id] = nil
|
||
|
lootbox_contents.uniques[unique_sec] = true
|
||
|
print_dbg("Lootbox unique %s opened", unique_sec)
|
||
|
end
|
||
|
lootbox_contents[id] = nil
|
||
|
alife_release_id(id)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- given a group id, return a lootbox section from that group, if applicable
|
||
|
function pick_lootbox(map)
|
||
|
|
||
|
if map_tiers[map] then
|
||
|
local group = nil
|
||
|
local bias = get_config("lootquality") or 1
|
||
|
while not group do
|
||
|
local m = random_entry(map_tiers[map])
|
||
|
if roll(m.chance * bias) then
|
||
|
group = m.group
|
||
|
end
|
||
|
end
|
||
|
return random_entry(variant_map[group])
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- spawn lootbox in box, tie the contents of box to creation time to prevent RNG cheese
|
||
|
function spawn_lootbox(box)
|
||
|
local id = box:id()
|
||
|
if lootbox_contents[id] then return end
|
||
|
local roll = rand(100)
|
||
|
if roll <= get_config("stashchance") then
|
||
|
local se_obj = alife_object(id)
|
||
|
local lvl = alife():level_name(game_graph():vertex(se_obj.m_game_vertex_id):level_id())
|
||
|
print_dbg("spawning box in %s", lvl)
|
||
|
local box_type = pick_lootbox(lvl)
|
||
|
|
||
|
if ini_sys:section_exist(box_type) then
|
||
|
print_dbg("Spawned in lootbox of type "..box_type)
|
||
|
se_itm = alife_create_item(box_type, box)
|
||
|
-- populate_lootbox(se_itm.id, "lootbox_"..box_type)
|
||
|
else
|
||
|
print_dbg("Could not spawn lootbox of type "..box_type)
|
||
|
end
|
||
|
end
|
||
|
if roll <= 10 then
|
||
|
local section = "lockpick"
|
||
|
local roll = rand(20)
|
||
|
-- 70% one pick, 25% bundle, 5% skelekey
|
||
|
if roll == 20 then
|
||
|
section = "skeleton_key"
|
||
|
-- elseif roll > 16 then
|
||
|
-- section = "bundle_lockpick"
|
||
|
else
|
||
|
section = "lockpick"
|
||
|
end
|
||
|
local se_obj = alife_create(section,box:position(),box:level_vertex_id(),box:game_vertex_id(),box:id(),false)
|
||
|
alife():register(se_obj)
|
||
|
end
|
||
|
-- set box id to true to mark that is has been checked
|
||
|
lootbox_contents[id] = true
|
||
|
end
|
||
|
|
||
|
-- spawn items on dead people
|
||
|
function spawn_stalker_loot(npc)
|
||
|
|
||
|
if rand(100) <= get_config("deathchance") then
|
||
|
local community = npc:character_community()
|
||
|
local rank_bias = (npc:rank() / 10000) or 1
|
||
|
if map_tiers[community] == nil then community = "stalker" end
|
||
|
local table = map_tiers[community]
|
||
|
local to_drop = nil
|
||
|
local iters = 0
|
||
|
while not to_drop do
|
||
|
local m = random_entry(map_tiers[community])
|
||
|
if not m.chance then m.chance = 0.5 end
|
||
|
if roll(m.chance * rank_bias) then
|
||
|
to_drop = m.items
|
||
|
end
|
||
|
end
|
||
|
tbl = str_explode(to_drop, ",")
|
||
|
if not tbl[2] then tbl[2] = 1 end
|
||
|
print_dbg("Spawning %s of %s on dead NPC", tbl[2], tbl[1])
|
||
|
for i=1,tonumber(tbl[2]) do
|
||
|
se_obj = alife_create(tbl[1], npc:position(), npc:level_vertex_id(), npc:game_vertex_id(), npc:id(), false)
|
||
|
alife():register(se_obj)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- monkey patch loot managers
|
||
|
SpawnTreasure = treasure_manager.try_spawn_treasure
|
||
|
|
||
|
function treasure_manager.try_spawn_treasure(box)
|
||
|
local id = box:id()
|
||
|
|
||
|
--printf("try_spawn_treasure [%s]",caches[id])
|
||
|
-- no spawn if the cache is already looted
|
||
|
if not (treasure_manager.caches[id]) then
|
||
|
return
|
||
|
end
|
||
|
spawn_lootbox(box)
|
||
|
SpawnTreasure(box)
|
||
|
end
|
||
|
|
||
|
-- string stuff
|
||
|
|
||
|
function build_diff(box_sec)
|
||
|
local difficulty = get_difficulty(box_sec) * 10
|
||
|
local clr = utils_xml.get_color_con(100 - tonumber(difficulty))
|
||
|
return clr .. " " .. gc("st_dot").. " " .. utils_xml.get_color("ui_gray_1") .. gc("st_box_difficulty") .. " " .. clr .. tostring(difficulty) .. "%" .. "\\n \\n" .. utils_xml.get_color("ui_gray_1")
|
||
|
end
|
||
|
|
||
|
function build_coins(box_sec)
|
||
|
local clr = utils_xml.get_color_con(0)
|
||
|
return clr .. " " .. gc("st_dot").. " " .. utils_xml.get_color("ui_gray_1") .. gc("st_box_difficulty") .. " " .. clr .. "??????" .. "\\n \\n" .. utils_xml.get_color("ui_gray_1")
|
||
|
end
|
||
|
|
||
|
function build_monoloot(box_sec)
|
||
|
local clr = utils_xml.get_color_con(0)
|
||
|
return clr .. " " .. gc("st_dot").. " " .. utils_xml.get_color("ui_gray_1") .. gc("st_box_difficulty") .. " " .. clr .. "9001%" .. "\\n \\n" .. utils_xml.get_color("ui_gray_1")
|
||
|
end
|
||
|
|
||
|
-- get custom display of difficulty
|
||
|
function build_string(box_sec)
|
||
|
local precond = SYS_GetParam(0, box_sec, "diff_str") or "arti_lootboxes.build_diff"
|
||
|
precond = str_explode(precond,"%.")
|
||
|
if _G[precond[1]] and _G[precond[1]][precond[2]] then
|
||
|
return _G[precond[1]][precond[2]](box_sec)
|
||
|
else return "" end
|
||
|
end
|
||
|
|
||
|
|
||
|
BuildFooter = ui_item.build_desc_footer
|
||
|
function ui_item.build_desc_footer(obj, sec, str)
|
||
|
str = str or gc(ini_sys:r_string_ex(sec,"description"))
|
||
|
if (not str) then return "" end
|
||
|
if is_box(nil, sec) then
|
||
|
return str .. build_string(sec)
|
||
|
else
|
||
|
return BuildFooter(obj, sec, str)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
CreateReleaseItem = death_manager.create_release_item
|
||
|
function death_manager.create_release_item(npc)
|
||
|
CreateReleaseItem(npc)
|
||
|
spawn_stalker_loot(npc)
|
||
|
end
|
||
|
|
||
|
|
||
|
function on_game_start()
|
||
|
if (USE_MARSHAL) then
|
||
|
RegisterScriptCallback("save_state",save_state)
|
||
|
RegisterScriptCallback("load_state",load_state)
|
||
|
end
|
||
|
RegisterScriptCallback("actor_on_item_take_from_box",actor_on_item_take_from_box)
|
||
|
RegisterScriptCallback("ActorMenu_on_item_drag_drop",on_item_drag_dropped)
|
||
|
RegisterScriptCallback("server_entity_on_unregister", server_entity_on_unregister)
|
||
|
RegisterScriptCallback("actor_on_item_take",spook_player)
|
||
|
init()
|
||
|
end
|