• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

What is the best way to remake the transmission functions?

Status
Not open for further replies.
Level 24
Joined
Jun 26, 2020
Messages
1,853
Since the transmission functions uses the infamous TriggerSleepAction and my map is multiplayer (double problem) I think I should remake them, but I'm not sure about to how to do it, I can just use the native functions and timers, but I have to create a function for each dialogue line and also each time set the delay for the timers making it more difficult to manage, but is this my better option or is there a way to "generate code" that I need?
 
Level 20
Joined
Apr 12, 2018
Messages
494
There's no reason to do that. Set the 'Wait' time in the transmission trigger to 'Don't Wait' and use your own 'wait' right after the transmission.
 
Level 24
Joined
Jun 26, 2020
Messages
1,853
There's no reason to do that. Set the 'Wait' time in the transmission trigger to 'Don't Wait' and use your own 'wait' right after the transmission.
Basically this:
I have to create a function for each dialogue line and also each time set the delay for the timers making it more difficult to manage
And that will create too much code, by the way I'm creating a library to solve this problem.
 
Level 20
Joined
Jul 10, 2009
Messages
479
Save your different transmission parameters (text, duration, etc.) in an array in the order they should be played and use a timer to loop over it. The timer needs to remember, at what index of your array it currently stays, i.e. what to play next. You don't need a lot of functions for that, but just one function that can start a transmission with the information from the current array slot (which gets called by the timer).
 
Level 24
Joined
Jun 26, 2020
Messages
1,853
Save your different transmission parameters (text, duration, etc.) in an array in the order they should be played and use a timer to loop over it. The timer needs to remember, at what index of your array it currently stays, i.e. what to play next. You don't need a lot of functions for that, but just one function that can start a transmission with the information from the current array slot (which gets called by the timer).
Thats what I think to my library, if you wanna know what am I doing in these hours here it is:
vJASS:
library Transmission requires /*
   
    */TimerUtils, /*
    */Alloc, /*
    */optional Table
   
    //! novjass
    //How it works:
    //    Set a value of type Transmission and use the function create like this:
       
            local Transmission curr=Transmission.create(toForce,data)
           
     //   "toForce" is the force that will see and hear the transmission, "data" is an integer that you can store to use at the end of the transmission.
       
     //   Then to add a dialogue line just do:
       
            call curr.AddLine(unit whichUnit,string unitName,sound soundHandle,string message,integer timeType,real timeVal,boolean wait,code actions)
           
        "whichUnit": The unit who do the dialogue line.
        "unitName": The name of that unit.
        "soundHandle": The sound that will be played during the dialogue line.
        "message": What the unit will say.
        "timeType": Is like the GUI function, you can use 0 (Transmission.ADD), 1 (Transmission.SET), 2 (Transmission.SUB), another value will make the wait time to 0.
        "timeVal": Is like the GUI function, the normal duration of the transmission is the duration of the sound and you can edit the value depending of this and the previous value.
        "wait": Is like the GUI function, if this is true the next transmission will be wait the time depending of the previous two values.
        "actions": If you wanna add an action that happens when the dialogue line is happening.
       
    //    You can also add an action to the end of the transmission using
   
            call curr.AddEnd(code func)
           
    /*    Where you can use the values */Transmission.Data/* if you stored one and */Transmission.Skipped/* to detect if the cinematic was skipped
          but if was skipped in the last line this value will remain false.*/
           
    //    After set the lines call the function
   
            call curr.Start()
           
    //    To start the transmission, and to skip the transmission just press the ESC button (this will skip all the transmissions where the player is),
    //    but if the implied force has more than one player the process will continue to the rest of players on that force. The transmission will end
    //    only if all the implied players skip the transmission (So if you use "All players" probably you never won't skip the transmission)
    /*    but you can use the */function Force(player p)/* to have a force just for 1 player (Don't destroy those forces).*/
    //! endnovjass
   
    //A function to use
    globals
        private integer Temp
        private force array What_Force
    endglobals
   
    private function Enum takes nothing returns nothing
        set Temp=Temp+1
    endfunction
   
    private function IsForceEmpty takes force f returns boolean
        if f==null then
            return true
        else
            set Temp=0
            call ForForce(f,function Enum)
        endif
        return Temp==0
    endfunction
   
    //This are the elements of each line transmission
   
    private struct Elements extends array
   
        implement Alloc
        unit talker
        playercolor color
        string name
        real duration
        integer timetype
        boolean wait
        sound mysound
        string text
        trigger actions
       
        method destroy takes nothing returns nothing
            set this.talker=null
            set this.name=null
            set this.mysound=null
            set this.text=null
            set this.timetype=-1
            set this.duration=0.00
            set this.wait=false
            call TriggerClearConditions(this.actions)
            call DestroyTrigger(this.actions)
            set this.actions=null
            call this.deallocate()
        endmethod
       
        static method create takes unit whichUnit, string unitName, sound soundHandle, string message, integer timeType, real timeVal, boolean wait, code func returns thistype
            local thistype this=thistype.allocate()
           
            set this.talker=whichUnit
            set this.name=unitName
            set this.mysound=soundHandle
            set this.text=message
            set this.timetype=timeType
            set this.duration=timeVal
            set this.wait=wait
            //Since I can't store codes I use triggers
            if func!=null then
                set this.actions=CreateTrigger()
                call TriggerAddCondition(this.actions,Condition(func))
            endif
           
            return this
        endmethod
       
    endstruct
   
    //A methods to have a linked list to each instance transmission
   
    private struct Next extends array
       
        static if LIBRARY_Table then
            static Table n
        else
            static hashtable n=InitHashtable()
        endif
       
        method operator []= takes integer k, integer v returns nothing
            static if LIBRARY_Table then
                set thistype.n[this].integer[k]=v
            else
                call SaveInteger(thistype.n,this,k,v)
            endif
        endmethod
       
        method operator [] takes integer k returns integer
            static if LIBRARY_Table then
                return thistype.n[this].integer[k]
            else
                return LoadInteger(thistype.n,this,k)
            endif
        endmethod
       
    endstruct
   
    private struct Prev extends array
   
        static if LIBRARY_Table then
            static Table p
        else
            static hashtable p=InitHashtable()
        endif
       
        method operator []= takes integer k, integer v returns nothing
            static if LIBRARY_Table then
                set thistype.p[this].integer[k]=v
            else
                call SaveInteger(thistype.p,this,k,v)
            endif
        endmethod
       
        method operator [] takes integer k returns integer
            static if LIBRARY_Table then
                return thistype.p[this].integer[k]
            else
                return LoadInteger(thistype.p,this,k)
            endif
        endmethod
       
    endstruct
   
    //The actual struct of the transmission
   
    private module M
       
        //These are to make it more elegant than just put integers
        static constant integer ADD=0
        static constant integer SET=1
        static constant integer SUB=2
       
        static integer Data
        static boolean Skipped
        static player LocalPlayer
        static integer array Index
        static integer array Index_current
       
        static if LIBRARY_Table then
            static Table Instance
        else
            static hashtable Instance=InitHashtable()
        endif
       
        //To store a temp "this" to pass it to another functions
        private static thistype temp
       
        Elements elements
        integer data
        force toForce
        trigger final
        timer t
       
        method operator next takes nothing returns Next
            return this
        endmethod
       
        method operator prev takes nothing returns Prev
            return this
        endmethod
       
        private static method ClearData takes nothing returns nothing
            local integer i=GetPlayerId(GetEnumPlayer())
            set thistype.Index_current[i]=thistype.Index_current[i]-1
            if thistype.Index_current[i]==0 then
                set thistype.Index[i]=0
                static if LIBRARY_Table then
                    call thistype.Instance[i].flush()
                else
                    call FlushChildHashtable(thistype.Instance,i)
                endif
            endif
        endmethod
       
        //When the transmission ends
        method finish takes nothing returns nothing
            set this.next[0]=0
            set this.prev[0]=0
            call ReleaseTimer(this.t)
            call ForForce(this.toForce,function thistype.ClearData)
            call DestroyForce(this.toForce)
            if this.final!=null then
                set thistype.Data=this.data
                call TriggerEvaluate(this.final)
                call TriggerClearConditions(this.final)
                call DestroyTrigger(this.final)
                set this.final=null
                set thistype.Data=0
            endif
            set thistype.Skipped=false
            set this.toForce=null
            set this.t=null
            call this.deallocate()
        endmethod
       
        static method callback takes nothing returns nothing
            local thistype this=GetTimerData(GetExpiredTimer())
            if this.elements==0 or IsForceEmpty(this.toForce) then
                if IsForceEmpty(this.toForce) and this.elements!=0 then
                //If the cinematic was skipped just in the last line it won't be counted as skipped
                    set thistype.Skipped=true
                endif
                call this.finish()
            else
                call this.cinematic_line()
            endif
        endmethod
       
        //Where the magic happens
        method cinematic_line takes nothing returns nothing
            local Elements what=this.elements
            local integer alpha=0
            local real delay=0.00
            if IsPlayerInForce(thistype.LocalPlayer,this.toForce) then
                if what.mysound!=null then
                    call StartSound(what.mysound)
                endif
                call SetCinematicScene(GetUnitTypeId(what.talker),what.color,what.name,what.text,what.duration+bj_TRANSMISSION_PORT_HANGTIME,what.duration)
                set alpha=255
            endif
            call UnitAddIndicator(what.talker,255,255,255,alpha)
           
            if what.actions!=null then
                call TriggerEvaluate(what.actions)
            endif
           
            set this.elements=this.next[this.elements]
           
            if what.wait then
                if what.timetype==thistype.ADD then
                    set delay=GetSoundDurationBJ(what.mysound)+what.duration
                elseif what.timetype==thistype.SET then
                    set delay=what.duration
                elseif what.timetype==thistype.SUB then
                    set delay=RMaxBJ(0.00,GetSoundDurationBJ(what.mysound)-what.duration)
                endif
                if delay>0.00 then
                    call TimerStart(this.t,delay,false,function thistype.callback)
                else
                    call thistype.callback()
                endif
            else
                call thistype.callback()
            endif
           
            call what.destroy()
        endmethod
       
        //If the instance is 0 then none of this methods will run
       
        method Start takes nothing returns nothing
            //If there is not a line so the trasnmition is not runned
            if this!=0 and this.next[0]!=0 then
                set this.elements=this.next[0]
                call this.cinematic_line()
            endif
        endmethod
       
        //Since I can't store codes I use triggers
        method AddEnd takes code func returns nothing
            if this!=0 then
                if this.final==null then
                    set this.final=CreateTrigger()
                endif
                call TriggerAddCondition(this.final,Condition(func))
            endif
        endmethod
       
        //To set a line with all the needed values
        method AddLine takes unit whichUnit, string unitName, sound soundHandle, string message, integer timeType, real timeVal, boolean wait, code actions returns nothing
            local Elements what
            if this!=0 then
                set what=Elements.create(whichUnit,unitName,soundHandle,message,timeType,timeVal,wait,actions)
       
                //Add to list
                set this.next[what]=0
                set this.prev[what]=this.prev[0]
                set this.next[this.prev[what]]=what
                set this.prev[0]=what
            endif
       
        endmethod
       
        private static method AddPlayer takes nothing returns nothing
            local thistype this=thistype.temp
            local player p=GetEnumPlayer()
            local integer i=GetPlayerId(p)
            call ForceAddPlayer(this.toForce,p)
            static if LIBRARY_Table then
                set thistype.Instance[i].integer[thistype.Index[i]]=this
            else
                call SaveInteger(thistype.Instance,i,thistype.Index[i],this)
            endif
            set thistype.Index[i]=thistype.Index[i]+1
            set thistype.Index_current[i]=thistype.Index_current[i]+1
            set p=null
        endmethod
       
        static method create takes force toForce, integer data returns thistype
            local thistype this
            //Only returns a valid value if the force is valid
            if toForce!=null and not IsForceEmpty(toForce) then
                set this=thistype.allocate()
               
                set this.toForce=CreateForce()
                set thistype.temp=this
                call ForForce(toForce,function thistype.AddPlayer)
                set this.t=NewTimerEx(this)
               
                set this.data=data
               
                return this
            endif
            return 0
        endmethod
       
        //If a player skips cinematic he will be removed from every instance trasnmission where is in
        static method onActions takes nothing returns nothing
            local thistype this
            local Elements what
            local player p=GetTriggerPlayer()
            local integer i=GetPlayerId(p)
            local integer j=0
            loop
                exitwhen j>thistype.Index[i]
                static if LIBRARY_Table then
                    set this=thistype.Instance[i].integer[j]
                else
                    set this=LoadInteger(thistype.Instance,i,j)
                endif
                call ForceRemovePlayer(this.toForce,p)
                set what=this.prev[this.elements]
                if thistype.LocalPlayer==p then
                    //I don't know if this is free of desync
                    call EndCinematicScene()
                    if what.mysound!=null then
                        call StopSound(what.mysound,false,true)
                    endif
                endif
               
                set j=j+1
            endloop
            set p=null
        endmethod
       
        static method onInit takes nothing returns nothing
            local trigger t=CreateTrigger()
            local integer i=0
            loop
                exitwhen i>PLAYER_NEUTRAL_AGGRESSIVE
                set What_Force[i]=GetForceOfPlayer(Player(i)) //To use it if you wanna send the transmission just for 1 player
                call TriggerRegisterPlayerEvent(t,Player(i),EVENT_PLAYER_END_CINEMATIC)
                set i=i+1
            endloop
            call TriggerAddCondition(t,Condition(function thistype.onActions))
            set t=null
            //-- --
            set thistype.LocalPlayer=GetLocalPlayer()
            call ForceCinematicSubtitles(true)
            //-- --
            static if LIBRARY_Table then
                set Next.n=TableArray[0x2000]
                set Prev.p=TableArray[0x2000]
                set thistype.Instance=TableArray[0x2000]
            endif
        endmethod
    endmodule
   
    struct Transmission extends array
        implement Alloc
        implement M
    endstruct
   
    function Force takes player p returns force
        return What_Force[GetPlayerId(p)]
    endfunction
   
endlibrary
The problem is I don't know if is well made, and I'm not sure how is the TableArray (I need ask to @Bribe or someone else who know how it is), because that crashes my test map, so also I will share it to you the test map (with the Table disabled).
 

Attachments

  • Transmission.w3x
    40 KB · Views: 9
Level 20
Joined
Apr 12, 2018
Messages
494
Basically this:

And that will create too much code, by the way I'm creating a library to solve this problem.
It really doesn't. I've done it this way since the game came out.

I don't consider code where you have to define seven different variables on their own separate lines before you can output it 'compact' code. The default UI transmission is suitable enough for cleaner lines.
 
Level 20
Joined
Jul 10, 2009
Messages
479
Here you go:

JASS:
/*
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
It should be well documented and includes example code (which might be too complicated, though).
Also chances are that I forgot to remove comments or code pieces that are specific to my own project. You can try (or just use your own system, lol).

Edit: The examples in the documentation definitely contain functions specific to my project :D But the system itself should still work.
 
Level 24
Joined
Jun 26, 2020
Messages
1,853
Here you go:

JASS:
/*
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
It should be well documented and includes example code (which might be too complicated, though).
Also chances are that I forgot to remove comments or code pieces that are specific to my own project. You can try (or just use your own system, lol).

Edit: The examples in the documentation definitely contain functions specific to my project :D But the system itself should still work.
I read it and it has a lot of things I really don't need and don't hava some things I need like a delay between actions or make it to more than one player and a end actions, but still thanks.
 
Level 20
Joined
Jul 10, 2009
Messages
479
You did notice that the documentation is just the top part until the //! endnovjass, right? Everything after it, i.e. beginning from the TutorialAction struct, is pure code and not necessary looking at.

What exactly don't you understand?
You could describe an example cinematic and I show you how to code it with the system.

But please note that the system is not indended to be a quick and dirty solution. There is no sense in using it, if you don't plan to understand how to.
 
Level 24
Joined
Jun 26, 2020
Messages
1,853
You did notice that the documentation is just the top part until the //! endnovjass, right? Everything after it, i.e. beginning from the TutorialAction struct, is pure code and not necessary looking at.

What exactly don't you understand?
You could describe an example cinematic and I show you how to code it with the system.

But please note that the system is not indended to be a quick and dirty solution. There is no sense in using it, if you don't plan to understand how to.
Is not I don't wanna understand, is I don't have the patience to read to much (even if the topic is something I get interested), I understand this:
vJASS:
* 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
(In retrospective I just can manipulate this to get what I said)
But to the rest is hard to me remember what means what or for what thing was that other thing.
 
Level 24
Joined
Jun 26, 2020
Messages
1,853
I think it was clear when I said
I need like a delay between actions or make it to more than one player and a end actions
how do your system do that?
Well, this is my actual system, and here is an example:
vJASS:
function End takes nothing returns nothing
    call ResetToGameCameraForPlayer(Player(0),0)
    call CinematicModeBJ(false,Force(Player(0)))
    call EnableTrigger(gg_trg_Test_Paladin_CINEMATIC_way_2)
endfunction

function Scene takes nothing returns nothing
    local Transmission curr=Transmission.instance
    local integer i=curr.StepNumber
    if i==2 or i==5 or i==8 then
        call BJDebugMsg(I2S(curr.LineNumber))
        call SetCameraFieldForPlayer(Player(0),CAMERA_FIELD_TARGET_DISTANCE,1500.00,0)
        call SetCameraFieldForPlayer(Player(0),CAMERA_FIELD_ROTATION,90.00,0)
        call SetCameraFieldForPlayer(Player(0),CAMERA_FIELD_ANGLE_OF_ATTACK,340.00,0)
        call PanCameraToTimedForPlayer(Player(0),GetUnitX(gg_unit_Hpal_0003),GetUnitY(gg_unit_Hpal_0003),0)
    elseif i==3 or i==4 or i==6 then
        call BJDebugMsg(I2S(curr.LineNumber))
        call SetCameraFieldForPlayer(Player(0),CAMERA_FIELD_TARGET_DISTANCE,1500.00,0)
        call SetCameraFieldForPlayer(Player(0),CAMERA_FIELD_ROTATION,340.00,0)
        call SetCameraFieldForPlayer(Player(0),CAMERA_FIELD_ANGLE_OF_ATTACK,340.00,0)
        call PanCameraToTimedForPlayer(Player(0),GetUnitX(gg_unit_Hmkg_0002),GetUnitY(gg_unit_Hmkg_0002),0)
    endif
endfunction

function Trig_Test_Paladin_CINEMATIC_Actions takes nothing returns nothing
    local force f=Force(Player(0))
    local Transmission curr=Transmission.create(f,0)
    /*1*/call curr.AddAction(null,2.00)
    /*2*/call curr.AddLine(gg_unit_Hmkg_0002,"Barbarian",gg_snd_H06Arthas01,"Greetings captain.",Transmission.ADD,0.00,true)
    /*3*/call curr.AddLine(gg_unit_Hpal_0003,"Paladin",gg_snd_H06Uther02,"Greetings.",Transmission.ADD,0.00,true)
    /*4*/call curr.AddLine(gg_unit_Hpal_0003,"Paladin",gg_snd_H01Uther07,"Any new information?",Transmission.ADD,0.00,true)
    /*5*/call curr.AddLine(gg_unit_Hmkg_0002,"Barbarian",gg_snd_H06Arthas03,"No Sir.",Transmission.ADD,0.00,true)
    /*6*/call curr.AddLine(gg_unit_Hpal_0003,"Paladin",gg_snd_H06Uther05,"Well, you can back out.",Transmission.ADD,0.00,true)
    /*7*/call curr.AddAction(null,2.00)
    /*8*/call curr.AddLine(gg_unit_Hmkg_0002,"Barbarian",gg_snd_H06Arthas06,"Very well captain.",Transmission.ADD,0.00,true)
    call curr.AddSceneActions(function Scene)
    call curr.AddEnd(function End)
    call curr.Start()
    call CinematicModeBJ(true,f)
    call DisableTrigger(gg_trg_Test_Paladin_CINEMATIC_way_2)
    set f=null
