Cutscene and Dialog System 0.0.7

LUA Declarative Cutscene & Dialog System
by Macielos

Let's say you wrote (or you plan to write) SHITLOADS of dialogs for your map or campaign in a form like below:

Arthas: I'm glad you could bake it, Uther.
Uther: Watch your tone with me, boy...

How much work do you need to actually turn it into a full-fledged in-game dialog or cutscene? Well, hell of a lot. That was my thought too and because I'm lazy, I decided to code something that will automagically manage cutscenes and dialogs for me, based on some configuration (a list of messages with some additional properties when necessary). In time, I added a lot more features to create a more interactive, RPG-style dialog structure in which you can make choices, react differently, have some extra topics you can ask NPCs about.

It's one of several larger pieces of code I'm working on for Exodus 2 campaign, the first bigger thing I coded in LUA (before I coded in Java, C#, Python and more) and so far the only one polished to a degree I'm not ashamed to show it to others. It still has some issues and you can surely expect me to work on it, but it's usable and already really time-saving, so I decided to share it to gather some feedback before you can see it in all its glory in Exodus 2.

You can find the first part of the Tutorial in a tab below. Keep in mind that it currently covers only some basic use cases, and will be expanded in the future, but it should give you a sense of how it works so you can further experiment on your own. Feel free to ask questions and provide feedback on which parts of the tutorial I should make clearer or more detailed, or which features you'd like to see explained first (cutscenes, dialogs with choices, etc.).

If you prefer to experiment with the library on your own, I suggest to familiarize with a demo map which tells a story of a fellow footman on an epic quest to find
beer. See the screenplays within the map triggers or on Github, then try to modify them and experiment. It's best to write screenplays in some IDE (I personally use IntelliJ Idea with LUA plugin) or text editor that highlights syntax, brackets, does formatting etc. You can find a list of available fields and their descriptions in a file ScreenplaySystem.lua,
near the beginning.

Features

Source Code

Getting Started

Tutorial [WiP]

Notes and extra tool [Advanced]

Known issues

Credits

Changelog


Declarative Dialog System
  • Send lists of unit transmissions one after another, with custom dialog window, auto-management of display times and nice text rendering animation
  • Automated scrolling up/down when your text is too long to fit
  • Skip single lines immediately with RIGHT arrow when you're done reading or just want to get through a dialog faster without skipping the entirety of it
  • Easily declare order of messages, branching, loops, resolving next message based on LUA functions or variables, allowing you to easily prepare highly interactive and non-linear dialog structures
  • Involve choices in your dialogs - display a list of options and make a scene halt until you pick a dialog option (UP/DOWN arrows) and confirm it (RIGHT arrow)
  • On each message, with delays if required, you can run your own trigger, custom function or even a list of actions
  • Every message is logged into a transmission log (under F12) so you can re-read them later

Automated Cutscenes
  • Playing a dialog with cutscene configuration requires FAR less code/trigger actions than preparing a cutscene trigger by hand
  • You can handle cutscene logic (e.g. camera movements, units moving or turning to one another) by attaching triggers or custom functions to specific messages (with delays if needed)
  • Unlike standard cutscenes, automated ones have user controls enabled so that the player can skip lines or make choices. A pleasant "side effect" is that you can pause a game, change options, see transmission log (with previous messages you missed), save and load a game during cutscenes. So far - surprisingly enough - I encountered no issues when loading a game in cutscene. You can easily disable this feature in configuration, but that way you won't have skipping lines or choices
  • Automated cutscene skipping - you can either skip single lines with RIGHT arrow, or an entire cutscene with ESC. You no longer need to have if-skipped-then-skip-actions-else-do-nothing after every action in a cutscene trigger. You only need to implement a dialog ending trigger and the mid-game logic is attached to specific dialog lines. For actions happening in the midst of a dialog line you can use utils.timedSkippable(delay, function() ... end). You can also make certain parts of a dialog unskippable (I plan to add some icon to show when a player can skip).
  • Fadeins/fadeouts handled by simply defining e.g. fadeOutDuration = 2.0 in a message and fadeInDuration = 2.0 in a following one

Dialog types and customization
  • Use a prepared screenplay in a cutscene, in an RPG-style dialog or a simple non-interrupting list of messages.
  • Use one of predefined screenplay variants, modify them or prepare your own. You can modify size and positions of UI elements, as well as dialog behaviour like whether or not to lock camera, pause units, show unit flashes or make cutscene interactive
  • Just fair warning - not every combination of configuration parameters has been tested, some options are not intended to use together, e.g. simple dialogs will act weird with choices because right arrow also moves the camera right. I suggest to start with predefined configs, but feel free to experiment at your own risk and report suggestions how to make configuration more intuitive and less prone to errors

The source code and screenplays from demo are available on Github:
GitHub - Macielos/Warcraft3LuaLibs

Key Concepts:
  • Instead of creating all triggers for dialogs and cutscenes manually, for each dialog/cutscene you prepare a LUA configuration file called Screenplay, e.g. “intro”, where you define a chain of messages to play and all the extra logic connected to each of them
  • Parameters that are repeatable for multiple dialogs are included in a ScreenplayVariant. Typical variants you will need are: a cutscene, an in-game dialog or an interactive dialog that pauses a game.
  • When you want to play your dialog/cutscene, you just need a single line of script ScreenplaySystem:startSceneByName(‘introCutscene’, ‘inGame’) that starts a screenplay called introCutscene using a variant inGame (which is a simple non-pausing dialog). The library will then render a cutscene/dialog, displaying all messages one by one, so you don’t have to worry about lots of stuff like message display times or skipping
  • If you need some custom logic on messages, on every message you can call triggers or LUA functions
Environment:
  • Download a demo map
  • Copy Import folder from the trigger editor
  • Open your map
  • Make sure you have LUA as your script language in map options
  • Paste Import folder to your trigger editor
  • Import the custom frames under their exact paths:
    • war3mapImported\CustomFrameFDF.fdf
    • war3mapImported\CustomFrameTOC.toc
From this point you are able to create and play your own dialogs and cutscenes.


Part 1: Simple dialog​

Let’s look at the Screenplay for the very first dialog from the demo map:

Lua:
ScreenplayFactory:saveBuilderForMessageChain("intro", function()
    return {
        [1] = {
            text = "Ahhh, the world's spinning! What did this fucking dwarf pour me!? Gotta find some beer.",
            actor = actorFootman,
        },
    }
end)

This script, called on map initialization, calls a function saveBuilderForMessageChain with two parameters: A name of your screenplay (“intro”) and a function with has to return a chain of messages for your screenplay. It will be validated and called when starting a screenplay by this name, so the variables you use in it don’t have to be created at game start. If your screenplay is incorrect (e.g. there is no actor – I’ll explain what an actor is below), it will return an error when you try to play it, not at game start.

Now let’s have a look at the message chain structure. In this case it’s pretty simple – it’s a LUA array containing one element – the simplest message with its text and actor. Text is just a string, so the only unknown here is the actor. It is simply a structure keeping data of whoever will be sending messages – an existing unit or a unit type. We can create it just like that:

Lua:
actorFootman = ScreenplayFactory.createActor(udg_footman, 'Footman Valdeck')

where udg_footman is just a unit variable. You can create actor variables in the screenplay builder function or you can keep them in global variables so they can be reused in multiple screenplays – it’s up to you.

Alternatively you can create an actor without an existing unit, in that case you need a unit type id (it is displayed in the object editor after you click on View -> Display Values as Raw Data), a player (for unit color) and a unit name to be displayed: here's another example from the demo map:
Lua:
local ghostUnitType = FourCC('ngh1')
local ghostPlayer = Player(2)
local actorGhost = ScreenplayFactory.createActorFromType(ghostUnitType, ghostPlayer, "Forest Ghost")

In the demo map all actors with units are created in a script screenplayActors which is just a simple function that creates global variables. For the sake of this tutorial we just need this:

Lua:
function createActors()
    actorFootman = ScreenplayFactory.createActor(udg_footman, 'Footman Valdeck')
end

Now, if you move just these two scripts (screenplay builder and createActors() function) to your map, and create a unit variable (udg_footman will refer to a variable named “footman” in a trigger editor), you should be able to run your first dialog. If you are unsure where to put the scripts, open the demo map, go to trigger editor and look at Triggers -> Demo.

The simplest trigger that calls a dialog on map initialization could look like this:
  • Init
    • Events
      • Map initialization
    • Conditions
    • Actions
      • Set VariableSet footman = Footman 0000 <gen>
      • Custom script: createActors()
      • Wait 0.10 seconds
      • Custom script: ScreenplaySystem:startSceneByName('intro', 'inGamePause')
A small delay is required when starting a scene on map initialization because the custom UI frame is loaded asynchronously.

Now we can expand our dialog. Let’s add more actors as in the demo map:

Lua:
function createActors()
    actorFootman = ScreenplayFactory.createActor(udg_footman, 'Footman Valdeck')
    actorDwarf = ScreenplayFactory.createActor(udg_dwarf, 'Rifleman Isgrinn')
    actorElf = ScreenplayFactory.createActor(udg_elf, "Elven Priest with Stick in his Holy Arse")
    actorGrunt = ScreenplayFactory.createActor(udg_grunt, 'Bored Grunt')
    actorPeon = ScreenplayFactory.createActor(udg_peon, 'Fearful Peon')
end

And more messages – we just add more items to our LUA array:

Lua:
ScreenplayFactory:saveBuilderForMessageChain("intro", function()
    return {
        [1] = {
            text = "Ahhh, the world's spinning! What did this fucking dwarf pour me!? Gotta find some beer.",
            actor = actorFootman,
        },
        [2] = {
            text = "Hey, lad. Good time drinkin' with ya! Until the next time!",
            actor = actorDwarf,
        },
        [3] = {
            text = "Yeeeeah, s-s-s-sure thing, pal!",
            actor = actorFootman,
        },
    }
end)

Last thing in this part – we usually want something to happen at the end of the dialog, e.g. display a quest update. We do so by adding a third parameter when starting our scene:

  • Custom script: ScreenplaySystem:startSceneByName('intro', 'inGamePause', gg_trg_Scene_Intro_End)
gg_trg_Scene_Intro_End is just a way WE scripts refer to existing triggers - gg_trg_ + trigger name with spaces replaced with “_”. So a trigger "Scene Intro End" will be called when our scene comes to an end.

Part 2: Screenplay Variants​

In the previous part of the tutorial we played a simple dialog in a small window which also paused the game while it was active. But in our maps and campaigns we may have various use cases – we might not want to pause the game, we might want to make a dialog skippable or not, finally we might want to create full-fledged cutscenes. Doing so via triggers we would have to copy lots of actions for each dialog or cutscene – pause the game, turning cinematic mode etc., sometimes we might extract this logic to a sub-trigger. But we’ll probably quickly figure out this logic is quite repetitive.

With that in mind Screenplay Variants were created – they are just pre-defined named configurations we can reuse throughout our map or campaign, each containing things like position of a dialog window, whether to pause a game, to turn cutscene mode, etc.

For start, you can just call predefined configurations by name when starting a scene:

ScreenplaySystem:startSceneByName('intro', 'inGame') – this will play a scene in a simple non-interactive dialog window without pausing a game. Typically used for simple one or two-line exchanges between characters mid-game, also in combat.

ScreenplaySystem:startSceneByName('intro', 'inGamePaused') – this will play a scene in the same dialog window, but in interactive mode – pausing the game and locking the camera on a currently speaking character. You can use this for longer dialogs for which you don’t necessarily need/want to make a cutscene. In this mode you can use a RIGHT arrow to skip a single line, ESC to rewind the whole dialog or include choices that will also be navigable using arrows (we’ll expand the choices part later).

ScreenplaySystem:startSceneByName('intro', ‘inCutsceneAutoplay’) – this will play a scene in a cutscene, auto-handling cinematic mode and several other things. Note that typically cutscene screenplays require some extra logic that handles e.g. camera movement, it is only put here to show how screenplay variants work. There will be a separate tutorial part about cutscenes.

In most cases for non-cutscene dialogs you will simply use either 'inGame' or 'inGamePaused'.

Now, if you want to dig into technical details, you can open a file ScreenplayVariants.lua – you will find exact parameter values for each screenplay variant, some more variants and a more detailed description of each variant. Of course, you can create your own variants by just adding entries to ScreenplayVariants object or you can modify existing ones to suit your needs.

Just keep in mind that not every combination of parameters has been tested and is guaranteed to work, sometimes it may lead to weird results or may simply make no sense, e.g. if you set skippable = true to ‘inGame’ variant, RIGHT arrow will both skip the dialog line and move the camera right. Same story if you add choices to a dialog run with ‘inGame’ variant.

Part 3: Branching and dynamic message order​

Now, let’s have a deeper dive into what can be done just with a chain of messages. Having our original simple linear dialog:

Lua:
ScreenplayFactory:saveBuilderForMessageChain("intro", function()
    return {
        [1] = {
            text = "Ahhh, the world's spinning! What did this fucking dwarf pour me!? Gotta find some beer.",
            actor = actorFootman,
        },
        [2] = {
            text = "Hey, lad. Good time drinkin' with ya! Until the next time!",
            actor = actorDwarf,
        },
        [3] = {
            text = "Yeeeeah, s-s-s-sure thing, pal!",
            actor = actorFootman,
        },
    }
end)

By default messages are displayed in an ordered manner, from the beginning to the end. But why be restricted to that? Let’s have our footman respond differently dependently on some conditions. Here after message 2 a dialog will go to 3 if a boolean variable udg_isInGoodMood (in trigger editor seen as isInGoodMood) equals true or to 4 otherwise:

Lua:
ScreenplayFactory:saveBuilderForMessageChain("intro", function()
    return {
        [1] = {
            text = "Ahhh, the world's spinning! What did this fucking dwarf pour me!? Gotta find some beer.",
            actor = actorFootman,
        },
        [2] = {
            text = "Hey, lad. Good time drinkin' with ya! Until the next time!",
            actor = actorDwarf,
            thenGoToFunc = function()
                if udg_isInGoodMood == true
                then
                    return 3
                else
                    return 4
                end
            end
        },
        [3] = {
            text = "Yeeeeah, s-s-s-sure thing, pal!",
            actor = actorFootman,
            thenEndCutscene = true,
        },
        [4] = {
            text = "No way, I’m not gonna drink with you anymore, inhuman beasts!",
            actor = actorFootman,
        },
    }
end)

So it’s a simple branch. Just one more detail – if we picked 3, and then we don’t want to go to 4 (as it's a different branch), so we added thenEndScene = true.

If we want the two branches to join instead and the scene to continue linearly, we could use thenGoTo = 5 instead. Or we can once again use thenGoToFunc and determine the next message dynamically - it's all up to us.

Lua:
ScreenplayFactory:saveBuilderForMessageChain("intro", function()
    return {
        [1] = {
            text = "Ahhh, the world's spinning! What did this fucking dwarf pour me!? Gotta find some beer.",
            actor = actorFootman,
        },
        [2] = {
            text = "Hey, lad. Good time drinkin' with ya! Until the next time!",
            actor = actorDwarf,
            thenGoToFunc = function()
                if udg_isInGoodMood == true
                then
                    return 3
                else
                    return 4
                end
            end
        },
        [3] = {
            text = "Yeeeeah, s-s-s-sure thing, pal!",
            actor = actorFootman,
            thenGoTo = 5,
        },
        [4] = {
            text = "No way, I’m not gonna drink with you anymore, inhuman beasts!",
            actor = actorFootman,
        },
        [5] = {
            text = "Anyway, watch out for monsters out there!",
            actor = actorDwarf,
        },
    }
end)

Part 4: Choices and loops​

Ever wanted to make an RPG-style choice window like in the Witcher or Dragon Age? Well, the Screenplay library can lend you a helpful hand in that. Let’s look at a simplified version of a dialog from a demo map:

Lua:
ScreenplayFactory:saveBuilderForMessageChain('orc', function()
    return {
        [1] = {
            text = "Hey, orc!",
            actor = actorFootman,
        },
        [2] = {
            text = "Whazzup, hummie?",
            actor = actorGrunt,
        },
        [3] = {
            actor = actorFootman,
            choices = {
                [1] = {
                    text = "Show me your wares.",
                    onChoice = function()
                        ScreenplaySystem:currentItem().choices[1].visible = false
                        ScreenplaySystem:goTo(4)
                    end
                },
                [2] = {
                    text = "I have to go.",
                    onChoice = function()
                        ScreenplaySystem:goTo(6)
                    end
                }
            }
        },
        [4] = {
            text = "Show me your wares!",
            actor = actorFootman,
        },
        [5] = {
            text = "Do I look like a merchant, hummie?!",
            actor = actorGrunt,
            thenGoTo = 3
        },
        [6] = {
            text = "I have to go.",
            actor = actorFootman,
        },
        [7] = {
            text = "Yeah, yeah, whatever.",
            actor = actorGrunt,
        },
    }
end)

So, the first two messages are linear, but then in message 3 instead of a text we have choices section, which contains a list of possible options from which we can choose. When a scene reaches such a message, it stops and allows the player to pick an option – he can use UP/DOWN arrows to scroll between them and RIGHT arrow to confirm it. Upon confirmation we can (or rather have to) call a custom function which set the next message (ScreenplaySystem:goTo(6)) and can also have other custom logic, calling any native functions or even triggers.

In an example above we also have a line ScreenplaySystem:currentItem().choices[1].visible = false which refers to the list of choices in a current dialog item (so the choices list) and marks the first item as not visible, so we can’t pick it again. We can also set visible = false in the choice list element in the screenplay and after picking some other option set visible = true in the same manner. The possibilities are almost endless.

In an example above, after picking choice 1 the dialog will go to 4, then 5, and because we added thenGoTo = 3, it will return to the choice list to we can pick another. After picking choice 2 the dialog will go to 6, then 7 and then will end.

See a script "screenplayOrc" in a demo map or screenplay03Orc.lua file on a Github repo for a more complex example of a dialog with choices.

Part 5: Attaching custom logic to messages​

Not everything can (and should) be done by configuration, and we usually want some custom actions to happen during our dialogs and cutscenes, e.g. a unit turning to another unit, a new character showing up etc. Here’s how you can do this:

Lua:
ScreenplayFactory:saveBuilderForMessageChain('orc', function()
    return {
        [1] = {
            text = "Hey, orc!",
            actor = actorFootman,
            func = function()
                print('something will happen here')
            end
        },
    }
end)

If you prefer to keep your custom logic in triggers, you can alternatively call a trigger on a message:

Lua:
ScreenplayFactory:saveBuilderForMessageChain('orc', function()
    return {
        [1] = {
            text = "Hey, orc!",
            actor = actorFootman,
            trigger = gg_trg_My_Trigger_Name
        },
    }
end)

Or you can even add a whole list of funcs or triggers to be called (although to be honest I rarely use this functionality - single functions and triggers usually cover all typical use cases):

Lua:
ScreenplayFactory:saveBuilderForMessageChain('orc', function()
    return {
        [1] = {
            text = "Hey, orc!",
            actor = actorFootman,
            actions = {
                [1] = {
                    trigger = gg_trg_My_Trigger_Name
                },
                [2] = {
                    func = function()
                        print('something will happen here')
                    end
                },
            }

        },
    }
end)

If you want stuff to happen DURING a dialog line (e.g. X seconds after a message starts playing) – it’s also possible, but it’s more complicated due to the automated skip mechanism - e.g. we can call a trigger 4 seconds after a message starts, but after 2 seconds the player will skip the line. It will be covered separately, probably in a cutscene tutorial. However, if you only use 'inGame' cutscenes (without automated skipping), you can simply start timers or call triggers with waits.

TO BE CONTINUED...

Screenplay Generator
You can speed up working on screenplays by using a small tool I wrote (requires Java 18+, so far you need to build the
project yourself using Maven, I can provide a jar if there's any interest in the tool):
GitHub - Macielos/ScreenplayGenerator

Notes:
  • This library overrides InitBlizzard() function to call custom initialization code
  • This library was never meant to be used in multiplayer. Scenes are displayed for local player only and there is no data syncing mechanism. My guess would be that non-pausing config variants will work, but for pausing ones you'd have to handle pausing game for other players and events like: one player has a choice window open and the other one attacks him.

Compatibility:
- I'm currently using the library on Reforged only. It may work on 1.31 as it only uses very basic native functions. Just make sure your map uses LUA. I remember that I ran some early versions on 1.31.


  • You can unlock a camera in cutscene by pressing e.g. F10. To fix it I periodically 'adjust' the camera.
  • Because of above, instead of standard camera functions/trigger actions you should use ScreenplayUtils.interpolateCamera(cameraFrom, cameraTo, duration) or ScreenplayUtils.interpolateCameraFromCurrent(cameraTo, duration). See elf screenplay in demo for examples.
  • For some rare camera angles in cutscenes camera does not get locked at all, so you can freely move it. EDIT: I think it happens when you interpolate between cameras with the same target position (so they differ with only e.g. rotation)
  • During in-game dialogs you can click on a window which disrupts a game a little. I wanted to make it not register clicks, but so far I didn't find an option for that (maybe someone here will give me a hint). In the worst case I'll just add a config to display in-game dialogs like in vanilla, without a dialog window.

@Planetary - my system began as a modification of this system:
In time I expanded and reworked it to a degree there's hardly any original code left, but I still use its UI files and some basic code structure. Planetary gave me permission to use fragments of his system. He said he doesn't need any credits, but his system is cool and gave me lots of inspiration, so I'm giving him credits anyway :p.

0.0.1
- initial version

0.0.2
[not published in demo map, only available on github]

0.0.3
note:
changed some naming for clarity - skipping now means current mechanism of skipping single lines, rewinding means interrupting the whole scene
- New options in screenplay variants:
skippable - true/false - allows to skip single lines by RIGHT arrow
1. rewindable - true/false - allows to rewind to the next interactive element OR exits the dialog if there is none
2. disableSelection -true/false - disable selecting and highlightin units, showing healthbars etc. during a dialog or cutscene
3. interruptExisting - true/false - if true, starting a scene of this variant when another one is active will interrupt the existing one, if false, new scene won't be started. This was previously only available in a parametr when starting a scene. I typically have this to true for cutscenes and major dialogs, to false for minor interactions, plus I can still pass a parameter to starting a scene to override this one
4. removed lockControls flag as it was already covered by other flags
- New options for dialog items:
1. onRewindGoTo - used to rewind a dialog with choices, contains an index of next line like in goTo, thenGoTo etc. If there is a cutscene that is generally linear, but allows the player to ask some extra questions or get straight to the mission, we might want pressinc ESC not to take us to these choices, but simply interrupt the whole scene like in WC3 cutscenes. You can now do this but setting this parameter in choices.
2. stopOnRewind - true/false. You might want an opposite behaviour for any line, thus this new field. When set to true, rewinding will stop at this particular item so you need to click esc again to rewind further
3. skipTimers - true/false, now timers added via timedSkippable() function will be interrupted only for lines with skipTimers = true. In practice - set this to true for a first line in a new shot or scene
  • Did some cleanups and refactoring after old libs I incorporated in the past - split old utils class onto several smaller utility classes, got rid of majority of global functions and moved 2 remaining ones to GlobalFunctions.lua, enclosed local functions in classes because apparently they are not actually local anyway
  • Added Elkonium's DebugUtils and GameConsole to demo map. One I get familiarize with them more, I'll probably remove my own debugFuncs

0.0.4
  • Added possibility to create actors without a physical unit by: ScreenplayFactory.createActorFromType(unitType, player, customName)
  • Added important API functions summary and some more docs

0.0.5
  • Got rid of two remaining global functions
  • Changed funcs in ScreenplaySystem into local where possible
  • Added scopes for main classes so that local functions are actually local
  • Removed last usage of legacy interface, removed legacy interfaces from demo map (I still need them for now, but you don't ;))

0.0.6
  • Added a simpler function to create screenplays from an array of messages
  • Small change in SimpleUtils I forgot to push earlier
  • Small fix in FrameUtils, not used in this project
0.0.7
  • A dialog window is now unclickable, so it doesn't disrupt clicking on units underneath
  • Cleaned up unused custom font files
Previews
Contents

Cutscene And Dialogue System 0.0.7 (Map)

ExodusDialogSystem - custom frames to import (Binary)

Reviews
Antares
While some of the issues have not be addressed fully, it is a system of a high enough quality to be approved anyway. Approved
Level 22
Joined
Oct 16, 2021
Messages
269
This is when the history of the bravest Legionnaire of the Enclave begins...

The thing works as fully-fledged replacement for obsolete dialogue system of WC3, is cool, is full of QoL, is idiot-proof and idiot-friendly. Tested on myself.
 

Macielos

Hosted Project: W3E
Level 23
Joined
Jul 9, 2010
Messages
396
This looks great. Do you think there's a way to fade in letters as they appear? Like in WoW quest texts?
A whole text, yes, single letters - problematic (using transparency in text tags doesn't work as far as I know). So far I'm focused on functional improvements, making things pretty is not my specialty :p.
 
Hey,

regarding the fading in of characters, I wrote this snippet as a proof of concept. Maybe you'd like to make something out of it. I really think it would make your system look great.

Lua:
if Debug then Debug.beginFile "WowQuestText" end
do
    local TEXT_WIDTH = 0.2
    local CHARACTERS_PER_SECOND = 25
    local FADE_IN_TIME = 0.6
    local TEXT_LINE_SPACING = 0.01
    local ALPHA_INCREMENT = 5

    local X_LEFT = 0.3
    local Y_TOP = 0.5

    local widthTestFrame = nil              ---@type framehandle

    function WriteQuestText(text)
        local charNum = 0
        local thisWord = text:sub(1, text:find(" ") - 1)
        local length = text:len()
        local remainingText
        local char
        local x, y = X_LEFT, Y_TOP

        TimerStart(CreateTimer(), 1/CHARACTERS_PER_SECOND, true, function()
            ::begin::
            charNum = charNum + 1

            if charNum > length then
                DestroyTimer(GetExpiredTimer())
                return
            end
            char = text:sub(charNum, charNum)

            if char == "\n" then
                x = X_LEFT
                y = y - TEXT_LINE_SPACING
                goto begin
            elseif char == " " then --Check if next word fits into current line
                remainingText = text:sub(charNum + 1, length)
                thisWord = remainingText:sub(1, (remainingText:find(" ") or remainingText:len() + 1) - 1) --Find next space or is last word.
                BlzFrameSetText(widthTestFrame, " " .. thisWord:gsub("\n", "")) --Width must include previous space.
                BlzFrameSetSize(widthTestFrame, 0, TEXT_LINE_SPACING)
                if x + BlzFrameGetWidth(widthTestFrame) > X_LEFT + TEXT_WIDTH then --Next line
                    x = X_LEFT
                    y = y - TEXT_LINE_SPACING
                    goto begin
                end
            end

            local newFrame = BlzCreateFrameByType("TEXT", "textFrame", BlzGetOriginFrame(ORIGIN_FRAME_WORLD_FRAME, 0), "", 0)
            BlzFrameSetAbsPoint(newFrame, FRAMEPOINT_BOTTOMLEFT, x, y)
            BlzFrameSetText(newFrame, char)
            BlzFrameSetSize(newFrame, 0, TEXT_LINE_SPACING)
            local alpha = 0
            BlzFrameSetAlpha(newFrame, 0)
            TimerStart(CreateTimer(), ALPHA_INCREMENT*FADE_IN_TIME/255, true, function()
                alpha = alpha + ALPHA_INCREMENT
                BlzFrameSetAlpha(newFrame, alpha)
                if alpha >= 255 then
                    DestroyTimer(GetExpiredTimer())
                end
            end)
            x = x + BlzFrameGetWidth(newFrame)
        end)
    end

    OnInit.final(function()
        widthTestFrame = BlzCreateFrameByType("TEXT", "textFrame", BlzGetOriginFrame(ORIGIN_FRAME_WORLD_FRAME, 0), "", 0)
        WriteQuestText([[There was a time when I was young and full of vigor like you, druid.

But now I'm old and unable to explore the world like I once did. Ah, the things I saw!

But there was one mystery that eluded me. For my final quest, the Explorers' League sent me in search of The Temple of Atal'Hakkar. It was rumored to be located in the Swamp of Sorrows.

Because of my frail state, I traveled the skies by gryphon in search of it but never found it.

Help an old dwarf out? Perhaps you will have better luck on foot.]])
    end)
end

Moving on to the review:

The documentation of your API could be better. You're mixing configurables with non-configurables and constants with no clear delimitation. Why is "onSceneEndTrigger" in the config, for example? As far as I can tell, that just seems to be an upvalue declaration for later.

You should also try to abide by the formatting conventions laid out here (globals UpperCase, locals lowerCase, constants CAPS_LOCK).

A global simply named "utils" could lead to name clashes. Consider renaming it or try to find a way to not make it a global. One way to do it is use Lua's _ENV feature. You also randomly have functions not in the utils table in that script file. It's a bit of a mess.

Because you have no enclosing scope around your script files, the local printDebug functions are visible everywhere.

On re-entering the rifleman beacon, the background frame of the dialog is transparent. That seems not intended.

My first instinct when the first multiple choice dialog popped up was to press the number keys to select the option. I think that would be a great addition. Also consider making the hotkeys customizable.

All in all, this is a fantastic system and easily a high quality rating once all the rough edges have been smoothed out. Looking forward to using this myself :psmile:.

Awaiting Update
 

Macielos

Hosted Project: W3E
Level 23
Joined
Jul 9, 2010
Messages
396
Thanks for feedback, I'll try to apply some fixes and refactors once I have a free moment.

The whole configuration is in ScreenplayVariants.lua, onSceneEndTrigger is just an internal variable. Perhaps you were misled by an old comment ("start of config settings") I forgot to remove once I extracted configuration from this class :p. I'll remove it.

Yeah, I admit I was a bit inconsistent with naming. Some were leftovers from the original library I was reworking, some (e.g. utils class) were left due to compatibility reasons, i.e. I have references to them in existing maps. Maybe I'll make proxy classes from them like I did with ScreenplayLegacyInterface.

As for keys, I used arrows mostly because simplicity and out of the box support in triggers without any additional library.
 
A lot of improvements, nice. The invisible frame bug doesn't appear anymore.

I see a small issue with the text as it is being written that the word first appears in the old line, then jumps to a new one once it no longer fits. I fixed that in the text system I mentioned last time by checking if the next word would fit in the current line first before starting to render it. If you wanna copy that solution, check it out here.

You have superfluous wrappers around your functions because some parts use the old function names? Can't you just go through your code with find and replace? :prazz:

I don't see a mention in your description about whether your system is working in multiplayer. I assume it is not?

You're overwriting InitBlizzard for your initialization. If you're overwriting a native, you have to declare that in the documentation. While it's not required, I encourage you to use TotalInitialization as your initialization method. This ensures that your library works well with other libraries (if someone else is initializing their libraries the same way, only one will work (edit: nvm, I'm mistaken)).

While the documentation of the ScreenplayVariants is good, the ScreenplaySystem library misses a listing of the API, preferably at the top of the script. Also, are any of these function supposed to be private?

Because of the last two issues, gonna have to put it back to awaiting update.
 
Last edited:

Macielos

Hosted Project: W3E
Level 23
Joined
Jul 9, 2010
Messages
396
Sorry, I haven't had much time to work on WC3 scripts recently. I added API list (not complete one, but with functions people will most likely use) and some more docs, including info about InitBlizzard and multi.

I see a small issue with the text as it is being written that the word first appears in the old line, then jumps to a new one once it no longer fits. I fixed that in the text system I mentioned last time by checking if the next word would fit in the current line first before starting to render it. If you wanna copy that solution, check it out here.
Thanks, I might use it once I get to beautification stage ;).

You have superfluous wrappers around your functions because some parts use the old function names? Can't you just go through your code with find and replace? :prazz:
I have replaced them in code long ago, but they are still used in multiple maps too. The libraries are in a single folder that I always copy between maps as a whole, but screenplays are kept near specific triggers, so I'd have to manually copy them file by file (unless there's some automagical hot-reload tool I am not aware of). So I did this workaround so that they work for now, and I will copy them lazily only when I make some changes in them. Believe me of not, this is a big time optimization ;).

