Fixing/creating a working, stable inventory system (desperate)

hi everybody.

i’ve been working on this darn inventory system for weeks now, and i cant figure it out. this thing has been weighing this entire side project and my sanity down and i cant figure out the issues, how to make it better, and if i should even bother.


as you can see, the rock on the right weighs 13.5 (nonsensical), whereas the other one on the left weighs 16-ish.

I don’t understand what i’m doing wrong, and i’ve gotten so desperate i began to plead for AI to spit out something helpful from its toxic maw.

I have a item defs module.

local ServerStorage = game:GetService("ServerStorage")
local Models = ServerStorage.Items

return {
	--[[
	["Rock"] = {
		Name = "Rock",
		Description = "Just a rock.",
		Stack = 1,
		Weight = 5,
		Type =  "Items",
		Rarity = "Common",
		Value = 1,
		Interaction = function(item: Instance, player: Player)
			return
		end,
		OnSpawn = function(item: Instance, owner: Player)
			return
		end,
		Hooks = {}
	}
	]]
	["Block"] = {
		Model = Models.Block,
		Description = "THIS IS A DEV ITEM THAT DOES NOTHING",
		Stack = 2,
		Weight = 100,
		Type =  "Items",
		Rarity = "Legendary",
		Value = 100,
		Hooks = {}
	},
	["Rock"] = {
		Model = Models.Rock,
		Description = "Just a rock.",
		Stack = 1,
		Weight = 5,
		Type =  "Items",
		Rarity = "Common",
		Value = 1,
		Hooks = {}
	},
}

I got an ItemManager module.

local Item = require(script.Parent.Item)

local ItemManager = {}
ItemManager.Items = {}
ItemManager.Players = {}

-- // SERVICES
local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- // MODULES
local ItemDefs = require(script.Parent.ItemDefinitions)

-- // REMOTES
local UpdateFunction = ReplicatedStorage.Remotes.Inventory:WaitForChild("InventoryUpdateFunction") :: RemoteFunction
local UpdateEvent = ReplicatedStorage.Remotes.Inventory:WaitForChild("InventoryUpdate") :: RemoteEvent
local ActionRemote = ReplicatedStorage.Remotes.Inventory:WaitForChild("InventoryAction") :: RemoteEvent

-- // CODE

-- [[ HELPER ]] --
local function deepCopy(original)
	local copy = {}
	for k, v in pairs(original) do
		if type(v) == "table" then
			copy[k] = deepCopy(v)
		else
			copy[k] = v
		end
	end
	return copy
end

--[[
Ensures the player has an inventory.
]]
function ItemManager:EnsurePlayerInventory(player: Player)
	if not self.Players[player.UserId] then
		self.Players[player.UserId] = {
			TotalWeight = 0,
			MaxWeight = 100,
			Items = {}
		}
	end
	return self.Players[player.UserId]
end

--[[
Retrieves defined item data, optionally overriding some data.
]]
function ItemManager:GetDefWithOverrides(original, overrides)
	local clone = table.clone(original)
	for k, v in pairs(overrides) do
		clone[k] = v
	end
	return clone
end

--[[
Creates a new item class, with optional overrides for the item data.
]]
function ItemManager:CreateItem(name: string, model: Instance, overrides: {[string]: any}?)
	local base = ItemDefs[name]
	assert(base, `No base item definition found for {name}`)

	local finalData = self:GetDefWithOverrides(base, overrides or {})
	local newItem = Item.new(name, model, finalData)
	table.insert(self.Items, newItem)
	return newItem
end

--[[
Creates and spawns a new item class.
]]
function ItemManager:SpawnItem(name: string, model: Instance, data: Item.ItemData, Cframe: CFrame, owner: Player?)
	local item = self:CreateItem(name, model, deepCopy(data))
	item:Spawn(Cframe, owner)
	return item
end

--[[
Uses an existing item class to spawn.
]]
function ItemManager:SpawnExisting(item, Cframe: CFrame, owner: Player)
	item:Spawn(Cframe, owner)
	return item
