@dcl-regenesislabs/opendcl 0.2.1-26165320302.commit-e6effe4 → 0.2.1-26238928766.commit-28648d7

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 (45) hide show
  1. package/README.md +5 -3
  2. package/context/sdk7-cheat-sheet.md +4 -0
  3. package/dist/index.js +0 -12
  4. package/dist/index.js.map +1 -1
  5. package/extensions/dcl-init.ts +58 -6
  6. package/package.json +3 -3
  7. package/prompts/system.md +71 -41
  8. package/skills/add-3d-models/SKILL.md +120 -70
  9. package/skills/add-interactivity/SKILL.md +74 -2
  10. package/skills/advanced-input/SKILL.md +34 -1
  11. package/skills/advanced-rendering/SKILL.md +82 -9
  12. package/skills/animations-tweens/SKILL.md +203 -98
  13. package/skills/audio-analysis/SKILL.md +164 -0
  14. package/skills/audio-video/SKILL.md +184 -83
  15. package/skills/build-ui/SKILL.md +25 -2
  16. package/skills/camera-control/SKILL.md +78 -7
  17. package/skills/create-scene/SKILL.md +56 -13
  18. package/skills/deploy-scene/SKILL.md +12 -0
  19. package/skills/deploy-worlds/SKILL.md +35 -0
  20. package/skills/editor-gizmo/.gitignore +11 -0
  21. package/skills/editor-gizmo/SKILL.md +222 -0
  22. package/skills/editor-gizmo/src/__editor/camera.ts +277 -0
  23. package/skills/editor-gizmo/src/__editor/discovery.ts +186 -0
  24. package/skills/editor-gizmo/src/__editor/drag.ts +265 -0
  25. package/skills/editor-gizmo/src/__editor/gizmo.ts +496 -0
  26. package/skills/editor-gizmo/src/__editor/history.ts +72 -0
  27. package/skills/editor-gizmo/src/__editor/index.ts +137 -0
  28. package/skills/editor-gizmo/src/__editor/input.ts +55 -0
  29. package/skills/editor-gizmo/src/__editor/math-utils.ts +114 -0
  30. package/skills/editor-gizmo/src/__editor/persistence.ts +113 -0
  31. package/skills/editor-gizmo/src/__editor/selection.ts +157 -0
  32. package/skills/editor-gizmo/src/__editor/state.ts +117 -0
  33. package/skills/editor-gizmo/src/__editor/ui.tsx +697 -0
  34. package/skills/game-design/SKILL.md +1 -2
  35. package/skills/lighting-environment/SKILL.md +103 -56
  36. package/skills/multiplayer-sync/SKILL.md +31 -2
  37. package/skills/nft-blockchain/SKILL.md +45 -40
  38. package/skills/npcs/SKILL.md +180 -0
  39. package/skills/optimize-scene/SKILL.md +7 -2
  40. package/skills/particle-system/SKILL.md +222 -0
  41. package/skills/player-avatar/SKILL.md +133 -7
  42. package/skills/player-physics/SKILL.md +93 -0
  43. package/skills/scene-runtime/SKILL.md +9 -5
  44. package/skills/visual-feedback/SKILL.md +1 -0
  45. package/extensions/dcl-setup-ollama.ts +0 -312
@@ -5,27 +5,30 @@ description: Add 3D models (.glb/.gltf) to a Decentraland scene using GltfContai
5
5
 
6
6
  # Adding 3D Models to Decentraland Scenes
7
7
 
8
- ## Loading a 3D Model
8
+ ## Where models go: `main-entities.ts`
9
9
 
10
- Use `GltfContainer` to load `.glb` or `.gltf` files:
10
+ A 3D model placed at author time is a static visible entity. **Declare it in `main-entities.ts`**, not via `engine.addEntity()` in `src/index.ts`. The build compiles `main-entities.ts` into `main.crdt`, the engine preloads it before `main()` runs, and the editor can drag/rotate the model interactively.
11
11
 
