@dcl/asset-packs 2.6.0 → 2.6.1-20251020110502.commit-097464e
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/bin/index.js +837 -522
- package/dclcontext/npc.mdc +224 -285
- package/dclcontext/sdk7-complete-reference.md +39 -12
- package/dclcontext/sdk7-examples.mdc +43 -23
- package/dist/admin-toolkit-ui/Header.js +13 -4
- package/dist/admin-toolkit-ui/ModerationControl/AddUserInput.d.ts +9 -3
- package/dist/admin-toolkit-ui/ModerationControl/AddUserInput.js +75 -58
- package/dist/admin-toolkit-ui/ModerationControl/BanUserDescription.d.ts +6 -0
- package/dist/admin-toolkit-ui/ModerationControl/BanUserDescription.js +16 -0
- package/dist/admin-toolkit-ui/ModerationControl/RemoveAdminConfirmation.js +1 -1
- package/dist/admin-toolkit-ui/ModerationControl/UsersList.d.ts +16 -0
- package/dist/admin-toolkit-ui/ModerationControl/UsersList.js +136 -0
- package/dist/admin-toolkit-ui/ModerationControl/api.d.ts +24 -2
- package/dist/admin-toolkit-ui/ModerationControl/api.js +33 -7
- package/dist/admin-toolkit-ui/ModerationControl/index.d.ts +4 -1
- package/dist/admin-toolkit-ui/ModerationControl/index.js +21 -22
- package/dist/admin-toolkit-ui/ModerationControl/styles/AddUserInputStyles.d.ts +25 -0
- package/dist/admin-toolkit-ui/ModerationControl/styles/AddUserInputStyles.js +73 -0
- package/dist/admin-toolkit-ui/ModerationControl/styles/ModerationControlStyles.d.ts +6 -0
- package/dist/admin-toolkit-ui/ModerationControl/styles/ModerationControlStyles.js +31 -0
- package/dist/admin-toolkit-ui/ModerationControl/styles/UsersListStyles.d.ts +48 -0
- package/dist/admin-toolkit-ui/ModerationControl/styles/UsersListStyles.js +206 -0
- package/dist/admin-toolkit-ui/ModerationControl/utils.d.ts +11 -0
- package/dist/admin-toolkit-ui/ModerationControl/utils.js +41 -0
- package/dist/admin-toolkit-ui/SmartItemsControl.js +4 -4
- package/dist/admin-toolkit-ui/TextAnnouncementsControl.js +5 -3
- package/dist/admin-toolkit-ui/index.d.ts +2 -0
- package/dist/admin-toolkit-ui/index.js +31 -18
- package/dist/bin/index.js +837 -522
- package/package.json +2 -2
- package/dist/admin-toolkit-ui/ModerationControl/AdminList.d.ts +0 -10
- package/dist/admin-toolkit-ui/ModerationControl/AdminList.js +0 -192
package/dclcontext/npc.mdc
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dcl-npc-toolkit Context7 Reference
|
|
2
2
|
|
|
3
3
|
## Installation & Import
|
|
4
|
+
|
|
5
|
+
To install the NPC toolkit, run the following command:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i dcl-npc-toolkit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Then import the toolkit in your code:
|
|
12
|
+
|
|
4
13
|
```typescript
|
|
5
|
-
|
|
6
|
-
import { AvatarShape, Transform, engine } from '@dcl/sdk/ecs'
|
|
7
|
-
import { Vector3, Color3 } from '@dcl/sdk/math'
|
|
8
|
-
import { getPlayer } from '@dcl/sdk/src/players'
|
|
14
|
+
import { createNPC, Dialog } from 'dcl-npc-toolkit'
|
|
9
15
|
```
|
|
10
16
|
|
|
17
|
+
Then you can use the toolkit to create NPCs.
|
|
18
|
+
|
|
19
|
+
|
|
11
20
|
## Basic NPC Creation
|
|
12
21
|
|
|
13
22
|
### Create Simple NPC
|
|
14
23
|
```typescript
|
|
15
|
-
const
|
|
24
|
+
const npcEntity = engine.addEntity()
|
|
16
25
|
|
|
17
26
|
// Add avatar shape component
|
|
18
|
-
AvatarShape.create(
|
|
27
|
+
AvatarShape.create(npcEntity, {
|
|
19
28
|
id: 'npc-001', // Required: unique identifier
|
|
20
29
|
name: 'Guide NPC', // Display name (optional, default: "NPC")
|
|
21
30
|
bodyShape: 'urn:decentraland:off-chain:base-avatars:BaseMale', // Optional
|
|
@@ -31,7 +40,7 @@ AvatarShape.create(npc, {
|
|
|
31
40
|
})
|
|
32
41
|
|
|
33
42
|
// Position the NPC
|
|
34
|
-
Transform.create(
|
|
43
|
+
Transform.create(npcEntity, {
|
|
35
44
|
position: Vector3.create(8, 0.25, 8),
|
|
36
45
|
rotation: { x: 0, y: 0, z: 0, w: 1 }
|
|
37
46
|
})
|
|
@@ -48,6 +57,27 @@ Transform.create(randomNPC, {
|
|
|
48
57
|
})
|
|
49
58
|
```
|
|
50
59
|
|
|
60
|
+
### Create NPC with toolkit (GLB + dialog)
|
|
61
|
+
```typescript
|
|
62
|
+
// Use the toolkit to instantiate an NPC from a GLB with a simple dialog and activation callback
|
|
63
|
+
const guide = createNPC(
|
|
64
|
+
{
|
|
65
|
+
position: { x: 8, y: 0, z: 8 },
|
|
66
|
+
rotation: { x: 0, y: 0, z: 0, w: 1 },
|
|
67
|
+
scale: { x: 1, y: 1, z: 1 }
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: 'custom',
|
|
71
|
+
model: 'models/guide.glb',
|
|
72
|
+
dialog: [{ text: "Welcome!", isEndOfDialog: true } as Dialog],
|
|
73
|
+
onActivate: () => {
|
|
74
|
+
// Called on click; open dialog or run logic
|
|
75
|
+
console.log('Guide activated')
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
51
81
|
## NPC Animations & Emotes
|
|
52
82
|
|
|
53
83
|
### Play Predefined Emotes
|
|
@@ -151,128 +181,153 @@ pointerEventsSystem.onPointerDown(
|
|
|
151
181
|
)
|
|
152
182
|
```
|
|
153
183
|
|
|
154
|
-
### Proximity Interaction
|
|
184
|
+
### Proximity Interaction (TriggerArea)
|
|
155
185
|
```typescript
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
186
|
+
// Use SDK7 TriggerArea directly (no @dcl-sdk/utils)
|
|
187
|
+
import { TriggerArea, triggerAreaEventsSystem, ColliderLayer } from '@dcl/sdk/ecs'
|
|
188
|
+
|
|
189
|
+
// Add a spherical trigger around the NPC
|
|
190
|
+
TriggerArea.setSphere(npcEntity, ColliderLayer.CL_PLAYER)
|
|
191
|
+
Transform.createOrReplace(npcEntity, {
|
|
192
|
+
position: Transform.get(npcEntity).position,
|
|
193
|
+
scale: Vector3.create(3, 3, 3) // radius ~3
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Listen for player entering/leaving
|
|
197
|
+
triggerAreaEventsSystem.onTriggerEnter(npcEntity, () => {
|
|
198
|
+
showNPCDialogue(npcEntity)
|
|
199
|
+
})
|
|
200
|
+
triggerAreaEventsSystem.onTriggerExit(npcEntity, () => {
|
|
201
|
+
hideNPCDialogue()
|
|
202
|
+
})
|
|
173
203
|
```
|
|
174
204
|
|
|
175
205
|
## NPC Dialogue Systems
|
|
176
206
|
|
|
177
207
|
### Simple Text Dialogue
|
|
178
208
|
```typescript
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
margin: { left: -200 }
|
|
195
|
-
}}
|
|
196
|
-
uiBackground={{ color: Color4.create(0, 0, 0, 0.8) }}
|
|
197
|
-
>
|
|
198
|
-
<Label
|
|
199
|
-
value={currentDialogue || ''}
|
|
200
|
-
color={Color4.White()}
|
|
201
|
-
fontSize={16}
|
|
202
|
-
textAlign="middle-center"
|
|
203
|
-
/>
|
|
204
|
-
</UiEntity>
|
|
205
|
-
))
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function hideNPCDialogue() {
|
|
209
|
-
currentDialogue = null
|
|
210
|
-
ReactEcsRenderer.setUiRenderer(() => null)
|
|
211
|
-
}
|
|
209
|
+
// Create a talking NPC using the toolkit's Dialog[]
|
|
210
|
+
const greeter = createNPC(
|
|
211
|
+
{ position: { x: 8, y: 0, z: 8 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } },
|
|
212
|
+
{
|
|
213
|
+
type: 'custom',
|
|
214
|
+
model: 'models/greeter.glb',
|
|
215
|
+
dialog: [
|
|
216
|
+
{ text: "Hello! I'm your guide. How can I help you today?", isEndOfDialog: true } as Dialog
|
|
217
|
+
],
|
|
218
|
+
onActivate: () => {
|
|
219
|
+
// Optional extra behavior when clicked
|
|
220
|
+
console.log('Greeter activated')
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
)
|
|
212
224
|
```
|
|
213
225
|
|
|
214
226
|
### Multi-Choice Dialogue
|
|
215
227
|
```typescript
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
228
|
+
// Create an NPC with a multi-choice dialog flow via toolkit Dialog[]
|
|
229
|
+
const questGiver = createNPC(
|
|
230
|
+
{ position: { x: 10, y: 0, z: 10 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } },
|
|
231
|
+
{
|
|
232
|
+
type: 'custom',
|
|
233
|
+
model: 'models/quest_giver.glb',
|
|
234
|
+
dialog: [
|
|
235
|
+
{
|
|
236
|
+
text: 'Welcome, traveler! What do you seek?',
|
|
237
|
+
isQuestion: true,
|
|
238
|
+
buttons: [
|
|
239
|
+
{ label: 'Tell me about this place', goToDialog: 1 },
|
|
240
|
+
{ label: 'Give me a quest', goToDialog: 2 },
|
|
241
|
+
{ label: 'Goodbye', goToDialog: 3 }
|
|
242
|
+
]
|
|
243
|
+
} as unknown as Dialog,
|
|
244
|
+
{ text: 'These lands are rich with secrets and stories.', isEndOfDialog: true } as Dialog,
|
|
245
|
+
{ text: 'Take this task and return victorious!', isEndOfDialog: true } as Dialog,
|
|
246
|
+
{ text: 'Safe travels!', isEndOfDialog: true } as Dialog
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Toolkit Dialog UI vs Bubble UI (SDK7)
|
|
253
|
+
|
|
254
|
+
When opening dialogs on an EXISTING entity (not created via `createNPC`), the toolkit expects internal per-NPC data to exist. Otherwise, UI helpers (e.g., `getTheme`, `displayDialog`) may crash.
|
|
255
|
+
|
|
256
|
+
- UI Dialog (React HUD)
|
|
257
|
+
1) Mount the toolkit UI once in your scene UI:
|
|
258
|
+
```typescript
|
|
259
|
+
import { NpcUtilsUi } from 'dcl-npc-toolkit'
|
|
220
260
|
|
|
221
|
-
function showDialogueOptions(npcEntity: Entity, options: DialogueOption[]) {
|
|
222
261
|
ReactEcsRenderer.setUiRenderer(() => (
|
|
223
|
-
<UiEntity
|
|
224
|
-
|
|
225
|
-
width: 400,
|
|
226
|
-
height: 200,
|
|
227
|
-
positionType: 'absolute',
|
|
228
|
-
position: { bottom: '20%', left: '50%' },
|
|
229
|
-
margin: { left: -200 }
|
|
230
|
-
}}
|
|
231
|
-
uiBackground={{ color: Color4.create(0, 0, 0, 0.8) }}
|
|
232
|
-
>
|
|
233
|
-
{options.map((option, index) => (
|
|
234
|
-
<Button
|
|
235
|
-
key={index}
|
|
236
|
-
value={option.text}
|
|
237
|
-
variant="secondary"
|
|
238
|
-
fontSize={14}
|
|
239
|
-
onMouseDown={option.action}
|
|
240
|
-
uiTransform={{
|
|
241
|
-
width: 350,
|
|
242
|
-
height: 40,
|
|
243
|
-
margin: { top: 10 }
|
|
244
|
-
}}
|
|
245
|
-
/>
|
|
246
|
-
))}
|
|
262
|
+
<UiEntity uiTransform={{ width: '100%', height: '100%', positionType: 'absolute' }}>
|
|
263
|
+
<NpcUtilsUi />
|
|
247
264
|
</UiEntity>
|
|
248
265
|
))
|
|
249
|
-
|
|
266
|
+
```
|
|
267
|
+
2) Ensure the NPC has toolkit dialog data before opening a window:
|
|
268
|
+
```typescript
|
|
269
|
+
import { addDialog } from 'dcl-npc-toolkit/dist/dialog'
|
|
270
|
+
import { openDialogWindow } from 'dcl-npc-toolkit'
|
|
271
|
+
import { npcDataComponent } from 'dcl-npc-toolkit/dist/npc'
|
|
272
|
+
|
|
273
|
+
function ensureNpcToolkitData(entity: Entity) {
|
|
274
|
+
if (npcDataComponent.has(entity)) return
|
|
275
|
+
npcDataComponent.set(entity as any, {
|
|
276
|
+
introduced: false,
|
|
277
|
+
inCooldown: false,
|
|
278
|
+
coolDownDuration: 5,
|
|
279
|
+
faceUser: undefined,
|
|
280
|
+
walkingSpeed: 2,
|
|
281
|
+
walkingAnim: undefined,
|
|
282
|
+
pathData: undefined,
|
|
283
|
+
currentPathData: [],
|
|
284
|
+
manualStop: false,
|
|
285
|
+
pathIndex: 0,
|
|
286
|
+
state: 'STANDING',
|
|
287
|
+
idleAnim: 'Idle',
|
|
288
|
+
hasBubble: false,
|
|
289
|
+
turnSpeed: 2,
|
|
290
|
+
theme: 'https://decentraland.org/images/ui/light-atlas-v3.png',
|
|
291
|
+
bubbleXOffset: 0,
|
|
292
|
+
bubbleYOffset: 0,
|
|
293
|
+
lastPlayedAnim: 'Idle'
|
|
294
|
+
})
|
|
295
|
+
}
|
|
250
296
|
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
```
|
|
297
|
+
// On setup
|
|
298
|
+
addDialog(npcEntity) // required for dialog state
|
|
299
|
+
ensureNpcToolkitData(npcEntity) // required for UI helpers
|
|
300
|
+
|
|
301
|
+
// On click
|
|
302
|
+
openDialogWindow(npcEntity, dialogs, startIndex)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
- Bubble Dialog (world-space speech bubbles)
|
|
306
|
+
1) Initialize a bubble instance per entity BEFORE calling `talkBubble`:
|
|
307
|
+
```typescript
|
|
308
|
+
import { addDialog } from 'dcl-npc-toolkit/dist/dialog'
|
|
309
|
+
import { createDialogBubble } from 'dcl-npc-toolkit/dist/bubble'
|
|
310
|
+
import { talkBubble } from 'dcl-npc-toolkit'
|
|
311
|
+
|
|
312
|
+
addDialog(npcEntity)
|
|
313
|
+
createDialogBubble(npcEntity)
|
|
314
|
+
talkBubble(npcEntity, dialogs, startIndex)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Common symptoms and fixes:
|
|
318
|
+
- Error: "Cannot set properties of undefined (setting 'script')" in bubble.js → Missing `createDialogBubble(npc)` before `talkBubble`.
|
|
319
|
+
- Error in `<NpcUtilsUi>` caused by `getTheme`/`displayDialog` → Missing `npcDataComponent` for that entity; call `addDialog` and ensure a minimal `npcDataComponent.set(...)` exists before `openDialogWindow`.
|
|
258
320
|
|
|
259
321
|
## NPC Movement & Behavior
|
|
260
322
|
|
|
261
323
|
### Simple Path Following
|
|
262
324
|
```typescript
|
|
263
|
-
import * as utils from '@dcl-sdk/utils'
|
|
264
|
-
|
|
265
325
|
function createPatrollingNPC() {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
Transform.create(patrollingNPC, {
|
|
274
|
-
position: Vector3.create(0, 0.25, 0)
|
|
275
|
-
})
|
|
326
|
+
// Create NPC via toolkit (handles walk/idle animations)
|
|
327
|
+
const patrollingNPC = createNPC(
|
|
328
|
+
{ position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } },
|
|
329
|
+
{ type: 'custom', model: 'models/guard.glb' }
|
|
330
|
+
)
|
|
276
331
|
|
|
277
332
|
// Define patrol path
|
|
278
333
|
const patrolPath = [
|
|
@@ -283,18 +338,20 @@ function createPatrollingNPC() {
|
|
|
283
338
|
Vector3.create(0, 0.25, 0)
|
|
284
339
|
]
|
|
285
340
|
|
|
286
|
-
// Start
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
341
|
+
// Start walking animation via toolkit (handles walk/idle clips)
|
|
342
|
+
// import * as npc from 'dcl-npc-toolkit'
|
|
343
|
+
npc.playAnimation(patrollingNPC, 'Walk', true)
|
|
344
|
+
|
|
345
|
+
// Patrol using Tween helpers; keep looping
|
|
346
|
+
Tween.setMove(patrollingNPC, patrolPath[0], patrolPath[1], 4000, EasingFunction.EF_LINEAR)
|
|
347
|
+
TweenSequence.create(patrollingNPC, {
|
|
348
|
+
sequence: [
|
|
349
|
+
{ duration: 4000, easingFunction: EasingFunction.EF_LINEAR, mode: Tween.Mode.Move({ start: patrolPath[1], end: patrolPath[2] }) },
|
|
350
|
+
{ duration: 4000, easingFunction: EasingFunction.EF_LINEAR, mode: Tween.Mode.Move({ start: patrolPath[2], end: patrolPath[3] }) },
|
|
351
|
+
{ duration: 4000, easingFunction: EasingFunction.EF_LINEAR, mode: Tween.Mode.Move({ start: patrolPath[3], end: patrolPath[4] }) }
|
|
352
|
+
],
|
|
353
|
+
loop: TweenLoop.TL_RESTART
|
|
354
|
+
})
|
|
298
355
|
|
|
299
356
|
return patrollingNPC
|
|
300
357
|
}
|
|
@@ -303,21 +360,16 @@ function createPatrollingNPC() {
|
|
|
303
360
|
### NPC Following Player
|
|
304
361
|
```typescript
|
|
305
362
|
function createFollowingNPC() {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
Transform.create(followingNPC, {
|
|
314
|
-
position: Vector3.create(2, 0.25, 2)
|
|
315
|
-
})
|
|
363
|
+
// Create NPC via toolkit
|
|
364
|
+
const followingNPC = createNPC(
|
|
365
|
+
{ position: { x: 2, y: 0, z: 2 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } },
|
|
366
|
+
{ type: 'custom', model: 'models/companion.glb' }
|
|
367
|
+
)
|
|
316
368
|
|
|
317
|
-
// System to make NPC follow player
|
|
369
|
+
// System to make NPC follow player (read player via engine.PlayerEntity)
|
|
318
370
|
engine.addSystem(() => {
|
|
319
|
-
|
|
320
|
-
|
|
371
|
+
if (!Transform.has(engine.PlayerEntity)) return
|
|
372
|
+
const playerPos = Transform.get(engine.PlayerEntity).position
|
|
321
373
|
|
|
322
374
|
const npcTransform = Transform.getMutable(followingNPC)
|
|
323
375
|
const currentPos = npcTransform.position
|
|
@@ -344,73 +396,38 @@ function createFollowingNPC() {
|
|
|
344
396
|
|
|
345
397
|
## NPC State Management
|
|
346
398
|
|
|
347
|
-
### NPC
|
|
399
|
+
### Toolkit-managed NPC state
|
|
348
400
|
```typescript
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
export class NPCState {
|
|
352
|
-
currentDialogue: string = ''
|
|
353
|
-
isInteracting: boolean = false
|
|
354
|
-
lastInteractionTime: number = 0
|
|
355
|
-
interactionCooldown: number = 5000 // 5 seconds
|
|
356
|
-
}
|
|
401
|
+
// The toolkit tracks basic interaction state and dialog progression internally.
|
|
402
|
+
// Prefer defining behavior via createNPC options and Dialog[] rather than custom components.
|
|
357
403
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
currentDialogue: 'Welcome to my shop!',
|
|
373
|
-
isInteracting: false,
|
|
374
|
-
lastInteractionTime: 0,
|
|
375
|
-
interactionCooldown: 3000
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
return npc
|
|
379
|
-
}
|
|
404
|
+
const merchant = createNPC(
|
|
405
|
+
{ position: { x: 8, y: 0, z: 8 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } },
|
|
406
|
+
{
|
|
407
|
+
type: 'custom',
|
|
408
|
+
model: 'models/merchant.glb',
|
|
409
|
+
dialog: [
|
|
410
|
+
{ text: 'Welcome to my shop!', isEndOfDialog: true } as Dialog
|
|
411
|
+
],
|
|
412
|
+
onActivate: () => {
|
|
413
|
+
// Optional: extra side effects on interaction
|
|
414
|
+
console.log('Merchant activated')
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
)
|
|
380
418
|
```
|
|
381
419
|
|
|
382
|
-
###
|
|
420
|
+
### Interaction flow
|
|
383
421
|
```typescript
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
currentTime - npcState.lastInteractionTime > npcState.interactionCooldown) {
|
|
392
|
-
NPCState.getMutable(entity).isInteracting = false
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
})
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Handle NPC interaction with cooldown
|
|
399
|
-
function interactWithNPC(npcEntity: Entity) {
|
|
400
|
-
const npcState = NPCState.getMutable(npcEntity)
|
|
401
|
-
const currentTime = Date.now()
|
|
402
|
-
|
|
403
|
-
if (npcState.isInteracting) {
|
|
404
|
-
console.log('NPC is busy, please wait...')
|
|
405
|
-
return
|
|
422
|
+
// Clicks are handled via the toolkit's onActivate; use TriggerArea for proximity prompts if needed.
|
|
423
|
+
const helper = createNPC(
|
|
424
|
+
{ position: { x: 6, y: 0, z: 6 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } },
|
|
425
|
+
{
|
|
426
|
+
type: 'custom', model: 'models/helper.glb',
|
|
427
|
+
dialog: [{ text: 'Need assistance?', isEndOfDialog: true } as Dialog],
|
|
428
|
+
onActivate: () => console.log('Helper clicked')
|
|
406
429
|
}
|
|
407
|
-
|
|
408
|
-
npcState.isInteracting = true
|
|
409
|
-
npcState.lastInteractionTime = currentTime
|
|
410
|
-
|
|
411
|
-
// Show dialogue
|
|
412
|
-
showNPCDialogue(npcEntity)
|
|
413
|
-
}
|
|
430
|
+
)
|
|
414
431
|
```
|
|
415
432
|
|
|
416
433
|
## NPC Multiplayer Considerations
|
|
@@ -468,60 +485,22 @@ messageBus.on('npc-interaction', (message) => {
|
|
|
468
485
|
|
|
469
486
|
### NPC with Multiple Dialogue States
|
|
470
487
|
```typescript
|
|
471
|
-
enum NPCState {
|
|
472
|
-
IDLE = 'idle',
|
|
473
|
-
GREETING = 'greeting',
|
|
474
|
-
IN_DIALOGUE = 'dialogue',
|
|
475
|
-
FAREWELL = 'farewell'
|
|
476
|
-
}
|
|
477
|
-
|
|
478
488
|
function createAdvancedNPC() {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
id: 'advanced-npc',
|
|
483
|
-
name: 'Quest Giver'
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
Transform.create(npc, {
|
|
487
|
-
position: Vector3.create(8, 0.25, 8)
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
// Add click interaction
|
|
491
|
-
pointerEventsSystem.onPointerDown(
|
|
489
|
+
// Multi-step dialog with branching via toolkit only
|
|
490
|
+
return createNPC(
|
|
491
|
+
{ position: { x: 8, y: 0, z: 8 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } },
|
|
492
492
|
{
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
493
|
+
type: 'custom', model: 'models/quest_giver.glb',
|
|
494
|
+
dialog: [
|
|
495
|
+
{ text: 'Hello traveler! I have a quest for you.', isQuestion: true, buttons: [
|
|
496
|
+
{ label: 'Tell me about the quest', goToDialog: 1 },
|
|
497
|
+
{ label: 'I\'ll pass for now', goToDialog: 2 }
|
|
498
|
+
] } as unknown as Dialog,
|
|
499
|
+
{ text: 'Retrieve the ancient token from the ruins to the east.', isEndOfDialog: true } as Dialog,
|
|
500
|
+
{ text: 'Come back if you change your mind!', isEndOfDialog: true } as Dialog
|
|
501
|
+
]
|
|
502
|
+
}
|
|
497
503
|
)
|
|
498
|
-
|
|
499
|
-
return npc
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function handleNPCClick(npcEntity: Entity) {
|
|
503
|
-
const currentState = getNPCState(npcEntity) // Custom function to get state
|
|
504
|
-
|
|
505
|
-
switch (currentState) {
|
|
506
|
-
case NPCState.IDLE:
|
|
507
|
-
setNPCState(npcEntity, NPCState.GREETING)
|
|
508
|
-
showDialogue(npcEntity, "Hello traveler! I have a quest for you.")
|
|
509
|
-
break
|
|
510
|
-
case NPCState.GREETING:
|
|
511
|
-
setNPCState(npcEntity, NPCState.IN_DIALOGUE)
|
|
512
|
-
showDialogueOptions(npcEntity, [
|
|
513
|
-
{ text: "Tell me about the quest", action: () => showQuestDetails(npcEntity) },
|
|
514
|
-
{ text: "I'll pass for now", action: () => setNPCState(npcEntity, NPCState.FAREWELL) }
|
|
515
|
-
])
|
|
516
|
-
break
|
|
517
|
-
case NPCState.IN_DIALOGUE:
|
|
518
|
-
// Already in dialogue, do nothing
|
|
519
|
-
break
|
|
520
|
-
case NPCState.FAREWELL:
|
|
521
|
-
setNPCState(npcEntity, NPCState.IDLE)
|
|
522
|
-
showDialogue(npcEntity, "Come back if you change your mind!")
|
|
523
|
-
break
|
|
524
|
-
}
|
|
525
504
|
}
|
|
526
505
|
```
|
|
527
506
|
|
|
@@ -673,46 +652,6 @@ const npc1 = npcPool.createNPC(Vector3.create(4, 0.25, 4))
|
|
|
673
652
|
const npc2 = npcPool.createNPC(Vector3.create(8, 0.25, 8))
|
|
674
653
|
```
|
|
675
654
|
|
|
676
|
-
### NPC LOD (Level of Detail)
|
|
677
|
-
```typescript
|
|
678
|
-
function createLODNPC() {
|
|
679
|
-
const npc = engine.addEntity()
|
|
680
|
-
|
|
681
|
-
AvatarShape.create(npc, {
|
|
682
|
-
id: 'lod-npc',
|
|
683
|
-
name: 'LOD NPC'
|
|
684
|
-
})
|
|
685
|
-
|
|
686
|
-
Transform.create(npc, {
|
|
687
|
-
position: Vector3.create(8, 0.25, 8)
|
|
688
|
-
})
|
|
689
|
-
|
|
690
|
-
// System to adjust NPC detail based on distance
|
|
691
|
-
engine.addSystem(() => {
|
|
692
|
-
const playerPos = utils.getPlayerPosition()
|
|
693
|
-
if (!playerPos) return
|
|
694
|
-
|
|
695
|
-
const npcPos = Transform.get(npc).position
|
|
696
|
-
const distance = Vector3.distance(playerPos, npcPos)
|
|
697
|
-
|
|
698
|
-
const avatarShape = AvatarShape.getMutable(npc)
|
|
699
|
-
|
|
700
|
-
if (distance > 20) {
|
|
701
|
-
// Far distance: hide name and disable interactions
|
|
702
|
-
avatarShape.name = ''
|
|
703
|
-
} else if (distance > 10) {
|
|
704
|
-
// Medium distance: show name, no interactions
|
|
705
|
-
avatarShape.name = 'LOD NPC'
|
|
706
|
-
} else {
|
|
707
|
-
// Close distance: full functionality
|
|
708
|
-
avatarShape.name = 'LOD NPC (Click to interact)'
|
|
709
|
-
}
|
|
710
|
-
})
|
|
711
|
-
|
|
712
|
-
return npc
|
|
713
|
-
}
|
|
714
|
-
```
|
|
715
|
-
|
|
716
655
|
## Best Practices
|
|
717
656
|
|
|
718
657
|
### NPC Design Guidelines
|
|
@@ -500,20 +500,14 @@ Material.setPbrMaterial(entity, {
|
|
|
500
500
|
#### Texture Animation
|
|
501
501
|
```typescript
|
|
502
502
|
// Animate texture offset
|
|
503
|
-
Tween.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
duration: 2000,
|
|
509
|
-
easingFunction: EasingFunction.EF_LINEAR
|
|
510
|
-
})
|
|
503
|
+
Tween.setTextureMove(entity,
|
|
504
|
+
Vector2.create(0, 0),
|
|
505
|
+
Vector2.create(1, 0),
|
|
506
|
+
2000
|
|
507
|
+
)
|
|
511
508
|
|
|
512
509
|
// Loop texture animation
|
|
513
|
-
TweenSequence.create(entity, {
|
|
514
|
-
sequence: [],
|
|
515
|
-
loop: TweenLoop.TL_RESTART
|
|
516
|
-
})
|
|
510
|
+
TweenSequence.create(entity, { sequence: [], loop: TweenLoop.TL_RESTART })
|
|
517
511
|
```
|
|
518
512
|
|
|
519
513
|
#### Transparency
|
|
@@ -561,6 +555,39 @@ Tip: set `path` to a specific mesh node to target only that part; use `Material.
|
|
|
561
555
|
|
|
562
556
|
### Move Entities
|
|
563
557
|
|
|
558
|
+
#### Tween helpers (concise syntax)
|
|
559
|
+
```typescript
|
|
560
|
+
// Move between two points
|
|
561
|
+
Tween.setMove(entity,
|
|
562
|
+
Vector3.create(4, 1, 4),
|
|
563
|
+
Vector3.create(8, 1, 8),
|
|
564
|
+
2000,
|
|
565
|
+
{ faceDirection: false, easingFunction: EasingFunction.EF_LINEAR }
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
// Rotate between two rotations
|
|
569
|
+
Tween.setRotate(entity,
|
|
570
|
+
Quaternion.fromEulerDegrees(0, 0, 0),
|
|
571
|
+
Quaternion.fromEulerDegrees(0, 180, 0),
|
|
572
|
+
700,
|
|
573
|
+
EasingFunction.EF_EASEOUTBOUNCE
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
// Scale between sizes
|
|
577
|
+
Tween.setScale(entity,
|
|
578
|
+
Vector3.create(1, 1, 1),
|
|
579
|
+
Vector3.create(4, 4, 4),
|
|
580
|
+
2000,
|
|
581
|
+
EasingFunction.EF_LINEAR
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
// Continuous movement (meters/second)
|
|
585
|
+
Tween.setMoveContinuous(entity, Vector3.create(0, 0, 1), 0.7)
|
|
586
|
+
|
|
587
|
+
// Continuous rotation (degrees/second)
|
|
588
|
+
Tween.setRotateContinuous(entity, Quaternion.fromEulerDegrees(0, -1, 0), 700)
|
|
589
|
+
```
|
|
590
|
+
|
|
564
591
|
#### Tween System
|
|
565
592
|
```typescript
|
|
566
593
|
// Move between points
|