diff --git a/Data/Script/origin/common.lua b/Data/Script/origin/common.lua
index 239cc77ed4..2254bf1118 100644
--- a/Data/Script/origin/common.lua
+++ b/Data/Script/origin/common.lua
@@ -56,6 +56,7 @@ COMMON = {}
require 'origin.common_shop'
require 'origin.common_vars'
require 'origin.common_tutor'
+require 'origin.menu.storage.WithdrawMenu'
--Automatically load the appropriate localization for the specified package, or defaults to english!
function COMMON.AutoLoadLocalizedStrings()
@@ -1029,8 +1030,8 @@ function COMMON.ShowTeamStorageMenu()
UI:StorageMenu()
UI:WaitForChoice()
elseif result == 2 then
- UI:WithdrawMenu()
- UI:WaitForChoice()
+ WithdrawMenu.run()
+
elseif result == 3 then
UI:ChoiceMenuYesNo(STRINGS:FormatKey('DLG_STORE_ALL_CONFIRM'), false);
UI:WaitForChoice()
diff --git a/Data/Script/origin/menu/storage/WithdrawMenu.lua b/Data/Script/origin/menu/storage/WithdrawMenu.lua
new file mode 100644
index 0000000000..77250bac5d
--- /dev/null
+++ b/Data/Script/origin/menu/storage/WithdrawMenu.lua
@@ -0,0 +1,458 @@
+--[[
+ WithdrawMenu
+ lua port by MistressNebula
+
+ Opens a menu, potentially with multiple pages, that allows the player to select one or
+ more items in their inventory.
+ It contains a run method for quick instantiation and an WithdrawChosenMenu port for confirmation.
+ This equivalent is NOT SAFE FOR REPLAYS. Do NOT use in dungeons until further notice.
+]]
+
+local EquipStateType = luanet.import_type('PMDC.Dungeon.EquipState')
+local EdibleStateType = luanet.import_type('PMDC.Dungeon.EdibleState')
+local OrbStateType = luanet.import_type('PMDC.Dungeon.OrbState')
+local WandStateType = luanet.import_type('PMDC.Dungeon.WandState')
+local ExclusiveStateType = luanet.import_type('PMDC.Dungeon.ExclusiveState')
+local item_index = _DATA.DataIndices[RogueEssence.Data.DataManager.DataType.Item]
+local UseTypes = RogueEssence.Data.ItemData.UseType
+
+local hasState = function(item_summary, stateType)
+ return item_summary:ContainsState(luanet.ctype(stateType))
+end
+
+--- Menu for selecting items from the player's inventory.
+WithdrawMenu = Class("WithdrawMenu")
+
+--- List of filters that can be used to find specific items more easily.
+--- Every filter's function takes an ItemSummary as input and must return a boolean.
+--- Filters with no function will display items that don't belong to any other filter.
+WithdrawMenu.Filters = {
+ {Key = "MENU_FILTER_EQUIP", Predicate = function(item) return hasState(item, EquipStateType) end},
+ {Key = "MENU_FILTER_THROW", Predicate = function(item) return item.UsageType == UseTypes.Throw end},
+ {Key = "MENU_FILTER_FOOD", Predicate = function(item) return hasState(item, EdibleStateType) end},
+ {Key = "MENU_FILTER_TM", Predicate = function(item) return item.UsageType == UseTypes.Learn end},
+ {Key = "MENU_FILTER_TOOLS", Predicate = function(item) return hasState(item, OrbStateType) or hasState(item, WandStateType) end},
+ {Key = "MENU_FILTER_OTHER"},
+ {Key = "MENU_FILTER_EXCL", Predicate = function(item) return hasState(item, ExclusiveStateType) end},
+ {Key = "MENU_FILTER_BOXES", Predicate = function(item) return item.UsageType == UseTypes.Box end}
+}
+
+--- Creates a new ``WithdrawMenu`` instance using the provided list and callbacks.
+--- @param defaultChoice number the choice that will be selected by default, counting from 0.
+--- @param continueOnChoose boolean if true, the menu will not be closed after selecting a target.
+--- @param filter_index number the index of the filter to load by default, counting from 1. 0 corresponds to the full unfiltered list. Defaults to 0.
+function WithdrawMenu:initialize(defaultChoice, continueOnChoose, filter_index)
+ -- constants
+ self.MAX_ELEMENTS = 8
+
+ -- preparing data
+ self.title = STRINGS:FormatKey("MENU_STORAGE_TITLE")
+ self.continueOnChoose = continueOnChoose
+ self.currentFilter = math.min(math.max(0, filter_index or 0), #WithdrawMenu.Filters)
+ self.refuseAction = function() _MENU:RemoveMenu() end
+ self.menuWidth = RogueEssence.Menu.ItemMenu.ITEM_MENU_WIDTH
+ self:generate_options()
+ self.currentOptions = {}
+ self.currentSlots = {}
+ self.filterFlags = {}
+ self:MakeCurrentFilteredLists()
+ if #self.currentOptions<=0 then --TODO understand why this isn't firing
+ self.currentFilter = 0
+ self:MakeCurrentFilteredLists()
+ end
+ self.max_choices = self:count_valid()
+ defaultChoice = math.min(math.max(0, defaultChoice), #self.currentOptions-1)
+ self.label = "WITHDRAW_MENU_LUA"
+
+ self.multiConfirmAction = function(list)
+ local slots = {}
+ for _, idx in ipairs(list) do
+ table.insert(slots, self.currentSlots[idx+1])
+ end
+ local menu = WithdrawChosenMenu:new(slots, self, self.continueOnChoose)
+ _MENU:AddMenu(menu.menu, true)
+ end
+
+ local GraphicsManager = RogueEssence.Content.GraphicsManager
+ -- creating the menu
+ local origin = RogueElements.Loc(16, 16)
+ local option_array = luanet.make_array(RogueEssence.Menu.MenuChoice, self.currentOptions)
+ self.menu = RogueEssence.Menu.ScriptableMultiPageMenu(label, origin, self.menuWidth, self.title, option_array, defaultChoice, self.MAX_ELEMENTS, self.refuseAction, self.refuseAction, false, self.max_choices, self.multiConfirmAction)
+ self.menu.ChoiceChangedFunction = function() self:updateSummary() end
+ self.menu.MultiSelectChangedFunction = function() self:Deselect() end
+ self.menu.UpdateFunction = function(input) self:updateFunction(input) end
+
+ -- create the summary window
+ self.summary = RogueEssence.Menu.ItemSummary(RogueElements.Rect.FromPoints(
+ RogueElements.Loc(16, GraphicsManager.ScreenHeight - 8 - GraphicsManager.MenuBG.TileHeight * 2 - RogueEssence.Menu.MenuBase.VERT_SPACE * 4),
+ RogueElements.Loc(GraphicsManager.ScreenWidth - 16, GraphicsManager.ScreenHeight - 8)))
+ self.menu.SummaryMenus:Add(self.summary)
+ self:updateSummary()
+end
+
+--- Processes the menu's properties and generates the ``RogueEssence.Menu.MenuElementChoice`` list that will be displayed.
+function WithdrawMenu:generate_options()
+ local sortedKeys = item_index:GetOrderedKeys(true)
+ self.slots_list = {}
+ self.options_list = {}
+ self.filtered_indices = {}
+ for i = 1,#WithdrawMenu.Filters, 1 do
+ if WithdrawMenu.Filters[i].Predicate then
+ self.filtered_indices[i] = {}
+ end
+ end
+ self.filtered_indices[0] = {}
+ self.filtered_indices[-1] = {}
+
+ local num_entries = 0
+
+ -- add storage in sort order
+ for id in luanet.each(sortedKeys) do
+ local qty = GAME:GetPlayerStorageItemCount(id)
+ if qty > 0 then
+ local item_data = item_index:Get(id)
+ local slot = RogueEssence.Dungeon.WithdrawSlot(false, id, 0)
+ local menuText = RogueEssence.Menu.MenuText(item_data:GetIconName(), RogueElements.Loc(2, 1))
+ local menuCount = RogueEssence.Menu.MenuText("("..qty..")", RogueElements.Loc(RogueEssence.Menu.ItemMenu.ITEM_MENU_WIDTH - 8 * 4, 1), RogueElements.DirV.Up, RogueElements.DirH.Right, Color.White)
+ local menuChoice = RogueEssence.Menu.MenuElementChoice(function() self:choose() end, true, menuText, menuCount)
+ table.insert(self.options_list, menuChoice)
+ table.insert(self.slots_list, slot)
+ num_entries = num_entries + 1
+
+ local in_other = true
+ for i = 1, #WithdrawMenu.Filters, 1 do
+ if WithdrawMenu.Filters[i].Predicate and WithdrawMenu.Filters[i].Predicate(item_data) then
+ self.filtered_indices[i][num_entries] = true
+ in_other = false
+ end
+ end
+ if in_other then
+ self.filtered_indices[-1][num_entries] = true
+ end
+ self.filtered_indices[0][num_entries] = true
+ end
+ end
+
+ -- add boxes
+ for i = 0, _DATA.Save.ActiveTeam.BoxStorage.Count-1, 1 do
+ local slot = RogueEssence.Dungeon.WithdrawSlot(true, "", i)
+ local boxId = _DATA.Save.ActiveTeam.BoxStorage[i].ID
+ local item_data = item_index:Get(boxId)
+ local menuChoice = RogueEssence.Menu.MenuTextChoice(item_data:GetIconName(), function() self:choose(slot) end)
+ table.insert(self.options_list, menuChoice)
+ table.insert(self.slots_list, slot)
+ num_entries = num_entries + 1
+
+ local in_other = true
+ for i = 1, #WithdrawMenu.Filters, 1 do
+ if WithdrawMenu.Filters[i].Predicate and WithdrawMenu.Filters[i].Predicate(item_data) then
+ self.filtered_indices[i][num_entries] = true
+ in_other = false
+ end
+ end
+ if in_other then
+ self.filtered_indices[-1][num_entries] = true
+ end
+ self.filtered_indices[0][num_entries] = true
+ end
+end
+
+--- Counts the number of valid options generated.
+--- @return number the number of valid options.
+function WithdrawMenu:count_valid()
+ if self.continueOnChoose then
+ return _DATA.Save.ActiveTeam:GetMaxInvSlots(_ZONE.CurrentZone) - _DATA.Save.ActiveTeam:GetInvCount()
+ else
+ return -1
+ end
+end
+
+--- Passes the currently hovered option index to the menu's confirmation callback.
+function WithdrawMenu:choose()
+ self.multiConfirmAction({self.menu.CurrentChoiceTotal})
+end
+
+--- Uses the current input to apply changes to the menu.
+--- @param input userdata the ``RogueEssense.InputManager``.
+function WithdrawMenu:updateFunction(input)
+ if input:JustPressed(RogueEssence.FrameInput.InputType.SortItems) then
+ _GAME:SE("Menu/Skip")
+ self:FilterCommand()
+ end
+end
+
+--- Opens the filter menu.
+function WithdrawMenu:FilterCommand()
+ local menu = WithdrawFilterMenu:new(self, self.currentFilter)
+ _MENU:AddMenu(menu.menu, true)
+end
+
+--- Removes a deselected option that doesn't fit the current filter
+function WithdrawMenu:Deselect()
+ local index = self.menu.CurrentChoiceTotal +1
+ if not self.currentOptions[index].Selected and not self.filterFlags[index] then
+ table.remove(self.currentOptions, index)
+ table.remove(self.currentSlots, index)
+ table.remove(self.filterFlags, index)
+ self.menu:ImportChoices(luanet.make_array(RogueEssence.Menu.MenuChoice, self.currentOptions))
+ self.menu:SetCurrentPage(self.menu.CurrentPage)
+ end
+ self:updateSummary()
+end
+
+--- Checks if the filter corresponding to the parameter has any entries.
+--- @param index number the index of the filter to check
+--- @return boolean true if the filter has entries, false otherwise
+function WithdrawMenu:CanApplyFilter(index)
+ local filter_index = -1
+ if index == 0 or WithdrawMenu.Filters[index].Predicate then filter_index = index end
+ for i, val in pairs(self.filtered_indices[filter_index]) do
+ if val then return true end
+ end
+ return false
+end
+
+--- Applies the filter corresponding to the parameter.
+--- @param index number the index of the filter to apply
+--- @param reset_page boolean if true, sets the current page to 0, otherwise it just refreshes the current page. defaults to true.
+function WithdrawMenu:ApplyFilter(index, reset_page)
+ if index == self.currentFilter then return end
+ if reset_page~=false then reset_page = true end
+ if index < 0 or index > #WithdrawMenu.Filters then return end
+ self.currentFilter = index
+ self:MakeCurrentFilteredLists()
+ self.menu:ImportChoices(luanet.make_array(RogueEssence.Menu.MenuChoice, self.currentOptions))
+ if reset_page then self.menu:SetCurrentPage(0)
+ else self.menu:SetCurrentPage(self.menu.CurrentPage) end
+end
+
+--- Updates the current lists and returns the resulting option list
+--- @return table a list of ``RogueEssence.Dungeon.MenuChoice``
+function WithdrawMenu:MakeCurrentFilteredLists()
+ local filter_index = -1
+ if self.currentFilter == 0 or WithdrawMenu.Filters[self.currentFilter].Predicate then filter_index = self.currentFilter end
+
+ local options, slots, flags = {}, {}, {}
+ for i, entry in ipairs(self.options_list) do
+ local add, flag = false, true
+ if self.filtered_indices[filter_index][i] then
+ add = true
+ elseif entry.Selected then
+ add, flag = true, false
+ end
+ if add then
+ table.insert(options, entry)
+ table.insert(slots, self.slots_list[i])
+ table.insert(flags, flag)
+ end
+ end
+ self.currentOptions = options
+ self.currentSlots = slots
+ self.filterFlags = flags
+end
+
+--- Updates the summary window.
+function WithdrawMenu:updateSummary()
+ local slot = self.currentSlots[self.menu.CurrentChoiceTotal+1]
+ if not slot.IsBox then
+ self.summary:SetItem(RogueEssence.Dungeon.InvItem(slot.ItemID))
+ else
+ self.summary:SetItem(_DATA.Save.ActiveTeam.BoxStorage[slot.BoxSlot])
+ end
+end
+
+--- Reinitializes this ``WithdrawMenu``, refreshing the instance's options and filter data.
+function WithdrawMenu:reinitialize()
+ -- regenerating data
+ self:generate_options()
+ if self:CanApplyFilter(self.currentFilter) then
+ self:ApplyFilter(self.currentFilter, false)
+ else
+ self:ApplyFilter(0)
+ self.menu:SetCurrentPage(0)
+ end
+ self.max_choices = self:count_valid()
+end
+
+
+
+
+WithdrawChosenMenu = Class("WithdrawChosenMenu")
+
+--- Creates a new ``WithdrawChosenMenu`` instance using the provided object as parent.
+--- @param slots table the list of selected WithdrawSlots
+--- @param parent userdata the parent menu
+--- @param continueOnChoose boolean if true, it will refresh the WithdrawMenu instead of closing it when an action is confirmed
+function WithdrawChosenMenu:initialize(slots, parent, continueOnChoose)
+ local x, y = parent.menu.Bounds.Right, parent.menu.Bounds.Top
+ local width = 72
+ local label = "WITHDRAW_CHOSEN_MENU_LUA"
+ self.parent = parent
+ self.slots = slots
+ self.continueOnChoose = continueOnChoose
+ if not slots[1].IsBox then
+ self.first_item = RogueEssence.Dungeon.InvItem(slots[1].ItemID)
+ else
+ self.first_item = _DATA.Save.ActiveTeam.BoxStorage[slots[1].BoxSlot]
+ end
+
+ local options = {
+ {STRINGS:FormatKey("MENU_ITEM_WITHDRAW"), true, function() self:confirm() end},
+ {STRINGS:FormatKey("MENU_INFO"), true, function() _MENU:AddMenu(RogueEssence.Menu.TeachInfoMenu(self.first_item.ID), false) end},
+ {STRINGS:FormatKey("MENU_EXIT"), true, function() _MENU:RemoveMenu() end}
+ }
+ if #slots>1 or item_index:Get(self.first_item.ID).UsageType ~= UseTypes.Learn then
+ table.remove(options, 2)
+ end
+
+ self.menu = RogueEssence.Menu.ScriptableSingleStripMenu(label, x, y, width, options, 0, function() _MENU:RemoveMenu() end)
+end
+
+function WithdrawChosenMenu:confirm(result)
+ _MENU:RemoveMenu()
+ if #self.slots>1 then
+ self:TakeItems(self.slots)
+ else
+ local selected = self.slots[1]
+ if not self.continueOnChoose then
+ local itemID
+ if selected.IsBox then
+ itemID = _DATA.Save.ActiveTeam.BoxStorage[selected.BoxSlot].ID
+ else
+ itemID = selected.ItemID
+ end
+
+ local entry = _DATA.DataIndices[RogueEssence.Data.DataManager.DataType.Item]:Get(itemID)
+
+ if entry.MaxStack > 1 then
+ _MENU:AddMenu(RogueEssence.Menu.ItemAmountMenu(RogueElements.Loc(self.menu.Bounds.X, self.menu.Bounds.End.Y), entry.MaxStack, function(n) self:TakeMultiple(n) end), true)
+ else
+ self:TakeItems(self.slots)
+ end
+ elseif not selected.IsBox then
+ local entry = _DATA.DataIndices[RogueEssence.Data.DataManager.DataType.Item]:Get(selected.ItemID)
+ local openSlots = _DATA.Save.ActiveTeam:GetMaxInvSlots(_ZONE.CurrentZone) - _DATA.Save.ActiveTeam:GetInvCount()
+ --stackable items need to be counted differently
+ if (entry.MaxStack > 1) then
+ local residualSlots = 0
+ for j = 0, _DATA.Save.ActiveTeam:GetInvCount()-1, 1 do
+ if _DATA.Save.ActiveTeam:GetInv(j).ID == selected.ItemID and _DATA.Save.ActiveTeam:GetInv(j).Amount < entry.MaxStack then
+ residualSlots = residualSlots + entry.MaxStack - _DATA.Save.ActiveTeam:GetInv(j).Amount
+ end
+ end
+ openSlots = (openSlots * entry.MaxStack) + residualSlots
+ end
+
+ openSlots = math.min(openSlots, _DATA.Save.ActiveTeam.Storage[selected.ItemID])
+ _MENU:AddMenu(RogueEssence.Menu.ItemAmountMenu(RogueElements.Loc(self.menu.Bounds.X, self.menu.Bounds.End.Y), openSlots, function(n) self:TakeMultiple(n) end), true)
+ else
+ self:TakeItems(self.slots)
+ end
+ end
+end
+
+function WithdrawChosenMenu:TakeMultiple(amount) --TODO fix this shit
+ local slots = {}
+ for i = 1, amount, 1 do
+ table.insert(slots, self.slots[1])
+ end
+ self:TakeItems(slots)
+end
+
+function WithdrawChosenMenu:TakeItems(slots)
+ local ListType = luanet.import_type('System.Collections.Generic.List`1')
+ local WithdrawSlotType = luanet.import_type('RogueEssence.Dungeon.WithdrawSlot')
+ local list_slots = LUA_ENGINE:MakeGenericType( ListType, { WithdrawSlotType }, { })
+ for _, slot in ipairs(slots) do list_slots:Add(slot) end
+ --remove items and fetch them
+ local items = _DATA.Save.ActiveTeam:TakeItems(list_slots)
+
+ for item in luanet.each(items) do
+ local entry = _DATA:GetItem(item.ID)
+ if entry.MaxStack > 1 then
+ for inv in luanet.each(LUA_ENGINE:MakeList(_DATA.Save.ActiveTeam:EnumerateInv())) do
+ if inv.ID == item.ID and inv.Cursed == item.Cursed and inv.Amount < entry.MaxStack then
+ local addValue = math.min(entry.MaxStack - inv.Amount, item.Amount)
+ inv.Amount = inv.Amount + addValue
+ item.Amount = item.Amount - addValue
+ if (item.Amount <= 0) then
+ break
+ end
+ end
+ end
+ --withdraw the remainder normally
+ if (item.Amount > 0) then
+ _DATA.Save.ActiveTeam:AddToInv(item)
+ end
+ else
+ _DATA.Save.ActiveTeam:AddToInv(item)
+ end
+ end
+
+ if self.continueOnChoose then
+ --refresh base menu
+ local hasStorage = (_DATA.Save.ActiveTeam.BoxStorage.Count > 0)
+ for entry in luanet.each(_DATA.Save.ActiveTeam.Storage) do
+ if (entry.Value > 0) then
+ hasStorage = true
+ break
+ end
+ end
+ if hasStorage then
+ self.parent:reinitialize(self.parent.menu.CurrentChoiceTotal, self.parent.continueOnChoose, self.parent.currentFilter)
+ else
+ _MENU:RemoveMenu()
+ end
+ else
+ _MENU:RemoveMenu()
+ end
+end
+
+
+
+
+WithdrawFilterMenu = Class("WithdrawFilterMenu")
+
+--- Creates a new ``WithdrawFilterMenu`` instance using the provided object as parent.
+--- @param parent userdata the parent menu
+function WithdrawFilterMenu:initialize(parent, defaultOption)
+ self.parent = parent
+ self.color = defaultOption > 0
+ self.defaultOption = math.max(0, defaultOption-1)
+ local x, y = self.parent.menu.Bounds.Right, self.parent.menu.Bounds.Top
+ local width = 72
+ local label = "WITHDRAW_FILTER_MENU_LUA"
+
+ self:generate_options()
+
+ self.menu = RogueEssence.Menu.ScriptableSingleStripMenu(label, x, y, width, self.options, self.defaultOption, function() _MENU:RemoveMenu() end)
+ local b = self.menu.Bounds
+ self.menu.Bounds = RogueElements.Rect(b.X, b.Y, b.Width, b.Height+2)
+end
+
+function WithdrawFilterMenu:generate_options()
+ self.options = {}
+ for i, entry in ipairs(WithdrawMenu.Filters) do
+ local str = STRINGS:FormatKey(entry.Key)
+ if i == self.defaultOption+1 and self.color then str = "[color=#FFFF00]"..str.."[color]" end
+ table.insert(self.options, {str, self.parent:CanApplyFilter(i), function() self:choose(i) end})
+ end
+ table.insert(self.options, {STRINGS:FormatKey("MENU_FILTER_ALL"), true, function() self:choose(0) end})
+end
+
+function WithdrawFilterMenu:choose(index)
+ self.parent:ApplyFilter(index)
+ _MENU:RemoveMenu()
+end
+
+
+
+
+
+
+--- Creates a ``WithdrawMenu`` instance and runs it.
+function WithdrawMenu.run()
+ local menu = WithdrawMenu:new(0, true, 0)
+ UI:SetCustomMenu(menu.menu)
+ UI:WaitForChoice()
+end
\ No newline at end of file
diff --git a/Strings/strings.resx b/Strings/strings.resx
index 59661bd2cb..c4202a80a2 100644
--- a/Strings/strings.resx
+++ b/Strings/strings.resx
@@ -921,6 +921,33 @@
Unfavorite
+
+ All
+ Menu option to stop filtering items
+
+ Boxes
+ Menu option to filter only boxes
+
+ Eqpt.
+ Menu option to filter only equipment
+
+ Excl.
+ Menu option to filter only exclusive items
+
+ Food
+ Menu option to filter only edible items
+
+ Other
+ Menu option to filter items that do not belong to any other categories
+
+ Tool
+ Menu option to filter orbs and wands
+
+ Throw
+ Menu option to filter only throwable items
+
+ TM
+ Menu option to filter only tms
GamePad