diff options
author | Singustromo <singustromo@disroot.org> | 2024-03-06 14:05:05 +0100 |
---|---|---|
committer | Singustromo <singustromo@disroot.org> | 2024-03-06 14:05:05 +0100 |
commit | a6bbda53ee681140363f36fbb81c74c0d0f0b080 (patch) | |
tree | b95c2894246c9ebb2d70f57a89ec8a83137913d1 | |
parent | 2da9c5c5ab1236701113025a5c6a59e62d2c810c (diff) | |
download | revised-ammo-maker-a6bbda53ee681140363f36fbb81c74c0d0f0b080.tar.gz revised-ammo-maker-a6bbda53ee681140363f36fbb81c74c0d0f0b080.zip |
Too many changes, can't remember
6 files changed, 779 insertions, 737 deletions
diff --git a/src/001 - Main Files/gamedata/configs/plugins/ammo_maker/importer.ltx b/src/001 - Main Files/gamedata/configs/plugins/ammo_maker/importer.ltx index f1db254..fe21741 100644 --- a/src/001 - Main Files/gamedata/configs/plugins/ammo_maker/importer.ltx +++ b/src/001 - Main Files/gamedata/configs/plugins/ammo_maker/importer.ltx @@ -11,7 +11,7 @@ ;salvage_rate = 1.0 ; result will never exceed part count of crafting recipe ;degradation = 0.003 ; rate for tool per salvage -;; ammunition sections are still declared as before +;; ammunition sections are still declared like before ;; parts = values are ignored and read from the recipe ;[ammo_357_hp_mag] ;salvage_rate = 0.75 diff --git a/src/001 - Main Files/gamedata/scripts/ammo_maker.script b/src/001 - Main Files/gamedata/scripts/ammo_maker.script index 9ce5161..f3b1a63 100644 --- a/src/001 - Main Files/gamedata/scripts/ammo_maker.script +++ b/src/001 - Main Files/gamedata/scripts/ammo_maker.script @@ -1,565 +1,544 @@ ---[[ - Name: (Revised) Ammo Maker - Author: Arti, Singustromo - Source: https://www.moddb.com/mods/stalker-anomaly/addons/revised-ammo-maker - Upstream: https://github.com/ahuyn/anomaly-compilation/tree/master/ammo%20maker - - Functions for external use: - get_ammo_recipe(section) - breakdown(ammo, tool, batch) - is_component(obj) - is_component_by_section(sec) - adjust_box_by_id(id, count) - create_components(tbl, npc) ---]] - -version=20240110 - -local ammo_settings_ini - -settings = ammo_maker_mcm -randomizer = libmath_bezier - -option = {} -- options set via mcm script -recipe_cache = {} - --------------------------- --- Debugging --------------------------- - -function log(message, ...) - if (option.debug) then printf("[ammo_maker] " .. message, ...) end -end - -dbg_results = {} - --- for debugging; does not persist throughout game reloads -function log_results(tbl) - printf("- Results until now:") - for name, amount in pairs(tbl) do - if amount then - dbg_results[name] = dbg_results[name] - and dbg_results[name] + amount or amount - end - printf("- [%s] %s", name, dbg_results[name]) - end -end - -function float_display(number) - return string.format("%.2f", number) -end - --- Only applies fraction between 0 and 1 as factor --- factor is applied when chance is nil / according to chance -function apply_factor(amount, limit, factor, chance) - if not factor or factor <= 0 or factor == 1 then return amount end - if chance and (type(chance) ~= 'number' or chance <= 0) then return amount end - - local product = factor * amount - local apply_limit = (factor > 1 and product > limit) - or (factor <= 1 and product <= limit) - local new_amount = (apply_limit) and limit or product - - if (chance and math.random(0,100) > chance*100) then - return amount - end - log("apply factor | %s*%s = %s", float_display(amount), factor, float_display(new_amount)) - return new_amount -end - -function valid_preset_values(tbl) - if not tbl then return end - local n = 0 - for k,v in pairs(tbl) do - v = tonumber(v) -- technically a string (nil if not number) - if not (v and v >=0 and v <= 1) then return end - n = n +1 - end - if not (n == 3 or n == 4) then return end - - return true -end - --- Also checks, if preset values are malformed -function ini_retrieve_preset_values(preset_name) - -- Will be used as sane fallback values - local presets = { - low = {0,0.62,0,1}, -- ~40% - normal = {0,1,0.4,1}, -- ~60% - high = {0,1,1,1}, -- ~75% - } - - local collected_presets = ammo_settings_ini:collect_section("preset") - - -- empty section; return fallback - if size_table(collected_presets) < 1 then - return presets[preset_name] - end - - local preset_values = str_explode(collected_presets[preset_name], ",") - if preset_values and valid_preset_values(preset_values) then - return preset_values - end - - return presets[preset_name] -end - ----------------------------- --- Callbacks ----------------------------- - -function load_settings() - if not (settings and settings.defaults) then - printe("[ammo_maker] MCM script not found! Unable to load defaults.") - callstack(true) - return - end - - for k, _ in pairs(settings.defaults) do - option[k] = settings.get_value(k) - end - - if option.salvage_preset == "dynamic" then - local tbl = { "high", "normal", "low" } - option.salvage_preset = tbl[game_difficulties.get_eco_factor("type") or 2] - end -end - ----------------------------- --- Dependencies ----------------------------- - -function random_amount(lower, upper, part_name) - local preset = ini_retrieve_preset_values(option.salvage_preset) - local result = randomizer.get_random_value(lower, upper, preset) - - log("randomizer | {%s} | %s [%s:%s] => %s", table.concat(preset, ","), - part_name, lower, upper, float_display(result)) - - return result -end - ----------------------------- --- Main ----------------------------- - --- slightly modified from workshop_autoinject -function valid_recipe(craft_string) - if not craft_string then return end - local t = str_explode(craft_string,",") - - if not (#t >= 6 and #t <= 10 and #t%2 == 0) then - log("Not enough components in recipe!") - return - end - - local tool = tonumber(t[1]) - if tool < 0 or tool > 5 then - log("Invalid tool %s! Must be 0-5", tool) - return - end - - if not string.find(t[2], "recipe") then - log("Invalid recipe %s!", t[2]) - return - end - - for i=3,#t,2 do -- skipping tool, rsp - local item = t[i] - local count = tonumber(t[i+1]) - if not ini_sys:section_exist(item) then - log("Invalid component %s!", item) - return - end - if not count or count <= 0 then - log("Invalid amount for component %s!", count) - return - end - end - return true -end - --- returns: table { tool, rsp, section, amount, section, amount, ... } -function get_ammo_recipe(section) - if not (section and string.find(section, "ammo_")) then return end - log("get_ammo_recipe | got '%s' as section", section) - - -- we use the same recipe for old ammo and half the yield later on - if string.find(section, "_bad") then - section = section:gsub("_bad", "") - end - - -- Using cache, because this function is indirectly called on hover - if recipe_cache.section then - log("get_ammo_recipe | Using cached values") - return recipe_cache.section - end - - if not ini_sys:section_exist(section) - or is_component_by_section(section) then return end - - local recipe = itms_manager.ini_craft:r_string_ex(6, "x_" .. section) - - if recipe == "" or not recipe then - printe("! ammo maker | No crafting recipe found for '%s'", section) - return - end - - if not valid_recipe(recipe) then - printe("! ammo maker | Invalid crafting recipe for '%s'", section) - return - end - - local tbl = str_explode(recipe, ",") - recipe_cache[section] = tbl - return tbl -end - --- Breaks ammunition boxes down --- Returns: table (Parts, their quantity and the overall ammo count) -function do_breakdown(ammo_list, tool_condition) - if is_empty(ammo_list) or not tool_condition then return end - - local tbl = {} - local release_table = {} - - local sec = ammo_list[1]:section() - local ammo_name = ui_item.get_sec_name(sec) - local parts_map = get_ammo_recipe(sec) - if not (parts_map) then return end - - -- salvage settings - local salvage_rate = ammo_settings_ini:r_float_ex(sec, "salvage_rate") or 1 - local degradation_rate = (ammo_settings_ini:r_float_ex(sec, "degradation") - or 0.003) * option.tool_degradation - - if string.find(sec, "_bad") then - salvage_rate = salvage_rate * option.rate_bad - end - - log("Salvaging %s | rate: %s | preset: %s", - ammo_name, salvage_rate, option.salvage_preset) - - local tool_degrade_sum = 0 - for _, box in pairs(ammo_list) do - local ammo_count = box:ammo_get_count() - local ammo_box_size = box:ammo_box_size() - - for i=3,#parts_map,2 do -- i=3: skipping <tool, unlock_recipe> values - local part_to_make = parts_map[i] - local part_modifier = tonumber(parts_map[i+1]) - local part_box_size = SYS_GetParam(2, part_to_make, "box_size", 15) - - local count = { min = 0, max = part_modifier * part_box_size, final = 0 } - - count.max = (ammo_count < ammo_box_size) - and ammo_count * (count.max / ammo_box_size) - or count.max - - count.min = (option.minimum_salvage > 0) - and option.minimum_salvage * count.max or 0 - - count.final = random_amount( - math.floor(count.min), round(count.max), part_to_make) - --- MOVE TO SEPARATE FUNCTION (Handle bonuses) - count.final = apply_factor(count.final, count.max, - string.find(part_to_make, "powder") and option.powder_bonus or 1) - - count.final = apply_factor(count.final, salvage_rate < 1 and count.min or count.max, salvage_rate) ---------------- - - count.final = count.final < 1 and 0 or round(count.final) - if (count.final > 0) then - if tbl[part_to_make] then - tbl[part_to_make] = tbl[part_to_make] + count.final - else - tbl[part_to_make] = count.final - end - end - end - - tbl['ammo_count'] = (tbl['ammo_count'] == nil) - and ammo_count or tbl['ammo_count'] + ammo_count - - table.insert(release_table, box:id()) - - tool_degrade_sum = tool_degrade_sum + degradation_rate * (ammo_count/ammo_box_size) - - -- short circuit, if tool gets depleted - if tool_degrade_sum >= tool_condition then break end - end - - return tbl, release_table, tool_degrade_sum -end - -function degrade_tool(tool, amount) - if not (tool and amount) then return end - log("degrade | %s (%s) by %s", - tool:section(), float_display(tool:condition()), amount) - - utils_item.degrade(tool, amount) -end - -function release_ammo_boxes(release_table) - if not (release_table and type(release_table) == 'table') then return end - if is_empty(release_table) then return end - - log("Releasing following IDs:\n/%s", table.concat(release_table, ", ")) - for _, id in pairs(release_table) do - alife_release_id(id) - end - return true -end - -function breakdown(ammo, tool, batch) - local ammo_sec = ammo and ammo:section() - local tool_condition = tool and tool:condition() - - if not (ammo_sec and tool_condition) then return end - - local parts, to_release, degrade_amount - local to_breakdown = {} - if (batch) then - db.actor:iterate_ruck(function(temp, item) - if (item:section() == ammo_sec) then - table.insert(to_breakdown, item) - end - end) - else - to_breakdown[1] = ammo - end - - parts, to_release, degrade_amount = do_breakdown(to_breakdown, tool_condition) - - if not release_ammo_boxes(to_release) then return end - degrade_tool(tool, degrade_amount) - - game_statistics.increment_statistic("items_disassembled") - actor_effects.play_item_fx("disassemble_metal_fast") -- Animation - - if not (parts and type(parts) == 'table') then return end - local key = "ammo_count" - local ammo_count = parts[key] - parts[key] = nil - - local npc = ammo.parent and type(ammo.parent) == 'function' and ammo:parent() - if not create_components(parts, npc) then return end - - show_salvage_news(parts, ammo_sec, ammo_count) - - if option.debug then - log_results(parts) - end -end - ----------------------------- --- Bypass Base Functions ----------------------------- - -function is_component_by_section(sec) - if not (sec and ini_sys:section_exist(sec)) then return end - return SYS_GetParam(1, sec, "is_component", false) -end - -function is_component(obj) - if not obj then return end - local sec = obj:section() - return is_component_by_section(sec) -end - --- changes box_size of an ammo object by id -function adjust_box_by_id(id, count) - if not (id and count) then return true end - local box = get_object_by_id(id) - - if (box) then - if IsAmmo(box) then - box:ammo_set_count(count) - end - return true - end -end - --- combines stacks in actor inventory of the same type as id -function aggregate_components_by_id(id) - if (not id) then return true end - local obj = get_object_by_id(id) - if (obj) then - item_weapon.ammo_aggregation(obj) - return true - end -end - --- Spawns components tbl{sec = amount, ...} in players inventory --- Circumvent get_object_by_id() errors and missing parts caused by alife():create_ammo(..) --- TODO: add checks from itms_manager: ItemProcessor:Create_Item -function create_components(tbl, npc) - if not type(tbl) == 'table' then return end - - if (not npc) then - npc = db.actor - end - - local aggregation_list = {} - - for part, amount in pairs(tbl) do - if not is_component_by_section(part) then - log("CREATE | %s not a component, skipping", part) - goto continue - end - - local part_box_size = SYS_GetParam(2, part, "box_size", 15) - local stacks_to_spawn = math.floor(amount / part_box_size) - amount = amount % part_box_size - - if stacks_to_spawn > 0 then - log("CREATE | %s | %s stacks (%s each)", part, - stacks_to_spawn, part_box_size) - end - - while (stacks_to_spawn > 0) do - alife_create(part, npc, true) - stacks_to_spawn = stacks_to_spawn - 1 - end - - if (amount < 1) then goto continue end - log("CREATE | %s | %s pieces", part, amount) - - local se_obj = alife_create(part, npc, true) - if not (se_obj) then goto continue end - - CreateTimeEvent("ammo_maker", "set_box_size_"..se_obj.id, 0.02, - adjust_box_by_id, se_obj.id, amount) - table.insert(aggregation_list, se_obj.id) - - ::continue:: - end - - for _, id in pairs(aggregation_list) do - CreateTimeEvent(ammo_maker, "component_aggregation_" .. id, - 0.2, aggregate_components_by_id, id) - end - - return true -end - ------------------------------ --- Monkey Patches ------------------------------ - -function _is_suitable_dtool(obj, obj_d) - if item_parts.is_suitable_dtool(obj, obj_d) then return true end - - if IsWeapon(obj) then - actor_menu.set_item_news('fail', 'weapon', "st_dis_text_3", " ") - else - news_manager.send_tip(db.actor, game.translate_string("st_news_dis_items"), nil, "swiss_knife", 6000) - end -end - -function wrap_disassemble(obj, obj_d, batch) - obj_d = obj_d or item_parts.get_suitable_dtool(obj) - if not _is_suitable_dtool(obj, obj_d) then return end - - if IsAmmo(obj) then - breakdown(obj, obj_d, batch) - else - item_parts.disassembly_item(obj, obj_d) - end -end - -Disassemble = item_parts.disassembly_item -function item_parts.disassembly_item(obj, obj_d) - obj_d = obj_d or item_parts.get_suitable_dtool(obj) - if not _is_suitable_dtool(obj, obj_d) then return end - - if IsAmmo(obj) then - breakdown(obj, obj_d, false) - else - Disassemble(obj, obj_d) - end -end - ----------------------------- --- User Interface ----------------------------- - -function create_disassemble_list(t) - local str = "" - for k,v in pairs(t) do - str = str .. "\\n- " .. v .. " " .. ui_item.get_sec_name(k) - end - return str -end - -function show_salvage_news(tbl, sec, ammo_count) - if not option.show_news then return end - - local name = sec and ui_item.get_sec_name(sec) - local parts_list = create_disassemble_list(tbl) - local msg = "" - local type= "success" - - local title = (ammo_count) and (ammo_count .. "x " .. name) or name - - if (not parts_list or parts_list == "") then - msg = "\\n" .. game.translate_string("st_dis_text_no_result") - type = "fail" - else - msg = msg .. game.translate_string("st_dis_text_9") - end - - actor_menu.set_item_news(type, "weapon_ammo", "st_dis_text_11", title, msg, parts_list) -end - -function get_ruck_ammocount(obj) - if not (obj) then return end - - local count = 0 - db.actor:iterate_ruck(function(tmp, item) - if (item:section() == obj:section()) then - count = count + item:ammo_get_count() - end - end) - - return count -end - -function check_batch(obj) - if not (obj and get_ammo_recipe(obj:section())) then return end - if get_ruck_ammocount(obj) < obj:ammo_box_size() +1 then return end - - return 'st_batch_breakdown' -end - -NameCustom = ui_inventory.UIInventory.Name_Custom -function ui_inventory.UIInventory:Name_Custom(obj, bag, temp, i) - obj = self:CheckItem(obj,"Name_Custom " .. i) - if i == 4 and IsAmmo(obj) then - return check_batch(obj) - else - return NameCustom(self, obj, bag, temp, i) - end -end - -ActionCustom = ui_inventory.UIInventory.Action_Custom -function ui_inventory.UIInventory:Action_Custom(obj, bag, temp, i) - obj = self:CheckItem(obj,"Action_Custom " .. i) - if i == 4 and IsAmmo(obj) then - wrap_disassemble(obj, nil, true) - else - ActionCustom(self, obj, bag, temp, i) - end -end - ----------------------------- --- Initialization ----------------------------- - -function on_game_start() - RegisterScriptCallback("on_option_change", load_settings) - RegisterScriptCallback("on_game_load", load_settings) - - ammo_settings_ini = ini_file_ex("plugins\\ammo_maker\\importer.ltx") -end +--[[ + REVISED AMMO MAKER + (https://www.moddb.com/mods/stalker-anomaly/addons/revised-ammo-maker) + + Author + Singustromo <singustromo at disroot.org> + + Upstream + https://github.com/ahuyn/anomaly-compilation/tree/master/ammo%20maker + Authors + Arti, Maid + + Functions for external use: + get_ammo_recipe(section) + breakdown(ammo, tool, batch) + is_component(obj) + is_component_by_section(sec) + adjust_box_by_id(id, count) + create_components(tbl, npc) +--]] + +VERSION=20240302 + +local ammo_settings_ini + +option = {} -- options set via mcm script +recipe_cache = {} + +---------------------------- +-- Dependencies -- +---------------------------- + +settings = ammo_maker_mcm +randomizer = libmath_bezier + +assert(settings, "Ammo Maker: Unable to assign MCM script!") +assert(randomizer, "Ammo Maker: Unable to assign libmath_bezier script!") + +-------------------------- +-- Debugging -- +-------------------------- + +function log(message, ...) + if (option.debug) then printf("[ammo_maker] " .. message, ...) end +end + +function float_display(number) + return number and string.format("%.2f", number) +end + +-- Only applies fraction between 0 and 1 as factor +-- factor is applied when chance is nil or according to chance +function apply_factor(amount, limit, factor, chance) + if not factor or factor <= 0 or factor == 1 then return amount end + if chance and (type(chance) ~= 'number' or chance <= 0) then return amount end + + local product = factor * amount + local apply_limit = (factor > 1 and product > limit) + or (factor <= 1 and product <= limit) + local new_amount = (apply_limit) and limit or product + + if (chance and math.random(0,100) > chance * 100) then + return amount + end + + log("apply factor | %s * %s = %s", + float_display(amount), factor, float_display(new_amount)) + return new_amount +end + +function valid_preset_values(tbl) + if not tbl then return end + local n = 0 + for k,v in pairs(tbl) do + v = tonumber(v) -- technically a string (nil if not number) + if not (v and v >=0 and v <= 1) then return end + n = n +1 + end + if not (n == 3 or n == 4) then return end + + return true +end + +-- Also checks, if preset values are malformed +function ini_retrieve_preset_values(preset_name) + -- Will be used as sane fallback values + local presets = { + low = {0,0.62,0,1}, -- ~40% + normal = {0,1,0.4,1}, -- ~60% + high = {0,1,1,1}, -- ~75% + } + + local collected_presets = ammo_settings_ini:collect_section("preset") + + -- empty section; return fallback + if size_table(collected_presets) < 1 then + return presets[preset_name] + end + + local preset_values = str_explode(collected_presets[preset_name], ",") + if preset_values and valid_preset_values(preset_values) then + return preset_values + end + + return presets[preset_name] +end + +-- This modifier adjusts the upper bound of the randomizer parameter +function get_tool_salvage_modifier(tool) + if not tool then return end + local section = tool:section() + local modifiers = { + grooming = 0.8, + swiss_knife = 0.9, + leatherman_tool = 1, + } + + return modifiers[section] or 1 +end + +---------------------------- +-- Callbacks +---------------------------- + +function load_settings() + if not (settings and settings.defaults) then + printe("[ammo_maker] MCM script not found! Unable to load defaults.") + callstack(true) + return + end + + for k, _ in pairs(settings.defaults) do + option[k] = settings.get_value(k) + end + + if option.salvage_preset == "dynamic" then + local tbl = { "high", "normal", "low" } + option.salvage_preset = tbl[game_difficulties.get_eco_factor("type") or 2] + end +end + +------------------------ +-- Dependencies -- +------------------------ + +function random_amount(lower, upper, part_name, tool) + local preset = ini_retrieve_preset_values(option.salvage_preset) + local tool_salvage_modifier = get_tool_salvage_modifier(tool) + + upper = (tool_salvage_modifier) and (upper * tool_salvage_modifier) + or upper + + local result = randomizer.get_random_value(lower, upper, preset) + log("randomizer | {%s} | %s [%s:%s] => %s", table.concat(preset, ","), + part_name, lower, upper, float_display(result)) + + return result +end + +------------------------ +-- Ammo recipes -- +------------------------ + +-- slightly modified from workshop_autoinject +function valid_recipe(craft_string) + if not craft_string then return end + local t = str_explode(craft_string,",") + + if not (#t >= 6 and #t <= 10 and #t%2 == 0) then + log("Not enough components in recipe!") + return + end + + local tool = tonumber(t[1]) + if tool < 0 or tool > 5 then + log("Invalid tool %s! Must be 0-5", tool) + return + end + + if not string.find(t[2], "recipe") then + log("Invalid recipe %s!", t[2]) + return + end + + for i=3,#t,2 do -- skipping tool, rsp + local item = t[i] + local count = tonumber(t[i+1]) + if not ini_sys:section_exist(item) then + log("Invalid component %s!", item) + return + end + if not count or count <= 0 then + log("Invalid amount for component %s!", count) + return + end + end + return true +end + +-- returns: table { tool, rsp, section, amount, section, amount, ... } +function get_ammo_recipe(section) + if not (section and string.find(section, "ammo_")) then return end + log("get_ammo_recipe | got '%s' as section", section) + + -- we use the same recipe for old ammo and half the yield later on + section = (string.find(section, "_bad")) + and section:gsub("_bad", "") or section + + -- Using cache, because this function is called on inventory hover + if recipe_cache.section then + log("get_ammo_recipe | Using cached values") + return recipe_cache.section + end + + if not ini_sys:section_exist(section) + or is_component_by_section(section) then return end + + -- Ammo is declared there under section 6 + local recipe = itms_manager.ini_craft:r_string_ex(6, "x_" .. section) + + if recipe == "" or not recipe then + printe("! ammo maker | No crafting recipe found for '%s'", section) + return + end + + if not valid_recipe(recipe) then + printe("! ammo maker | Invalid crafting recipe for '%s'", section) + return + end + + local tbl = str_explode(recipe, ",") + recipe_cache[section] = tbl + return tbl +end + +------------------------ +-- Main -- +------------------------ + +-- Breaks ammunition boxes down +-- Returns: table (Parts, their quantity and the overall ammo count) +function do_breakdown(ammo_list, tool_condition, tool) + if is_empty(ammo_list) or not tool_condition then return end + + local tbl = {} + local release_table = {} + + local sec = ammo_list[1]:section() + local ammo_name = ui_item.get_sec_name(sec) + local parts_map = get_ammo_recipe(sec) + if not (parts_map) then return end + + -- salvage settings + local salvage_rate = ammo_settings_ini:r_float_ex(sec, "salvage_rate") or 1 + local degradation_rate = (ammo_settings_ini:r_float_ex(sec, "degradation") + or 0.003) * option.tool_degradation + + if string.find(sec, "_bad") then + salvage_rate = salvage_rate * option.rate_bad + end + + log("Salvaging %s | rate: %s | preset: %s", + ammo_name, salvage_rate, option.salvage_preset) + + local tool_degrade_sum = 0 + for _, box in pairs(ammo_list) do + local ammo_count = box:ammo_get_count() + local ammo_box_size = box:ammo_box_size() + + for i=3,#parts_map,2 do -- i=3: skipping <tool, unlock_recipe> values + local part_to_make = parts_map[i] + local part_modifier = tonumber(parts_map[i+1]) + local part_box_size = SYS_GetParam(2, part_to_make, "box_size", 15) + + local count = { min = 0, max = part_modifier * part_box_size, final = 0 } + + count.max = (ammo_count < ammo_box_size) + and ammo_count * (count.max / ammo_box_size) + or count.max + + count.min = (option.minimum_salvage > 0) + and option.minimum_salvage * count.max or 0 + + count.final = random_amount(math.floor(count.min), round(count.max), part_to_make, tool) + +-- MOVE TO SEPARATE FUNCTION (Handle bonuses) + count.final = apply_factor(count.final, count.max, + string.find(part_to_make, "powder") and option.powder_bonus or 1) + + count.final = apply_factor(count.final, salvage_rate < 1 and count.min or count.max, salvage_rate) +--------------- + + count.final = count.final < 1 and 0 or round(count.final) + if (count.final > 0) then + if tbl[part_to_make] then + tbl[part_to_make] = tbl[part_to_make] + count.final + else + tbl[part_to_make] = count.final + end + end + end + + tbl['ammo_count'] = (tbl['ammo_count'] == nil) + and ammo_count or tbl['ammo_count'] + ammo_count + + table.insert(release_table, box:id()) + + tool_degrade_sum = tool_degrade_sum + degradation_rate * (ammo_count / ammo_box_size) + + -- short circuit, if tool gets depleted + if tool_degrade_sum >= tool_condition then break end + end + + return tbl, release_table, tool_degrade_sum +end + +function degrade_tool(tool, amount) + if not (tool and amount) then return end + log("degrade | %s (%s) by %s", + tool:section(), float_display(tool:condition()), amount) + + utils_item.degrade(tool, amount) +end + +function release_ammo_boxes(release_table) + if not (release_table and type(release_table) == 'table') then return end + if is_empty(release_table) then return end + + log("Releasing following IDs:\n/%s", table.concat(release_table, ", ")) + for _, id in pairs(release_table) do + alife_release_id(id) + end + return true +end + +-- This is our main routine +function breakdown(ammo, tool, batch) + local ammo_sec = ammo and ammo:section() + local tool_condition = tool and tool:condition() + if not (ammo_sec and tool_condition) then return end + + local parts, to_release, degrade_amount + local to_breakdown = {} + if (batch) then + db.actor:iterate_ruck(function(temp, item) + if (item:section() == ammo_sec) then + table.insert(to_breakdown, item) + end + end) + else + to_breakdown[1] = ammo + end + + parts, to_release, degrade_amount = do_breakdown( + to_breakdown, tool_condition, tool) + + if not release_ammo_boxes(to_release) then return end + degrade_tool(tool, degrade_amount) + + game_statistics.increment_statistic("items_disassembled") + actor_effects.play_item_fx("disassemble_metal_fast") -- Animation + + if not (parts and type(parts) == 'table') then return end + local key = "ammo_count" + local ammo_count = parts[key] + parts[key] = nil + + --local npc = ammo.parent and type(ammo.parent) == 'function' and ammo:parent() + if not create_components(parts) then return end + + show_salvage_news(parts, ammo_sec, ammo_count) +end + +---------------------------- +-- Bypass Base Functions +---------------------------- + +function is_component_by_section(sec) + if not (sec and ini_sys:section_exist(sec)) then return end + return SYS_GetParam(1, sec, "is_component", false) +end + +function is_component(obj) + local sec = obj and obj:section() + return sec and is_component_by_section(sec) +end + +-- changes box_size of an ammo object by id +function adjust_box_by_id(id, count) + if not (id and count) then return true end + + local box = get_object_by_id(id) + if not (box) then return end + + if IsAmmo(box) then + box:ammo_set_count(count) + end + return true +end + +-- combines stacks in actor inventory of the same type as id +function aggregate_components_by_id(id) + if (not id) then return true end + + local obj = get_object_by_id(id) + if not (obj) then return end + + item_weapon.ammo_aggregation(obj) + return true +end + +-- Circumvents get_object_by_id() errors and missing parts caused by alife():create_ammo(..) +-- Spawns components tbl{sec = amount, ...} in players inventory +-- TODO: add checks from itms_manager: ItemProcessor:Create_Item +function create_components(tbl, npc) + if not type(tbl) == 'table' then return end + + if (not npc) then + npc = db.actor + end + + local aggregation_list = {} + + for part, amount in pairs(tbl) do + if not is_component_by_section(part) then + log("CREATE | %s not a component, skipping", part) + goto continue + end + + local part_box_size = SYS_GetParam(2, part, "box_size", 15) + local stacks_to_spawn = math.floor(amount / part_box_size) + amount = amount % part_box_size + + if stacks_to_spawn > 0 then + log("CREATE | %s | %s stacks (%s each)", part, + stacks_to_spawn, part_box_size) + end + + while (stacks_to_spawn > 0) do + alife_create(part, npc, true) + stacks_to_spawn = stacks_to_spawn - 1 + end + + if (amount < 1) then goto continue end + log("CREATE | %s | %s pieces", part, amount) + + local se_obj = alife_create(part, npc, true) + if not (se_obj) then goto continue end + + CreateTimeEvent("ammo_maker", "set_box_size_"..se_obj.id, 0, + adjust_box_by_id, se_obj.id, amount) + table.insert(aggregation_list, se_obj.id) + + ::continue:: + end + + for _, id in pairs(aggregation_list) do + CreateTimeEvent(ammo_maker, "component_aggregation_" .. id, + 0.1, aggregate_components_by_id, id) + end + + return true +end + +----------------------------- +-- Monkey Patches +----------------------------- + +Disassemble = item_parts.disassembly_item +function item_parts.disassembly_item(obj, obj_d) + obj_d = obj_d or item_parts.get_suitable_dtool(obj) + if not _is_suitable_dtool(obj, obj_d) then return end + + if IsAmmo(obj) then + breakdown(obj, obj_d, false) + else + Disassemble(obj, obj_d) + end +end + +-- called through ui_inventory drop-down menu monkey patch +function wrap_disassemble(obj, obj_d, batch) + obj_d = obj_d or item_parts.get_suitable_dtool(obj) + if not _is_suitable_dtool(obj, obj_d) then return end + + if IsAmmo(obj) then + breakdown(obj, obj_d, batch) + else + item_parts.disassembly_item(obj, obj_d) + end +end + +function _is_suitable_dtool(obj, obj_d) + if item_parts.is_suitable_dtool(obj, obj_d) then return true end + + if IsWeapon(obj) then + actor_menu.set_item_news('fail', 'weapon', "st_dis_text_3", " ") + else + news_manager.send_tip(db.actor, game.translate_string("st_news_dis_items"), nil, "swiss_knife", 6000) + end +end + +---------------------------- +-- User Interface +---------------------------- + +function create_disassemble_list(t) + local str = "" + for k,v in pairs(t) do + str = str .. "\\n- " .. v .. " " .. ui_item.get_sec_name(k) + end + return str +end + +function show_salvage_news(tbl, sec, ammo_count) + if not option.show_news then return end + + local name = sec and ui_item.get_sec_name(sec) + local parts_list = create_disassemble_list(tbl) + local msg = "" + local type= "success" + + local title = (ammo_count) and (ammo_count .. "x " .. name) or name + + if (not parts_list or parts_list == "") then + msg = "\\n" .. game.translate_string("st_dis_text_no_result") + type = "fail" + else + msg = msg .. game.translate_string("st_dis_text_9") + end + + actor_menu.set_item_news(type, "weapon_ammo", "st_dis_text_11", title, msg, parts_list) +end + +---------------------------- +-- Initialization +---------------------------- + +function on_game_start() + RegisterScriptCallback("on_option_change", load_settings) + RegisterScriptCallback("on_game_load", load_settings) + + ammo_settings_ini = ini_file_ex("plugins\\ammo_maker\\importer.ltx") +end diff --git a/src/001 - Main Files/gamedata/scripts/ammo_maker_mcm.script b/src/001 - Main Files/gamedata/scripts/ammo_maker_mcm.script index 58e0141..8b5bdf2 100644 --- a/src/001 - Main Files/gamedata/scripts/ammo_maker_mcm.script +++ b/src/001 - Main Files/gamedata/scripts/ammo_maker_mcm.script @@ -1,61 +1,62 @@ ---[[ - Name: (Revised) Ammo Maker - Version: 1.2 - Author: Singustromo - Source: https://www.moddb.com/mods/stalker-anomaly/addons/revised-ammo-maker ---]] - -parent = ammo_maker -local acronym = "ram" - --- If you don't use MCM, change your defaults from here. -defaults = { - ["salvage_preset"] = "dynamic", -- low|normal|high|dynamic (progression difficulty) - ["tool_degradation"] = 4.5, - ["powder_bonus"] = 1.05, -- 1 to deactivate - ["minimum_salvage"] = 0, - ["rate_bad"] = 0.35, -- salvage rate for old ammunition - ["show_news"] = true, -- pda notification - ["debug"] = false, -- debug messages -} - -local clr_title = {255, 255, 255, 0} -local clr_desc = {200, 200, 255, 200} -local clr_notice = {255, 232, 61, 102} - -function on_mcm_load() - if not (parent and parent.get_ammo_recipe) then return end - - op = { id=acronym, sh=true, gr={ - {id = "title", type="slide", link="ui_options_slider_gameplay_diff", text="ui_mcm_menu_"..acronym.."_title", size={512,50}, spacing=20}, - {id = "salvage_preset", type="list", val=0, def=defaults["salvage_preset"], - content={ - {"low", acronym.."_preset_low"}, - {"normal", acronym.."_preset_normal"}, - {"high", acronym.."_preset_high"}, - {"dynamic", acronym.."_preset_dynamic"}, - }, - }, - {id = "tool_degradation", type="track", val=2, min=0, max=10, step=0.5, def=defaults["tool_degradation"]}, - {id = "powder_bonus", type="track", val=2, min=0.5, max=1.2, step=0.02, def=defaults["powder_bonus"]}, - {id = "minimum_salvage", type="track", val=2, min=0, max=0.4, step=0.05, def=defaults["minimum_salvage"]}, - {id = "rate_bad", type="track", val=2, min=0, max=1, step=0.05, def=defaults["rate_bad"]}, - {id = "show_news", type="check", val=1, def=defaults["show_news"]}, - - {id = "divider", type = "line" }, - {id = "header", type = "desc", text = "ui_mcm_"..acronym.."_title_debug", clr=clr_title}, - {id = "debug", type="check", val=1, def=defaults["debug"]}, - }, - } - return op -end - -function get_value(key) - if not (key and type(key) == 'string') then return end - - if ui_mcm and type(ui_mcm.get) == 'function' then - return ui_mcm.get(acronym .. "/" .. key) - end - - return defaults[key] -end +--[[ + Name: (Revised) Ammo Maker + Author: Singustromo + Source: https://www.moddb.com/mods/stalker-anomaly/addons/revised-ammo-maker +--]] + +parent = ammo_maker +local acronym = "ram" + +-- If you don't use MCM, change your defaults from here. +defaults = { + ["salvage_preset"] = "dynamic", -- low|normal|high|dynamic (progression difficulty) + ["tool_degradation"] = 4.5, + ["powder_bonus"] = 1.05, -- 1 to deactivate + ["minimum_salvage"] = 0, + ["rate_bad"] = 0.35, -- salvage rate for old ammunition + ["show_news"] = true, -- pda notification + ["debug"] = false, -- debug messages +} + +local clr_title = {255, 255, 255, 0} +local clr_desc = {200, 200, 255, 200} +local clr_notice = {255, 232, 61, 102} + +function on_mcm_load() + -- We check this here, otherwise MCM will also indirectly load our main script + if not (parent and parent.VERSION and parent.VERSION >= 20240101) then return end + + op = { id=acronym, sh=true, gr={ + {id = "title", type="slide", link="ui_options_slider_gameplay_diff", text="ui_mcm_menu_"..acronym.."_title", size={512,50}, spacing=20}, + {id = "salvage_preset", type="list", val=0, def=defaults["salvage_preset"], + content={ + {"low", acronym.."_preset_low"}, + {"normal", acronym.."_preset_normal"}, + {"high", acronym.."_preset_high"}, + {"dynamic", acronym.."_preset_dynamic"}, + }, + }, + {id = "tool_degradation", type="track", val=2, min=0, max=10, step=0.5, def=defaults["tool_degradation"]}, + {id = "powder_bonus", type="track", val=2, min=0.5, max=1.2, step=0.02, def=defaults["powder_bonus"]}, + {id = "minimum_salvage", type="track", val=2, min=0, max=0.4, step=0.05, def=defaults["minimum_salvage"]}, + {id = "rate_bad", type="track", val=2, min=0, max=1, step=0.05, def=defaults["rate_bad"]}, + {id = "show_news", type="check", val=1, def=defaults["show_news"]}, + + {id = "divider", type = "line" }, + {id = "header", type = "desc", text = "ui_mcm_"..acronym.."_title_debug", clr=clr_title}, + {id = "debug", type="check", val=1, def=defaults["debug"]}, + }, + } + + return op +end + +function get_value(key) + if not (key and type(key) == 'string') then return end + + if ui_mcm and type(ui_mcm.get) == 'function' then + return ui_mcm.get(acronym .. "/" .. key) + end + + return defaults[key] +end diff --git a/src/001 - Main Files/gamedata/scripts/itms_manager_monkey_ammo_maker.script b/src/001 - Main Files/gamedata/scripts/itms_manager_monkey_ammo_maker.script index 0b8a456..c53f1ba 100644 --- a/src/001 - Main Files/gamedata/scripts/itms_manager_monkey_ammo_maker.script +++ b/src/001 - Main Files/gamedata/scripts/itms_manager_monkey_ammo_maker.script @@ -1,55 +1,61 @@ --- Enables highlighting of ammunition components - -parent = ammo_maker -if not (parent and parent.version and parent.version >= 20240110) then - printe("Unable to find parent script!") - callstack(true) - return -end - -get_ammo_recipe = parent and parent.get_ammo_recipe - -local focus_last_sec -local focus_tbl = {} -local focus_upgr = {} - -local FocusReceive = itms_manager.ActorMenu_on_item_focus_receive -function itms_manager.ActorMenu_on_item_focus_receive(obj) -- highlight compatible items - if not IsAmmo(obj) then - empty_table(focus_tbl) - FocusReceive(obj) - return - end - - local sec_focus = obj:section() - if (focus_last_sec ~= sec_focus) then - local id = obj:id() - focus_last_sec = sec_focus - empty_table(focus_tbl) - - local parent_sec = ini_sys:r_string_ex(sec_focus,"parent_section") or sec_focus - - if IsItem("ammo", parent_sec) then - parts = get_ammo_recipe(parent_sec) - end - - if parts then - for i=3,#parts do - focus_tbl[#focus_tbl + 1] = parts[i] - end - end - end - - local inventory = GetActorMenu() - if not ((#focus_tbl > 0) or (inventory and inventory:IsShown())) then - return - end - - for i=1,#focus_tbl do - inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iActorBag) - inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iPartnerTradeBag) - inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iDeadBodyBag) - inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iActorTrade) - inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iPartnerTrade) - end -end +--[[ + Name: (Revised) Ammo Maker + Author: Singustromo + Source: https://www.moddb.com/mods/stalker-anomaly/addons/revised-ammo-maker + + Enables inventory highlighting of ammunition components +--]] + +parent = ammo_maker +if not (parent and parent.VERSION and parent.VERSION >= 20240110) then + printe("Unable to find parent script!") + callstack(true) + return +end + +get_ammo_recipe = parent and parent.get_ammo_recipe + +local focus_last_sec +local focus_tbl = {} +local focus_upgr = {} + +local FocusReceive = itms_manager.ActorMenu_on_item_focus_receive +function itms_manager.ActorMenu_on_item_focus_receive(obj) -- highlight compatible items + if not IsAmmo(obj) then + empty_table(focus_tbl) + FocusReceive(obj) + return + end + + local sec_focus = obj:section() + if (focus_last_sec ~= sec_focus) then + local id = obj:id() + focus_last_sec = sec_focus + empty_table(focus_tbl) + + local parent_sec = ini_sys:r_string_ex(sec_focus,"parent_section") or sec_focus + + if IsItem("ammo", parent_sec) then + parts = get_ammo_recipe(parent_sec) + end + + if parts then + for i=3,#parts do + focus_tbl[#focus_tbl + 1] = parts[i] + end + end + end + + local inventory = GetActorMenu() + if not ((#focus_tbl > 0) or (inventory and inventory:IsShown())) then + return + end + + for i=1,#focus_tbl do + inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iActorBag) + inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iPartnerTradeBag) + inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iDeadBodyBag) + inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iActorTrade) + inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iPartnerTrade) + end +end diff --git a/src/001 - Main Files/gamedata/scripts/libmath_bezier.script b/src/001 - Main Files/gamedata/scripts/libmath_bezier.script index c01c702..111721e 100644 --- a/src/001 - Main Files/gamedata/scripts/libmath_bezier.script +++ b/src/001 - Main Files/gamedata/scripts/libmath_bezier.script @@ -1,55 +1,55 @@ --- A tiny library for picking random values between two numbers using bézier curves --- Written in Lua by Singustromo --- Based on and inspired by randomizing_functions from Demonized - --- Usage ---[[ - local randomizer = libmath_bezier - local a = randomizer.get_random_value(min, max, {p0, p1, p2, p3, p4}) ---]] - --- Pick a random float between min_cond and max_cond --- random value will be picked according to the function graph -function get_random_value(min_cond, max_cond, params) - local min_cond = min_cond or 0 - local max_cond = max_cond or 1 - - if not params or type(params) ~= 'table' or #params < 3 then - params = {0,0.5,1} -- linear interpolation - end - - local rand = 1 - if #params == 4 then - rand = cubic_bezier(math.random(), params) - elseif #params == 3 then - rand = quadratic_bezier(math.random(), params) - end - if not rand then return end - - if min_cond > max_cond then - max_cond, min_cond = min_cond, max_cond - end - - local a = max_cond - min_cond - local b = a * rand - local c = min_cond + b - return c -end - -function quadratic_bezier(x, p) - -- linear interpolation: saving compute time - if p[1] == 0 and p[2] == 0.5 and p[3] == 1 then return x end - - return p[1]*(1-x)^2 + 2*p[2]*x*(1-x) + p[3]*x^2 -end - -function cubic_bezier(x, p) - if #p < 4 then return end - - -- linear interpolation: saving compute time - if p[1] == 0 and p[2] == 1 and p[3] == 0 and p[4] == 1 then - return x - end - - return p[1]*(1-x)^3 + 3*p[2]*(1-x)^2*x + 3*p[3]*(1-x)*x^2 + p[4]*x^3 -end +-- A tiny library for picking random values between two numbers using bézier curves +-- Written in Lua by Singustromo +-- Based on and inspired by randomizing_functions from Demonized + +-- Usage +--[[ + local randomizer = libmath_bezier + local a = randomizer.get_random_value(min, max, {p0, p1, p2, p3, p4}) +--]] + +-- Pick a random float between min_cond and max_cond +-- random value will be picked according to the function graph +function get_random_value(min_cond, max_cond, params) + local min_cond = min_cond or 0 + local max_cond = max_cond or 1 + + if not params or type(params) ~= 'table' or #params < 3 then + params = {0,0.5,1} -- linear interpolation + end + + local rand = 1 + if #params == 4 then + rand = cubic_bezier(math.random(), params) + elseif #params == 3 then + rand = quadratic_bezier(math.random(), params) + end + if not rand then return end + + if min_cond > max_cond then + max_cond, min_cond = min_cond, max_cond + end + + local a = max_cond - min_cond + local b = a * rand + local c = min_cond + b + return c +end + +function quadratic_bezier(x, p) + -- linear interpolation: saving compute time + if p[1] == 0 and p[2] == 0.5 and p[3] == 1 then return x end + + return p[1]*(1-x)^2 + 2*p[2]*x*(1-x) + p[3]*x^2 +end + +function cubic_bezier(x, p) + if #p < 4 then return end + + -- linear interpolation: saving compute time + if p[1] == 0 and p[2] == 1 and p[3] == 0 and p[4] == 1 then + return x + end + + return p[1]*(1-x)^3 + 3*p[2]*(1-x)^2*x + 3*p[3]*(1-x)*x^2 + p[4]*x^3 +end diff --git a/src/001 - Main Files/gamedata/scripts/ui_inventory_monkey_ammo_maker.script b/src/001 - Main Files/gamedata/scripts/ui_inventory_monkey_ammo_maker.script new file mode 100644 index 0000000..2cfce75 --- /dev/null +++ b/src/001 - Main Files/gamedata/scripts/ui_inventory_monkey_ammo_maker.script @@ -0,0 +1,56 @@ +--[[ + Name: (Revised) Ammo Maker + Author: Singustromo + Source: https://www.moddb.com/mods/stalker-anomaly/addons/revised-ammo-maker + + These are the main monkey patches for interaction with the inventory system +--]] + +parent = ammo_maker +if not (parent and parent.VERSION and parent.VERSION >= 20240302) then return end + +-- Superseded functions +NameCustom = ui_inventory.UIInventory.Name_Custom +ActionCustom = ui_inventory.UIInventory.Action_Custom + +function check_batch(obj) + if not (obj and get_ammo_recipe(obj:section())) then return end + if get_ruck_itemcount(obj) < obj:ammo_box_size() +1 then return end + + return "st_batch_breakdown" -- string to display +end + +function get_ruck_itemcount(obj) + if not (obj) then return end + + local count = 0 + db.actor:iterate_ruck(function(tmp, item) + if (item:section() == obj:section()) then + count = count + item:ammo_get_count() + end + end) + + return count +end + +-------------------------- +-- Monkey Patches -- +-------------------------- + +function ui_inventory.UIInventory:Name_Custom(obj, bag, temp, i) + obj = self:CheckItem(obj,"Name_Custom " .. i) + if i == 4 and IsAmmo(obj) then + return check_batch(obj) + else + return NameCustom(self, obj, bag, temp, i) + end +end + +function ui_inventory.UIInventory:Action_Custom(obj, bag, temp, i) + obj = self:CheckItem(obj,"Action_Custom " .. i) + if i == 4 and IsAmmo(obj) then + parent.wrap_disassemble(obj, nil, true) + else + ActionCustom(self, obj, bag, temp, i) + end +end |