12
12
  ```typescript
13
- import { engine, Transform, GltfContainer, ColliderLayer } from '@dcl/sdk/ecs'
14
- import { Vector3, Quaternion } from '@dcl/sdk/math'
15
-
16
- const model = engine.addEntity()
17
- Transform.create(model, {
18
- position: Vector3.create(8, 0, 8),
19
- rotation: Quaternion.fromEulerDegrees(0, 0, 0),
20
- scale: Vector3.create(1, 1, 1)
21
- })
22
- GltfContainer.create(model, {
23
- src: 'models/myModel.glb',
24
- visibleMeshesCollisionMask: ColliderLayer.CL_PHYSICS | ColliderLayer.CL_POINTER
25
- })
13
+ // main-entities.ts
14
+ import type { Scene } from '@dcl/sdk/scene-types'
15
+
16
+ export const scene = {
17
+ my_model: {
18
+ components: {
19
+ Transform: { position: { x: 8, y: 0, z: 8 } },
20
+ GltfContainer: {
21
+ src: 'models/myModel.glb',
22
+ visibleMeshesCollisionMask: 3 // CL_PHYSICS | CL_POINTER
23
+ }
24
+ }
25
+ }
26
+ } satisfies Scene
26
27
  ```
27
28
 
28
- > **Always set `visibleMeshesCollisionMask`** when loading models. Catalog models don't include separate collider meshes — using the visible mesh as the collider ensures the model is solid and clickable.
29
+ > **Always set `visibleMeshesCollisionMask`** on `GltfContainer`. Catalog models don't include separate collider meshes — using the visible mesh as the collider ensures the model is solid and clickable. Use the integer value (`ColliderLayer.CL_PHYSICS = 1`, `CL_POINTER = 2`, both = `3`) inside `main-entities.ts` since enums aren't allowed in the literal.
30
+
31
+ **When to use `engine.addEntity()` in `src/index.ts` instead**: only when the model is spawned dynamically (procedurally placed in a loop, dropped on an event, gated by NFT ownership, etc.). For static props, always use `main-entities.ts`.
29
32
 
30
33
  ## File Organization
31
34
 
@@ -46,52 +49,80 @@ project/
46
49
  ## Colliders
47
50
 
48
51
  ### Using Model's Built-in Colliders
49
- Models exported with collision meshes work automatically. Set the collision mask:
52
+
50
53
  ```typescript
51
- GltfContainer.create(model, {
52
- src: 'models/building.glb',
53
- visibleMeshesCollisionMask: ColliderLayer.CL_PHYSICS | ColliderLayer.CL_POINTER,
54
- invisibleMeshesCollisionMask: ColliderLayer.CL_PHYSICS
55
- })
54
+ // main-entities.ts — declared inline with GltfContainer
55
+ building: {
56
+ components: {
57
+ Transform: { position: { x: 8, y: 0, z: 8 } },
58
+ GltfContainer: {
59
+ src: 'models/building.glb',
60
+ visibleMeshesCollisionMask: 3, // CL_PHYSICS | CL_POINTER
61
+ invisibleMeshesCollisionMask: 1 // CL_PHYSICS
62
+ }
63
+ }
64
+ }
56
65
  ```
57
66
 
58
67
  ### Adding Simple Colliders
59
- For basic shapes, add `MeshCollider`:
68
+
69
+ For basic shapes (no GLTF), add `MeshCollider`:
70
+
60
71
  ```typescript
61
- import { MeshCollider } from '@dcl/sdk/ecs'
62
- MeshCollider.setBox(model) // Box collider
63
- MeshCollider.setSphere(model) // Sphere collider
72
+ // main-entities.ts
73
+ invisible_wall: {
74
+ components: {
75
+ Transform: { position: { x: 0, y: 1, z: 8 }, scale: { x: 0.1, y: 2, z: 16 } },
76
+ MeshCollider: { mesh: { $case: 'box', box: { uvs: [] } } }
77
+ }
78
+ }
64
79
  ```
65
80
 