end

--[[
If the item has a special interaction, trigger it.
Else, the object is picked up.
]]
function ItemManager:Interact(item, player)
	if not item.Interaction then
		self:PickUp(item, player)
	else
		item:Interact(player)
	end
end

--[[
Picks up the item, and adds it to the player inventory.
]]
function ItemManager:PickUp(item, player: Player, amount: number)
	if not item or not item.Data then
		warn("Attempted pickup on nil item/data!")
		return
	end

	if not item.Instance then
		warn("Tried to pick up an item with no active instance!")
		return
	end
	
	amount = amount or 1

	local newStack = math.clamp(item:GetStack() - amount, 0, math.huge)
	item:TweakData("Stack", newStack)
	
	self:AddToInventory(player, item, amount)

	if item:GetStack() <= 0 then
		self:DestroyItem(item)
	end
end

--[[
Destroys the item class.
]]
function ItemManager:DestroyItem(item)
	for i, v in ipairs(self.Items) do
		if v == item then
			item:Destroy()
			table.remove(self.Items, i)
			break
		end
	end
end

--[[
Looks up the item name in the item list, returning the first value.
]]
function ItemManager:LookupByName(name: string)
	for _, item in ipairs(self.Items) do
		if item.Name == name then
			return item
		end
	end
	return nil
end

--[[
Looks up the item UUID in the item list, returning a match.
]]
function ItemManager:LookupByUUID(uuid: string)
	for _, item in ipairs(self.Items) do
		local itemUUID = item.Instance:GetAttribute("UUID")
		if itemUUID == uuid then
			return item
		end
	end
	warn("LookupByUUID failed for", uuid)
	return nil
end

--[[
Looks up the table for matching instances.
]]
function ItemManager:LookupByInstance(instance: Instance)
	for _, item in ipairs(self.Items) do
		if item.Instance == instance then
			return item
		end
	end
	warn("LookupByInstance failed for", instance:GetFullName())
	return nil
end

--[[
Adds the item to the player inventory.
]]
function ItemManager:AddToInventory(player: Player, item, amount: number)
	assert(item, "Item not provided!")
	assert(player, "Player not provided!")
	amount = amount or 1
	
	local playerData = self:EnsurePlayerInventory(player)
	local items = playerData.Items

	local index = items[item.Name]
	if index then
		index.Amount += amount
		index.Weight = item:GetWeight() * index.Amount
		index.Value = item:GetValue() * index.Amount
	else
		items[item.Name] = {
			Name = item.Name,
			Description = item:GetDescription(),
			Rarity = item:GetRarity(),
			Weight = item:GetWeight() * amount,
			Value = item:GetValue() * amount,
			Type = item:GetType(),
			Amount = amount,
		}
	end
	
	local total = 0
	for _, entry in pairs(playerData.Items) do
		total += entry.Weight
	end
	playerData.TotalWeight = total
	
	local updatedInventory = self:GetPlayerInvData(player)
	UpdateEvent:FireClient(player, updatedInventory)
end

--[[
Removes the item from the player inventory.
]]
function ItemManager:RemoveFromInventory(player: Player, item, amount: number)
	assert(item, "Item not provided!")
	assert(player, "Player not provided!")
	amount = amount or 1

	local playerData = self:EnsurePlayerInventory(player)
	local items = playerData.Items

	local index = items[item.Name]
	if index then
		index.Amount -= amount
		index.Weight = item:GetWeight() * index.Amount
		index.Value = item:GetValue() * index.Amount
	else
		warn("Tried to remove item that doesn't exist in inventory!")
		return
	end

	if index.Amount <= 0 then
		items[item.Name] = nil
	end
	
	local total = 0
	for _, entry in pairs(playerData.Items) do
		total += entry.Weight
	end
	playerData.TotalWeight = total

	local updatedInventory = self:GetPlayerInvData(player)
	UpdateEvent:FireClient(player, updatedInventory)
end


