• 🏆 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!
  • ✅ The POLL for Hive's Texturing Contest #33 is OPEN! Vote for the TOP 3 SKINS! 🔗Click here to cast your vote!

WoW-Like Quest Text

This bundle is marked as pending. It has not been reviewed by a staff member yet.
A simple, light-weight system to create a pretty text for an RPG that softly fades in as it is being written, similar to the quest texts in World of Warcraft when Instant Quest Text is disabled. Can also be used for dialog or exposition text, of course.

This system focuses solely on displaying the text and has no additional UI elements. I might create something on top of it in the future, but feel free to create your own system with it if you like. If you do, please give credit/put me as the co-author.

Will make JASS/GUI versions on request.

Lua:
if Debug then Debug.beginFile "QuestText" end
do
    --[[
    =============================================================================================================================================================
                                                                        Quest Text
                                                                        by Antares

                                    Writes a text into a text box, smoothly fading in each character as it is written.

                                Requires:
                                TotalInitialization                https://www.hiveworkshop.com/threads/total-initialization.317099/
                           
    =============================================================================================================================================================
                                                                          A P I
    =============================================================================================================================================================

    QuestTextNew(text)                          Starts writing the specified text into the quest text box.
    QuestTextSkipToEnd()                        Fully displays the current quest text.
    QuestTextReachedEnd()                       Returns whether the quest text has finished writing.
    QuestTextFullyVisible()                     Returns whether all characters have fully faded in.
    QuestTextFadeOut(duration)                  Fades out the current quest text over the specified duration.
    QuestTextRemove()                           Instantly removes the current quest text.

    Each function uses an optional player parameter. This will create a quest text that is only visible to that player/affect only that quest text. For a
    single-player map, you can omit that parameter. Each player can only see one quest text at a time.

    Additional optional parameters for QuestTextNew:
    parentFrame                                 Sets the parentFrame of the text. The default, if not specified, is ORIGIN_FRAME_WORLD_FRAME.
    onReachingEnd                               A function that is executed when the text reaches the end. The function is called with (whichPlayer, ...).
    ...                                         Any additional arguments will be passed into the onReachingEnd function.

    =============================================================================================================================================================
                                                                        C O N F I G
    =============================================================================================================================================================
    ]]

    local TEXT_WIDTH            = 0.25      ---@type number
    --The width of the text box.
    local X_LEFT                = 0.275     ---@type number
    --The x-position of the left edge of the text box.
    local Y_TOP                 = 0.5       ---@type number
    --The y-position of the top edge of the text box.
    local TEXT_LINE_SPACING     = 0.012     ---@type number
    --Distance between two lines.
    local FONT_SIZE             = 12        ---@type number
    --Font size.

    local CHARACTERS_PER_SECOND = 30        ---@type number
    --Writing speed of the quest text.
    local FADE_IN_TIME          = 0.6       ---@type number
    --The time it takes for a character to fully fade in after first appearing.

    local COPY_TO_MESSAGE_LOG   = true      ---@type boolean
    --(Only singleplayer) Copies messages to the message log by printing out the message, then clearing all text. Will interfere with default text messages.

    --===========================================================================================================================================================

    local ALPHA_INCREMENT =             ---@type number
        255/(CHARACTERS_PER_SECOND*FADE_IN_TIME)

    local questTextOfTimer = {}         ---@type QuestText
    local widthTestFrame = nil          ---@type framehandle
    local currentQuestText = {}         ---@type table
    local lastCreatedQuestText = nil    ---@type QuestText | nil

    ---@class QuestText
    local QuestText = {
        text = nil,             ---@type string
        words = nil,            ---@type string[]
        frames = nil,           ---@type framehandle[]
        alpha = nil,            ---@type number[]
        length = nil,           ---@type integer
        pos = nil,              ---@type integer
        writeTimer = nil,       ---@type timer
        fadeTimer = nil,        ---@type timer
        x = nil,                ---@type number
        y = nil,                ---@type number
        endReached = nil,       ---@type boolean
        fullyVisible = nil,     ---@type boolean
        fadeOutTime = nil,      ---@type number
        currentColor = nil,     ---@type string | nil
        player = nil,           ---@type player
        parentFrame = nil,      ---@type framehandle
        onReachingEnd = nil,    ---@type function | nil
        args = nil,             ---@type table | nil
    }

    local function FadeOut()
        local self = questTextOfTimer[GetExpiredTimer()]
        local fadeOut = false

        for i = 1, #self.frames do
            if self.alpha[i] > 0 then
                fadeOut = true
                self.alpha[i] = math.max(0, (self.alpha[i] - 255/(CHARACTERS_PER_SECOND*self.fadeOutTime)))
                BlzFrameSetAlpha(self.frames[i], self.alpha[i] // 1)
            end
            i = i + 1
        end

        if not fadeOut then
            QuestTextRemove()
        end
    end

    local function Fade(self)
        self = self or questTextOfTimer[GetExpiredTimer()]

        local i = #self.frames
        while i >= 1 and self.alpha[i] < 255 do
            self.alpha[i] = math.min(255, (self.alpha[i] + ALPHA_INCREMENT))
            BlzFrameSetAlpha(self.frames[i], self.alpha[i] // 1)
            i = i - 1
        end

        if i == #self.frames and self.endReached then
            self.fullyVisible = true
            PauseTimer(self.fadeTimer)
        end
    end

    local function NextChar(self)
        self = self or questTextOfTimer[GetExpiredTimer()]

        ::begin::

        self.pos = self.pos + 1
        if self.pos > self.length then
            self.endReached = true
            PauseTimer(self.writeTimer)
            if self.onReachingEnd then
                self.onReachingEnd(self.player, table.unpack(self.args))
            end
            return
        end

        local char = self.text:sub(self.pos, self.pos)

        if char == "\n" then
            self.x = X_LEFT
            self.y = self.y - TEXT_LINE_SPACING
            self.currentWord = self.currentWord + 1
            goto begin
        elseif char == " " then
            self.currentWord = self.currentWord + 1
            BlzFrameSetText(widthTestFrame, self.words[self.currentWord])
            BlzFrameSetSize(widthTestFrame, 0, TEXT_LINE_SPACING)
            if self.x + BlzFrameGetWidth(widthTestFrame) > X_LEFT + TEXT_WIDTH then --Next line
                self.x = X_LEFT
                self.y = self.y - TEXT_LINE_SPACING
                goto begin
            end
        elseif char == "|" then
            local nextChar = self.text:sub(self.pos + 1, self.pos + 1):lower()
            if nextChar == "c" then
                self.currentColor = self.text:sub(self.pos, self.pos + 9)
                self.pos = self.pos + 9
                goto begin
            elseif nextChar == "r" then
                self.currentColor = nil
                self.pos = self.pos + 1
                goto begin
            end
        end

        local newFrame = BlzCreateFrameByType("TEXT", "textFrame", self.parentFrame, "", 0) ---@type framehandle
        BlzFrameSetAbsPoint(newFrame, FRAMEPOINT_BOTTOMLEFT, self.x, self.y)
        if self.currentColor then
            BlzFrameSetText(newFrame, self.currentColor .. char .. "|r")
        else
            BlzFrameSetText(newFrame, char)
        end
        if self.player then
            BlzFrameSetVisible(newFrame, GetLocalPlayer() == self.player)
        end
        BlzFrameSetScale(newFrame, FONT_SIZE/10)
        BlzFrameSetSize(newFrame, 0, TEXT_LINE_SPACING)
        BlzFrameSetAlpha(newFrame, 0)

        self.x = self.x + BlzFrameGetWidth(newFrame)
        table.insert(self.frames, newFrame)
        table.insert(self.alpha, 0)
    end

    local function Init()
        widthTestFrame = BlzCreateFrameByType("TEXT", "textFrame", BlzGetOriginFrame(ORIGIN_FRAME_WORLD_FRAME, 0), "", 0)
        BlzFrameSetVisible(widthTestFrame, false)
    end

    OnInit.global("QuestText", Init)

    --===========================================================================================================================================================
    --API
    --===========================================================================================================================================================

    ---@param text string
    ---@param whichPlayer? player
    ---@param parentFrame? framehandle
    ---@param onReachingEnd? function
    ---@vararg any
    function QuestTextNew(text, whichPlayer, parentFrame, onReachingEnd, ...)
        local self = {}         ---@type QuestText
        self.text = text
        self.frames = {}
        self.alpha = {}
        self.words = {}
        self.length = text:len()
        self.writeTimer = CreateTimer()
        self.fadeTimer = CreateTimer()
        questTextOfTimer[self.writeTimer] = self
        questTextOfTimer[self.fadeTimer] = self
        self.x = X_LEFT
        self.y = Y_TOP
        self.pos = 0
        self.currentWord = 1
        self.player = whichPlayer
        self.parentFrame = parentFrame or BlzGetOriginFrame(ORIGIN_FRAME_WORLD_FRAME, 0)

        if onReachingEnd then
            self.onReachingEnd = onReachingEnd
            self.args = table.pack(...)
        end

        if bj_isSinglePlayer and COPY_TO_MESSAGE_LOG then
            print(text)
            ClearTextMessages()
        end

        if whichPlayer then
            if currentQuestText[whichPlayer] then
                QuestTextRemove(whichPlayer)
            end
            currentQuestText[whichPlayer] = self
        elseif lastCreatedQuestText then
            QuestTextRemove()
            for i = 0, 23 do
                if currentQuestText[Player(i)] then
                    QuestTextRemove(Player(i))
                end
            end
        end
        lastCreatedQuestText = self

        local char
        local beginningOfWord = 0
        for i = 1, self.length do
            char = text:sub(i, i)
            if char == " " or char == "\n" then
                self.words[#self.words + 1] = text:sub(beginningOfWord, i-1):gsub("\n", "")
                beginningOfWord = i
            elseif i == self.length then
                self.words[#self.words + 1] = text:sub(beginningOfWord, i):gsub("\n", "")
            end
        end

        TimerStart(self.writeTimer, 1/CHARACTERS_PER_SECOND, true, NextChar)
        TimerStart(self.fadeTimer, 0.02, true, Fade)
    end

    ---@param whichPlayer? player
    function QuestTextSkipToEnd(whichPlayer)
        local self = whichPlayer and currentQuestText[whichPlayer] or lastCreatedQuestText
        if self == nil then
            return
        end
        while not self.endReached do
            NextChar(self)
        end
        DestroyTimer(self.fadeTimer)
        questTextOfTimer[self.fadeTimer] = nil
        for i = 1, #self.frames do
            self.alpha[i] = 255
            BlzFrameSetAlpha(self.frames[i], 255)
        end
    end

    ---@param whichPlayer? player
    ---@return boolean
    function QuestTextReachedEnd(whichPlayer)
        local self = whichPlayer and currentQuestText[whichPlayer] or lastCreatedQuestText
        if self == nil then
            return false
        end
        return currentQuestText.endReached
    end

    ---@param whichPlayer? player
    ---@return boolean
    function QuestTextFullyVisible(whichPlayer)
        local self = whichPlayer and currentQuestText[whichPlayer] or lastCreatedQuestText
        if self == nil then
            return false
        end
        return currentQuestText.fullyVisible
    end

    ---@param duration number
    ---@param whichPlayer? player
    function QuestTextFadeOut(duration, whichPlayer)
        local self = whichPlayer and currentQuestText[whichPlayer] or lastCreatedQuestText
        if self == nil then
            return
        end
        if not self.fullyVisible then
            PauseTimer(self.writeTimer)
        end
        self.fadeOutTime = duration
        TimerStart(self.fadeTimer, 0.02, true, FadeOut)
    end

    ---@param whichPlayer? player
    function QuestTextRemove(whichPlayer)
        local self = whichPlayer and currentQuestText[whichPlayer] or lastCreatedQuestText
        if self == nil then
            return
        end
        questTextOfTimer[self.writeTimer] = nil
        questTextOfTimer[self.fadeTimer] = nil
        DestroyTimer(self.writeTimer)
        DestroyTimer(self.fadeTimer)

        for i = 1, #self.frames do
            BlzDestroyFrame(self.frames[i])
            self.frames[i] = nil
        end

        if whichPlayer then
            currentQuestText[whichPlayer] = nil
        else
            lastCreatedQuestText = nil
        end
    end
end
if Debug then Debug.endFile() end
Contents

WoW QuestText (Map)

WoW QuestText (Binary)

Top