@dcl-regenesislabs/opendcl 0.2.1 → 0.2.2-26850672477.commit-99ffd91

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 +210 -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 +138 -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 +699 -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,6 +5,11 @@ description: Add sound effects, music, audio streaming, and video players to Dec
5
5
 
6
6
  # Audio and Video in Decentraland
7
7
 
8
+ ## Authoring split
9
+
10
+ - **`AudioSource`** (local audio files), **`AudioStream`** (streaming URLs), and **`VideoPlayer`** are all supported in `main-entities.ts` — declare the speaker / radio / screen entity fully there with the streaming/playback config.
11
+ - Volume / play / pause toggles at runtime happen in `src/index.ts` via `getMutable`.
12
+
8
13
  ## When to Use Which Media Component
9
14
 
10
15
  | Need | Component | Key Difference |
@@ -20,22 +25,26 @@ description: Add sound effects, music, audio streaming, and video players to Dec
20
25
 
21
26
  ## Audio Source (Sound Effects & Music)
22
27
 
23
- Play audio clips from files:
28
+ Declare the speaker in `main-entities.ts`:
24
29
 
25
30
  ```typescript
26
- import { engine, Transform, AudioSource } from '@dcl/sdk/ecs'
27
- import { Vector3 } from '@dcl/sdk/math'
28
-
29
- const speaker = engine.addEntity()
30
- Transform.create(speaker, { position: Vector3.create(8, 1, 8) })
31
-
32
- AudioSource.create(speaker, {
33
- audioClipUrl: 'sounds/music.mp3',
34
- playing: true,
35
- loop: true,
36
- volume: 0.5, // 0 to 1
37
- pitch: 1.0 // Playback speed (0.5 = half speed, 2.0 = double)
38
- })
31
+ // main-entities.ts
32
+ import type { Scene } from '@dcl/sdk/scene-types'
33
+
34
+ export const scene = {
35
+ speaker: {
36
+ components: {
37
+ Transform: { position: { x: 8, y: 1, z: 8 } },
38
+ AudioSource: {
39
+ audioClipUrl: 'sounds/music.mp3',
40
+ playing: true,
41
+ loop: true,
42
+ volume: 0.5, // 0 to 1
43
+ pitch: 1.0 // Playback speed (0.5 = half speed, 2.0 = double)
44
+ }
45
+ }
46
+ }
47
+ } satisfies Scene
39
48
  ```
40
49
 
41
50
  ### Supported Formats