81
+ Other shapes: `{ $case: 'sphere', sphere: {} }`, `{ $case: 'plane', plane: { uvs: [] } }`, `{ $case: 'cylinder', cylinder: {} }`.
82
+
83
+ ## ⚠️ Important: Never Pass `undefined` in Transform Fields
84
+
85
+ The SDK serializer crashes if any Transform field (`position`, `rotation`, `scale`) is present but `undefined`. **Omit the key entirely** instead — both in `main-entities.ts` literals and in any runtime helpers. In `main-entities.ts` this is natural (you just don't write the field).
86
+
66
87
  ## Common Model Operations
67
88
 
68
89
  ### Scaling
69
- ```typescript
70
- Transform.create(model, {
71
- position: Vector3.create(8, 0, 8),
72
- scale: Vector3.create(2, 2, 2) // 2x size
73
- })
74
- ```
75
90
 
76
- ### Rotation
77
91
  ```typescript
78
- Transform.create(model, {
79
- position: Vector3.create(8, 0, 8),
80
- rotation: Quaternion.fromEulerDegrees(0, 90, 0) // Rotate 90° on Y axis
81
- })
92
+ // main-entities.ts
93
+ big_statue: {
94
+ components: {
95
+ Transform: { position: { x: 8, y: 0, z: 8 }, scale: { x: 2, y: 2, z: 2 } },
96
+ GltfContainer: { src: 'models/statue.glb' }
97
+ }
98
+ }
82
99
  ```
83
100
 
84
- ### Parenting (Attach to Another Entity)
85
- ```typescript
86
- const parent = engine.addEntity()
87
- Transform.create(parent, { position: Vector3.create(8, 0, 8) })
101
+ ### Rotation (Euler angles converted to a quaternion at author time)
102
+
103
+ Quaternions are `{ x, y, z, w }`. For `Quaternion.fromEulerDegrees(0, 90, 0)` the equivalent literal is `{ x: 0, y: 0.7071, z: 0, w: 0.7071 }`. If you need exact-degree rotations and don't want to compute by hand, set `rotation` to identity in `main-entities.ts` and rotate at runtime in `src/index.ts` using `Transform.getMutable(entity).rotation = Quaternion.fromEulerDegrees(0, 90, 0)`.
104
+
105
+ ### Parenting
106
+
107
+ Reference the parent by **name** (a string key from the same `scene` object). The build resolves names to entity IDs in a second pass.
88
108
 
89
- const child = engine.addEntity()
90
- Transform.create(child, {
91
- position: Vector3.create(0, 2, 0), // 2m above parent
92
- parent: parent
93
- })
94
- GltfContainer.create(child, { src: 'models/hat.glb' })
109
+ ```typescript
110
+ // main-entities.ts
111
+ character: {
112
+ components: {
113
+ Transform: { position: { x: 8, y: 0, z: 8 } },
114
+ GltfContainer: { src: 'models/character.glb' }
115
+ }
116
+ },
117
+ hat: {
118
+ components: {
119
+ Transform: {
120
+ position: { x: 0, y: 2, z: 0 }, // 2m above parent's origin
121
+ parent: 'character'
122
+ },
123
+ GltfContainer: { src: 'models/hat.glb' }
124
+ }
125
+ }
95
126
  ```
96
127
 
97
128
  ## Free 3D Models — OpenDCL Catalog (5,700+ models)
@@ -146,19 +177,34 @@ curl -o models/zombie-purple.glb "https://models.dclregenesislabs.xyz/blobs/bafy
146
177
  ```
147
178
 
148
179
  ```typescript
149
- // Use in code with animations
150
- import { engine, Transform, GltfContainer, Animator } from '@dcl/sdk/ecs'
151
- import { Vector3 } from '@dcl/sdk/math'
180
+ // main-entities.ts declare the entity with all its initial state
181
+ import type { Scene } from '@dcl/sdk/scene-types'
182
+
183
+ export const scene = {
184
+ zombie: {
185
+ components: {
186
+ Transform: { position: { x: 8, y: 0, z: 8 } },
187
+ GltfContainer: { src: 'models/zombie-purple.glb' },
188
+ Animator: {
189
+ states: [
190
+ { clip: 'ZombieWalk', playing: true, loop: true },
191
+ { clip: 'ZombieAttack', playing: false, loop: false }
192
+ ]
193
+ }
194
+ }
195
+ }
196
+ } satisfies Scene
197
+ ```
198
+
199
+ To switch animations at runtime (e.g., trigger attack on click), use `src/index.ts`:
152
200
 
153
- const zombie = engine.addEntity()
154
- Transform.create(zombie, { position: Vector3.create(8, 0, 8) })
155
- GltfContainer.create(zombie, { src: 'models/zombie-purple.glb' })
156
- Animator.create(zombie, {
157
- states: [
158
- { clip: 'ZombieWalk', playing: true, loop: true },
159
- { clip: 'ZombieAttack', playing: false, loop: false }
160
- ]
161
- })
201
+ ```typescript
202
+ import { engine, Animator } from '@dcl/sdk/ecs'
203
+
204
+ export function main() {
205
+ const zombie = engine.getEntityOrNullByName('zombie')
206
+ if (zombie) Animator.playSingleAnimation(zombie, 'ZombieAttack')
207
+ }
162
208
  ```
163
209
 
164
210
  > **Important**: `GltfContainer` only works with **local files**. Never use external URLs for the model `src` field. Always download models into `models/` first.
@@ -166,19 +212,23 @@ Animator.create(zombie, {
166
212
 
167
213
  ### Checking Model Load State
168
214
 
169
- Use `GltfContainerLoadingState` to check if a model has finished loading:
215
+ Load-state polling is runtime behavior — put it in `src/index.ts` and reference the entity by name:
170
216
 
171
217
  ```typescript
172
- import { GltfContainer, GltfContainerLoadingState, LoadingState } from '@dcl/sdk/ecs'
173
-
174
- engine.addSystem(() => {
175
- const state = GltfContainerLoadingState.getOrNull(modelEntity)
176
- if (state && state.currentState === LoadingState.FINISHED) {
177
- console.log('Model loaded successfully')
178
- } else if (state && state.currentState === LoadingState.FINISHED_WITH_ERROR) {
179
- console.log('Model failed to load')
180
- }
181
- })
218
+ import { engine, GltfContainerLoadingState, LoadingState } from '@dcl/sdk/ecs'
219
+
220
+ export function main() {
221
+ engine.addSystem(() => {
222
+ const model = engine.getEntityOrNullByName('zombie')
223
+ if (!model) return
224
+ const state = GltfContainerLoadingState.getOrNull(model)
225
+ if (state?.currentState === LoadingState.FINISHED) {
226
+ console.log('Model loaded successfully')
227
+ } else if (state?.currentState === LoadingState.FINISHED_WITH_ERROR) {
228
+ console.log('Model failed to load')
229
+ }
230
+ })
231
+ }
182
232
  ```
183
233
 
184
234
  ## Troubleshooting
@@ -5,6 +5,37 @@ description: Add click handlers, hover effects, pointer events, trigger areas, r
5
5
 
6
6
  # Adding Interactivity to Decentraland Scenes
7
7
 
8
+ ## Authoring split
9
+
10
+ The clickable entity (cube, button, model) is static — declare it in `main-entities.ts` with its Transform / Mesh / Material. The clickability itself is **always** added at runtime in `src/index.ts` via `pointerEventsSystem.onPointerDown(...)` (or the related helpers). The helper writes the `PointerEvents` component AND registers the callback in a single call — do NOT also declare `PointerEvents` in `main-entities.ts`; the helper would just overwrite it and the duplication invites drift.
11
+
12
+ ```typescript
13
+ // main-entities.ts — entity only, no PointerEvents
14
+ clickable_cube: {
15
+ components: {
16
+ Transform: { position: { x: 8, y: 1, z: 8 } },
17
+ MeshRenderer: { mesh: { $case: 'box', box: { uvs: [] } } }
18
+ }
19
+ }
20
+ ```
21
+
22
+ ```typescript
23
+ // src/index.ts — register clickability via the helper system
24
+ import { engine, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
25
+
26
+ export function main() {
27
+ const cube = engine.getEntityOrNullByName('clickable_cube')
28
+ if (cube) {
29
+ pointerEventsSystem.onPointerDown(
30
+ { entity: cube, opts: { button: InputAction.IA_POINTER, hoverText: 'Open' } },
31
+ () => { /* what happens on click */ }
32
+ )
33
+ }
34
+ }
35
+ ```
36
+
37
+ `TriggerArea` and `Raycast` are also runtime — they live in `src/index.ts`. Code examples below that create entities inline with `engine.addEntity()` are for runtime/technical entities (raycast probes, trigger volumes generated from data); for static clickable props, declare the prop in `main-entities.ts` and attach handlers in `src/index.ts` as above.
38
+
8
39
  ## Decision Tree
9
40
 
10
41
  | Need | Approach | API |
@@ -89,10 +120,10 @@ pointerEventsSystem.removeOnPointerUp(cube)
89
120
  ```
90
121
 
91
122
  ### Important: Colliders Required
92
- Pointer events only work on entities with a **collider**. Add one if your entity doesn't have a mesh:
123
+ Pointer events only work on entities with a **collider on the `CL_POINTER` layer**. Add one if your entity doesn't have a mesh:
93
124
  ```typescript
94
125
  import { MeshCollider } from '@dcl/sdk/ecs'
95
- MeshCollider.setBox(entity) // Invisible box collider
126
+ MeshCollider.setBox(entity) // Invisible box collider — defaults include CL_POINTER
96
127
  ```
97
128
 
98
129
  For GLTF models, set the collision mask:
@@ -105,6 +136,47 @@ GltfContainer.create(entity, {
105
136
 
106
137
  ---
107
138
 
139
+ ## Proximity Events (Pointer-Free Triggers)
140
+
141
+ Like `pointerEventsSystem.onPointerDown`, but fires based on **player distance** to the entity instead of a click. Useful for "press E when near" interactions and signposts that highlight on approach. No collider required — the system polls the player position vs the entity transform.
142
+
143
+ ```typescript
144
+ import { engine, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
145
+
146
+ const door = engine.getEntityOrNullByName('shop_door')
147
+ if (door) {
148
+ pointerEventsSystem.onProximityDown(
149
+ {
150
+ entity: door,
151
+ opts: {
152
+ button: InputAction.IA_PRIMARY,
153
+ hoverText: 'Open shop',
154
+ maxPlayerDistance: 3 // metres
155
+ }
156
+ },
157
+ () => { /* run when the player presses the button within range */ }
158
+ )
159
+
160
+ pointerEventsSystem.onProximityEnter(
161
+ { entity: door, opts: { maxPlayerDistance: 5 } },
162
+ () => { /* fired once when the player enters the radius */ }
163
+ )
164
+
165
+ pointerEventsSystem.onProximityLeave(
166
+ { entity: door, opts: { maxPlayerDistance: 5 } },
167
+ () => { /* fired once when the player leaves the radius */ }
168
+ )
169
+ }
170
+ ```
171
+
172
+ - `maxPlayerDistance` is required and is measured from the **avatar root**, not the camera.
173
+ - `priority` (number) — if multiple proximity events overlap, the higher value wins.
174
+ - Remove with `pointerEventsSystem.removeOnProximityDown(entity)` etc.
175
+
176
+ Prefer **proximity events** over `pointerEventsSystem.onPointerDown` when the entity has no visible collider or when the player shouldn't need to aim at it (signs, doors that just open when approached, etc.).
177
+
178
+ ---
179
+
108
180
  ## Trigger Areas (Proximity Detection)
109
181
 
110
182
  Detect when the player enters, exits, or stays inside an area:
@@ -83,6 +83,8 @@ engine.addSystem(myInputSystem)
83
83
 
84
84
  ### Global Input Checks
85
85
 
86
+ Fires regardless of whether the player's cursor was over an entity.
87
+
86
88
  ```typescript
87
89
  function globalInputSystem() {
88
90
  // Was the key just pressed this frame?
@@ -99,6 +101,31 @@ function globalInputSystem() {
99
101
  engine.addSystem(globalInputSystem)
100
102
  ```
101
103
 
104
+ ### Tag-Based Input Batching
105
+
106
+ If you have many similar entities that all respond to the same input (e.g., every barrel responds to E to break), tag them via the `Tag` component and iterate the tag query each frame:
107
+
108
+ ```typescript
109
+ import { engine, Tag, inputSystem, InputAction, PointerEventType } from '@dcl/sdk/ecs'
110
+
111
+ // At setup: tag the entities (or declare Tag in main-entities.ts).
112
+ for (const barrel of [b1, b2, b3]) {
113
+ Tag.createOrReplace(barrel, { value: 'breakable' })
114
+ }
115
+
116
+ engine.addSystem(() => {
117
+ for (const [entity] of engine.getEntitiesByTag('breakable')) {
118
+ const cmd = inputSystem.getInputCommand(InputAction.IA_PRIMARY, PointerEventType.PET_DOWN, entity)
119
+ if (cmd) {
120
+ // entity was the IA_PRIMARY target this frame
121
+ engine.removeEntity(entity)
122
+ }
123
+ }
124
+ })
125
+ ```
126
+
127
+ Cleaner than registering N individual `pointerEventsSystem.onPointerDown` handlers when the behavior is uniform.
128
+
102
129
  ## All InputAction Values
103
130
 
104
131
  | InputAction | Key/Button |
@@ -138,11 +165,15 @@ InputModifier.create(engine.PlayerEntity, {
138
165
  mode: InputModifier.Mode.Standard({ disableAll: true })
139
166
  })
140
167
 
141
- // Restrict specific movement
168
+ // Restrict specific movement (every flag is optional and defaults to false)
142
169
  InputModifier.createOrReplace(engine.PlayerEntity, {
143
170
  mode: InputModifier.Mode.Standard({
171
+ disableWalk: false,
172
+ disableJog: false,
144
173
  disableRun: true,
145
174
  disableJump: true,
175
+ disableDoubleJump: true,
176
+ disableGliding: true,
146
177
  disableEmote: true
147
178
  })
148
179
  })
@@ -151,6 +182,8 @@ InputModifier.createOrReplace(engine.PlayerEntity, {
151
182
  InputModifier.deleteFrom(engine.PlayerEntity)
152
183
  ```
153
184
 
185
+ While restricted: gravity still applies, the camera still rotates, and pointer / proximity events still fire. All restrictions auto-lift when the player leaves the scene.
186
+
154
187
  **Important:** InputModifier only works in the DCL 2.0 desktop client. It has no effect in the web browser explorer.
155
188
 
156
189
  ### Cutscene Pattern
@@ -5,6 +5,22 @@ description: Advanced rendering in Decentraland scenes. Billboard (face camera),
5
5
 
6
6
  # Advanced Rendering in Decentraland
7
7
 
8
+ ## Authoring split
9
+
10
+ `Billboard`, `TextShape`, `Material`, `MeshRenderer`, `GltfContainer`, `VisibilityComponent`, and `GltfNodeModifiers` are **all supported in `main-entities.ts`** — declare the visual entity (sign, label, billboard, glowing prop, model with per-node overrides) fully there with its visual components. Examples below that show `engine.addEntity()` followed by `Transform.create` + `MeshRenderer` / `Billboard` / `TextShape` are pre-`main-entities.ts` patterns — translate them by moving the entity declaration into `main-entities.ts` and keeping only runtime modifications (e.g., `VisibilityComponent.getMutable(entity).visible = false`) in `src/index.ts`.
11
+
12
+ ```typescript
13
+ // Example: floating label
14
+ // main-entities.ts
15
+ shop_label: {
16
+ components: {
17
+ Transform: { position: { x: 8, y: 3, z: 8 } },
18
+ TextShape: { text: 'OPEN', fontSize: 4, textColor: { r: 1, g: 1, b: 1, a: 1 } },
19
+ Billboard: { billboardMode: 7 } // BM_ALL
20
+ }
21
+ }
22
+ ```
23
+
8
24
  ## When to Use Which Rendering Feature
9
25
 
10
26
  | Need | Component | When |
@@ -230,21 +246,78 @@ engine.addSystem(lodSystem)
230
246
 
231
247
  ### Per-Node Modifiers (GltfNodeModifiers)
232
248
 
233
- Override material or shadow casting on specific nodes within a GLTF model:
249
+ Override material or shadow casting on specific nodes within a GLTF model. Supported in `main-entities.ts`:
250
+
251
+ ```typescript
252
+ // main-entities.ts
253
+ armored_knight: {
254
+ components: {
255
+ Transform: { position: { x: 8, y: 0, z: 8 } },
256
+ GltfContainer: { src: 'models/knight.glb' },
257
+ GltfNodeModifiers: {
258
+ modifiers: [
259
+ {
260
+ path: 'RootNode/Armor', // GLTF hierarchy path
261
+ castShadows: false // disable shadows for this node
262
+ }
263
+ ]
264
+ }
265
+ }
266
+ }
267
+ ```
268
+
269
+ **Whole-model override**: pass `path: ''` (empty string) to apply the modifier to every node in the model. Useful for re-skinning an entire model with a single material swap:
234
270
 
235
271
  ```typescript
236
- import { GltfNodeModifiers } from '@dcl/sdk/ecs'
272
+ red_team_unit: {
273
+ components: {
274
+ Transform: { position: { x: 4, y: 0, z: 8 } },
275
+ GltfContainer: { src: 'models/unit.glb' },
276
+ GltfNodeModifiers: {
277
+ modifiers: [
278
+ {
279
+ path: '',
280
+ material: {
281
+ material: {
282
+ $case: 'pbr',
283
+ pbr: { albedoColor: { r: 1, g: 0, b: 0, a: 1 } }
284
+ }
285
+ }
286
+ }
287
+ ]
288
+ }
289
+ }
290
+ }
291
+ ```
292
+
293
+ ### Texture Tweens (Animate Material Textures)
294
+
295
+ Animate a material's texture offset/tiling over time — useful for water, lava, conveyor belts, scrolling backgrounds. Tween component is supported in `main-entities.ts`:
237
296
 
238
- GltfNodeModifiers.create(entity, {
239
- modifiers: [
240
- {
241
- path: 'RootNode/Armor', // GLTF hierarchy path
242
- castShadows: false // Disable shadow casting for this node
297
+ ```typescript
298
+ // main-entities.ts — continuous texture scroll
299
+ conveyor: {
300
+ components: {
301
+ Transform: { position: { x: 8, y: 1, z: 8 } },
302
+ MeshRenderer: { mesh: { $case: 'plane', plane: { uvs: [] } } },
303
+ Material: { material: { $case: 'pbr', pbr: { texture: { tex: { $case: 'texture', texture: { src: 'images/belt.png' } } } } } },
304
+ Tween: {
305
+ duration: 2000,
306
+ easingFunction: 0, // EF_LINEAR
307
+ mode: {
308
+ $case: 'textureMoveContinuous',
309
+ textureMoveContinuous: { direction: { x: 1, y: 0 }, speed: 0.5 }
310
+ // movementType defaults to 0 = TMT_OFFSET; use 1 = TMT_TILING to scale instead
311
+ }
243
312
  }
244
- ]
245
- })
313
+ }
314
+ }
246
315
  ```
247
316
 
317
+ For a finite from-to texture move, use `{ $case: 'textureMove', textureMove: { start: { x: 0, y: 0 }, end: { x: 1, y: 0 }, movementType: 0 } }` with a `duration`.
318
+
319
+ Runtime helpers (in `src/index.ts`): `Tween.setTextureMove(entity, from, to, durationMs)` and `Tween.setTextureMoveContinuous(entity, direction, speed)`.
320
+
248
321
  ### Avatar Texture
249
322
 
250
323
  Generate a texture from a player's avatar: