/*
Cinematic Tutorial System by Eikonium.
Requires LinkedList by AGD: https://www.hiveworkshop.com/threads/linkedlist-modules.325635/
*/
/***************************************************
*************API Tutorial System******************
***************************************************/
/**********************************************
* This system simplifies making more or less cinematic minigame tutorials for players to watch before they start playing.
* It does all the cinematic interface fading and transmission stuff for you.
* You basically only have to deal with the questions: what explanatory text do I want to display to the players and what shall happen in the tutorial cinematic at which point in time.
* The system can play different tutorial cinematics for different players at the same time.
***********************
* Basic terms:
* The term "Tutorial Sequence" describes a cinematic scene, consisting of one text transmission that is displayed for a duration and all visual stuff that you want to happen during that duration.
* Each Tutorial Sequence can take any number of functions to be executed at specified points during the sequence runtime, so called Tutorial Actions.
* All Tutorial Sequences together form the Tutorial.
* That means you build a Tutorial consisting of Tutorial Sequences, each including any number of Tutorial Actions.
*************************************************/
//! novjass
/*************************************************************
* API Functions:
**************************************************************
*
* 1) JASS API. Very simplified API only attributing for basic tutorial needs. Should be enough in most cases.
*/
function CreateTutorial takes nothing returns Tutorial/*
* - Creates an empty Tutorial. Required to define Tutorial Sequences and Actions.
* - Be sure to save the returned Tutorial in a global variable to be able to play it later for all players.
* - Example:
* globals
* Tutorial X_StandardTutorial
* endglobals
*
* function SetupTutorial takes nothing returns nothing
* set X_StandardTutorial = CreateTutorial()
* endfunction
*
*/ function AddSequenceToLastCreatedTutorial takes string transmissionTitle, string transmissionText, real durationInSeconds returns TutorialSequence/*
* - Adds a text transmission to the Tutorial (title + body) that is displayed to the players for the specified duration.
* - Returns a TutorialSequence, but you don't need to save it in a variable, if you are just doing standard stuff (i.e. using the JASS API).
* - Parameters:
* transmissionTitle - Title of the transmission displayed. Giving hints about the Tutorial progress here is recommended, e.g. "Part 1/4".
* transmissionText - Text of the transmission displayed. This is, what explains your minigame to the players.
* durationInSeconds - duration of the transmission to be displayed.
*
*/ function AddActionToLastCreatedTutorialSequence takes real callAfterSecondsOfSequence, code actionFunc returns nothing /*
* - Adds a function to the last created tutorial sequence that shall be executed after x seconds of the sequence.
* - Use this to control, what happens on the screen, while the Tutorial Sequence is running. E.g. moving the camera, creating units, creating special effects, etc.
* - Parameters:
* callAfterSecondsOfSequence - timestamp for the function to execute during the sequence
* actionFunc - Function to be executed
*
*/ function StartTutorialForPlayer takes Tutorial tutorial, player whichPlayer returns nothing /*
* - Starts the specified Tutorial for the specified player.
* - You can only start the tutorial for one player at once. Use a loop to start tutorials for all players.
* - It's important to know that you can start the same tutorial for different players at different times, ). You can catch the currently watching player in your Tutorial Action functions using the GetTutorialPlayer function below.
*
*/ function GetTutorialPlayer takes nothing returns player /*
* - Returns the player watching the tutorial, when defining functions for your Tutorial Actions.
***********
* Example Code:
*
* globals
* Tutorial X_StandardTutorial
* endglobals
*
* function X_Sequence1_Beginning takes nothing returns nothing //creates a unit only visible for the Tutorial player, moving from one point to another and taking the camera with it.
* local unit u = CreateUnit(GetTutorialPlayer(), 'hfoo', 0., 0., 0.)
* call UnitApplyTimedLifeBJ( 15.00, 'BTLF', u ) //automatically destroys the unit after 15 seconds
* call ShowUnit(u, false) //hides the unit for all players
* if GetLocalPlayer() == GetTutorialPlayer() then
* call ShowUnit(u, true) //shows the unit for the tutorial player only
* endif
* call SetCameraTargetControllerNoZForPlayer( GetTutorialPlayer(), u, 0, 0, false ) //locks Tutorial players camera to his unit u
* call IssuePointOrder(u, "move", 200., 200.) //orders the unit to move somewhere else
* call SaveUnitHandle(udg_AttachTable, GetHandleId(GetTutorialPlayer()), StringHash("TutorialUnit"), u) //remember the unit to later retreive it for further use.
* endfunction
*
* function X_Sequence2_SpecialEffect takes nothing returns nothing
* local unit u = LoadUnitHandle(udg_AttachTable, GetHandleId(GetTutorialPlayer()), StringHash("TutorialUnit"))
* call CreateDummyAnimationXY('xd00', "stand", GetUnitX(u), GetUnitY(u)) //creates a special effect 'xd00' at the units location
* endfunction
*
* function X_SetupTutorial takes nothing returns nothing //we assume that this function is called during minigame setup.
* local string sequenceText //not necessary, but better to read
*
* set X_StandardTutorial = CreateTutorial()
*
* //Scene 1
* set sequenceText = "Look at this dumbass footman. He has no clue what to do in this minigame."
* call AddSequenceToLastCreatedTutorial("Part 1/2", sequenceText, 8.) //displays the specified text for 8 seconds to the players
* call AddActionToLastCreatedTutorialSequence(0., function X_Sequence1_Beginning)
*
* //Scene 2
* set sequenceText = "Footman Hugo seems to have found a direction, although it's clearly the wrong one. Look at this special effect, haha!"
* call AddSequenceToLastCreatedTutorial("Part 2/2", sequenceText, 5.)
* call AddActionToLastCreatedTutorialSequence(2., function X_Sequence2_SpecialEffect) //creates a special effect at the units location at 2 seconds sequence time
* endfunction
*
* //this function would start the same tutorial for all players. Note that each player has his own tutorial unit, invisible to the other players.
* //of course you can only create one unit and force all players to watch it. Still note that the functions in the Tutorial Actions get still executed for every player, so you would need to ensure by if-condition that they happen only once, or create a leading Tutorial and a subsidiary Tutorial.
* function X_DoThisToStartTheTutorial takes nothing returns nothing
* local integer i = 1
* loop
* exitwhen i > udg_Num_PlayersMinigame
* call StartTutorialForPlayer(X_StandardTutorial, MinigamePlayer(i) )
* set i = i+1
* endloop
* endfunction
*
**************************************************************
*
* 2) vJASS API.
*
* - All attributes show their default values. If you want to use defaults, setting the values is not required.
* - Disclaimer: For "standard use", you would only need to use the functions indented with *!!!*
* - vJASS users still need two functions from the JASS API above:*/ GetTutorialPlayer() /*and*/ StartTutorialForPlayer(Tutorial, player)/*
*
*/ struct Tutorial /*
* - The Tutorial object is a cinematic object consisting of different Sequences (Text Transmissions and/or action executions).
*!!!*/ static method create takes nothing returns Tutorial /*
* - returns the object that you need to add sequences and playing it for players.
* - set X_Tutorial = Tutorial.create() //requires a global Tutorial variable.
*/ public boolean applyLastKnownSettingsToNewSequences = true /*
* - Setting this to true makes every newly added Tutorial Sequence copy the UI, fog and transmission settings from the previously added Tutorial Sequence. Affects exactly the attributes not indented with *!!!* at the beginning of the line.
* - Allows you to only specify your UI, fog and transmission settings once for the first Tutorial Sequence. Obviously recommended, when all of your sequences are supposed to have the same settings.
* - Setting this to false will always let new Tutorial Sequences apply Standard settings (see defaults in TutorialSequence struct).
* - This setting only determines the settings applied on Sequence creation (addSequence, see below). You can still overwrite everything after sequence creation.
* - Thus, setting this boolean to true would require you to specify the settings that change from each sequence to the next, while setting it to false would require you to specify the settings that differ from the defaults stated below.
* - If you only want to use default settings, then this attribute is completely irrelevant.
*/ public real interfaceFadeInTime = 0. /*
* - defines, how many second it takes to return from letterbox mode to the normal interface after the tutorial is over. Only takes effect, when the last tutorial sequence was using letterbox mode.
*!!!*/ method addSequence takes nothing returns TutorialSequence /*
* - adds a new Tutorial Sequence to the Tutorial and returns it.
* - Example:
* local TutorialSequence seq
* set seq = X_Tutorial.addSequence()
*/ method destroy takes nothing returns nothing /*
* - destroys the Tutorial and all Sequences and Actions that were attached to it.
*
*/ struct TutorialSequence /*
* - A TutorialSequence is two things: A text transmission (optional) to the player and a collection of functions to execute, letting things happen on screen (also optional).
* - create new TutorialSequences only with the addSequence method of the Tutorial struct above.
* - All Settings except the Fog related ones apply for the watching player only.
*!!!*/ method setDuration takes real durationInSeconds returns nothing /*
* - sets the duration of the TutorialSequence, i.e. how long the Text Transmission will be displayed to the players (if there is one).
* - If you don't set the duration via this function, the duration will instead be the maximum timestamp of all Tutorial Actions added to this sequence (which might be exactly what you want).
*!!!*/ method addAction takes real callAfterSecondsOfSequence, code actionFunc returns nothing /*
* - lets the specified function be executed after the specified amount of seconds in the sequence
* - this is your main tool to let everything apart from the transmissions happen during the tutorial.
* - like always, actionFunc has to be a takes-nothing-returns-nothing function
* - use GetTutorialPlayer() to access the watching player inside the actionFuncs.
*
* TRANSMISSION SETTINGS
*/ public boolean playTransmission = true /* [affected by applyLastKnownSettingsToNewSequences]
* - Set to true in case you want to send a transmission to the watching player. Why shouldn't you, right?
* - If you set this to false, all other transmission related settings below won't have any effect on the sequence.
* - the display style of the transmission depends on the letterboxMode setting below. LetterboxMode = true yields a proper cinematic, where text is displayed in the interface area on the bottom of the screen. If LetterboxMode = false, Transmissions will display as (good looking) text messages.
*!!!*/ public string transmissionTitle = null /*
* - Defines the title of the transmission displayed to the players.
* - People expect the speaker's name to be displayed, but I recommend using a tutorial progress indicator like "Part 1/4" instead.
*!!!*/ public string transmissionText = null /*
* - Defines the text body of the transmission displayed to the players.
* - Most important setting of a transmission. This contains the actual explanation that players need to see.
*/ public boolean showCountdownInTransmissionTitle = true /* [affected by applyLastKnownSettingsToNewSequences]
* - If set to true, the transmission will show the remaining sequence Time in the transmission Title, as an indicator for the players, how long they can still read the current text, before the next sequence starts.
*/ public boolean usePlayerSpecificTransmissionUnits = true /* [affected by applyLastKnownSettingsToNewSequences]
* - defines, if the Portrait unit used during transmission depends on the watching player or not.
* - if set to true, default behaviour is that people see the portrait of their own minigame character they have chosen at map beginning. If you want to set other player depending portraits, use the setPlayerTransmissionUnitTypeId function below.
* - if set to false, all players would see the portrait of the unittype set in transmissionUnitType below.
*/ public integer transmissionUnitType = 'nmyr' /* [affected by applyLastKnownSettingsToNewSequences]
* - only takes effect, when usePlayerSpecificTransmissionUnits is set to false. Will then set the portrait unit to the one specified for all players.
*/ method setPlayerTransmissionUnittypeId takes player whichPlayer, integer portraitUnittypeId returns nothing /* [affected by applyLastKnownSettingsToNewSequences]
* - only takes effect, when usePLayerSpecificTransmissionUnits is set to true. Defines for a specific player, what portrait he would see during transmission.
* - by default (if you don't use this method), portraits are set to those of players minigame characters.
*
* UI & FOG SETTINGS
*/ public boolean disableUserControl = true /* [affected by applyLastKnownSettingsToNewSequences]
* - prevents the user from giving any valid input while the cinematic is running.
* - This includes: Visibility of mouse cursor, ability to select and order units, ability to move camera via mouse or arrow keys, maybe even visibility of selection circles.
* - letterbox Mode would hinder the player from giving orders anyway (I believe). If letterbox mode is activated, this setting would still decide, if the rest (camera movement etc.) is possible or not.
*/ public boolean letterboxMode = true /* [affected by applyLastKnownSettingsToNewSequences]
* - replaces the standard game interface by a big cinematic textbox, which will then be used to display transmission text.
* - Setting this to false will cause transmissions to display as text messages instead (good looking ones, but still meh).
*/ public real letterboxModeFadeTime = 0. /* [affected by applyLastKnownSettingsToNewSequences]
* - sets the time in seconds that the transition from normal interface into letterbox Mode or vice versa will take. Only has an effect, when this sequence's letterbox is different from the previous.
*/ public boolean clearScreenOfTextMessages = true /* [affected by applyLastKnownSettingsToNewSequences]
* - Setting this to true clears text messages (system generated and user written) upon sequence start to prevent the screen from being clustered with unrelated information
*/ public boolean disableFogOfWar = true /* [affected by applyLastKnownSettingsToNewSequences]
* - disables fog of war during this sequence. Affects ALL players, because disabling it for one player only would cause desyncs.
* - If you want to have different fog settings for different sequences, be sure to start the tutorial simultaneously for all players to avoid bugs (because all watching players would trigger global fog changes for all players at the same time, which might be unwanted, if players are at different points in the tutorial).
*/ public boolean disableBlackMask = true /* [affected by applyLastKnownSettingsToNewSequences]
* - disables black mask during this sequence. Affects ALL players, because disabling it for one player only would cause desyncs.
* - If you want to have different fow settings for different sequences, be sure to start the tutorial simultaneously for all players to avoid bugs.
*
* METAGAME FUNCTIONS (only for Eike)
*/ static method setDefaultPlayerTransmissionUnittypeId takes player whichPlayer, integer portraitUnittypeId returns nothing /*
* - sets the default portraits to be used in transmissions with player specific portraits.
* - to be called after players have chosen their minigame models.
*
***********
* Example Code:
*
* globals
* Tutorial X_StandardTutorial
* endglobals
*
* function X_Sequence1_Beginning takes nothing returns nothing //like in the JASS example above
*
* function X_Sequence2_SpecialEffect takes nothing returns nothing //like in the JASS example above
*
* function X_SetupTutorial takes nothing returns nothing //we assume that this function is called during minigame setup.
* local TutorialSequence seq //used below to apply settings
*
* set X_StandardTutorial = Tutorial.create()
*
* //Scene 1
* set seq = X_StandardTutorial.addSequence()
* call seq.setDuration(8.)
* set seq.transmissionTitle = "Part 1/2"
* set seq.transmissionText = "Look at this dumbass footman. He has no clue what to do in this minigame."
* set seq.usePlayerSpecificTransmissionUnits = false //this setting will carry over to the next created sequence, because X_StandardTutorial.applyLastKnownSettingsToNewSequences is set to true
* set seq.transmissionUnitType = 'hfoo' //this setting will also carry over
* call seq.addAction(0., function X_Sequence1_Beginning)
*
* //Scene 2
* set seq = X_StandardTutorial.addSequence()
* call seq.setDuration(5.)
* set seq.sequenceTitle = "Part 2/2"
* set seq.sequenceText = "Footman Hugo seems to have found a direction, although it's clearly the wrong one. Look at this special effect, haha!"
* set seq.addAction(2., function X_Sequence2_SpecialEffect)
* //usePlayerSpecificTransmissionUnits = false and transmissionUnitType = 'hfoo' is automatically adapted from the previous Sequence.
*
* endfunction
*
* function X_DoThisToStartTheTutorial takes nothing returns nothing //like in the JASS example above
*
*****************************************************************/
//! endnovjass
struct TutorialAction
public real callAfterSecondsOfSequence = 0.
private trigger actionsToExecute
implement List
static method create takes nothing returns thistype
local TutorialAction this = TutorialAction.allocate()
call TutorialAction.makeHead(this) //is fine even for nodes that get created later and are not supposed to be head, because non-head nodes next and prev get overwritten upon insertion in the list
return this
endmethod
public method setActionsToExecute takes code actionFunc returns nothing
local trigger actionTrigger = CreateTrigger()
call TriggerAddAction(actionTrigger, actionFunc)
set actionsToExecute = actionTrigger
endmethod
method execute takes nothing returns nothing
call TriggerExecute(this.actionsToExecute)
endmethod
method destroy takes nothing returns nothing
call DestroyTrigger(this.actionsToExecute)
call this.deallocate()
endmethod
method onRemove takes thistype node returns nothing
call node.destroy()
endmethod
endstruct
struct TutorialSequence
private integer numActions = 0
private real duration = 0. //used to display a countdown in integer steps, showing the players how long they can still read the transmission
TutorialAction tutorialActionList
private integer array playerTransmissionUnitIds[30] //should automatically set to the MinigameCharacters, but... minigame API comes later. to be done.
static integer array defaultPlayerTransmissionUnitIds[30] //saves the default values for the player specific transmission portraits. can be filled with Minigame Characters from outside.
private boolean useDefaultPlayerTransmissionUnitIds = true //determines, where player-specific transmission portraits get read from (static array or non-static array)
//UI and fog settings
public boolean disableUserControl = true //prevents the user from giving any valid input while the cinematic is running. Might also disable selectionCircles for that time.
public boolean disableFogOfWar = true //disables fow during this sequence. Affects ALL players. if you want to have different fow settings for different sequences, be sure to start the tutorial simultaneously for all players to avoid bugs.
public boolean disableBlackMask = true //disables black mask during this sequence. Affects ALL players. if you want to have different fow settings for different sequences, be sure to start the tutorial simultaneously for all players to avoid bugs.
public boolean letterboxMode = true //replaces the standard game interface by a big cinematic textbox, which will then be used to display transmission text. Setting this to false will cause transmissions to display as text messages instead (good looking ones, but still meh). Activating LetterboxMode
public real letterboxModeFadeTime = 0. //sets the time in seconds that the transition from normal interface into letterbox Mode or vice versa will take. Only has an effect, when this sequence's letterbox is different from the previous.
public boolean clearScreenOfTextMessages = true //clears text messages (system generated and user written) upon sequence start to prevent the screen from being clustered with unrelated information
//transmission settings
public boolean playTransmission = true //set to true in case you want to send a transmission to the watching player. Why shouldn't you, right?
public string transmissionTitle = null
public string transmissionText = null
public boolean usePlayerSpecificTransmissionUnits = true //setting this to true will cause the transmission to show a unit portrait depending on the watching player. Setting this to false will display the transmission from the unittype set in transmissionUnitType.
public integer transmissionUnitType = 'nmyr' //only takes effect, when usePlayerSpecificTransmissionUnits is set to false. Will then set the portrait unit to the one specified for all players.
public boolean showCountdownInTransmissionTitle = true //If set to true, shows the remaining time to read in the transmissionTitle
implement List
static method create takes nothing returns thistype
local thistype this = TutorialSequence.allocate()
call TutorialSequence.makeHead(this) //is fine even for nodes that get created later and are not supposed to be head, because non-head nodes next and prev get overwritten upon insertion in the list
set this.tutorialActionList = TutorialAction.create()
return this
endmethod
method applyHeadSettings takes nothing returns nothing //used for the TutorialSequencelist headNode to simulate standard game settings (so the tutorial play engine can properly determine which UI things to change, when the first real sequence starts.)
set this.disableUserControl = false
set this.disableFogOfWar = false //ToDo: In der TutorialPlayEngine beim Start eines Tutorials die zu dieser Zeit aktiven Settings annehmen, damit kein ungewollter MapSightReset durch Aktivierung und Deakt. stattfindet. Vielleicht gar nicht nötig, weil die Standardsettings annehmen, dass FoW aktiviert ist und genau dann kein Reset stattfindet. Falls FoW deaktiviert ist, ist auch alles gut, weil dann nichts resetted werden kann.
set this.disableBlackMask = false
set this.letterboxMode = false
set this.playTransmission = false //the head node is not supposed to play a transmission
endmethod
method copySettings takes TutorialSequence seq returns nothing //copies public settings from this TutorialSequence to another
local integer i = 0
//UI, fog settings
set seq.disableUserControl = this.disableUserControl
set seq.disableFogOfWar = this.disableFogOfWar
set seq.disableBlackMask = this.disableBlackMask
set seq.letterboxMode = this.letterboxMode
set seq.letterboxModeFadeTime = this.letterboxModeFadeTime //if all sequences retain identical settings, then the fading time only applies to the very first and last sequence (transition from normal interface )
//transmission settings
set seq.playTransmission = this.playTransmission
set seq.usePlayerSpecificTransmissionUnits = this.usePlayerSpecificTransmissionUnits
set seq.transmissionUnitType = this.transmissionUnitType
set seq.showCountdownInTransmissionTitle = this.showCountdownInTransmissionTitle
set seq.useDefaultPlayerTransmissionUnitIds = this.useDefaultPlayerTransmissionUnitIds
if this.usePlayerSpecificTransmissionUnits then
loop
exitwhen i == 30
set seq.playerTransmissionUnitIds[i] = this.playerTransmissionUnitIds[i]
set i = i+1
endloop
endif
endmethod
method setPlayerTransmissionUnittypeId takes player whichPlayer, integer portraitUnittypeId returns nothing
set this.useDefaultPlayerTransmissionUnitIds = false //defaults are no longer in use, because custom portraits have been set.
set this.playerTransmissionUnitIds[GetPlayerId(whichPlayer)] = portraitUnittypeId
endmethod
static method setDefaultPlayerTransmissionUnittypeId takes player whichPlayer, integer portraitUnittypeId returns nothing
set TutorialSequence.defaultPlayerTransmissionUnitIds[GetPlayerId(whichPlayer)] = portraitUnittypeId
endmethod
method getPlayerTransmissionUnittypeId takes player whichPlayer returns integer
if this.useDefaultPlayerTransmissionUnitIds then
return TutorialSequence.defaultPlayerTransmissionUnitIds[GetPlayerId(whichPlayer)] //return default value
endif
return this.playerTransmissionUnitIds[GetPlayerId(whichPlayer)] //return user set value
endmethod
method addAction takes real callAfterSecondsOfSequence, code actionFunc returns TutorialAction
local TutorialAction newNode = TutorialAction.create()
local TutorialAction loopNode = this.tutorialActionList.front //equals head node in case of empty list
local boolean correctNode = false
loop //find the correct spot to insert
exitwhen correctNode or (loopNode == this.tutorialActionList) //loop until head element is reached. If so, newNode is inserted after head.prev, which is at the very end
if callAfterSecondsOfSequence < loopNode.callAfterSecondsOfSequence then
set correctNode = TRUE
else
set loopNode = loopNode.next
endif
endloop
//call DisplayAll("Insert new Node " + I2S(newNode) + " after node " + I2S(loopNode.prev))
call this.tutorialActionList.insert(loopNode.prev, newNode) //insert(a,new) inserts new after a, i.e. more towards back, i.e. a.next = new.
set this.numActions = this.numActions + 1
set newNode.callAfterSecondsOfSequence = callAfterSecondsOfSequence
call newNode.setActionsToExecute(actionFunc)
set this.duration = RMaxBJ(this.duration, callAfterSecondsOfSequence)
return newNode
endmethod
method setDuration takes real durationInSeconds returns nothing
//the duration is the maximum of the specified duration and the last action timestamp of the sequence
//actually we just make that implicitely by adding a dummy action at timestamp durationInSeconds
call this.addAction(durationInSeconds, null)
endmethod
method getDuration takes nothing returns real
return this.duration
endmethod
method destroy takes nothing returns nothing
call this.tutorialActionList.flush() //removes all nodes and destroys them on remove
call this.deallocate()
endmethod
method onRemove takes thistype node returns nothing
call node.destroy()
endmethod
endstruct
struct Tutorial
integer numSequences = 0
TutorialSequence tutorialSequenceList
static Tutorial lastCreatedTutorial
public boolean applyLastKnownSettingsToNewSequences = true
public real interfaceFadeInTime = 0. //defines, how many second it takes to return from letterbox mode to the normal interface after the tutorial is over. Only takes effect, when the last tutorial sequence was using letterbox mode.
static method create takes nothing returns thistype
local thistype this = Tutorial.allocate()
set this.tutorialSequenceList = TutorialSequence.create() //creates an empty Tutorial Sequence as starting point for the Linked List
call this.tutorialSequenceList.applyHeadSettings() //allows the Tutorial Play Engine to see, what UI settings have to be changed.
set Tutorial.lastCreatedTutorial = this
return this
endmethod
method addSequence takes nothing returns TutorialSequence
local TutorialSequence newNode = TutorialSequence.create()
call TutorialSequence.insert(this.tutorialSequenceList.back, newNode)
set this.numSequences = this.numSequences + 1
if this.numSequences >= 2 and this.applyLastKnownSettingsToNewSequences then //the second and further sequences copy its settings from its predecessor. The first shouldn't do that as the headNode's default setting relate to the standard game settings and are bad defaults for real sequences.
call newNode.prev.copySettings(newNode)
endif
return newNode
endmethod
method destroy takes nothing returns nothing
call this.tutorialSequenceList.flush() //calls .remove() for every node, hooking the onRemove in the TutorialSequence struct
call this.deallocate()
endmethod
endstruct
struct TutorialPlayEngine
static player getTutorialPlayer //global to retreive the player related to the tutorial actions via GetTutorialPlayer-function
private timer actionTimer //this timer sets the time to pass until the next action
private timer countdownTimer //this timer sets the time to pass until the next sequence
private boolean tutorialIsBeingPlayed = false
private real remainingSequenceTime = 0.
player tutorialPlayer //every player gets his own instance of TutorialPlayEngine and his own actionTimer
public Tutorial tutorial
private TutorialSequence currentTutorialSequence
private TutorialAction currentTutorialAction
static constant integer KEY_TUTORIAL_TIMER = StringHash("StructBelongingToTimer")
private static hashtable SystemAttachTable = InitHashtable()
//standard create method
method destroy takes nothing returns nothing
call FlushChildHashtable(SystemAttachTable, GetHandleId(this.actionTimer))
call FlushChildHashtable(SystemAttachTable, GetHandleId(this.countdownTimer))
//call PauseTimer(this.actionTimer)
call DestroyTimer(this.actionTimer)
call DestroyTimer(this.countdownTimer)
set this.actionTimer = null //prevents agent leaks and frees the handle id
set this.countdownTimer = null
call this.deallocate()
endmethod
private method attachStructToTimer takes timer whichTimer returns nothing
call SaveInteger(SystemAttachTable, GetHandleId(whichTimer), KEY_TUTORIAL_TIMER, this)
endmethod
static method getStructFromTimer takes timer whichTimer returns integer
return LoadInteger(SystemAttachTable, GetHandleId(whichTimer), KEY_TUTORIAL_TIMER)
endmethod
private method startCustomCinematicMode takes TutorialSequence previousTutorialSequence returns nothing
//global stuff
if this.currentTutorialSequence.disableFogOfWar != previousTutorialSequence.disableFogOfWar then
call FogEnable(not this.currentTutorialSequence.disableFogOfWar)
endif
if this.currentTutorialSequence.disableBlackMask != previousTutorialSequence.disableBlackMask then
call FogMaskEnable(not this.currentTutorialSequence.disableBlackMask)
endif
//local stuff
if GetLocalPlayer() == this.tutorialPlayer then
if this.currentTutorialSequence.clearScreenOfTextMessages then
call ClearTextMessages()
endif
if this.currentTutorialSequence.letterboxMode != previousTutorialSequence.letterboxMode then
call ShowInterface(not this.currentTutorialSequence.letterboxMode, this.currentTutorialSequence.letterboxModeFadeTime)
endif
call EnableUserControl(not this.currentTutorialSequence.disableUserControl) //must always fire without change condition, because enabling/disabling letterbox mode internally changes user control.
endif
endmethod
private method endCustomCinematicMode takes nothing returns nothing
//doesn't reset FoW and Black Mask settings, because the minigame might have different settings and we don't want to screw up anything.
if GetLocalPlayer() == this.tutorialPlayer then //this block does nothing in case the settings are already active.
call EnableUserControl(true)
if this.tutorial.tutorialSequenceList.back.letterboxMode then //if the last tutorial sequence was using letterbox mode, we switch back to normal interface.
call ShowInterface(true, this.tutorial.interfaceFadeInTime)
endif
endif
endmethod
method endTutorial takes nothing returns nothing
call this.endCustomCinematicMode()
call this.destroy()
endmethod
method updateTransmission takes nothing returns nothing
local TutorialSequence seq = this.currentTutorialSequence
local integer portraitUnitId = seq.transmissionUnitType
if seq.usePlayerSpecificTransmissionUnits then
set portraitUnitId = seq.getPlayerTransmissionUnittypeId(this.tutorialPlayer)
endif
if GetLocalPlayer() == this.tutorialPlayer then
if seq.showCountdownInTransmissionTitle then //countdown Scenes must update every second
call SetCinematicScene(portraitUnitId, GetPlayerColor(this.tutorialPlayer), seq.transmissionTitle + " - " + I2S(R2I(this.remainingSequenceTime)), seq.transmissionText, RMinBJ(this.remainingSequenceTime, 1.), RMinBJ(this.remainingSequenceTime, 1.)) //first duratin defines the length of the transmission. Second duration the "voiceoverDuration", whatever that means.
elseif seq.getDuration() == this.remainingSequenceTime then //non-countdown Scenes must play only once at the very beginning
call SetCinematicScene(portraitUnitId, GetPlayerColor(this.tutorialPlayer), seq.transmissionTitle , seq.transmissionText, seq.getDuration(), seq.getDuration()) //first duration defines the length of the transmission. Second duration the "voiceoverDuration", whatever that means.
endif
endif
endmethod
static method timerFuncDoCountdownTick takes nothing returns nothing
local TutorialPlayEngine engine = TutorialPlayEngine.getStructFromTimer(GetExpiredTimer())
//call DisplayAll("|cffffcc00" + I2S(R2I(engine.remainingSequenceTime)) + "/"+ I2S(R2I(engine.currentTutorialSequence.getDuration())) + "|r")
if engine.currentTutorialSequence.playTransmission then
call engine.updateTransmission()
endif
set engine.remainingSequenceTime = engine.remainingSequenceTime - 1. //this is obviously wrong right now, but right at the beginning of the next call of this function, which is what it's supposed for ;)
//update transmission/ countdown display
//can't use RMinBJ(engine.remainingSequenceTime, 1.) in the timer duration, because it might equal 0 at the very end of the sequence, thus proccing very often in a row
if not (engine.remainingSequenceTime < 1.) then
call TimerStart(engine.countdownTimer, 1., false, function TutorialPlayEngine.timerFuncDoCountdownTick)
endif
endmethod
private method startSequence takes TutorialSequence seq returns nothing //does everything apart from starting the timer
local TutorialSequence previousTutorialSequence = this.currentTutorialSequence //used to determine which UI and environment settings need a change (e.g. change in cinematicInterface settings).
set this.currentTutorialSequence = seq
set this.remainingSequenceTime = seq.getDuration() //.getDuration() returns the maximum of the specified sequence time and latest action timestamp in the sequence.
call this.startCustomCinematicMode(previousTutorialSequence) //does UI and fog changes.
set this.currentTutorialAction = seq.tutorialActionList //not .front, because the current action is the head node, until the first actions timestamp is reached
//call DisplayAll("Start Sequence " + I2S(seq) + " with " + R2FS(this.remainingSequenceTime) + " duration")
call TimerStart(this.countdownTimer, RMinBJ(this.remainingSequenceTime, 0.), false, function TutorialPlayEngine.timerFuncDoCountdownTick) //RMinBJ(this.remainingSequenceTime, 0.).... warum?
endmethod
static method startSequenceAfterDelay takes nothing returns nothing
local TutorialPlayEngine engine = TutorialPlayEngine.getStructFromTimer(GetExpiredTimer())
call engine.startSequence(engine.currentTutorialSequence.next) //current TutorialSequence is over after nextActionTimerDur.
endmethod
static method timerFuncDoTutorialLoop takes nothing returns nothing
local TutorialPlayEngine engine = TutorialPlayEngine.getStructFromTimer(GetExpiredTimer())
local real lastActionTimestamp = engine.currentTutorialAction.callAfterSecondsOfSequence //remembers, after how many seconds of the current sequence the action is getting called. Yields 0, if current action is the head element of the list.
local real nextActionTimerDur
set TutorialPlayEngine.getTutorialPlayer = engine.tutorialPlayer //allows the player to be retreived via TP_GetTutorialPlayer() in the following trigger execution
call engine.currentTutorialAction.execute()
if engine.currentTutorialAction == engine.currentTutorialSequence.tutorialActionList.back then //last action of current TutorialSequence has been played.
if engine.currentTutorialSequence == engine.tutorial.tutorialSequenceList.back then //Tutorial is over
call engine.endTutorial() //also destroys the engine.
return
else //start next sequence
call engine.startSequence(engine.currentTutorialSequence.next) //current TutorialSequence is over after nextActionTimerDur.
set nextActionTimerDur = 0. //immediately starts the first action, which is the head node of the action list. Only starts the next countdown for the real first action. Step needed in case action list is empty.
endif
else //resume current TutorialSequence with next action
set engine.currentTutorialAction = engine.currentTutorialAction.next
set nextActionTimerDur = engine.currentTutorialAction.callAfterSecondsOfSequence - lastActionTimestamp
endif
call TimerStart(engine.actionTimer, nextActionTimerDur, false, function TutorialPlayEngine.timerFuncDoTutorialLoop)
//call DisplayAll("Intending to start timer " + I2S(GetHandleId(engine.actionTimer)) + " with " + R2FS(nextActionTimerDur))
endmethod
method startTutorial takes nothing returns nothing
if this.tutorialIsBeingPlayed then
return
endif
set this.tutorialIsBeingPlayed = true
set this.actionTimer = CreateTimer()
call this.attachStructToTimer(this.actionTimer)
set this.countdownTimer = CreateTimer()
call this.attachStructToTimer(this.countdownTimer)
call this.startSequence(this.tutorial.tutorialSequenceList.front)
//executes actions and sequences
call TimerStart(this.actionTimer, this.currentTutorialAction.callAfterSecondsOfSequence, false, function TutorialPlayEngine.timerFuncDoTutorialLoop)
endmethod
endstruct
function CreateTutorial takes nothing returns Tutorial
return Tutorial.create()
endfunction
function AddSequenceToLastCreatedTutorial takes string transmissionTitle, string transmissionText, real durationInSeconds returns TutorialSequence
local TutorialSequence seq = Tutorial.lastCreatedTutorial.addSequence()
set seq.transmissionTitle = transmissionTitle
set seq.transmissionText = transmissionText
call seq.setDuration(durationInSeconds)
return seq
endfunction
function AddActionToLastCreatedTutorialSequence takes real callAfterSecondsOfSequence, code actionFunc returns nothing
call Tutorial.lastCreatedTutorial.tutorialSequenceList.back.addAction(callAfterSecondsOfSequence, actionFunc)
endfunction
function StartTutorialForPlayer takes Tutorial tutorial, player whichPlayer returns nothing
local TutorialPlayEngine engine = TutorialPlayEngine.create()
set engine.tutorial = tutorial
set engine.tutorialPlayer = whichPlayer
if tutorial != 0 then //non-initialized tutorials should not be executed. else the engine would endless loop
call engine.startTutorial()
endif
endfunction
function GetTutorialPlayer takes nothing returns player
return TutorialPlayEngine.getTutorialPlayer
endfunction