This ensures that your library works well with other libraries (if someone else is initializing their libraries the same way, only one will work (edit: nvm, I'm mistaken)).
I think they will work no matter the order, each one will override the previous one.

While the documentation of the ScreenplayVariants is good, the ScreenplaySystem library misses a listing of the API, preferably at the top of the script. Also, are any of these function supposed to be private?
Well, some of these functions could indeed be private and they would be in a "normal" language. However, in my maps (where i have this lib + many others) I run into a limitation of max. 200 local functions, so I have to limit their usage. Besides, as far as I noticed, these local functions are not really local since they're compiled into one big script and their accessibility just depends on script order. Plus - that's my personal bias of course - I don't like the fact i need to declare a function higher than its invocation - it reduces code readibility.
 
I think they will work no matter the order, each one will override the previous one.
You're right. I figured that out after I posted my comment.

Well, some of these functions could indeed be private and they would be in a "normal" language. However, in my maps (where i have this lib + many others) I run into a limitation of max. 200 local functions, so I have to limit their usage. Besides, as far as I noticed, these local functions are not really local since they're compiled into one big script and their accessibility just depends on script order. Plus - that's my personal bias of course - I don't like the fact i need to declare a function higher than its invocation - it reduces code readibility.
I was talking about the fact that you have no API listing and you might have functions in your Screenplay table that are to be considered private, adding to the confusion.

I've run into the 200 locals limit as well while working on ALICE, but that is a much larger script than what you have here, so that's surprising to me. I also don't understand what you're saying about local functions are not local. Are you putting a scope around them with do end?

If you run into the problem of no-forward declaration, you can define the function variable as nil at the top of the scope, then overwrite it further down. Example:

Lua:
do
    local testFunc = nil
    
    local function CallTestFunc()
        testFunc()
    end

    testFunc = function() print("test succesful") end

    CallTestFunc()
end
 

Macielos

Hosted Project: W3E
Level 23
Joined
Jul 9, 2010
Messages
396
I was talking about the fact that you have no API listing and you might have functions in your Screenplay table that are to be considered private, adding to the confusion.
Okay, sorry for a long delay, I uploaded some refactors. I added scopes and made funcs local where possible, now that they are truly local. I also got rid of the remaining global funcs completely.

I mean, I know how to do a forward declaration, I just don't like it as it reduces code readability. In a OOP language you usually have public methods on top, which then call some private methods, which are below, and you read it like a book. Here I have to either jump up and down, OR have this forward declarations. It's not a blocker, of course, it's just annoying. But I guess I need to get rid of some biases from Java-like languages when coding in LUA.

I think in my next libs I'll limit usage of LUA classes and make them stateless, and keep all internal variables as local scoped. Right now fields of e.g. ScreenplaySystem are in fact public, but they needn't be.

I've run into the 200 locals limit as well while working on ALICE, but that is a much larger script than what you have here, so that's surprising to me.
I ran into this limit on another map, where I have this library plus several others, including e.g. lots of custom skills. I need to check out if putting local funcs within scopes make them not count to this limit.
 
Good progress on the code refactoring. It looks a lot cleaner! However
Lua:
onInit(function()
    ScreenplaySystem:init()
end)
A three line library? Can't this be part of the ScreenplaySystem script?

Lua:
ScreenplaySystem = {   -- main dialogue class, configuration options can be found in ScreenplayVariants.lua
    FDF_BACKDROP = "EscMenuBackdrop",
    FDF_TITLE = "CustomText", -- from imported .fdf
    FDF_TEXT_AREA = "CustomTextArea", -- ``
    TITLE_COLOR_HEX = "|cffffce22", -- character title text color.
    TEXT_COLOR_HEX = "|cffffffff", -- character speech text color.
    INACTIVE_CHOICE_COLOR_HEX = "|cff808080", -- greyish text color for choices other than the selected one

    debug = false, -- print debug messages for certain functions.

    --leftovers from original lib, could be moved to ScreenplayVariants, but so far I saw no need to customize them
    fade = true, -- should dialogue components have fading eye candy effects?
    fadeDuration = 0.81, -- how fast to fade if fade is enabled.

    --internal state of the config and the screenplay
    frameInitialized = false,
    currentVariantConfig = nil,
    onSceneEndTrigger = nil,
    messageUncoverTimer,
    trackingCameraTimer,
    autoplayTimer,
    cameraInterpolationTimer,
    delayTimer,
    fadeoutTimer,
    lastActorUnitTypeSpeaking,
}
You're not defining the fields messageUncoverTimer, trackingCameraTimer etc. here, but assigning undeclared globals to the sequence part of the table. This makes my VS Code go crazy with warnings. You need to add the = nil to each of them.

Lua:
function SimpleUtils.debugFunc(func, name)
    local name = name or ""
    local result
    local passed, data = pcall(function()
        result = func()
        return "func " .. name .. " passed"
    end)
    if not passed then
        print(name, passed, data)
    end
    passed = nil
    data = nil
    return result
end
You're generating a lot of anonymous functions throughout your code. You could try to reduce that on those that are called more often, like the debugFunc here.

I have the feeling that the ScreenplayFactory:saveBuilder function could be solved in a much simpler way with an optional name parameter in the builder functions.

The inconsistent use of dot and colon syntax is also something you should address. This can lead to a lot of headache for a user when using the wrong notation. For functions such as function ScreenplaySystem:init(), you could simply remove the colon and set local self = ScreenplaySystem in the first line of the function. Adding type notations with EmmyLUA would also help immensely.

Apart from these issues, I'm still missing an overarching documentation. You have a lot of different sub-libraries and it is not obvious what the exact responsibilities of each of them are to the uninitiated user. You have different snippets of information scattered throughout the hiveworkshop page, the test map triggers, and your code, but nothing that I can just read and follow to get started. I want to have a tutorial for "I have imported the libraries into my map. Now what?" It should guide me through step by step how I can create my first screenplay, list the API, and explain what the responsibilities of each library are.

A good documentation is really crucial for a resource this complex!
 

Macielos

Hosted Project: W3E
Level 23
Joined
Jul 9, 2010
Messages
396
Okay, I'm back after a while and I plan to do some work on the lib soon.

You're generating a lot of anonymous functions throughout your code. You could try to reduce that on those that are called more often, like the debugFunc here.
Yeah, they're everywhere since I didn't know DebugUtils back then, so without them I didn't get any error messages. They will soon be removed entirely.

I have the feeling that the ScreenplayFactory:saveBuilder function could be solved in a much simpler way with an optional name parameter in the builder functions.
Not sure if I understand you correctly, but no, the name can't be in the builder because the name is stored at game start while the builder function is called (by name) when playing a particular scene.

The inconsistent use of dot and colon syntax is also something you should address. This can lead to a lot of headache for a user when using the wrong notation. For functions such as function ScreenplaySystem:init(), you could simply remove the colon and set local self = ScreenplaySystem in the first line of the function. Adding type notations with EmmyLUA would also help immensely.
As a rule of thumb I tried to consistently use colon in all usages of ScreenplaySystem and related classes that have state. But yeah, I don't like this class notation either.

Okay, I'll add type notations, at least for functions meant to be called by end user.

Apart from these issues, I'm still missing an overarching documentation. You have a lot of different sub-libraries and it is not obvious what the exact responsibilities of each of them are to the uninitiated user. You have different snippets of information scattered throughout the hiveworkshop page, the test map triggers, and your code, but nothing that I can just read and follow to get started. I want to have a tutorial for "I have imported the libraries into my map. Now what?" It should guide me through step by step how I can create my first screenplay, list the API, and explain what the responsibilities of each library are.

A good documentation is really crucial for a resource this complex!
For now you can analyze examples from the demo map. I tried to prepare them rather simply and self-describing, but surely a full-fledged tutorial would be better. I plan to write it one day, for now I was waiting to see if there's demand for it ;).
 
Last edited:
Nice that you're still working on it!

I'm more nitpicky with your resource only because I think it is really good and I would like to see it reach its full potential.

I think more API documentation and a "you've installed this, now what?" (other than "go look at the examples") is sorely needed; a full-fledged tutorial would be nice, but only if you have the time and energy for it. The rest are just suggestions and are not preventing your system from being approved.

I have to look into it again with the ScreenplayFactory thing. Don't remember what that was about.
 
Top