endfunction
You can see it in action in the example map of the link (Is when the mountain king goes to the paladin), how it could be with your system?
 
Level 20
Joined
Jul 10, 2009
Messages
479
It would look like this with my system:

JASS:
function Trig_Start_Cinematic_Conditions takes nothing returns boolean
    if ( not ( GetEnteringUnit() == gg_unit_Hmkg_0002 ) ) then
        return false
    endif
    return true
endfunction

function Camera1 takes nothing returns nothing
        call SetCameraFieldForPlayer(GetTutorialPlayer(),CAMERA_FIELD_TARGET_DISTANCE,1500.00,0)
        call SetCameraFieldForPlayer(GetTutorialPlayer(),CAMERA_FIELD_ROTATION,90.00,0)
        call SetCameraFieldForPlayer(GetTutorialPlayer(),CAMERA_FIELD_ANGLE_OF_ATTACK,340.00,0)
        call PanCameraToTimedForPlayer(GetTutorialPlayer(),GetUnitX(gg_unit_Hpal_0003),GetUnitY(gg_unit_Hpal_0003),0)
endfunction

function Camera2 takes nothing returns nothing
        call SetCameraFieldForPlayer(GetTutorialPlayer(),CAMERA_FIELD_TARGET_DISTANCE,1500.00,0)
        call SetCameraFieldForPlayer(GetTutorialPlayer(),CAMERA_FIELD_ROTATION,340.00,0)
        call SetCameraFieldForPlayer(GetTutorialPlayer(),CAMERA_FIELD_ANGLE_OF_ATTACK,340.00,0)
        call PanCameraToTimedForPlayer(GetTutorialPlayer(),GetUnitX(gg_unit_Hmkg_0002),GetUnitY(gg_unit_Hmkg_0002),0)
