diff --git a/ReplicatedStorage/Shared/ClientState.lua b/ReplicatedStorage/Shared/ClientState.lua index bfc8a0b..2051fcc 100644 --- a/ReplicatedStorage/Shared/ClientState.lua +++ b/ReplicatedStorage/Shared/ClientState.lua @@ -117,10 +117,12 @@ function ClientState:SetSelectedSlot(slot: number) return end local hotbar = replicaForPlayer.Data.hotbar - if not hotbar or not hotbar[slot] then + if not hotbar then return end - replicaForPlayer:FireServer("SelectHotbarSlot", slot) + if slot and slot >= 1 and slot <= HOTBAR_SIZE then + replicaForPlayer:FireServer("SelectHotbarSlot", slot) + end end ClientState.Changed = changed.Event diff --git a/ReplicatedStorage/Shared/PlacementManager.lua b/ReplicatedStorage/Shared/PlacementManager.lua index 956e0ff..fbd7600 100644 --- a/ReplicatedStorage/Shared/PlacementManager.lua +++ b/ReplicatedStorage/Shared/PlacementManager.lua @@ -79,7 +79,7 @@ local function findBlockRoot(inst: Instance, chunkFolder: Instance): Instance? if current:IsA("BasePart") then return current end - current = current.Parent + current = current.Parent end return nil end @@ -265,13 +265,15 @@ local function ensureChunkFolder(): Instance? end -- Gets the block and normalid of the block (and surface) the player is looking at -function PlacementManager:Raycast() +function PlacementManager:Raycast(skipSelection: boolean?) if not Mouse then Mouse = game:GetService("Players").LocalPlayer:GetMouse() end local chunkFolder = ensureChunkFolder() if not chunkFolder then - clearSelection("chunk folder missing") + if not skipSelection then + clearSelection("chunk folder missing") + end script.RaycastResult.Value = nil return end @@ -285,7 +287,9 @@ function PlacementManager:Raycast() local ray = Mouse.UnitRay local result = workspace:Raycast(ray.Origin, ray.Direction * MAX_REACH, raycastParams) if not result then - clearSelection("raycast miss") + if not skipSelection then + clearSelection("raycast miss") + end script.RaycastResult.Value = nil debugPlacementLog("[PLACE][CLIENT][RAYCAST]", "miss") return @@ -293,7 +297,9 @@ function PlacementManager:Raycast() local objLookingAt = result.Instance if not objLookingAt then - clearSelection("raycast nil instance") + if not skipSelection then + clearSelection("raycast nil instance") + end script.RaycastResult.Value = nil debugPlacementWarn("[PLACE][CLIENT][RAYCAST]", "nil instance in result") return @@ -308,7 +314,9 @@ function PlacementManager:Raycast() "parent", objLookingAt.Parent and objLookingAt.Parent:GetFullName() or "nil" ) - clearSelection("target not in chunk folder") + if not skipSelection then + clearSelection("target not in chunk folder") + end script.RaycastResult.Value = nil return end @@ -318,7 +326,9 @@ function PlacementManager:Raycast() "chunk flagged ns", hitChunkFolder:GetFullName() ) - clearSelection("target chunk marked ns") + if not skipSelection then + clearSelection("target chunk marked ns") + end script.RaycastResult.Value = nil return end @@ -327,7 +337,9 @@ function PlacementManager:Raycast() local blockRoot = findBlockRoot(objLookingAt, chunkFolder) or objLookingAt local chunkName, blockName = findChunkAndBlock(blockRoot) if not chunkName or not blockName then - clearSelection("failed to resolve chunk/block") + if not skipSelection then + clearSelection("failed to resolve chunk/block") + end script.RaycastResult.Value = nil return end @@ -338,7 +350,9 @@ function PlacementManager:Raycast() return Util.BlockPosStringToCoords(blockName) end) if not okChunk or not okBlock then - clearSelection("failed to parse chunk/block names") + if not skipSelection then + clearSelection("failed to parse chunk/block names") + end script.RaycastResult.Value = nil return end @@ -347,7 +361,9 @@ function PlacementManager:Raycast() -- block is being optimistically broken, do not highlight it if getPendingBreak(chunkKey, blockKey) then - clearSelection("block pending break") + if not skipSelection then + clearSelection("block pending break") + end script.RaycastResult.Value = nil return end @@ -356,22 +372,28 @@ function PlacementManager:Raycast() local chunk = ChunkManager:GetChunk(chunkCoords.X, chunkCoords.Y, chunkCoords.Z) local blockData = chunk and chunk:GetBlockAt(blockCoords.X, blockCoords.Y, blockCoords.Z) if not blockData or blockData == 0 or blockData.id == 0 then - clearSelection("block missing/air") + if not skipSelection then + clearSelection("block missing/air") + end script.RaycastResult.Value = nil return end local blockInstance = resolveBlockInstance(chunkFolder, chunkName, blockName) or blockRoot if not blockInstance then - clearSelection("missing block instance") + if not skipSelection then + clearSelection("missing block instance") + end script.RaycastResult.Value = nil return end lastRaycastFailure = nil - if lastSelectedChunkKey ~= chunkKey or lastSelectedBlockKey ~= blockKey then - setSelection(blockInstance, PlacementManager.ChunkFolder) - lastSelectedChunkKey = chunkKey - lastSelectedBlockKey = blockKey + if not skipSelection then + if lastSelectedChunkKey ~= chunkKey or lastSelectedBlockKey ~= blockKey then + setSelection(blockInstance, PlacementManager.ChunkFolder) + lastSelectedChunkKey = chunkKey + lastSelectedBlockKey = blockKey + end end script.RaycastResult.Value = objLookingAt lastNormalId = vectorToNormalId(result.Normal) @@ -400,6 +422,10 @@ local tickRemote = game:GetService("ReplicatedStorage").Tick -- FIRES REMOTE function PlacementManager:PlaceBlock(cx, cy, cz, x, y, z, blockId: string) debugPlacementLog("[PLACE][CLIENT][PLACE_CALL]", "chunk", cx, cy, cz, "block", x, y, z, "blockId", blockId) + if blockId == "hand" then + debugPlacementWarn("[PLACE][CLIENT][REJECT]", "hand cannot place") + return + end if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then debugPlacementWarn("[PLACE][CLIENT][REJECT]", "chunk type", cx, cy, cz, x, y, z, blockId) return @@ -551,14 +577,16 @@ local function applyBreakBlockLocal(cx, cy, cz, x, y, z) chunk:RemoveBlock(x, y, z) end -function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector3} +function PlacementManager:GetBlockAtMouse(skipSelection: boolean?): nil | {chunk:Vector3, block: Vector3} pcall(function() - PlacementManager:Raycast() + PlacementManager:Raycast(skipSelection) end) local selectedPart = PlacementManager:RaycastGetResult() --print(selectedPart and selectedPart:GetFullName() or nil) if selectedPart == nil then - clearSelection() + if not skipSelection then + clearSelection() + end script.RaycastResult.Value = nil debugPlacementLog("[PLACE][CLIENT][TARGET]", "no selectedPart after raycast", lastRaycastFailure) return nil @@ -607,8 +635,8 @@ function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector end -function PlacementManager:GetTargetAtMouse(): nil | {chunk:Vector3, block: Vector3, normal: Enum.NormalId} - local hit = PlacementManager:GetBlockAtMouse() +function PlacementManager:GetTargetAtMouse(skipSelection: boolean?): nil | {chunk:Vector3, block: Vector3, normal: Enum.NormalId} + local hit = PlacementManager:GetBlockAtMouse(skipSelection) if not hit then return nil end @@ -621,8 +649,8 @@ function PlacementManager:GetTargetAtMouse(): nil | {chunk:Vector3, block: Vecto } end -function PlacementManager:GetPlacementAtMouse(): nil | {chunk:Vector3, block: Vector3} - local hit = PlacementManager:GetTargetAtMouse() +function PlacementManager:GetPlacementAtMouse(skipSelection: boolean?): nil | {chunk:Vector3, block: Vector3} + local hit = PlacementManager:GetTargetAtMouse(skipSelection) if not hit then return nil end @@ -647,8 +675,8 @@ function PlacementManager:GetPlacementAtMouse(): nil | {chunk:Vector3, block: Ve } end -function PlacementManager:DebugGetPlacementOrWarn() - local placement = PlacementManager:GetPlacementAtMouse() +function PlacementManager:DebugGetPlacementOrWarn(skipSelection: boolean?) + local placement = PlacementManager:GetPlacementAtMouse(skipSelection) if not placement then debugPlacementWarn("[PLACE][CLIENT][REJECT]", "no placement target under mouse", lastRaycastFailure) end diff --git a/ServerScriptService/Actor/ClientState.lua b/ServerScriptService/Actor/ClientState.lua index 110d39d..94e1888 100644 --- a/ServerScriptService/Actor/ClientState.lua +++ b/ServerScriptService/Actor/ClientState.lua @@ -81,9 +81,6 @@ local function sanitizeSelection(hotbar, selectedSlot) if selectedSlot < 1 or selectedSlot > HOTBAR_SIZE then return (#hotbar > 0) and 1 or 0 end - if not hotbar[selectedSlot] then - return (#hotbar > 0) and 1 or 0 - end return selectedSlot end @@ -138,7 +135,7 @@ local function handleReplicaEvents(player: Player, replica) if not hotbar then return end - if slot and slot >= 1 and slot <= HOTBAR_SIZE and hotbar[slot] then + if slot and slot >= 1 and slot <= HOTBAR_SIZE then replica:Set({"selectedSlot"}, slot) end end diff --git a/StarterGui/Hotbar/LocalScript.client.lua b/StarterGui/Hotbar/LocalScript.client.lua index 5eab6f5..e4b160a 100644 --- a/StarterGui/Hotbar/LocalScript.client.lua +++ b/StarterGui/Hotbar/LocalScript.client.lua @@ -53,7 +53,7 @@ local function isTextInputFocused(): boolean end local function resolveSelectedSlot(slots, desired) - if desired and desired >= 1 and desired <= HOTBAR_SIZE and slots[desired] ~= "" then + if desired and desired >= 1 and desired <= HOTBAR_SIZE then return desired end for i = 1, HOTBAR_SIZE do @@ -146,30 +146,31 @@ function Hotbar:init() names = nextNames, selected = nextSelected, }) - local id = nextSlots[nextSelected] or "" + local rawId = nextSlots[nextSelected] or "" + local effectiveId = rawId ~= "" and rawId or "hand" local name = "" - if id ~= "" then - name = nextNames[id] or id + if rawId ~= "" then + name = nextNames[rawId] or rawId end - PlacementState:SetSelected(id, name) + PlacementState:SetSelected(effectiveId, name) end self._setSelected = function(slot: number) if slot < 1 or slot > HOTBAR_SIZE then return end - local info = ClientState:GetSlotInfo(slot) - if not info then - return - end ClientState:SetSelectedSlot(slot) self:setState({ selected = slot, }) - local id = tostring(info.id) - local name = info.name or id - Util.StudioLog("[PLACE][CLIENT][SELECT]", "slot", slot, "id", id, "name", name) - PlacementState:SetSelected(id, name) + local rawId = self.state.slots[slot] or "" + local effectiveId = rawId ~= "" and rawId or "hand" + local name = "" + if rawId ~= "" then + name = self.state.names[rawId] or rawId + end + Util.StudioLog("[PLACE][CLIENT][SELECT]", "slot", slot, "id", effectiveId, "name", name) + PlacementState:SetSelected(effectiveId, name) end self._handleInput = function(input: InputObject, gameProcessedEvent: boolean) @@ -207,7 +208,7 @@ function Hotbar:init() elseif input.UserInputType == Enum.UserInputType.MouseButton2 then Util.StudioLog("[INPUT][CLIENT]", "MouseButton2", "processed", gameProcessedEvent) -- Allow click even if gameProcessedEvent (UI can set this), but only if we're actually pointing at a block - local mouseBlock = PM:DebugGetPlacementOrWarn() + local mouseBlock = PM:DebugGetPlacementOrWarn(true) -- skip selection outline on right click if not mouseBlock then return end @@ -249,7 +250,7 @@ function Hotbar:init() return end local delta = direction > 0 and -1 or 1 - local nextSlot = math.clamp(self.state.selected + delta, 1, HOTBAR_SIZE) + local nextSlot = ((self.state.selected - 1 + delta) % HOTBAR_SIZE) + 1 if nextSlot ~= self.state.selected then self._setSelected(nextSlot) end @@ -268,12 +269,13 @@ function Hotbar:didMount() self._syncFromClientState() self:_refreshViewports() -- initialize selection broadcast - local id = self.state.slots and self.state.slots[self.state.selected] or "" + local rawId = self.state.slots and self.state.slots[self.state.selected] or "" + local effectiveId = rawId ~= "" and rawId or "hand" local name = "" - if id ~= "" and self.state.names then - name = self.state.names[id] or id + if rawId ~= "" and self.state.names then + name = self.state.names[rawId] or rawId end - PlacementState:SetSelected(id, name) + PlacementState:SetSelected(effectiveId, name) end function Hotbar:willUnmount() @@ -345,7 +347,7 @@ function Hotbar:render() }), IndexLabel = Roact.createElement("TextLabel", { BackgroundTransparency = 1, - Position = UDim2.fromOffset(4, 2), + Position = UDim2.fromOffset(8, 4), Size = UDim2.fromOffset(18, 14), Font = Enum.Font.Gotham, Text = i == 10 and "0" or tostring(i), @@ -412,6 +414,7 @@ function Hotbar:render() BorderSizePixel = 0, Position = UDim2.new(0.5, 0, 1, -80-10), Size = UDim2.fromOffset(0, 25), + Visible = selectedName ~= "", }, { Corner = Roact.createElement("UICorner", { CornerRadius = UDim.new(0, 8),