--[[ ----------------------------------- Copyright (C) 2012 Alundaio This program is free software; you can redistribute and/or modify it under the terms of the Open S.T.A.L.K.E.R. Mod License version 1.0. ----------------------------------- ponney68 ----------------------------------- Tronex Last edit: 2019/12/17 Added Loot all button, organized script, inventory cell system, alife spawn handlers ----------------------------------- trashBattery (tB) Last edit: 2024/10/23 Rewrote the logic for mutant loot handling to support new "percent of max" method and removed the vanilla "magical duplication" methods. See readme for more info. -]] --Load Reserouces local ini_mutant = ini_file_ex("items\\settings\\mutant_loot.ltx") --Using ini_file_ex now instead of utils_data:ini_file -tB local ini_tbHuntKit_settings = ini_file_ex("tbHuntKit_settings.ltx") --Configs for the features added by tbHuntKit (this mod) -tB local ini_actorFX = ini_file_ex("plugins\\actor_effects.ltx") --Need this for reading the anim data later -tB --Load configs. Descriptions are in tbHuntKit_settings.ltx -tB local envHardcap,envHarvestDiff,envScavengeDiff,envMinItems,disableExtraSounds,muteHarvestSounds,useHarvestSoundFallback function loadConfigs() --Tied to "on_option_change" callback. -tB if ui_mcm then envHardcap = ui_mcm.get("tb_HuntKit/tb_HuntKit_settings/harvestHardcap")/100 or 0.92 envHarvestDiff = ui_mcm.get("tb_HuntKit/tb_HuntKit_settings/harvestDifficulty") or 1.00 envScavengeDiff = ui_mcm.get("tb_HuntKit/tb_HuntKit_settings/scavengeDifficulty") or 1.00 envMinItems = ui_mcm.get("tb_HuntKit/tb_HuntKit_settings/minItems") or 1.00 disableExtraSounds = ui_mcm.get("tb_HuntKit/tb_HuntKit_settings/disableExtraSounds") or false muteHarvestSounds = ui_mcm.get("tb_HuntKit/tb_HuntKit_settings/muteHarvestSounds") or false useHarvestSoundFallback = ui_mcm.get("tb_HuntKit/tb_HuntKit_settings/useHarvestSoundFallback") or false printf("-ui_mutant_loot loaded configs from MCM") else envHardcap = ini_tbHuntKit_settings:r_float_ex("environment","harvestHardcap")/100 or 0.92 envHarvestDiff = ini_tbHuntKit_settings:r_float_ex("environment","harvestDifficulty") or 1.00 envScavengeDiff = ini_tbHuntKit_settings:r_float_ex("environment","scavengeDifficulty") or 1.00 envMinItems = ini_tbHuntKit_settings:r_float_ex("environment","minItems") or 1.00 disableExtraSounds = ini_tbHuntKit_settings:r_bool_ex("compatiblity","disableExtraSounds") or false muteHarvestSounds = ini_tbHuntKit_settings:r_bool_ex("compatiblity","muteHarvestSounds") or false useHarvestSoundFallback = ini_tbHuntKit_settings:r_bool_ex("compatiblity","useHarvestSoundFallback") or false printf("-ui_mutant_loot loaded configs from tbHuntKit_settings.ltx") end end loadConfigs() --The game came with 9 sounds for harvesting and only ever uses 1?! Let's use them all (or not if you disable them) :D -tB local harvestSFX = { "inv_mutant_loot_animal", "inv_mutant_loot_animal2", "inv_mutant_loot_crow", "inv_mutant_loot_grease", "inv_mutant_loot_grease2", "inv_mutant_loot_human", "inv_mutant_loot_human2", "inv_mutant_loot_rotten", "inv_mutant_loot_smaller" } --Extract creature whitelist. -tB local validCreatures = {} local forIncr = 0 for k in pairs(ini_mutant:collect_section("whitelist")) do forIncr=forIncr+1 validCreatures[forIncr]=k end local sec_kit_hunt = "kit_hunt" local item_prop_table = { cond_r = {30,70} , cond_ct = "part" , cond_cr = {0.5,0.75,1} } -- Loot Mutant local MutantLootDecayTime = ini_mutant:r_float_ex("mutant_loot_mod","decay_time") or 7200 local kind_to_section = { ["SM_KARLIK"] = "karlik", ["SM_PSYSUCKER"] = "psysucker", ["SM_LURKER"] = "lurker" } local clsid_to_section = { [clsid.bloodsucker_s] = "bloodsucker", [clsid.boar_s] = "boar", [clsid.burer_s] = "burer", [clsid.chimera_s] = "chimera", [clsid.controller_s] = "controller", [clsid.dog_s] = "dog", [clsid.flesh_s] = "flesh", [clsid.gigant_s] = "gigant", [clsid.poltergeist_s] = "poltergeist", [clsid.psy_dog_s] = "psy_dog", [clsid.psy_dog_phantom_s] = "psy_dog", [clsid.pseudodog_s] = "pseudodog", [clsid.snork_s] = "snork", [clsid.tushkano_s] = "tushkano", [clsid.cat_s] = "cat", [clsid.fracture_s] = "fracture", [clsid.zombie_s] = "zombie", [clsid.crow] = "crow", [clsid.rat_s] = "rat" } local clsdbg_to_section = { ["SM_KARLIK"] = "karlik", ["SM_PSYSUCKER"] = "psysucker", ["SM_LURKER"] = "lurker" } local killed_mutant_tbl = { -- ponney68: This table based on "species" of mutants -- TRX: A-Life Revamp psysucker = {file="ui\\ui_actor_monsters_pda_3",x="393",y="0",type="small"}, lurker = {file="ui\\ui_actor_monsters_pda_3",x="0",y="0",type="small"}, karlik = {file="ui\\ui_actor_monsters_pda_3",x="0",y="200",type="small"}, snork = {file="ui\\ui_actor_monsters_pda",x="393",y="0",type="small"}, dog = {file="ui\\ui_actor_monsters_pda",x="0",y="800",type="small"}, pseudodog = {file="ui\\ui_actor_monsters_pda",x="393",y="200",type="small"}, psy_dog = {file="ui\\ui_actor_monsters_pda",x="393",y="200",type="small"}, poltergeist = {file="ui\\ui_actor_monsters_pda",x="0",y="400",type="small"}, bloodsucker = {file="ui\\ui_actor_monsters_pda",x="393",y="400",type="human"}, controller = {file="ui\\ui_actor_monsters_pda",x="393",y="800",type="human"}, chimera = {file="ui\\ui_actor_monsters_pda",x="0",y="600",type="large"}, tushkano = {file="ui\\ui_actor_monsters_pda",x="0",y="0",type="small"}, rat = {file="ui\\ui_actor_monsters_pda",x="0",y="0",type="small"}, flesh = {file="ui\\ui_actor_monsters_pda",x="393",y="600",type="large"}, tark = {file="ui\\ui_actor_monsters_pda_2",x="0",y="0",type="human"}, rotan = {file="ui\\ui_actor_monsters_pda",x="0",y="0",type="human"}, burer = {file="ui\\ui_actor_monsters_pda_1",x="0",y="0",type="large"}, boar = {file="ui\\ui_actor_monsters_pda_1",x="393",y="0",type="large"}, giant = {file="ui\\ui_actor_monsters_pda_1",x="0",y="200",type="large"}, cat = {file="ui\\ui_actor_monsters_pda_2",x="0",y="0",type="small"}, fracture = {file="ui\\ui_actor_monsters_pda_2",x="393",y="200",type="human"}, bird = {file="ui\\ui_actor_monsters_pda_2",x="393",y="0",type="small"}, zombie = {file="ui\\ui_actor_monsters_pda_2",x="0",y="200",type="human"}, bloodsucker_arena = {file="ui\\ui_actor_monsters_pda",x="393",y="400",type="human"}, burer_arena = {file="ui\\ui_actor_monsters_pda_1",x="0",y="0",type="large"}, pseudodog_arena = {file="ui\\ui_actor_monsters_pda",x="393",y="200",type="small"}, snork_arena = {file="ui\\ui_actor_monsters_pda",x="393",y="0",type="human"}, } function check_hunterKit(useNeedsEquipped) --Replaces "is_huntkit" variable with a more versitile function. Ignores the game option by default. The option is redundant due to the new design. -tB local actor = db.actor local backpack = actor:item_in_slot(13) local needs_equipped_hk = false if useNeedsEquipped then needs_equipped_hk = ui_options.get("gameplay/general/need_equipped_hkit") end if ((needs_equipped_hk and backpack == sec_kit_hunt) or (not needs_equipped_hk and actor:object(sec_kit_hunt))) then return true end return false end function actor_effects_getAnimLen(name) --actor_effects.ltx uses a non-standard method of animation formatting so I have to do this -tB local kMax = 0 for k in pairs(ini_actorFX:collect_section(name)) do k = tonumber(k) kMax = k>kMax and k or kMax end return kMax end function play_harvestAnim(override) --Play the correct harvesting animation based on the state of the player. Return animation time in seconds. -tB local boost,anim if (actor_effects) then if override then boost = override else boost = (game_achievements.has_achievement("well_dressed") and 1 or 0) + (check_hunterKit() and 0 or 1) end if (boost >= 2) then anim = "mutant_looting_boost_2" elseif (boost == 1) then anim = "mutant_looting_boost_1" else anim = "mutant_looting" end end actor_effects.play_item_fx(anim) return actor_effects_getAnimLen(anim)/1000 end function loot_mutant(section, clsid, loot_table, npc, dont_create, victim) -- Prepare mutant loot npc = npc or db.actor local clsid = clsid or obj and obj:clsid() local kind = section and ini_sys:r_string_ex(section,"kind") or "unknown" if not (clsid) then return end local loot, sec, count local str_explode = str_explode local mutant = clsdbg_to_section[kind] or clsid_to_section[clsid] --Creating possible_items{} using ini_file_ex from _g instead of the old utils_data method. -tB local possible_items = {} forIncr=0 for k in pairs(ini_mutant:collect_section(mutant)) do forIncr=forIncr+1 possible_items[forIncr]=k end local sim = alife() local npc_id = npc and npc:id() local npc_pos = npc and npc:position() local npc_lvl_id = npc and npc:level_vertex_id() local npc_game_id = npc and npc:game_vertex_id() --Check if the looted creature is part of the whitelist. -tB local isCreature = false for i=1,#validCreatures do if mutant == validCreatures[i] then isCreature = true break end end -- Spawn items on NPC if he looted the mutant for i=1,#possible_items do local chance local chanceFloor = envMinItems-1 --Min # of items to drop before scaling takes effect. Using #-1 felt better for configs.-tB local chanceExp = 1 local flatChance = false loot = str_explode(possible_items[i],",") sec = tostring(loot[1]) count = tonumber(loot[2]) if not count then printf("ui_mutant_loot.loot_mutant() >> Unacceptable quantity of loot item ["..sec.."]") count = 0 end --Check if an override chance was set. If so, use vanilla linear probability. Backpack doubles chances. -tB if loot[3] then flatChance = true chance = tonumber(loot[3]) or 1 if (isCreature and (string.match(sec, '^mutant_part_') or (string.match(sec, '^hide_')))) then if(check_hunterKit()) then chance = chance*2*envHarvestDiff else chance = chance*envScavengeDiff end end else --Otherwise, configure probability curve. -tB if (isCreature and (string.match(sec, '^mutant_part_') or (string.match(sec, '^hide_')))) then if(check_hunterKit()) then chanceExp = 0.5*(1/(envHarvestDiff+0.001)) else chanceExp = 2.2*(1/(envScavengeDiff+0.001)) end end chanceFloor = chanceFloor + math.floor(count*0.1) end for i=1,count do if not flatChance then if i < chanceFloor then chance = 1 else chance = (1/(i-chanceFloor))^chanceExp end end if chance > envHardcap then chance = envHardcap end if (math.random() <= chance) then -- In case we don't want to bother with loot table local se_obj if (not dont_create) then se_obj = alife_create_item(sec, npc, item_prop_table) end -- Fill loot table if needed if (loot_table) then local sec_d, uses = utils_item.get_defined_uses(sec) if (not loot_table[sec_d]) then loot_table[sec_d] = {} end local c = loot_table[sec_d].count c = c and (c + 1) or 1 loot_table[sec_d].count = c if se_obj then loot_table[sec_d][c] = se_obj.id end --printf("loot_mutant") --[[ if npc and npc:id() ~= AC_ID then se_obj = alife_create_item(sec, npc, item_prop_table) end --]] end end end end -- Unlock relevant mutant article in guide. if mutant and npc and (npc:id() == AC_ID) then SendScriptCallback("actor_on_interaction", "mutants", nil, mutant) end SendScriptCallback("monster_on_loot_init",victim,loot_table) end ---------------------------------------------------------------------- GUI = nil -- instance, don't touch function start(obj, for_bug1, for_bug2) if (not obj) then printf("!ERROR ui_mutant_loot | no game object passed!") return end if (not GUI) then GUI = UIMutantLoot() end if (GUI) and (not GUI:IsShown()) then local can_show = GUI:Reset(obj, for_bug1, for_bug2) if can_show then GUI:ShowDialog(true) Register_UI("UIMutantLoot","ui_mutant_loot") end end end function start_CreateTimeEvent(obj, for_bug1, for_bug2) level.enable_input() start(obj, for_bug1, for_bug2) return true end ---------------------------------------------------------------------- -- CALLBACKS ---------------------------------------------------------------------- function monster_on_actor_use_callback(obj,who) -- Open mutant loot UI -- Return if mutant is already looted local looted = se_load_var(obj:id(),obj:name(),"looted") local firstTime = load_var(obj,"loot") if (looted) then return end se_save_var(obj:id(),obj:name(),"looted",true) --bind_crow.script updatates no longer require the old logic to check if it's a crow. -tB -- This is important so NPCs don't try to loot the corpse the player is looting xr_corpse_detection.set_valuable_loot(obj:id(),false) -- if mutant corpse is lefted for long time, body is decayed local st = db.storage[obj:id()] if (st and st.death_time and game.get_game_time():diffSec(st.death_time) > MutantLootDecayTime) then actor_menu.set_msg(1, game.translate_string("st_body_decayed"),4) -- Start the Mutant Loot UI else --Play the harvest animation and sounds only the frist time the mutant is cut open. -tB if not firstTime then item_knife.degradate() --Damage the knife for the first cut into the creatutre. binder will damage further for each item removed. -tB local aTime = play_harvestAnim() if not muteHarvestSounds then if disableExtraSounds then if useHarvestSoundFallback then xr_sound.set_sound_play(AC_ID,"inv_mutant_loot_animal2") else xr_sound.set_sound_play(AC_ID,"inv_mutant_loot_animal") end else local harvestSFX_RND = harvestSFX[math.random(1,#harvestSFX)] if harvestSFX_RND == "inv_mutant_loot_animal" and useHarvestSoundFallback then harvestSFX_RND = "inv_mutant_loot_animal2" end xr_sound.set_sound_play(AC_ID,harvestSFX_RND) end else end level.disable_input() CreateTimeEvent("UIML_HarvestAnim_e","UIML_HarvestAnim_a",aTime,start_CreateTimeEvent,obj, obj:id(), obj:section(), obj:clsid()) else start(obj, obj:id(), obj:section(), obj:clsid()) end end end function monster_on_loot_init(obj,t) -- t['conserva'] = { -- count = 3 -- } -- utils_data.print_table(t,obj and obj:name() or "no_obj") end function on_game_start() RegisterScriptCallback("monster_on_actor_use_callback",monster_on_actor_use_callback) RegisterScriptCallback("monster_on_loot_init",monster_on_loot_init) RegisterScriptCallback("on_option_change",loadConfigs) end ---------------------------------------------------------------------- -- UI ---------------------------------------------------------------------- class "UIMutantLoot" (CUIScriptWnd) function UIMutantLoot:__init() super() self:InitControls() self:InitCallBacks() end function UIMutantLoot:__finalize() end function UIMutantLoot:InitControls() self:SetWndRect (Frect():set(0,0,1024,768)) self:SetAutoDelete(true) self.xml = CScriptXmlInit() self.xml:ParseFile ("ui_mutant_loot.xml") local xml = self.xml self.dialog = xml:InitStatic("mutant_loot:background",self) -- Mutant image self.image = self.xml:InitStatic("mutant_loot:image",self.dialog) -- Loot self.frame = xml:InitStatic("mutant_loot:frame",self.dialog) self.CC = utils_ui.UICellContainer("loot", self, nil, "mutant_loot:cont_loot", self.dialog) self.CC.showcase = true -- self.CC.can_select = true self.CC.disable_drag = true self.CC.disable_stack = true self.CC:SetGridSpecs(35, 2) self.item_info = utils_ui.UIInfoItem(self, 1000) -- Button Loot one self.btn_loot_one = xml:Init3tButton("mutant_loot:btn_loot",self.dialog) self:Register(self.btn_loot_one, "button_loot") -- Button Loot all self.btn_loot_all = xml:Init3tButton("mutant_loot:btn_loot_all",self.dialog) self:Register(self.btn_loot_all, "button_loot_all") -- Button Cancel self.btn_cancel = xml:Init3tButton("mutant_loot:btn_cancel",self.dialog) self:Register(self.btn_cancel, "button_cancel") end function UIMutantLoot:InitCallBacks() self:AddCallback("button_loot",ui_events.BUTTON_CLICKED,self.OnButton_LootSelected,self) self:AddCallback("button_loot_all",ui_events.BUTTON_CLICKED,self.OnButton_LootAll,self) self:AddCallback("button_cancel",ui_events.BUTTON_CLICKED,self.Close,self) end function UIMutantLoot:Reset(obj, for_bug1, for_bug2) local function is_number(var) local function lets_try(var) var = tonumber(var) return (var > 0) or (var < 0) or var or -var end if pcall(function() lets_try(var) end) then return true else return false end end if not (is_number(obj)) then self.section = obj:section() self.clsid = obj:clsid() self.id = obj:id() self.obj = obj else self.id = obj self.section=for_bug1 self.clsid = for_bug2 self.obj = nil end self:SetMutantImage() return self:FillList() end function UIMutantLoot:Update() CUIScriptWnd.Update(self) -- Highlight selected items for idx,ci in pairs(self.CC.cell) do if (not ci:IsCursorOverWindow()) then if ci.flags.selected then ci:Highlight(true,"green") else ci:Highlight(false) end end end -- Updating item info box and item cell containers local found_cell = self.CC:Update(self.item_info) if (not found_cell) then self.item_info:Update() end end -- Utility function UIMutantLoot:SetMutantImage() local mutant_id = game.translate_string(ini_sys:r_string_ex(self.section,"species") or "") local kind = ini_sys:r_string_ex(self.section,"kind") or "unknown" mutant_id = kind_to_section[kind] or mutant_id local mutant_f = "ui\\ui_actor_monsters_pda_1" local mutant_x = 0 local mutant_y = 0 mutant_f = tostring(killed_mutant_tbl[mutant_id].file) mutant_x = tostring(killed_mutant_tbl[mutant_id].x) mutant_y = tostring(killed_mutant_tbl[mutant_id].y) local x1 = mutant_x local y1 = mutant_y local mutant_width = 393 local mutant_height = 200 local x2 = x1 + mutant_width local y2 = y1 + mutant_height self.image:InitTexture(tostring(mutant_f)) self.image:SetTextureRect(Frect():set(x1,y1,x2,y2)) self.image:SetStretchTexture(true) end function UIMutantLoot:Loot(loot_all) local obj_mutant = level.object_by_id(self.id) if (not obj_mutant) then self:Close() return end local is_looted local sim = alife() local actor = db.actor --[[Disable old magic hunter kit. I'm leaving this here in case anyone wants to know what it used to be. -tB local backpack = actor:item_in_slot(13) local is_huntkit local needs_equipped_hk = ui_options.get("gameplay/general/need_equipped_hkit") if (needs_equipped_hk and (backpack and (backpack:section() == sec_kit_hunt))) or (not needs_equipped_hk and db.actor:object(sec_kit_hunt)) then is_huntkit = true end ]]-- -- Spawn selected items, clean from loot table if loot_all then local tbl = self.loot -- temp for sec,t in pairs(tbl) do for i=1,t.count do is_looted = true item_knife.degradate() alife_create_item(sec, db.actor, item_prop_table) --[[Disable old magic hunter kit. -tB if is_huntkit and (math.random(100) < bonus_part_chance) then alife_create_item(sec, db.actor, item_prop_table) end ]]-- self.loot[sec].count = self.loot[sec].count - 1 if (self.loot[sec].count == 0) then self.loot[sec] = nil end end end else for idx,ci in pairs(self.CC.cell) do if ci.flags.selected then local sec = ci.section is_looted = true item_knife.degradate() alife_create_item(sec, db.actor, item_prop_table) --[[Disable old magic hunter kit. -tB if is_huntkit and (math.random(100) < bonus_part_chance) then alife_create_item(sec, db.actor, item_prop_table) end ]]-- self.loot[sec].count = self.loot[sec].count - 1 if self.loot[sec].count == 0 then self.loot[sec] = nilt end end end end -- If no item is looted, don't proceed if (not is_looted) then return end -- Increat field dressings stat game_statistics.increment_statistic("field_dressings") -- Mutant post-state save_var(obj_mutant,"loot",self.loot) local is_more_loot = not is_empty(self.loot) -- Refill loot list if there's loot left if ((not actor_effects.is_animations_on()) and is_more_loot) then self:FillList() else self:Close() end end function UIMutantLoot:FillList() --developed by Dimeyne, copied by Wafel self.loot = load_var(self.obj,"loot",nil) if not self.loot then self.loot = {} loot_mutant(self.section, self.clsid, self.loot, nil, true, self.obj) save_var(self.obj,"loot",self.loot) end local is_there_loot local inv = {} for sec,t in pairs(self.loot) do for i=1,t.count do inv[#inv + 1] = sec end is_there_loot = true end if (self.obj:clsid() ~= clsid.crow) and load_var(self.obj,"looted",nil) then --Exclude crows from this check since they're managed by bind_crow.script -tB is_there_loot = false end if is_there_loot then --Flipping the inventory array so unique parts show up first like eyes and feet. I could sort it more specificly, but this is good enough. -tB local flippedInv = {} for i=#inv, 1, -1 do flippedInv[#flippedInv + 1] = inv[i] end self:ShowDialog(true) self.CC:Reinit(flippedInv,nil,true) return true else actor_menu.set_msg(1, "st_body_useless",3) end end function UIMutantLoot:SetMutantState(is_more_loot, obj_mutant) obj_mutant = obj_mutant or level.object_by_id(self.id) if (is_more_loot == nil) then is_more_loot = not is_empty(self.loot) end -- We set mutant state to looted or not if there's loot left, so other NPCs can decide what to do with the corpse if obj_mutant then if is_more_loot then se_save_var(obj_mutant:id(),obj_mutant:name(),"looted",false) xr_corpse_detection.set_valuable_loot(self.id,true) else se_save_var(obj_mutant:id(),obj_mutant:name(),"looted",true) xr_corpse_detection.set_valuable_loot(self.id,false) end else printe("!ERROR ui_mutant_loot | can't retrieve online object of mutant [%s](%s)", self.section, self.id) end end -- Callbacks function UIMutantLoot:On_CC_Mouse1(cont, idx) local ci = self.CC.cell[idx] if (not ci) then return end if (not ci.flags.selected) then ci.flags.selected = true else ci.flags.selected = nil end end function UIMutantLoot:OnButton_LootSelected() self:Loot(false) end function UIMutantLoot:OnButton_LootAll() self:Loot(true) end function UIMutantLoot:Close() self:SetMutantState() self:HideDialog() Unregister_UI("UIMutantLoot") end function UIMutantLoot:OnKeyboard(dik, keyboard_action) local res = CUIScriptWnd.OnKeyboard(self,dik,keyboard_action) if (res == false) then self.CC:OnKeyboard(dik, keyboard_action) if (dik == DIK_keys.DIK_RETURN) then self:OnButton_LootAll() elseif (dik == DIK_keys.DIK_ESCAPE) then self:Close() end end return res end