--[[
Retrieves player's data.
]]
function ItemManager:GetPlayerInvData(player: Player)
	local data = self:EnsurePlayerInventory(player)
	return data
end

UpdateFunction.OnServerInvoke = function(player: Player, action, args)
	if action == "Get" then
		return ItemManager:GetPlayerInvData(player)
	elseif action == "Drop" then
		local itemName = args.Item.Name
		local amount = args.Amount or 1
		local playerInv = ItemManager.Players[player.UserId]
		if not playerInv then return end

		local itemData = playerInv.Items[itemName]
		if not itemData then return end

		local fakeItem = {
			Name = itemName,
			GetWeight = function() return itemData.Weight / itemData.Amount end,
			GetValue = function() return itemData.Value / itemData.Amount end,
		}

		ItemManager:RemoveFromInventory(player, fakeItem, amount)

		local updatedItemDefData = deepCopy(ItemDefs[itemName])
		updatedItemDefData.Stack = amount
		updatedItemDefData.Value = fakeItem.GetValue() * amount
		updatedItemDefData.Weight = fakeItem.GetWeight() * amount

		local spawnCFrame = player.Character and player.Character:GetPivot() + Vector3.new(0, 2, 0) or CFrame.new()
		--local result = workspace:GetPartBoundsInRadius(spawnCFrame.Position, 7)
		--if result then
		--	for _, part in result do
		--		local item = ItemManager:LookupByInstance(part:FindFirstAncestor(itemName))
		--		if item then
		--			item:TweakData("Stack", item:GetStack() + amount)
		--			break
		--		end
		--	end
		--end
		ItemManager:SpawnItem(itemName, nil, updatedItemDefData, spawnCFrame)

		return ItemManager:GetPlayerInvData(player)
	end
end

return ItemManager

this is the local script.

-- // SERVICES
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

-- // MODULE
local Rarities = require(ReplicatedStorage.Modules.Rarities)

-- // REMOTES
local UpdateFunc = ReplicatedStorage.Remotes.Inventory.InventoryUpdateFunction
local UpdateEvent = ReplicatedStorage.Remotes.Inventory.InventoryUpdate

local ActionEvent = ReplicatedStorage.Remotes.Inventory.InventoryAction

-- // VARIABLES
local Player = Players.LocalPlayer

local Template = script.Item_Template
local Inventory = script.Parent
local Menu = Inventory.Parent.Parent
local TopBar = Menu.TopPanel.Tabs
local InvButton = TopBar.Inventory 

local InventoryContents = Inventory.Items

-- // CODE
local function UpdateInventory(updatedInv)
	for _, frame in InventoryContents:GetChildren() do
		if frame:IsA("Frame") then
			frame:Destroy()
		end
	end

	for itemName, value in pairs(updatedInv.Items) do
		local newFrame = Template:Clone()
		print(itemName)
		newFrame.Name = "Item_"..itemName
		local rarityName = value.Rarity or "Common"
		newFrame.Rarity.Text = "[ "..string.upper(rarityName).." ]" or "[ COMMON ]"
		newFrame.Rarity.TextColor3 = (Rarities[rarityName] and Rarities[rarityName].Color) or Color3.new(1, 1, 1)
		newFrame.ItemName.Text = itemName
		newFrame.Value.Text = `Prognosis: {value.Value} Rations`
		newFrame.Weight.Text = `{value.Weight} wt.`
		newFrame.Buttons.Drop.Text = `Drop [x{value.Amount}]`
		newFrame.Parent = InventoryContents
		newFrame.Visible = true
		
		newFrame.Buttons.Drop.Activated:Connect(function()
			local dropArgs = {
				Item = value,
				Amount = 1
			}
			UpdateFunc:InvokeServer("Drop", dropArgs)
		end)

		if value.Type ~= "Tool" and value.Type ~= "Apparel" then
			newFrame.Buttons.Equip.Visible = false
		end
	end


	if Inventory:FindFirstChild("Weight") then
		Inventory.Weight.Text = `Weight: {updatedInv.TotalWeight}/{updatedInv.MaxWeight}`
	end
