0ad/binaries/data/mods/public/maps/scripts/Tutorial.js
Vantha 0cc025f675 Indicate if a tutorial step is already done
If a new tutorial step has already been completed by the player it isn't
skipped, but instead shown with the continue button, so that the player
can manually switch to the next one. However, it wasn't well
communicated to the player that this was the case and why the continue
button was shown at the same time as the instruction. This patch adds a
hint "You have already done this." to explain the this to the player.
2026-07-22 23:51:49 +02:00

120 lines
3.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)
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 isLast = this.stepIndex === this.tutorialSteps.length - 1;
const showContinueButton = isDone || isLast || (step.panelData.showContinueButton === undefined ?
this.tutorialEvents.every(event => !step[event]) :
step.panelData.showContinueButton
);
this.DisplayStep(step, showContinueButton, isDone, isLast);
};
Trigger.prototype.DisplayStep = function(step, showContinueButton = false, isDone = false, isLast = false)
{
const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "tutorial",
"players": [1],
"step": {
"type": step.type,
"panelData": {
...step.panelData, showContinueButton, isDone, isLast
}
}
});
};
Trigger.prototype.DisplayWarning = function(warning)
{
const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "tutorial",
"players": [1],
"warning": warning
});
};
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);
};