From 1f4ac179424385d3104de88e9e57a65585c16eb2 Mon Sep 17 00:00:00 2001 From: AnisLahouar <38791856+AnisLahouar@users.noreply.github.com> Date: Wed, 21 Jun 2023 20:57:09 +0100 Subject: [PATCH 1/5] feat: add other to activate and walkaway --- src/npc.ts | 39 +++++++++++++++++++++++---------------- src/types.ts | 14 +++++++------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/npc.ts b/src/npc.ts index 2750696..241d364 100644 --- a/src/npc.ts +++ b/src/npc.ts @@ -79,12 +79,12 @@ export function create( createDialogBubble(npc, npcDataComponent.get(npc).bubbleHeight) } - onActivateCbs.set(npc, ()=>{ - data.onActivate() + onActivateCbs.set(npc, (other: Entity)=>{ + data.onActivate(other) }) if (data && data.hasOwnProperty("onWalkAway")) { - onWalkAwayCbs.set(npc, ()=>{ + onWalkAwayCbs.set(npc, (other: Entity)=>{ if(!data || !data.continueOnWalkAway){ if(npcDialogComponent.has(npc)){ npcDialogComponent.get(npc).visible = false @@ -95,7 +95,7 @@ export function create( npcDialogComponent.get(npc).visible = false } } - data.onWalkAway!() + data.onWalkAway!(other) }) } @@ -192,7 +192,7 @@ function addClickReactions(npc:Entity, data:NPCData){ npc, function () { if (isCooldown.has(npc) || (npcDialogComponent.get(npc).visible)) return - activate(npc) + activate(npc, engine.PlayerEntity) }, { button: activateButton, @@ -211,7 +211,7 @@ function addTriggerArea(npc:Entity, data:NPCData){ let triggerData: TriggerData = {} if (!data || (data && !data.onlyExternalTrigger && !data.onlyClickTrigger && !data.onlyETrigger)){ - onActivateCbs.set(npc, ()=>{ + onActivateCbs.set(npc, (other:Entity)=>{ if (isCooldown.has(npc)) { console.log(npc, ' in cooldown') return @@ -223,15 +223,15 @@ function addTriggerArea(npc:Entity, data:NPCData){ ) { return } - data.onActivate() + data.onActivate(other) }) triggerData.onCameraEnter = onActivateCbs.get(npc) } // when exiting trigger if (!data || (data && !data.continueOnWalkAway)) { - triggerData.onCameraExit = () => { - handleWalkAway(npc) + triggerData.onCameraExit = (other) => { + handleWalkAway(npc, other) } } @@ -240,7 +240,7 @@ function addTriggerArea(npc:Entity, data:NPCData){ !data || (data && !data.onlyExternalTrigger && !data.onlyClickTrigger && !data.onlyETrigger) ) { - triggerData.onCameraEnter = () => { + triggerData.onCameraEnter = (other) => { if (isCooldown.has(npc)) { console.log(npc, ' in cooldown') return @@ -252,13 +252,20 @@ function addTriggerArea(npc:Entity, data:NPCData){ // ) { // return // } - activate(npc) + activate(npc, other) } } // add trigger if (triggerData.onCameraEnter || triggerData.onCameraExit) { - utils.triggers.addTrigger(npc,254,1,[{type:'sphere', position: Vector3.Zero(), radius: data.reactDistance != undefined ? data.reactDistance : 6}], triggerData.onCameraEnter ? triggerData.onCameraEnter : undefined, triggerData.onCameraExit ? triggerData.onCameraExit : undefined, Color3.Red()) + utils.triggers.addTrigger(npc, + utils.NO_LAYERS, + utils.LAYER_1, + [{type:'sphere', position: Vector3.Zero(), radius: data.reactDistance != undefined ? data.reactDistance : 6}], + (other)=> {if(triggerData.onCameraEnter) triggerData.onCameraEnter(other)}, + (other)=> {if(triggerData.onCameraExit) triggerData.onCameraExit(other)}, + Color3.Red() + ) } } @@ -446,7 +453,7 @@ export function isActiveNpcSet(){ /** * Calls the NPC's activation function (set on NPC definition). If NPC has `faceUser` = true, it will rotate to face the player. It starts a cooldown counter to avoid reactivating. */ -export function activate(npc:Entity) { +export function activate(npc: Entity, other: Entity) { if(activeNPC != 0){ console.log('we have a current npc, needto remove') @@ -455,7 +462,7 @@ export function activate(npc:Entity) { } activeNPC = npc - onActivateCbs.get(npc)() + onActivateCbs.get(npc)(other) let npcData = npcDataComponent.get(npc) if (npcData.faceUser) { @@ -505,7 +512,7 @@ function endInteraction(npc:Entity) { /** * Ends interaction and calls the onWalkAway function */ -export function handleWalkAway(npc:Entity) { +export function handleWalkAway(npc:Entity, other: Entity) { let npcData = npcDataComponent.get(npc) if (npcData.state == NPCState.FOLLOWPATH) { return @@ -514,7 +521,7 @@ export function handleWalkAway(npc:Entity) { endInteraction(npc) if (onWalkAwayCbs.get(npc)) { - onWalkAwayCbs.get(npc)() + onWalkAwayCbs.get(npc)(other) } } diff --git a/src/types.ts b/src/types.ts index 21ac35f..0cd4307 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { PBAvatarShape, PBGltfContainer } from "@dcl/sdk/ecs" +import { Entity, PBAvatarShape, PBGltfContainer } from "@dcl/sdk/ecs" import { Vector3 } from "@dcl/sdk/math" /** @@ -54,10 +54,10 @@ export type Dialog = { export type TriggerData = { layer?: number triggeredByLayer?: number - onTriggerEnter?: () => void - onTriggerExit?: () => void - onCameraEnter?: () => void - onCameraExit?: () => void + onTriggerEnter?: (other: Entity) => void + onTriggerExit?: (other: Entity) => void + onCameraEnter?: (other: Entity) => void + onCameraExit?: (other: Entity) => void enableDebug?: boolean } @@ -140,8 +140,8 @@ export type Dialog = { onlyExternalTrigger?: boolean onlyClickTrigger?: boolean onlyETrigger?: boolean - onActivate: () => void - onWalkAway?: () => void + onActivate: (other: Entity) => void + onWalkAway?: (other: Entity) => void continueOnWalkAway?: boolean darkUI?: boolean coolDownDuration?: number From 88403ffe784da1e14a19c2345c7926858d28cb71 Mon Sep 17 00:00:00 2001 From: AnisLahouar <38791856+AnisLahouar@users.noreply.github.com> Date: Wed, 21 Jun 2023 20:59:21 +0100 Subject: [PATCH 2/5] lint: lint npc npcData and types files --- src/npc.ts | 1076 ++++++++++++++++++++++++++---------------------- src/npcData.ts | 30 +- src/types.ts | 448 ++++++++++---------- 3 files changed, 813 insertions(+), 741 deletions(-) diff --git a/src/npc.ts b/src/npc.ts index 241d364..85a85e3 100644 --- a/src/npc.ts +++ b/src/npc.ts @@ -1,18 +1,32 @@ -import * as utils from '@dcl-sdk/utils'; -import { Animator, AvatarShape, engine, Entity, GltfContainer, InputAction, MeshCollider, MeshRenderer, PBAvatarShape, PBGltfContainer, pointerEventsSystem, Transform, TransformType } from '@dcl/sdk/ecs'; -import { Color3, Quaternion, Vector3 } from '@dcl/sdk/math'; -import { bubbles, closeBubble, createDialogBubble, openBubble } from './bubble'; -import { IsFollowingPath, TrackUserFlag } from './components'; -import { addDialog, closeDialog, findDialogByName, npcDialogComponent, openDialog } from './dialog'; -import { faceUserSystem, handleBubbletyping, handleDialogTyping, handlePathTimes, inputListenerSystem } from './systems'; -import { Dialog, FollowPathData, ImageData, NPCData, NPCPathType, NPCState, NPCType, TriggerData } from './types'; -import { darkTheme, lightTheme } from './ui'; - -export const walkingTimers: Map = new Map() +import * as utils from '@dcl-sdk/utils' +import { + Animator, + AvatarShape, + engine, + Entity, + GltfContainer, + InputAction, + MeshCollider, + MeshRenderer, + PBAvatarShape, + PBGltfContainer, + pointerEventsSystem, + Transform, + TransformType +} from '@dcl/sdk/ecs' +import { Color3, Quaternion, Vector3 } from '@dcl/sdk/math' +import { bubbles, closeBubble, createDialogBubble, openBubble } from './bubble' +import { IsFollowingPath, TrackUserFlag } from './components' +import { addDialog, closeDialog, findDialogByName, npcDialogComponent, openDialog } from './dialog' +import { faceUserSystem, handleBubbletyping, handleDialogTyping, handlePathTimes, inputListenerSystem } from './systems' +import { Dialog, FollowPathData, ImageData, NPCData, NPCPathType, NPCState, NPCType, TriggerData } from './types' +import { darkTheme, lightTheme } from './ui' + +export const walkingTimers: Map = new Map() export const npcDataComponent: Map = new Map() -export let NULL_NPC:Entity = 0 as Entity -export let activeNPC:Entity = NULL_NPC -export let blankDialog:number = 0 +export let NULL_NPC: Entity = 0 as Entity +export let activeNPC: Entity = NULL_NPC +export let blankDialog: number = 0 engine.addSystem(handlePathTimes) engine.addSystem(handleDialogTyping) @@ -24,591 +38,655 @@ engine.addSystem(inputListenerSystem) const isCooldown: Map = new Map() const onActivateCbs: Map = new Map() const onWalkAwayCbs: Map = new Map() -const animTimers: Map = new Map() +const animTimers: Map = new Map() const pointReachedCallbacks: Map = new Map() const onFinishCallbacks: Map = new Map() -export function showDebug(debug:boolean){ - utils.triggers.enableDebugDraw(debug) +export function showDebug(debug: boolean) { + utils.triggers.enableDebugDraw(debug) } -export function getData(npc:Entity){ - return npcDataComponent.get(npc) +export function getData(npc: Entity) { + return npcDataComponent.get(npc) } -export function create( - transform: any, - data: NPCData -){ - let npc = engine.addEntity() - - let t:TransformType = {position: transform.position ? transform.position : Vector3.create(0,0,0), rotation: transform.rotation ? transform.rotation : Quaternion.Zero(), scale: transform.scale ? transform.scale : Vector3.One()} - Transform.create(npc, t) - - npcDataComponent.set(npc,{ - introduced: false, - inCooldown: false, - coolDownDuration: data && data.coolDownDuration ? data.coolDownDuration : 5, - faceUser: data && data.faceUser ? data.faceUser : undefined, - walkingSpeed:2, - walkingAnim: data && data.walkingAnim ? data.walkingAnim : undefined, - pathData: data.pathData ? data.pathData : undefined, - currentPathData: [], - manualStop:false, - pathIndex:0, - state:NPCState.STANDING, - idleAnim: data && data.idleAnim ? data.idleAnim : "Idle", - bubbleHeight: data && data.textBubble && data.bubbleHeight ? data.bubbleHeight : undefined, - bubbleSound: data.dialogSound ? data.dialogSound : undefined, - hasBubble: data && data.textBubble ? true : false, - turnSpeed: data && data.turningSpeed ? data.turningSpeed : 2, - theme: data.darkUI ? darkTheme : lightTheme, - bubbleXOffset: data.bubbleXOffset? data.bubbleXOffset : 0, - bubbleYOffset: data.bubbleYOffset? data.bubbleYOffset : 0 - }) - - if(data && data.noUI){} - else if(data && data.portrait){ - addDialog(npc, data && data.dialogSound ? data.dialogSound : undefined, typeof data.portrait === `string` ? { path: data.portrait } : data.portrait) +export function create(transform: any, data: NPCData) { + let npc = engine.addEntity() + + let t: TransformType = { + position: transform.position ? transform.position : Vector3.create(0, 0, 0), + rotation: transform.rotation ? transform.rotation : Quaternion.Zero(), + scale: transform.scale ? transform.scale : Vector3.One() + } + Transform.create(npc, t) + + npcDataComponent.set(npc, { + introduced: false, + inCooldown: false, + coolDownDuration: data && data.coolDownDuration ? data.coolDownDuration : 5, + faceUser: data && data.faceUser ? data.faceUser : undefined, + walkingSpeed: 2, + walkingAnim: data && data.walkingAnim ? data.walkingAnim : undefined, + pathData: data.pathData ? data.pathData : undefined, + currentPathData: [], + manualStop: false, + pathIndex: 0, + state: NPCState.STANDING, + idleAnim: data && data.idleAnim ? data.idleAnim : 'Idle', + bubbleHeight: data && data.textBubble && data.bubbleHeight ? data.bubbleHeight : undefined, + bubbleSound: data.dialogSound ? data.dialogSound : undefined, + hasBubble: data && data.textBubble ? true : false, + turnSpeed: data && data.turningSpeed ? data.turningSpeed : 2, + theme: data.darkUI ? darkTheme : lightTheme, + bubbleXOffset: data.bubbleXOffset ? data.bubbleXOffset : 0, + bubbleYOffset: data.bubbleYOffset ? data.bubbleYOffset : 0 + }) + + if (data && data.noUI) { + } else if (data && data.portrait) { + addDialog( + npc, + data && data.dialogSound ? data.dialogSound : undefined, + typeof data.portrait === `string` ? { path: data.portrait } : data.portrait + ) + } else { + addDialog(npc, data && data.dialogSound ? data.dialogSound : undefined) + } + + if (data && data.textBubble) { + createDialogBubble(npc, npcDataComponent.get(npc).bubbleHeight) + } + + onActivateCbs.set(npc, (other: Entity) => { + data.onActivate(other) + }) + + if (data && data.hasOwnProperty('onWalkAway')) { + onWalkAwayCbs.set(npc, (other: Entity) => { + if (!data || !data.continueOnWalkAway) { + if (npcDialogComponent.has(npc)) { + npcDialogComponent.get(npc).visible = false + } + } else { + if (npcDialogComponent.has(npc)) { + npcDialogComponent.get(npc).visible = false } - else{ - addDialog(npc, data && data.dialogSound ? data.dialogSound : undefined) - } - - if (data && data.textBubble) { - createDialogBubble(npc, npcDataComponent.get(npc).bubbleHeight) } - - onActivateCbs.set(npc, (other: Entity)=>{ - data.onActivate(other) + data.onWalkAway!(other) }) + } - if (data && data.hasOwnProperty("onWalkAway")) { - onWalkAwayCbs.set(npc, (other: Entity)=>{ - if(!data || !data.continueOnWalkAway){ - if(npcDialogComponent.has(npc)){ - npcDialogComponent.get(npc).visible = false - } - } - else{ - if(npcDialogComponent.has(npc)){ - npcDialogComponent.get(npc).visible = false - } - } - data.onWalkAway!(other) - }) - } + addNPCBones(npc, data) + addClickReactions(npc, data) + addTriggerArea(npc, data) - addNPCBones(npc, data) - addClickReactions(npc, data) - addTriggerArea(npc, data) - - if (data && data.pathData && data.pathData.speed) { - let npcData = npcDataComponent.get(npc) - npcData.walkingSpeed = data.pathData.speed - } + if (data && data.pathData && data.pathData.speed) { + let npcData = npcDataComponent.get(npc) + npcData.walkingSpeed = data.pathData.speed + } - if (data && data.coolDownDuration) { - let npcData = npcDataComponent.get(npc) - npcData.coolDownDuration = data.coolDownDuration - } + if (data && data.coolDownDuration) { + let npcData = npcDataComponent.get(npc) + npcData.coolDownDuration = data.coolDownDuration + } - if (data && data.pathData){ - let npcData = npcDataComponent.get(npc) - npcData.pathData.loop = true - followPath(npc, npcData.pathData) - } + if (data && data.pathData) { + let npcData = npcDataComponent.get(npc) + npcData.pathData.loop = true + followPath(npc, npcData.pathData) + } - return npc + return npc } -function addNPCBones(npc:Entity, data:NPCData){ - const modelIsString = data && data.model && typeof data.model === `string` - const modelAvatarData:PBAvatarShape|undefined = modelIsString ? undefined : data.model && (data.model as any).bodyShape ? data.model as PBAvatarShape : undefined - const modelGLTFData:PBGltfContainer|undefined = modelIsString ? undefined : data.model && (data.model as any).src ? data.model as PBGltfContainer : undefined - - switch(data.type){ - case NPCType.AVATAR: - AvatarShape.create(npc, - !data || !data.model || !modelAvatarData ? - { - id: "npc", - name: "NPC", - bodyShape:"urn:decentraland:off-chain:base-avatars:BaseMale", - emotes: [], - wearables: [ - "urn:decentraland:off-chain:base-avatars:f_eyes_00", - "urn:decentraland:off-chain:base-avatars:f_eyebrows_00", - "urn:decentraland:off-chain:base-avatars:f_mouth_00", - "urn:decentraland:off-chain:base-avatars:comfy_sport_sandals", - "urn:decentraland:off-chain:base-avatars:soccer_pants", - "urn:decentraland:off-chain:base-avatars:elegant_sweater", - ], - } : modelAvatarData - ) - break; - - case NPCType.CUSTOM: - GltfContainer.create(npc, - modelIsString && typeof data.model === `string` ? - { src: data && data.model ? data.model : "" } - : modelGLTFData - ) - Animator.create(npc, { - states:[{ - name:data && data.idleAnim ? data.idleAnim : 'Idle', - clip:data && data.idleAnim ? data.idleAnim : 'Idle', - loop:true - }] - }) - - let npcData = npcDataComponent.get(npc) - npcData.idleAnim = data && data.idleAnim ? data.idleAnim : 'Idle' - npcData.lastPlayedAnim = npcDataComponent.get(npc).idleAnim - - Animator.playSingleAnimation(npc, npcDataComponent.get(npc).idleAnim) - - - if (data && data.walkingAnim) { - npcDataComponent.get(npc).walkingAnim = data.walkingAnim - let animations = Animator.getMutable(npc) - animations.states.push({name:data.walkingAnim, clip: data.walkingAnim, loop:true}) +function addNPCBones(npc: Entity, data: NPCData) { + const modelIsString = data && data.model && typeof data.model === `string` + const modelAvatarData: PBAvatarShape | undefined = modelIsString + ? undefined + : data.model && (data.model as any).bodyShape + ? (data.model as PBAvatarShape) + : undefined + const modelGLTFData: PBGltfContainer | undefined = modelIsString + ? undefined + : data.model && (data.model as any).src + ? (data.model as PBGltfContainer) + : undefined + + switch (data.type) { + case NPCType.AVATAR: + AvatarShape.create( + npc, + !data || !data.model || !modelAvatarData + ? { + id: 'npc', + name: 'NPC', + bodyShape: 'urn:decentraland:off-chain:base-avatars:BaseMale', + emotes: [], + wearables: [ + 'urn:decentraland:off-chain:base-avatars:f_eyes_00', + 'urn:decentraland:off-chain:base-avatars:f_eyebrows_00', + 'urn:decentraland:off-chain:base-avatars:f_mouth_00', + 'urn:decentraland:off-chain:base-avatars:comfy_sport_sandals', + 'urn:decentraland:off-chain:base-avatars:soccer_pants', + 'urn:decentraland:off-chain:base-avatars:elegant_sweater' + ] } + : modelAvatarData + ) + break - break; + case NPCType.CUSTOM: + GltfContainer.create( + npc, + modelIsString && typeof data.model === `string` ? { src: data && data.model ? data.model : '' } : modelGLTFData + ) + Animator.create(npc, { + states: [ + { + name: data && data.idleAnim ? data.idleAnim : 'Idle', + clip: data && data.idleAnim ? data.idleAnim : 'Idle', + loop: true + } + ] + }) + + let npcData = npcDataComponent.get(npc) + npcData.idleAnim = data && data.idleAnim ? data.idleAnim : 'Idle' + npcData.lastPlayedAnim = npcDataComponent.get(npc).idleAnim + + Animator.playSingleAnimation(npc, npcDataComponent.get(npc).idleAnim) + + if (data && data.walkingAnim) { + npcDataComponent.get(npc).walkingAnim = data.walkingAnim + let animations = Animator.getMutable(npc) + animations.states.push({ name: data.walkingAnim, clip: data.walkingAnim, loop: true }) + } - case NPCType.BLANK: - MeshRenderer.setBox(npc) - MeshCollider.setBox(npc) - break; + break + case NPCType.BLANK: + MeshRenderer.setBox(npc) + MeshCollider.setBox(npc) + break + } +} + +function addClickReactions(npc: Entity, data: NPCData) { + let activateButton = data && data.onlyClickTrigger ? InputAction.IA_POINTER : InputAction.IA_PRIMARY + + pointerEventsSystem.onPointerDown( + npc, + function () { + if (isCooldown.has(npc) || npcDialogComponent.get(npc).visible) return + activate(npc, engine.PlayerEntity) + }, + { + button: activateButton, + hoverText: data && data.hoverText ? data.hoverText : 'Talk', + showFeedback: data && data.onlyExternalTrigger ? false : true } + ) + + if (data && data.onlyExternalTrigger) { + pointerEventsSystem.removeOnPointerDown(npc) + } } -function addClickReactions(npc:Entity, data:NPCData){ - let activateButton = data && data.onlyClickTrigger ? InputAction.IA_POINTER : InputAction.IA_PRIMARY +function addTriggerArea(npc: Entity, data: NPCData) { + let triggerData: TriggerData = {} - pointerEventsSystem.onPointerDown( - npc, - function () { - if (isCooldown.has(npc) || (npcDialogComponent.get(npc).visible)) return - activate(npc, engine.PlayerEntity) - }, - { - button: activateButton, - hoverText: data && data.hoverText ? data.hoverText : 'Talk', - showFeedback: data && data.onlyExternalTrigger ? false : true, - } - ) + if (!data || (data && !data.onlyExternalTrigger && !data.onlyClickTrigger && !data.onlyETrigger)) { + onActivateCbs.set(npc, (other: Entity) => { + if (isCooldown.has(npc)) { + console.log(npc, ' in cooldown') + return + } else if ( + (npcDialogComponent.has(npc) && npcDialogComponent.get(npc).visible) || + (data && data.onlyExternalTrigger) || + (data && data.onlyClickTrigger) + ) { + return + } + data.onActivate(other) + }) + triggerData.onCameraEnter = onActivateCbs.get(npc) + } + + // when exiting trigger + if (!data || (data && !data.continueOnWalkAway)) { + triggerData.onCameraExit = (other) => { + handleWalkAway(npc, other) + } + } - if (data && data.onlyExternalTrigger) { - pointerEventsSystem.removeOnPointerDown(npc) - } + // when entering trigger + if (!data || (data && !data.onlyExternalTrigger && !data.onlyClickTrigger && !data.onlyETrigger)) { + triggerData.onCameraEnter = (other) => { + if (isCooldown.has(npc)) { + console.log(npc, ' in cooldown') + return + } + // else if ( + // (this.dialog && this.dialog.isDialogOpen) || + // (data && data.onlyExternalTrigger) || + // (data && data.onlyClickTrigger) + // ) { + // return + // } + activate(npc, other) + } + } + + // add trigger + if (triggerData.onCameraEnter || triggerData.onCameraExit) { + utils.triggers.addTrigger( + npc, + utils.NO_LAYERS, + utils.LAYER_1, + [{ type: 'sphere', position: Vector3.Zero(), radius: data.reactDistance != undefined ? data.reactDistance : 6 }], + (other) => { + if (triggerData.onCameraEnter) triggerData.onCameraEnter(other) + }, + (other) => { + if (triggerData.onCameraExit) triggerData.onCameraExit(other) + }, + Color3.Red() + ) + } } -function addTriggerArea(npc:Entity, data:NPCData){ - - let triggerData: TriggerData = {} - - if (!data || (data && !data.onlyExternalTrigger && !data.onlyClickTrigger && !data.onlyETrigger)){ - onActivateCbs.set(npc, (other:Entity)=>{ - if (isCooldown.has(npc)) { - console.log(npc, ' in cooldown') - return - } - else if ( - (npcDialogComponent.has(npc) && npcDialogComponent.get(npc).visible) || - (data && data.onlyExternalTrigger) || - (data && data.onlyClickTrigger) - ) { - return - } - data.onActivate(other) - }) - triggerData.onCameraEnter = onActivateCbs.get(npc) - } +export function followPath(npc: Entity, data?: FollowPathData) { + let npcData = npcDataComponent.get(npc) + let path: any[] = [] - // when exiting trigger - if (!data || (data && !data.continueOnWalkAway)) { - triggerData.onCameraExit = (other) => { - handleWalkAway(npc, other) - } + if (npcData.faceUser) { + if (TrackUserFlag.has(npc)) { + TrackUserFlag.deleteFrom(npc) } - - // when entering trigger - if ( - !data || - (data && !data.onlyExternalTrigger && !data.onlyClickTrigger && !data.onlyETrigger) - ) { - triggerData.onCameraEnter = (other) => { - if (isCooldown.has(npc)) { - console.log(npc, ' in cooldown') - return - } - // else if ( - // (this.dialog && this.dialog.isDialogOpen) || - // (data && data.onlyExternalTrigger) || - // (data && data.onlyClickTrigger) - // ) { - // return - // } - activate(npc, other) - } + } + + if (npcData.manualStop) { + let duration = npcData.pathData.totalDuration + let currentTimer: number = walkingTimers.get(npc)! + console.log('current time is', currentTimer) + if (currentTimer) { + duration -= currentTimer } - // add trigger - if (triggerData.onCameraEnter || triggerData.onCameraExit) { - utils.triggers.addTrigger(npc, - utils.NO_LAYERS, - utils.LAYER_1, - [{type:'sphere', position: Vector3.Zero(), radius: data.reactDistance != undefined ? data.reactDistance : 6}], - (other)=> {if(triggerData.onCameraEnter) triggerData.onCameraEnter(other)}, - (other)=> {if(triggerData.onCameraExit) triggerData.onCameraExit(other)}, - Color3.Red() + let path: any[] = [] + npcData.pathData.path.forEach((p: any) => { + path.push(p) + }) + path.splice(0, npcData.pathIndex) + + let pos = Transform.get(npc).position + path.unshift(Vector3.create(pos.x, pos.y, pos.z)) + walkNPC( + npc, + npcData, + npcData.pathData.pathType, + duration, + path, + pointReachedCallbacks.get(npc), + onFinishCallbacks.get(npc) + ) + } else { + if (data) { + npcData.pathData = data + + if (data.startingPoint) { + data.path?.splice(0, data.startingPoint - 1) + } + + let pos = Transform.get(npc).position + path.push(Vector3.create(pos.x, pos.y, pos.z)) + data.path?.forEach((p) => { + path.push(p) + }) + + onFinishCallbacks.set(npc, () => { + console.log('on finished callback') + if (data && data.onFinishCallback && !data.loop) { + data.onFinishCallback() + } + stopPath(npc) + }) + + pointReachedCallbacks.set(npc, () => { + console.log('on point reached callback') + let data = npcDataComponent.get(npc) + data.pathIndex += 1 + data.onReachedPointCallback ? data.onReachedPointCallback : undefined + }) + walkNPC( + npc, + npcData, + data.pathType!, + data.totalDuration, + path, + pointReachedCallbacks.get(npc), + onFinishCallbacks.get(npc) ) + } else { + if (npcData.manualStop) { + console.log('we have manual stop, need to pick back up where we left off') + } else { + console.log('we are trying to follow a path witout starting one prior') + } } + } } -export function followPath(npc:Entity, data?:FollowPathData){ - - let npcData = npcDataComponent.get(npc) - let path:any[] =[] +function walkNPC( + npc: Entity, + npcData: any, + type: NPCPathType, + duration: number, + path: Vector3[], + pointReachedCallback?: any, + finishedCallback?: any +) { + if (IsFollowingPath.has(npc)) { + IsFollowingPath.deleteFrom(npc) + walkingTimers.delete(npc) + } + IsFollowingPath.create(npc) - if(npcData.faceUser){ - if(TrackUserFlag.has(npc)){ - TrackUserFlag.deleteFrom(npc) + if (type) { + if (type == NPCPathType.RIGID_PATH) { + utils.paths.startStraightPath( + npc, + path, + duration, + true, + () => { + finishedCallback() + }, + () => { + pointReachedCallback() } + ) + } else { + utils.paths.startSmoothPath( + npc, + path, + duration, + 30, + true, + () => { + finishedCallback() + }, + () => { + pointReachedCallback() + } + ) } + } else { + utils.paths.startSmoothPath( + npc, + path, + duration, + 20, + true, + () => { + finishedCallback() + }, + () => { + pointReachedCallback() + } + ) + } + + if (npcData.walkingAnim) { + clearAnimationTimer(npc) + Animator.playSingleAnimation(npc, npcDataComponent.get(npc).walkingAnim, true) + npcData.lastPlayedAnim = npcDataComponent.get(npc).walkingAnim + } + npcData.state = NPCState.FOLLOWPATH + npcData.manualStop = false +} - if(npcData.manualStop){ +export function stopWalking(npc: Entity, duration?: number, finished?: boolean) { + let npcData = npcDataComponent.get(npc) + npcData.state = NPCState.STANDING + npcData.manualStop = true + + stopPath(npc) + + if (duration) { + utils.timers.setTimeout(() => { + //if (this.dialog && this.dialog.isDialogOpen) return + if (npcData.path) { + Animator.stopAllAnimations(npc, true) + if (npcDataComponent.get(npc).walkingAnim) { + clearAnimationTimer(npc) + Animator.playSingleAnimation(npc, npcDataComponent.get(npc).walkingAnim, true) + npcData.lastPlayedAnim = npcDataComponent.get(npc).walkingAnim + } let duration = npcData.pathData.totalDuration - let currentTimer:number = walkingTimers.get(npc)! + let currentTimer: number = walkingTimers.get(npc)! console.log('current time is', currentTimer) - if(currentTimer){ - duration -= currentTimer + if (currentTimer) { + duration -= currentTimer } - let path:any[] = [] - npcData.pathData.path.forEach((p:any)=>{ - path.push(p) + let path: any[] = [] + npcData.pathData.path.forEach((p: any) => { + path.push(p) }) - path.splice(0,npcData.pathIndex) + path.splice(0, npcData.pathIndex) let pos = Transform.get(npc).position path.unshift(Vector3.create(pos.x, pos.y, pos.z)) - walkNPC(npc,npcData, npcData.pathData.pathType, duration, path, pointReachedCallbacks.get(npc), onFinishCallbacks.get(npc)) - } - else{ - if(data){ - npcData.pathData = data - - if(data.startingPoint){ - data.path?.splice(0,data.startingPoint - 1) - } - - let pos = Transform.get(npc).position - path.push(Vector3.create(pos.x, pos.y, pos.z)) - data.path?.forEach((p)=>{ - path.push(p) - }) - - onFinishCallbacks.set(npc,()=>{ - console.log('on finished callback') - if(data && data.onFinishCallback && !data.loop){ - data.onFinishCallback() - } - stopPath(npc) - }) - - pointReachedCallbacks.set(npc, ()=>{ - console.log('on point reached callback') - let data = npcDataComponent.get(npc) - data.pathIndex += 1 - data.onReachedPointCallback ? data.onReachedPointCallback : undefined - }) - walkNPC(npc, npcData, data.pathType!, data.totalDuration, path, pointReachedCallbacks.get(npc), onFinishCallbacks.get(npc)) - }else{ - if(npcData.manualStop){ - console.log('we have manual stop, need to pick back up where we left off') - } - else{ - console.log('we are trying to follow a path witout starting one prior') - } - } - - } - - - } -function walkNPC(npc:Entity, npcData:any, type:NPCPathType, duration:number, path:Vector3[], pointReachedCallback?:any, finishedCallback?:any){ - - if(IsFollowingPath.has(npc)){ - IsFollowingPath.deleteFrom(npc) - walkingTimers.delete(npc) - } - IsFollowingPath.create(npc) - - if(type){ - if(type== NPCPathType.RIGID_PATH){ - utils.paths.startStraightPath(npc, path, duration,true, - ()=>{finishedCallback()}, ()=>{pointReachedCallback()}) - } - else{ - utils.paths.startSmoothPath(npc, path, duration, 30, true, - ()=>{finishedCallback()}, ()=>{pointReachedCallback()}) - } - } - else{ - utils.paths.startSmoothPath(npc, path, duration, 20, true, - ()=>{finishedCallback()}, ()=>{pointReachedCallback()}) - } - - if (npcData.walkingAnim) { - clearAnimationTimer(npc) - Animator.playSingleAnimation(npc, npcDataComponent.get(npc).walkingAnim, true) - npcData.lastPlayedAnim = npcDataComponent.get(npc).walkingAnim + //npcData.manualStop = false + walkNPC( + npc, + npcData, + npcData.pathData.pathType, + duration, + path, + pointReachedCallbacks.get(npc), + onFinishCallbacks.get(npc) + ) } - npcData.state = NPCState.FOLLOWPATH - npcData.manualStop = false + }, duration * 1000) + } } -export function stopWalking(npc:Entity, duration?: number, finished?:boolean) { - let npcData = npcDataComponent.get(npc) - npcData.state = NPCState.STANDING - npcData.manualStop = true - - stopPath(npc) - - if (duration) { - utils.timers.setTimeout(()=>{ - //if (this.dialog && this.dialog.isDialogOpen) return - if(npcData.path){ - Animator.stopAllAnimations(npc, true) - if(npcDataComponent.get(npc).walkingAnim){ - clearAnimationTimer(npc) - Animator.playSingleAnimation(npc, npcDataComponent.get(npc).walkingAnim,true) - npcData.lastPlayedAnim = npcDataComponent.get(npc).walkingAnim - } - let duration = npcData.pathData.totalDuration - let currentTimer:number = walkingTimers.get(npc)! - console.log('current time is', currentTimer) - if(currentTimer){ - duration -= currentTimer - } - - let path:any[] = [] - npcData.pathData.path.forEach((p:any)=>{ - path.push(p) - }) - path.splice(0,npcData.pathIndex) - - let pos = Transform.get(npc).position - path.unshift(Vector3.create(pos.x, pos.y, pos.z)) - - //npcData.manualStop = false - walkNPC(npc,npcData, npcData.pathData.pathType, duration, path, pointReachedCallbacks.get(npc), onFinishCallbacks.get(npc)) - } - - },duration * 1000) - } -} +export function stopPath(npc: Entity) { + utils.paths.stopPath(npc) + IsFollowingPath.deleteFrom(npc) -export function stopPath(npc:Entity){ - utils.paths.stopPath(npc) - IsFollowingPath.deleteFrom(npc) - - let npcData = npcDataComponent.get(npc) - if (npcData.walkingAnim) { - clearAnimationTimer(npc) - Animator.playSingleAnimation(npc, npcDataComponent.get(npc).idleAnim) - npcData.lastPlayedAnim = npcData.idleAnim - } - - if(!npcData.manualStop){ - if(npcData.pathData.loop){ - npcData.pathIndex = 0 - walkingTimers.delete(npc) - console.log('we are looping path', npcData) - followPath(npc, npcData.pathData) - console.log(npcData) - } + let npcData = npcDataComponent.get(npc) + if (npcData.walkingAnim) { + clearAnimationTimer(npc) + Animator.playSingleAnimation(npc, npcDataComponent.get(npc).idleAnim) + npcData.lastPlayedAnim = npcData.idleAnim + } + + if (!npcData.manualStop) { + if (npcData.pathData.loop) { + npcData.pathIndex = 0 + walkingTimers.delete(npc) + console.log('we are looping path', npcData) + followPath(npc, npcData.pathData) + console.log(npcData) } + } } -export function clearNPC(){ - activeNPC = NULL_NPC +export function clearNPC() { + activeNPC = NULL_NPC } -export function setActiveNPC(npc:Entity){ - activeNPC = npc +export function setActiveNPC(npc: Entity) { + activeNPC = npc } - -export function isActiveNpcSet(){ - return activeNPC && npcDialogComponent.has(activeNPC) +export function isActiveNpcSet() { + return activeNPC && npcDialogComponent.has(activeNPC) } - /** * Calls the NPC's activation function (set on NPC definition). If NPC has `faceUser` = true, it will rotate to face the player. It starts a cooldown counter to avoid reactivating. */ export function activate(npc: Entity, other: Entity) { - - if(activeNPC != 0){ - console.log('we have a current npc, needto remove') - endInteraction(activeNPC) - // closeDialog(activeNPC) + if (activeNPC != 0) { + console.log('we have a current npc, needto remove') + endInteraction(activeNPC) + // closeDialog(activeNPC) + } + + activeNPC = npc + onActivateCbs.get(npc)(other) + + let npcData = npcDataComponent.get(npc) + if (npcData.faceUser) { + if (TrackUserFlag.has(npc)) { + TrackUserFlag.deleteFrom(npc) } - activeNPC = npc - onActivateCbs.get(npc)(other) - - let npcData = npcDataComponent.get(npc) - if (npcData.faceUser) { - if(TrackUserFlag.has(npc)){ - TrackUserFlag.deleteFrom(npc) - } - - TrackUserFlag.create(npc,{ - lockXZRotation:true, - active:true, - rotSpeed:npcData.turnSpeed - }) - } - isCooldown.set(npc, true) - npcData.inCooldown = true - - utils.timers.setTimeout( - function() { - isCooldown.delete(npc) - npcDataComponent.get(npc).inCooldown = false - }, - 1000 * npcData.coolDownDuration - ) - console.log('activated npc,', npcDataComponent.get(npc)) + TrackUserFlag.create(npc, { + lockXZRotation: true, + active: true, + rotSpeed: npcData.turnSpeed + }) + } + isCooldown.set(npc, true) + npcData.inCooldown = true + + utils.timers.setTimeout(function () { + isCooldown.delete(npc) + npcDataComponent.get(npc).inCooldown = false + }, 1000 * npcData.coolDownDuration) + console.log('activated npc,', npcDataComponent.get(npc)) } -function endInteraction(npc:Entity) { - let npcData = npcDataComponent.get(npc) - npcData.state = NPCState.STANDING +function endInteraction(npc: Entity) { + let npcData = npcDataComponent.get(npc) + npcData.state = NPCState.STANDING - if (npcDialogComponent.has(npc)){//} && npcDialogComponent.get(npc).visible) { - closeDialog(npc) - } + if (npcDialogComponent.has(npc)) { + //} && npcDialogComponent.get(npc).visible) { + closeDialog(npc) + } - if(npcData.faceUser){ - if(TrackUserFlag.has(npc)){ - TrackUserFlag.deleteFrom(npc) - } - } + if (npcData.faceUser) { + if (TrackUserFlag.has(npc)) { + TrackUserFlag.deleteFrom(npc) + } + } - console.log('ending interaction', npcData, bubbles.get(npc)) - if(npcData.hasBubble && bubbles.get(npc).isBubbleOpen){ - closeBubble(npc) - } + console.log('ending interaction', npcData, bubbles.get(npc)) + if (npcData.hasBubble && bubbles.get(npc).isBubbleOpen) { + closeBubble(npc) + } } /** * Ends interaction and calls the onWalkAway function */ -export function handleWalkAway(npc:Entity, other: Entity) { - let npcData = npcDataComponent.get(npc) - if (npcData.state == NPCState.FOLLOWPATH) { - return - } +export function handleWalkAway(npc: Entity, other: Entity) { + let npcData = npcDataComponent.get(npc) + if (npcData.state == NPCState.FOLLOWPATH) { + return + } - endInteraction(npc) + endInteraction(npc) - if (onWalkAwayCbs.get(npc)) { - onWalkAwayCbs.get(npc)(other) - } + if (onWalkAwayCbs.get(npc)) { + onWalkAwayCbs.get(npc)(other) + } } -export function playAnimation(npc:Entity, anim:string, noLoop?:boolean, duration?:number){ - let animations = Animator.getMutable(npc) - if(animations.states.filter((animation)=> animation.name === anim).length == 0){ - animations.states.push({name:anim, clip:anim, loop: noLoop? false : true}) - } +export function playAnimation(npc: Entity, anim: string, noLoop?: boolean, duration?: number) { + let animations = Animator.getMutable(npc) + if (animations.states.filter((animation) => animation.name === anim).length == 0) { + animations.states.push({ name: anim, clip: anim, loop: noLoop ? false : true }) + } - let npcData = npcDataComponent.get(npc) - if(npcData.state == NPCState.FOLLOWPATH){ - utils.paths.stopPath(npc) - } + let npcData = npcDataComponent.get(npc) + if (npcData.state == NPCState.FOLLOWPATH) { + utils.paths.stopPath(npc) + } - clearAnimationTimer(npc) + clearAnimationTimer(npc) - Animator.stopAllAnimations(npc, true) - Animator.playSingleAnimation(npc, anim, true) - if(duration){ - console.log('have a duration to play animation') + Animator.stopAllAnimations(npc, true) + Animator.playSingleAnimation(npc, anim, true) + if (duration) { + console.log('have a duration to play animation') + clearAnimationTimer(npc) + animTimers.set( + npc, + utils.timers.setTimeout(() => { clearAnimationTimer(npc) - animTimers.set(npc, utils.timers.setTimeout(()=>{ - clearAnimationTimer(npc) - Animator.stopAllAnimations(npc, true) - if(npcData.idleAnim){ - Animator.playSingleAnimation(npc, npcData.idleAnim) - npcData.lastPlayedAnim = npcData.idleAnim - } - }, 1000 * duration))// - } + Animator.stopAllAnimations(npc, true) + if (npcData.idleAnim) { + Animator.playSingleAnimation(npc, npcData.idleAnim) + npcData.lastPlayedAnim = npcData.idleAnim + } + }, 1000 * duration) + ) // + } - npcData.lastPlayedAnim = anim + npcData.lastPlayedAnim = anim } -export function changeIdleAnim(npc:Entity, animation:string, play?:boolean){ - let npcData = npcDataComponent.get(npc) - npcData.idleAnim = animation +export function changeIdleAnim(npc: Entity, animation: string, play?: boolean) { + let npcData = npcDataComponent.get(npc) + npcData.idleAnim = animation - let animations = Animator.getMutable(npc) - if(animations.states.filter((anim)=> anim.name === animation).length == 0){ - animations.states.push({name:animation, clip:animation, loop: true}) - } + let animations = Animator.getMutable(npc) + if (animations.states.filter((anim) => anim.name === animation).length == 0) { + animations.states.push({ name: animation, clip: animation, loop: true }) + } - if(play){ - playAnimation(npc, animation, true) - npcDataComponent.get(npc).lastPlayedAnim = animation - } + if (play) { + playAnimation(npc, animation, true) + npcDataComponent.get(npc).lastPlayedAnim = animation + } } -export function talkBubble(npc:Entity, script:Dialog[], startIndex?:number | string){ - openBubble(npc, script,startIndex) +export function talkBubble(npc: Entity, script: Dialog[], startIndex?: number | string) { + openBubble(npc, script, startIndex) } -export function createDialogWindow(defaultPortrait?:ImageData, sound?:string){ - let dialog = engine.addEntity() - addDialog(dialog,sound, defaultPortrait) - return dialog +export function createDialogWindow(defaultPortrait?: ImageData, sound?: string) { + let dialog = engine.addEntity() + addDialog(dialog, sound, defaultPortrait) + return dialog } -export function openDialogWindow(npc:Entity, dialog:Dialog[], startIndex?:number | string){ - activeNPC = npc - - if(npcDialogComponent.has(npc)){ - let index:any - if (!startIndex) { - index = 0 - } else if (typeof startIndex === 'number') { - index = startIndex - } else { - index = findDialogByName(dialog, startIndex) - } - openDialog(npc,dialog, index) +export function openDialogWindow(npc: Entity, dialog: Dialog[], startIndex?: number | string) { + activeNPC = npc + + if (npcDialogComponent.has(npc)) { + let index: any + if (!startIndex) { + index = 0 + } else if (typeof startIndex === 'number') { + index = startIndex + } else { + index = findDialogByName(dialog, startIndex) } + openDialog(npc, dialog, index) + } } -export function closeDialogWindow(window:Entity){ - let dialog = npcDialogComponent.get(window) - if(window){ - closeDialog(dialog) - } +export function closeDialogWindow(window: Entity) { + let dialog = npcDialogComponent.get(window) + if (window) { + closeDialog(dialog) + } } function clearAnimationTimer(npc: Entity): boolean { - if (animTimers.has(npc)) { - utils.timers.clearTimeout(animTimers.get(npc) as number) - animTimers.delete(npc) - return true - } - return false -} \ No newline at end of file + if (animTimers.has(npc)) { + utils.timers.clearTimeout(animTimers.get(npc) as number) + animTimers.delete(npc) + return true + } + return false +} diff --git a/src/npcData.ts b/src/npcData.ts index afc6355..dc25b9a 100644 --- a/src/npcData.ts +++ b/src/npcData.ts @@ -1,16 +1,14 @@ -import { Schemas, engine } from "@dcl/sdk/ecs"; -export const NPCDataComponent = engine.defineComponent( - "npcdatacomponent", - { - introduced: Schemas.Boolean, - inCooldown: Schemas.Boolean, - coolDownDuration: Schemas.Number, - faceUser: Schemas.Boolean, - walkingSpeed: Schemas.Number, - bubbleHeight: Schemas.Number, - state: Schemas.String, - walkingAnim: Schemas.String, - idleAnim: Schemas.String, - lastPlayedAnim: Schemas.String, - path:Schemas.Array(Schemas.Vector3) - }) \ No newline at end of file +import { Schemas, engine } from '@dcl/sdk/ecs' +export const NPCDataComponent = engine.defineComponent('npcdatacomponent', { + introduced: Schemas.Boolean, + inCooldown: Schemas.Boolean, + coolDownDuration: Schemas.Number, + faceUser: Schemas.Boolean, + walkingSpeed: Schemas.Number, + bubbleHeight: Schemas.Number, + state: Schemas.String, + walkingAnim: Schemas.String, + idleAnim: Schemas.String, + lastPlayedAnim: Schemas.String, + path: Schemas.Array(Schemas.Vector3) +}) diff --git a/src/types.ts b/src/types.ts index 0cd4307..4ec8fba 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ -import { Entity, PBAvatarShape, PBGltfContainer } from "@dcl/sdk/ecs" -import { Vector3 } from "@dcl/sdk/math" +import { Entity, PBAvatarShape, PBGltfContainer } from '@dcl/sdk/ecs' +import { Vector3 } from '@dcl/sdk/math' /** * Fragment of a conversation with an NPC @@ -22,227 +22,223 @@ import { Vector3 } from "@dcl/sdk/math" * */ export type Dialog = { - text: string - name?: string - fontSize?: number - offsetX?: number - offsetY?: number - typeSpeed?: number - isEndOfDialog?: boolean - triggeredByNext?: () => void - portrait?: ImageData - image?: ImageData - isQuestion?: boolean - isFixedScreen?: boolean - buttons?: ButtonData[] - audio?: string - skipable?: boolean - timeOn?: number - } - - /** - * - * @typedef {Object} TriggerData - Object with data for a NPCTriggerComponent - * @property {number} layer layer of the Trigger, useful to discriminate between trigger events. You can set multiple layers by using a | symbol. - * @property {number} triggeredByLayer against which layers to check collisions - * @property {(entity: Entity) => void } onTriggerEnter callback when an entity of a valid layer enters the trigger area - * @property {(entity: Entity) => void} onTriggerExit callback when an entity of a valid layer leaves the trigger area - * @property {() => void} onCameraEnter callback when the player enters the trigger area - * @property {() => void} onCameraExit callback when the player leaves the trigger area - * @property {boolean} enableDebug when true makes the trigger area visible for debug purposes. - */ - export type TriggerData = { - layer?: number - triggeredByLayer?: number - onTriggerEnter?: (other: Entity) => void - onTriggerExit?: (other: Entity) => void - onCameraEnter?: (other: Entity) => void - onCameraExit?: (other: Entity) => void - enableDebug?: boolean - } - - /** - * Data for Button to show on a question in a Dialog entry - * - * @typedef {Object} ButtonData - Object with data for a Dialog UI button - * @property {string|number} goToDialog The index or name of the next dialog entry to display when activated. - * @property {string} label The label to show on the button. - * @property {() => void} triggeredActions An additional function to run whenever the button is activated - * @property {number} fontSize Font size of the text - * @property {number}offsetX Offset of the text on the X axis, relative to its normal position. - * @property {number} offsetY Offset of the text on the Y axis, relative to its normal position. - * - */ - export type ButtonData = { - goToDialog: number | string - label: string - triggeredActions?: () => void - fontSize?: number - offsetX?: number - offsetY?: number - } - - export enum ButtonStyles { - E = `e`, - F = `f`, - DARK = `dark`, - RED = `red`, - ROUNDBLACK = `roundblack`, - ROUNDWHITE = `roundwhite`, - ROUNDSILVER = `roundsilver`, - ROUNDGOLD = `roundgold`, - SQUAREBLACK = `squareblack`, - SQUAREWHITE = `squarewhite`, - SQUARESILVER = `squaresilver`, - SQUAREGOLD = `squaregold`, - WHITE = `white` - } - - /** - * An NPC capable of having conversations with the player, and play different animations. - * - * @typedef {Object} NPCData Object with data to instance a new NPC - * @property {string|ImageData} portrait 2D image to show on the left-hand side of the dialog window. The structure of an `ImageData` object is described in detail below. - * @property {number} reactDistance Radius in meters for the player to activate the NPC or trigger the `onWalkAway()` function when leaving the radius. - * @property {string} idleAnim Name of the idle animation in the model. This animation is always looped. After playing a non-looping animation it returns to looping this one. - * @property {boolean} faceUser Set if the NPC rotates to face the user while active. - * @property {boolean} onlyExternalTrigger If true, the NPC can't be activated by clicking or walking near. Just by calling its `activate()` function. - * @property {boolean} onlyClickTrigger If true, the NPC can't be activated by walking near. Just by clicking on it or calling its `activate()` function. - * @property {boolean} onlyETrigger If true, the NPC can't be activated by walking near. Just by pressing E on it or calling its `activate()` function. - * @property {() => void} onWalkAway Function to call every time the player walks out of the `reactDistance` radius. - * @property {boolean} continueOnWalkAway f true,when the player walks out of the `reactDistance` radius, the dialog window stays open and the NPC keeps turning to face the player (if applicable). It doesn't affect the triggering of the `onWalkAway()` function. - * @property {boolean} darkUI If true, the dialog UI uses the dark theme. - * @property {number} coolDownDuration Change the cooldown period for activating the NPC again. The number is in seconds. - * @property {string} hoverText Set the UI hover feedback when pointing the cursor at the NPC. _TALK_ by default. - * @property {string} dialogSound Path to sound file to play once for every line of dialog read on the UI. - * @property {string} walkingAnim Animation to play when walking with followPath - * @property {number} walkingSpeed Default speed to use when walking with followPath - * @property {Vector3[]} path Array of Vector3 points representing the default path to walk over. The NPC will walk looping over these points - * @property {boolean} textBubble If true, the NPC can display text bubbles with dialogs - * @property {number} bubbleHeight The default height to display text bubbles over the NPC's position - * @property {boolean} noUI If true, no UI dialog elements are constructed. The NPC can use speech bubbles. - * - */ - - export type NPCData = { - - type:NPCType - body?:NPCBodyType - model?:string | PBGltfContainer | PBAvatarShape - - - walkingAnim?:string - portrait?: string | ImageData - reactDistance?: number - idleAnim?: string - faceUser?: boolean - turningSpeed?: number - onlyExternalTrigger?: boolean - onlyClickTrigger?: boolean - onlyETrigger?: boolean - onActivate: (other: Entity) => void - onWalkAway?: (other: Entity) => void - continueOnWalkAway?: boolean - darkUI?: boolean - coolDownDuration?: number - hoverText?: string - dialogSound?: string - //dialogCustomTheme?: Texture - textBubble?: boolean - bubbleHeight?: number - noUI?: boolean - - pathData?:FollowPathData - - bubbleXOffset?: number - bubbleYOffset?: number - } - - /** - * Make an NPC walk following a path - * - * @typedef {Object} FollowPathData - Object with data to describe a path that an NPC can walk - * @property {Vector3[]} path Array of `Vector3` positions to walk over. - * @property {number} speed Speed to move at while walking this path. If no `speed` or `totalDuration` is provided, it uses the NPC's `walkingSpeed`, which is _2_ by default. - * @property {number} totalDuration The duration in _seconds_ that the whole path should take. The NPC will move at the constant speed required to finish in that time. This value overrides that of the _speed_. - * @property {boolean} loop _boolean_ If true, the NPC walks in circles over the provided set of points in the path. _false_ by default, unless the NPC is initiated with a `path`, in which case it starts as _true_. - * @property {boolean} curve _boolean_ If true, the path is traced a single smooth curve that passes over each of the indicated points. The curve is made out of straight-line segments, the path is stored with 4 times as many points as originally defined. _false_ by default. - * @property {number} startingPoint Index position for what point to start from on the path. _0_ by default. - * @property {() => void} onFinishCallback Function to call when the NPC finished walking over all the points on the path. This is only called when `loop` is _false_. - * @property {() => void} onReachedPointCallback Function to call once every time the NPC reaches a point in the path. - * - */ - export type FollowPathData = { - startingPoint?: number - loop?: boolean - curve?: boolean - totalDuration: number - speed?: number - path?: Vector3[] - pathType?: NPCPathType - onFinishCallback?: () => void - onReachedPointCallback?: () => void - } - - /** - * Cut out a section of an image file - * - * @typedef {Object} ImageSection - Object with data to only display a section of an image - * @property {number} sourceWidth Width in pixels to select from image, starting from the sourceLeft, going right - * @property {number} sourceHeight Height in pixels to select from image, starting from the sourceTop, going down - * @property {number} sourceLeft Leftmost pixel to select from image - * @property {number} sourceTop Topmost pixel to select from image - * - */ - export type ImageSection = { - sourceWidth: number - sourceHeight: number - sourceLeft?: number - sourceTop?: number - } - - /** - * - * - * @typedef {Object} ImageData - Object with data for displaying an image - * @property {string} path Path to the image file. - * @property {number} offsetX Offset on X, relative to the normal position of the image. - * @property {number} offsetY Offset on Y, relative to the normal position of the image. - * @property {number} height The height to show the image onscreen. - * @property {number} width The width to show the image onscreen. - * @property {ImageSection} section Use only a section of the image file, useful when arranging multiple icons into an image atlas. This field takes an `ImageSection` object, specifying `sourceWidth` and `sourceHeight`, and optionally also `sourceLeft` and `sourceTop`. - * - */ - export type ImageData = { - path: string - offsetX?: number - offsetY?: number - height?: number - width?: number - section?: ImageSection - } - - export enum NPCState { - STANDING = 'standing', - TALKING = 'talking', - FOLLOWPATH = 'followPath' - //FOLLOWPLAYER = 'followPlayer' - } - - export enum NPCBodyType { - MALE= "BaseMale", - FEMALE= "BaseFemale" - } - - export enum NPCType { - BLANK = "blank", - CUSTOM = "custom", - AVATAR = 'avatar' - } - - - export enum NPCPathType { - SMOOTH_PATH = 'smooth', //will follow the path but can cut sharp corners - RIGID_PATH = 'rigid', //will ensure each corner is hit - } - \ No newline at end of file + text: string + name?: string + fontSize?: number + offsetX?: number + offsetY?: number + typeSpeed?: number + isEndOfDialog?: boolean + triggeredByNext?: () => void + portrait?: ImageData + image?: ImageData + isQuestion?: boolean + isFixedScreen?: boolean + buttons?: ButtonData[] + audio?: string + skipable?: boolean + timeOn?: number +} + +/** + * + * @typedef {Object} TriggerData - Object with data for a NPCTriggerComponent + * @property {number} layer layer of the Trigger, useful to discriminate between trigger events. You can set multiple layers by using a | symbol. + * @property {number} triggeredByLayer against which layers to check collisions + * @property {(entity: Entity) => void } onTriggerEnter callback when an entity of a valid layer enters the trigger area + * @property {(entity: Entity) => void} onTriggerExit callback when an entity of a valid layer leaves the trigger area + * @property {() => void} onCameraEnter callback when the player enters the trigger area + * @property {() => void} onCameraExit callback when the player leaves the trigger area + * @property {boolean} enableDebug when true makes the trigger area visible for debug purposes. + */ +export type TriggerData = { + layer?: number + triggeredByLayer?: number + onTriggerEnter?: (other: Entity) => void + onTriggerExit?: (other: Entity) => void + onCameraEnter?: (other: Entity) => void + onCameraExit?: (other: Entity) => void + enableDebug?: boolean +} + +/** + * Data for Button to show on a question in a Dialog entry + * + * @typedef {Object} ButtonData - Object with data for a Dialog UI button + * @property {string|number} goToDialog The index or name of the next dialog entry to display when activated. + * @property {string} label The label to show on the button. + * @property {() => void} triggeredActions An additional function to run whenever the button is activated + * @property {number} fontSize Font size of the text + * @property {number}offsetX Offset of the text on the X axis, relative to its normal position. + * @property {number} offsetY Offset of the text on the Y axis, relative to its normal position. + * + */ +export type ButtonData = { + goToDialog: number | string + label: string + triggeredActions?: () => void + fontSize?: number + offsetX?: number + offsetY?: number +} + +export enum ButtonStyles { + E = `e`, + F = `f`, + DARK = `dark`, + RED = `red`, + ROUNDBLACK = `roundblack`, + ROUNDWHITE = `roundwhite`, + ROUNDSILVER = `roundsilver`, + ROUNDGOLD = `roundgold`, + SQUAREBLACK = `squareblack`, + SQUAREWHITE = `squarewhite`, + SQUARESILVER = `squaresilver`, + SQUAREGOLD = `squaregold`, + WHITE = `white` +} + +/** + * An NPC capable of having conversations with the player, and play different animations. + * + * @typedef {Object} NPCData Object with data to instance a new NPC + * @property {string|ImageData} portrait 2D image to show on the left-hand side of the dialog window. The structure of an `ImageData` object is described in detail below. + * @property {number} reactDistance Radius in meters for the player to activate the NPC or trigger the `onWalkAway()` function when leaving the radius. + * @property {string} idleAnim Name of the idle animation in the model. This animation is always looped. After playing a non-looping animation it returns to looping this one. + * @property {boolean} faceUser Set if the NPC rotates to face the user while active. + * @property {boolean} onlyExternalTrigger If true, the NPC can't be activated by clicking or walking near. Just by calling its `activate()` function. + * @property {boolean} onlyClickTrigger If true, the NPC can't be activated by walking near. Just by clicking on it or calling its `activate()` function. + * @property {boolean} onlyETrigger If true, the NPC can't be activated by walking near. Just by pressing E on it or calling its `activate()` function. + * @property {() => void} onWalkAway Function to call every time the player walks out of the `reactDistance` radius. + * @property {boolean} continueOnWalkAway f true,when the player walks out of the `reactDistance` radius, the dialog window stays open and the NPC keeps turning to face the player (if applicable). It doesn't affect the triggering of the `onWalkAway()` function. + * @property {boolean} darkUI If true, the dialog UI uses the dark theme. + * @property {number} coolDownDuration Change the cooldown period for activating the NPC again. The number is in seconds. + * @property {string} hoverText Set the UI hover feedback when pointing the cursor at the NPC. _TALK_ by default. + * @property {string} dialogSound Path to sound file to play once for every line of dialog read on the UI. + * @property {string} walkingAnim Animation to play when walking with followPath + * @property {number} walkingSpeed Default speed to use when walking with followPath + * @property {Vector3[]} path Array of Vector3 points representing the default path to walk over. The NPC will walk looping over these points + * @property {boolean} textBubble If true, the NPC can display text bubbles with dialogs + * @property {number} bubbleHeight The default height to display text bubbles over the NPC's position + * @property {boolean} noUI If true, no UI dialog elements are constructed. The NPC can use speech bubbles. + * + */ + +export type NPCData = { + type: NPCType + body?: NPCBodyType + model?: string | PBGltfContainer | PBAvatarShape + + walkingAnim?: string + portrait?: string | ImageData + reactDistance?: number + idleAnim?: string + faceUser?: boolean + turningSpeed?: number + onlyExternalTrigger?: boolean + onlyClickTrigger?: boolean + onlyETrigger?: boolean + onActivate: (other: Entity) => void + onWalkAway?: (other: Entity) => void + continueOnWalkAway?: boolean + darkUI?: boolean + coolDownDuration?: number + hoverText?: string + dialogSound?: string + //dialogCustomTheme?: Texture + textBubble?: boolean + bubbleHeight?: number + noUI?: boolean + + pathData?: FollowPathData + + bubbleXOffset?: number + bubbleYOffset?: number +} + +/** + * Make an NPC walk following a path + * + * @typedef {Object} FollowPathData - Object with data to describe a path that an NPC can walk + * @property {Vector3[]} path Array of `Vector3` positions to walk over. + * @property {number} speed Speed to move at while walking this path. If no `speed` or `totalDuration` is provided, it uses the NPC's `walkingSpeed`, which is _2_ by default. + * @property {number} totalDuration The duration in _seconds_ that the whole path should take. The NPC will move at the constant speed required to finish in that time. This value overrides that of the _speed_. + * @property {boolean} loop _boolean_ If true, the NPC walks in circles over the provided set of points in the path. _false_ by default, unless the NPC is initiated with a `path`, in which case it starts as _true_. + * @property {boolean} curve _boolean_ If true, the path is traced a single smooth curve that passes over each of the indicated points. The curve is made out of straight-line segments, the path is stored with 4 times as many points as originally defined. _false_ by default. + * @property {number} startingPoint Index position for what point to start from on the path. _0_ by default. + * @property {() => void} onFinishCallback Function to call when the NPC finished walking over all the points on the path. This is only called when `loop` is _false_. + * @property {() => void} onReachedPointCallback Function to call once every time the NPC reaches a point in the path. + * + */ +export type FollowPathData = { + startingPoint?: number + loop?: boolean + curve?: boolean + totalDuration: number + speed?: number + path?: Vector3[] + pathType?: NPCPathType + onFinishCallback?: () => void + onReachedPointCallback?: () => void +} + +/** + * Cut out a section of an image file + * + * @typedef {Object} ImageSection - Object with data to only display a section of an image + * @property {number} sourceWidth Width in pixels to select from image, starting from the sourceLeft, going right + * @property {number} sourceHeight Height in pixels to select from image, starting from the sourceTop, going down + * @property {number} sourceLeft Leftmost pixel to select from image + * @property {number} sourceTop Topmost pixel to select from image + * + */ +export type ImageSection = { + sourceWidth: number + sourceHeight: number + sourceLeft?: number + sourceTop?: number +} + +/** + * + * + * @typedef {Object} ImageData - Object with data for displaying an image + * @property {string} path Path to the image file. + * @property {number} offsetX Offset on X, relative to the normal position of the image. + * @property {number} offsetY Offset on Y, relative to the normal position of the image. + * @property {number} height The height to show the image onscreen. + * @property {number} width The width to show the image onscreen. + * @property {ImageSection} section Use only a section of the image file, useful when arranging multiple icons into an image atlas. This field takes an `ImageSection` object, specifying `sourceWidth` and `sourceHeight`, and optionally also `sourceLeft` and `sourceTop`. + * + */ +export type ImageData = { + path: string + offsetX?: number + offsetY?: number + height?: number + width?: number + section?: ImageSection +} + +export enum NPCState { + STANDING = 'standing', + TALKING = 'talking', + FOLLOWPATH = 'followPath' + //FOLLOWPLAYER = 'followPlayer' +} + +export enum NPCBodyType { + MALE = 'BaseMale', + FEMALE = 'BaseFemale' +} + +export enum NPCType { + BLANK = 'blank', + CUSTOM = 'custom', + AVATAR = 'avatar' +} + +export enum NPCPathType { + SMOOTH_PATH = 'smooth', //will follow the path but can cut sharp corners + RIGID_PATH = 'rigid' //will ensure each corner is hit +} From 8bebdba49988a4904c5bcc31887c4613994f1141 Mon Sep 17 00:00:00 2001 From: AnisLahouar <38791856+AnisLahouar@users.noreply.github.com> Date: Wed, 21 Jun 2023 21:18:47 +0100 Subject: [PATCH 3/5] chore: remove use of deprecated method --- src/npc.ts | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/npc.ts b/src/npc.ts index 85a85e3..a84017f 100644 --- a/src/npc.ts +++ b/src/npc.ts @@ -144,13 +144,13 @@ function addNPCBones(npc: Entity, data: NPCData) { const modelAvatarData: PBAvatarShape | undefined = modelIsString ? undefined : data.model && (data.model as any).bodyShape - ? (data.model as PBAvatarShape) - : undefined + ? (data.model as PBAvatarShape) + : undefined const modelGLTFData: PBGltfContainer | undefined = modelIsString ? undefined : data.model && (data.model as any).src - ? (data.model as PBGltfContainer) - : undefined + ? (data.model as PBGltfContainer) + : undefined switch (data.type) { case NPCType.AVATAR: @@ -158,19 +158,19 @@ function addNPCBones(npc: Entity, data: NPCData) { npc, !data || !data.model || !modelAvatarData ? { - id: 'npc', - name: 'NPC', - bodyShape: 'urn:decentraland:off-chain:base-avatars:BaseMale', - emotes: [], - wearables: [ - 'urn:decentraland:off-chain:base-avatars:f_eyes_00', - 'urn:decentraland:off-chain:base-avatars:f_eyebrows_00', - 'urn:decentraland:off-chain:base-avatars:f_mouth_00', - 'urn:decentraland:off-chain:base-avatars:comfy_sport_sandals', - 'urn:decentraland:off-chain:base-avatars:soccer_pants', - 'urn:decentraland:off-chain:base-avatars:elegant_sweater' - ] - } + id: 'npc', + name: 'NPC', + bodyShape: 'urn:decentraland:off-chain:base-avatars:BaseMale', + emotes: [], + wearables: [ + 'urn:decentraland:off-chain:base-avatars:f_eyes_00', + 'urn:decentraland:off-chain:base-avatars:f_eyebrows_00', + 'urn:decentraland:off-chain:base-avatars:f_mouth_00', + 'urn:decentraland:off-chain:base-avatars:comfy_sport_sandals', + 'urn:decentraland:off-chain:base-avatars:soccer_pants', + 'urn:decentraland:off-chain:base-avatars:elegant_sweater' + ] + } : modelAvatarData ) break @@ -215,16 +215,17 @@ function addClickReactions(npc: Entity, data: NPCData) { let activateButton = data && data.onlyClickTrigger ? InputAction.IA_POINTER : InputAction.IA_PRIMARY pointerEventsSystem.onPointerDown( - npc, - function () { + { + entity: npc, opts: { + button: activateButton, + hoverText: data && data.hoverText ? data.hoverText : 'Talk', + showFeedback: data && data.onlyExternalTrigger ? false : true + } + }, + () => { if (isCooldown.has(npc) || npcDialogComponent.get(npc).visible) return activate(npc, engine.PlayerEntity) }, - { - button: activateButton, - hoverText: data && data.hoverText ? data.hoverText : 'Talk', - showFeedback: data && data.onlyExternalTrigger ? false : true - } ) if (data && data.onlyExternalTrigger) { From 396e2e5e3cb8253399bb4e70fa6c575b35d485c6 Mon Sep 17 00:00:00 2001 From: AnisLahouar <38791856+AnisLahouar@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:24:35 +0100 Subject: [PATCH 4/5] fix: compatibility issue --- src/npc.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/npc.ts b/src/npc.ts index a84017f..2e50dd1 100644 --- a/src/npc.ts +++ b/src/npc.ts @@ -214,9 +214,11 @@ function addNPCBones(npc: Entity, data: NPCData) { function addClickReactions(npc: Entity, data: NPCData) { let activateButton = data && data.onlyClickTrigger ? InputAction.IA_POINTER : InputAction.IA_PRIMARY + /* pointerEventsSystem.onPointerDown( { - entity: npc, opts: { + entity: npc, + opts: { button: activateButton, hoverText: data && data.hoverText ? data.hoverText : 'Talk', showFeedback: data && data.onlyExternalTrigger ? false : true @@ -227,6 +229,20 @@ function addClickReactions(npc: Entity, data: NPCData) { activate(npc, engine.PlayerEntity) }, ) + */ + + pointerEventsSystem.onPointerDown( + npc, + function () { + if (isCooldown.has(npc) || npcDialogComponent.get(npc).visible) return + activate(npc, engine.PlayerEntity) + }, + { + button: activateButton, + hoverText: data && data.hoverText ? data.hoverText : 'Talk', + showFeedback: data && data.onlyExternalTrigger ? false : true + } + ) if (data && data.onlyExternalTrigger) { pointerEventsSystem.removeOnPointerDown(npc) From 8763e92f58102cf762e422bf114b3a1361374393 Mon Sep 17 00:00:00 2001 From: William Caine Date: Thu, 22 Jun 2023 09:49:58 -0400 Subject: [PATCH 5/5] using trigger data layer flags --- src/npc.ts | 4 ++-- src/types.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/npc.ts b/src/npc.ts index 2e50dd1..4c01df3 100644 --- a/src/npc.ts +++ b/src/npc.ts @@ -298,8 +298,8 @@ function addTriggerArea(npc: Entity, data: NPCData) { if (triggerData.onCameraEnter || triggerData.onCameraExit) { utils.triggers.addTrigger( npc, - utils.NO_LAYERS, - utils.LAYER_1, + triggerData.layer != undefined ? triggerData.layer : utils.NO_LAYERS, + triggerData.triggeredByLayer != undefined ? triggerData.triggeredByLayer : utils.LAYER_1, [{ type: 'sphere', position: Vector3.Zero(), radius: data.reactDistance != undefined ? data.reactDistance : 6 }], (other) => { if (triggerData.onCameraEnter) triggerData.onCameraEnter(other) diff --git a/src/types.ts b/src/types.ts index 4ec8fba..2910f8e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,8 +43,8 @@ export type Dialog = { /** * * @typedef {Object} TriggerData - Object with data for a NPCTriggerComponent - * @property {number} layer layer of the Trigger, useful to discriminate between trigger events. You can set multiple layers by using a | symbol. - * @property {number} triggeredByLayer against which layers to check collisions + * @property {number} layer layer of the Trigger, useful to discriminate between trigger events. You can set multiple layers by using a | symbol. defaults to NO_LAYERS + * @property {number} triggeredByLayer against which layers to check collisions. defaults to LAYER_1 * @property {(entity: Entity) => void } onTriggerEnter callback when an entity of a valid layer enters the trigger area * @property {(entity: Entity) => void} onTriggerExit callback when an entity of a valid layer leaves the trigger area * @property {() => void} onCameraEnter callback when the player enters the trigger area