@dcl-regenesislabs/opendcl 0.1.3-22336215175.commit-94dca45 → 0.1.3-22336575051.commit-c88f897
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.
- package/README.md +3 -2
- package/context/sdk7-cheat-sheet.md +161 -0
- package/package.json +2 -2
- package/prompts/system.md +9 -4
- package/skills/add-3d-models/SKILL.md +17 -0
- package/skills/add-interactivity/SKILL.md +27 -0
- package/skills/advanced-input/SKILL.md +0 -1
- package/skills/advanced-rendering/SKILL.md +48 -5
- package/skills/animations-tweens/SKILL.md +66 -0
- package/skills/audio-video/SKILL.md +62 -0
- package/skills/authoritative-server/SKILL.md +3 -3
- package/skills/build-ui/SKILL.md +70 -0
- package/skills/camera-control/SKILL.md +0 -2
- package/skills/deploy-scene/SKILL.md +24 -3
- package/skills/lighting-environment/SKILL.md +18 -2
- package/skills/multiplayer-sync/SKILL.md +78 -0
- package/skills/nft-blockchain/SKILL.md +22 -2
- package/skills/player-avatar/SKILL.md +33 -2
- package/skills/scene-runtime/SKILL.md +257 -0
- package/skills/smart-items/SKILL.md +60 -0
- package/context/sdk7-complete-reference.md +0 -3684
- package/context/sdk7-examples.md +0 -1709
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ The result: **more creators building more scenes, faster.**
|
|
|
28
28
|
- **Branded header** — on startup, displays a block-character "Decentraland" ASCII art banner with version and working directory. Falls back to a compact text header on narrow terminals
|
|
29
29
|
- **Multi-provider LLM support** — works with Claude, OpenAI, Google, Ollama (free/local), OpenRouter, and more
|
|
30
30
|
- **Scene-aware** — automatically detects your project's `scene.json`, SDK version, and entry points
|
|
31
|
-
- **
|
|
31
|
+
- **19 built-in skills** — scaffolding, 3D models, interactivity, UI, animations, multiplayer, authoritative server, audio/video, deployment (Genesis City & Worlds), optimization, smart items, camera control, lighting, player/avatar, NFT/blockchain, advanced rendering, advanced input, scene runtime
|
|
32
32
|
- **Integrated commands** — `/init` to scaffold, `/preview` to launch the dev server, `/tasks` to manage running processes, `/review` to audit code
|
|
33
33
|
- **TypeScript validation** — catches type errors immediately after writing code
|
|
34
34
|
- **Free asset catalogs** — 2,700+ Creator Hub 3D models, 900+ CC0-licensed models, and 50 audio files the agent proactively suggests when building scenes
|
|
@@ -126,6 +126,7 @@ OpenDCL loads domain-specific skills on demand based on what you're asking:
|
|
|
126
126
|
| `nft-blockchain` | Display NFTs, wallet checks, smart contracts |
|
|
127
127
|
| `advanced-rendering` | Billboards, 3D text, materials, transparency |
|
|
128
128
|
| `advanced-input` | Cursor state, movement restriction, WASD patterns |
|
|
129
|
+
| `scene-runtime` | Async tasks, fetch, timers, realm info, restricted actions, testing |
|
|
129
130
|
|
|
130
131
|
## How It Works
|
|
131
132
|
|
|
@@ -134,7 +135,7 @@ OpenDCL is built on [pi-coding-agent](https://github.com/badlogic/pi-mono), the
|
|
|
134
135
|
- **System prompt** with SDK7 architecture knowledge (ECS, QuickJS sandbox, parcel constraints)
|
|
135
136
|
- **Extensions** that detect your project, inject context, validate TypeScript, and provide slash commands
|
|
136
137
|
- **Skills** with detailed guides for every common scene development task
|
|
137
|
-
- **Reference docs** (
|
|
138
|
+
- **Reference docs** (SDK cheat sheet, component tables, 3D asset and audio catalogs)
|
|
138
139
|
|
|
139
140
|
The agent has full access to standard coding tools (read, write, edit, bash, grep, find) and uses them to understand and modify your scene code.
|
|
140
141
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# SDK7 Cheat Sheet
|
|
2
|
+
|
|
3
|
+
Quick reference for Decentraland SDK7 fundamentals. For detailed API usage, see the relevant skill.
|
|
4
|
+
|
|
5
|
+
## Imports
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { engine, Entity, Transform, GltfContainer, MeshRenderer, MeshCollider,
|
|
9
|
+
Material, AudioSource, VideoPlayer, TextShape, Animator, Tween, TweenSequence,
|
|
10
|
+
Billboard, VisibilityComponent, PointerEvents, Raycast, RaycastResult,
|
|
11
|
+
AvatarAttach, AvatarModifierArea, NftShape, CameraModeArea, VirtualCamera,
|
|
12
|
+
pointerEventsSystem, tweenSystem, inputSystem, raycastSystem,
|
|
13
|
+
InputAction, ColliderLayer, AvatarAnchorPointType, CameraType,
|
|
14
|
+
syncEntity, parentEntity, removeEntityWithChildren,
|
|
15
|
+
executeTask, Schemas } from '@dcl/sdk/ecs'
|
|
16
|
+
import { Vector3, Quaternion, Color3, Color4, Matrix } from '@dcl/sdk/math'
|
|
17
|
+
import ReactEcs, { ReactEcsRenderer, UiEntity, Label, Button, Input, Dropdown } from '@dcl/sdk/react-ecs'
|
|
18
|
+
import { movePlayerTo, teleportTo, triggerEmote, changeRealm,
|
|
19
|
+
openExternalUrl, openNftDialog, triggerSceneEmote,
|
|
20
|
+
copyToClipboard } from '~system/RestrictedActions'
|
|
21
|
+
import { getSceneInformation, getRealm, readFile } from '~system/Runtime'
|
|
22
|
+
import { getWorldTime, getExplorerInformation } from '~system/EnvironmentApi'
|
|
23
|
+
import { signedFetch, getHeaders } from '~system/SignedFetch'
|
|
24
|
+
import { getPlayer } from '@dcl/sdk/src/players'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## ECS Core
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// Entities
|
|
31
|
+
const entity = engine.addEntity()
|
|
32
|
+
engine.removeEntity(entity)
|
|
33
|
+
removeEntityWithChildren(engine, entity)
|
|
34
|
+
|
|
35
|
+
// Components — CRUD
|
|
36
|
+
Transform.create(entity, { position: Vector3.create(8, 1, 8) })
|
|
37
|
+
const t = Transform.get(entity) // read-only, throws if missing
|
|
38
|
+
const t = Transform.getMutable(entity) // mutable reference
|
|
39
|
+
const t = Transform.getOrNull(entity) // read-only, returns null if missing
|
|
40
|
+
Transform.has(entity) // boolean
|
|
41
|
+
Transform.deleteFrom(entity) // remove component
|
|
42
|
+
Transform.createOrReplace(entity, { ... }) // upsert
|
|
43
|
+
|
|
44
|
+
// Systems
|
|
45
|
+
engine.addSystem((dt: number) => { /* runs every frame */ })
|
|
46
|
+
engine.addSystem(mySystem, priority) // higher priority = runs first
|
|
47
|
+
engine.removeSystem(mySystem)
|
|
48
|
+
|
|
49
|
+
// Queries
|
|
50
|
+
for (const [entity, transform, mesh] of engine.getEntitiesWith(Transform, MeshRenderer)) {
|
|
51
|
+
// iterate entities that have both components
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Custom Components
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const MyComponent = engine.defineComponent('game::MyComponent', {
|
|
59
|
+
score: Schemas.Int,
|
|
60
|
+
label: Schemas.String,
|
|
61
|
+
active: Schemas.Boolean,
|
|
62
|
+
speed: Schemas.Float,
|
|
63
|
+
position: Schemas.Vector3,
|
|
64
|
+
color: Schemas.Color4,
|
|
65
|
+
items: Schemas.Array(Schemas.String),
|
|
66
|
+
data: Schemas.Map({ key: Schemas.String }),
|
|
67
|
+
opt: Schemas.Optional(Schemas.Int),
|
|
68
|
+
kind: Schemas.EnumNumber<MyEnum>(MyEnum, MyEnum.Default),
|
|
69
|
+
choice: Schemas.OneOf({ str: Schemas.String, num: Schemas.Int }),
|
|
70
|
+
timestamp: Schemas.Int64, // use Int64 for Date.now() values
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Reserved Entities
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
engine.PlayerEntity // the local player
|
|
78
|
+
engine.CameraEntity // the camera
|
|
79
|
+
engine.RootEntity // scene root (parent of all top-level entities)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Math Utilities
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// Vector3
|
|
86
|
+
Vector3.create(x, y, z)
|
|
87
|
+
Vector3.add(a, b) Vector3.subtract(a, b)
|
|
88
|
+
Vector3.scale(v, n) Vector3.normalize(v)
|
|
89
|
+
Vector3.distance(a, b) Vector3.lerp(a, b, t)
|
|
90
|
+
Vector3.rotate(v, q) Vector3.Zero() Vector3.One() Vector3.Up()
|
|
91
|
+
|
|
92
|
+
// Quaternion
|
|
93
|
+
Quaternion.fromEulerDegrees(x, y, z)
|
|
94
|
+
Quaternion.fromAngleAxis(degrees, axis)
|
|
95
|
+
Quaternion.lookRotation(forward, up?)
|
|
96
|
+
Quaternion.multiply(a, b)
|
|
97
|
+
Quaternion.toEulerAngles(q)
|
|
98
|
+
Quaternion.slerp(a, b, t)
|
|
99
|
+
Quaternion.Zero() Quaternion.Identity()
|
|
100
|
+
|
|
101
|
+
// Color
|
|
102
|
+
Color3.create(r, g, b) // 0-1 range
|
|
103
|
+
Color4.create(r, g, b, a) // 0-1 range
|
|
104
|
+
Color4.Red() .Green() .Blue() .White() .Black() .Yellow() .Gray() .Purple()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## ColliderLayer Enum
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
ColliderLayer.CL_NONE // no collision
|
|
111
|
+
ColliderLayer.CL_POINTER // responds to pointer events / raycasts
|
|
112
|
+
ColliderLayer.CL_PHYSICS // blocks player movement
|
|
113
|
+
ColliderLayer.CL_CUSTOM1 … CL_CUSTOM8 // user-defined layers
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## scene.json Schema
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"ecs7": true,
|
|
121
|
+
"runtimeVersion": "7",
|
|
122
|
+
"display": { "title": "Scene Title", "description": "...", "navmapThumbnail": "thumbnail.png" },
|
|
123
|
+
"scene": { "parcels": ["0,0", "1,0"], "base": "0,0" },
|
|
124
|
+
"main": "bin/index.js",
|
|
125
|
+
"contact": { "name": "Author", "email": "email@example.com" },
|
|
126
|
+
"tags": ["game", "art"],
|
|
127
|
+
"spawnPoints": [
|
|
128
|
+
{ "name": "spawn1", "default": true, "position": { "x": [1, 5], "y": [0, 0], "z": [2, 4] }, "cameraTarget": { "x": 8, "y": 1, "z": 8 } }
|
|
129
|
+
],
|
|
130
|
+
"requiredPermissions": [
|
|
131
|
+
"ALLOW_TO_MOVE_PLAYER_INSIDE_SCENE",
|
|
132
|
+
"ALLOW_TO_TRIGGER_AVATAR_EMOTE",
|
|
133
|
+
"ALLOW_MEDIA_HOSTNAMES"
|
|
134
|
+
],
|
|
135
|
+
"allowedMediaHostnames": ["video.example.com"],
|
|
136
|
+
"featureToggles": { "voiceChat": "enabled" },
|
|
137
|
+
"worldConfiguration": {
|
|
138
|
+
"name": "my-world.dcl.eth",
|
|
139
|
+
"skyboxConfig": { "fixedHour": 14.0 }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Scene Limits (by parcel count)
|
|
145
|
+
|
|
146
|
+
| Parcels | Entities | Triangles | Textures (MB) | Materials | Bodies | Height (m) |
|
|
147
|
+
|---------|----------|-----------|---------------|-----------|--------|------------|
|
|
148
|
+
| 1 | 512 | 10,000 | 10 | 20 | 64 | 20 |
|
|
149
|
+
| 2 | 1,024 | 10,000 | 10 | 20 | 64 | 20 |
|
|
150
|
+
| 4 | 2,048 | 20,000 | 20 | 40 | 128 | 40 |
|
|
151
|
+
| 9 | 4,096 | 40,000 | 40 | 80 | 256 | 40 |
|
|
152
|
+
| 16 | 4,096 | 40,000 | 40 | 80 | 256 | 40 |
|
|
153
|
+
|
|
154
|
+
## Runtime Restrictions
|
|
155
|
+
|
|
156
|
+
- **Sandboxed QuickJS** — no Node.js APIs (`fs`, `http`, `path`, `process`)
|
|
157
|
+
- **setTimeout/setInterval** — supported (runtime polyfill)
|
|
158
|
+
- **fetch** — supported (plain and signed)
|
|
159
|
+
- **WebSocket** — supported
|
|
160
|
+
- **Entry point** — `export function main() {}` in `src/index.ts`
|
|
161
|
+
- **All coordinates** — in meters, Y is up, origin at southwest corner of base parcel
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcl-regenesislabs/opendcl",
|
|
3
|
-
"version": "0.1.3-
|
|
3
|
+
"version": "0.1.3-22336575051.commit-c88f897",
|
|
4
4
|
"description": "AI coding assistant for Decentraland SDK7 scene development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"prompts/",
|
|
67
67
|
"context/"
|
|
68
68
|
],
|
|
69
|
-
"commit": "
|
|
69
|
+
"commit": "c88f897269368599d19ce4323a89cc4889e58ec3"
|
|
70
70
|
}
|
package/prompts/system.md
CHANGED
|
@@ -9,7 +9,7 @@ You are **OpenDCL**, an AI coding assistant specialized in Decentraland SDK7 sce
|
|
|
9
9
|
- You help creators build interactive 3D scenes for Decentraland using SDK7.
|
|
10
10
|
- You are beginner-friendly: always explain what you're doing and why.
|
|
11
11
|
- You are precise about SDK7 APIs and never invent components or functions that don't exist.
|
|
12
|
-
- When unsure, read the context
|
|
12
|
+
- When unsure, read the `context/sdk7-cheat-sheet.md` for quick SDK7 reference, or rely on the relevant skill for detailed API docs.
|
|
13
13
|
|
|
14
14
|
## Decentraland SDK7 Fundamentals
|
|
15
15
|
|
|
@@ -96,8 +96,7 @@ scene-project/
|
|
|
96
96
|
### Empty Folder (No scene.json)
|
|
97
97
|
1. Ask the user what they want to build.
|
|
98
98
|
2. **Use the `init` tool first** — this uses the official SDK scaffolding to create scene.json, package.json, tsconfig.json, and src/index.ts with the correct, up-to-date configuration, and installs dependencies. Never create these files manually.
|
|
99
|
-
3. After init completes, customize `scene.json` (title, description, parcels) and `src/index.ts
|
|
100
|
-
4. Use the `preview` tool to start the preview server.
|
|
99
|
+
3. After init completes, customize `scene.json` (title, description, parcels) and add the first element to `src/index.ts`. Then offer next steps — don't build the entire scene at once.
|
|
101
100
|
|
|
102
101
|
### Existing Scene
|
|
103
102
|
1. Read scene.json and src/index.ts to understand the project.
|
|
@@ -110,7 +109,7 @@ scene-project/
|
|
|
110
109
|
- For 3D models, use `GltfContainer.create(entity, { src: 'models/myModel.glb' })`.
|
|
111
110
|
- `GltfContainer` only works with **local files** — never use external URLs for the `src` field. Always download models into the scene's `models/` directory first.
|
|
112
111
|
- Place `.glb` files in a `models/` directory, textures in `images/`.
|
|
113
|
-
-
|
|
112
|
+
- Don't start the preview server automatically after writing code. The user will type `/preview` when ready.
|
|
114
113
|
- **Proactively suggest 3D assets**: When building a scene, always check both asset catalogs for free models that match the user's theme:
|
|
115
114
|
- `context/open-source-3d-assets.md` — 991 CC0 models from Polygonal Mind (nature, medieval, cyberpunk, sci-fi, etc.)
|
|
116
115
|
- `context/asset-packs-catalog.md` — 2,700+ models from the official Decentraland Creator Hub (furniture, structures, decorations, etc.)
|
|
@@ -126,3 +125,9 @@ You have these Decentraland-specific tools — **use them directly** when the us
|
|
|
126
125
|
|
|
127
126
|
The user can also type these as `/init`, `/preview`, `/deploy`, `/tasks` slash commands directly.
|
|
128
127
|
Additional user-only commands: `/review`, `/explain`, `/setup`, `/setup-ollama`
|
|
128
|
+
|
|
129
|
+
## Pacing
|
|
130
|
+
|
|
131
|
+
**New scenes (no scene.json):** Work one step at a time. Scaffold first, then add one thing (a model, a piece of interactivity, a UI element). After each step, briefly say what you did and offer 2-3 concrete next steps as a numbered list. Don't combine unrelated changes in one response. If the user asks for something complex ("build a medieval tavern"), break it into steps and do the first one.
|
|
132
|
+
|
|
133
|
+
**Existing scenes:** Do exactly what the user asks — one focused change per response. Don't pile on extras the user didn't request (e.g., if they ask to add a door, don't also add furniture, lighting, and a UI). Keep each response to one logical change unless the user explicitly asks for more.
|
|
@@ -128,6 +128,23 @@ curl -o models/tree.glb "https://raw.githubusercontent.com/ToxSam/cc0-models-Pol
|
|
|
128
128
|
|
|
129
129
|
> **Important**: `GltfContainer` only works with **local files**. Never use external URLs for the model `src` field. Always download models into `models/` first.
|
|
130
130
|
|
|
131
|
+
### Checking Model Load State
|
|
132
|
+
|
|
133
|
+
Use `GltfContainerLoadingState` to check if a model has finished loading:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { GltfContainer, GltfContainerLoadingState, LoadingState } from '@dcl/sdk/ecs'
|
|
137
|
+
|
|
138
|
+
engine.addSystem(() => {
|
|
139
|
+
const state = GltfContainerLoadingState.getOrNull(modelEntity)
|
|
140
|
+
if (state && state.currentState === LoadingState.FINISHED) {
|
|
141
|
+
console.log('Model loaded successfully')
|
|
142
|
+
} else if (state && state.currentState === LoadingState.FINISHED_WITH_ERROR) {
|
|
143
|
+
console.log('Model failed to load')
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
131
148
|
## Model Best Practices
|
|
132
149
|
|
|
133
150
|
- Keep models under 50MB per file for good loading times
|
|
@@ -186,6 +186,33 @@ pointerEventsSystem.onPointerDown(
|
|
|
186
186
|
)
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
### Raycast System Helpers
|
|
190
|
+
|
|
191
|
+
Use `raycastSystem` for convenient raycasting without manual component management:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { raycastSystem, RaycastQueryType, ColliderLayer } from '@dcl/sdk/ecs'
|
|
195
|
+
|
|
196
|
+
// Register a continuous local-direction raycast
|
|
197
|
+
raycastSystem.registerLocalDirectionRaycast(
|
|
198
|
+
{ entity: myEntity, opts: { queryType: RaycastQueryType.RQT_HIT_FIRST, direction: Vector3.Forward(), maxDistance: 16, collisionMask: ColliderLayer.CL_POINTER } },
|
|
199
|
+
(result) => {
|
|
200
|
+
if (result.hits.length > 0) {
|
|
201
|
+
console.log('Hit:', result.hits[0].entityId)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// Register a global-direction raycast
|
|
207
|
+
raycastSystem.registerGlobalDirectionRaycast(
|
|
208
|
+
{ entity: myEntity, opts: { queryType: RaycastQueryType.RQT_HIT_FIRST, direction: Vector3.Down(), maxDistance: 20 } },
|
|
209
|
+
(result) => { /* handle hits */ }
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
// Remove raycast from entity
|
|
213
|
+
raycastSystem.removeRaycasterEntity(myEntity)
|
|
214
|
+
```
|
|
215
|
+
|
|
189
216
|
## Best Practices
|
|
190
217
|
|
|
191
218
|
- Always set `maxDistance` on pointer events (8-16m is typical)
|
|
@@ -235,4 +235,3 @@ engine.addSystem(actionBarSystem)
|
|
|
235
235
|
- WASD keys (`IA_FORWARD`, etc.) also control player movement — polling them reads the movement state but doesn't override it
|
|
236
236
|
|
|
237
237
|
For basic pointer events and click handlers, see the `add-interactivity` skill.
|
|
238
|
-
For component field details, see `context/components-reference.md`.
|
|
@@ -50,8 +50,7 @@ Transform.create(label, { position: Vector3.create(8, 3, 8) })
|
|
|
50
50
|
TextShape.create(label, {
|
|
51
51
|
text: 'Hello World!',
|
|
52
52
|
fontSize: 24,
|
|
53
|
-
|
|
54
|
-
color: Color4.White(),
|
|
53
|
+
textColor: Color4.White(),
|
|
55
54
|
outlineColor: Color4.Black(),
|
|
56
55
|
outlineWidth: 0.1,
|
|
57
56
|
textAlign: TextAlignMode.TAM_MIDDLE_CENTER
|
|
@@ -83,7 +82,7 @@ Transform.create(floatingLabel, { position: Vector3.create(8, 4, 8) })
|
|
|
83
82
|
TextShape.create(floatingLabel, {
|
|
84
83
|
text: 'NPC Name',
|
|
85
84
|
fontSize: 16,
|
|
86
|
-
|
|
85
|
+
textColor: Color4.White(),
|
|
87
86
|
outlineColor: Color4.Black(),
|
|
88
87
|
outlineWidth: 0.08,
|
|
89
88
|
textAlign: TextAlignMode.TAM_BOTTOM_CENTER
|
|
@@ -213,6 +212,52 @@ function lodSystem() {
|
|
|
213
212
|
engine.addSystem(lodSystem)
|
|
214
213
|
```
|
|
215
214
|
|
|
215
|
+
### Per-Node Material Overrides (GltfNodeModifiers)
|
|
216
|
+
|
|
217
|
+
Override materials on specific nodes within a GLTF model without modifying the model file:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { GltfNode, GltfNodeState } from '@dcl/sdk/ecs'
|
|
221
|
+
|
|
222
|
+
// Hide a specific node in a model
|
|
223
|
+
GltfNode.create(entity, { path: 'RootNode/Armor', visible: false })
|
|
224
|
+
|
|
225
|
+
// Override a node's material
|
|
226
|
+
GltfNode.create(entity, {
|
|
227
|
+
path: 'RootNode/Helmet',
|
|
228
|
+
materialOverride: Material.Texture.Common({ src: 'images/custom-skin.png' })
|
|
229
|
+
})
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Avatar Texture
|
|
233
|
+
|
|
234
|
+
Generate a texture from a player's avatar:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
Material.setPbrMaterial(portraitFrame, {
|
|
238
|
+
texture: Material.Texture.Avatar({ userId: '0x...' })
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Texture Modes
|
|
243
|
+
|
|
244
|
+
Control how textures are filtered and wrapped:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { TextureFilterMode, TextureWrapMode } from '@dcl/sdk/ecs'
|
|
248
|
+
|
|
249
|
+
Material.setPbrMaterial(entity, {
|
|
250
|
+
texture: Material.Texture.Common({
|
|
251
|
+
src: 'images/pixel-art.png',
|
|
252
|
+
filterMode: TextureFilterMode.TFM_POINT, // crisp pixels (no smoothing)
|
|
253
|
+
wrapMode: TextureWrapMode.TWM_REPEAT // tile the texture
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Filter modes: `TFM_POINT` (pixelated), `TFM_BILINEAR` (smooth), `TFM_TRILINEAR` (smoothest).
|
|
259
|
+
Wrap modes: `TWM_REPEAT` (tile), `TWM_CLAMP` (stretch edges), `TWM_MIRROR` (mirror tile).
|
|
260
|
+
|
|
216
261
|
## Best Practices
|
|
217
262
|
|
|
218
263
|
- Use `BillboardMode.BM_Y` instead of `BM_ALL` — looks more natural and renders faster
|
|
@@ -222,5 +267,3 @@ engine.addSystem(lodSystem)
|
|
|
222
267
|
- `MTM_ALPHA_TEST` is cheaper than `MTM_ALPHA_BLEND` — use cutout when smooth transparency isn't needed
|
|
223
268
|
- Combine Billboard + TextShape for floating name labels above NPCs or objects
|
|
224
269
|
- Use VisibilityComponent for LOD systems instead of removing/re-adding entities
|
|
225
|
-
|
|
226
|
-
For more component details, see `context/components-reference.md`.
|
|
@@ -163,6 +163,72 @@ function spinSystem(dt: number) {
|
|
|
163
163
|
engine.addSystem(spinSystem)
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
### Tween Helper Methods
|
|
167
|
+
|
|
168
|
+
Use shorthand helpers instead of creating Tween components manually:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { Tween, EasingFunction } from '@dcl/sdk/ecs'
|
|
172
|
+
|
|
173
|
+
// Move
|
|
174
|
+
Tween.createOrReplace(entity, Tween.setMove(
|
|
175
|
+
Vector3.create(0, 1, 0), Vector3.create(0, 3, 0),
|
|
176
|
+
{ duration: 1500, easingFunction: EasingFunction.EF_EASEINBOUNCE }
|
|
177
|
+
))
|
|
178
|
+
|
|
179
|
+
// Rotate
|
|
180
|
+
Tween.createOrReplace(entity, Tween.setRotate(
|
|
181
|
+
Quaternion.fromEulerDegrees(0, 0, 0), Quaternion.fromEulerDegrees(0, 180, 0),
|
|
182
|
+
{ duration: 2000, easingFunction: EasingFunction.EF_EASEOUTQUAD }
|
|
183
|
+
))
|
|
184
|
+
|
|
185
|
+
// Scale
|
|
186
|
+
Tween.createOrReplace(entity, Tween.setScale(
|
|
187
|
+
Vector3.One(), Vector3.create(2, 2, 2),
|
|
188
|
+
{ duration: 1000, easingFunction: EasingFunction.EF_LINEAR }
|
|
189
|
+
))
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Yoyo Loop Mode
|
|
193
|
+
|
|
194
|
+
`TL_YOYO` reverses the tween at each end instead of restarting:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
TweenSequence.create(entity, {
|
|
198
|
+
sequence: [{ duration: 1000, ... }],
|
|
199
|
+
loop: TweenLoop.TL_YOYO
|
|
200
|
+
})
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Detecting Tween Completion
|
|
204
|
+
|
|
205
|
+
Use `tweenSystem.tweenCompleted()` to check if a tween finished this frame:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
engine.addSystem(() => {
|
|
209
|
+
if (tweenSystem.tweenCompleted(entity)) {
|
|
210
|
+
console.log('Tween finished on', entity)
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Animator Extras
|
|
216
|
+
|
|
217
|
+
Additional `Animator` features:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// Get a specific clip to modify
|
|
221
|
+
const clip = Animator.getClip(entity, 'Walk')
|
|
222
|
+
|
|
223
|
+
// shouldReset: restart animation from beginning when re-triggered
|
|
224
|
+
Animator.playSingleAnimation(entity, 'Attack', true) // resets to start
|
|
225
|
+
|
|
226
|
+
// weight: blend between animations (0.0 to 1.0)
|
|
227
|
+
const anim = Animator.getMutable(entity)
|
|
228
|
+
anim.states[0].weight = 0.5 // blend walk at 50%
|
|
229
|
+
anim.states[1].weight = 0.5 // blend idle at 50%
|
|
230
|
+
```
|
|
231
|
+
|
|
166
232
|
## Best Practices
|
|
167
233
|
|
|
168
234
|
- Use Tweens for simple A-to-B animations (doors, platforms, UI elements)
|
|
@@ -227,6 +227,68 @@ AudioSource.create(entity, { audioClipUrl: 'sounds/ambient_1.mp3', playing: true
|
|
|
227
227
|
|
|
228
228
|
> **Important**: `AudioSource` only works with **local files**. Never use external URLs for the `audioClipUrl` field. Always download audio into `sounds/` first.
|
|
229
229
|
|
|
230
|
+
### Video State Polling
|
|
231
|
+
|
|
232
|
+
Check video playback state programmatically:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { videoEventsSystem, VideoState } from '@dcl/sdk/ecs'
|
|
236
|
+
|
|
237
|
+
engine.addSystem(() => {
|
|
238
|
+
const state = videoEventsSystem.getVideoState(videoEntity)
|
|
239
|
+
if (state) {
|
|
240
|
+
console.log('Video state:', state.state) // VideoState.VS_PLAYING, VS_PAUSED, etc.
|
|
241
|
+
console.log('Current time:', state.currentOffset)
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Audio Playback Events
|
|
247
|
+
|
|
248
|
+
Use the `AudioEvent` component to detect audio state changes:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { AudioEvent } from '@dcl/sdk/ecs'
|
|
252
|
+
|
|
253
|
+
engine.addSystem(() => {
|
|
254
|
+
const event = AudioEvent.getOrNull(audioEntity)
|
|
255
|
+
if (event) {
|
|
256
|
+
console.log('Audio state:', event.state) // playing, paused, finished
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Permission for External Media
|
|
262
|
+
|
|
263
|
+
External audio/video URLs require the `ALLOW_MEDIA_HOSTNAMES` permission in scene.json:
|
|
264
|
+
|
|
265
|
+
```json
|
|
266
|
+
{
|
|
267
|
+
"requiredPermissions": ["ALLOW_MEDIA_HOSTNAMES"],
|
|
268
|
+
"allowedMediaHostnames": ["stream.example.com", "cdn.example.com"]
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Multiple Video Surfaces
|
|
273
|
+
|
|
274
|
+
Share one VideoPlayer across multiple screens by referencing the same `videoPlayerEntity`:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
Material.setPbrMaterial(screen1, {
|
|
278
|
+
texture: Material.Texture.Video({ videoPlayerEntity: videoEntity })
|
|
279
|
+
})
|
|
280
|
+
Material.setPbrMaterial(screen2, {
|
|
281
|
+
texture: Material.Texture.Video({ videoPlayerEntity: videoEntity })
|
|
282
|
+
})
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Video Limits & Tips
|
|
286
|
+
|
|
287
|
+
- **Simultaneous videos**: 1 in preview, 5 in Explorer, 10 max across the scene
|
|
288
|
+
- **Distance-based control**: Pause video when player is far away to save bandwidth
|
|
289
|
+
- **Supported formats**: `.mp4` (H.264), `.webm`, HLS (`.m3u8`) for live streaming
|
|
290
|
+
- **Live streaming**: Use HLS (`.m3u8`) URLs — most reliable across clients
|
|
291
|
+
|
|
230
292
|
## Important Notes
|
|
231
293
|
|
|
232
294
|
- Audio files must be in the project's directory (relative paths from project root)
|
|
@@ -7,7 +7,7 @@ description: Build multiplayer scenes with a headless authoritative server that
|
|
|
7
7
|
|
|
8
8
|
Build multiplayer Decentraland scenes where a **headless server** controls game state, validates changes, and prevents cheating. The same codebase runs on both server and client, with the server having full authority.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
For basic CRDT multiplayer (no server), see the `multiplayer-sync` skill instead.
|
|
11
11
|
|
|
12
12
|
## Setup
|
|
13
13
|
|
|
@@ -315,7 +315,7 @@ Put synced components and messages in `shared/` so both server and client import
|
|
|
315
315
|
- **Log prefixes**: Use `[Server]` and `[Client]` prefixes in `console.log()` to distinguish server and client output in the terminal.
|
|
316
316
|
- **Stale CRDT files**: If you see "Outside of the bounds of written data" errors, delete `main.crdt` and `main1.crdt` files and restart.
|
|
317
317
|
- **Storage inspection**: Check `node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json` to inspect persisted data during local development.
|
|
318
|
-
- **
|
|
318
|
+
- **Timers**: `setTimeout`/`setInterval` are available via runtime polyfill. For game logic, prefer `engine.addSystem()` with a delta-time accumulator to stay in sync with the frame loop.
|
|
319
319
|
- **Entity sync issues**: Verify you call `syncEntity(entity, [componentIds])` with the correct component IDs (`MyComponent.componentId`).
|
|
320
320
|
|
|
321
321
|
## Important Notes
|
|
@@ -324,6 +324,6 @@ Put synced components and messages in `shared/` so both server and client import
|
|
|
324
324
|
- **Room readiness**: Clients must wait for `RealmInfo.get(engine.RootEntity).isConnectedSceneRoom` before sending messages.
|
|
325
325
|
- **Custom vs built-in validation**: Custom components use global `validateBeforeChange((value) => ...)`. Built-in components (Transform, GltfContainer) use per-entity `validateBeforeChange(entity, (value) => ...)`.
|
|
326
326
|
- **Single codebase**: Both server and client run the same `index.ts` entry point. Use `isServer()` to branch.
|
|
327
|
-
- **No Node.js APIs**: The DCL runtime uses sandboxed QuickJS — no `fs`, `http`, `setTimeout
|
|
327
|
+
- **No Node.js APIs**: The DCL runtime uses sandboxed QuickJS — no `fs`, `http`, etc. `setTimeout`/`setInterval` are supported. Use SDK-provided APIs (Storage, EnvVar, engine systems) for server-side operations.
|
|
328
328
|
- **SDK branch**: The auth-server pattern requires `@dcl/sdk@auth-server`, not the standard `@dcl/sdk` package.
|
|
329
329
|
- For basic CRDT multiplayer without a server, see the `multiplayer-sync` skill.
|
package/skills/build-ui/SKILL.md
CHANGED
|
@@ -227,6 +227,76 @@ const HealthBar = () => (
|
|
|
227
227
|
/>
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
+
### Screen Dimensions
|
|
231
|
+
|
|
232
|
+
Read screen size via `UiCanvasInformation`:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { UiCanvasInformation } from '@dcl/sdk/ecs'
|
|
236
|
+
|
|
237
|
+
engine.addSystem(() => {
|
|
238
|
+
const canvas = UiCanvasInformation.getOrNull(engine.RootEntity)
|
|
239
|
+
if (canvas) {
|
|
240
|
+
console.log('Screen:', canvas.width, 'x', canvas.height)
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Nine-Slice Textures
|
|
246
|
+
|
|
247
|
+
Use `textureSlices` for scalable UI backgrounds (buttons, panels) that don't stretch corners:
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
<UiEntity
|
|
251
|
+
uiTransform={{ width: 200, height: 100 }}
|
|
252
|
+
uiBackground={{
|
|
253
|
+
textureMode: 'nine-slices',
|
|
254
|
+
texture: { src: 'images/panel.png' },
|
|
255
|
+
textureSlices: { top: 0.1, bottom: 0.1, left: 0.1, right: 0.1 }
|
|
256
|
+
}}
|
|
257
|
+
/>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Hover Events
|
|
261
|
+
|
|
262
|
+
Respond to mouse enter/leave for hover effects:
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
<UiEntity
|
|
266
|
+
uiTransform={{ width: 100, height: 40 }}
|
|
267
|
+
onMouseEnter={() => { isHovered = true }}
|
|
268
|
+
onMouseLeave={() => { isHovered = false }}
|
|
269
|
+
uiBackground={{ color: isHovered ? Color4.White() : Color4.Gray() }}
|
|
270
|
+
/>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Flex Wrap
|
|
274
|
+
|
|
275
|
+
Allow UI children to wrap to the next line:
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
<UiEntity uiTransform={{ flexWrap: 'wrap', width: 300 }}>
|
|
279
|
+
{items.map(item => (
|
|
280
|
+
<UiEntity key={item.id} uiTransform={{ width: 80, height: 80, margin: 4 }} />
|
|
281
|
+
))}
|
|
282
|
+
</UiEntity>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Dropdown Extras
|
|
286
|
+
|
|
287
|
+
The `Dropdown` component supports additional props:
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
<Dropdown
|
|
291
|
+
options={['Option A', 'Option B', 'Option C']}
|
|
292
|
+
selectedIndex={selectedIdx}
|
|
293
|
+
onChange={(idx) => { selectedIdx = idx }}
|
|
294
|
+
fontSize={14}
|
|
295
|
+
color={Color4.White()}
|
|
296
|
+
disabled={false}
|
|
297
|
+
/>
|
|
298
|
+
```
|
|
299
|
+
|
|
230
300
|
## Important Notes
|
|
231
301
|
|
|
232
302
|
- React hooks (`useState`, `useEffect`, etc.) are **NOT** available — use module-level variables
|
|
@@ -206,5 +206,3 @@ engine.addSystem(followNpcCamera)
|
|
|
206
206
|
- Read camera state via `engine.CameraEntity` — never try to write to it directly
|
|
207
207
|
- For look-at detection, combine camera position with raycasting (see `add-interactivity` skill)
|
|
208
208
|
- Camera control is read-only outside of VirtualCamera and CameraModeArea — you cannot directly move the player's camera
|
|
209
|
-
|
|
210
|
-
For component field details, see `context/components-reference.md`.
|