endfunction

function ResetCamera takes nothing returns nothing
        call ResetToGameCameraForPlayer(GetTutorialPlayer(),0)
endfunction

function InitCinematic takes nothing returns Tutorial
    local Tutorial tutorial = Tutorial.create()
    local TutorialSequence seq

    //Scene 1
    set seq = tutorial.addSequence()
    call seq.setDuration(1.)
    set seq.transmissionUnitType = 0
    set seq.showCountdownInTransmissionTitle = false //setting will carry over to next sequences
    set seq.usePlayerSpecificTransmissionUnits = false //setting will carry over to next sequences

    //Scene 2
    set seq = tutorial.addSequence()
    call seq.setDuration(3.)
    set seq.transmissionTitle = "Barbarian"
    set seq.transmissionText = "Greetings captain."
    set seq.transmissionUnitType = GetUnitTypeId(gg_unit_Hmkg_0002)
    call seq.addAction(0., function Camera1)

    //Scene 3
    set seq = tutorial.addSequence()
    call seq.setDuration(3.)
    set seq.transmissionTitle = "Paladin"
    set seq.transmissionText = "Greetings."
    set seq.transmissionUnitType = GetUnitTypeId(gg_unit_Hpal_0003)
    call seq.addAction(0., function Camera2)

    //Scene 4
    set seq = tutorial.addSequence()
    call seq.setDuration(3.)
    set seq.transmissionTitle = "Paladin"
    set seq.transmissionText = "Any new information?"
    set seq.transmissionUnitType = GetUnitTypeId(gg_unit_Hpal_0003)
    call seq.addAction(0., function Camera2)

    //Scene 5
    set seq = tutorial.addSequence()
    call seq.setDuration(3.)
    set seq.transmissionTitle = "Barbarian"
    set seq.transmissionText = "No Sir."
    set seq.transmissionUnitType = GetUnitTypeId(gg_unit_Hmkg_0002)
    call seq.addAction(0., function Camera1)

    //Scene 6
    set seq = tutorial.addSequence()
    call seq.setDuration(3.)
    set seq.transmissionTitle = "Paladin"
    set seq.transmissionText = "Well, you can back out."
    set seq.transmissionUnitType = GetUnitTypeId(gg_unit_Hpal_0003)
    call seq.addAction(0., function Camera2)

    //Scene 7
    set seq = tutorial.addSequence()
    call seq.setDuration(0.5)
    //This is an empty scene, why do you have it?

    //Scene 8
    set seq = tutorial.addSequence()
    call seq.setDuration(3.)
    set seq.transmissionTitle = "Barbarian"
    set seq.transmissionText = "Very well captain."
    set seq.transmissionUnitType = GetUnitTypeId(gg_unit_Hmkg_0002)
    call seq.addAction(0., function Camera1)
    call seq.addAction(3., function ResetCamera)

    return tutorial