end

-- Button behavior
local function ButtonTriggered()
	local UpdatedInv = UpdateFunc:InvokeServer("Get")
	UpdateInventory(UpdatedInv)
end

-- Event connections
UpdateEvent.OnClientEvent:Connect(UpdateInventory)

I get i’m probably doing horrible practices, and ai too, but I genuinely just want to get this thing over with so i could continue working on other things. Please, i am begging anyone to pitch in and help me out here.

sincerely, thank you.

just ask or say so if you need any additional scripts. all and any help is accepted and awaited.

1 Like

Can you explain an overview of what should happen in relation to the weight, ‘prognosis’ etc.? I think this would be useful since I don’t exactly understand what everything is trying to do here.
Also, if you’re asking AI, try to get an understanding of what changes it is actually making so you’re sure it’s not doing something unintentional

EDIT: are you sure you’re not just incorrectly setting the weight label above the rock to whatever that ‘prognosis’ value is? they seem to be the same - 13.5

‘Prognosis’ is a term for how valuable the item is… and that might be the case.

What is meant to happen:

  • Pick up an item (or its stack), get that item, update inventory to adjust to values.
  • Drop an item, it spawns the item in the world, removing one (for now) from the inventory, updating and adjusting its values. If a dropped item is already nearby, increase that instance’s stack.

Additionally, the problem is, there’s ONE item in the stack and according to the itemDef module, the values should be entirely different than what is shown.

Could I see the code for the UI that shows above the rock? I don’t think you gave that.

1 Like

I’ll keep this in mind and update you when I can hop on my computer. Currently doing outdoor work.

Right, here’s the code:

-- // SERVICES
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CollectionService = game:GetService("CollectionService")
local UIS = game:GetService("UserInputService")

-- // VARIABLES
local Player = Players.LocalPlayer
local PlayerGUI = Player:WaitForChild("PlayerGui")
local Mouse = Player:GetMouse()

local HoverInfo = PlayerGUI:WaitForChild("InfoGUI")
local Highlight = PlayerGUI:WaitForChild("Highlight")
local uiInfo = HoverInfo.Info

local Rarities = require(ReplicatedStorage.Modules.Rarities)
local InputRemote = ReplicatedStorage.Remotes.Input

local Keybinds = {
	[Enum.KeyCode.F] = function(uuid: string, Keycode)
		InputRemote:FireServer(uuid, Keycode)
	end,
	[Enum.KeyCode.Q] = function(uuid: string, Keycode)
		InputRemote:FireServer(uuid, Keycode)
	end,
}



-- // FUNCTIONS
local function SetAdornee(adornee)
	HoverInfo.Adornee = adornee
	Highlight.Adornee = adornee
	
	if adornee == nil then
		HoverInfo.Enabled = false
	else
		HoverInfo.Enabled = true
	end
end

local function Adjust(targetParent)
	if not targetParent then return end

	local rarity = targetParent:GetAttribute("Rarity")
	local stack = targetParent:GetAttribute("Stack")
	local weight = targetParent:GetAttribute("StackWeight")
	local name = targetParent.Name
	
	local interactable = CollectionService:HasTag(targetParent, "Item_Interactable")

	if not rarity or not name or not stack or not weight then
		warn("Missing attributes on item model:", targetParent)
		return
	end

	local index = Rarities[rarity]
	if index then
		HoverInfo.Divider.BackgroundColor3 = index.Color
		HoverInfo.Title.Text = name.." ["..tostring(stack).."]"
		HoverInfo.Title.TextColor3 = index.Color
		uiInfo.Rarity.Text = "Rarity: "..(index.Name or "N/A")
		uiInfo.Rarity.TextColor3 = index.Color
		uiInfo.Weight.Text = "Weight: "..tostring(weight) or "N/A"
		uiInfo.Use.Visible = interactable
		Highlight.FillColor = index.Color
	end
end


local currentModelConnection
local lastTarget -- stores the last hovered valid item

