mirror of
https://github.com/TemujinCalidius/SurvivorCore.git
synced 2026-08-14 00:58:01 +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>
277 lines
8.8 KiB
Luau
277 lines
8.8 KiB
Luau
--!nonstrict
|
|
--[[
|
|
ContentAdminUi — the no-code content editor pages. One sidebar page per category
|
|
(mountCategory), each rendering the MERGED roster (issue #40):
|
|
|
|
• authored entries — Configurations under SurvivorCoreContent/<folder>: the full editor
|
|
(every field seeded, Delete destroys the def), exactly as before.
|
|
• override entries — Configurations under SurvivorCoreContent/Overrides/<folder>: DELTAS
|
|
over a def registered from code. Blank = inherit the code value (the plugin can't show
|
|
code defaults in Edit mode — registries fill at runtime); Remove restores the pristine
|
|
code def on the next Play.
|
|
|
|
buildRosterEntry/matchEntry are exported so the global search page renders the same editable
|
|
panels. All writes route through the `actions` table of record()-wrapped callbacks the plugin
|
|
main supplies. Styling comes from Theme.
|
|
]]
|
|
|
|
local Theme = require(script.Parent.Theme)
|
|
|
|
local ContentAdminUi = {}
|
|
|
|
local ROW_H = Theme.ROW_H
|
|
|
|
-- A single authored-field row: label .... TextBox (committed on focus loss).
|
|
local function buildField(catKey: string, id: string, field: any, value: any, actions): Instance
|
|
local row = Theme.make("Frame", { Size = UDim2.new(1, 0, 0, ROW_H), BackgroundTransparency = 1 })
|
|
Theme.label({
|
|
Size = UDim2.fromScale(0.4, 1),
|
|
Text = field.label,
|
|
TextColor3 = Theme.COLOR.DIM,
|
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
|
}).Parent =
|
|
row
|
|
local box = Theme.textBox({
|
|
Size = UDim2.new(0.6, -4, 0, ROW_H - 4),
|
|
Position = UDim2.new(0.4, 4, 0, 2),
|
|
Text = tostring(value),
|
|
PlaceholderText = field.placeholder or tostring(field.default),
|
|
})
|
|
box.Parent = row
|
|
box.FocusLost:Connect(function()
|
|
actions.set(catKey, id, field, box.Text)
|
|
end)
|
|
return row
|
|
end
|
|
|
|
-- A single override-field row: blank = inherit. Booleans cycle (inherit) → true → false.
|
|
local function buildOverrideField(catKey: string, id: string, field: any, state: any, actions): Instance
|
|
local row = Theme.make("Frame", { Size = UDim2.new(1, 0, 0, ROW_H), BackgroundTransparency = 1 })
|
|
Theme.label({
|
|
Size = UDim2.fromScale(0.4, 1),
|
|
Text = field.label,
|
|
TextColor3 = if state.hasOverride then Theme.COLOR.TEXT else Theme.COLOR.DIM,
|
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
|
}).Parent =
|
|
row
|
|
|
|
if field.kind == "boolean" then
|
|
local control = Theme.button({
|
|
Size = UDim2.new(0.6, -4, 0, ROW_H - 4),
|
|
Position = UDim2.new(0.4, 4, 0, 2),
|
|
Text = if state.hasOverride then tostring(state.value) else "(inherit)",
|
|
TextColor3 = if state.hasOverride then Theme.COLOR.TEXT else Theme.COLOR.DIM,
|
|
TextSize = 13,
|
|
})
|
|
control.Parent = row
|
|
control.MouseButton1Click:Connect(function()
|
|
-- (inherit) → true → false → (inherit)
|
|
local nextRaw: any
|
|
if not state.hasOverride then
|
|
nextRaw = true
|
|
elseif state.value == true then
|
|
nextRaw = false
|
|
else
|
|
nextRaw = "" -- blank = remove the attribute = inherit
|
|
end
|
|
actions.setOverride(catKey, id, field, nextRaw)
|
|
end)
|
|
else
|
|
local box = Theme.textBox({
|
|
Size = UDim2.new(0.6, -4, 0, ROW_H - 4),
|
|
Position = UDim2.new(0.4, 4, 0, 2),
|
|
Text = if state.hasOverride then tostring(state.value) else "",
|
|
PlaceholderText = "(code default)",
|
|
})
|
|
box.Parent = row
|
|
box.FocusLost:Connect(function()
|
|
actions.setOverride(catKey, id, field, box.Text)
|
|
end)
|
|
end
|
|
return row
|
|
end
|
|
|
|
-- One roster entry panel — branches authored vs override.
|
|
function ContentAdminUi.buildRosterEntry(catKey: string, entry: any, ContentAdmin: any, actions): Instance
|
|
local cat = ContentAdmin.CATEGORIES[catKey]
|
|
local panel = Theme.panel()
|
|
local title = Theme.make("Frame", { Size = UDim2.new(1, 0, 0, 24), BackgroundTransparency = 1, LayoutOrder = 0 })
|
|
title.Parent = panel
|
|
|
|
local del = Theme.button({
|
|
Size = UDim2.fromOffset(56, 20),
|
|
Position = UDim2.new(1, -56, 0.5, -10),
|
|
Text = if entry.override then "Remove" else "Delete",
|
|
TextColor3 = Theme.COLOR.DANGER,
|
|
})
|
|
del.Parent = title
|
|
del.MouseButton1Click:Connect(function()
|
|
if entry.override then
|
|
actions.deleteOverride(catKey, entry.id)
|
|
else
|
|
actions.delete(catKey, entry.id)
|
|
end
|
|
end)
|
|
|
|
local labelRight = 64
|
|
|
|
if entry.override then
|
|
-- Accent pill marking this as a delta over a code-registered def.
|
|
local pill = Theme.badge("override", Theme.COLOR.ACCENT)
|
|
pill.AnchorPoint = Vector2.new(1, 0.5)
|
|
pill.Position = UDim2.new(1, -64, 0.5, 0)
|
|
pill.Parent = title
|
|
labelRight = 128
|
|
elseif
|
|
(catKey == "Resources" or catKey == "Mobs" or catKey == "Weapons" or catKey == "Quests") and actions.spawn
|
|
then
|
|
-- Gatherables + Mobs + Quests drop a tagged, def-linked instance; Weapons drop a starter
|
|
-- Tool you can model the look on — all in front of the camera.
|
|
local add = Theme.button({
|
|
Size = UDim2.fromOffset(104, 20),
|
|
Position = UDim2.new(1, -168, 0.5, -10),
|
|
Text = if catKey == "Weapons"
|
|
then "+ Tool model"
|
|
elseif catKey == "Quests" then "+ Quest giver"
|
|
else "+ Add to World",
|
|
TextColor3 = Theme.COLOR.ACCENT,
|
|
TextSize = 11,
|
|
})
|
|
add.Parent = title
|
|
add.MouseButton1Click:Connect(function()
|
|
actions.spawn(catKey, entry.id)
|
|
end)
|
|
labelRight = 176
|
|
end
|
|
|
|
Theme.label({
|
|
Size = UDim2.new(1, -labelRight, 1, 0),
|
|
Text = entry.id,
|
|
TextSize = 14,
|
|
Font = Theme.FONT_BOLD,
|
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
|
}).Parent =
|
|
title
|
|
|
|
for i, field in cat.fields do
|
|
local row: any
|
|
if entry.override then
|
|
local state = ContentAdmin.readOverrideField(catKey, entry.id, field)
|
|
row = buildOverrideField(catKey, entry.id, field, state, actions)
|
|
else
|
|
row = buildField(catKey, entry.id, field, ContentAdmin.read(catKey, entry.id, field), actions)
|
|
end
|
|
row.LayoutOrder = i
|
|
row.Parent = panel
|
|
end
|
|
|
|
return panel
|
|
end
|
|
|
|
-- Case-insensitive substring match on the entry id and its `name` attribute (when the category
|
|
-- has one). Shared by the category pages and the global search page.
|
|
function ContentAdminUi.matchEntry(ContentAdmin: any, catKey: string, entry: any, loweredQuery: string): boolean
|
|
if string.find(string.lower(entry.id), loweredQuery, 1, true) then
|
|
return true
|
|
end
|
|
local node = if entry.override
|
|
then ContentAdmin.getOverrideNode(catKey, entry.id)
|
|
else ContentAdmin.getNode(catKey, entry.id)
|
|
local name = node and node:GetAttribute("name")
|
|
if typeof(name) == "string" and string.find(string.lower(name), loweredQuery, 1, true) then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Mount one category's page. `actions` = { set, create, delete, spawn, createOverride,
|
|
-- setOverride, deleteOverride } — all record()-wrapped by the plugin main.
|
|
function ContentAdminUi.mountCategory(
|
|
container: Frame,
|
|
catKey: string,
|
|
ContentAdmin: any,
|
|
actions
|
|
): { refresh: () -> () }
|
|
local cat = ContentAdmin.CATEGORIES[catKey]
|
|
local header = Theme.header(container, cat.title)
|
|
local scroll = Theme.scroll(32, 0, 8)
|
|
scroll.Parent = container
|
|
|
|
local function refresh()
|
|
for _, child in scroll:GetChildren() do
|
|
if not child:IsA("UIBase") then
|
|
child:Destroy()
|
|
end
|
|
end
|
|
|
|
-- Create row: [ new id ............ ] [ + Create ] [ + Override ]
|
|
local createRow =
|
|
Theme.make("Frame", { Size = UDim2.new(1, 0, 0, 28), BackgroundTransparency = 1, LayoutOrder = 1 })
|
|
createRow.Parent = scroll
|
|
local idBox = Theme.textBox({
|
|
Size = UDim2.new(1, -172, 1, 0),
|
|
Text = "",
|
|
PlaceholderText = cat.keyLabel,
|
|
})
|
|
idBox.Parent = createRow
|
|
local createBtn = Theme.button({
|
|
Size = UDim2.fromOffset(76, 28),
|
|
Position = UDim2.new(1, -164, 0, 0),
|
|
Text = "+ Create",
|
|
TextSize = 13,
|
|
})
|
|
createBtn.Parent = createRow
|
|
createBtn.MouseButton1Click:Connect(function()
|
|
actions.create(catKey, idBox.Text)
|
|
end)
|
|
local overrideBtn = Theme.button({
|
|
Size = UDim2.fromOffset(84, 28),
|
|
Position = UDim2.new(1, -84, 0, 0),
|
|
Text = "+ Override",
|
|
TextColor3 = Theme.COLOR.ACCENT,
|
|
TextSize = 13,
|
|
})
|
|
overrideBtn.Parent = createRow
|
|
overrideBtn.MouseButton1Click:Connect(function()
|
|
actions.createOverride(catKey, idBox.Text)
|
|
end)
|
|
|
|
Theme.label({
|
|
Size = UDim2.fromScale(1, 0),
|
|
AutomaticSize = Enum.AutomaticSize.Y,
|
|
Text = "Create authors a full def here. Override tunes a def registered from code"
|
|
.. " (invisible in Edit — registries fill at runtime): type its id, set ONLY the"
|
|
.. " fields to change, blank = inherit. Typos warn in Output on Play.",
|
|
TextColor3 = Theme.COLOR.DIM,
|
|
TextWrapped = true,
|
|
TextSize = 11,
|
|
LayoutOrder = 2,
|
|
}).Parent =
|
|
scroll
|
|
|
|
local roster = ContentAdmin.listRoster(catKey)
|
|
if #roster == 0 then
|
|
Theme.label({
|
|
Size = UDim2.new(1, 0, 0, 22),
|
|
Text = `No {string.lower(cat.title)} yet — create one above.`,
|
|
TextColor3 = Theme.COLOR.DIM,
|
|
TextSize = 12,
|
|
LayoutOrder = 3,
|
|
}).Parent =
|
|
scroll
|
|
else
|
|
for i, entry in roster do
|
|
local panel = ContentAdminUi.buildRosterEntry(catKey, entry, ContentAdmin, actions)
|
|
panel.LayoutOrder = 3 + i
|
|
panel.Parent = scroll
|
|
end
|
|
end
|
|
end
|
|
|
|
header.refreshBtn.MouseButton1Click:Connect(refresh)
|
|
refresh()
|
|
return { refresh = refresh }
|
|
end
|
|
|
|
return ContentAdminUi
|