Project components & Component Studio
Project components are custom component types stored inside your scene file. They behave like built-ins in the inspector, Object Flow catalog, and runtime— but you author them in the Components workspace without editing engine source.
Opening the Studio
- Click the Components tab in the editor mode bar (alongside Arrange, Object Flow, Motion).
- The stage shows Component Studio: sidebar list + detail pane.
- Built-in rows are labeled Built-in; yours show Project.
+ New creates a blank project component and selects it for editing.
Project component record
Saved in scene.projectComponents[]:
{
"id": "rage_meter",
"label": "Rage Meter",
"category": "Combat",
"description": "Builds rage on hit; fires rageFull at max.",
"version": 1,
"fields": {
"value": { "type": "number", "default": 0, "min": 0, "max": 100 },
"max": { "type": "number", "default": 100 }
},
"events": {
"rageFull": { "label": "Rage Full" }
},
"actions": {
"charge": {
"label": "Charge",
"params": { "amount": { "type": "number", "default": 5 } },
"kind": "steps",
"steps": [ /* … */ ]
}
},
"conditions": {
"isFull": {
"label": "Is Full",
"kind": "script",
"body": "return self.value >= self.max;"
}
}
}On scene load, syncProjectComponents() compiles each record and registers it in the global registry so objects can attach rage_meter like any built-in.
Authoring fields
Supported field types (same as built-ins):
| Type | Inspector widget | Use for |
|---|---|---|
number / int | Number input | Stats, timers |
boolean | Checkbox | Flags |
string | Text | Names, keys |
enum | Select | Fixed options |
vector | X/Y pair | Offsets |
color | Color picker | UI tints |
objectRef | Object picker | Targets in scene |
componentRef | Object picker (filtered) | Objects with a component |
assetRef | Asset reference | Sprites, sounds |
eventKey | Event name | Custom subscriptions |
Hidden and read-only flags work the same as built-in field specs.
Action bodies: Script vs Steps
Each action chooses a kind:
Script (kind: "script")
A JavaScript function body compiled with compileScript. Available identifiers:
| Name | Meaning |
|---|---|
self | This component’s fields object |
params | Action parameters from the graph or caller |
event | Current flow event (if any) |
source / target | Event actors |
thing | Host Thing |
components | ComponentHost — call components.run("health", "damage", { amount: 5 }) |
Use emit("eventName", { ... }) when you need to fire declared events (provided in the action context).
Example — charge rage:
const next = Math.min(self.max, self.value + (params.amount || 0));
self.value = next;
if (self.value >= self.max) emit("rageFull");Steps (kind: "steps")
Visual blocks edited in the Studio— no semicolons required. Good for designers and for logic that stays inspectable in the scene JSON.
See Visual step language below.
Toggle Script / Steps on each action in the Studio; switching kind preserves separate body vs steps arrays.
Conditions
Project conditions support script bodies only (return boolean):
return self.value >= 10;They appear in Object Flow as Component Condition nodes like built-ins.
Attaching project components
- Arrange → select object → Components card → + Add Component.
- Your project types appear in the same menu as built-ins (search by label or id).
- Field editors are generated from your schema.
Deleting a project component
In the Studio detail header, Delete removes the record from projectComponents[], unregisters the type, and saves the scene. Objects that still had the type attached may warn on load and drop unknown types.
Rename the id carefully— saved scenes and blueprints reference the id string.
Visual step language
Steps are an ordered list executed synchronously when an action runs. Nested lists appear under If branches and For Each loops.
Step types
| Type | Purpose |
|---|---|
| Set Field | Write self.field, another component’s field, or a Thing property |
| Call Component | host.run(componentId, method, params) on self, source, target, or by id |
| Emit Event | Fire an event from this component’s event list |
| If / Else | Evaluate expression; run then or else step lists |
| For Each | Loop an array; bind item / index (configurable names) |
| Script | Escape hatch — same sandbox as Script action bodies |
| Comment | Documentation only (no runtime effect) |
Expressions in steps
- Values that support dynamic data can start with
=followed by a JavaScript expression.- Example:
=self.hp + params.amount
- Example:
- If and For Each expression fields are always evaluated as JS (you may omit the
=prefix in the editor; the runtime adds it).
Set Field targets
| Target | Writes to |
|---|---|
self | Current component fields |
host | The Thing (object-level property) |
<componentId> | Sibling component’s fields on the same Thing |
Call Component targets
| Target | Runs on |
|---|---|
self / omitted | Current Thing’s host |
source / target | Event source or target Thing |
Object id or =expression | Resolved Thing in the scene |
Example — charge with cap and emit
If / Else: self.value < self.max
Then:
Set Field: self.value = Math.min(self.max, self.value + params.amount)
If / Else: self.value >= self.max
Then:
Emit Event: rageFull (payload optional)
Else:
Comment: Already fullLimitations (by design)
- Steps run synchronously in one frame— no
await, delays, or tweens. - Use Object Flow for timelines, waits, and scene-wide orchestration.
- Errors in a step are logged; later steps still attempt to run unless the interpreter throws.
Studio UI reference
| Control | Effect |
|---|---|
| + New | New project component with default id |
| Sidebar row click | Select; built-ins are read-only detail |
| Add Field / Event / Action / Condition | Extend schema |
| Add Param on actions | Typed parameters for graphs |
| Delete (header) | Remove project component |
| Step ↑ ↓ × | Reorder or remove steps |
| + Set Field, + If, … | Add step at end of list |
Built-in components show a read-only breakdown of fields, events, actions, and conditions— useful as a live reference while authoring project types.
Compilation pipeline
Scene load
→ syncProjectComponents(records)
→ compileProjectComponent(each)
→ defineComponent({ …, actions with run fns })
→ registerComponent(definition)
→ createSpriteThing / hydrate Thing.componentsScript bodies compile once and cache. Step bodies compile to a function that calls runSteps(steps, ctx).
Tips
- Start with Health as a template — open
src/game/components/built-in/health.jswhile designing project fields/events. - Keep actions small — one action per user-visible verb (
charge,spend,reset). - Declare events in the Studio even if you only emit them from steps— they show up in documentation and future graph tooling.
- Test in Play with a throwaway object that only has your component before wiring full battle graphs.
Next: Object Flow integration · Architecture