local function TrackAttributeChanges(model)
	if currentModelConnection then
		currentModelConnection:Disconnect()
	end

	currentModelConnection = model:GetAttributeChangedSignal("Rarity"):Connect(function()
		Adjust(model)
	end)

	model:GetAttributeChangedSignal("Stack"):Connect(function()
		Adjust(model)
	end)

	model:GetAttributeChangedSignal("StackWeight"):Connect(function()
		Adjust(model)
	end)

	model:GetAttributeChangedSignal("Name"):Connect(function()
		Adjust(model)
	end)
end

local function OnRenderStep(dt)
	local target = Mouse.Target

	if target and target.Parent and CollectionService:HasTag(target.Parent, "Item") then
		if target ~= lastTarget then
			SetAdornee(target)
			Adjust(target.Parent)
			TrackAttributeChanges(target.Parent) -- Add this
			lastTarget = target
		end
	else
		if lastTarget then
			SetAdornee(nil)
			uiInfo.Rarity.Text = ""
			uiInfo.Weight.Text = ""
			HoverInfo.Title.Text = ""
			Highlight.FillColor = Color3.new(0, 0, 0)
			lastTarget = nil
			if currentModelConnection then
				currentModelConnection:Disconnect()
				currentModelConnection = nil
			end
		end
	end
end

local function OnInput(Input: InputObject, GPE: boolean)
	if GPE then return end

	local target = Mouse.Target
	if not target then return end

	local model = target:FindFirstAncestorOfClass("Model")
	if not model then return end

	if CollectionService:HasTag(model, "Item") then
		local action = Keybinds[Input.KeyCode]
		local uuid = model:GetAttribute("UUID")
		if action and uuid then
			action(uuid, Input.KeyCode)
		end
	end
end

UIS.InputBegan:Connect(OnInput)
RunService.RenderStepped:Connect(OnRenderStep)

It looks like the StackWeight attribute of the stack is mistakedly being set to the ‘prognosis’ instead somewhere. Do you have the code that sets this?

“I get i’m probably doing horrible practices” Probably, if you’re here with this now.

Should be somewhere I provided.

Or how about this - maybe actually contribute and help out someone who needs it?

I’d have to have it in front of me. Program it out, what do you need, what steps do you take.

this is a bump since i’ve yet to receive any help.

i might’ve been sleep deprived or something, because looking back, i have now (i think) understood what you meant.

if i am reading this correctly - you have advised me to write out what i want from my system and how it should work. sorry for my passive(?) aggressiveness before.

The way the code should work is:

  • Player picks up the entire stack or however much they can pick up without reaching the weight limit. (Q - full (or however much possible) stack, F - one at a time)
  • It goes into the inventory and values update (total weight, the stack of the item in the inventory’s value and weight counters). If it doesn’t exist in the inventory already - clone a template, and add the values accordingly.
  • If the item is equippable (tagged with “Item_Equippable” or has the type “Tool”) - show the equip button.
  • Pressing the drop button drops one item from the stack, spawning it in the world, or placing it in the nearest dropped item stack of the same type (name, so if its “rock”, it’d increase “rock’s” stack). (I’ll update this to support TextBoxes so players could enter their custom amounts)
  • Obviously, run a check on the server to see if they can do this.

I really hope I understood you right. I’ve just been frustrated as I’ve done little to no progress. Sorry for my rude behavior.

I’ll try rewriting my system for the third time when I can, first I think I should focus on other things in my game before I jump back in. Help is still wanted, and accepted.

It doesn’t seem to be… I think this is the problem though
there’s no usage of SetAttribute in any of these scripts, meaning it’s probably somewhere else, try using the Find All option to search for SetAttribute("StackWeight", this should tell you where this is done. (And yes, the missing bracket is intentional)

The rock text shows it weighs 13.5… Is that the problem?
What is Prognosis 13.5 Rations? Is there a mix-up variable going to that text?

There could be, now that I think about it. I’ll fix that (when I can) and report back.

Right, so I do know it is a mix-up somewhere, but I can’t pinpoint where exactly it is.