@dcl-regenesislabs/opendcl 0.1.0-22234509684.commit-63dfd19

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 (54) hide show
  1. package/README.md +234 -0
  2. package/context/components-reference.md +113 -0
  3. package/context/open-source-3d-assets.md +705 -0
  4. package/context/sdk7-complete-reference.md +3684 -0
  5. package/context/sdk7-examples.md +1709 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +76 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/scene-context.d.ts +97 -0
  11. package/dist/scene-context.d.ts.map +1 -0
  12. package/dist/scene-context.js +203 -0
  13. package/dist/scene-context.js.map +1 -0
  14. package/dist/utils.d.ts +2 -0
  15. package/dist/utils.d.ts.map +1 -0
  16. package/dist/utils.js +4 -0
  17. package/dist/utils.js.map +1 -0
  18. package/extensions/dcl-context.ts +123 -0
  19. package/extensions/dcl-deploy.ts +89 -0
  20. package/extensions/dcl-header.ts +162 -0
  21. package/extensions/dcl-init.ts +62 -0
  22. package/extensions/dcl-preview.ts +144 -0
  23. package/extensions/dcl-setup-ollama.ts +312 -0
  24. package/extensions/dcl-setup.ts +96 -0
  25. package/extensions/dcl-status.ts +88 -0
  26. package/extensions/dcl-tasks.ts +102 -0
  27. package/extensions/dcl-update-check.ts +79 -0
  28. package/extensions/dcl-validate.ts +80 -0
  29. package/extensions/plan-mode/index.ts +340 -0
  30. package/extensions/plan-mode/utils.ts +168 -0
  31. package/extensions/process-registry.ts +25 -0
  32. package/extensions/scene-utils.ts +31 -0
  33. package/package.json +65 -0
  34. package/prompts/explain.md +16 -0
  35. package/prompts/review.md +19 -0
  36. package/prompts/system.md +126 -0
  37. package/skills/add-3d-models/SKILL.md +115 -0
  38. package/skills/add-interactivity/SKILL.md +176 -0
  39. package/skills/advanced-input/SKILL.md +238 -0
  40. package/skills/advanced-rendering/SKILL.md +235 -0
  41. package/skills/animations-tweens/SKILL.md +173 -0
  42. package/skills/audio-video/SKILL.md +167 -0
  43. package/skills/authoritative-server/SKILL.md +329 -0
  44. package/skills/build-ui/SKILL.md +231 -0
  45. package/skills/camera-control/SKILL.md +199 -0
  46. package/skills/create-scene/SKILL.md +67 -0
  47. package/skills/deploy-scene/SKILL.md +106 -0
  48. package/skills/deploy-worlds/SKILL.md +107 -0
  49. package/skills/lighting-environment/SKILL.md +216 -0
  50. package/skills/multiplayer-sync/SKILL.md +132 -0
  51. package/skills/nft-blockchain/SKILL.md +246 -0
  52. package/skills/optimize-scene/SKILL.md +160 -0
  53. package/skills/player-avatar/SKILL.md +239 -0
  54. package/skills/smart-items/SKILL.md +181 -0
