A few things going on here, and the first one explains why leaderstats never update.
The server can’t see your text label change.
Your LocalScript sets EquippedKaguneTextLabel.Text, and then the server does this:
game.ReplicatedStorage.KaguneValue1.OnServerEvent:Connect(function(player, KaguneValue1, Kagune1TextLabel, EquippedKaguneTextLabel)
EquippedKaguneTextLabel.Changed:Connect(function()
GUI property changes made by a LocalScript are not replicated back to the server. The server has its own copy of that TextLabel — the one it handed out from StarterGui — and nothing on the server ever writes to it, so that .Changed never fires. Every bit of your leaderstats and ProfileStore code sits inside that callback, so it is dead code. That is exactly why you couldn’t get leaderstats to update.
The bigger one: you’re connecting OnServerEvent inside a per-player function.
InitializeWeapon is called from your PlayerAdded handler, and it connects OnServerEvent inside itself. So the second player who joins adds a second handler to the same RemoteEvent, the third adds a third, and one button click now runs all of them.
They aren’t equivalent handlers, either. profile, character and weapon are upvalues captured from whichever player’s InitializeWeapon call created that connection, while player is the inner function’s own parameter (which shadows the outer one). So when player A clicks Equip, the handler created for player B also runs — and writes to B’s profile. That is your “breaks completely after that”, and it gets worse with every extra player in the server.
Connect each remote exactly once, at the top level of the script, and look up per-player state inside the handler:
local profiles = {} -- [player] = profile
EquipRemote.OnServerEvent:Connect(function(player, slot)
if typeof(slot) ~= "number" or slot % 1 ~= 0 or slot < 1 or slot > 3 then return end
local profile = profiles[player]
if not profile then return end
-- swap, write leaderstats, save
end)
Don’t send GUI objects through remotes at all. Send the slot number, like above. Right now the server takes the weapon name out of the client’s own text label, which means anyone can equip anything by editing a label locally — the server is trusting the exact thing the exploiter controls.
Two smaller bugs:
EquipButton1.MouseButton1Click:Connect(function(player) — MouseButton1Click passes no arguments. That player is nil, and it shadows the real player you defined at the top.
repeat EquippedKaguneTextLabel.Text = "Equipped Kagune: Eto" until EquippedKaguneTextLabel.Text == "Equipped Kagune: Eto"
This is an assignment written as a loop. It either exits after one pass or spins forever with no yield in it and freezes that script. Just assign it.
Also profile.Data.Weapon = tostring(weapon) on a StringValue gives you the instance’s Name, not its Value. It only looks right because you named the instance the same as its value — use weapon.Value.
The shape I’d aim for
- Server owns the state:
profile.Data.Slots = {"None", "None", "None"} plus profile.Data.Equipped.
- Client sends intent only — “equip slot 2”, “store my current weapon in slot 3”.
- Server validates, swaps, writes leaderstats and the profile, then fires one remote back with the new state.
- Client renders all four labels from whatever it just received, and never decides anything itself.
That will also collapse the file a long way. The repeated per-weapon if chains all become one table lookup keyed by weapon name, and you stop having two copies of the truth that can disagree.