endfunction

function Trig_Start_Cinematic_Actions takes nothing returns nothing
    local Tutorial cinematic = InitCinematic()
    call StartTutorialForPlayer(cinematic, Player(0)) //you could also start it for other players
endfunction

//===========================================================================
function InitTrig_Start_Cinematic takes nothing returns nothing
    set gg_trg_Start_Cinematic = CreateTrigger(  )
    call TriggerRegisterEnterRectSimple( gg_trg_Start_Cinematic, gg_rct_Regi__n_001 )
    call TriggerAddCondition( gg_trg_Start_Cinematic, Condition( function Trig_Start_Cinematic_Conditions ) )
    call TriggerAddAction( gg_trg_Start_Cinematic, function Trig_Start_Cinematic_Actions )
endfunction

I attached your example map with my code.
 

Attachments

  • Transmission_edited.w3x
    49.5 KB · Views: 10
Level 24
Joined
Jun 26, 2020
Messages
1,853
I can notice it needs more lines than mine
//This is an empty scene, why do you have it?
Is a delay after he talks, because he said "What?" (In the sound) and it hears better, something I don't know is how the sounds were played? because in your code there is no where you set them and how could I skip it? because in your code I didn't find a trigger for that.
I would see the map in the editor but I can't open it because "it has invalid data".
 
