458 lines
12 KiB
Lua
458 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
|
|
local displayName = id ~= "" and (self.state.names and self.state.names[id] or id) or ""
|
|
|
|
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 = displayName,
|
|
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)
|