Divergent/mods/More Melee Features/gamedata/scripts/demonized_crafter.script

619 lines
19 KiB
Plaintext
Raw Normal View History

2024-03-17 20:18:03 -04:00
-- Crafting items by right clicking
-- Supports multiple entries and any item combinations
-- Supports multicrafting all items at once until enough material in the inventory with holding Left Shift
-- Written by demonized
--[[
Crafting recipes
The structure of crafting recipes is
tool
item1:amount1, (item2:amount2_min-amount2_max), ...
recipe1
recipe2
...
Tool can craft multiple items at once per entry (item1, item2), different amount of items (item1:amount1) or randomized amount of items between min and max (item2:amount2_min-amount2_max)
Recipes has form of "item1:amount1,item2:amount2,..."
Order of recipe matters, first one possible to use will be used
You can omit amount if you except 1 item to be consumed, ie.
vodka,box_matches,prt_i_paper is equivalent to vodka:1,box_matches:1,prt_i_paper:1
Tools that use condition can be written as "item1:0.2" to degrade 20% of item
It is possible to consume the whole tool if it has at least a condition specified in a recipe. To enable that, tool can be written as "item1>0.2". The ">", ">=", "<", "<=" comparisons are supported.
Example of crafting recipes table
craft_recipes = {
vodka = {
["molotov_cocktail"] = {
"vodka,box_matches,prt_i_paper",
"vodka:1,box_matches:1,prt_o_fabrics_1:1",
},
},
vodka2 = {
["molotov_cocktail:2, prt_i_paper:1-2"] = {
"vodka2:1,box_matches:1,prt_i_paper:1",
"vodka2:1,box_matches:1,prt_o_fabrics_1:1",
},
}
}
To add a crafting recipe, use function demonized_crafter.add_craft_recipe(craft_recipe_table, args)
craft_recipe_table is the table with the structure as above
args is optional table and specifies additional arguments to craft menu. Possible table fields in args table are:
before - boolean, tells to add recipe before existing ones
craft_strings - table, contains string ids that will act as replacements for default "Craft: <item>" string in menu, have same structure as craft_recipe_table, for example:
{
vodka = {
molotov_cocktail = "st_make_molotov"
},
vodka2 = {
molotov_cocktail = "st_make_molotov"
},
}
To remove recipes use function demonized_crafter.remove_craft_recipe(craft_recipe_table)
craft_recipe_table is the table with the structure as above
You can remove all recipes for a given tool with function demonized_crafter.remove_all_recipes_for_tool(tool)
When crafting is successful, the tip will be printed telling what items were produced and consumes
When not, the tip will print what recipes is enabled for an item and tool and what items are and not in players inventory
--]]
local text_color = utils_xml.get_color("pda_white")
local arg_color = utils_xml.get_color("d_green")
local bad_color = utils_xml.get_color("d_red")
local function colorize(s)
return arg_color .. s .. text_color
end
local function colorize_bad(s)
return bad_color .. s .. text_color
end
local print_tip = print_tip or function(text, ...)
local text = tostring(text)
-- printf(text, ...)
if not db.actor then
return
end
local ico = "ui_inGame2_Dengi_otdani"
local text_color = utils_xml.get_color("pda_white")
local arg_color = utils_xml.get_color("d_green")
local function colorize(s)
return arg_color .. s .. text_color
end
local i = 0
local t = {...}
if #t > 0 then
local function sr(a)
i = i + 1
if (type(t[i]) == 'userdata') then
if (t[i].x and t[i].y) then
return colorize(vec_to_str(t[i]))
end
return colorize('userdata')
end
return colorize(tostring(t[i]))
end
text = string.gsub(game.translate_string(text), "%%s", sr)
else
text = game.translate_string(text)
end
text = text_color .. text
news_manager.send_tip(db.actor, text, nil, ico, 6000)
end
local gc = game.translate_string
local function table_find(t, el)
for i = 1, #t do
if t[i] == el then
return i
end
end
end
local degradable_cache = {}
function is_degradable(sec, obj)
if degradable_cache[sec] ~= nil then
return degradable_cache[sec]
end
local res
local function degradable()
return utils_item.is_degradable(nil, sec) and not IsItem("part", sec)
end
if obj then
sec = obj:section()
res = degradable() or IsWeapon(obj) or IsOutfit(obj) or IsHeadgear(obj) or IsArtefact(obj)
else
res = degradable()
end
-- Cast to boolean
degradable_cache[sec] = not not res
return degradable_cache[sec]
end
local holding_shift = false
local multi_craft = false
local craft_func_key = "st_demonized_crafter" -- DONT CHANGE THIS!!
craft_recipes = {}
craft_item_strings = {}
-- Helper functions to add and remove recipes
-- They accept same table structure as described above
function add_craft_recipe(craft_recipe_table, args)
if not args then args = {} end
for tool, items in pairs(craft_recipe_table) do
if not craft_recipes[tool] then
craft_recipes[tool] = items
else
for item, recipes in pairs(items) do
if not craft_recipes[tool][item] then
craft_recipes[tool][item] = recipes
else
for _, recipe in ipairs(recipes) do
local recipe = string.gsub(recipe, ":1", "")
if not table_find(craft_recipes[tool][item], recipe) then
if args.before then
table.insert(craft_recipes[tool][item], 1, recipe)
else
table.insert(craft_recipes[tool][item], recipe)
end
end
end
end
end
end
end
if args.craft_strings then
for tool, items in pairs(args.craft_strings) do
if not craft_item_strings[tool] then
craft_item_strings[tool] = {}
end
for item, str in pairs(items) do
craft_item_strings[tool][item] = str
end
end
end
refresh_craft_menu()
return craft_recipes
end
function remove_craft_recipe(craft_recipe_table)
local remove_functor = custom_functor_autoinject.remove_functor
for tool, items in pairs(craft_recipe_table) do
if craft_recipes[tool] then
for item, recipes in pairs(items) do
if craft_recipes[tool][item] then
if craft_item_strings[tool] then
craft_item_strings[tool][item] = nil
end
for _, recipe in ipairs(recipes) do
local pos = table_find(craft_recipes[tool][item], recipe)
if pos then
table.remove(craft_recipes[tool][item], pos)
end
local recipe = string.gsub(recipe, ":1", "")
local pos = table_find(craft_recipes[tool][item], recipe)
if pos then
table.remove(craft_recipes[tool][item], pos)
end
end
if is_empty(craft_recipes[tool][item]) then
remove_functor(craft_func_key .. "_" .. tool .. "_" .. item)
craft_recipes[tool][item] = nil
end
end
end
if is_empty(craft_recipes[tool]) then
craft_recipes[tool] = nil
end
end
end
refresh_craft_menu()
return craft_recipes
end
function remove_all_recipes_for_tool(tool)
if not craft_recipes[tool] then return end
for item, recipes in pairs(craft_recipes[tool]) do
remove_functor(craft_func_key .. "_" .. tool .. "_" .. item)
end
craft_recipes[tool] = nil
refresh_craft_menu()
return craft_recipes
end
-- This function expands craft recipes in a way that any item in recipe is considered a tool and can invoke right click menu
-- Unused for now
function expand_craft()
end
function add_craft_menu(recipe_table)
local add_functor = custom_functor_autoinject.add_functor
local remove_functor = custom_functor_autoinject.remove_functor
for tool, items in pairs(recipe_table) do
for item, recipes in pairs(items) do
local result_item = item
local result_item_table = {}
for i, v in ipairs(str_explode(result_item, ",")) do
local s = str_explode(v, ":")
local res = { section = s[1] }
if s[2] then
if s[2]:find("-") then
local r = str_explode(s[2], "-")
res.amount = function()
return math.random(r[1], r[2])
end
res.amount_str = s[2]
else
res.amount = function() return tonumber(s[2]) end
res.amount_str = s[2]
end
else
res.amount = function() return 1 end
res.amount_str = 1
end
table.insert(result_item_table, res)
end
local function print_result_items()
local s = ""
for i, v in ipairs(result_item_table) do
-- Return craft string + item name to craft
local item_name = ui_item.get_sec_name(v.section)
local amount = v.amount_str
s = s .. item_name
if i ~= #result_item_table then
s = s .. ", "
end
end
return s
end
local craft_func = {
check = function(obj, bag, mode)
-- Check tool
local modes = {
["inventory"] = true,
}
local bags = {
["actor_bag"] = true,
["actor_equ"] = true,
["actor_belt"] = true,
}
return obj:section() == tool and modes[mode] and bags[bag]
end,
str = function(obj, bag, mode)
-- Enable multicraft is shift was held while right clicking
multi_craft = holding_shift
if craft_item_strings[tool] and craft_item_strings[tool][item] then
return gc(craft_item_strings[tool][item])
else
return gc(craft_func_key) .. (multi_craft and (" " .. gc(craft_func_key .. "_craft_all")) or "") .. ": " .. print_result_items()
end
end,
func = function(obj, bag, mode)
-- Parse recipe table
local res = {}
local craft_items = {}
local craft_item_modes = {}
for _, recipe in ipairs(recipes) do
local t = {}
for _, m in pairs(str_explode(trim(recipe), ",")) do
if m:find("[<>]") then
local material_data = {m:match("(.*)([<>][=]*)(.*)")}
local material = material_data[1]
local func = material_data[2]
local amount = material_data[3] and tonumber(material_data[3]) or 1
if not t[material] then
t[material] = amount
end
if not craft_items[material] then
craft_items[material] = amount
end
craft_item_modes[material] = {func, amount}
else
local material = str_explode(m, ":")
if not t[material[1]] then
t[material[1]] = 0
end
if not craft_items[material[1]] then
craft_items[material[1]] = 0
end
if not material[2] then
material[2] = 1
end
material[2] = tonumber(material[2])
t[material[1]] = t[material[1]] + material[2]
craft_items[material[1]] = craft_items[material[1]] + material[2]
end
end
res[#res + 1] = t
end
-- Get items for recipes
local actor = obj:parent()
local inventory_items = {}
local function iterate(npc, obj)
local sec = obj:section()
if not craft_items[sec] then return end
if not inventory_items[sec] then
inventory_items[sec] = {}
end
if IsItem("multiuse", sec) then
inventory_items[sec][obj:id()] = obj:get_remaining_uses()
elseif is_degradable(sec, obj) then
inventory_items[sec][obj:id()] = round_idp(obj:condition(), 2)
else
inventory_items[sec][obj:id()] = 1
end
end
if IsStalker(actor) then
actor:iterate_inventory(iterate, actor)
elseif IsInvbox(actor) then
actor:iterate_inventory_box(iterate, actor)
else
print_tip("item parent is undefined %s, %s", actor:name(), actor:section())
return
end
local function count_items(sec)
if not inventory_items[sec] then return 0 end
local s = 0
for k, v in pairs(inventory_items[sec]) do
s = s + v
end
return s
end
-- Check recipes for possibility to craft
local can_craft_at_all = true
local crafted_amount = 0
local crafted_items = {}
local spent_items = {}
local wasted_items = {}
local function print_crafted_items()
local s = ""
local sep = ", "
for k, v in pairs(crafted_items) do
local item_name = ui_item.get_sec_name(k)
local amount = v
s = s .. item_name .. ":" .. amount .. sep
end
s = s:sub(1, -(sep:len() + 1))
return s
end
local function craft_item_mode_check(craft_item_mode, inv_amount)
local t = {
['>'] = function() return inv_amount > craft_item_mode[2] end,
['>='] = function() return inv_amount >= craft_item_mode[2] end,
['<'] = function() return inv_amount < craft_item_mode[2] end,
['<='] = function() return inv_amount <= craft_item_mode[2] end,
}
return t[craft_item_mode[1]] and t[craft_item_mode[1]]()
end
repeat
can_craft_at_all = true
empty_table(spent_items)
local crafted = false
for i, r in ipairs(res) do
local can_craft = true
spent_items[i] = {}
for recipe_item, recipe_amount in pairs(r) do
if craft_item_modes[recipe_item] then
spent_items[i][recipe_item] = {count_items(recipe_item), 1}
if not craft_item_mode_check(craft_item_modes[recipe_item], count_items(recipe_item)) then
can_craft = false
end
else
spent_items[i][recipe_item] = {count_items(recipe_item), recipe_amount}
if spent_items[i][recipe_item][1] < recipe_amount then
can_craft = false
end
end
end
-- If can craft - craft the item
if can_craft then
local function sort_asc(t, a, b)
return t[a] < t[b]
end
for recipe_item, v in pairs(spent_items[i]) do
local recipe_amount = v[2]
-- Consume items with least uses first
for id, amount in spairs(inventory_items[recipe_item], sort_asc) do
if recipe_amount <= 0 then
break
end
if is_degradable(recipe_item, level.object_by_id(id)) then
if craft_item_modes[recipe_item] then
alife_release_id(id)
else
utils_item.degrade(level.object_by_id(id), recipe_amount)
end
elseif amount < recipe_amount then
alife_release_id(id)
else
utils_item.discharge(level.object_by_id(id), recipe_amount)
end
if not wasted_items[recipe_item] then
wasted_items[recipe_item] = 0
end
wasted_items[recipe_item] = wasted_items[recipe_item] + math.min(recipe_amount, amount)
inventory_items[recipe_item][id] = inventory_items[recipe_item][id] - recipe_amount
recipe_amount = recipe_amount - amount
if inventory_items[recipe_item][id] <= 0 then
inventory_items[recipe_item][id] = nil
end
end
end
for i, v in ipairs(result_item_table) do
local result_item = v.section
local amount = v.amount()
if not crafted_items[result_item] then
crafted_items[result_item] = 0
end
crafted_items[result_item] = crafted_items[result_item] + amount
for j = 1, amount do
alife_create_item(result_item, db.actor)
crafted_amount = crafted_amount + 1
crafted = true
end
end
-- Play camera animation
if not multi_craft then
-- Print what is crafted and what items were consumed
local tip_string = text_color .. gc(craft_func_key .. "_craft_success") .. " " .. colorize(print_crafted_items()) .. " \\n" .. gc(craft_func_key .. "_craft_spent_items")
for recipe_item, recipe_amount in pairs(r) do
tip_string = tip_string .. " \\n" .. ui_item.get_sec_name(recipe_item) .. ": " .. (craft_item_modes[recipe_item] and colorize("1") or (is_degradable(recipe_item) and colorize(round(recipe_amount * 100) .. "%") or colorize(recipe_amount)))
end
print_tip(tip_string)
actor_effects.play_item_fx("item_combination")
return
end
end
end
can_craft_at_all = crafted
until not can_craft_at_all
-- Disable multi craft if it was enabled
multi_craft = false
-- If crafted something and was in multi craft
if crafted_amount > 0 then
-- Print what is crafted and what items were consumed
local tip_string = text_color .. gc(craft_func_key .. "_craft_success") .. " " .. colorize(print_crafted_items()) .. " \\n" .. gc(craft_func_key .. "_craft_spent_items")
for recipe_item, recipe_amount in pairs(wasted_items) do
tip_string = tip_string .. " \\n" .. ui_item.get_sec_name(recipe_item) .. ": " .. (craft_item_modes[recipe_item] and colorize("1") or (is_degradable(recipe_item) and colorize(round(recipe_amount * 100) .. "%") or colorize(recipe_amount)))
end
print_tip(tip_string)
-- Play camera animation
actor_effects.play_item_fx("item_combination")
return
end
-- If cant craft - print tip of possible recipes and required amount
local failed_tip_string = bad_color .. gc(craft_func_key .. "_craft_fail") .. text_color .. " " .. colorize(print_result_items()) .. " \\n" .. gc(craft_func_key .. "_craft_possible_combinations") .. ": "
for i, v in ipairs(spent_items) do
for recipe_item, recipe_amount in pairs(v) do
failed_tip_string = failed_tip_string .. " \\n" .. ui_item.get_sec_name(recipe_item) .. ": " ..
(
craft_item_modes[recipe_item] and
(
(
craft_item_mode_check(craft_item_modes[recipe_item], recipe_amount[1])
and
colorize(round(recipe_amount[1] * 100) .. "%")
or
colorize_bad(round(recipe_amount[1] * 100) .. "%")
) .. " " .. craft_item_modes[recipe_item][1] .. " " .. colorize(round(craft_item_modes[recipe_item][2] * 100) .. "%")
)
or
(
recipe_amount[1] < recipe_amount[2] and
(
is_degradable(recipe_item)
and
colorize_bad(round(recipe_amount[1] * 100))
or
colorize_bad(recipe_amount[1])
)
or
(
is_degradable(recipe_item)
and
colorize(round(recipe_amount[1] * 100))
or
colorize(recipe_amount[1])
)
)
.. "/" ..
(
is_degradable(recipe_item)
and
colorize(round(recipe_amount[2] * 100) .. "%")
or
colorize(recipe_amount[2])
)
)
end
if i ~= #spent_items then
failed_tip_string = failed_tip_string .. " \\n \\n" .. gc(craft_func_key .. "_craft_or") .. " \\n"
end
end
print_tip(failed_tip_string)
-- Print generic message if there are too much recipes
if #recipes > 2 then
local failed_tip_small_string = bad_color .. gc(craft_func_key .. "_craft_fail") .. text_color .. " " .. colorize(print_result_items()) .. " \\n" .. gc(craft_func_key .. "_craft_check_pda_news")
print_tip(failed_tip_small_string)
end
end
}
add_functor(craft_func_key .. "_" .. tool .. "_" .. result_item, craft_func.check, craft_func.str, nil, craft_func.func, true)
end
end
end
function remove_craft_menu()
local add_functor = custom_functor_autoinject.add_functor
local remove_functor = custom_functor_autoinject.remove_functor
for tool, items in pairs(craft_recipes) do
for item, recipes in pairs(items) do
remove_functor(craft_func_key .. "_" .. tool .. "_" .. item)
end
end
end
function refresh_craft_menu()
remove_craft_menu()
add_craft_menu(craft_recipes)
end
function on_key_hold(key)
if key == DIK_keys["DIK_LSHIFT"] then
holding_shift = true
end
end
function on_key_release(key)
if key == DIK_keys["DIK_LSHIFT"] then
holding_shift = false
end
end
function on_game_start()
RegisterScriptCallback("on_key_hold", on_key_hold)
RegisterScriptCallback("on_key_release", on_key_release)
refresh_craft_menu()
end