@neocompose/cli 0.87.2 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +14 -0
  3. package/dist/neo-runtime.mjs +3288 -724
  4. package/dist/test-worker.mjs +2445 -114
  5. package/package.json +1 -1
  6. package/skills/neocompose-cli/SKILL.md +95 -279
  7. package/skills/neocompose-cli/references/cli-development.md +101 -117
  8. package/skills/neocompose-cli/references/declarations/annotations-and-storage.md +112 -0
  9. package/skills/neocompose-cli/references/declarations/constructors.md +164 -0
  10. package/skills/neocompose-cli/references/declarations/declarations.md +103 -0
  11. package/skills/neocompose-cli/references/declarations/initializers.md +100 -0
  12. package/skills/neocompose-cli/references/declarations/member-kinds.md +107 -0
  13. package/skills/neocompose-cli/references/declarations/overloads.md +50 -0
  14. package/skills/neocompose-cli/references/dialogues.md +145 -0
  15. package/skills/neocompose-cli/references/neoscript/collections.md +98 -0
  16. package/skills/neocompose-cli/references/neoscript/control-flow.md +123 -0
  17. package/skills/neocompose-cli/references/neoscript/delegates-and-listeners.md +107 -0
  18. package/skills/neocompose-cli/references/neoscript/math-and-random.md +114 -0
  19. package/skills/neocompose-cli/references/neoscript/neoscript.md +112 -0
  20. package/skills/neocompose-cli/references/neoscript/strings.md +47 -0
  21. package/skills/neocompose-cli/references/neoscript/testing-and-migrations.md +73 -0
  22. package/skills/neocompose-cli/references/neoscript/types-and-narrowing.md +111 -0
  23. package/skills/neocompose-cli/references/sync/branches-and-history.md +69 -0
  24. package/skills/neocompose-cli/references/sync/conflicts-and-recovery.md +68 -0
  25. package/skills/neocompose-cli/references/sync/sync.md +96 -0
  26. package/skills/neocompose-cli/references/sync/unity-export.md +47 -0
  27. package/skills/neocompose-cli/references/values/defaults-and-localization.md +86 -0
  28. package/skills/neocompose-cli/references/values/files.md +71 -0
  29. package/skills/neocompose-cli/references/values/identities.md +97 -0
  30. package/skills/neocompose-cli/references/values/references.md +151 -0
  31. package/skills/neocompose-cli/references/values/values.md +110 -0
  32. package/skills/neocompose-cli/references/world/animation.md +133 -0
  33. package/skills/neocompose-cli/references/world/grids-and-cell-patterns.md +55 -0
  34. package/skills/neocompose-cli/references/world/variants.md +112 -0
  35. package/skills/neocompose-cli/references/world/world.md +105 -0
  36. package/skills/neocompose-cli/references/animation-and-world-authoring.md +0 -582
  37. package/skills/neocompose-cli/references/commands-and-sync.md +0 -408
  38. package/skills/neocompose-cli/references/declarations-and-construction.md +0 -773
  39. package/skills/neocompose-cli/references/neoscript.md +0 -990
  40. package/skills/neocompose-cli/references/values-identities-and-references.md +0 -421