@@ -43,6 +52,26 @@ AudioSource.create(speaker, {
43
52
  - `.ogg`
44
53
  - `.wav`
45
54
 
55
+ ### Spatial vs Non-Spatial Audio
56
+
57
+ `AudioSource` defaults to spatial (volume falls off with distance). For background music / radio / non-positional sound effects, set `global: true`:
58
+
59
+ ```typescript
60
+ // main-entities.ts
61
+ bg_music: {
62
+ components: {
63
+ Transform: { position: { x: 0, y: 0, z: 0 } }, // ignored when global
64
+ AudioSource: {
65
+ audioClipUrl: 'sounds/bg.mp3',
66
+ playing: true,
67
+ loop: true,
68
+ volume: 0.5,
69
+ global: true // heard everywhere in the scene at constant volume
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
46
75
  ### File Organization
47
76
  ```
48
77
  project/
@@ -55,97 +84,127 @@ project/
55
84
  └── scene.json
56
85
  ```
57
86
 
58
- ### Play/Stop/Toggle
87
+ ### Play/Stop/Toggle (runtime, in `src/index.ts`)
59
88
  ```typescript
60
- // Play
61
- AudioSource.getMutable(speaker).playing = true
89
+ import { engine, AudioSource } from '@dcl/sdk/ecs'
90
+
91
+ export function main() {
92
+ const speaker = engine.getEntityOrNullByName('speaker')
93
+ if (!speaker) return
62
94
 
63
- // Stop
64
- AudioSource.getMutable(speaker).playing = false
95
+ AudioSource.getMutable(speaker).playing = true // play
96
+ AudioSource.getMutable(speaker).playing = false // stop
65
97
 
66
- // Toggle
67
- const audio = AudioSource.getMutable(speaker)
68
- audio.playing = !audio.playing
98
+ // toggle
99
+ const audio = AudioSource.getMutable(speaker)
100
+ audio.playing = !audio.playing
101
+ }
69
102
  ```
70
103
 
71
104
  ### Play on Click
72
- ```typescript
73
- import { pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
74
-
75
- const button = engine.addEntity()
76
- // ... set up transform and mesh ...
77
-
78
- const audioEntity = engine.addEntity()
79
- Transform.create(audioEntity, { position: Vector3.create(8, 1, 8) })
80
- AudioSource.create(audioEntity, {
81
- audioClipUrl: 'sounds/click.mp3',
82
- playing: false,
83
- loop: false,
84
- volume: 0.8
85
- })
86
105
 
87
- pointerEventsSystem.onPointerDown(
88
- { entity: button, opts: { button: InputAction.IA_POINTER, hoverText: 'Play sound' } },
89
- () => {
90
- // Reset and play
91
- const audio = AudioSource.getMutable(audioEntity)
92
- audio.playing = false
93
- audio.playing = true
106
+ Static entities (the button mesh and the click-sfx speaker) go in `main-entities.ts`. `PointerEvents` and the click handler are runtime — they live in `src/index.ts`.
107
+
108
+ ```typescript
109
+ // main-entities.ts
110
+ sfx_button: {
111
+ components: {
112
+ Transform: { position: { x: 8, y: 1, z: 8 } },
113
+ MeshRenderer: { mesh: { $case: 'box', box: { uvs: [] } } }
114
+ }
115
+ },
116
+ click_sfx: {
117
+ components: {
118
+ Transform: { position: { x: 8, y: 1, z: 8 } },
119
+ AudioSource: {
120
+ audioClipUrl: 'sounds/click.mp3',
121
+ playing: false,
122
+ loop: false,
123
+ volume: 0.8
124
+ }
94
125
  }
95
- )
126
+ }
127
+ ```
128
+
129
+ ```typescript
130
+ // src/index.ts
131
+ import { engine, AudioSource, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
132
+
133
+ export function main() {
134
+ const button = engine.getEntityOrNullByName('sfx_button')
135
+ const sfx = engine.getEntityOrNullByName('click_sfx')
136
+ if (!button || !sfx) return
137
+
138
+ pointerEventsSystem.onPointerDown(
139
+ { entity: button, opts: { button: InputAction.IA_POINTER, hoverText: 'Play sound' } },
140
+ () => {
141
+ // Reset and play
142
+ const audio = AudioSource.getMutable(sfx)
143
+ audio.playing = false
144
+ audio.playing = true
145
+ }
146
+ )
147
+ }
96
148
  ```
97
149
 
98
150
  ## Audio Streaming
99
151
 
100
- Stream audio from a URL (radio, live streams):
152
+ `AudioStream` is supported in `main-entities.ts` — declare the radio entity with its streaming config in one place:
101
153
 
102
154
  ```typescript
103
- import { engine, Transform, AudioStream } from '@dcl/sdk/ecs'
104
- import { Vector3 } from '@dcl/sdk/math'
105
-
106
- const radio = engine.addEntity()
107
- Transform.create(radio, { position: Vector3.create(8, 1, 8) })
108
-
109
- AudioStream.create(radio, {
110
- url: 'https://example.com/stream.mp3',
111
- playing: true,
112
- volume: 0.3
113
- })
155
+ // main-entities.ts
156
+ radio: {
157
+ components: {
158
+ Transform: { position: { x: 8, y: 1, z: 8 } },
159
+ GltfContainer: { src: 'models/radio.glb' },
160
+ AudioStream: {
161
+ url: 'https://example.com/stream.mp3',
162
+ playing: true,
163
+ volume: 0.3
164
+ }
165
+ }
166
+ }
114
167
  ```
115
168
 
169
+ Toggling play / volume at runtime is the same `getMutable` pattern as `AudioSource`.
170
+
116
171
  ## Video Player
117
172
 
118
- Play video on a surface:
173
+ `VideoPlayer`, `MeshRenderer`, and the screen Transform all go in `main-entities.ts`. The video **texture binding** in `Material` needs a runtime Entity ID, not a name — the build only resolves `Transform.parent` by name. So `Material` is set at runtime in `src/index.ts`:
119
174
 
120
175
  ```typescript
121
- import { engine, Transform, VideoPlayer, Material, MeshRenderer } from '@dcl/sdk/ecs'
122
- import { Vector3 } from '@dcl/sdk/math'
123
-
124
- // Create a screen
125
- const screen = engine.addEntity()
126
- Transform.create(screen, {
127
- position: Vector3.create(8, 3, 15.9),
128
- scale: Vector3.create(8, 4.5, 1) // 16:9 ratio
129
- })
130
- MeshRenderer.setPlane(screen)
131
-
132
- // Add video player
133
- VideoPlayer.create(screen, {
134
- src: 'https://example.com/video.mp4',
135
- playing: true,
136
- loop: true,
137
- volume: 0.5,
138
- playbackRate: 1.0,
139
- position: 0 // Start time in seconds
140
- })
176
+ // main-entities.ts
177
+ video_screen: {
178
+ components: {
179
+ Transform: {
180
+ position: { x: 8, y: 3, z: 15.9 },
181
+ scale: { x: 8, y: 4.5, z: 1 } // 16:9 ratio
182
+ },
183
+ MeshRenderer: { mesh: { $case: 'plane', plane: { uvs: [] } } },
184
+ VideoPlayer: {
185
+ src: 'https://example.com/video.mp4',
186
+ playing: true,
187
+ loop: true,
188
+ volume: 0.5,
189
+ playbackRate: 1.0,
190
+ position: 0 // start time in seconds
191
+ }
192
+ }
193
+ }
194
+ ```
141
195
 
142
- // Create video texture
143
- const videoTexture = Material.Texture.Video({ videoPlayerEntity: screen })
196
+ ```typescript
197
+ // src/index.ts
198
+ import { engine, Material } from '@dcl/sdk/ecs'
144
199
 
145
- // Basic material (recommended — better performance)
146
- Material.setBasicMaterial(screen, {
147
- texture: videoTexture
148
- })
200
+ export function main() {
201
+ const screen = engine.getEntityOrNullByName('video_screen')
202
+ if (!screen) return
203
+
204
+ const videoTexture = Material.Texture.Video({ videoPlayerEntity: screen })
205
+ // Basic material — better performance than PBR for video surfaces
206
+ Material.setBasicMaterial(screen, { texture: videoTexture })
207
+ }
149
208
  ```
150
209
 
151
210
  ### Video Controls
@@ -182,6 +241,48 @@ Material.setPbrMaterial(screen, {
182
241
  })
183
242
  ```
184
243
 
244
+ ### Video on a GLTF Surface (Curved Screens, TVs, Monitors)
245
+
246
+ When the "screen" is part of a model (a TV in a living room scene, a curved arena display), keep the GLTF and override its screen material with the video texture via `GltfNodeModifiers` at runtime:
247
+
248
+ ```typescript
249
+ // main-entities.ts — declare the TV model
250
+ tv: {
251
+ components: {
252
+ Transform: { position: { x: 8, y: 1.5, z: 8 } },
253
+ GltfContainer: { src: 'models/tv.glb' },
254
+ VideoPlayer: { src: 'https://example.com/show.mp4', playing: true, loop: true }
255
+ }
256
+ }
257
+ ```
258
+
259
+ ```typescript
260
+ // src/index.ts — bind the video texture to the screen sub-mesh by path
261
+ import { engine, Material, GltfNodeModifiers } from '@dcl/sdk/ecs'
262
+
263
+ export function main() {
264
+ const tv = engine.getEntityOrNullByName('tv')
265
+ if (!tv) return
266
+
267
+ const videoTexture = Material.Texture.Video({ videoPlayerEntity: tv })
268
+ GltfNodeModifiers.createOrReplace(tv, {
269
+ modifiers: [
270
+ {
271
+ path: 'TV/Screen', // GLTF node path to the screen sub-mesh
272
+ material: {
273
+ material: {
274
+ $case: 'unlit',
275
+ unlit: { texture: videoTexture }
276
+ }
277
+ }
278
+ }
279
+ ]
280
+ })
281
+ }
282
+ ```
283
+
284
+ Use `path: ''` (empty) to apply the video material to every node of the model — useful when the whole model is the screen (e.g., a flat billboard mesh exported from Blender).
285
+
185
286
  ### Video Events
186
287
 
187
288
  Monitor video playback state:
@@ -38,7 +38,11 @@ const MyUI = () => (
38
38
  )
39
39
 
40
40
  export function setupUi() {
41
- ReactEcsRenderer.setUiRenderer(MyUI)
41
+ // ALWAYS pass virtualWidth + virtualHeight — the renderer scales the layout
42
+ // to fit the player's window using these as the reference. Without them,
43
+ // sizes are interpreted in raw pixels and won't behave consistently across
44
+ // resolutions and aspect ratios.
45
+ ReactEcsRenderer.setUiRenderer(MyUI, { virtualWidth: 1920, virtualHeight: 1080 })
42
46
  }
43
47
  ```
44
48
 
@@ -304,6 +308,25 @@ The `Dropdown` component supports additional props:
304
308
  />
305
309
  ```
306
310
 
311
+ ### Multiple UI Modules (`addUiRenderer` / `removeUiRenderer`)
312
+
313
+ If you have several independent UI modules — e.g., a HUD, a dialog system, a debug overlay — combine them under a single root *or* use `addUiRenderer` to mount each module against an **owner entity**. When the owner entity is deleted, the UI renderer is removed automatically.
314
+
315
+ ```tsx
316
+ import { engine, ReactEcsRenderer } from '@dcl/sdk/react-ecs'
317
+
318
+ const hudOwner = engine.addEntity()
319
+ ReactEcsRenderer.addUiRenderer(hudOwner, () => <HudOverlay />)
320
+
321
+ // later, when the HUD should disappear:
322
+ engine.removeEntity(hudOwner) // also removes the UI renderer
323
+
324
+ // or explicitly:
325
+ ReactEcsRenderer.removeUiRenderer(hudOwner)
326
+ ```
327
+
328
+ Each `addUiRenderer` mount renders independently. Useful for dynamic UIs that should appear/disappear based on game state without manually conditioning every sub-tree of one giant root component.
329
+
307
330
  ## Troubleshooting
308
331
 
309
332
  | Problem | Cause | Solution |
@@ -312,7 +335,7 @@ The `Dropdown` component supports additional props:
312
335
  | UI elements overlapping | Missing `flexDirection` or wrong layout | Set `flexDirection: 'column'` on the parent container |
313
336
  | Button clicks not registering | Missing `onMouseDown` handler | Add `onMouseDown={() => { ... }}` to the Button or UiEntity |
314
337
  | JSX errors at compile time | File extension is `.ts` instead of `.tsx` | Rename the file to `.tsx` |
315
- | Multiple UIs fighting | More than one `setUiRenderer` call | Only call `setUiRenderer` once combine all UI into a single root component |
338
+ | Multiple UIs fighting | More than one `setUiRenderer` call | Use ONE `setUiRenderer` for the main UI; for independent modules use `addUiRenderer(ownerEntity, ...)` instead |
316
339
  | Text not visible | Text color matches background | Set contrasting `color` on Label or `uiText` |
317
340
 
318
341
  > **World interactions instead of screen UI?** See the **add-interactivity** skill for click handlers and pointer events on 3D objects.
@@ -5,6 +5,59 @@ description: Control camera behavior in Decentraland scenes. CameraMode detectio
5
5
 
6
6
  # Camera Control in Decentraland
7
7
 
8
+ ## Authoring split
9
+
10
+ `CameraModeArea` and `VirtualCamera` are supported in `main-entities.ts` — both static-by-nature components belong there. `MainCamera` is NOT supported because it lives on the reserved `engine.CameraEntity`; activate a virtual camera at runtime in `src/index.ts`.
11
+
12
+ `VirtualCamera.lookAtEntity` accepts an entity **name** in `main-entities.ts` (resolved to an Entity ID at build time, same as `Transform.parent`).
13
+
14
+ ```typescript
15
+ // main-entities.ts
16
+ cinematic_cam: {
17
+ components: {
18
+ Transform: {
19
+ position: { x: 12, y: 4, z: 8 },
20
+ rotation: { x: 0, y: 0.7071, z: 0, w: 0.7071 }
21
+ },
22
+ VirtualCamera: {
23
+ defaultTransition: { transitionMode: { $case: 'time', time: 2 } },
24
+ lookAtEntity: 'shopkeeper' // name of another entity in this file
25
+ }
26
+ }
27
+ },
28
+ first_person_zone: {
29
+ components: {
30
+ Transform: { position: { x: 8, y: 1, z: 8 } },
31
+ CameraModeArea: {
32
+ area: { x: 16, y: 2, z: 16 },
33
+ mode: 0 // CameraType.CT_FIRST_PERSON
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ Activate the cinematic camera at runtime:
40
+
41
+ ```typescript
42
+ // src/index.ts
43
+ import { engine, MainCamera } from '@dcl/sdk/ecs'
44
+
45
+ export function main() {
46
+ const cam = engine.getEntityOrNullByName('cinematic_cam')
47
+ if (cam) MainCamera.createOrReplace(engine.CameraEntity, { virtualCameraEntity: cam })
48
+ }
49
+ ```
50
+
51
+ The reserved `engine.CameraEntity` and `engine.PlayerEntity` are engine-managed and have no representation in `main-entities.ts`.
52
+
53
+ ### CameraType values for `CameraModeArea.mode`
54
+
55
+ | value | enum | meaning |
56
+ |---|---|---|
57
+ | 0 | CT_FIRST_PERSON | Force first-person inside the zone |
58
+ | 1 | CT_THIRD_PERSON | Force third-person inside the zone |
59
+ | 2 | CT_CINEMATIC | Force cinematic camera inside the zone |
60
+
8
61
  ## Reading Camera State
9
62
 
10
63
  Access the camera's current position and rotation via the reserved `engine.CameraEntity`:
@@ -199,12 +252,30 @@ engine.addSystem(followNpcCamera)
199
252
 
200
253
  > **Freezing player during cutscenes?** Combine VirtualCamera with `InputModifier` from the **advanced-input** skill to prevent player movement during cinematic sequences.
201
254
 
255
+ ## Camera vs Colliders (preventing camera-through-wall)
256
+
257
+ In third-person mode the player's camera can slide through walls if the wall's collider mask doesn't include `CL_POINTER`. The camera uses the **pointer** collider mask for occlusion checks — not `CL_PHYSICS`. Set both masks on walls and architecture that the camera should bounce off:
258
+
259
+ ```typescript
260
+ // main-entities.ts
261
+ wall: {
262
+ components: {
263
+ Transform: { position: { x: 8, y: 1.5, z: 16 } },
264
+ GltfContainer: {
265
+ src: 'models/wall.glb',
266
+ visibleMeshesCollisionMask: 3 // CL_PHYSICS | CL_POINTER
267
+ }
268
+ }
269
+ }
270
+ ```
271
+
272
+ For GLBs that ship with invisible collider meshes (Creator Hub asset packs), set `invisibleMeshesCollisionMask: 3` instead. Default of `CL_PHYSICS` only would let the camera pass through.
273
+
202
274
  ## Best Practices
203
275
 
204
- - Only one VirtualCamera should be active at a time
205
- - Use `CameraModeArea` to force first-person in tight indoor spaces
206
- - Keep transition speeds between 0.5 and 3.0 for comfortable camera movement
207
- - Always provide a way for the player to exit forced camera modes (e.g., leave the area)
208
- - Read camera state via `engine.CameraEntity` never try to write to it directly
209
- - For look-at detection, combine camera position with raycasting (see `add-interactivity` skill)
210
- - Camera control is read-only outside of VirtualCamera and CameraModeArea — you cannot directly move the player's camera
276
+ - Only one VirtualCamera should be active at a time.
277
+ - Use `CameraModeArea` to force first-person in tight indoor spaces.
278
+ - Keep transition speeds between 0.5 and 3.0 for comfortable camera movement.
279
+ - Read camera state via `engine.CameraEntity` never try to write to it directly.
280
+ - For look-at detection, combine camera position with raycasting (see `add-interactivity` skill).
281
+ - Camera control is read-only outside of VirtualCamera and CameraModeArea you cannot directly move the player's camera.
@@ -52,26 +52,68 @@ Update the `display` fields and parcels:
52
52
  - `scene.parcels` — for multi-parcel scenes, list all parcels (e.g., `["0,0", "0,1", "1,0", "1,1"]` for 2x2)
53
53
  - `scene.base` — set to the southwest corner parcel
54
54
 
55
- ### src/index.ts
56
- Replace the generated code with the user's scene. Example:
55
+ ### `main-entities.ts` + `src/index.ts`
56
+
57
+ OpenDCL scenes use a **two-file authoring model**:
58
+
59
+ - `main-entities.ts` (scene root) — typed declarative entities + their data components, keyed by Name. Compiled to `main.crdt` at build time and preloaded by the engine before `main()` runs.
60
+ - `src/index.ts` — behavior only. References entities by `Name` via `engine.getEntityOrNullByName<EntityName>(name)` and attaches systems, pointer events, tweens, etc.
61
+
62
+ **Example `main-entities.ts`:**
57
63
 
58
64
  ```typescript
59
- import { engine, Transform, MeshRenderer, Material } from '@dcl/sdk/ecs'
60
- import { Vector3, Color4 } from '@dcl/sdk/math'
65
+ import type { Scene } from '@dcl/sdk/scene-types'
66
+
67
+ export const scene = {
68
+ blue_cube: {
69
+ components: {
70
+ Transform: { position: { x: 8, y: 1, z: 8 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } },
71
+ MeshRenderer: { mesh: { $case: 'box', box: { uvs: [] } } },
72
+ Material: {
73
+ material: { $case: 'pbr', pbr: { albedoColor: { r: 0.2, g: 0.5, b: 1, a: 1 } } },
74
+ },
75
+ },
76
+ },
77
+ } satisfies Scene
78
+ ```
79
+
80
+ The `satisfies Scene` clause keeps the literal keys typed (so `keyof typeof scene` gives the typed entity-name union), while still validating the shape against the `Scene` schema.
81
+
82
+ **Example `src/index.ts`:**
83
+
84
+ ```typescript
85
+ import { engine, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
86
+ import type { scene } from '../main-entities'
87
+
88
+ type EntityName = keyof typeof scene
61
89
 
62
90
  export function main() {
63
- // Create a cube at the center of the scene
64
- const cube = engine.addEntity()
65
- Transform.create(cube, {
66
- position: Vector3.create(8, 1, 8)
67
- })
68
- MeshRenderer.setBox(cube)
69
- Material.setPbrMaterial(cube, {
70
- albedoColor: Color4.create(0.2, 0.5, 1, 1)
71
- })
91
+ const cube = engine.getEntityOrNullByName<EntityName>('blue_cube')
92
+ if (cube === null) return
93
+
94
+ pointerEventsSystem.onPointerDown(
95
+ { entity: cube, opts: { button: InputAction.IA_POINTER, hoverText: 'Click me' } },
96
+ () => console.log('clicked'),
97
+ )
98
+ }
99
+ ```
100
+
101
+ `tsconfig.json` should include `main-entities.ts` so it gets type-checked:
102
+ ```json
103
+ {
104
+ "extends": "@dcl/sdk/types/tsconfig.ecs7.json",
105
+ "include": ["src/**/*.ts", "src/**/*.tsx", "main-entities.ts"]
72
106
  }
73
107
  ```
74
108
 
109
+ **Rules:**
110
+
111
+ - Every editable / declared entity must have a unique Name in `main-entities.ts`.
112
+ - Dynamic entities created at runtime (effects, projectiles, dynamic UI markers) use `engine.addEntity()` directly. **Don't give dynamic entities a Name** — they don't go in `main-entities.ts`.
113
+ - Anything that's pure data (Transform, GltfContainer, MeshRenderer, MeshCollider, Material, AudioSource, VideoPlayer, TextShape, Animator config, NftShape, Billboard, VisibilityComponent) goes in `main-entities.ts`.
114
+ - Anything that's behavior (pointer callbacks, systems, tween triggers, conditional logic) goes in `src/`.
115
+ - The `scene` literal must be JSON-compatible — no function calls, no spreads, no comments inside the object.
116
+
75
117
  ### scene.json Reference
76
118
 
77
119
  All valid `scene.json` fields:
@@ -147,3 +189,4 @@ After customizing the files:
147
189
  - Y axis is up, minimum Y=0 (ground)
148
190
  - The `main` field in scene.json MUST be `"bin/index.js"` — this is the compiled output path
149
191
  - The `jsx` and `jsxImportSource` tsconfig settings are already included by `/init` — do not modify them
192
+ - **Never pass `undefined` values in Transform fields** (position, rotation, scale) — the SDK serializer crashes. If a field is optional, omit the key entirely instead of including it with an `undefined` value.
@@ -77,6 +77,18 @@ npx @dcl/sdk-commands deploy
77
77
  }
78
78
  ```
79
79
 
80
+ ### Scene Tipping (`creator`)
81
+
82
+ Let visitors send MANA tips to the scene creator. Add a `creator` field with the recipient's wallet address:
83
+
84
+ ```json
85
+ {
86
+ "creator": "0x1234567890123456789012345678901234567890"
87
+ }
88
+ ```
89
+
90
+ When set, a piggy-bank icon appears in the top-left for visitors. Clicking it opens a MANA tip modal. If the address is linked to a Decentraland NAME, the name is shown in the modal. Creators receive an in-app notification for each tip. Also configurable via Creator Hub → scene Settings → Details → Creator wallet address.
91
+
80
92
  ### Spawn Points
81
93
 
82
94
  Configure where players appear when entering the scene:
@@ -44,6 +44,41 @@ All Worlds are automatically listed on the [Places page](https://places.decentra
44
44
  }
45
45
  ```
46
46
 
47
+ ### Skybox + Minimap configuration
48
+
49
+ `worldConfiguration` accepts extra fields that aren't available for Genesis City scenes:
50
+
51
+ ```json
52
+ {
53
+ "worldConfiguration": {
54
+ "name": "my-name.dcl.eth",
55
+ "skyboxConfig": {
56
+ "fixedTime": 36000,
57
+ "textures": ["textures/skybox.png"]
58
+ },
59
+ "miniMapConfig": {
60
+ "visible": true,
61
+ "dataImage": "images/minimap.png",
62
+ "estateImage": "images/estate.png"
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ `skyboxConfig.fixedTime`:
69
+
70
+ | Value | Time of day |
71
+ |---|---|
72
+ | `0` | Midnight |
73
+ | `18000` | 6 AM (sunrise) |
74
+ | `36000` | Noon |
75
+ | `45000` | 6 PM (sunset) |
76
+ | `50400` | Maximum |
77
+
78
+ Omit `fixedTime` for a dynamic day/night cycle. `textures` is an array of cubemap face textures (top/bottom/front/back/left/right) when you want a fully custom sky.
79
+
80
+ `miniMapConfig`: set `visible: true` to show the minimap inside the World; `dataImage` is the base tile, `estateImage` overlays estate boundaries.
81
+
47
82
  ## 2. Deploy
48
83
 
49
84
  **Use the `/deploy` command** — it auto-detects the `worldConfiguration` in scene.json and deploys to the Worlds content server automatically.
@@ -0,0 +1,11 @@
1
+ # Only track the skill doc and editor module.
2
+ # All test-scene scaffolds (scene.json, package.json, src/index.ts, main.json,
3
+ # models/, .vscode/, etc.) are auto-ignored by the *-then-allowlist below.
4
+ *
5
+ !.gitignore
6
+ !SKILL.md
7
+ !src/
8
+ !src/__editor/
9
+ !src/__editor/**
10
+ node_modules/
11
+ bin/