mirror of
https://github.com/TemujinCalidius/SurvivorCore.git
synced 2026-08-14 00:58:01 +00:00
Select a Part or Model in Studio, answer "what is this object?", fill a form — it becomes a gatherable node, a mob or a quest giver. Closes the gap between authoring a def and setting up a world object, which until now meant knowing to tag a part and hand-typing PascalCase attributes in the property panel. Engine — components can declare an attribute SCHEMA: - src/components/Schema.luau (new): AttributeSpec/Display/ComponentSchema types, normalize/defaults/get/list, and the schemas for Gatherable, Mob, QuestGiver. Dependency-free ON PURPOSE: the plugin requires it live at edit time, and the component modules themselves can't be required there (Harvesting asserts IsServer; Remotes creates instances in ReplicatedStorage). - Components.define now accepts EITHER the legacy `attr = default` map or a schema array, normalizing both to one ordered spec list; bind() reads the derived default map, so binding is byte-identical. Legacy maps are sorted, as `pairs` order is arbitrary and would make a UI jitter. New getSchema/ listSchemas. The three shipped components pull name/tag/display/attributes from the schema; their onSetup bodies are untouched (defaults verified identical, all 23 attributes). Plugin — the Build page: - Field.luau (new): coerce/format/equalsDefault, lifted from ConfigAdmin (which now delegates), shared by every schema-driven editor. - FieldRow.luau (new): the shared [○/●] label … control + help row, including a ⌄ picker that cycles authored ids for fields declaring `ref`. - BuildAdmin.luau (new): live schema read with three distinct empty states, selection/eligibility/identify, deltas-only attribute writes, applyType (tag + clear any other component) and clear. - BuildAdminUi.luau (new): chooser cards, grouped form, multi-select apply, stale-bind-marker warning, SelectionChanged-driven refresh. - init.server.luau: record()-wrapped buildActions + the page. Docs: docs/admin-plugin.md Build section + a 60-second walkthrough, docs/extending.md schema guide, a CONTRIBUTING rule that new creator components declare one, CHANGELOG. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
117 lines
3.8 KiB
Luau
117 lines
3.8 KiB
Luau
--[[
|
|
Components — the creator-facing layer. A component binds engine behavior to a
|
|
CollectionService tag, reading per-instance Attributes (with defaults). Creators
|
|
tag their OWN objects and fill in attributes; no engine-side definition required.
|
|
|
|
Components.define({
|
|
name = "Gatherable",
|
|
tag = "Gatherable",
|
|
attributes = { ItemId = "unknown", Yield = 1, HP = 3 },
|
|
onSetup = function(instance, values) ... end,
|
|
})
|
|
|
|
Call Components.scan() once (SurvivorCore.start does this) to bind existing tagged
|
|
instances and watch for new ones.
|
|
|
|
`attributes` accepts EITHER form:
|
|
• the shorthand map above (attribute -> default), or
|
|
• a SCHEMA array of AttributeSpec (kind/label/help/choices/…) — see components/Schema.luau.
|
|
Both bind identically; the schema form additionally lets the Studio plugin render a setup form
|
|
for the component, so a creator picks "what is this object?" instead of typing attribute names.
|
|
Declare a `display` too ({ title, summary, instance }) so it can appear in that chooser.
|
|
]]
|
|
|
|
local CollectionService = game:GetService("CollectionService")
|
|
|
|
local Schema = require(script.Schema)
|
|
|
|
local Components = {}
|
|
|
|
export type AttributeSpec = Schema.AttributeSpec
|
|
|
|
export type Spec = {
|
|
name: string,
|
|
tag: string,
|
|
-- attribute -> default (shorthand) OR an array of AttributeSpec (schema form)
|
|
attributes: ({ [string]: any } | { Schema.AttributeSpec })?,
|
|
display: Schema.Display?, -- builder-UI presentation (title / summary / eligible class / hint)
|
|
onSetup: ((instance: Instance, values: { [string]: any }) -> ())?,
|
|
}
|
|
|
|
local defined: { [string]: Spec } = {}
|
|
local schemas: { [string]: Schema.ComponentSchema } = {}
|
|
local defaults: { [string]: { [string]: any } } = {} -- the attr->default map bind() reads
|
|
|
|
function Components.define(spec: Spec): Spec
|
|
assert(spec.name and spec.tag, "Component requires `name` and `tag`")
|
|
assert(defined[spec.name] == nil, `Component '{spec.name}' already defined`)
|
|
local attributes = Schema.normalize(spec.attributes)
|
|
defined[spec.name] = spec
|
|
schemas[spec.name] = {
|
|
name = spec.name,
|
|
tag = spec.tag,
|
|
display = spec.display or { title = spec.name, summary = "" },
|
|
attributes = attributes,
|
|
}
|
|
defaults[spec.name] = Schema.defaults(attributes)
|
|
return spec
|
|
end
|
|
|
|
-- Introspection (the runtime mirror of components/Schema.luau, including anything a game defined
|
|
-- at runtime — which the edit-time plugin can't see).
|
|
function Components.getSchema(name: string): Schema.ComponentSchema?
|
|
return schemas[name]
|
|
end
|
|
|
|
function Components.listSchemas(): { Schema.ComponentSchema }
|
|
local out = {}
|
|
for _, entry in schemas do
|
|
table.insert(out, entry)
|
|
end
|
|
table.sort(out, function(a, b)
|
|
local ao = (a.display and tonumber(a.display.order)) or 100
|
|
local bo = (b.display and tonumber(b.display.order)) or 100
|
|
if ao ~= bo then
|
|
return ao < bo
|
|
end
|
|
return a.name < b.name
|
|
end)
|
|
return out
|
|
end
|
|
|
|
local function readAttributes(instance: Instance, attributes: { [string]: any }?)
|
|
local values = {}
|
|
if attributes then
|
|
for attrName, default in attributes do
|
|
local v = instance:GetAttribute(attrName)
|
|
values[attrName] = if v == nil then default else v
|
|
end
|
|
end
|
|
return values
|
|
end
|
|
|
|
local function bind(instance: Instance, spec: Spec)
|
|
if instance:GetAttribute("_scBound") then
|
|
return
|
|
end
|
|
instance:SetAttribute("_scBound", true)
|
|
if spec.onSetup then
|
|
-- Always the normalized attr->default map, so both `attributes` forms bind identically.
|
|
spec.onSetup(instance, readAttributes(instance, defaults[spec.name]))
|
|
end
|
|
end
|
|
|
|
-- Bind everything currently tagged, then keep binding new instances as they appear.
|
|
function Components.scan()
|
|
for _, spec in defined do
|
|
for _, instance in CollectionService:GetTagged(spec.tag) do
|
|
task.spawn(bind, instance, spec)
|
|
end
|
|
CollectionService:GetInstanceAddedSignal(spec.tag):Connect(function(instance)
|
|
bind(instance, spec)
|
|
end)
|
|
end
|
|
end
|
|
|
|
return Components
|