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>
279 lines
9.5 KiB
Luau
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)
|