Files
block-game/StarterGui/Hotbar/LocalScript.client.lua
2026-01-08 23:52:32 +02:00

457 lines
12 KiB
Lua

--!native
--!optimize 2
if not game:IsLoaded() then
game.Loaded:Wait()
end
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UIS = game:GetService("UserInputService")
local TextChatService = game:GetService("TextChatService")
local objects = ReplicatedStorage:WaitForChild("Objects", 9e9)
objects:WaitForChild("MLLoaded", 9e9)
objects:WaitForChild("CSMLLoaded", 9e9)
local Roact = require(ReplicatedStorage.Packages.roact)
local PM = require(ReplicatedStorage.Shared.PlacementManager)
local BlockManager = require(ReplicatedStorage.Shared.ChunkManager.BlockManager)
local PlacementState = require(ReplicatedStorage.Shared.PlacementState)
local Util = require(ReplicatedStorage.Shared.Util)
local ClientState = require(ReplicatedStorage.Shared.ClientState)
local HOTBAR_SIZE = 10
local keyToSlot = {
[Enum.KeyCode.One] = 1,
[Enum.KeyCode.Two] = 2,
[Enum.KeyCode.Three] = 3,
[Enum.KeyCode.Four] = 4,
[Enum.KeyCode.Five] = 5,
[Enum.KeyCode.Six] = 6,
[Enum.KeyCode.Seven] = 7,
[Enum.KeyCode.Eight] = 8,
[Enum.KeyCode.Nine] = 9,
[Enum.KeyCode.Zero] = 10,
}
local colors = {
base = Color3.fromRGB(30, 30, 46),
slot = Color3.fromRGB(17, 17, 27),
stroke = Color3.fromRGB(88, 91, 112),
selectedStroke = Color3.fromRGB(137, 180, 250),
text = Color3.fromRGB(205, 214, 244),
subtext = Color3.fromRGB(166, 173, 200),
}
local function isTextInputFocused(): boolean
if UIS:GetFocusedTextBox() then
return true
end
local config = TextChatService:FindFirstChildOfClass("ChatInputBarConfiguration")
return config ~= nil and config.IsFocused
end
local function resolveSelectedSlot(slots, desired)
if desired and desired >= 1 and desired <= HOTBAR_SIZE and slots[desired] ~= "" then
return desired
end
for i = 1, HOTBAR_SIZE do
if slots[i] and slots[i] ~= "" then
return i
end
end
return desired or 1
end
local function buildHotbarFromState()
local slots = table.create(HOTBAR_SIZE)
local names = {}
for i = 1, HOTBAR_SIZE do
local info = ClientState:GetSlotInfo(i)
if info then
slots[i] = tostring(info.id)
names[slots[i]] = info.name or slots[i]
else
slots[i] = ""
end
end
local selected = resolveSelectedSlot(slots, ClientState:GetSelectedSlot())
return slots, names, selected
end
local function ensurePreviewRig(part: Instance)
for _, descendant in ipairs(part:GetDescendants()) do
if descendant:IsA("BasePart") then
descendant.Anchored = true
descendant.CanCollide = false
end
end
if part:IsA("BasePart") then
part.Anchored = true
part.CanCollide = false
end
end
local function updateViewport(viewport: ViewportFrame, blockId: string)
viewport:ClearAllChildren()
if blockId == "" then
return
end
local camera = Instance.new("Camera")
camera.Parent = viewport
viewport.CurrentCamera = camera
local world = Instance.new("WorldModel")
world.Parent = viewport
local resolvedId = tonumber(blockId) or blockId
local preview = BlockManager:GetBlock(resolvedId)
preview.Parent = world
ensurePreviewRig(preview)
local cf, size
if preview:IsA("BasePart") then
cf = preview.CFrame
size = preview.Size
else
cf, size = preview:GetBoundingBox()
end
local maxSize = math.max(size.X, size.Y, size.Z)
local distance = maxSize * 1.8
local target = cf.Position
camera.CFrame = CFrame.new(target + Vector3.new(distance, distance, distance), target)
preview:PivotTo(CFrame.new())
end
local Hotbar = Roact.Component:extend("Hotbar")
function Hotbar:init()
local slots, names, selected = buildHotbarFromState()
self.state = {
slots = slots,
names = names,
selected = selected,
}
self._syncFromClientState = function()
local nextSlots, nextNames, nextSelected = buildHotbarFromState()
nextSelected = resolveSelectedSlot(nextSlots, nextSelected or self.state.selected)
self:setState({
slots = nextSlots,
names = nextNames,
selected = nextSelected,
})
local id = nextSlots[nextSelected] or ""
local name = ""
if id ~= "" then
name = nextNames[id] or id
end
PlacementState:SetSelected(id, 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)
end
self._handleInput = function(input: InputObject, gameProcessedEvent: boolean)
if isTextInputFocused() then
return
end
local slot = keyToSlot[input.KeyCode]
if slot then
if gameProcessedEvent then
return
end
self._setSelected(slot)
return
end
if input.UserInputType == Enum.UserInputType.MouseButton1 then
Util.StudioLog("[INPUT][CLIENT]", "MouseButton1", "processed", gameProcessedEvent)
-- Allow click even if gameProcessedEvent (UI can set this), but only if we're actually pointing at a block
if not PM:GetBlockAtMouse() then
return
end
local mouseBlock = PM:GetBlockAtMouse()
if not mouseBlock then
return
end
PM:BreakBlock(
mouseBlock.chunk.X,
mouseBlock.chunk.Y,
mouseBlock.chunk.Z,
mouseBlock.block.X,
mouseBlock.block.Y,
mouseBlock.block.Z
)
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()
if not mouseBlock then
return
end
local id = PlacementState:GetSelected()
if not id or id == "" then
Util.StudioWarn("[PLACE][CLIENT][REJECT]", "no selected id")
return
end
Util.StudioLog(
"[PLACE][CLIENT][SEND][CLICK]",
"chunk",
mouseBlock.chunk,
"block",
mouseBlock.block,
"id",
id
)
PM:PlaceBlock(
mouseBlock.chunk.X,
mouseBlock.chunk.Y,
mouseBlock.chunk.Z,
mouseBlock.block.X,
mouseBlock.block.Y,
mouseBlock.block.Z,
id
)
end
end
self._handleScroll = function(input: InputObject, gameProcessedEvent: boolean)
if gameProcessedEvent or isTextInputFocused() then
return
end
if input.UserInputType ~= Enum.UserInputType.MouseWheel then
return
end
local direction = input.Position.Z
if direction == 0 then
return
end
local delta = direction > 0 and -1 or 1
local nextSlot = math.clamp(self.state.selected + delta, 1, HOTBAR_SIZE)
if nextSlot ~= self.state.selected then
self._setSelected(nextSlot)
end
end
self._viewportRefs = {}
self._viewportState = {}
end
function Hotbar:didMount()
self._connections = {
ClientState.Changed:Connect(self._syncFromClientState),
UIS.InputBegan:Connect(self._handleInput),
UIS.InputChanged:Connect(self._handleScroll),
}
self._syncFromClientState()
self:_refreshViewports()
-- initialize selection broadcast
local id = self.state.slots and self.state.slots[self.state.selected] or ""
local name = ""
if id ~= "" and self.state.names then
name = self.state.names[id] or id
end
PlacementState:SetSelected(id, name)
end
function Hotbar:willUnmount()
for _, conn in ipairs(self._connections or {}) do
conn:Disconnect()
end
self._connections = nil
end
function Hotbar:didUpdate(prevProps, prevState)
if prevState.slots ~= self.state.slots then
self:_refreshViewports()
end
end
function Hotbar:_refreshViewports()
for i = 1, HOTBAR_SIZE do
local viewport = self._viewportRefs[i]
if viewport then
local id = self.state.slots[i] or ""
if self._viewportState[i] ~= id then
self._viewportState[i] = id
updateViewport(viewport, id)
end
end
end
end
function Hotbar:render()
local slotElements = {}
local selectedId = self.state.slots[self.state.selected] or ""
local selectedName = ""
if selectedId ~= "" and self.state.names then
selectedName = self.state.names[selectedId] or selectedId
end
for i = 1, HOTBAR_SIZE do
local id = self.state.slots[i] or ""
local isSelected = i == self.state.selected
slotElements[`Slot{i-1}`] = Roact.createElement("TextButton", {
Size = UDim2.fromOffset(50, 50),
BackgroundColor3 = colors.slot,
BorderSizePixel = 0,
AutoButtonColor = false,
ClipsDescendants = true,
Text = "",
LayoutOrder = i,
[Roact.Event.Activated] = function()
self._setSelected(i)
end,
}, {
Corner = Roact.createElement("UICorner", {
CornerRadius = UDim.new(0, 13),
}),
Stroke = Roact.createElement("UIStroke", {
Color = isSelected and colors.selectedStroke or colors.stroke,
Thickness = isSelected and 2 or 1,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border
}),
Preview = Roact.createElement("ViewportFrame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
BorderSizePixel = 0,
[Roact.Ref] = function(r)
self._viewportRefs[i] = r
end,
}),
IndexLabel = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Position = UDim2.fromOffset(4, 2),
Size = UDim2.fromOffset(18, 14),
Font = Enum.Font.Gotham,
Text = i == 10 and "0" or tostring(i),
TextColor3 = colors.subtext,
TextSize = 12,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
}),
IdLabel = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Position = UDim2.fromOffset(4, 26),
Size = UDim2.new(1, -8, 0, 18),
Font = Enum.Font.GothamBold,
Text = id,
TextColor3 = colors.text,
TextSize = 15,
TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Bottom,
}),
})
end
local hotbarFrame = Roact.createElement("Frame", {
AnchorPoint = Vector2.new(0.5, 1),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = colors.base,
BorderSizePixel = 0,
Position = UDim2.new(0.5, 0, 1, -20),
Size = UDim2.fromOffset(0, 58),
}, {
Corner = Roact.createElement("UICorner", {
CornerRadius = UDim.new(0, 16),
}),
Stroke = Roact.createElement("UIStroke", {
Color = colors.selectedStroke,
Thickness = 2,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border
}),
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 5),
PaddingRight = UDim.new(0, 5),
PaddingTop = UDim.new(0, 5),
PaddingBottom = UDim.new(0, 5),
}),
Slots = Roact.createElement("Frame", {
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 1, 0),
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 5),
}),
Slots = Roact.createFragment(slotElements),
}),
})
local selectedNameFrame = Roact.createElement("Frame", {
AnchorPoint = Vector2.new(0.5, 1),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = colors.base,
BorderSizePixel = 0,
Position = UDim2.new(0.5, 0, 1, -80-10),
Size = UDim2.fromOffset(0, 25),
}, {
Corner = Roact.createElement("UICorner", {
CornerRadius = UDim.new(0, 8),
}),
Stroke = Roact.createElement("UIStroke", {
Color = colors.selectedStroke,
Thickness = 2,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border
}),
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 18),
PaddingRight = UDim.new(0, 18),
PaddingTop = UDim.new(0, 2),
PaddingBottom = UDim.new(0, 2),
}),
Label = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
Font = Enum.Font.JosefinSans,
RichText = true,
Text = selectedName ~= "" and selectedName or " ",
TextColor3 = colors.text,
TextSize = 19,
TextWrapped = true,
AutomaticSize = Enum.AutomaticSize.X,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center
}),
})
return Roact.createFragment({
Hotbar = hotbarFrame,
SelectedName = selectedNameFrame,
})
end
local handle = Roact.mount(Roact.createElement(Hotbar), script.Parent, "RoactHotbar")
script.AncestryChanged:Connect(function(_, parent)
if parent == nil then
Roact.unmount(handle)
end
end)