summary refs log tree commit diff
diff options
context:
space:
mode:
authorSingustromo <singustromo@disroot.org>2024-03-06 14:05:05 +0100
committerSingustromo <singustromo@disroot.org>2024-03-06 14:05:05 +0100
commita6bbda53ee681140363f36fbb81c74c0d0f0b080 (patch)
treeb95c2894246c9ebb2d70f57a89ec8a83137913d1
parent2da9c5c5ab1236701113025a5c6a59e62d2c810c (diff)
downloadrevised-ammo-maker-a6bbda53ee681140363f36fbb81c74c0d0f0b080.tar.gz
revised-ammo-maker-a6bbda53ee681140363f36fbb81c74c0d0f0b080.zip
Too many changes, can't remember
-rw-r--r--src/001 - Main Files/gamedata/configs/plugins/ammo_maker/importer.ltx2
-rw-r--r--src/001 - Main Files/gamedata/scripts/ammo_maker.script1109
-rw-r--r--src/001 - Main Files/gamedata/scripts/ammo_maker_mcm.script123
-rw-r--r--src/001 - Main Files/gamedata/scripts/itms_manager_monkey_ammo_maker.script116
-rw-r--r--src/001 - Main Files/gamedata/scripts/libmath_bezier.script110
-rw-r--r--src/001 - Main Files/gamedata/scripts/ui_inventory_monkey_ammo_maker.script56
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