Quests SDK Library
Use the Quests Client for SDK 7 library in your scenes (or portable experiences) to connect to the Quests Service to track player’s progress, send events and receive updates of player’s progress from your scenes.
What the library provides #
- An interface with the Quests Client API, to send your Custom events and receive updates of the player’s progress.
- SDK System helper to track Location, Emote, and Jump actions. Passing a callback function to the helper, you can send the action to the Quests Service when relevant.
- A fully-customizable Quest HUD UI to display the player’s progress on your Quest. You can use this UI as is, or as an example to build your own. You can find the code here .
Usage #
Installation #
Via the Editor #
Follow the steps in Install a dependency :
-
Open the Decentraland Editor tab on Visual Studio. Note that the bottom section lists all of your project’s currently installed dependencies.
-
Click the
+
icon on the header of the Dependencies view. -
Visual Studio opens an input box at the top of the screen. Write
@dcl/quests-client
.
Via the CLI #
Run this command in your scene’s directory:
$ npm install @dcl/quests-client
Quests Client overview #
The types below are defined in Quests Client for SDK 7 .
import { QuestInstance as QuestInstanceProtocol } from './protocol/decentraland/quests/definitions.gen'
type QuestsClient = {
startQuest: () => Promise<boolean>
abortQuest: () => Promise<boolean>
sendEvent: (event: { action: Action }) => Promise<EventResponse | undefined>
onStarted: (callback: OnStartedCallback) => void
onUpdate: (callback: OnUpdateCallback) => void
isQuestStarted: () => boolean
getQuestInstance: () => QuestInstance | null
getInstances: () => QuestInstance[]
}
type Required<T> = T & {
[P in keyof T]: NonNullable<T[P]>
}
type QuestInstance = Required<QuestInstanceProtocol>
type OnStartedCallback = (instance: QuestInstance) => void
type OnUpdateCallback = (instance: QuestInstance) => void
The Quest Instance #
The QuestInstance
type is used in several methods. The QuestInstance
type has the same fields as QuestInstanceProtocol
but with no undefined
fields (the undefined
are caused by proto
, and it’s impossible that these fields are undefined
). A QuestInstance
(or QuestInstanceProtocol
) is an instance of a specific Quest and it contains the state (or progress) of the player in that specific Quest. The QuestInstance
type has the following fields:
id
: The id of the Quest Instance. It’s a unique identifier of the player’s Quest Instance.quest
: The Quest object defined in the Quest’s protocol file here .state
: The progress of the player along the Quest defined in the Quest’s protocol file here .
Quest client methods #
The following methods are available in the Quests Client:
-
startQuest
: Use this function to make the player start your Quest. In the background, the function calls the Quest RPC Service. The function returns aboolean
. If the Quest was started successfully, it returnstrue
. If there was an error, it returnsfalse
. -
abortQuest
: Use this function to abort the player’s instance of the Quest. In the background, the function will call the Quest RPC Service. If the Quest was aborted successfully, it returnstrue
. If there was an error, it returnsfalse
.
📔 Note: Quests that are fully completed cannot be aborted, only partially completed quests. A player can’t do a same quest more than once.
-
sendEvent
: Use this function send a custom event to the Quest RPC Service. The function receives anAction
(action item with its type and parameters), representing an action that the player has already completed in the scene. The function returns anEventResponse
object that contains the result of the request, both in case of an error or success. BothAction
andEventResponse
are defined in the Quest’s protocol file here . -
onStarted
: Use this function to register one or multiple callbacks that are called when the player starts your Quest. Callbacks will only be called when the user starts the Quest that matches the Quest ID passed when creating the client instance. Use these callbacks for the scene to react to the start of your Quest. The callback receives anQuestInstance
object that contains the information of the Quest that the player has started. -
onUpdate
: Use this function to register one or multiple callbacks that are called whenever the player makes progress on a Quest. Callbacks will only be called when the user makes progress on the Quest that matches the Quest ID passed when you created the client instance. Use these callbacks to apply changes on your scene that correlate to this progress. The callback receives aQuestInstance
object that contains the information of the Quest that the player has made progress on. -
isQuestStarted
: Use this function to check if the player has started your Quest. The function returns aboolean
. If the player has started the Quest, it returnstrue
. If the player hasn’t started the Quest, it returnsfalse
. -
getQuestInstance
: This function allows you to get the Instance of the Quest that matches the Quest ID passed when you created the Client. The function returns aQuestInstance
object, including data about the player’s progress in that quest. If the user hasn’t started the Quest, it returnsnull
. -
getInstances
: This function allows you to get all the Quest Instances of the player. The function returns an array ofQuestInstance
objects.
Setting up the client #
To initialize the Quests Client in your scene, start by importing createQuestsClient
.
// index.ts
//...
import { createQuestsClient } from '@dcl/quests-client'
Then run the function createQuestsClient
to initialize the Quests Client. This function returns a promise that resolves when the Quests Client is ready to be used. Run this function inside an async function, for this you can use the executeTask
function, see
asynchronous functions
.
The function createQuestsClient()
takes a parameter with the URL to connect to the
Quests API
. The following options are available:
- Development:
wss://quests-rpc.decentraland.zone
- Production:
wss://quests-rpc.decentraland.org
- Custom: Set up your own Quests Service locally for development or testing. You can find more information on how to do so on Quests Service’s repository
// index.ts
//...
import { executeTask } from '@dcl/sdk/ecs'
import { createQuestsClient } from '@dcl/quests-client'
const MY_QUEST_ID = 'quest-id-1234-5678-9012'
executeTask(async () => {
const serviceUrl = 'wss://quests-rpc.decentraland.zone'
try {
const questsClient = await createQuestsClient(serviceUrl, MY_QUEST_ID)
console.log('Quests Client is ready to use!')
} catch (e) {
console.error('Error on connecting to Quests Service')
}
})
Using observables #
Since the instancing of the questClient
is scoped within executeAsync
function, interacting with this object from other parts of your scene’s code can be a challenge. You’ll likely end up having lots of parts in your scene’s code needing to either make updates to the player’s progress or know the player’s current progress along the quest, and all of these will need to make use of the questClient
object. The recommended approach is to use observables (also known as event emitters) to send and to listen to events anywhere.
To use observables, we recommend you use the mitt
library. Find more information about this library
here
.
To install this library, follow the steps in Install a dependency :
-
Open the Decentraland Editor tab on Visual Studio. Note that the bottom section lists all of your project’s currently installed dependencies.
-
Click the
+
icon on the header of the Dependencies view. -
Visual Studio opens an input box at the top of the screen. Write
mitt
.
To otherwise install the library via the command line, run:
$ npm install mitt
In its simplest form, you can use mitt like this to send and listen to events:
const event = mitt()
event.emit('start')
event.on('start', () => {
console.log('started')
})
The code example below works across three files in the scene:
- In
events.ts
you simply define an event emitter. This can then be imported to other files - In
cube.ts
you send this “start” event when the player performs an action. In this example, thestart
event is sent when the player clicks on a Cube mesh. - In
index.ts
you listen for this “stat” after you have initialized thequestClient
. Then, when this event is listened to, thestartQuest
function is called with the Quest ID.
// events.ts
import mitt from 'mitt'
export const startEvent = mitt()
// index.ts
//...
import { executeTask } from '@dcl/sdk/ecs'
import { createQuestsClient, QuestInstance } from '@dcl/quests-client'
import { startEvent } from './events'
const MY_QUEST_ID = 'quest-id-1234-5678-9012'
executeTask(async () => {
const serviceUrl = 'wss://quests-rpc.decentraland.zone'
try {
const questsClient = await createQuestsClient(serviceUrl, MY_QUEST_ID)
console.log('Quests Client is ready to use!')
questsClient.onUpdate((quest: QuestInstance) => {
// apply some changes on your scene to reflect the player's progress
})
questsClient.onStarted((quest: QuestInstance) => {
// react to the start of your Quest by applying some change on your scene
})
startEvent.on('start', async () => {
await questsClient.startQuest()
})
} catch (e) {
console.error('Error on connecting to Quests Service')
}
})
// cube.ts
//...
import { pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
//...
// create a clickable cubeMeshEntity
pointerEventsSystem.onPointerDown(
cubeMeshEntity,
(cmd) => {
startEvent.emit('start')
},
{ button: InputAction.IA_PRIMARY, hoverText: 'E to Start Quest' }
)
//...
Trigger the Start of your Quest #
To trigger the start of a Quest, call the startQuest
function. This function receives the id of the quest you want to start.
const MY_QUEST_ID = 'quest-id-1234-5678-9012'
executeTask(async () => {
const serviceUrl = 'wss://quests-rpc.decentraland.zone'
try {
const questsClient = await createQuestsClient(serviceUrl, MY_QUEST_ID)
const result = await questsClient.startQuest()
if (result) {
console.log('Quest started successfully!')
} else {
console.error("Quest couldn't be started")
}
} catch (e) {
console.error('Error on connecting to Quests Service')
}
})
The example above initializes the quest as soon as the scene loads. You may prefer to instead start the quest as response to a player’s interaction. For example when the player enters a specific area of your scene or when they interact with an NPC. For an example of how to do this, see the Using observables section above.
React to the start of your Quest #
When the player starts your Quest, you may want the scene to react to this event. You can register a callback using onStarted
function, provided by the client:
// index.ts
//...
import { executeTask } from '@dcl/sdk/ecs'
import { createQuestsClient, QuestInstance } from '@dcl/quests-client'
import { startEvent } from './events'
const MY_QUEST_ID = 'quest-id-1234-5678-9012'
executeTask(async () => {
const serviceUrl = 'wss://quests-rpc.decentraland.zone'
try {
const questsClient = await createQuestsClient(serviceUrl, MY_QUEST_ID)
console.log('Quests Client is ready to use!')
client.onStarted((quest: QuestInstance) => {
// react to the start of your Quest
})
} catch (e) {
console.error('Error on connecting to Quests Service')
}
})
Send Custom Actions to the Quests Service #
When an action is of type CUSTOM
, you must use the sendEvent
function to update the quest service.
Like with other functions from the quest client, you can use an event emitter to route events from anywhere in your scene’s code to the context where the quest client object is initialized. A good practice is to define an “action” event with mitt that includes all of the data about the action. That way you only need to create a single listener, that handles all the actions from your scene.
Come back to the events.ts
file iny your scene, to define a type of event that sends an Action
object. For this, you need to import the Action
type provided by the Quests Client library and defined in the Quest’s Protocol file
here
.
// events.ts
import mitt from 'mitt'
import { Action } from '@dcl/quests-client'
export const startEvent = mitt()
export const actionEvents = mitt<{ action: Action }>()
The above code initializes an event emitter used to send events to the Quests Service. This event emitter emits an event called “action”, and includes an Action
object. Import this emitter on the file where you have the questClient
initialized and listen to the action
event.
// index.ts
//...
import { executeTask } from '@dcl/sdk/ecs'
import { createQuestsClient, QuestInstance } from '@dcl/quests-client'
import { startEvent, actionEvents } from './events'
const MY_QUEST_ID = 'quest-id-1234-5678-9012'
executeTask(async () => {
const serviceUrl = 'wss://quests-rpc.decentraland.zone'
try {
const questsClient = await createQuestsClient(serviceUrl, MY_QUEST_ID)
console.log('Quests Client is ready to use!')
client.onUpdate((quest: QuestInstance) => {
// apply some changes on your scene to reflect the player's progress
})
startEvent.on('start', async (value: boolean) => {
await questsClient.startQuest()
})
actionEvents.on('action', async (action: Action) => {
await questsClient.sendEvent({ action })
})
} catch (e) {
console.error('Error on connecting to Quests Service')
}
})
The code above includes the actionEvents
emitter listening to the action
event. When this event is listened to, the quest client’s sendEvent
function is called, using the data from the Action
object that was passed with this event.
With this set up, you can emit events with the actionEvents
emitter from any part of your code, and they will be forwarded to the Quest Service. Below is some pseudocode example:
// another-file.ts
//...
import { actionEvents } from './events'
//...
actionEvents.emit('action', {
type: 'CUSTOM',
parameters: { id: 'my-custom-action-id' },
})
Sending other types of actions #
For actions of type LOCATION
, JUMP
, and EMOTE
, you can make use of the initActionsTracker
function. This function registers a set of systems to track these types of actions. You only need to call this function once, with the action types you want to track.
For example: to track the LOCATION
type of action.
- Import the
initActionsTracker
function from@dcl/quests-client/dist/systemHelpers
and call it, passing the following parameters: a. The SDKengine
constant. b. A callback function. You may use it to send the action using theQuestClient
or using a event emitter, as covered in other examples. c. The type of action you want to track.
import { initActionsTracker } from '@dcl/quests-client/dist/systemHelpers'
initActionsTracker(
engine,
(action) => {
// You can add a check here to only send the action if the player has started the quest
// Also, you can add a check here to only send the action if the action itself meets some criteria or condition.
// Or it's indeed a valid action for the quest so that you don't send invalid actions to the Quests Service.
client.sendEvent(action)
},
'location'
)
To track more than one action, list them as additional parameters. For example:
import { initActionsTracker } from '@dcl/quests-client/dist/systemHelpers'
initActionsTracker(
engine,
(action) => {
// You can add a check here to only send the action if the player has started the quest
// Also, you can add a check here to only send the action if the action itself meets some criteria or condition.
// Or it's indeed a valid action for the quest so that you don't send invalid actions to the Quests Service.
client.sendEvent(action)
},
'location',
'jump'
)
Or all three action types:
import { initActionsTracker } from '@dcl/quests-client/dist/systemHelpers'
initActionsTracker(
engine,
(action) => {
// You can add a check here to only send the action if the player has started the quest
// Also, you can add a check here to only send the action if the action itself meets some criteria or condition.
// Or it's indeed a valid action for the quest so that you don't send invalid actions to the Quests Service.
client.sendEvent(action)
},
'location',
'jump',
'emote'
)
The order of the action type parameters is not relevant here. Each action type that you pass as parameter will be tracked by the system.
React to changes on the player’s progress #
You may want to react and apply different changes to your scene when the player makes progress on a Quest. To react to these changes, register a callback with onUpdate
:
// index.ts
//...
import { executeTask } from '@dcl/sdk/ecs'
import { createQuestsClient, QuestInstance } from '@dcl/quests-client'
const MY_QUEST_ID = 'quest-id-1234-5678-9012'
executeTask(async () => {
const serviceUrl = 'wss://quests-rpc.decentraland.zone'
try {
const questsClient = await createQuestsClient(serviceUrl, MY_QUEST_ID)
console.log('Quests Client is ready to use!')
client.onUpdate((quest: QuestInstance) => {
// update your state here to react to the quest updates
})
} catch (e) {
console.error('Error on connecting to Quests Service')
}
})
The onUpdate
function receives a callback function that is called every time the player makes progress on a Quest. The callback receives a QuestInstance
object that contains information about the Quest that the player has made progress on.
The QuestInstance
object has the following fields:
type QuestInstance = {
id: string
quest: Quest
state: QuestState
}
id
: The id of the Quest Instance. It’s a unique identifier of the player’s Quest Instance.quest
: The Quest object. Find more information about the Quest object here .state
: The progress of the player on the Quest.
Find details about the Quest
and QuestState
types in the Quest’s protocol file
here
.
💡 Tip: When there are changes to the scene’s state that reflect the player’s progress throughout the quest, always make the changes via this function, rather than as a direct consequence of the player’s action. That way, if for whatever reason the quest service is not successful, the scene will remain in a consistent state with the player’s progress.
Once again, it’s recommended to use event emitters so that the effects of a change in the quest progress can make changes anywhere in your code.
// events.ts
import mitt from 'mitt'
import { Action } from '@dcl/quests-client'
export const startEvent = mitt()
export const actionEvents = mitt<{ action: Action }>()
export const questProgress = mitt<{ step: number }>()
// index.ts
//...
import { executeTask } from '@dcl/sdk/ecs'
import {
createQuestsClient,
QuestInstance,
questProgress,
} from '@dcl/quests-client'
const MY_QUEST_ID = 'quest-id-1234-5678-9012'
executeTask(async () => {
const serviceUrl = 'wss://quests-rpc.decentraland.zone'
try {
const questsClient = await createQuestsClient(serviceUrl, MY_QUEST_ID)
console.log('Quests Client is ready to use!')
questsClient.onUpdate((quest: QuestInstance) => {
for (let step of quest.state.stepsCompleted) {
switch (step) {
case 'my_step_1':
questProgress.emit('step', 1)
case 'my_step_2':
questProgress.emit('step', 2)
case 'my_step_3':
questProgress.emit('step', 3)
}
}
})
} catch (e) {
console.error('Error on connecting to Quests Service')
}
})
// cube.ts
import { questProgress } from '@dcl/quests-client'
//...
questProgress.on('step', (stepNumber: number) => {
if (stepNumber >= 2) {
console.log("we're ready for step 3!")
}
})
Check for progress in other sessions #
The player might be coming back to your scene after having started the quest before. The quest server keeps track of how far they got, but your scene might have to load changes that reflect the past actions of the player. For example, you might have to hide items that were already collected, unlock doors that were already opened, etc. You should also update the quest UI to reflect their initial progress.
Use the getQuestInstance()
function to fetch a QuestInstance
object, which contains a stepsCompleted
array.
// index.ts
//...
import { executeTask } from '@dcl/sdk/ecs'
import { createQuestsClient, QuestInstance } from '@dcl/quests-client'
const MY_QUEST_ID = 'quest-id-1234-5678-9012'
executeTask(async () => {
const serviceUrl = 'wss://quests-rpc.decentraland.zone'
try {
const questsClient = await createQuestsClient(serviceUrl, MY_QUEST_ID)
console.log('Quests Client is ready to use!')
const currentProgress = questsClient.getQuestInstance()
if (currentProgress) {
sendQuestUpdates(currentProgress)
}
questsClient.onUpdate((quest: QuestInstance) => {
sendQuestUpdates(quest)
})
} catch (e) {
console.error('Error on connecting to Quests Service')
}
})
function sendQuestUpdates(questInstance: QuestInstance) {
for (let step of questInstance.state.stepsCompleted) {
switch (step) {
case 'my_step_1':
questProgress.emit('step', 1)
case 'my_step_2':
questProgress.emit('step', 2)
case 'my_step_3':
questProgress.emit('step', 3)
}
}
}
The example above defines a function sendQuestUpdates
to handle both the initial state of the quest when the scene loads, obtained from getQuestInstance()
, and any updates done later, obtained from onUpdate()
. The scene can then react to these messages that are emitted and change the states of relevant things.