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

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 +3 -5
  2. package/context/sdk7-cheat-sheet.md +0 -4
  3. package/dist/index.js +12 -0
  4. package/dist/index.js.map +1 -1
  5. package/extensions/dcl-init.ts +6 -58
  6. package/extensions/dcl-setup-ollama.ts +312 -0
  7. package/package.json +4 -3
  8. package/prompts/system.md +41 -71
  9. package/skills/add-3d-models/SKILL.md +70 -120
  10. package/skills/add-interactivity/SKILL.md +2 -74
  11. package/skills/advanced-input/SKILL.md +1 -34
  12. package/skills/advanced-rendering/SKILL.md +9 -82
  13. package/skills/animations-tweens/SKILL.md +98 -203
  14. package/skills/audio-video/SKILL.md +83 -184
  15. package/skills/build-ui/SKILL.md +2 -25
  16. package/skills/camera-control/SKILL.md +7 -78
  17. package/skills/create-scene/SKILL.md +13 -56
  18. package/skills/deploy-scene/SKILL.md +0 -12
  19. package/skills/deploy-worlds/SKILL.md +0 -35
  20. package/skills/game-design/SKILL.md +2 -1
  21. package/skills/lighting-environment/SKILL.md +56 -103
  22. package/skills/multiplayer-sync/SKILL.md +2 -31
  23. package/skills/nft-blockchain/SKILL.md +40 -45
  24. package/skills/optimize-scene/SKILL.md +2 -7
  25. package/skills/player-avatar/SKILL.md +7 -133
  26. package/skills/scene-runtime/SKILL.md +5 -9
  27. package/skills/visual-feedback/SKILL.md +0 -1
  28. package/skills/audio-analysis/SKILL.md +0 -164
  29. package/skills/editor-gizmo/.gitignore +0 -11
  30. package/skills/editor-gizmo/SKILL.md +0 -222
  31. package/skills/editor-gizmo/src/__editor/camera.ts +0 -277
  32. package/skills/editor-gizmo/src/__editor/discovery.ts +0 -186
  33. package/skills/editor-gizmo/src/__editor/drag.ts +0 -265
  34. package/skills/editor-gizmo/src/__editor/gizmo.ts +0 -496
  35. package/skills/editor-gizmo/src/__editor/history.ts +0 -72
  36. package/skills/editor-gizmo/src/__editor/index.ts +0 -137
  37. package/skills/editor-gizmo/src/__editor/input.ts +0 -55
  38. package/skills/editor-gizmo/src/__editor/math-utils.ts +0 -114
  39. package/skills/editor-gizmo/src/__editor/persistence.ts +0 -113
  40. package/skills/editor-gizmo/src/__editor/selection.ts +0 -157
  41. package/skills/editor-gizmo/src/__editor/state.ts +0 -117
  42. package/skills/editor-gizmo/src/__editor/ui.tsx +0 -697
  43. package/skills/npcs/SKILL.md +0 -180
  44. package/skills/particle-system/SKILL.md +0 -222
  45. package/skills/player-physics/SKILL.md +0 -93
