mirror of
https://github.com/TemujinCalidius/SurvivorCore.git
synced 2026-08-14 09:02:29 +00:00
New shell: Theme (shared visual system), Nav (rail + lazy router + badges), OverviewPage, SearchPage (global editable results), SpawnHelpers; init.server restructured around data-driven pages with last-page persistence and undo/redo-aware refresh. Widget: floating 660×580 (min 480×380), new ID SurvivorCoreStudio. ConfigAdmin/-Ui: deltas-only editor pages for all 11 engine Config sections incl. Theme colors (R,G,B + swatch) and fonts. ContentAdmin/-Ui: per-category pages over the merged roster; + Override entries (blank = inherit) tuning code-registered defs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
339 lines
9.8 KiB
Luau
339 lines
9.8 KiB
Luau
--!nonstrict
|
|
--[[
|
|
ConfigAdminUi — the Engine Config editor pages (issue #21). One sidebar page per Config
|
|
section; each page renders its groups as headed panels of typed field rows (the StatAdminUi
|
|
row pattern, generalized): reset dot ●/○, label, and a control per kind — number/string
|
|
TextBox (placeholder = engine default), boolean/enum click-cycle, color3 TextBox "R, G, B"
|
|
with a live swatch, font TextBox with a cycle button over common fonts. All writes route
|
|
through the plugin main's record() wrappers (one undo step each), deltas-only via ConfigAdmin.
|
|
]]
|
|
|
|
local Theme = require(script.Parent.Theme)
|
|
|
|
local ConfigAdminUi = {}
|
|
|
|
local ROW_H = Theme.ROW_H
|
|
local FOOTER_H = 36
|
|
|
|
-- The click-cycle list for font fields (any valid Enum.Font name can still be typed).
|
|
local COMMON_FONTS = {
|
|
"Gotham",
|
|
"GothamMedium",
|
|
"GothamBold",
|
|
"GothamBlack",
|
|
"SourceSans",
|
|
"SourceSansBold",
|
|
"Arial",
|
|
"ArialBold",
|
|
"Merriweather",
|
|
"Bangers",
|
|
"PermanentMarker",
|
|
}
|
|
|
|
-- One field row. `groupName` may be nil (root attributes).
|
|
local function buildRow(ConfigAdmin, sectionId, groupName, field, eff, applyEdit, applyReset): Instance
|
|
local row = Theme.make("Frame", {
|
|
Size = UDim2.new(1, 0, 0, ROW_H),
|
|
BackgroundTransparency = 1,
|
|
})
|
|
|
|
local reset = Theme.button({
|
|
Size = UDim2.fromOffset(18, 18),
|
|
Position = UDim2.new(0, 0, 0.5, -9),
|
|
Text = eff.hasOverride and "●" or "○",
|
|
TextColor3 = eff.hasOverride and Theme.COLOR.ACCENT or Theme.COLOR.DIM,
|
|
})
|
|
reset.Parent = row
|
|
reset.MouseButton1Click:Connect(function()
|
|
applyReset(sectionId, groupName, field.attr)
|
|
end)
|
|
|
|
Theme.label({
|
|
Size = UDim2.new(0.42, -28, 1, 0),
|
|
Position = UDim2.fromOffset(26, 0),
|
|
Text = field.label,
|
|
TextColor3 = eff.hasOverride and Theme.COLOR.TEXT or Theme.COLOR.DIM,
|
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
|
}).Parent =
|
|
row
|
|
|
|
local controlPos = UDim2.new(0.42, 4, 0, 2)
|
|
local controlSize = UDim2.new(0.58, -4, 0, ROW_H - 4)
|
|
|
|
if field.kind == "boolean" or field.kind == "enum" then
|
|
local control = Theme.button({
|
|
Size = controlSize,
|
|
Position = controlPos,
|
|
Text = ConfigAdmin.formatValue(field, eff.value),
|
|
TextSize = 13,
|
|
})
|
|
control.Parent = row
|
|
control.MouseButton1Click:Connect(function()
|
|
local nextValue: any
|
|
if field.kind == "boolean" then
|
|
nextValue = not (eff.value == true)
|
|
else
|
|
local choices = field.choices or {}
|
|
local i = table.find(choices, tostring(eff.value)) or 0
|
|
nextValue = choices[(i % #choices) + 1]
|
|
end
|
|
applyEdit(sectionId, groupName, field, nextValue, eff.default)
|
|
end)
|
|
elseif field.kind == "color3" then
|
|
-- [ R, G, B text box ][ swatch ] — the swatch previews the effective color live.
|
|
local box = Theme.textBox({
|
|
Size = UDim2.new(0.58, -28, 0, ROW_H - 4),
|
|
Position = controlPos,
|
|
Text = ConfigAdmin.formatValue(field, eff.value),
|
|
PlaceholderText = ConfigAdmin.formatValue(field, eff.default),
|
|
})
|
|
box.Parent = row
|
|
local swatch = Theme.make("Frame", {
|
|
Size = UDim2.fromOffset(18, 18),
|
|
Position = UDim2.new(1, -18, 0.5, -9),
|
|
BackgroundColor3 = if typeof(eff.value) == "Color3" then eff.value else Color3.new(0, 0, 0),
|
|
BorderSizePixel = 0,
|
|
}, { Theme.corner(4), Theme.stroke(0.7) })
|
|
swatch.Parent = row
|
|
box.FocusLost:Connect(function()
|
|
applyEdit(sectionId, groupName, field, box.Text, eff.default)
|
|
end)
|
|
elseif field.kind == "font" then
|
|
-- [ font name box ][ ↻ cycle ] — cycles common fonts; any Enum.Font name can be typed.
|
|
local box = Theme.textBox({
|
|
Size = UDim2.new(0.58, -28, 0, ROW_H - 4),
|
|
Position = controlPos,
|
|
Text = ConfigAdmin.formatValue(field, eff.value),
|
|
PlaceholderText = ConfigAdmin.formatValue(field, eff.default),
|
|
})
|
|
box.Parent = row
|
|
box.FocusLost:Connect(function()
|
|
applyEdit(sectionId, groupName, field, box.Text, eff.default)
|
|
end)
|
|
local cycle = Theme.button({
|
|
Size = UDim2.fromOffset(22, ROW_H - 4),
|
|
Position = UDim2.new(1, -22, 0, 2),
|
|
Text = "↻",
|
|
TextColor3 = Theme.COLOR.DIM,
|
|
})
|
|
cycle.Parent = row
|
|
cycle.MouseButton1Click:Connect(function()
|
|
local current = ConfigAdmin.formatValue(field, eff.value)
|
|
local i = table.find(COMMON_FONTS, current) or 0
|
|
applyEdit(sectionId, groupName, field, COMMON_FONTS[(i % #COMMON_FONTS) + 1], eff.default)
|
|
end)
|
|
else
|
|
local box = Theme.textBox({
|
|
Size = controlSize,
|
|
Position = controlPos,
|
|
Text = ConfigAdmin.formatValue(field, eff.value),
|
|
PlaceholderText = ConfigAdmin.formatValue(field, eff.default),
|
|
})
|
|
box.Parent = row
|
|
box.FocusLost:Connect(function()
|
|
applyEdit(sectionId, groupName, field, box.Text, eff.default)
|
|
end)
|
|
end
|
|
|
|
return row
|
|
end
|
|
|
|
-- Mount one section's page. Re-reads the schema on every refresh (self-heals after re-syncs).
|
|
local function mountSection(
|
|
container: Frame,
|
|
ConfigAdmin: any,
|
|
sectionId: string,
|
|
applyEdit,
|
|
applyReset,
|
|
applyResetSection
|
|
): { refresh: () -> () }
|
|
local header = Theme.header(container, "Engine Config")
|
|
local scroll = Theme.scroll(32, FOOTER_H, 6)
|
|
scroll.Parent = container
|
|
|
|
local footer = Theme.make("Frame", {
|
|
Size = UDim2.new(1, 0, 0, FOOTER_H),
|
|
Position = UDim2.new(0, 0, 1, -FOOTER_H),
|
|
BackgroundColor3 = Theme.COLOR.PANEL,
|
|
BorderSizePixel = 0,
|
|
})
|
|
footer.Parent = container
|
|
local resetBtn = Theme.button({
|
|
Size = UDim2.fromOffset(96, 22),
|
|
Position = UDim2.new(0, 10, 0.5, -11),
|
|
Text = "Reset section",
|
|
TextColor3 = Theme.COLOR.DANGER,
|
|
})
|
|
resetBtn.Parent = footer
|
|
local status = Theme.label({
|
|
Size = UDim2.new(1, -120, 1, 0),
|
|
Position = UDim2.fromOffset(112, 0),
|
|
Text = "",
|
|
TextColor3 = Theme.COLOR.DIM,
|
|
TextXAlignment = Enum.TextXAlignment.Right,
|
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
|
TextSize = 11,
|
|
})
|
|
status.Parent = footer
|
|
|
|
local currentSection: any = nil
|
|
|
|
local function refresh()
|
|
for _, child in scroll:GetChildren() do
|
|
if not child:IsA("UIBase") then
|
|
child:Destroy()
|
|
end
|
|
end
|
|
|
|
local schema = ConfigAdmin.readSchema()
|
|
if not schema.ok then
|
|
currentSection = nil
|
|
status.Text = ""
|
|
Theme.label({
|
|
Size = UDim2.new(1, 0, 0, 80),
|
|
Text = schema.reason or "Engine not found.",
|
|
TextColor3 = Theme.COLOR.DIM,
|
|
TextWrapped = true,
|
|
}).Parent =
|
|
scroll
|
|
return
|
|
end
|
|
|
|
local section: any = nil
|
|
for _, s in schema.sections do
|
|
if s.id == sectionId then
|
|
section = s
|
|
break
|
|
end
|
|
end
|
|
if not section then
|
|
status.Text = ""
|
|
Theme.label({
|
|
Size = UDim2.new(1, 0, 0, 60),
|
|
Text = `Section '{sectionId}' is not in this engine's schema (older engine?).`,
|
|
TextColor3 = Theme.COLOR.DIM,
|
|
TextWrapped = true,
|
|
}).Parent =
|
|
scroll
|
|
return
|
|
end
|
|
currentSection = section
|
|
local defaults = schema.defaults[sectionId] or {}
|
|
|
|
local order = 0
|
|
Theme.label({
|
|
Size = UDim2.new(1, 0, 0, 20),
|
|
Text = section.title,
|
|
Font = Theme.FONT_BOLD,
|
|
TextSize = 15,
|
|
LayoutOrder = order,
|
|
}).Parent =
|
|
scroll
|
|
|
|
if section.note then
|
|
order += 1
|
|
Theme.label({
|
|
Size = UDim2.fromScale(1, 0),
|
|
AutomaticSize = Enum.AutomaticSize.Y,
|
|
Text = section.note,
|
|
TextColor3 = Theme.COLOR.DIM,
|
|
TextWrapped = true,
|
|
TextSize = 12,
|
|
LayoutOrder = order,
|
|
}).Parent =
|
|
scroll
|
|
end
|
|
|
|
for _, group in section.groups do
|
|
order += 1
|
|
local panel = Theme.panel()
|
|
panel.Size = UDim2.new(1, 0, 0, 22 + ROW_H * #group.fields + 8)
|
|
panel.AutomaticSize = Enum.AutomaticSize.None
|
|
panel.LayoutOrder = order
|
|
panel.Parent = scroll
|
|
|
|
Theme.label({
|
|
Size = UDim2.new(1, 0, 0, 22),
|
|
Text = group.label,
|
|
Font = Theme.FONT_BOLD,
|
|
TextSize = 14,
|
|
LayoutOrder = 0,
|
|
}).Parent =
|
|
panel
|
|
|
|
local groupDefaults = if group.name then defaults[group.name] else defaults
|
|
for i, field in group.fields do
|
|
local default = if typeof(groupDefaults) == "table" then groupDefaults[field.attr] else nil
|
|
local eff = ConfigAdmin.readEffective(section.id, group.name, field.attr, default)
|
|
local row = buildRow(ConfigAdmin, section.id, group.name, field, eff, applyEdit, applyReset)
|
|
row.LayoutOrder = i
|
|
row.Parent = panel
|
|
end
|
|
end
|
|
|
|
local overridden = ConfigAdmin.countOverrides(section)
|
|
status.Text = if overridden == 0
|
|
then "no overrides — engine defaults"
|
|
else `{overridden} field(s) overridden · applies on next Play`
|
|
end
|
|
|
|
resetBtn.MouseButton1Click:Connect(function()
|
|
if currentSection then
|
|
applyResetSection(currentSection)
|
|
end
|
|
end)
|
|
header.refreshBtn.MouseButton1Click:Connect(refresh)
|
|
refresh()
|
|
return { refresh = refresh }
|
|
end
|
|
|
|
-- Build the Nav pages: one per section when the engine is present, else a single explainer page.
|
|
-- Callbacks are the plugin main's record() wrappers.
|
|
function ConfigAdminUi.pages(ConfigAdmin: any, applyEdit, applyReset, applyResetSection): { any }
|
|
local pages = {}
|
|
local schema = ConfigAdmin.readSchema()
|
|
if schema.ok then
|
|
for _, section in schema.sections do
|
|
table.insert(pages, {
|
|
id = "config/" .. section.id,
|
|
label = section.title,
|
|
section = "Engine Config",
|
|
mount = function(frame)
|
|
return mountSection(frame, ConfigAdmin, section.id, applyEdit, applyReset, applyResetSection)
|
|
end,
|
|
})
|
|
end
|
|
else
|
|
table.insert(pages, {
|
|
id = "config/unavailable",
|
|
label = "Engine Config",
|
|
section = "Engine Config",
|
|
mount = function(frame)
|
|
local header = Theme.header(frame, "Engine Config")
|
|
local message = Theme.label({
|
|
Size = UDim2.new(1, -24, 0, 100),
|
|
Position = UDim2.fromOffset(12, 40),
|
|
Text = (schema.reason or "Engine not found.")
|
|
.. " Once the engine is in the place, close and reopen this window to load the sections.",
|
|
TextColor3 = Theme.COLOR.DIM,
|
|
TextWrapped = true,
|
|
TextYAlignment = Enum.TextYAlignment.Top,
|
|
})
|
|
message.Parent = frame
|
|
local function refresh()
|
|
local retry = ConfigAdmin.readSchema()
|
|
if retry.ok then
|
|
message.Text = "Engine found — close and reopen the SurvivorCore Studio window"
|
|
.. " to load the Engine Config sections."
|
|
end
|
|
end
|
|
header.refreshBtn.MouseButton1Click:Connect(refresh)
|
|
return { refresh = refresh }
|
|
end,
|
|
})
|
|
end
|
|
return pages
|
|
end
|
|
|
|
return ConfigAdminUi
|