@@ -0,0 +1,1709 @@
1
+ # Decentraland SDK 7 Scenes Context7 Reference
2
+
3
+ This reference documents common patterns, components, and systems used in Decentraland SDK 7 scenes based on example implementations.
4
+
5
+ ## Table of Contents
6
+ - @Scene Structure
7
+ - @Entity-Component System
8
+ - @Component Reference
9
+ - @UI System
10
+ - @Runtime Data
11
+ - @Player Data & Camera Controls
12
+ - @Input Handling
13
+ - @Event Listeners
14
+ - @Movement & Animation
15
+ - @Lights & Visual Effects
16
+ - @Triggers & Interactions
17
+ - @Scene Optimization
18
+ - @Restricted Actions
19
+ - @Testing Framework
20
+ - @Network Connections
21
+ - @Blockchain Operations
22
+
23
+ ## Scene Structure
24
+
25
+ ### Basic Project Structure
26
+ ```
27
+ ├── src/
28
+ │ ├── index.ts # Main entry point
29
+ │ ├── components.ts # Custom component definitions
30
+ │ ├── systems.ts # Custom system implementations
31
+ │ ├── factory.ts # Entity creation functions
32
+ │ ├── utils.ts # Helper functions
33
+ │ └── ui.tsx # UI definitions with React
34
+ ├── package.json
35
+ └── tsconfig.json
36
+ ```
37
+
38
+ ### Main Entry Point
39
+ ```typescript
40
+ // index.ts
41
+ import { engine } from '@dcl/sdk/ecs'
42
+ import { setupUi } from './ui'
43
+ import { mySystem } from './systems'
44
+
45
+ export function main() {
46
+ // Add systems to the engine
47
+ engine.addSystem(mySystem)
48
+
49
+ // Initialize UI
50
+ setupUi()
51
+
52
+ // Create initial entities
53
+ // ...
54
+ }
55
+ ```
56
+
57
+ ## Entity-Component System
58
+
59
+ ### Creating Entities
60
+ ```typescript
61
+ import { engine, Transform, MeshRenderer, MeshCollider } from '@dcl/sdk/ecs'
62
+ import { Vector3 } from '@dcl/sdk/math'
63
+
64
+ // Create a new entity
65
+ const entity = engine.addEntity()
66
+
67
+ // Add components to the entity
68
+ Transform.create(entity, {
69
+ position: Vector3.create(8, 1, 8),
70
+ scale: Vector3.create(1, 1, 1)
71
+ })
72
+
73
+ // Add a visual mesh
74
+ MeshRenderer.setBox(entity) // Predefined shapes: setBox, setSphere, setPlane, etc.
75
+
76
+ // Add collision
77
+ MeshCollider.setBox(entity)
78
+ ```
79
+
80
+ ### Defining Custom Components
81
+ ```typescript
82
+ // components.ts
83
+ import { Schemas, engine } from '@dcl/sdk/ecs'
84
+
85
+ // Define a component with properties
86
+ export const Spinner = engine.defineComponent('spinner', {
87
+ speed: Schemas.Number
88
+ })
89
+
90
+ // Define a tag component (no properties)
91
+ export const Cube = engine.defineComponent('cube-id', {})
92
+ ```
93
+
94
+ ### Creating Systems
95
+ ```typescript
96
+ // systems.ts
97
+ import { engine, Transform } from '@dcl/sdk/ecs'
98
+ import { Quaternion, Vector3 } from '@dcl/sdk/math'
99
+ import { Spinner } from './components'
100
+
101
+ // System that rotates entities with the Spinner component
102
+ export function circularSystem(dt: number) {
103
+ // Query all entities with both Spinner and Transform components
104
+ const entitiesWithSpinner = engine.getEntitiesWith(Spinner, Transform)
105
+
106
+ for (const [entity, spinner, transform] of entitiesWithSpinner) {
107
+ // Get a mutable reference to modify the component
108
+ const mutableTransform = Transform.getMutable(entity)
109
+
110
+ // Apply rotation based on the spinner speed and delta time
111
+ mutableTransform.rotation = Quaternion.multiply(
112
+ mutableTransform.rotation,
113
+ Quaternion.fromAngleAxis(dt * spinner.speed, Vector3.Up())
114
+ )
115
+ }
116
+ }
117
+ ```
118
+
119
+ ## Component Reference
120
+
121
+ ### Transform
122
+ ```typescript
123
+ // Position, rotation, and scale
124
+ Transform.create(entity, {
125
+ position: Vector3.create(x, y, z),
126
+ rotation: Quaternion.fromEulerDegrees(x, y, z), // or Quaternion.create()
127
+ scale: Vector3.create(x, y, z),
128
+ parent: parentEntity // optional, for hierarchical transformations
129
+ })
130
+
131
+ // Update transform
132
+ const transform = Transform.getMutable(entity)
133
+ transform.position = Vector3.create(newX, newY, newZ)
134
+ ```
135
+
136
+ ### Mesh Rendering
137
+ ```typescript
138
+ // Basic shapes
139
+ MeshRenderer.setBox(entity)
140
+ MeshRenderer.setSphere(entity)
141
+ MeshRenderer.setPlane(entity)
142
+
143
+ // Material
144
+ import { Material, MeshRenderer } from '@dcl/sdk/ecs'
145
+ import { Color4 } from '@dcl/sdk/math'
146
+
147
+ // PBR material
148
+ Material.setPbrMaterial(entity, {
149
+ albedoColor: Color4.fromHexString("#FF0000"),
150
+ metallic: 0.5,
151
+ roughness: 0.5,
152
+ // other properties: emissiveColor, reflectivityColor, etc.
153
+ })
154
+
155
+ // Basic material
156
+ Material.setBasicMaterial(entity, {
157
+ diffuseColor: Color4.White()
158
+ })
159
+ ```
160
+
161
+ ### 3D Models
162
+ ```typescript
163
+ import { GltfContainer } from '@dcl/sdk/ecs'
164
+
165
+ // Load a 3D model
166
+ GltfContainer.create(entity, {
167
+ src: 'models/model.glb',
168
+ visibleMeshesCollisionMask: ColliderLayer.CL_POINTER // Optional
169
+ })
170
+
171
+ // Check loading state
172
+ if (GltfContainerLoadingState.get(entity).currentState === LoadingState.FINISHED) {
173
+ // Model is loaded
174
+ }
175
+ ```
176
+
177
+ ### Colliders
178
+ ```typescript
179
+ import { MeshCollider, ColliderLayer } from '@dcl/sdk/ecs'
180
+
181
+ // Basic colliders
182
+ MeshCollider.setBox(entity)
183
+ MeshCollider.setSphere(entity)
184
+ MeshCollider.setPlane(entity)
185
+
186
+ // With specific collision layer
187
+ MeshCollider.setBox(entity, ColliderLayer.CL_PHYSICS)
188
+ ```
189
+
190
+ ### Text
191
+ ```typescript
192
+ import { TextShape } from '@dcl/sdk/ecs'
193
+ import { Color4 } from '@dcl/sdk/math'
194
+
195
+ TextShape.create(entity, {
196
+ text: 'Hello Decentraland',
197
+ fontSize: 3,
198
+ textColor: Color4.White(),
199
+ outlineWidth: 0.1,
200
+ outlineColor: Color4.Black(),
201
+ width: 4,
202
+ height: 2,
203
+ textWrapping: true
204
+ })
205
+ ```
206
+
207
+ ### Billboard
208
+ ```typescript
209
+ import { Billboard } from '@dcl/sdk/ecs'
210
+
211
+ // Makes an entity always face the camera
212
+ Billboard.create(entity)
213
+ ```
214
+
215
+ ### Audio
216
+ ```typescript
217
+ import { AudioSource } from '@dcl/sdk/ecs'
218
+
219
+ AudioSource.create(entity, {
220
+ audioClipUrl: 'sounds/mySound.mp3',
221
+ playing: true,
222
+ loop: false,
223
+ volume: 1.0
224
+ })
225
+
226
+ // Play sound
227
+ AudioSource.getMutable(entity).playing = true
228
+ ```
229
+
230
+ ## UI System
231
+
232
+ ### Setting Up React UI
233
+ ```typescript
234
+ // ui.tsx
235
+ import ReactEcs, { ReactEcsRenderer, UiEntity, Label, Button } from '@dcl/sdk/react-ecs'
236
+ import { Color4 } from '@dcl/sdk/math'
237
+
238
+ export function setupUi() {
239
+ ReactEcsRenderer.setUiRenderer(uiComponent)
240
+ }
241
+
242
+ const uiComponent = () => (
243
+ <UiEntity
244
+ uiTransform={{
245
+ width: 400,
246
+ height: 230,
247
+ margin: '16px 0 8px 270px',
248
+ padding: 4
249
+ }}
250
+ uiBackground={{ color: Color4.create(0.5, 0.8, 0.1, 0.6) }}
251
+ >
252
+ <Label
253
+ value="Hello Decentraland"
254
+ color={Color4.White()}
255
+ fontSize={24}
256
+ />
257
+ <Button
258
+ value="Click Me"
259
+ variant="primary"
260
+ fontSize={14}
261
+ onMouseDown={() => {
262
+ console.log('Button clicked')
263
+ }}
264
+ />
265
+ </UiEntity>
266
+ )
267
+ ```
268
+
269
+ ### UI Components
270
+
271
+ #### UiEntity
272
+ ```typescript
273
+ <UiEntity
274
+ uiTransform={{
275
+ width: 400, // Pixels or percentage (e.g. '100%')
276
+ height: 300,
277
+ position: { top: 10, left: 10 }, // For absolute positioning
278
+ positionType: 'absolute', // 'absolute' or 'relative'
279
+ display: 'flex', // 'flex' or 'none'
280
+ flexDirection: 'column', // 'column' or 'row'
281
+ alignItems: 'center', // 'center', 'flex-start', 'flex-end'
282
+ justifyContent: 'center', // 'center', 'flex-start', 'flex-end', 'space-between'
283
+ margin: 5, // Or { top: 5, right: 10, bottom: 5, left: 10 }
284
+ padding: 5 // Same as margin
285
+ }}
286
+ uiBackground={{
287
+ color: Color4.White(),
288
+ texture: { src: 'images/image.png' },
289
+ textureMode: 'stretch', // 'stretch', 'nine-slices', 'center'
290
+ avatarTexture: { userId: 'user-id' } // For rendering avatar
291
+ }}
292
+ />
293
+ ```
294
+
295
+ #### Label
296
+ ```typescript
297
+ <Label
298
+ value="Text content"
299
+ color={Color4.Black()}
300
+ fontSize={18}
301
+ textAlign="middle-center" // 'top-left', 'middle-right', etc.
302
+ font="serif" // 'serif', 'monospace', or default sans-serif
303
+ uiTransform={{ width: 200, height: 50 }}
304
+ />
305
+ ```
306
+
307
+ #### Button
308
+ ```typescript
309
+ <Button
310
+ value="Click Me"
311
+ variant="primary" // 'primary', 'secondary', etc.
312
+ fontSize={14}
313
+ color={Color4.White()} // Text color
314
+ uiTransform={{ width: 100, height: 40 }}
315
+ onMouseDown={() => { /* action */ }}
316
+ uiBackground={{ color: Color4.Blue() }} // Override default button style
317
+ />
318
+ ```
319
+
320
+ #### Input
321
+ ```typescript
322
+ <Input
323
+ placeholder="Enter text..."
324
+ placeholderColor={Color4.Gray()}
325
+ color={Color4.Black()} // Text color
326
+ fontSize={16}
327
+ onChange={(value) => { console.log('Value changing: ' + value) }}
328
+ onSubmit={(value) => { console.log('Submitted: ' + value) }}
329
+ uiTransform={{ width: 200, height: 40 }}
330
+ />
331
+ ```
332
+
333
+ #### Dropdown
334
+ ```typescript
335
+ <Dropdown
336
+ options={['Option 1', 'Option 2', 'Option 3']}
337
+ onChange={(index) => { console.log('Selected option: ' + index) }}
338
+ fontSize={16}
339
+ color={Color4.Black()}
340
+ uiTransform={{ width: 200, height: 40 }}
341
+ acceptEmpty={true}
342
+ emptyLabel="-- Select an option --"
343
+ />
344
+ ```
345
+
346
+ ### Canvas Information
347
+ ```typescript
348
+ import { UiCanvasInformation, engine } from '@dcl/sdk/ecs'
349
+
350
+ // Get screen info
351
+ const canvasInfo = UiCanvasInformation.get(engine.RootEntity)
352
+ const screenWidth = canvasInfo.width
353
+ const screenHeight = canvasInfo.height
354
+ const pixelRatio = canvasInfo.devicePixelRatio
355
+ ```
356
+
357
+ ## Runtime Data
358
+
359
+ ### World Time
360
+ ```typescript
361
+ import { getWorldTime } from '~system/Runtime'
362
+
363
+ // Get the current time in the Decentraland world
364
+ executeTask(async () => {
365
+ const time = await getWorldTime({})
366
+ console.log(`Current time: ${time.seconds} seconds since day start`)
367
+
368
+ // Convert to hours (24-hour cycle)
369
+ const hours = (time.seconds / 3600) % 24
370
+ console.log(`Current hour: ${hours.toFixed(2)}`)
371
+
372
+ // Check if it's night time (between 19:50 and 6:15)
373
+ const isNight = time.seconds > 19.833 * 3600 || time.seconds < 6.25 * 3600
374
+ if (isNight) {
375
+ console.log('It is night time')
376
+ }
377
+ })
378
+ ```
379
+
380
+ ### Realm Information
381
+ ```typescript
382
+ import { getRealm } from '~system/Runtime'
383
+
384
+ // Get information about the current realm
385
+ executeTask(async () => {
386
+ const { realmInfo } = await getRealm({})
387
+ console.log(`Current realm: ${realmInfo.realmName}`)
388
+ console.log(`Network ID: ${realmInfo.networkId}`)
389
+ console.log(`Is preview: ${realmInfo.isPreview}`)
390
+
391
+ // Check if connected to scene room
392
+ if (realmInfo.isConnectedSceneRoom) {
393
+ console.log('Connected to scene room')
394
+ }
395
+ })
396
+ ```
397
+
398
+ ### Platform Detection
399
+ ```typescript
400
+ import { getPlatform } from '~system/EnvironmentApi'
401
+
402
+ // Detect the platform the player is using
403
+ executeTask(async () => {
404
+ const { platform } = await getPlatform()
405
+
406
+ if (platform === 'BROWSER') {
407
+ console.log('Running in browser')
408
+ // Optimize for browser performance
409
+ } else if (platform === 'DESKTOP') {
410
+ console.log('Running in desktop app')
411
+ // Enable higher quality features
412
+ }
413
+ })
414
+ ```
415
+
416
+ ### Engine Information
417
+ ```typescript
418
+ import { EngineInfo, engine } from '@dcl/sdk/ecs'
419
+
420
+ // Access engine information
421
+ engine.addSystem((deltaTime) => {
422
+ const engineInfo = EngineInfo.getOrNull(engine.RootEntity)
423
+ if (!engineInfo) return
424
+
425
+ // Get current frame number
426
+ const currentFrame = engineInfo.frameNumber
427
+
428
+ // Get total runtime in seconds
429
+ const runtime = engineInfo.totalRuntime
430
+
431
+ // Get current tick number
432
+ const currentTick = engineInfo.tickNumber
433
+
434
+ // Example: Log every 100 frames
435
+ if (currentFrame % 100 === 0) {
436
+ console.log(`Runtime: ${runtime.toFixed(2)}s, Frame: ${currentFrame}, Tick: ${currentTick}`)
437
+ }
438
+ })
439
+ ```
440
+
441
+ ## Player Data & Camera Controls
442
+
443
+ ### Player Position and Rotation
444
+ ```typescript
445
+ import { engine, Transform } from '@dcl/sdk/ecs'
446
+ import { Vector3, Quaternion } from '@dcl/sdk/math'
447
+
448
+ // Get player position and rotation
449
+ function getPlayerPosition() {
450
+ if (!Transform.has(engine.PlayerEntity)) return
451
+ if (!Transform.has(engine.CameraEntity)) return
452
+
453
+ // Player position (at chest height, ~0.88m above ground)
454
+ const playerPos = Transform.get(engine.PlayerEntity).position
455
+
456
+ // Player rotation (direction the avatar is facing)
457
+ const playerRot = Transform.get(engine.PlayerEntity).rotation
458
+
459
+ // Camera position (at eye level, ~1.75m above ground in 1st person)
460
+ const cameraPos = Transform.get(engine.CameraEntity).position
461
+
462
+ // Camera rotation
463
+ const cameraRot = Transform.get(engine.CameraEntity).rotation
464
+
465
+ console.log('Player position:', playerPos)
466
+ console.log('Player rotation:', playerRot)
467
+ console.log('Camera position:', cameraPos)
468
+ console.log('Camera rotation:', cameraRot)
469
+ }
470
+
471
+ // Add as a system to continuously track player position
472
+ engine.addSystem(getPlayerPosition)
473
+
474
+ // Get player position once
475
+ executeTask(async () => {
476
+ // Wait for player entity to be available
477
+ await new Promise(resolve => setTimeout(resolve, 1000))
478
+ getPlayerPosition()
479
+ })
480
+ ```
481
+
482
+ ### Player Identity Data
483
+ ```typescript
484
+ import { engine, PlayerIdentityData, AvatarBase, AvatarEquippedData } from '@dcl/sdk/ecs'
485
+
486
+ // Access player identity and avatar data
487
+ function getPlayerData() {
488
+ for (const [entity, identity, base, equipped] of engine.getEntitiesWith(
489
+ PlayerIdentityData,
490
+ AvatarBase,
491
+ AvatarEquippedData
492
+ )) {
493
+ // Player address and guest status
494
+ console.log('Player address:', identity.address)
495
+
496
+ // Avatar base information
497
+ console.log('Player name:', base.name)
498
+ console.log('Body shape:', base.bodyShapeUrn)
499
+ console.log('Skin color:', base.skinColor)
500
+ console.log('Eye color:', base.eyeColor)
501
+ console.log('Hair color:', base.hairColor)
502
+
503
+ // Equipped wearables and emotes
504
+ console.log('Wearables:', equipped.wearableUrns)
505
+ console.log('Emotes:', equipped.emoteUrns)
506
+ }
507
+ }
508
+
509
+ // Add as a system to continuously track player data
510
+ engine.addSystem(getPlayerData)
511
+ ```
512
+
513
+ ### Get Player Information
514
+ ```typescript
515
+ import { getPlayer } from '@dcl/sdk/network'
516
+
517
+ // Get current player information
518
+ executeTask(async () => {
519
+ const player = getPlayer()
520
+
521
+ console.log('Player ID:', player.userId)
522
+ console.log('Player address:', player.publicKey)
523
+ console.log('Player name:', player.name)
524
+ console.log('Player description:', player.description)
525
+ console.log('Player avatar:', player.avatar)
526
+
527
+ // Access avatar details
528
+ console.log('Body shape:', player.avatar.bodyShape)
529
+ console.log('Wearables:', player.avatar.wearables)
530
+ console.log('Emotes:', player.avatar.emotes)
531
+ })
532
+ ```
533
+
534
+ ### Get Portable Experiences
535
+ ```typescript
536
+ import { getPortableExperiencesLoaded } from '~system/PortableExperiences'
537
+
538
+ // Check if player has portable experiences loaded
539
+ executeTask(async () => {
540
+ const { loaded } = await getPortableExperiencesLoaded({})
541
+
542
+ if (loaded.length > 0) {
543
+ console.log('Player has portable experiences loaded:')
544
+ loaded.forEach(px => {
545
+ console.log(`- ID: ${px.id}`)
546
+ })
547
+ } else {
548
+ console.log('Player has no portable experiences loaded')
549
+ }
550
+ })
551
+ ```
552
+
553
+ ### Camera Mode
554
+ ```typescript
555
+ import { engine, CameraMode, CameraType } from '@dcl/sdk/ecs'
556
+
557
+ // Check player's camera mode (1st or 3rd person)
558
+ function checkCameraMode() {
559
+ if (!CameraMode.has(engine.CameraEntity)) return
560
+
561
+ const cameraMode = CameraMode.get(engine.CameraEntity)
562
+
563
+ if (cameraMode.mode === CameraType.CT_THIRD_PERSON) {
564
+ console.log('Player is using 3rd person camera')
565
+ } else {
566
+ console.log('Player is using 1st person camera')
567
+ }
568
+ }
569
+
570
+ // Add as a system to continuously check camera mode
571
+ engine.addSystem(checkCameraMode)
572
+
573
+ // Create a camera mode area to force first-person view
574
+ import { CameraModeArea } from '@dcl/sdk/ecs'
575
+
576
+ function createFirstPersonArea() {
577
+ const area = engine.addEntity()
578
+
579
+ CameraModeArea.create(area, {
580
+ area: Vector3.create(5, 5, 5), // Box size
581
+ mode: CameraType.CT_FIRST_PERSON
582
+ })
583
+
584
+ Transform.create(area, {
585
+ position: Vector3.create(8, 1, 8)
586
+ })
587
+ }
588
+ ```
589
+
590
+ ### Pointer Lock
591
+ ```typescript
592
+ import { engine, PointerLock } from '@dcl/sdk/ecs'
593
+
594
+ // Check if the player's cursor is locked
595
+ function checkPointerLock() {
596
+ if (!PointerLock.has(engine.CameraEntity)) return
597
+
598
+ const isLocked = PointerLock.get(engine.CameraEntity).isPointerLocked
599
+
600
+ if (isLocked) {
601
+ console.log('Cursor is locked (camera control mode)')
602
+ } else {
603
+ console.log('Cursor is unlocked (UI interaction mode)')
604
+ }
605
+ }
606
+
607
+ // Add as a system to continuously check pointer lock
608
+ engine.addSystem(checkPointerLock)
609
+
610
+ // Listen for pointer lock changes
611
+ import { inputSystem, InputAction, PointerEventType } from '@dcl/sdk/ecs'
612
+
613
+ function setupPointerLockListener() {
614
+ // Check for right mouse button press (unlocks cursor)
615
+ if (inputSystem.isTriggered(InputAction.IA_SECONDARY, PointerEventType.PET_DOWN)) {
616
+ console.log('Right mouse button pressed - cursor unlocked')
617
+ }
618
+
619
+ // Check for left mouse button press (locks cursor)
620
+ if (inputSystem.isTriggered(InputAction.IA_PRIMARY, PointerEventType.PET_DOWN)) {
621
+ console.log('Left mouse button pressed - cursor locked')
622
+ }
623
+ }
624
+
625
+ engine.addSystem(setupPointerLockListener)
626
+ ```
627
+
628
+ ## Input Handling
629
+
630
+ ### Pointer Events
631
+ ```typescript
632
+ import { PointerEvents, PointerEventType, InputAction, pointerEventsSystem } from '@dcl/sdk/ecs'
633
+
634
+ // Add clickable behavior to an entity
635
+ PointerEvents.create(entity, {
636
+ pointerEvents: [
637
+ {
638
+ eventType: PointerEventType.PET_DOWN,
639
+ eventInfo: {
640
+ button: InputAction.IA_POINTER,
641
+ hoverText: 'Click me',
642
+ showFeedback: true, // Show interaction feedback
643
+ maxDistance: 10 // Max interaction distance
644
+ }
645
+ }
646
+ ]
647
+ })
648
+
649
+ // Response to click events
650
+ pointerEventsSystem.onPointerDown(
651
+ { entity, opts: { button: InputAction.IA_POINTER } },
652
+ (event) => {
653
+ console.log('Entity clicked!')
654
+ // Handle the click
655
+ }
656
+ )
657
+ ```
658
+
659
+ ### Input System
660
+ ```typescript
661
+ import { inputSystem, InputAction, PointerEventType } from '@dcl/sdk/ecs'
662
+
663
+ // Check if a key/button is pressed
664
+ if (inputSystem.isPressed(InputAction.IA_FORWARD)) {
665
+ // W key or forward movement is active
666
+ }
667
+
668
+ // Check for a single press/trigger
669
+ if (inputSystem.isTriggered(InputAction.IA_JUMP, PointerEventType.PET_DOWN)) {
670
+ // Space bar was just pressed
671
+ }
672
+
673
+ // Check for key release
674
+ if (inputSystem.isTriggered(InputAction.IA_PRIMARY, PointerEventType.PET_UP)) {
675
+ // Primary button was just released
676
+ }
677
+ ```
678
+
679
+ ### Input Modifiers
680
+ ```typescript
681
+ import { InputModifier } from '@dcl/sdk/ecs'
682
+
683
+ // Disable player movement controls
684
+ InputModifier.create(engine.PlayerEntity, {
685
+ mode: {
686
+ $case: 'standard',
687
+ standard: {
688
+ disableWalk: true, // Disable walking
689
+ disableRun: true, // Disable running
690
+ disableJump: true // Disable jumping
691
+ }
692
+ }
693
+ })
694
+
695
+ // Re-enable movement
696
+ InputModifier.getMutable(engine.PlayerEntity).mode = {
697
+ $case: 'standard',
698
+ standard: {
699
+ disableWalk: false,
700
+ disableRun: false,
701
+ disableJump: false
702
+ }
703
+ }
704
+ ```
705
+
706
+ ## Event Listeners
707
+
708
+ ### Scene Entry/Exit Events
709
+ ```typescript
710
+ import { onEnterScene, onLeaveScene } from '@dcl/sdk/src/players'
711
+
712
+ // Listen for players entering the scene
713
+ onEnterScene((player) => {
714
+ if (!player) return
715
+ console.log('Player entered:', player.userId)
716
+ console.log('Player data:', player)
717
+ })
718
+
719
+ // Listen for players leaving the scene
720
+ onLeaveScene((userId) => {
721
+ if (!userId) return
722
+ console.log('Player left:', userId)
723
+ })
724
+
725
+ // Filter for current player only
726
+ import { getPlayer } from '@dcl/sdk/network'
727
+
728
+ export function main() {
729
+ let myPlayer = getPlayer()
730
+
731
+ onEnterScene((player) => {
732
+ if (!player) return
733
+ if (myPlayer && player.userId == myPlayer.userId) {
734
+ console.log('I entered the scene')
735
+ }
736
+ })
737
+
738
+ onLeaveScene((userId) => {
739
+ if (!userId) return
740
+ if (myPlayer && userId == myPlayer.userId) {
741
+ console.log('I left the scene')
742
+ }
743
+ })
744
+ }
745
+ ```
746
+
747
+ ### Avatar Emote Events
748
+ ```typescript
749
+ import { AvatarEmoteCommand } from '@dcl/sdk/ecs'
750
+
751
+ // Listen for emote events from the current player
752
+ AvatarEmoteCommand.onChange(engine.PlayerEntity, (emote) => {
753
+ if (!emote) return
754
+ console.log('Emote played:', emote.emoteUrn)
755
+ console.log('Is looping:', emote.loop)
756
+ console.log('Timestamp:', emote.timestamp)
757
+ })
758
+
759
+ // Listen for emote events from other players
760
+ function setupEmoteListener(playerEntity: Entity) {
761
+ AvatarEmoteCommand.onChange(playerEntity, (emote) => {
762
+ if (!emote) return
763
+ console.log('Player emote:', emote.emoteUrn)
764
+ })
765
+ }
766
+ ```
767
+
768
+ ### Camera Mode Events
769
+ ```typescript
770
+ import { CameraMode } from '@dcl/sdk/ecs'
771
+
772
+ // Listen for camera mode changes
773
+ CameraMode.onChange(engine.CameraEntity, (cameraComponent) => {
774
+ if (!cameraComponent) return
775
+ console.log('Camera mode:', cameraComponent.mode)
776
+ // 0 = first person
777
+ // 1 = third person
778
+ })
779
+ ```
780
+
781
+ ### Pointer Lock Events
782
+ ```typescript
783
+ import { PointerLock } from '@dcl/sdk/ecs'
784
+
785
+ // Listen for cursor lock state changes
786
+ PointerLock.onChange(engine.CameraEntity, (pointerLock) => {
787
+ if (!pointerLock) return
788
+ console.log('Cursor locked:', pointerLock.isPointerLocked)
789
+ })
790
+ ```
791
+
792
+ ### Avatar Profile Events
793
+ ```typescript
794
+ import { AvatarEquippedData, AvatarBase } from '@dcl/sdk/ecs'
795
+
796
+ // Listen for wearable/emote changes
797
+ AvatarEquippedData.onChange(engine.PlayerEntity, (equipped) => {
798
+ if (!equipped) return
799
+ console.log('Wearables:', equipped.wearableUrns)
800
+ console.log('Emotes:', equipped.emoteUrns)
801
+ })
802
+
803
+ // Listen for avatar base changes
804
+ AvatarBase.onChange(engine.PlayerEntity, (body) => {
805
+ if (!body) return
806
+ console.log('Name:', body.name)
807
+ console.log('Body shape:', body.bodyShapeUrn)
808
+ console.log('Skin color:', body.skinColor)
809
+ console.log('Eye color:', body.eyeColor)
810
+ console.log('Hair color:', body.hairColor)
811
+ })
812
+ ```
813
+
814
+ ## Movement & Animation
815
+
816
+ ### Tweens
817
+ ```typescript
818
+ import { Tween, EasingFunction, TweenSequence, TweenLoop } from '@dcl/sdk/ecs'
819
+ import { Vector3, Quaternion } from '@dcl/sdk/math'
820
+
821
+ // Move an entity
822
+ Tween.setMove(entity,
823
+ Vector3.create(1, 0, 1),
824
+ Vector3.create(5, 0, 5),
825
+ 2000,
826
+ EasingFunction.EF_LINEAR
827
+ )
828
+
829
+ // Rotate an entity
830
+ Tween.setRotate(entity,
831
+ Quaternion.fromEulerDegrees(0, 0, 0),
832
+ Quaternion.fromEulerDegrees(0, 180, 0),
833
+ 2000,
834
+ EasingFunction.EF_EASEINQUAD
835
+ )
836
+
837
+ // Scale an entity
838
+ Tween.setScale(entity,
839
+ Vector3.create(1, 1, 1),
840
+ Vector3.create(2, 2, 2),
841
+ 2000,
842
+ EasingFunction.EF_EASEOUTQUAD
843
+ })
844
+
845
+ // Move continuously
846
+ Tween.setMoveContinuous(entity,
847
+ Vector3.create(0, 0, 1),
848
+ 2000
849
+ )
850
+
851
+ // Rotate continuously
852
+ Tween.setRotateContinuous(entity,
853
+ Quaternion.fromEulerDegrees(0, 0, 90),
854
+ 2000
855
+ )
856
+
857
+ // Tween sequences (chained animations)
858
+ TweenSequence.create(entity, {
859
+ sequence: [
860
+ {
861
+ mode: Tween.Mode.Move({
862
+ start: Vector3.create(5, 0, 5),
863
+ end: Vector3.create(10, 0, 5)
864
+ }),
865
+ duration: 2000,
866
+ easingFunction: EasingFunction.EF_LINEAR
867
+ },
868
+ {
869
+ mode: Tween.Mode.Move({
870
+ start: Vector3.create(10, 0, 5),
871
+ end: Vector3.create(10, 0, 10)
872
+ }),
873
+ duration: 2000,
874
+ easingFunction: EasingFunction.EF_LINEAR
875
+ }
876
+ ],
877
+ loop: TweenLoop.TL_RESTART // Can be TL_RESTART, TL_YOYO, or undefined (no loop)
878
+ })
879
+
880
+ // Texture move
881
+ Tween.setTextureMove(entity,
882
+ Vector2.create(0, 0),
883
+ Vector2.create(1, 0),
884
+ 2000
885
+ )
886
+
887
+ // Texture move continuously
888
+ Tween.setTextureMoveContinuous(entity,
889
+ Vector2.create(0, 1),
890
+ 2000
891
+ )
892
+
893
+
894
+ // Control tween playback
895
+ const tween = Tween.getMutable(entity)
896
+ tween.playing = false // Pause the tween
897
+ tween.currentTime = 0 // Reset to beginning
898
+ ```
899
+
900
+ ### Animator Component
901
+ ```typescript
902
+ import { Animator, engine } from '@dcl/sdk/ecs'
903
+
904
+ // Create an entity with a 3D model
905
+ const shark = engine.addEntity()
906
+ GltfContainer.create(shark, {
907
+ src: 'models/shark.glb'
908
+ })
909
+
910
+ // Add the Animator component with animation states
911
+ Animator.create(shark, {
912
+ states: [
913
+ {
914
+ clip: 'swim', // Name of the animation clip in the model
915
+ playing: true,
916
+ loop: true,
917
+ speed: 1.0,
918
+ weight: 1.0
919
+ },
920
+ {
921
+ clip: 'bite',
922
+ playing: false,
923
+ loop: false,
924
+ speed: 1.0,
925
+ weight: 0.0
926
+ }
927
+ ]
928
+ })
929
+
930
+ // Play a specific animation
931
+ Animator.playSingleAnimation(shark, 'swim')
932
+
933
+ // Stop all animations
934
+ Animator.stopAllAnimations(shark)
935
+
936
+ // Get a specific animation clip to modify its properties
937
+ const swimAnim = Animator.getClip(shark, 'swim')
938
+ if (swimAnim) {
939
+ swimAnim.speed = 0.5 // Play at half speed
940
+ swimAnim.weight = 0.8 // Set animation weight
941
+ }
942
+
943
+ // Play multiple animations with different weights
944
+ Animator.create(shark, {
945
+ states: [
946
+ {
947
+ clip: 'swim',
948
+ playing: true,
949
+ loop: true,
950
+ weight: 0.7
951
+ },
952
+ {
953
+ clip: 'bite',
954
+ playing: true,
955
+ loop: false,
956
+ weight: 0.3
957
+ }
958
+ ]
959
+ })
960
+
961
+ // Create an animation that resets to the first frame when finished
962
+ Animator.create(shark, {
963
+ states: [
964
+ {
965
+ clip: 'bite',
966
+ playing: true,
967
+ loop: false,
968
+ shouldReset: true // Return to first frame when animation ends
969
+ }
970
+ ]
971
+ })
972
+ ```
973
+
974
+ ### Moving the Player
975
+ ```typescript
976
+ import { movePlayerTo } from '~system/RestrictedActions'
977
+
978
+ // Move the player to a position in the scene
979
+ movePlayerTo({
980
+ newRelativePosition: { x: 8, y: 0, z: 8 },
981
+ cameraTarget: { x: 10, y: 1, z: 8 } // Optional: where to look at
982
+ })
983
+ ```
984
+
985
+ ### Avatar Shapes
986
+ ```typescript
987
+ import { AvatarShape } from '@dcl/sdk/ecs'
988
+
989
+ // Create an NPC avatar
990
+ AvatarShape.create(entity, {
991
+ id: 'npc-id',
992
+ name: 'NPC Name',
993
+ bodyShape: 'urn:decentraland:off-chain:base-avatars:BaseMale', // or BaseFemale
994
+ wearables: [
995
+ 'urn:decentraland:off-chain:base-avatars:eyebrows_00',
996
+ 'urn:decentraland:off-chain:base-avatars:mouth_00',
997
+ 'urn:decentraland:off-chain:base-avatars:eyes_00',
998
+ 'urn:decentraland:off-chain:base-avatars:blue_tshirt',
999
+ 'urn:decentraland:off-chain:base-avatars:brown_pants',
1000
+ 'urn:decentraland:off-chain:base-avatars:classic_shoes',
1001
+ 'urn:decentraland:off-chain:base-avatars:short_hair'
1002
+ ],
1003
+ hairColor: { r: 0.92, g: 0.76, b: 0.62 }, // RGB values 0-1
1004
+ skinColor: { r: 0.94, g: 0.85, b: 0.6 }, // RGB values 0-1
1005
+ emotes: []
1006
+ })
1007
+ ```
1008
+
1009
+ ### Camera Control
1010
+ ```typescript
1011
+ import { MainCamera, VirtualCamera, CameraModeArea, CameraType } from '@dcl/sdk/ecs'
1012
+
1013
+ // Create a virtual camera
1014
+ VirtualCamera.create(entity, {
1015
+ lookAtEntity: targetEntity, // Optional: entity to focus on
1016
+ defaultTransition: {
1017
+ transitionMode: VirtualCamera.Transition.Time(2) // 2 second transition
1018
+ // Or VirtualCamera.Transition.Speed(10) // Speed-based transition
1019
+ }
1020
+ })
1021
+
1022
+ // Activate a virtual camera
1023
+ MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = cameraEntity
1024
+
1025
+ // Return to normal camera
1026
+ MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = undefined
1027
+
1028
+ // Create a camera mode area to force first-person view
1029
+ CameraModeArea.create(entity, {
1030
+ area: Vector3.create(5, 5, 5), // Box size
1031
+ mode: CameraType.CT_FIRST_PERSON // Or CT_THIRD_PERSON
1032
+ })
1033
+ ```
1034
+
1035
+ ### Emotes
1036
+ ```typescript
1037
+ import { triggerEmote, triggerSceneEmote } from '~system/RestrictedActions'
1038
+
1039
+ // Play a predefined avatar emote
1040
+ triggerEmote({ predefinedEmote: 'robot' }) // 'wave', 'dance', etc.
1041
+
1042
+ // Play a custom animation
1043
+ triggerSceneEmote({
1044
+ src: 'animations/myAnimation.glb',
1045
+ loop: false
1046
+ })
1047
+ ```
1048
+
1049
+ ## Lights & Visual Effects
1050
+
1051
+ ### Lights
1052
+ ```typescript
1053
+ import { LightSource, PBLightSource_ShadowType } from '@dcl/sdk/ecs'
1054
+ import { Color3 } from '@dcl/sdk/math'
1055
+
1056
+ // Create a point light
1057
+ LightSource.create(entity, {
1058
+ color: Color3.White(),
1059
+ intensity: 1.0,
1060
+ range: 10,
1061
+ active: true,
1062
+ type: LightSource.Type.Point({
1063
+ shadow: PBLightSource_ShadowType.ST_HARD // ST_HARD, ST_SOFT, or ST_NONE
1064
+ })
1065
+ })
1066
+
1067
+ // Create a spotlight
1068
+ LightSource.create(entity, {
1069
+ color: Color3.Yellow(),
1070
+ intensity: 1.5,
1071
+ range: 15,
1072
+ active: true,
1073
+ type: LightSource.Type.Spot({
1074
+ innerAngle: 30, // Inner cone angle in degrees
1075
+ outerAngle: 60, // Outer cone angle in degrees
1076
+ shadow: PBLightSource_ShadowType.ST_HARD,
1077
+ shadowMaskTexture: Material.Texture.Common({ src: 'textures/mask.png' }) // Optional light mask
1078
+ })
1079
+ })
1080
+ ```
1081
+
1082
+ ### Visibility Control
1083
+ ```typescript
1084
+ import { VisibilityComponent } from '@dcl/sdk/ecs'
1085
+
1086
+ // Hide an entity
1087
+ VisibilityComponent.create(entity, { visible: false })
1088
+
1089
+ // Show it again
1090
+ VisibilityComponent.getMutable(entity).visible = true
1091
+ ```
1092
+
1093
+ ## Triggers & Interactions
1094
+
1095
+ ### Raycasting
1096
+ ```typescript
1097
+ import { Raycast, RaycastQueryType, raycastSystem, RaycastResult } from '@dcl/sdk/ecs'
1098
+ import { Vector3 } from '@dcl/sdk/math'
1099
+ import { ColliderLayer } from '@dcl/sdk/ecs'
1100
+
1101
+ // Basic raycast from entity
1102
+ Raycast.create(entity, {
1103
+ originOffset: Vector3.Zero(), // Offset from entity position
1104
+ direction: { $case: 'globalDirection', globalDirection: Vector3.Down() },
1105
+ maxDistance: 10,
1106
+ queryType: RaycastQueryType.RQT_HIT_FIRST, // First hit
1107
+ timestamp: Date.now() // Used to identify this raycast
1108
+ })
1109
+
1110
+ // Direction types for raycasts
1111
+ // 1. Global direction (ignores entity rotation)
1112
+ direction: { $case: 'globalDirection', globalDirection: Vector3.Down() }
1113
+
1114
+ // 2. Local direction (relative to entity's forward direction)
1115
+ direction: { $case: 'localDirection', localDirection: Vector3.Forward() }
1116
+
1117
+ // 3. Global target (points to a specific world position)
1118
+ direction: { $case: 'globalTarget', globalTarget: Vector3.create(10, 0, 10) }
1119
+
1120
+ // 4. Target entity (points to another entity)
1121
+ direction: { $case: 'targetEntity', targetEntity: targetEntityId }
1122
+
1123
+ // Query types
1124
+ queryType: RaycastQueryType.RQT_HIT_FIRST // Returns only the first hit
1125
+ queryType: RaycastQueryType.RQT_QUERY_ALL // Returns all hits along the ray
1126
+
1127
+ // Global raycast with callback
1128
+ raycastSystem.registerGlobalDirectionRaycast(
1129
+ {
1130
+ entity: engine.PlayerEntity,
1131
+ opts: {
1132
+ direction: Vector3.Down(),
1133
+ maxDistance: 10,
1134
+ collisionMask: ColliderLayer.CL_PHYSICS // Only hit specific layers
1135
+ }
1136
+ },
1137
+ (raycastResult) => {
1138
+ if (raycastResult.hits.length > 0) {
1139
+ console.log('Hit at', raycastResult.hits[0].position)
1140
+ }
1141
+ }
1142
+ )
1143
+
1144
+ // Access raycast results in a system
1145
+ engine.addSystem(() => {
1146
+ for (const [entity, result] of engine.getEntitiesWith(RaycastResult)) {
1147
+ if (result.hits.length > 0) {
1148
+ // Process hits
1149
+ for (const hit of result.hits) {
1150
+ console.log(`Hit entity: ${hit.entityId}`)
1151
+ console.log(`Hit position: ${hit.position}`)
1152
+ console.log(`Hit normal: ${hit.normalHit}`)
1153
+ console.log(`Hit distance: ${hit.length}`)
1154
+ }
1155
+ }
1156
+ }
1157
+ })
1158
+
1159
+ // Raycast from camera
1160
+ raycastSystem.registerGlobalDirectionRaycast(
1161
+ {
1162
+ entity: engine.CameraEntity,
1163
+ opts: {
1164
+ queryType: RaycastQueryType.RQT_HIT_FIRST,
1165
+ direction: Vector3.rotate(
1166
+ Vector3.Forward(),
1167
+ Transform.get(engine.CameraEntity).rotation
1168
+ ),
1169
+ },
1170
+ },
1171
+ function (raycastResult) {
1172
+ console.log(raycastResult)
1173
+ }
1174
+ )
1175
+
1176
+ // Continuous raycast (runs every frame)
1177
+ Raycast.create(entity, {
1178
+ direction: { $case: 'localDirection', localDirection: Vector3.Forward() },
1179
+ maxDistance: 16,
1180
+ queryType: RaycastQueryType.RQT_HIT_FIRST,
1181
+ originOffset: Vector3.create(0.5, 0, 0), // Prevent self-collision
1182
+ continuous: true // Run every frame
1183
+ })
1184
+
1185
+ // Raycast between two entities
1186
+ Raycast.create(entity1, {
1187
+ direction: {
1188
+ $case: "targetEntity",
1189
+ targetEntity: entity2
1190
+ },
1191
+ maxDistance: 16,
1192
+ queryType: RaycastQueryType.RQT_QUERY_ALL
1193
+ })
1194
+ ```
1195
+
1196
+ ### Avatar Modifier Areas
1197
+ ```typescript
1198
+ import { AvatarModifierArea, AvatarModifierType } from '@dcl/sdk/ecs'
1199
+
1200
+ // Create an area that hides other avatars
1201
+ AvatarModifierArea.create(entity, {
1202
+ area: Vector3.create(5, 5, 5), // Box size
1203
+ modifiers: [AvatarModifierType.AMT_HIDE_AVATARS],
1204
+ // Or AMT_DISABLE_PASSPORTS
1205
+ excludeIds: ['user-address-1', 'user-address-2'] // Optional: players not affected
1206
+ })
1207
+ ```
1208
+
1209
+ ### Portable Experiences
1210
+ ```typescript
1211
+ import { spawn, kill, SpawnResponse } from '~system/PortableExperiences'
1212
+
1213
+ // Launch a portable experience
1214
+ let pxId: SpawnResponse
1215
+ spawn({ ens: 'experience.dcl.eth' }).then((response) => {
1216
+ pxId = response
1217
+ })
1218
+
1219
+ // Close a portable experience
1220
+ if (pxId?.pid) {
1221
+ kill({ pid: pxId.pid })
1222
+ }
1223
+ ```
1224
+
1225
+ ## Restricted Actions
1226
+
1227
+ ### External Links
1228
+ ```typescript
1229
+ import { openExternalUrl, openNftDialog } from '~system/RestrictedActions'
1230
+
1231
+ // Open a webpage
1232
+ openExternalUrl({ url: 'https://decentraland.org' })
1233
+
1234
+ // Open NFT info dialog
1235
+ openNftDialog({
1236
+ urn: 'urn:decentraland:ethereum:erc721:0x06012c8cf97bead5deae237070f9587f8e7a266d:1540722'
1237
+ })
1238
+ ```
1239
+
1240
+ ### Teleportation
1241
+ ```typescript
1242
+ import { teleportTo, changeRealm } from '~system/RestrictedActions'
1243
+
1244
+ // Teleport to another scene
1245
+ teleportTo({ worldCoordinates: { x: 10, y: 20 } })
1246
+
1247
+ // Change Decentraland realm
1248
+ changeRealm({
1249
+ realm: 'https://peer.decentraland.org',
1250
+ message: 'Do you want to change realms?' // Optional confirmation message
1251
+ })
1252
+ ```
1253
+
1254
+ ## Testing Framework
1255
+
1256
+ ### Writing Tests
1257
+ ```typescript
1258
+ import { test } from '@dcl/sdk/testing'
1259
+ import { assertComponentValue } from '@dcl/sdk/testing/assert'
1260
+
1261
+ test('my test case', function* (context) {
1262
+ // Create test setup
1263
+ const entity = engine.addEntity()
1264
+ Transform.create(entity, { position: Vector3.One() })
1265
+
1266
+ // Let the engine run for a frame
1267
+ yield
1268
+
1269
+ // Check component values
1270
+ assertComponentValue(entity, Transform, {
1271
+ position: Vector3.One(),
1272
+ scale: Vector3.One(),
1273
+ rotation: Quaternion.Identity()
1274
+ })
1275
+ })
1276
+ ```
1277
+
1278
+ ### Test Assertions
1279
+ ```typescript
1280
+ import { assertEquals, assertEntitiesCount } from '@dcl/sdk/testing/assert'
1281
+
1282
+ // Basic assertions
1283
+ assertEquals(actual, expected, 'Optional message')
1284
+
1285
+ // Entity assertions
1286
+ assertEntitiesCount(engine.getEntitiesWith(MeshRenderer), 5, 'Should have 5 entities with MeshRenderer')
1287
+ ```
1288
+
1289
+ ## Scene Optimization
1290
+
1291
+ ### Entity Pooling
1292
+ ```typescript
1293
+ // Create an entity pool for reuse
1294
+ const entityPool: Entity[] = []
1295
+
1296
+ function getEntityFromPool(): Entity {
1297
+ if (entityPool.length > 0) {
1298
+ return entityPool.pop()!
1299
+ } else {
1300
+ return createNewEntity()
1301
+ }
1302
+ }
1303
+
1304
+ function returnEntityToPool(entity: Entity) {
1305
+ // Reset the entity to a clean state
1306
+ VisibilityComponent.getMutable(entity).visible = false
1307
+ entityPool.push(entity)
1308
+ }
1309
+ ```
1310
+
1311
+ ### Visibility Culling
1312
+ ```typescript
1313
+ // Create a system that hides distant entities
1314
+ engine.addSystem(() => {
1315
+ const playerPos = Transform.get(engine.PlayerEntity).position
1316
+
1317
+ for (const [entity, transform] of engine.getEntitiesWith(Transform, VisibilityComponent)) {
1318
+ const distance = Vector3.distance(playerPos, transform.position)
1319
+
1320
+ if (distance > 20) {
1321
+ VisibilityComponent.getMutable(entity).visible = false
1322
+ } else {
1323
+ VisibilityComponent.getMutable(entity).visible = true
1324
+ }
1325
+ }
1326
+ })
1327
+ ```
1328
+
1329
+ ## Network Connections
1330
+
1331
+ ### Fetch API
1332
+ ```typescript
1333
+ import { executeTask } from '@dcl/sdk/ecs'
1334
+
1335
+ // Basic GET request
1336
+ executeTask(async () => {
1337
+ try {
1338
+ const response = await fetch('https://api.example.com/data')
1339
+ const json = await response.json()
1340
+ console.log('Response:', json)
1341
+ } catch (error) {
1342
+ console.error('Failed to fetch:', error)
1343
+ }
1344
+ })
1345
+
1346
+ // POST request with headers and body
1347
+ executeTask(async () => {
1348
+ try {
1349
+ const response = await fetch('https://api.example.com/data', {
1350
+ method: 'POST',
1351
+ headers: {
1352
+ 'Content-Type': 'application/json'
1353
+ },
1354
+ body: JSON.stringify({
1355
+ key: 'value'
1356
+ })
1357
+ })
1358
+ const json = await response.json()
1359
+ console.log('Response:', json)
1360
+ } catch (error) {
1361
+ console.error('Failed to fetch:', error)
1362
+ }
1363
+ })
1364
+ ```
1365
+
1366
+ ### Signed Fetch
1367
+ ```typescript
1368
+ import { executeTask } from '@dcl/sdk/ecs'
1369
+ import { signedFetch } from '@dcl/sdk/network'
1370
+
1371
+ // Basic signed GET request
1372
+ executeTask(async () => {
1373
+ try {
1374
+ const response = await signedFetch('https://api.example.com/data')
1375
+ const json = await response.json()
1376
+ console.log('Response:', json)
1377
+ } catch (error) {
1378
+ console.error('Failed to fetch:', error)
1379
+ }
1380
+ })
1381
+
1382
+ // Signed POST request with headers and body
1383
+ executeTask(async () => {
1384
+ try {
1385
+ const response = await signedFetch('https://api.example.com/data', {
1386
+ method: 'POST',
1387
+ headers: {
1388
+ 'Content-Type': 'application/json'
1389
+ },
1390
+ body: JSON.stringify({
1391
+ key: 'value'
1392
+ })
1393
+ })
1394
+ const json = await response.json()
1395
+ console.log('Response:', json)
1396
+ } catch (error) {
1397
+ console.error('Failed to fetch:', error)
1398
+ }
1399
+ })
1400
+ ```
1401
+
1402
+ ### WebSocket Connections
1403
+ ```typescript
1404
+ import { executeTask } from '@dcl/sdk/ecs'
1405
+
1406
+ // Basic WebSocket connection
1407
+ executeTask(async () => {
1408
+ const ws = new WebSocket('wss://example.com/ws')
1409
+
1410
+ ws.onopen = () => {
1411
+ console.log('Connected to WebSocket')
1412
+ ws.send('Hello Server!')
1413
+ }
1414
+
1415
+ ws.onmessage = (event) => {
1416
+ console.log('Received:', event.data)
1417
+ }
1418
+
1419
+ ws.onerror = (error) => {
1420
+ console.error('WebSocket error:', error)
1421
+ }
1422
+
1423
+ ws.onclose = () => {
1424
+ console.log('Disconnected from WebSocket')
1425
+ }
1426
+ })
1427
+
1428
+ // WebSocket with reconnection logic
1429
+ executeTask(async () => {
1430
+ let ws: WebSocket | null = null
1431
+ let reconnectAttempts = 0
1432
+ const maxReconnectAttempts = 5
1433
+
1434
+ function connect() {
1435
+ ws = new WebSocket('wss://example.com/ws')
1436
+
1437
+ ws.onopen = () => {
1438
+ console.log('Connected to WebSocket')
1439
+ reconnectAttempts = 0
1440
+ ws?.send('Hello Server!')
1441
+ }
1442
+
1443
+ ws.onmessage = (event) => {
1444
+ console.log('Received:', event.data)
1445
+ }
1446
+
1447
+ ws.onerror = (error) => {
1448
+ console.error('WebSocket error:', error)
1449
+ }
1450
+
1451
+ ws.onclose = () => {
1452
+ console.log('Disconnected from WebSocket')
1453
+ if (reconnectAttempts < maxReconnectAttempts) {
1454
+ reconnectAttempts++
1455
+ setTimeout(connect, 1000 * reconnectAttempts)
1456
+ }
1457
+ }
1458
+ }
1459
+
1460
+ connect()
1461
+ })
1462
+ ```
1463
+
1464
+ ### Entity Synchronization
1465
+ ```typescript
1466
+ import { syncEntity } from '@dcl/sdk/network'
1467
+ import { engine, Transform, MeshRenderer } from '@dcl/sdk/ecs'
1468
+ import { Vector3 } from '@dcl/sdk/math'
1469
+
1470
+ // Create a synced entity with a specific entityEnumId
1471
+ const syncedEntity = engine.addEntity()
1472
+ Transform.create(syncedEntity, {
1473
+ position: Vector3.create(8, 1, 8),
1474
+ scale: Vector3.create(1, 1, 1)
1475
+ })
1476
+ MeshRenderer.setBox(syncedEntity)
1477
+
1478
+ // Sync the entity with other players
1479
+ syncEntity(syncedEntity, [Transform.componentId], 1) // entityEnumId: 1
1480
+
1481
+ // Create a synced entity with multiple components
1482
+ const complexEntity = engine.addEntity()
1483
+ Transform.create(complexEntity, {
1484
+ position: Vector3.create(5, 1, 5)
1485
+ })
1486
+ MeshRenderer.setBox(complexEntity)
1487
+
1488
+ // Sync multiple components
1489
+ syncEntity(complexEntity, [
1490
+ Transform.componentId,
1491
+ MeshRenderer.componentId
1492
+ ], 2) // entityEnumId: 2
1493
+
1494
+ // Create a synced entity that responds to player interaction
1495
+ const interactiveEntity = engine.addEntity()
1496
+ Transform.create(interactiveEntity, {
1497
+ position: Vector3.create(3, 1, 3)
1498
+ })
1499
+ MeshRenderer.setBox(interactiveEntity)
1500
+
1501
+ // Sync the entity and handle updates
1502
+ syncEntity(interactiveEntity, [Transform.componentId], 3) // entityEnumId: 3
1503
+
1504
+ // Update synced entity position
1505
+ Transform.getMutable(interactiveEntity).position = Vector3.create(4, 1, 4)
1506
+ ```
1507
+
1508
+ ### Message Bus
1509
+ ```typescript
1510
+ import { MessageBus } from '@dcl/sdk/message-bus'
1511
+
1512
+ // Create a message bus instance
1513
+ const messageBus = new MessageBus()
1514
+
1515
+ // Define message types
1516
+ type SpawnMessage = {
1517
+ position: { x: number; y: number; z: number }
1518
+ entityEnumId: number
1519
+ }
1520
+
1521
+ type UpdateMessage = {
1522
+ entityId: number
1523
+ position: { x: number; y: number; z: number }
1524
+ }
1525
+
1526
+ // Send a spawn message
1527
+ messageBus.emit('spawn', {
1528
+ position: { x: 8, y: 1, z: 8 },
1529
+ entityEnumId: 1
1530
+ } as SpawnMessage)
1531
+
1532
+ // Listen for spawn messages
1533
+ messageBus.on('spawn', (message: SpawnMessage) => {
1534
+ const entity = engine.addEntity()
1535
+ Transform.create(entity, {
1536
+ position: Vector3.create(
1537
+ message.position.x,
1538
+ message.position.y,
1539
+ message.position.z
1540
+ )
1541
+ })
1542
+ MeshRenderer.setBox(entity)
1543
+
1544
+ // Sync the newly created entity
1545
+ syncEntity(entity, [Transform.componentId], message.entityEnumId)
1546
+ })
1547
+
1548
+ // Send an update message
1549
+ messageBus.emit('update', {
1550
+ entityId: 1,
1551
+ position: { x: 10, y: 1, z: 10 }
1552
+ } as UpdateMessage)
1553
+
1554
+ // Listen for update messages
1555
+ messageBus.on('update', (message: UpdateMessage) => {
1556
+ // Find the entity by its synced ID
1557
+ const entity = engine.getEntityById(message.entityId)
1558
+ if (entity) {
1559
+ Transform.getMutable(entity).position = Vector3.create(
1560
+ message.position.x,
1561
+ message.position.y,
1562
+ message.position.z
1563
+ )
1564
+ }
1565
+ })
1566
+
1567
+ // Example of a complete multiplayer interaction
1568
+ function handlePlayerInteraction(entity: Entity) {
1569
+ // When a player interacts with an entity
1570
+ messageBus.emit('interaction', {
1571
+ entityId: entity,
1572
+ action: 'click',
1573
+ timestamp: Date.now()
1574
+ })
1575
+ }
1576
+
1577
+ // Listen for player interactions
1578
+ messageBus.on('interaction', (message) => {
1579
+ console.log(`Entity ${message.entityId} was ${message.action}ed at ${message.timestamp}`)
1580
+ // Handle the interaction for all players
1581
+ })
1582
+ ```
1583
+
1584
+ ## Blockchain Operations
1585
+
1586
+ ### Get Player's Ethereum Account
1587
+ ```typescript
1588
+ import { getPlayer } from '@dcl/sdk/src/players'
1589
+
1590
+ export function main() {
1591
+ let userData = getPlayer()
1592
+ if (!userData.isGuest) {
1593
+ console.log(userData.userId)
1594
+ } else {
1595
+ log('Player is not connected with Web3')
1596
+ }
1597
+ }
1598
+ ```
1599
+
1600
+ ### Check Gas Price
1601
+ ```typescript
1602
+ import { RequestManager } from 'eth-connect'
1603
+ import { createEthereumProvider } from '@dcl/sdk/ethereum-provider'
1604
+
1605
+ executeTask(async function () {
1606
+ // Create an instance of the web3 provider to interface with Metamask
1607
+ const provider = createEthereumProvider()
1608
+ // Create the object that will handle the sending and receiving of RPC messages
1609
+ const requestManager = new RequestManager(provider)
1610
+ // Check the current gas price on the Ethereum network
1611
+ const gasPrice = await requestManager.eth_gasPrice()
1612
+ // log response
1613
+ console.log({ gasPrice })
1614
+ })
1615
+ ```
1616
+
1617
+ ### Import Contract ABI
1618
+ ```typescript
1619
+ // Example of one function in the MANA ABI
1620
+ {
1621
+ anonymous: false,
1622
+ inputs: [
1623
+ {
1624
+ indexed: true,
1625
+ name: 'burner',
1626
+ type: 'address'
1627
+ },
1628
+ {
1629
+ indexed: false,
1630
+ name: 'value',
1631
+ type: 'uint256'
1632
+ }
1633
+ ],
1634
+ name: 'Burn',
1635
+ type: 'event'
1636
+ }
1637
+
1638
+ // Import the ABI
1639
+ import { abi } from '../contracts/mana'
1640
+ ```
1641
+
1642
+ ### Instance a Contract
1643
+ ```typescript
1644
+ import { RequestManager, ContractFactory } from 'eth-connect'
1645
+ import { createEthereumProvider } from '@dcl/sdk/ethereum-provider'
1646
+ import { abi } from '../contracts/mana'
1647
+
1648
+ executeTask(async () => {
1649
+ // Create an instance of the web3 provider to interface with Metamask
1650
+ const provider = createEthereumProvider()
1651
+ // Create the object that will handle the sending and receiving of RPC messages
1652
+ const requestManager = new RequestManager(provider)
1653
+ // Create a factory object based on the abi
1654
+ const factory = new ContractFactory(requestManager, abi)
1655
+ // Use the factory object to instance a `contract` object, referencing a specific contract
1656
+ const contract = (await factory.at(
1657
+ '0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb'
1658
+ )) as any
1659
+ })
1660
+ ```
1661
+
1662
+ ### Call Contract Methods
1663
+ ```typescript
1664
+ import { getPlayer } from '@dcl/sdk/src/players'
1665
+ import { createEthereumProvider } from '@dcl/sdk/ethereum-provider'
1666
+ import { RequestManager, ContractFactory } from 'eth-connect'
1667
+ import { abi } from '../contracts/mana'
1668
+
1669
+ executeTask(async () => {
1670
+ try {
1671
+ // Setup steps explained in the section above
1672
+ const provider = createEthereumProvider()
1673
+ const requestManager = new RequestManager(provider)
1674
+ const factory = new ContractFactory(requestManager, abi)
1675
+ const contract = (await factory.at(
1676
+ '0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb'
1677
+ )) as any
1678
+ let userData = getPlayer()
1679
+ if (userData.isGuest) {
1680
+ return
1681
+ }
1682
+
1683
+ // Perform a function from the contract
1684
+ const res = await contract.setBalance(
1685
+ '0xaFA48Fad27C7cAB28dC6E970E4BFda7F7c8D60Fb',
1686
+ 100,
1687
+ {
1688
+ from: userData.userId,
1689
+ }
1690
+ )
1691
+ // Log response
1692
+ console.log(res)
1693
+ } catch (error: any) {
1694
+ console.log(error.toString())
1695
+ }
1696
+ })
1697
+ ```
1698
+
1699
+ ### Send Custom RPC Messages
1700
+ ```typescript
1701
+ import { sendAsync } from '~system/EthereumController'
1702
+
1703
+ // send a message
1704
+ await sendAsync({
1705
+ id: 1,
1706
+ method: 'myMethod',
1707
+ jsonParams: '{ myParam: myValue }',
1708
+ })
1709
+ ```