Level 20
Joined
Jul 10, 2009
Messages
479
I can notice it needs more lines than mine
Dude, number of lines is not a quality mark and it has never been.
The system needs more lines that yours, because the settings are written one per line instead of being passed as many arguments to one single function. That way, the system can offer a much bigger variety of optional settings that you only need to declare, if you want to differ from the defaults, and can ignore otherwise.

I didn't yet include your sound files, because it was just supposed to be showcase for you, how to use the system, and I wanted to finish that in a reasonable time.

Not sure, what you mean by "invalid data". Are you using the latest patch?
 
Level 24
Joined
Jun 26, 2020
Messages
1,853
Are you upset?, because your reply sound like you are scolding me, if not, sorry.
Now answering you.
Dude, number of lines is not a quality mark and it has never been.
The system needs more lines that yours, because the settings are written one per line instead of being passed as many arguments to one single function. That way, the system can offer a much bigger variety of optional settings that you only need to declare, if you want to differ from the defaults, and can ignore otherwise.
This argument will be valid if your way was shorter than mine (for that of "don't need specify some values") because if we compare you have 5 necessary lines:
vJASS:
set seq = tutorial.addSequence()
call seq.setDuration(3.)
set seq.transmissionTitle = "Barbarian"
set seq.transmissionText = "Very well captain."
set seq.transmissionUnitType = GetUnitTypeId(gg_unit_Hmkg_0002) //Maybe not this
While with my method the shortest way without putting no needed values is:
vJASS:
call curr.AddLine(gg_unit_Hmkg_0002,null,"Barbarian",null,"Very well captain.",0,0.00,true)
Or:
vJASS:
call curr.AddLinebyId('Hmkg',null,"Barbarian",null,"Very well captain.",0,0.00,true)
Also I think you took what I said before:
something I don't know is how the sounds were played? because in your code there is no where you set them and how could I skip it? because in your code I didn't find a trigger for that.
As I was complaining, just in case, no, I wasn't complaining, I was confused because the transmission was made with your code, but the sounds actually were played and I could skip it, so I asked how did you do that?
Not sure, what you mean by "invalid data". Are you using the latest patch?
I have the last patch, but I tried to open it with the JNGP editor and I didn't tried it with the recent editor because I didn't think the problem was the version because it should throw me "Is coming from a recent version" (In case that is what you tried to say) but the message was "It has invalid data" like if someone "protected" the map.
 
