Files
Samuel LisonandClaude Opus 4.8 3349738779 fix: address adversarial-review findings across engine + plugin
Engine: quest overrides with objective*/reward* fields on nested code quests
now WARN at boot (they merge nothing — QuestData prefers nested tables);
EngineConfig + ConfigAdmin reject non-finite numbers (inf/nan).

Plugin: Config group panels auto-size (fixed-height math clipped the last row
of big groups — Theme group lost its Bold font row); Stats panels get the gap
math right; the Engine Config explainer page now REBUILDS the window when the
engine appears (the old hint was impossible — pages were assembled once at
plugin load); create() refuses ids that already have an override (mirror
guard); failed Create/Override reasons render under the create row; rejected
config edits report in the footer; explicit navigation cancels a pending
debounced search jump; the mount-time page restore no longer clobbers the
saved last-page setting during an engine-less session; undo/redo refresh
skips while typing in one of the plugin's own text boxes; search results past
the 50-cap no longer render empty category headers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 12:39:56 +10:00

310 lines
8.9 KiB
Luau

--!nonstrict
--[[
StatAdminUi builds the dock-widget content. A thin layer over StatAdmin (the logic):
it never touches the config instance itself. Edits are routed through `applyEdit` /
`applyReset` callbacks the plugin main supplies (they wrap the StatAdmin call in a
ChangeHistoryService recording, then refresh). Styling follows docs/design-language.md.
]]
local Theme = require(script.Parent.Theme)
local StatAdminUi = {}
local COL_BG = Theme.COLOR.BG
local COL_PANEL = Theme.COLOR.PANEL
local COL_TEXT = Theme.COLOR.TEXT
local COL_DIM = Theme.COLOR.DIM
local COL_ACCENT = Theme.COLOR.ACCENT -- marks an overridden field
local COL_FIELD = Theme.COLOR.FIELD
local FONT = Theme.FONT
local ROW_H = Theme.ROW_H
local FOOTER_H = 36 -- the Preview / Clear control bar at the bottom
local make = Theme.make
local corner = Theme.corner
-- A single field row: [reset] label .... control. `applyEdit/applyReset` take (stat, attr, …).
local function buildRow(stat: string, spec: any, eff: any, applyEdit, applyReset): Instance
local row = make("Frame", {
Size = UDim2.new(1, 0, 0, ROW_H),
BackgroundTransparency = 1,
})
-- reset dot/button — accent when overridden, dim otherwise; click removes the attribute.
local reset = make("TextButton", {
Size = UDim2.fromOffset(18, 18),
Position = UDim2.new(0, 0, 0.5, -9),
BackgroundColor3 = COL_FIELD,
AutoButtonColor = true,
Text = eff.hasOverride and "●" or "○",
TextColor3 = eff.hasOverride and COL_ACCENT or COL_DIM,
TextSize = 12,
Font = FONT,
BorderSizePixel = 0,
}, { corner(4) })
reset.Parent = row
reset.MouseButton1Click:Connect(function()
applyReset(stat, spec.attr)
end)
make("TextLabel", {
Size = UDim2.new(0.42, -28, 1, 0),
Position = UDim2.fromOffset(26, 0),
BackgroundTransparency = 1,
Text = spec.label,
TextColor3 = eff.hasOverride and COL_TEXT or COL_DIM,
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = 13,
Font = FONT,
}).Parent =
row
local controlPos = UDim2.new(0.42, 4, 0, 2)
local controlSize = UDim2.new(0.58, -4, 0, ROW_H - 4)
if spec.kind == "boolean" or spec.kind == "enum" then
-- a click-to-cycle button (boolean: true/false; enum: through the choices)
local control = make("TextButton", {
Size = controlSize,
Position = controlPos,
BackgroundColor3 = COL_FIELD,
AutoButtonColor = true,
Text = tostring(eff.value),
TextColor3 = COL_TEXT,
TextSize = 13,
Font = FONT,
BorderSizePixel = 0,
}, { corner(4) })
control.Parent = row
control.MouseButton1Click:Connect(function()
local nextValue: any
if spec.kind == "boolean" then
nextValue = not (eff.value == true)
else
local choices = spec.choices
local i = table.find(choices, tostring(eff.value)) or 0
nextValue = choices[(i % #choices) + 1]
end
applyEdit(stat, spec.attr, nextValue, eff.default)
end)
else
-- number / string: a text box committed on focus loss
local box = make("TextBox", {
Size = controlSize,
Position = controlPos,
BackgroundColor3 = COL_FIELD,
Text = tostring(eff.value),
PlaceholderText = tostring(eff.default),
TextColor3 = COL_TEXT,
TextSize = 13,
Font = FONT,
ClearTextOnFocus = false,
BorderSizePixel = 0,
}, { corner(4), make("UIPadding", { PaddingLeft = UDim.new(0, 6), PaddingRight = UDim.new(0, 6) }) })
box.Parent = row
box.FocusLost:Connect(function()
applyEdit(stat, spec.attr, box.Text, eff.default)
end)
end
return row
end
-- Mount the widget UI into `container`. Returns { refresh } — call refresh() after any edit
-- or when the engine sync changes. `StatAdmin` is the logic module; the two callbacks route
-- writes through the plugin main (ChangeHistory + refresh).
function StatAdminUi.mount(
container: Instance,
StatAdmin: any,
applyEdit,
applyReset,
applyPreview,
applyClear
): { refresh: () -> () }
for _, child in container:GetChildren() do
if not child:IsA("UIBase") then
child:Destroy()
end
end
(container :: any).BackgroundColor3 = COL_BG
local header = make("Frame", {
Size = UDim2.new(1, 0, 0, 32),
BackgroundTransparency = 1,
})
header.Parent = container
make("TextLabel", {
Size = UDim2.new(1, -84, 1, 0),
Position = UDim2.fromOffset(12, 0),
BackgroundTransparency = 1,
Text = "Survival Stats",
TextColor3 = COL_TEXT,
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = 15,
Font = Enum.Font.GothamBold,
}).Parent =
header
local refreshBtn = make("TextButton", {
Size = UDim2.fromOffset(72, 22),
Position = UDim2.new(1, -80, 0.5, -11),
BackgroundColor3 = COL_PANEL,
AutoButtonColor = true,
Text = "Refresh",
TextColor3 = COL_TEXT,
TextSize = 12,
Font = FONT,
BorderSizePixel = 0,
}, { corner(4) })
refreshBtn.Parent = header
local scroll = make("ScrollingFrame", {
Size = UDim2.new(1, 0, 1, -(32 + FOOTER_H)),
Position = UDim2.fromOffset(0, 32),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = 6,
CanvasSize = UDim2.new(),
AutomaticCanvasSize = Enum.AutomaticSize.Y,
}, {
make("UIListLayout", { Padding = UDim.new(0, 4), SortOrder = Enum.SortOrder.LayoutOrder }),
make("UIPadding", {
PaddingLeft = UDim.new(0, 10),
PaddingRight = UDim.new(0, 10),
PaddingTop = UDim.new(0, 6),
PaddingBottom = UDim.new(0, 12),
}),
})
scroll.Parent = container
-- Footer: the edit-mode HUD preview controls. Preview paints resolved icons + sample fills
-- onto the StarterGui HUD (so you see play-time styling without pressing Play); Clear restores.
local footer = make("Frame", {
Size = UDim2.new(1, 0, 0, FOOTER_H),
Position = UDim2.new(0, 0, 1, -FOOTER_H),
BackgroundColor3 = COL_PANEL,
BorderSizePixel = 0,
})
footer.Parent = container
local previewBtn = make("TextButton", {
Size = UDim2.fromOffset(92, 22),
Position = UDim2.new(0, 10, 0.5, -11),
BackgroundColor3 = COL_FIELD,
AutoButtonColor = true,
Text = "Preview HUD",
TextColor3 = COL_TEXT,
TextSize = 12,
Font = FONT,
BorderSizePixel = 0,
}, { corner(4) })
previewBtn.Parent = footer
local clearBtn = make("TextButton", {
Size = UDim2.fromOffset(56, 22),
Position = UDim2.new(0, 108, 0.5, -11),
BackgroundColor3 = COL_FIELD,
AutoButtonColor = true,
Text = "Clear",
TextColor3 = COL_DIM,
TextSize = 12,
Font = FONT,
BorderSizePixel = 0,
}, { corner(4) })
clearBtn.Parent = footer
local status = make("TextLabel", {
Size = UDim2.new(1, -184, 1, 0),
Position = UDim2.fromOffset(174, 0),
BackgroundTransparency = 1,
Text = "",
TextColor3 = COL_DIM,
TextXAlignment = Enum.TextXAlignment.Right,
TextTruncate = Enum.TextTruncate.AtEnd,
TextSize = 11,
Font = FONT,
})
status.Parent = footer
previewBtn.MouseButton1Click:Connect(function()
local res = applyPreview and applyPreview()
if typeof(res) == "table" then
status.Text = if res.ok then `previewing {res.count}` else (res.reason or "no HUD found")
end
end)
clearBtn.MouseButton1Click:Connect(function()
local res = applyClear and applyClear()
if typeof(res) == "table" then
status.Text = if res.ok then "cleared" else (res.reason or "")
end
end)
local function refresh()
for _, child in scroll:GetChildren() do
if not child:IsA("UIBase") then
child:Destroy()
end
end
local roster = StatAdmin.readRoster()
if not roster.ok then
make("TextLabel", {
Size = UDim2.new(1, 0, 0, 80),
BackgroundTransparency = 1,
Text = roster.reason or "Engine not found.",
TextColor3 = COL_DIM,
TextWrapped = true,
TextSize = 13,
Font = FONT,
}).Parent =
scroll
return
end
local order = 0
for _, entry in roster.stats do
order += 1
local section = make("Frame", {
BackgroundColor3 = COL_PANEL,
BorderSizePixel = 0,
LayoutOrder = order,
-- Content height = padding (4+4) + header (22) + rows (ROW_H each) + 2px list
-- gaps between the header and every row — the +2 per row below covers the gaps.
Size = UDim2.new(1, 0, 0, 22 + (ROW_H + 2) * #StatAdmin.TUNABLE + 10),
AutomaticSize = Enum.AutomaticSize.None,
}, {
corner(8),
make("UIPadding", {
PaddingLeft = UDim.new(0, 8),
PaddingRight = UDim.new(0, 8),
PaddingTop = UDim.new(0, 4),
PaddingBottom = UDim.new(0, 4),
}),
make("UIListLayout", { Padding = UDim.new(0, 2), SortOrder = Enum.SortOrder.LayoutOrder }),
})
section.Parent = scroll
make("TextLabel", {
Size = UDim2.new(1, 0, 0, 22),
BackgroundTransparency = 1,
Text = entry.name,
TextColor3 = COL_TEXT,
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = 14,
Font = Enum.Font.GothamBold,
LayoutOrder = 0,
}).Parent =
section
for i, spec in StatAdmin.TUNABLE do
local eff = StatAdmin.readEffective(entry.name, spec.attr, entry.defaults[spec.attr])
local row = buildRow(entry.name, spec, eff, applyEdit, applyReset)
row.LayoutOrder = i
row.Parent = section
end
end
end
refreshBtn.MouseButton1Click:Connect(refresh)
refresh()
return { refresh = refresh }
end
return StatAdminUi