@@ -1,164 +0,0 @@
1
- ---
2
- name: audio-analysis
3
- description: Read real-time amplitude and 8-band frequency data from any AudioSource, AudioStream, or VideoPlayer entity in a Decentraland SDK7 scene with the AudioAnalysis component. Renderer fills the component each frame; scenes copy values into a plain JS view via readIntoView/tryReadIntoView and drive entity scale, color, lights, materials, particles, or UI from amplitude (overall loudness) and bands[0..7] (low→high frequency bins). Use when the user asks for music visualizers, beat reactivity, audio-reactive scenes, equalizers, dancing lights, scaling cubes that pulse to music, audio-driven materials, or anything that should react to sound. Do NOT use to play sound (see audio-video) or to detect player-emitted audio (this reads only entity-attached AudioSource/AudioStream/VideoPlayer audio).
4
- ---
5
-
6
- # AudioAnalysis
7
-
8
- Real-time audio signal analysis attached to any entity that already has an `AudioSource`, `AudioStream`, or `VideoPlayer`. The renderer analyzes the audio frame buffer and writes results back into the component each tick. Scenes read those results to drive visualizers, beat-reactive geometry, audio-driven lights, etc.
9
-
10
- ## Authoring split
11
-
12
- The **audio-emitting entity** (a speaker / radio / video screen) is static and belongs in `main-entities.ts` with its `AudioSource` / `AudioStream` / `VideoPlayer` component. `AudioAnalysis` itself is **not** in the supported declarative list — attach it at runtime in `src/index.ts` via `getEntityOrNullByName`. Reactive entities (pulsing cubes, EQ bars) are likewise added in code since their visuals are driven dynamically.
13
-
14
- ```typescript
15
- // main-entities.ts — the audio source
16
- dj_speaker: {
17
- components: {
18
- Transform: { position: { x: 8, y: 1, z: 8 } },
19
- AudioSource: { audioClipUrl: 'sounds/track.mp3', playing: true, loop: true, volume: 0.8 }
20
- }
21
- }
22
- ```
23
-
24
- ```typescript
25
- // src/index.ts — attach analysis + reactive systems
26
- import { engine, AudioAnalysis, PBAudioAnalysisMode, Transform } from '@dcl/sdk/ecs'
27
- import { Vector3 } from '@dcl/sdk/math'
28
-
29
- export function main() {
30
- const speaker = engine.getEntityOrNullByName('dj_speaker')
31
- if (!speaker) return
32
- AudioAnalysis.createAudioAnalysis(speaker, PBAudioAnalysisMode.MODE_LOGARITHMIC)
33
- // ... reactive systems below
34
- }
35
- ```
36
-
37
- ## RULE: Requires an audio-emitting component on the same entity
38
-
39
- `AudioAnalysis` does nothing on its own. The entity MUST also have one of: `AudioSource`, `AudioStream`, or `VideoPlayer`. The renderer taps that component's audio frame buffer to compute amplitude/bands. An entity with only `AudioAnalysis` produces no data.
40
-
41
- ## RULE: Audio must be playing for non-zero output
42
-
43
- Values are derived from live audio frames. If the source is paused, muted, or not yet loaded, `amplitude` and all `bands[]` stay at `0`. There is no "ready" event — start your reactive systems unconditionally, they will simply animate toward `0` while silent.
44
-
45
- ## RULE: Only the Unity explorer implements this
46
-
47
- Bevy and the mobile Godot explorer ignore the component (no analysis written). Treat `AudioAnalysis` as a Unity-explorer-only enhancement and design fallbacks (e.g. a base scale that doesn't depend on `amplitude`) so the scene still looks reasonable elsewhere.
48
-
49
- ## RULE: Read via `readIntoView` into a pre-allocated view
50
-
51
- `readIntoView` / `tryReadIntoView` write into a caller-owned `AudioAnalysisView = { amplitude: number, bands: number[] }`. Allocate the view once at scene init and reuse it every frame — do not `new` it inside the system. The `bands` array MUST be pre-sized to 8.
52
-
53
- ## RULE: Use `createAudioAnalysis`, not `create`
54
-
55
- Use the helper `AudioAnalysis.createAudioAnalysis(entity, mode?, amplitudeGain?, bandsGain?)`. It pre-fills all required protobuf fields (8 bands + amplitude + mode) with safe defaults. Calling the raw `AudioAnalysis.create(entity, {...})` requires you to provide every band/amplitude field manually. Use `createOrReplaceAudioAnalysis` to overwrite an existing one without throwing.
56
-
57
- ## Import
58
-
59
- ```typescript
60
- import {
61
- AudioAnalysis,
62
- AudioAnalysisView,
63
- PBAudioAnalysisMode,
64
- AudioSource, // or AudioStream / VideoPlayer
65
- engine,
66
- Transform
67
- } from '@dcl/sdk/ecs'
68
- import { Vector3 } from '@dcl/sdk/math'
69
- ```
70
-
71
- `AudioAnalysisView` is a TypeScript type alias (not a component), exported from `@dcl/sdk/ecs`.
72
-
73
- ## Component fields
74
-
75
- Output (filled by the renderer; read in your systems):
76
-
77
- | Field | Type | Range | Notes |
78
- |---|---|---|---|
79
- | `amplitude` | `number` | `0..~1` (mode-dep.) | Aggregate signal strength of the current audio frame. |
80
- | `band0` | `number` | `0..~1` (mode-dep.) | Lowest frequency bin (sub-bass). |
81
- | `band1..6` | `number` | `0..~1` (mode-dep.) | Increasing frequency bins, log-spaced under MODE_LOGARITHMIC. |
82
- | `band7` | `number` | `0..~1` (mode-dep.) | Highest frequency bin (treble/air). |
83
-
84
- Inputs (configure once at create time):
85
-
86
- | Field | Type | Default | Notes |
87
- |---|---|---|---|
88
- | `mode` | `PBAudioAnalysisMode` | `MODE_LOGARITHMIC` | `MODE_RAW = 0` (raw FFT magnitudes) / `MODE_LOGARITHMIC = 1` (perceptual log mapping, recommended). |
89
- | `amplitudeGain` | `number?` | `5.0` | Multiplier applied to `amplitude`. Only used in MODE_LOGARITHMIC. |
90
- | `bandsGain` | `number?` | `0.05` | Multiplier applied to all 8 bands. Only used in MODE_LOGARITHMIC. |
91
-
92
- > Values are unbounded floats — gains can push them above `1`. Clamp or scale in your system if the visual you drive needs `0..1`. For typical music at default gains, expect peaks roughly in `0..1` with normal content sitting `0..0.5`.
93
-
94
- ## AudioAnalysisView
95
-
96
- ```typescript
97
- type AudioAnalysisView = {
98
- amplitude: number
99
- bands: number[] // length 8 — bands[0] = lowest freq, bands[7] = highest
100
- }
101
- ```
102
-
103
- ## Reading the data
104
-
105
- ```typescript
106
- const view: AudioAnalysisView = { amplitude: 0, bands: new Array<number>(8) }
107
-
108
- engine.addSystem(() => {
109
- AudioAnalysis.readIntoView(audioEntity, view)
110
- // Or, defensive variant — returns false if the component is missing:
111
- // if (!AudioAnalysis.tryReadIntoView(audioEntity, view)) return
112
-
113
- // view.amplitude and view.bands[0..7] are now populated
114
- })
115
- ```
116
-
117
- ## Common patterns
118
-
119
- ```typescript
120
- // 1. Pulse an entity's scale to overall amplitude
121
- const view: AudioAnalysisView = { amplitude: 0, bands: new Array<number>(8) }
122
- engine.addSystem(() => {
123
- AudioAnalysis.readIntoView(audioEntity, view)
124
- const t = Transform.getMutable(pulseEntity)
125
- const s = 1 + view.amplitude * 10
126
- t.scale = Vector3.create(s, s, s)
127
- })
128
-
129
- // 2. 8-bar equalizer (one entity per band, scale Y by bands[i])
130
- for (const [entity, _] of engine.getEntitiesWith(VisualBar, Transform)) {
131
- const i = VisualBar.get(entity).index // 0..7
132
- Transform.getMutable(entity).scale = Vector3.create(1, view.bands[i] * BAR_HEIGHT, 1)
133
- }
134
-
135
- // 3. Bass-only kick
136
- const bass = view.bands[0] + view.bands[1]
137
- if (bass > 0.7) { /* trigger flash */ }
138
-
139
- // 4. Custom gains (less sensitive amplitude, punchier bands)
140
- AudioAnalysis.createAudioAnalysis(
141
- audioEntity,
142
- PBAudioAnalysisMode.MODE_LOGARITHMIC,
143
- 2.0,
144
- 0.1
145
- )
146
- ```
147
-
148
- ## Mode selection
149
-
150
- - `MODE_LOGARITHMIC` (default) — bands are log-spaced and gain-scaled to roughly fit `0..1` for typical music. Use for visualizers, scaling, color reactivity. `amplitudeGain` / `bandsGain` apply.
151
- - `MODE_RAW` — raw FFT-derived magnitudes, linearly spaced. Lower bands dominate visually because most musical energy is there. Gains are ignored. Use only if you intend to do your own normalization.
152
-
153
- ## Gotchas
154
-
155
- - **Output values can exceed `1.0`** with high gains or loud sources. Clamp downstream if you feed UI bars or alpha channels expecting `0..1`.
156
- - **Throttled updates.** Renderer runs analysis under a frame-time budget — values may skip frames under load. Drive smooth animations with `dt` interpolation.
157
- - **Multi-source scenes.** Each audio-emitting entity needs its own `AudioAnalysis`. No global mix-down.
158
- - **Works on `VideoPlayer` audio too.** The same audio frame buffer interface backs `AudioStream` and `VideoPlayer`, so you can react to a video's soundtrack.
159
- - **No `pitch` interaction.** `AudioSource.pitch` changes playback speed; analysis runs on the actual played frames, so a higher pitch shifts perceived band energy upward.
160
- - **Don't call `readIntoView` before `createAudioAnalysis`.** Reading without the component throws. Use `tryReadIntoView` if the component may not be attached yet.
161
-
162
- ## Permissions
163
-
164
- No special scene permission beyond what `AudioSource` / `AudioStream` / `VideoPlayer` already requires. Streamed audio still needs `ALLOW_MEDIA_HOSTNAMES` in `scene.json` for its hostname (see `audio-video` skill).
@@ -1,11 +0,0 @@
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/
@@ -1,222 +0,0 @@
1
- ---
2
- name: editor-gizmo
3
- description: Enable the visual editor in a Decentraland scene with translate/rotate gizmos. Adds click-to-select, drag-to-move arrows, drag-to-rotate rings, plane handles, wireframe selection box, and UI overlay. Auto-discovers all entities declared in main-entities.ts. Use when user wants to enable the editor, add gizmos, edit the scene interactively, or tweak object positions and rotations in preview.
4
- ---
5
-
6
- # Visual Editor Gizmo
7
-
8
- Add an in-scene visual editor that lets users click objects to select them, then drag arrow/disc handles to move or rotate them. The editor only edits entities declared in `main-entities.ts` — runtime-spawned entities are hidden from the hierarchy.
9
-
10
- ## How It Works
11
-
12
- - **Preview only**: the editor only activates in `/preview`. Deployed scenes never show editor UI.
13
- - **Editor toggle**: a pencil button in the bottom-right corner toggles the editor on/off (starts OFF).
14
- - **Auto-discovery**: finds all entities with `Transform` + `MeshRenderer` or `GltfContainer`. Hierarchy filters to entities whose `Name` is declared in `main-entities.ts`.
15
- - **Click to select**: shows a wireframe bounding box and spawns the gizmo.
16
- - **Translate mode**: 3 colored arrows (R/G/B = X/Y/Z) — drag to move along a world axis. 3 plane handles (XZ/XY/YZ) for 2-axis constrained movement.
17
- - **Rotate mode**: 3 colored ring outlines — drag to rotate around a world axis.
18
- - **World-aligned gizmos**: arrows and rings always point along world X/Y/Z, regardless of entity or parent rotation. Drag deltas are converted to local space for child entities.
19
- - **E key**: toggle between Move and Rotate.
20
- - **F key** or **click ground**: deselect.
21
- - **Undo/redo**: key 4 = undo, Shift+4 = redo.
22
- - **Auto-save**: changes POST to `${realm.baseUrl}/editor/changes` on every drag end. The preview server merges them into `main-entities.ts` and synchronously regenerates `main.crdt`.
23
-
24
- ## Setup Steps
25
-
26
- ### Step 0: Check if editor is already installed (and up-to-date)
27
-
28
- If `src/__editor/state.ts` exists, read the first line and look for `EDITOR_VERSION`. Compare it with the version in `{baseDir}/src/__editor/state.ts`. If the versions match, skip Step 1 — the files are current. If they differ (or `src/__editor/` doesn't exist), proceed with Step 1 to install or update.
29
-
30
- ### Step 1: Copy editor files into the scene
31
-
32
- ```bash
33
- mkdir -p src/__editor && cp -rf {baseDir}/src/__editor/* src/__editor/
34
- ```
35
-
36
- This creates a self-contained editor directory:
37
- ```
38
- src/__editor/
39
- ├── index.ts — Entry point + enableEditor() export
40
- ├── state.ts — Shared state and types
41
- ├── persistence.ts — HTTP POST/GET to {baseUrl}/editor/changes
42
- ├── selection.ts — Select/deselect + highlight
43
- ├── discovery.ts — Auto-discover scene entities
44
- ├── gizmo.ts — Translate/rotate gizmo handles
45
- ├── drag.ts — Drag system (ray-plane intersection)
46
- ├── camera.ts — Editor camera (orbit, WASD pan)
47
- ├── input.ts — Key bindings (E, F, 1-4)
48
- ├── history.ts — Undo/redo stack
49
- ├── math-utils.ts — Vector/quaternion helpers
50
- └── ui.tsx — Toolbar + hierarchy + properties panel
51
- ```
52
-
53
- ### Step 2: Add `enableEditor()` to the scene's main function
54
-
55
- In `src/index.ts`:
56
-
57
- ```typescript
58
- import { enableEditor } from './__editor'
59
-
60
- export function main() {
61
- // ... your scene code ...
62
- enableEditor()
63
- }
64
- ```
65
-
66
- `enableEditor()` is a no-op outside preview mode, so it's safe to leave in deployed scenes.
67
-
68
- ### Step 3: Make sure the scene has a `main-entities.ts`
69
-
70
- If the scene doesn't have one yet, create `main-entities.ts` at the scene root with at least one entity (see "Authoring Model" below). Without it, the editor's hierarchy panel falls back to permissive mode and shows every named entity, including runtime ones.
71
-
72
- ### Step 4: Update tsconfig.json to include `main-entities.ts`
73
-
74
- The default scene `tsconfig.json` only checks `src/**/*`. Widen the include so the typed `Scene` shape is validated:
75
-
76
- ```json
77
- {
78
- "extends": "@dcl/sdk/types/tsconfig.ecs7.json",
79
- "include": ["src/**/*.ts", "src/**/*.tsx", "main-entities.ts"]
80
- }
81
- ```
82
-
83
- ### Step 5: Verify
84
-
85
- Run `/preview`. You should see a pencil button bottom-right. Click it to toggle the editor. Click any entity declared in `main-entities.ts` to select it; drag arrows or rings to move or rotate.
86
-
87
- ## Scene Authoring Model
88
-
89
- The editor only edits **declared** entities — those that exist in `main-entities.ts`. Dynamic entities created at runtime via `engine.addEntity()` are hidden from the hierarchy and not draggable.
90
-
91
- ### `main-entities.ts` (canonical entity declarations)
92
-
93
- `main-entities.ts` lives at the scene root, exports a typed `scene` constant, and is bundled into `main.crdt` at build time. The `satisfies Scene` clause keeps the literal keys typed (so code can reference entity names safely) while still validating the shape against the schema.
94
-
95
- ```typescript
96
- import type { Scene } from '@dcl/sdk/scene-types'
97
-
98
- export const scene = {
99
- "barrel_1": {
100
- "components": {
101
- "Transform": {
102
- "position": { "x": 5, "y": 0, "z": 8 },
103
- "rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
104
- "scale": { "x": 1, "y": 1, "z": 1 }
105
- },
106
- "GltfContainer": { "src": "models/Barrel.glb" }
107
- }
108
- },
109
- "lamp_1": {
110
- "components": {
111
- "Transform": {
112
- "position": { "x": 0, "y": 1.5, "z": 0 },
113
- "rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
114
- "scale": { "x": 1, "y": 1, "z": 1 },
115
- "parent": "barrel_1"
116
- },
117
- "GltfContainer": { "src": "models/Lamp.glb" }
118
- }
119
- }
120
- } satisfies Scene
121
- ```
122
-
123
- **Rules:**
124
- - **Names are unique** within `scene` — they're the stable ID the editor and code use to reference an entity.
125
- - **Parents are referenced by Name**, not entity ID (e.g. `"parent": "barrel_1"`). The build resolves names to IDs.
126
- - **`Transform.position` is required.** `rotation` defaults to identity, `scale` defaults to `(1,1,1)`, but you must provide all three keys when authoring.
127
- - **Literal-only constraint:** values inside `scene` must be plain JSON-compatible literals — no function calls (`Vector3.create(...)`), no spread, no computed expressions, no comments inside the literal. The build parses the AST and the editor save handler rewrites the whole literal as JSON, so anything outside this discipline gets stripped or breaks.
128
-
129
- ### Behavior in `src/index.ts`
130
-
131
- Code references entities by Name and attaches behavior. Use a type-only import of `scene` so the bundle stays small, and derive `EntityName` for typo-safe lookups:
132
-
133
- ```typescript
134
- import { engine, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
135
- import { enableEditor } from './__editor'
136
- import type { scene } from '../main-entities'
137
-
138
- type EntityName = keyof typeof scene
139
-
140
- export function main() {
141
- const barrel = engine.getEntityOrNullByName<EntityName>('barrel_1')
142
- if (barrel === null) return
143
-
144
- pointerEventsSystem.onPointerDown(
145
- { entity: barrel, opts: { button: InputAction.IA_POINTER, hoverText: 'Open' } },
146
- () => console.log('clicked barrel')
147
- )
148
-
149
- enableEditor()
150
- }
151
- ```
152
-
153
- - `engine.getEntityOrNullByName<EntityName>(name)` looks up an entity by its `Name` component, populated by the `main.crdt` preload (built from `main-entities.ts` at bundle time).
154
- - The `<EntityName>` type parameter makes typos a compile error: passing `'barrl_1'` (typo) fails type-checking.
155
- - Renaming an entity in `main-entities.ts` immediately surfaces every stale reference in code as a compile error.
156
- - Use `getEntityOrNullByName` rather than `getEntityByName` — the SDK's typed `getEntityByName` requires awkward generics and silently returns `undefined` cast as `Entity`, so null-handling is cleaner.
157
-
158
- ### Dynamic entities
159
-
160
- Anything that needs to spawn at runtime (effects, projectiles, dynamic UI markers) still uses `engine.addEntity()` directly:
161
-
162
- ```typescript
163
- const explosion = engine.addEntity()
164
- Transform.create(explosion, { position: Vector3.create(8, 1, 8) })
165
- GltfContainer.create(explosion, { src: 'models/Explosion.glb' })
166
- // Don't give dynamic entities a Name — they don't go in main-entities.ts.
167
- ```
168
-
169
- **Rule**: only declarative, editable entities go in `main-entities.ts`. Dynamic runtime entities use `engine.addEntity` and don't get `Name` components.
170
-
171
- ## Persistence
172
-
173
- When the user drags an entity in preview:
174
-
175
- 1. The scene applies the new Transform client-side (instant).
176
- 2. The editor POSTs `${realm.baseUrl}/editor/changes` with the entity's new Transform, keyed by Name.
177
- 3. The preview server merges the change into `main-entities.ts` on disk by parsing the AST, mutating the scene object, and splicing the new JSON back into the source — preserving everything outside the `scene` literal (imports, `satisfies` clause, comments above the export).
178
- 4. The same handler synchronously regenerates `main.crdt` so the next reload preloads the updated state.
179
- 5. Both `main-entities.ts` and `main.crdt` are excluded from the file watcher, so editor saves do not trigger a scene reload.
180
-
181
- There is no manual "save" step — every drag persists.
182
-
183
- If the AI / a human edits `main-entities.ts` directly (without going through the editor), a dedicated watcher on `main-entities.ts` regenerates `main.crdt` out-of-band as well, with an mtime check that skips redundant work when the editor's POST handler already produced a fresh CRDT.
184
-
185
- ## Removing the Editor
186
-
187
- To remove:
188
- 1. Delete `src/__editor/`
189
- 2. Remove the `import { enableEditor } from './__editor'` line and the `enableEditor()` call
190
-
191
- `main-entities.ts` is unaffected — it remains the source of truth for declared entities, regardless of whether the editor is installed.
192
-
193
- ## Adding New Entities
194
-
195
- When the user asks to add a new entity (e.g., "add a barrel"):
196
-
197
- 1. **Add the entry to `main-entities.ts`** with a unique Name and the components needed to render it (`Transform`, `GltfContainer` or `MeshRenderer` + `Material`, etc.). The TS compiler will validate the shape against `Scene`.
198
- 2. **Reference it in code** if you need to attach behavior:
199
- ```typescript
200
- const barrel = engine.getEntityOrNullByName<EntityName>('barrel_1')
201
- ```
202
- 3. Run `/preview` — the entity will appear in the scene at the position declared in `main-entities.ts`, and will be draggable in the editor.
203
-
204
- ## Components supported in `main-entities.ts`
205
-
206
- All ECS data components that the client renders/uses:
207
-
208
- - `Transform` (required for every entity)
209
- - `GltfContainer`, `MeshRenderer`, `MeshCollider`, `Material`
210
- - `VisibilityComponent`, `Billboard`
211
- - `AudioSource`, `VideoPlayer`, `TextShape`, `NftShape`
212
- - `Animator` (state-machine config; runtime control via code)
213
-
214
- Behavior — pointer event callbacks, systems, tweens, conditional logic — stays in `src/`.
215
-
216
- ## Common Pitfalls
217
-
218
- - **Component shapes mirror the SDK protobuf.** `MeshRenderer` is `{ mesh: { $case: 'box', box: { uvs: [] } } }`, not `MeshRenderer.setBox()`. The TS compiler validates against the protobuf-derived types and will flag mismatches.
219
- - **Don't use `Vector3.create(...)` inside `main-entities.ts`.** Use plain `{ x, y, z }` objects. The literal-only constraint means function calls and identifier references will break the build/save pipeline.
220
- - **`box` mesh requires `uvs: []`.** Same for `plane`. Sphere and cylinder default to `{}`. The TS type forces this — pay attention to red squiggles.
221
- - **Renaming an entity** breaks every code reference until you update them. That's the type system working as intended; let TS guide you to the broken references.
222
- - **Comments inside the `scene` literal get wiped on the first editor save.** Keep comments outside the literal (above the import, before the `export const scene =` line).
@@ -1,277 +0,0 @@
1
- /**
2
- * Editor orbit camera + drag lock camera.
3
- *
4
- * Orbit model: target + yaw + pitch + distance.
5
- * Lock camera: freezes view during gizmo drag (non-editor-cam mode only).
6
- */
7
-
8
- import {
9
- engine,
10
- Entity,
11
- Transform,
12
- VirtualCamera,
13
- MainCamera,
14
- InputModifier,
15
- inputSystem,
16
- InputAction,
17
- PrimaryPointerInfo,
18
- } from '@dcl/sdk/ecs'
19
- import { Vector3, Quaternion } from '@dcl/sdk/math'
20
- import { getSceneInformation } from '~system/Runtime'
21
- import { state, editorEntities, selectableInfoMap, gizmoClickConsumed } from './state'
22
- import { copyVec3, copyQuat } from './math-utils'
23
-
24
- // ── Scene bounds (computed once) ────────────────────────
25
- let sceneCenter = Vector3.create(8, 0, 8)
26
- let sceneBoundsMin = Vector3.create(0, 0, 0)
27
- let sceneBoundsMax = Vector3.create(16, 20, 16)
28
-
29
- async function getSceneInformationAsync() {
30
- try {
31
- const info = await getSceneInformation({})
32
- const metadata = JSON.parse(info.metadataJson)
33
- const parcels: string[] = metadata?.scene?.parcels ?? ['0,0']
34
- let minX = Infinity, minZ = Infinity, maxX = -Infinity, maxZ = -Infinity
35
- for (const p of parcels) {
36
- const [px, pz] = p.split(',').map(Number)
37
- if (px < minX) minX = px
38
- if (pz < minZ) minZ = pz
39
- if (px > maxX) maxX = px
40
- if (pz > maxZ) maxZ = pz
41
- }
42
- const base = parcels[0].split(',').map(Number)
43
- const offX = -base[0] * 16
44
- const offZ = -base[1] * 16
45
- sceneBoundsMin = Vector3.create((minX * 16) + offX, 0, (minZ * 16) + offZ)
46
- sceneBoundsMax = Vector3.create(((maxX + 1) * 16) + offX, 20, ((maxZ + 1) * 16) + offZ)
47
- sceneCenter = Vector3.create(
48
- (sceneBoundsMin.x + sceneBoundsMax.x) / 2,
49
- 0,
50
- (sceneBoundsMin.z + sceneBoundsMax.z) / 2,
51
- )
52
- } catch (e) {
53
- console.log('[editor] failed to read scene bounds, using defaults', e)
54
- }
55
- }
56
- void getSceneInformationAsync()
57
-
58
- // ============================================================
59
- // Editor Camera
60
- // ============================================================
61
-
62
- let editorCamEntity: Entity | undefined
63
-
64
- const editorCam = {
65
- target: Vector3.create(16, 2, 16),
66
- yaw: -45,
67
- pitch: 35,
68
- distance: 25,
69
- }
70
-
71
- // Tuning
72
- const PAN_SPEED = 12
73
- const VERTICAL_SPEED = 8
74
- const ORBIT_SENSITIVITY = 0.15
75
- const ZOOM_SPEED = 15
76
- const MIN_DISTANCE = 3
77
- const MAX_DISTANCE = 80
78
- const MIN_PITCH = 5
79
- const MAX_PITCH = 89
80
- const FOCUS_DISTANCE = 8
81
-
82
- export function createEditorCamera() {
83
- editorCamEntity = engine.addEntity()
84
- Transform.create(editorCamEntity, {
85
- position: Vector3.Zero(),
86
- rotation: Quaternion.Identity(),
87
- })
88
- VirtualCamera.create(editorCamEntity, {
89
- defaultTransition: { transitionMode: VirtualCamera.Transition.Time(0) },
90
- })
91
- editorEntities.add(editorCamEntity)
92
- }
93
-
94
- function updateEditorCamera() {
95
- if (editorCamEntity === undefined) return
96
-
97
- const pitchRad = editorCam.pitch * (Math.PI / 180)
98
- const yawRad = editorCam.yaw * (Math.PI / 180)
99
-
100
- const cosP = Math.cos(pitchRad)
101
- const offset = Vector3.create(
102
- -Math.sin(yawRad) * cosP * editorCam.distance,
103
- Math.sin(pitchRad) * editorCam.distance,
104
- -Math.cos(yawRad) * cosP * editorCam.distance,
105
- )
106
-
107
- const camPos = Vector3.add(editorCam.target, offset)
108
- const forward = Vector3.normalize(Vector3.subtract(editorCam.target, camPos))
109
- const camRot = Quaternion.lookRotation(forward)
110
-
111
- const t = Transform.getMutable(editorCamEntity)
112
- copyVec3(t.position, camPos)
113
- copyQuat(t.rotation, camRot)
114
- }
115
-
116
- export function activateEditorCamera() {
117
- if (state.editorCamActive || state.isDragging) return
118
- state.editorCamActive = true
119
-
120
- // Center on scene
121
- editorCam.target = Vector3.create(sceneCenter.x, 0, sceneCenter.z)
122
-
123
- updateEditorCamera()
124
- MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = editorCamEntity
125
-
126
- InputModifier.createOrReplace(engine.PlayerEntity, {
127
- mode: InputModifier.Mode.Standard({ disableAll: true }),
128
- })
129
-
130
- console.log('[editor] editor camera ON — WASD pan, Space/Shift up/down, 2/3 zoom, drag to orbit, F focus')
131
- }
132
-
133
- export function deactivateEditorCamera() {
134
- if (!state.editorCamActive) return
135
- state.editorCamActive = false
136
-
137
- MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = undefined
138
-
139
- if (InputModifier.has(engine.PlayerEntity)) {
140
- InputModifier.deleteFrom(engine.PlayerEntity)
141
- }
142
-
143
- console.log('[editor] editor camera OFF')
144
- }
145
-
146
- export function toggleEditorCamera() {
147
- if (state.editorCamActive) deactivateEditorCamera()
148
- else activateEditorCamera()
149
- }
150
-
151
- export function focusSelectedEntity() {
152
- if (state.selectedEntity === undefined || !Transform.has(state.selectedEntity)) return
153
- const info = selectableInfoMap.get(state.selectedEntity)
154
- const entityPos = Transform.get(state.selectedEntity).position
155
- const offset = info?.centerOffset ?? Vector3.Zero()
156
-
157
- editorCam.target = Vector3.create(
158
- entityPos.x + offset.x,
159
- entityPos.y + offset.y,
160
- entityPos.z + offset.z,
161
- )
162
- editorCam.distance = FOCUS_DISTANCE
163
-
164
- updateEditorCamera()
165
- console.log(`[editor] focused on ${info?.name ?? 'entity'}`)
166
- }
167
-
168
- function getCamRight(): Vector3 {
169
- const yawRad = editorCam.yaw * (Math.PI / 180)
170
- return Vector3.create(Math.cos(yawRad), 0, -Math.sin(yawRad))
171
- }
172
-
173
- function getCamForward(): Vector3 {
174
- const yawRad = editorCam.yaw * (Math.PI / 180)
175
- return Vector3.create(Math.sin(yawRad), 0, Math.cos(yawRad))
176
- }
177
-
178
- export function editorCameraSystem(dt: number) {
179
- if (!state.editorActive || !state.editorCamActive) return
180
-
181
- let changed = false
182
- const right = getCamRight()
183
- const forward = getCamForward()
184
-
185
- if (inputSystem.isPressed(InputAction.IA_FORWARD)) {
186
- editorCam.target = Vector3.add(editorCam.target, Vector3.scale(forward, PAN_SPEED * dt))
187
- changed = true
188
- }
189
- if (inputSystem.isPressed(InputAction.IA_BACKWARD)) {
190
- editorCam.target = Vector3.add(editorCam.target, Vector3.scale(forward, -PAN_SPEED * dt))
191
- changed = true
192
- }
193
- if (inputSystem.isPressed(InputAction.IA_RIGHT)) {
194
- editorCam.target = Vector3.add(editorCam.target, Vector3.scale(right, PAN_SPEED * dt))
195
- changed = true
196
- }
197
- if (inputSystem.isPressed(InputAction.IA_LEFT)) {
198
- editorCam.target = Vector3.add(editorCam.target, Vector3.scale(right, -PAN_SPEED * dt))
199
- changed = true
200
- }
201
- if (inputSystem.isPressed(InputAction.IA_JUMP)) {
202
- editorCam.target.y += VERTICAL_SPEED * dt
203
- changed = true
204
- }
205
- if (inputSystem.isPressed(InputAction.IA_WALK)) {
206
- editorCam.target.y -= VERTICAL_SPEED * dt
207
- changed = true
208
- }
209
- if (inputSystem.isPressed(InputAction.IA_ACTION_4)) {
210
- editorCam.distance = Math.max(MIN_DISTANCE, editorCam.distance - ZOOM_SPEED * dt)
211
- changed = true
212
- }
213
- if (inputSystem.isPressed(InputAction.IA_ACTION_5)) {
214
- editorCam.distance = Math.min(MAX_DISTANCE, editorCam.distance + ZOOM_SPEED * dt)
215
- changed = true
216
- }
217
-
218
- if (inputSystem.isPressed(InputAction.IA_POINTER) && !state.isDragging && !gizmoClickConsumed) {
219
- const pointer = PrimaryPointerInfo.getOrNull(engine.RootEntity)
220
- if (pointer && pointer.screenDelta) {
221
- const dx = pointer.screenDelta.x ?? 0
222
- const dy = pointer.screenDelta.y ?? 0
223
- if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
224
- editorCam.yaw += dx * ORBIT_SENSITIVITY
225
- editorCam.pitch = Math.max(MIN_PITCH, Math.min(MAX_PITCH, editorCam.pitch + dy * ORBIT_SENSITIVITY))
226
- changed = true
227
- }
228
- }
229
- }
230
-
231
- if (changed) updateEditorCamera()
232
- }
233
-
234
- // ============================================================
235
- // Drag Lock Camera
236
- // ============================================================
237
-
238
- let lockCamEntity: Entity | undefined
239
-
240
- export function createLockCamera() {
241
- lockCamEntity = engine.addEntity()
242
- Transform.create(lockCamEntity, {
243
- position: Vector3.Zero(),
244
- rotation: Quaternion.Identity(),
245
- })
246
- VirtualCamera.create(lockCamEntity, {
247
- defaultTransition: { transitionMode: VirtualCamera.Transition.Time(0) },
248
- })
249
- editorEntities.add(lockCamEntity)
250
- }
251
-
252
- export function lockCamera() {
253
- if (state.editorCamActive || lockCamEntity === undefined) return
254
- if (!Transform.has(engine.CameraEntity)) return
255
- const camT = Transform.get(engine.CameraEntity)
256
- const lockT = Transform.getMutable(lockCamEntity)
257
- copyVec3(lockT.position, camT.position)
258
- copyQuat(lockT.rotation, camT.rotation)
259
- MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = lockCamEntity
260
- }
261
-
262
- export function unlockCamera() {
263
- if (state.editorCamActive || lockCamEntity === undefined) return
264
- MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = undefined
265
- }
266
-
267
- // ============================================================
268
- // Active Camera Helper
269
- // ============================================================
270
-
271
- /** Returns the active camera transform -- editor cam when active, otherwise player camera. */
272
- export function getActiveCameraTransform() {
273
- if (state.editorCamActive && editorCamEntity !== undefined && Transform.has(editorCamEntity)) {
274
- return Transform.get(editorCamEntity)
275
- }
276
- return Transform.get(engine.CameraEntity)
277
- }