Level 20
Joined
Jul 10, 2009
Messages
479
Are you upset?
I'd rather call it annoyed.
This argument will be valid if your way was shorter than mine
Sigh. Look, it's not always about short code, but about functionality. If you put 8 input parameters into a single AddLine function, your system will be much harder to change later on. You can't add or remove arguments from your function in an existing system, because it would break all code, where you (or other people) have already used it. That's why the best practice is to do it the way I've done it. If I want to add or remove functionality, I can just do it without breaking any existing code. Yes, people need to write more lines in order to use my system, but it's neither harder to read nor in any other way disadvantageous.
Plus, when reading the lines of code in my system, one immediately understands what's going on, because everything is explicitely named. In contrast, when looking at the "AddLine"-function of your system, one would have to backtrack to your documentation to understand, what the meaning of all the function arguments is.

Honestly let's maybe just stop this discussion. It's leading nowhere.
 
Level 24
Joined
Jun 26, 2020
Messages
1,853
I'd rather call it annoyed.

Sigh. Look, it's not always about short code, but about functionality. If you put 8 input parameters into a single AddLine function, your system will be much harder to change later on. You can't add or remove arguments from your function in an existing system, because it would break all code, where you (or other people) have already used it. That's why the best practice is to do it the way I've done it. If I want to add or remove functionality, I can just do it without breaking any existing code. Yes, people need to write more lines in order to use my system, but it's neither harder to read nor in any other way disadvantageous.
Plus, when reading the lines of code in my system, one immediately understands what's going on, because everything is explicitely named. In contrast, when looking at the "AddLine"-function of your system, one would have to backtrack to your documentation to understand, what the meaning of all the function arguments is.

Honestly let's maybe just stop this discussion. It's leading nowhere.
You know, you are right, if the people wanna edit the arguments, they need a way to do it, and that way should be easy to read and , I won't change the AddLine function, I just gonna add functions to do that, because yes, if someone wanna change a single value, they need a function to do that instead a function with 8 arguments.

But well, I don't know why you got annoyed, some people replies also get to bother me but I just say "please don't do what bothers me" or I try to hide it.
 
Last edited:
Status
Not open for further replies.
Top