Files
SurvivorCore/plugin/init.server.luau
T
Samuel LisonandClaude Opus 4.8 91c7a87f89 feat(plugin): SurvivorCore Studio — floating window, sidebar nav, search, Engine Config editor, roster overrides (#11, #21, #40)
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>
2026-07-07 12:07:48 +10:00

279 lines
9.5 KiB
Luau

--!nonstrict
--[[
SurvivorCore Studio admin plugin (main).
ONE floating (dockable) window with a sidebar rail and a detail pane, one page per editor:
Overview engine status + content counts (click-through) + how-it-works.
Survival Stats tune stats (deltas-only, locked) on SurvivalStatsConfig + HUD preview.
Engine Config every engine Config section (Movement/Combat/Mobs//UI theme) as
deltas-only overrides on SurvivorCoreEngineConfig (issue #21).
Content create/edit/delete Items/Weapons/Arrows/Resources/Mobs/Quests/Achievements
as SurvivorCoreContent instances, plus OVERRIDES that tune code-registered
defs (issue #40). A rail search box finds any entry by id/name.
Every mutation is wrapped in a single ChangeHistoryService recording (one undo step), then the
active page refreshes (others refresh on next show). All real logic lives in the requirable
modules (StatAdmin / ConfigAdmin / ContentAdmin); the forms in the *Ui modules; the shared look
in Theme. This is the Builder / Admin plugin (issue #11).
]]
local ChangeHistoryService = game:GetService("ChangeHistoryService")
local Theme = require(script.Theme)
local Nav = require(script.Nav)
local SpawnHelpers = require(script.SpawnHelpers)
local OverviewPage = require(script.OverviewPage)
local SearchPage = require(script.SearchPage)
local StatAdmin = require(script.StatAdmin)
local StatAdminUi = require(script.StatAdminUi)
local HudPreview = require(script.HudPreview)
local ContentAdmin = require(script.ContentAdmin)
local ContentAdminUi = require(script.ContentAdminUi)
local ConfigAdmin = require(script.ConfigAdmin)
local ConfigAdminUi = require(script.ConfigAdminUi)
local SETTING_LAST_PAGE = "SurvivorCoreStudio.lastPage"
local toolbar = plugin:CreateToolbar("SurvivorCore")
local button = toolbar:CreateButton("Studio", "Tune the engine + create items/weapons/gatherables/mobs (no-code)", "")
button.ClickableWhenViewportHidden = true
-- CreateDockWidgetPluginGui is flagged deprecated by the tooling, but it is still the only API for
-- a dockable plugin widget (there is no replacement) — so allow it here. NOTE: the widget ID is
-- "SurvivorCoreStudio" (was "SurvivorCoreAdmin") — Studio persists dock state PER ID and ignores
-- new initial values for known IDs, so the rename is what makes the floating default reach
-- existing users (their old dock position resets once).
-- selene: allow(deprecated)
local widget = plugin:CreateDockWidgetPluginGui(
"SurvivorCoreStudio",
DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, false, 660, 580, 480, 380)
)
widget.Title = "SurvivorCore Studio"
widget.Name = "SurvivorCoreStudio"
local root = Instance.new("Frame")
root.Size = UDim2.fromScale(1, 1)
root.BackgroundColor3 = Theme.COLOR.BG
root.BorderSizePixel = 0
root.Parent = widget
local nav -- assigned after the pages are assembled; closures below capture it
-- Wrap a mutation in one ChangeHistory recording (one undo step), then refresh the UI.
local function record(name: string, mutate: () -> any): any
local recording = ChangeHistoryService:TryBeginRecording(name)
local result = mutate()
if recording then
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
end
if nav then
nav.refreshAll()
end
return result
end
-- ── Stats callbacks ───────────────────────────────────────────────────────────
local function applyEdit(statName: string, attr: string, raw: any, default: any): any
return record(`Stats: {statName}.{attr}`, function()
return StatAdmin.setOverride(statName, attr, raw, default)
end)
end
local function applyReset(statName: string, attr: string): any
return record(`Stats: reset {statName}.{attr}`, function()
return StatAdmin.resetOverride(statName, attr)
end)
end
local function applyPreview(): any
return record("Stats: preview HUD", function()
return HudPreview.apply(StatAdmin)
end)
end
local function applyClear(): any
return record("Stats: clear HUD preview", function()
return HudPreview.clear()
end)
end
-- ── Engine Config callbacks ───────────────────────────────────────────────────
local function applyConfigEdit(sectionId: string, groupName: string?, field: any, raw: any, default: any): any
return record(`Config: {sectionId}.{field.attr}`, function()
return ConfigAdmin.setOverride(sectionId, groupName, field, raw, default)
end)
end
local function applyConfigReset(sectionId: string, groupName: string?, attr: string): any
return record(`Config: reset {sectionId}.{attr}`, function()
return ConfigAdmin.resetOverride(sectionId, groupName, attr)
end)
end
local function applyConfigResetSection(section: any): any
return record(`Config: reset section {section.id}`, function()
return ConfigAdmin.resetSection(section)
end)
end
-- ── Content callbacks (authored + overrides), bundled for the pages ──────────
local actions = {
set = function(catKey: string, id: string, field: any, raw: any): any
return record(`Content: {catKey} {id}.{field.attr}`, function()
return ContentAdmin.set(catKey, id, field, raw)
end)
end,
create = function(catKey: string, rawId: any): any
return record(`Content: create {catKey}`, function()
return ContentAdmin.create(catKey, rawId)
end)
end,
delete = function(catKey: string, id: string): any
return record(`Content: delete {catKey} {id}`, function()
ContentAdmin.delete(catKey, id)
end)
end,
spawn = function(catKey: string, id: string): any
return record(`Content: add {catKey} '{id}' to world`, function()
return SpawnHelpers.spawn(ContentAdmin, catKey, id)
end)
end,
createOverride = function(catKey: string, rawId: any): any
return record(`Content: override {catKey}`, function()
return ContentAdmin.createOverride(catKey, rawId)
end)
end,
setOverride = function(catKey: string, id: string, field: any, raw: any): any
return record(`Content: override {catKey} {id}.{field.attr}`, function()
return ContentAdmin.setOverrideField(catKey, id, field, raw)
end)
end,
deleteOverride = function(catKey: string, id: string): any
return record(`Content: remove override {catKey} {id}`, function()
ContentAdmin.deleteOverride(catKey, id)
end)
end,
}
-- ── Search state (rail box → hidden results page) ─────────────────────────────
local currentQuery = ""
local searchGeneration = 0
local lastNonSearchId = "overview"
-- ── NAV assembly (data-driven; new editors slot in as more pages) ─────────────
local pages = {}
table.insert(pages, {
id = "overview",
label = "Overview",
section = "",
mount = function(frame)
return OverviewPage.mount(frame, {
StatAdmin = StatAdmin,
ContentAdmin = ContentAdmin,
selectPage = function(id: string)
nav.selectPage(id)
end,
})
end,
})
table.insert(pages, {
id = "stats",
label = "Survival Stats",
section = "Stats",
mount = function(frame)
return StatAdminUi.mount(frame, StatAdmin, applyEdit, applyReset, applyPreview, applyClear)
end,
})
for _, page in ConfigAdminUi.pages(ConfigAdmin, applyConfigEdit, applyConfigReset, applyConfigResetSection) do
table.insert(pages, page)
end
for _, catKey in ContentAdmin.ORDER do
local cat = ContentAdmin.CATEGORIES[catKey]
table.insert(pages, {
id = "content/" .. catKey,
label = cat.title,
section = "Content",
badge = function()
return #ContentAdmin.listRoster(catKey)
end,
mount = function(frame)
return ContentAdminUi.mountCategory(frame, catKey, ContentAdmin, actions)
end,
})
end
table.insert(pages, {
id = "search",
label = "Search",
section = "",
hidden = true,
mount = function(frame)
return SearchPage.mount(frame, {
ContentAdmin = ContentAdmin,
ContentAdminUi = ContentAdminUi,
actions = actions,
getQuery = function()
return currentQuery
end,
})
end,
})
-- Restore the last-open page (validated against the live page list — ids may change across
-- plugin versions or when the engine's schema grows/shrinks).
local restoredId = "overview"
local saved = plugin:GetSetting(SETTING_LAST_PAGE)
if typeof(saved) == "string" then
for _, page in pages do
if page.id == saved and not page.hidden then
restoredId = saved
break
end
end
end
nav = Nav.mount(root, pages, {
initialId = restoredId,
onNavigate = function(id: string)
lastNonSearchId = id
if plugin:GetSetting(SETTING_LAST_PAGE) ~= id then
plugin:SetSetting(SETTING_LAST_PAGE, id)
end
end,
onSearch = function(text: string)
currentQuery = text
searchGeneration += 1
local generation = searchGeneration
task.delay(0.25, function()
if generation ~= searchGeneration or not widget.Enabled then
return
end
if string.match(currentQuery, "^%s*$") then
if nav.getActiveId() == "search" then
nav.selectPage(lastNonSearchId)
end
else
nav.selectPage("search")
nav.refreshAll() -- re-render the results with the new query
end
end)
end,
})
-- An undo/redo can revert any edit the pages show — refresh so they never go stale.
ChangeHistoryService.OnUndo:Connect(function()
nav.refreshAll()
end)
ChangeHistoryService.OnRedo:Connect(function()
nav.refreshAll()
end)
button.Click:Connect(function()
widget.Enabled = not widget.Enabled
if widget.Enabled then
nav.refreshAll()
end
end)
widget:GetPropertyChangedSignal("Enabled"):Connect(function()
button:SetActive(widget.Enabled)
end)