As you like to show the mechanics of your module or system to your new users, you may do it by tours.
In this example, you can see how to
Hooks.once("setup", async function () {
registerMyTours();
}
// necessary to toggle tab from settings to chat before you are able to target #chat
Hooks.on("closeToursManagement", function (event) {
ui.sidebar.activateTab("chat");
});
class MyTour extends Tour {
async _preStep() {
await super._preStep();
const currentStep = this.currentStep;
if(currentStep.id == "chat") {
ChatMessage.create({
content: "<h2>Demo MyTour</h2>",
speaker: ChatMessage.getSpeaker({alias: "MyTour", color: "#ff0000"})
});
} else {
console.log("MyTours | Tours _preStep: ",currentStep.id);
}
}
}
async function registerMyTours() {
try {
game.tours.register(moduleName, 'format', await MyTour.fromJSON('/modules/'+moduleName+'/tours/chat.json'));
if(game.user.isGM) {
game.tours.register(moduleName, 'settings', await MyTour.fromJSON('/modules/'+moduleName+'/tours/settings.json'));
}
} catch (error) {
console.error("MyTour | Error registering tours: ",error);
}
}
{
"title": "Chat Guide",
"description": "Howto use the chat",
"canBeResumed": false,
"display": true,
"steps": [
{
"id": "chat",
"selector": "#chat-form",
"title": "Chat",
"content": "Here one can write messages to the other players.",
"tab": {
"parent": "sidebar",
"id": "chat"
}
},
{
"id": "stepTwo",
// ...
}
]
}
There are common tours one can start manually but also special tours types inherit from Tours Class as:
Each type of tour has specific methods to interact with their domain.
(Todo: explain by example)
The JSON for a tour may contain several fields one can use for
{
"title": "MYMODULE.title", // see below at "localization"
"description": "...",
"canBeResumed": false, // once started resume at last step
"display": true // visible in tour list
"closeWindows": false, // default closes all on tour.start()
"steps": [ // array of objects each with possible fields
{
"id" "nameOfStep",
"selector": "#htmlTagID", // htmlElementTyp[parameter-name=\"name\"]
"title": "...",
"content": "...",
"sidebarTab": "chat", // {"chat", "combat", "scenes", "actors", "items", "journal", "tables", "cards", "playlists", "compedium", "settings"}
"tooltipDirection": "RIGHT" // {"UP", "CENTER", "RIGHT", "LEFT", ...}
}
],
"suggestedNextTours": [
"myModule.tourName" // tourName.json / find names by typing 'game.tours' in browser-console
],
"localization": { // this should be done in language/en.json
"MYMODULE.title": "bla bla",
"MYMODULE.description": "foobar"
}
}
The field suggestedNextTours is not able to take more then one tour (by Tours Class ). At this point also the following workaround for conditional suggestedNextTour could help out:
class MyTour extends Tour {
async _preStep() {
await super._preStep();
const currentStep = this.currentStep;
if(currentStep.id == "chat") {
switch(game.settings.get(moduleName, "chatMode")) {
case "shutUpAndListenTheGM":
this.config.suggestedNextTours = [moduleName+".strongGMwordsMSG"];
break;
case "haveFun":
this.config.suggestedNextTours = [moduleName+".funGMgreatings"];
break;
case "headCinemaTeamwork":
this.config.suggestedNextTours = [moduleName+".cinemaWishings"];
break;
default:
break
}
}
}
}
To get faster finding the right selector here you can find some examples.
{
"title": "Configuration Settings",
"description": "Howto mark the Button Configuration Settings for Tours",
"canBeResumed": false,
"display": true,
"steps": [
{
"id": "configSettings",
"selector": "button[data-action=\"configure\"]", // html element "button" with [parameter] data-action="configure"
"title": "Config Tour",
"content": "MYTRANSLATION.tours.configSettings",
"sidebarTab" "settings" // {"chat", "combat", "scenes", "actors", "items", "journal", "tables", "cards", "playlists", "compedium", "settings"}
},
{
"id": "stepTwo",
// ...
}
]
}
{
"title": "..",
"description": "...",
"display": true,
"steps": [
{
"id": "configSettings",
"selector": ".tabs>a[data-tab='actors']", // .class "tabs" with chield > element "a" and [parameter] "data-tab"
//...
}
]
}
To be able guiding your users through the settings one have to know how to trigger this:
Hooks.on("renderFormApplication", (app, html, data) => {
if(html[0].id == "client-settings") { // else Tour-Management Window also will get the listener
html.find('#client-settings').prevObject[0].querySelectorAll("[data-tab='MyModuleSettingName']").forEach(element => {
element.addEventListener('click', (event) => {
const tour = game.tours.get("myModuleName.tourName"); // find by console.log(game.tours)
tour.start();
});
});
}
});
If you like to start the tour once module is activated and players log in, you can do this by checking the status of the tour. Have in mind you also can check if the user isGM if it is a settings tour.
Hooks.once("ready", async function () {
if(game.tours.get("myModuleName.tourName").status == "unstarted") // {"unstarted", "in-progress", "completed"}
{
game.tours.get("myModuleName.tourName").start();
}
});
Tipp: If you open a window like client-settings you should call tour.start() within a timeout function
setTimeout(function(){
game.tours.get("myModuleName.tourName").start();
}, 1000);
If you like to create more complex tours using interaction and training for your users, you want to take a look at the API-Documentation of the Tours Class.