@@ -0,0 +1,110 @@
1
+ # Values
2
+
3
+ How stored values are written: the root envelope, static members, local
4
+ initializers, and the single-owner rule. Open a leaf from
5
+ [References](#references) for IDs, `Reference<T>`, defaults, localization, or
6
+ files.
7
+
8
+ ## Root and static bindings
9
+
10
+ Edit only the initializer after `=` in the compiler-owned `Root.neo`. The root
11
+ member's name, type, ID, lock, and storage are read-only.
12
+
13
+ ```neo
14
+ Root root = new() {
15
+ @id("root-assets-member-id")
16
+ @storage(allowed: .Immutable)
17
+ Assets Assets = new() {
18
+ Outposts = [Assets.Capitol.Clone()],
19
+ };
20
+ }
21
+ ```
22
+
23
+ Persisted reusable values are static members on ordinary classes:
24
+
25
+ ```neo
26
+ @id("assets-class-id")
27
+ @storage(allowed: .Immutable)
28
+ class Assets {
29
+ @id("capitol-member-id")
30
+ static Outpost Capitol = new {
31
+ Name = "Capitol",
32
+ Image = Images.Capitol.Slice(0),
33
+ };
34
+
35
+ static Outpost Home => Assets.Capitol;
36
+ }
37
+ ```
38
+
39
+ The member ID owns a root or static binding; never add a second `@id` before
40
+ its initializer. A computed static getter such as `Home` is an alias with no
41
+ binding or row. Statics created together may refer to each other in any
42
+ order with no preliminary push.
43
+
44
+ ## One owner per value
45
+
46
+ Class, List, Set, and Dictionary values have exactly one owner. Scalars and
47
+ delegates copy. Placing an owned value elsewhere is refused at its authored
48
+ position by status, push, and `neo test`: a static, a static's field, a
49
+ collection entry, a local placed twice, or a constructor parameter stored in
50
+ an ordinary field. Clone it or reference it:
51
+
52
+ ```neo
53
+ static Outpost Annex = Assets.Capitol.Clone();
54
+ static Reference<Outpost> Seat = Reference(Assets.Capitol);
55
+ ```
56
+
57
+ ## Local initializers
58
+
59
+ Project initializers may live in any `.neo` file and need no imports. They
60
+ are source organization, not runtime globals or server records.
61
+
62
+ ```neo
63
+ List<NeoSortingLayer> SORTING_LAYERS = [
64
+ @id("existing-layer-id")
65
+ new NeoSortingLayer() { Name = "Default" }
66
+ ];
67
+ ```
68
+
69
+ - Consume it with `SortingLayers = SORTING_LAYERS`. The consuming member owns
70
+ the identity; add no ID at the use. Keep existing nested IDs when
71
+ extracting; omit IDs on new rows.
72
+ - An aggregate local has one owning use. Use `Reference(SORTING_LAYERS)` where
73
+ the member allows it; for an independent copy, copy the initializer without
74
+ IDs.
75
+ - Scalars may have several uses. If the web later makes them diverge, pull
76
+ inlines the differing uses.
77
+ - Complete member and collection entries compose inside constructor-backed
78
+ initializers and keep their concrete class.
79
+ - A class local passed as a constructor argument or executable operand names
80
+ its single stored instance; see
81
+ [identities](identities.md#locals-as-constructor-arguments).
82
+ - Scalar locals can't be used in expressions such as `BASE + 1`; inline the
83
+ expression. Executable closures can't read locals.
84
+ - The editor offers extract-to-file, inline on a sole use, and duplicate at a
85
+ use. Move created files freely.
86
+
87
+ ## Dictionary keys
88
+
89
+ Enum-keyed dictionaries take contextual or qualified option keys:
90
+
91
+ ```neo
92
+ Dictionary<ToolType, float> Damage = { .Pickaxe: 10, ToolType.Axe: 2 };
93
+ ```
94
+
95
+ Pull writes `.Option`. A quoted raw option ID also works; a quoted option name
96
+ such as `"Pickaxe"` is not an ID. Keys of deleted options stay as quoted IDs.
97
+ String keys are data: `dictionary["Id"]` reads an entry, while a class's
98
+ `.Id` is its identity. String keys may be empty, contain whitespace, or use
99
+ names like `__proto__`.
100
+
101
+ ## References
102
+
103
+ - [Identities](identities.md): when to keep, omit, or author `@id`; moving
104
+ nested values; locals as constructor arguments.
105
+ - [References](references.md): `Reference<T>` declaration, handle vs slot
106
+ semantics, source forms, same-push resolution.
107
+ - [Defaults and localization](defaults-and-localization.md): virtual
108
+ members, sparse overrides, construction rebuilds, localized text, `neo loc`.
109
+ - [Managed files](files.md): image and audio registries, binaries, Unity
110
+ import settings.
@@ -0,0 +1,133 @@
1
+ # Animation
2
+
3
+ Frame overrides, child selectors, segments, and tracks.
4
+
5
+ A `NeoAnimationClip<T>` has `FPS`, `Duration`, `Frames`, and `Tracks`. Its
6
+ `NeoAnimationFrame<T>` rows hold `Index`, `Overrides`, `ChildOverrides`, and
7
+ `Actions`. Frames are sparse: each applies from its index until the next row.
8
+
9
+ ## Partial overrides
10
+
11
+ Frame `Overrides` and [variant overrides](variants.md) accept partial
12
+ structured literals:
13
+
14
+ ```neo
15
+ new NeoAnimationFrame<MySprite> {
16
+ Index = 2,
17
+ Overrides = new { Sprite = new { SliceIndex = 1 } },
18
+ }
19
+ ```
20
+
21
+ An empty `new { }` changes nothing. A partial changes only named fields; a
22
+ whole-leaf value replaces the leaf. Frame overrides may change `Enabled`,
23
+ `FlipX`, and `SortingOrder` but never add, remove, or reorder `Children`.
24
+
25
+ ## Child selectors
26
+
27
+ Tracks and frame overrides find direct children through selector delegates,
28
+ not row IDs. Match on the project's own stable identity, such as an immutable
29
+ `Name`, slug, or semantic position:
30
+
31
+ ```neo
32
+ class LegPart : NeoObject {
33
+ public PantsSprite SelectPants() {
34
+ var child = this.Children.FirstOrDefault((candidate) => {
35
+ return candidate.Name == "Pants";
36
+ });
37
+ if (child is PantsSprite pants) {
38
+ return pants;
39
+ }
40
+ throw "Pants child is missing or has the wrong type";
41
+ }
42
+ }
43
+ ```
44
+
45
+ - Prefer a named function when the rule is reused or deserves a clear
46
+ failure; compatible inline closures also work.
47
+ - A selector such as `this.SelectPants` runs in the lexical scope of the
48
+ class that declares the clip: `this` is the `LegPart`, not the track row or
49
+ an outer object that later copies the default.
50
+ - Only for a deliberately authored child slot whose placement clones carry
51
+ `sourceValueId`, return
52
+ `Reference<T>(id: "<authored-child-id>", withProvenance: true)`. Missing
53
+ optional slots are skipped with diagnostics. A plain exact-ID reference
54
+ reintroduces the row-ID coupling selectors exist to avoid.
55
+
56
+ ## Segments
57
+
58
+ `NeoAnimationSegment<T>` is a frame-indexed value lane. Its
59
+ `NeoAnimationSegmentFrame<T>` rows have `Index` and `Value`; sparse values
60
+ hold until the next row or `Duration`. Pass `Value` through the constructor:
61
+ `new NeoAnimationSegmentFrame<int>(value: 60)`. `NeoSpriteAnimationSegment`
62
+ is the sprite specialization.
63
+
64
+ ## Tracks
65
+
66
+ `NeoAnimationClip.Tracks` is one polymorphic list:
67
+
68
+ - `NeoAnimationChildTrack` plays `ClipKey` on the child its `Selector`
69
+ returns.
70
+ - A project subclass of `NeoAnimationSegmentTrack<TChild, TValue>` plays a
71
+ segment onto one target member. Implement `Segment` as a stored value,
72
+ lookup, or getter.
73
+ - `NeoSpriteAnimationSegmentTrack<TChild extends NeoSpriteObject>` targets
74
+ `NeoSpriteObject.Sprite`.
75
+
76
+ ```neo
77
+ class StoredPantsTrack : NeoSpriteAnimationSegmentTrack<PantsSprite> {
78
+ public override NeoSpriteAnimationSegment Segment = new();
79
+ }
80
+
81
+ // inside LegPart
82
+ public NeoAnimationClip<LegPart> Walk = new NeoAnimationClip<LegPart> {
83
+ Tracks = [
84
+ new StoredPantsTrack(selector: this.SelectPants) {
85
+ StartFrame = 0,
86
+ Direction = .Reverse,
87
+ OffsetEndIndex = 3,
88
+ },
89
+ ]
90
+ };
91
+ ```
92
+
93
+ - Tracks have `StartFrame`, `Direction` (`.Forward`/`.Reverse`), and a crop
94
+ window `OffsetStartIndex`/`OffsetEndIndex`. Cropping happens before
95
+ reversing; content past the clip truncates. Empty or inverted windows and
96
+ tracks that never enter the clip are rejected.
97
+ - The selector's return type must fit `TChild` and the segment value `TValue`.
98
+ - `Refresh` controls selector re-evaluation; track reads and segment getters
99
+ re-resolve on the next frame after a watched dependency changes.
100
+ - Tracks writing the same member in one frame apply in list order; the last
101
+ wins.
102
+ - New track rows omit `@id`; keep IDs already on pulled rows.
103
+
104
+ ## Animating class members
105
+
106
+ A track can target a class member. Each frame writes the whole value, and a
107
+ `null` frame clears it. The target must be a writable Save, Session, or
108
+ Writable member on the child, and the segment class must fit it. Every
109
+ `NeoObjectBase` declares `Collider`, so an attack hit box is a Session
110
+ `Collider` override plus a `NeoCollider?` track:
111
+
112
+ ```neo
113
+ class ColliderAnimationSegment : NeoAnimationSegment<NeoCollider?> {
114
+ }
115
+
116
+ @settings(target: NeoObjectBase.Collider)
117
+ class SwingColliderTrack(NeoDelegate<SwordSprite> selector)
118
+ : NeoAnimationSegmentTrack<SwordSprite, NeoCollider?>(selector: selector) {
119
+ public override ColliderAnimationSegment Segment = new ColliderAnimationSegment() {
120
+ Duration = 3,
121
+ Frames = [
122
+ new NeoAnimationSegmentFrame<NeoCollider?>(value: new NeoCollider() {
123
+ Size = new(1, 1),
124
+ Offset = new(1, 1),
125
+ IsTrigger = true
126
+ }) { Index = 1 },
127
+ new NeoAnimationSegmentFrame<NeoCollider?>(value: null) { Index = 2 }
128
+ ]
129
+ };
130
+ }
131
+ ```
132
+
133
+ On `SwordSprite`: `@storage(allowed: .Session) public override NeoCollider? Collider;`
@@ -0,0 +1,55 @@
1
+ # Grids and cell patterns
2
+
3
+ ## NeoCellPattern
4
+
5
+ An immutable ordered list of `Vector2Int` offsets. The constructor copies its
6
+ input and keeps duplicates.
7
+
8
+ ```neo
9
+ NeoCellPattern neighbors = NeoCellPattern.Box(1, excluding: .Center);
10
+ NeoCellPattern custom = new(offsets: [new(-1, 0), new(2, 1)]);
11
+ ```
12
+
13
+ - Shapes: `Box`, `Cross`, `Ring`, `Rect`, `Line`. Presets: `Center`,
14
+ `FourNeighbors`, `EightNeighbors`, `Empty`.
15
+ - Box, Cross, and Line include the origin unless `excluding: .Center`.
16
+ Line's `length` counts steps beyond the origin, so `Line(new(1, 0), 3)` has
17
+ four cells.
18
+ - `WithCenter`, `WithoutCenter`, `Translate`, and `Union` return patterns;
19
+ `Union` drops duplicates, keeping first occurrences.
20
+ - `Cells(origin)` returns absolute cells in pattern order.
21
+ - Presets are stored statics, so a field that owns a pattern can't default to
22
+ one (`= NeoCellPattern.EightNeighbors` is refused). Clone it or reference it:
23
+
24
+ ```neo
25
+ class Sprinkler : NeoObject {
26
+ public NeoCellPattern Reach = NeoCellPattern.EightNeighbors.Clone();
27
+ public Reference<NeoCellPattern> Splash = NeoCellPattern.Center;
28
+ }
29
+ ```
30
+
31
+ In C#, `NeoCellPattern` implements `INeoCellPattern`
32
+ (`IReadOnlyList<Vector2Int>`), which SDK grid helpers accept. The exclusion
33
+ argument is `NeoCellPatternExcluding.None` or `.Center`.
34
+
35
+ ## Grid queries
36
+
37
+ On a placed `NeoObject`:
38
+
39
+ - `this.Cell` is its grid cell.
40
+ - `this.GetObjects<Plant>(pattern)` returns placed objects in the same grid
41
+ that are `Plant` or a subclass.
42
+ - `this.GetTile(pattern)` returns the first tile in pattern order, or null.
43
+ - `this.GetTiles(pattern)` returns the tile at each occupied cell in pattern
44
+ order. `GetTiles<DirtDryTile>(pattern)` filters each cell's top tile and
45
+ skips cells whose top tile differs, even with a dry tile underneath.
46
+ - Each query takes an optional `offset` that shifts the whole pattern:
47
+ `this.GetTiles(neighbors, new(0, 2))`.
48
+ - Queries track empty cells too, so getters update when a neighbor appears.
49
+ Calling them on an unplaced object is an error.
50
+
51
+ `this.GetArea()` works on any `NeoObjectBase` or `NeoTile` and returns the
52
+ nearest owning `NeoTileGrid`, or null. Sprite children, objects held in
53
+ another object's field, and tiles in nested layer links answer with their
54
+ placed root's grid. `this.GetArea<FarmGrid>()` is null in any other grid. It
55
+ sees spawns, moves, and removals made earlier in the same evaluation.
@@ -0,0 +1,112 @@
1
+ # Object variants
2
+
3
+ A variant is a named configuration of one `NeoObject`-derived class: how to
4
+ construct a fresh instance and how to move an existing one into the
5
+ configuration. Variants are records parented to their class, not values in
6
+ it.
7
+
8
+ ```neo
9
+ // Variants/ExampleObject.neo
10
+ @id("5f0c2b9e-7d41-4a58-9b1a-3f6f0e2d8c11")
11
+ NeoVariant<ExampleObject> Down = new(
12
+ initialize: () => { return new ExampleObject(.Down); },
13
+ apply: (source) => { source.FacingDir = .Down; },
14
+ overrides: new { Name = "Facing down" },
15
+ );
16
+ ```
17
+
18
+ - `initialize` is required, takes no parameters, and returns the class.
19
+ - `apply` takes one parameter of the class and is void. Omit it for a
20
+ construct-only variant.
21
+ - Both are ordinary closures with block or expression bodies:
22
+ `() => new ExampleObject(.Down)`, `(source) => source.FacingDir = .Down`.
23
+ Each has its own scope. The server compiles them.
24
+ - `overrides` and child overrides' `overrides` are `Partial<T>` deltas,
25
+ written `new { ... }` or `new Partial<T> { ... }`. They never pass
26
+ constructor arguments, even when `T` has a required constructor.
27
+
28
+ A nested class partial changes only named members. A list override replaces
29
+ the whole list; it does not patch entries by index. In particular, overriding
30
+ a clip's `Frames` requires the complete desired list, not just the changed
31
+ frame. For editing an existing clip, see [frame overrides](animation.md).
32
+
33
+ ## Applying
34
+
35
+ - `Variants.X.Initialize()` runs `initialize`, then overrides and child
36
+ overrides.
37
+ - `obj.ToVariant(X)` runs `apply`, then overrides and child overrides, never
38
+ `initialize`.
39
+ - An instance remembers the variant that built it and the one last applied.
40
+ The applied variant's overrides win where they answer, `apply` writes stay
41
+ as instance overrides, and `ToVariant(Base)` removes the applied layer.
42
+ - On an object nested inside another instance's construction, `ToVariant`
43
+ applies in place and records nothing.
44
+ - Selections compare equal when class, variant, and bound lookup row match.
45
+ An unset optional selection differs from `Base`.
46
+
47
+ ## Folders
48
+
49
+ ```neo
50
+ // Variants/ExampleObject/Inverted.neo
51
+ @id("b7d2e9f0-3c15-4a68-8b24-9e0f1a2c3d44")
52
+ NeoVariantFolder<ExampleObject> Inverted = new("Inverted");
53
+
54
+ @id("e3b9c0d1-4a26-4f81-92e7-6b5a4c3d2e66")
55
+ @settings(folder: Inverted)
56
+ NeoVariant<ExampleObject> Down = new(
57
+ initialize: () => { return new ExampleObject(.Down); },
58
+ );
59
+ ```
60
+
61
+ Nest with `/` in the path; intermediate segments are implicit
62
+ (`new("Trees/Oak")` needs no `Trees` record).
63
+
64
+ Rules push and the language service enforce:
65
+
66
+ - A variant or folder belongs to its type argument's class; a variant may use
67
+ only its own class's folders.
68
+ - Variant names and folder segments share one static scope per folder and
69
+ are unique case-insensitively. Different classes may reuse names.
70
+ - `Base` is reserved case-insensitively; `<Class>.Variants.Base` is the class
71
+ with no variant.
72
+ - Names must survive C# identifier normalization.
73
+
74
+ Push accepts variants in any `.neo` file. Pull writes
75
+ `Variants/<Class>.neo` and `Variants/<Class>/<Folder path>.neo`, one file per
76
+ class and folder, folder record first and variants by name.
77
+
78
+ ## Lookup folders
79
+
80
+ A lookup folder binds one concrete list and offers the same variants for each
81
+ row. The second type argument is the entry class. An empty path binds the
82
+ class root.
83
+
84
+ ```neo
85
+ @id("18b93b80-5733-4747-bf5f-3609336e4f17")
86
+ NeoLookupVariantFolder<ExampleObject, NeoSortingLayer> Root = new(
87
+ path: "",
88
+ collection: Assets.SortingLayers,
89
+ collectionValue: Reference(root.Assets.SortingLayers),
90
+ );
91
+
92
+ @id("736dddf8-2881-47e1-a1d3-55fb65ea9f2a")
93
+ @settings(folder: Root)
94
+ NeoLookupVariant<ExampleObject, NeoSortingLayer> Layered = new(
95
+ initialize: (value) => { return new ExampleObject(); },
96
+ );
97
+ ```
98
+
99
+ - `collection` names the eligible list member; `collectionValue` the stored
100
+ list whose rows it enumerates. Prefer the `Reference(root...)` form pull
101
+ emits.
102
+ - Lookup `initialize` receives the row; lookup `apply` receives the object,
103
+ then the row.
104
+ - A `NeoLookupVariant<TObject, TValue>` member holds an unbound handle. A
105
+ plain `NeoVariant<TObject>` member may pick one row with
106
+ `.Bind(Reference(...))`; never bind a row to the lookup-handle type.
107
+
108
+ ## Type restrictions
109
+
110
+ `NeoVariantFolder` and `NeoLookupVariantFolder` are top-level records, never
111
+ member or parameter types. Variant handles are valid as Variant members only,
112
+ not as collection entries, lookup targets, parameters, or generic bindings.
@@ -0,0 +1,105 @@
1
+ # World objects
2
+
3
+ Objects, tiles, sorting, and structured leaf values. Open a leaf from
4
+ [References](#references) for grid queries, variants, or animation.
5
+
6
+ ## Contracts
7
+
8
+ Extend visible system world classes with ordinary project classes and
9
+ overrides. Never copy `@system` into project source. A layer link is a
10
+ concrete project subclass of `NeoTileLayerLink` or `NeoObjectLayerLink` that
11
+ resolves exactly one `targetLayer` relation; add no value-level target
12
+ metadata and don't ask painting operations to create or repair relation
13
+ targets.
14
+
15
+ ## Tiles
16
+
17
+ A placement is a concrete `NeoTile` value with a `Cell`, listed directly in a
18
+ layer link's `Tiles`:
19
+
20
+ ```neo
21
+ new GlassFloorTile { Cell = new(9, -4) }
22
+ ```
23
+
24
+ - `Name`, `Sprite`, and `SmartTile` are class-level `readonly` members; tiles
25
+ render per class, so override them on a subclass. Push rejects placements
26
+ that set them. `Cell` is the writable field.
27
+ - `tile.TryConvert<GlassFloorTile>()` or `tile.TryConvert(nextTile)` changes a
28
+ writable placement's class, keeping its row and cell. The second form uses
29
+ the value's runtime class without copying fields. Both return `false` when
30
+ placement validation (grid imports, compatible-layer relations) rejects it.
31
+ - `Tiles.Add` and `Tiles.Remove` edit placement data directly; the SDK
32
+ validates and re-renders from the data changes. Flattened SDK layers also
33
+ have `TrySetTile<T>(cell)`.
34
+
35
+ ## Objects
36
+
37
+ - `PlacementTiles` is a `List<NeoPlacementTile>`, each marking a footprint
38
+ cell with its own `Cell`. It is not `NeoTile`; rendered tile classes don't
39
+ belong there. An object with no placement tiles occupies no cells, blocks
40
+ nothing, and isn't returned by cell queries, but still has a `Cell`.
41
+ - Spawn with `TrySpawn(cell, constructedObject)`; adoption keeps the object's
42
+ identity. Clone an owned object explicitly before placing another copy.
43
+ - `NeoSpriteObject` extends `NeoObject`. Place it like any object, give it
44
+ `Children`, and override `OnObjectSpawned`/`OnObjectDespawned`. Spawn hooks
45
+ run parent first, despawn children first. `NeoObjectBehaviour.IsRoot` is
46
+ true only on the placed root.
47
+ - Painting a different class replaces the placement with a new row. To change
48
+ configuration in place, use [variants](variants.md).
49
+ - Authored placements may carry variant and source metadata beside their
50
+ schema keys. Keep it when editing and never declare it as schema fields.
51
+ Tile placements carry only their class and `Cell`.
52
+
53
+ ## Sorting and optional children
54
+
55
+ ```neo
56
+ class Tree : NeoObject {
57
+ public virtual NeoSortingGroup? SortingGroup = null;
58
+ }
59
+ ```
60
+
61
+ - A non-null `NeoSortingGroup` makes the object and descendants sort as one
62
+ unit. `SortAtRoot` hoists a nested group to the scene root. `SortPoint` is
63
+ the Y-sort point in cells from the origin cell's corner (default
64
+ `(0.5, 0)`, bottom center); moving it never moves art. Set it per class,
65
+ variant, or frame.
66
+ - Order: sorting layer, order in layer, sort-point Y (lower draws in front),
67
+ then document order. `NeoSpriteObject.SortingOrder` is an offset added to
68
+ the layer order, not a replacement. Sprites also expose `FlipX`, `FlipY`,
69
+ and `MaskInteraction`.
70
+ - `NeoObjectBase.Enabled = false` hides an object and its subtree without
71
+ removing its `Children` row: no rendering or collision, but values and
72
+ animation keep running. Runtime structural children aren't supported, so
73
+ author known optional slots and toggle `Enabled`.
74
+
75
+ ## Structured leaves
76
+
77
+ Addressable fields: `SpriteInfo.FileId` and `SliceIndex` (PascalCase), vector
78
+ components `x`, `y`, `z`, `w` where the type has them, and Color `r`, `g`,
79
+ `b`, `a` (lowercase).
80
+
81
+ ```neo
82
+ Sprite = Images.Hero.Slice(0);
83
+ Placeholder = SpriteInfo.Empty;
84
+ Position = new(0, 0, 0);
85
+ Tint = Color(1, 1, 1, 1);
86
+
87
+ Leg.Sprite.SliceIndex = 2; // in NeoScript, when storage permits
88
+ Leg.Position.y = 0.25;
89
+ ```
90
+
91
+ - Use `.Slice(index)` for a complete `SpriteInfo`. `SpriteInfo.Empty` is the
92
+ canonical non-null empty sprite (no file, slice zero); use `null` only for
93
+ `SpriteInfo?`.
94
+ - Field writes are read-modify-write of the whole leaf. Unknown fields,
95
+ missing vector components, non-integer values on int vectors, and color
96
+ channels outside [0, 1] are rejected.
97
+
98
+ ## References
99
+
100
+ - [Grids and cell patterns](grids-and-cell-patterns.md): `NeoCellPattern`,
101
+ `GetObjects`, `GetTile(s)`, `GetArea`.
102
+ - [Variants](variants.md): `NeoVariant`, folders, lookup variants,
103
+ `Initialize`/`ToVariant`.
104
+ - [Animation](animation.md): partial overrides, child selectors, segments,
105
+ tracks, animated colliders.