0ad/binaries/data/mods/public/maps/scripts/Tutorial.js
Vantha 27fbf02ff4 Improve the ending of tutorials
This patch introduces two functions `CompleteTutorial` and
`FailTutorial` for the tutorial steps to call. They can pass a
message to it to be shown on the victory/defeat screen.
`CompleteTutorial` is also called automatically when all steps are
finished, which resolves #8583 and prevents it from happening in the
future.
`FailTutorial` isn't used anywhere at the moment, but it'll be useful
for the future.

Fixes #8583
2026-07-22 23:51:49 +02:00

147 lines
4.2 KiB
JavaScript

// Needs to be kept in sync with the one in gui/session/tutorial/Tutorial.js
var TUTORIAL_STEP_TYPE = deepfreeze({
"INSTRUCTION": 1,
"INFO": 2
});
Engine.RegisterGlobal("TUTORIAL_STEP_TYPE", TUTORIAL_STEP_TYPE);
Trigger.prototype.InitTutorial = function(data)
{
this.stepIndex = -1;
this.tutorialEvents = [];
// Register needed triggers
this.RegisterTrigger("OnDeserialized", "DeserializedAction", { "enabled": true });
this.RegisterTrigger("OnPlayerCommand", "BasePlayerCommandAction", { "enabled": true });
for (const step of this.tutorialSteps)
{
for (const key in step)
{
if (typeof step[key] !== "function" || this.tutorialEvents.indexOf(key) != -1)
continue;
if (key == "Init")
continue;
if (key == "IsDone")
continue;
const action = key.substring(2) + "Action";
this.RegisterTrigger(key, action, { "enabled": false });
this.tutorialEvents.push(key);
}
}
this.NextStep();
};
Trigger.prototype.NextStep = function(deserializing = false)
{
if (++this.stepIndex >= this.tutorialSteps.length)
{
this.CompleteTutorial();
return;
}
const step = this.tutorialSteps[this.stepIndex];
Trigger.prototype.Init = step.Init || null;
if (!deserializing && this.Init)
this.Init();
for (const event of this.tutorialEvents)
{
const action = event.substring(2) + "Action";
if (step[event])
{
Trigger.prototype[action] =
event == "OnPlayerCommand" ?
(msg) =>
{
// Don't forward tutorial continue commands, since the step trigger actions aren't supposed to handle them,
// plus we might have already loaded in the next step.
if (msg.cmd.type != "dialog-answer" || msg.cmd.tutorial != "continue")
step.OnPlayerCommand.call(this, msg);
} :
step[event];
this.EnableTrigger(event, action);
}
else
this.DisableTrigger(event, action);
}
Trigger.prototype.IsDone = step.IsDone || (() => false);
const isDone = this.IsDone();
const showContinueButton = isDone || (step.panelData.showContinueButton === undefined ?
this.tutorialEvents.every(event => !step[event]) :
step.panelData.showContinueButton
);
this.DisplayStep(step, showContinueButton, isDone);
};
Trigger.prototype.DisplayStep = function(step, showContinueButton = false, isDone = false)
{
const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "tutorial",
"players": [1],
"step": {
"type": step.type,
"panelData": {
...step.panelData, showContinueButton, isDone
}
}
});
};
Trigger.prototype.DisplayWarning = function(warning)
{
const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "tutorial",
"players": [1],
"warning": warning
});
};
Trigger.prototype.CompleteTutorial = function(message = markForTranslation("You have successfully completed the tutorial."))
{
// End the tutorial first, before marking the player won, since otherwise the triggers would be invoked
// once more when doing so.
this.EndTutorial(message);
TriggerHelper.SetPlayerWon(this.playerID, () => markForTranslation("%(player)s has completed the tutorial."), () => "", message);
};
Trigger.prototype.FailTutorial = function(message = markForTranslation("You have failed to complete the tutorial."))
{
// End the tutorial first, before marking the player defeated, since otherwise the triggers would be invoked
// once more when doing so.
this.EndTutorial(message);
TriggerHelper.DefeatPlayer(this.playerID, markForTranslation("%(player)s has failed the tutorial."), message);
};
Trigger.prototype.EndTutorial = function(message)
{
this.DisableAllTriggers();
this.DisplayStep({
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
"panelData": { "isLast": true, "text": message }
}, true, false);
};
Trigger.prototype.BasePlayerCommandAction = function(msg)
{
if (msg.cmd.type == "dialog-answer" && msg.cmd.tutorial == "continue")
this.NextStep();
};
Trigger.prototype.DeserializedAction = function()
{
this.stepIndex = Math.max(0, this.stepIndex - 1);
// Display messages from already processed steps
for (let i = 0; i <= this.stepIndex; ++i)
this.DisplayStep(this.tutorialSteps[i], false, false);
this.NextStep(true);
};