You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

310 lines
8.6 KiB

local _, L = ...
local Timer, GetTime = CreateFrame('Frame'), GetTime
-- Borrowed fixes from Storyline :)
local LINE_FEED_REPLACE, LINE_BREAK_REPLACE
do local LINE_FEED, CARRIAGE_RETURN = string.char(10), string.char(13)
LINE_FEED_REPLACE = LINE_FEED .. '+'
LINE_BREAK_REPLACE = LINE_FEED .. CARRIAGE_RETURN .. LINE_FEED
end
local TEXT_TIME_DIVISOR -- set later as baseline divisor for (text length / time).
local TEXT_TIME_PADDING = 2 -- static padding, feels more natural with a pause to breathe.
local MAX_UNTIL_SPLIT = 200 -- start recursive string splitting if the text is too long.
Timer.Texts = {}
L.TextMixin = {}
local Text = L.TextMixin
----------------------------------
-- Text: manage text input
----------------------------------
function Text:SetText(text)
TEXT_TIME_DIVISOR = L('delaydivisor')
self:PreparePlayback()
self.storedText = text
if text then
local timeToFinish, strings, timers = self:CreateLineData(text)
self.numTexts = #strings
self.timeToFinish = timeToFinish
self.timeStarted = GetTime()
self:QueueTexts(strings, timers)
end
end
function Text:ReplaceLinefeed(text)
return text:gsub(LINE_FEED_REPLACE, '\n'):gsub(LINE_BREAK_REPLACE, '\n')
end
function Text:ReplaceNatural(str)
local new = str -- substitute natural breaks with newline.
:gsub('%.%s%.%s%.', '...') -- ponder special case
:gsub('%.%s+', '.\n') -- sentence
:gsub('%.%.%.\n', '...\n...') -- ponder
:gsub('%!%s+', '!\n') -- exclamation
:gsub('%?%s+', '?\n') -- question
return new, (new == str) -- return new string, and whether something changed
end
function Text:CreateLineData(text)
text = self:ReplaceLinefeed(text)
local timeToFinish, strings, timers = 0, {}, {}
for _, paragraph in ipairs({strsplit('\n', text)}) do
timeToFinish = timeToFinish + self:AddString(paragraph, strings, timers)
end
return timeToFinish, strings, timers
end
function Text:CalculateLineTime(length)
return (length / (TEXT_TIME_DIVISOR or 15) ) + TEXT_TIME_PADDING
end
function Text:AddString(str, strings, timers)
local length, timer, new, forceShow = str:len(), 0
if length > MAX_UNTIL_SPLIT then
new, forceShow = self:ReplaceNatural(str)
--[[ If the string is unchanged, this will recurse infinitely, therefore
force the long string to be shown. This safeguard is probably meaningless,
as it requires 200+ chars without any punctuation. ]]
if not forceShow then -- recursively split the altered string
for _, sentence in ipairs({strsplit('\n', new)}) do
timer = timer + self:AddString(sentence, strings, timers)
end
return timer
end
end
if ( length ~= 0 or forceShow ) then
timer = self:CalculateLineTime(length)
timers[ #strings + 1] = timer
strings[ #strings + 1 ] = str
end
return timer
end
----------------------------------
-- Text: playback
----------------------------------
function Text:QueueTexts(strings, timers)
assert(strings, 'No strings added to object '.. ( self:GetName() or '<unnamed fontString>' ) )
assert(timers, 'No timers added to object '.. ( self:GetName() or '<unnamed fontString>' ) )
self.strings = strings
self.timers = timers
Timer:AddText(self)
end
function Text:ForceNext()
if self:HasLineData() then
local _, remainingLineTime = self:RemoveLine()
self.timeToFinish = self.timeToFinish - remainingLineTime
if self:HasLine() then
self:SetToCurrentLine()
else
self:PauseTimer()
self:RepeatTexts()
self:FlagForceFinished(true)
end
if not self:HasFollowup() then
self:OnFinished()
self:FlagForceFinished(true)
end
end
end
function Text:SetToCurrentLine()
self:DisplayLine(self:GetLine())
end
function Text:SetCurrentLineTime(time)
self.currentLineTime = time or 0
end
function Text:UpdateCurrentLineTime(delta)
self.timers[1] = self.timers[1] + delta
end
function Text:RepeatTexts()
if self.storedText then
self:SetText(self.storedText)
end
end
function Text:OnFinished()
self.strings = nil
self.timers = nil
end
function Text:FlagForceFinished(state)
self.forceFinished = state
end
function Text:IsForceFinishedFlagged()
return self.forceFinished
end
function Text:PreparePlayback()
self.numTexts = nil
self:FlagForceFinished(false)
self:PauseTimer()
self:OnFinished()
self:DisplayLine()
end
function Text:ResumeTimer()
if self:HasLineData() then
Timer:AddText(self)
return true
end
end
function Text:PauseTimer()
Timer:RemoveText(self)
end
----------------------------------
-- Text: display
----------------------------------
function Text:DisplayLine(text, time)
if not self:GetFont() then
self:CheckApplicableFonts()
self:SetFontObject(self.fontObjectsToTry[1])
end
getmetatable(self).__index.SetText(self, text)
self:SetCurrentLineTime(time)
self:ApplyFontObjects()
if self.OnDisplayLineCallback then
self:OnDisplayLineCallback(text, time)
end
end
function Text:SetFontObjectsToTry(...)
self.fontObjectsToTry = { ... }
if self:GetText() then
self:ApplyFontObjects()
end
end
function Text:ApplyFontObjects()
self:CheckApplicableFonts()
for i, fontObject in ipairs(self.fontObjectsToTry) do
self:SetFontObject(fontObject)
if not self:IsTruncated() then
break
end
end
end
function Text:CheckApplicableFonts()
if not self.fontObjectsToTry or not self.fontObjectsToTry[1] then
error('No fonts applied to TextMixin, call SetFontObjectsToTry first')
end
end
----------------------------------
-- Text: state getters
----------------------------------
function Text:GetTimeRemaining()
if self.timeStarted and self.timeToFinish then
local difference = ( self.timeStarted + self.timeToFinish ) - GetTime()
return difference < 0 and 0 or difference
end
return 0
end
function Text:GetProgress()
local full = self:GetNumTexts()
local remaining = self:GetNumRemaining()
return ('%d/%d'):format(full - remaining + 1, full)
end
function Text:GetProgressPercent()
if self.timeStarted and self.timeToFinish then
local progress = ( GetTime() - self.timeStarted ) / self.timeToFinish
return ( progress > 1 ) and 1 or progress
end
return 1
end
function Text:GetCurrentProgress()
local modifiedTime = self:GetModifiedTime()
local fullTime = self:GetOriginalTime()
if modifiedTime and fullTime and fullTime > 0 then
return (1 - modifiedTime / fullTime)
end
end
function Text:IsFinished() return not self.strings end
function Text:IsSequence() return self.numTexts and self.numTexts > 1 end
function Text:IsLineFinished() return self.timers[1] <= 0 end
function Text:GetNumTexts() return self.numTexts or 0 end
function Text:GetNumRemaining() return self.strings and #self.strings or 0 end
function Text:HasLineData() return self.strings and self.timers end
function Text:HasLine() return self.strings and self.strings[1] and true end
function Text:HasFollowup() return self.strings and self.strings[2] and true end
function Text:GetModifiedTime() return self.timers and self.timers[1] end
function Text:GetOriginalTime() return self.currentLineTime or 0 end
function Text:GetLineProgress() return (self.timers and self.currentLineTime) and (self.timers[1]/self.currentLineTime) or 1 end
function Text:GetLine() return self.strings[1], self.timers[1] end
function Text:RemoveLine() return tremove(self.strings, 1), tremove(self.timers, 1) end
----------------------------------
-- Timer handle
----------------------------------
function Timer:AddText(fontString)
if fontString then
self.Texts[fontString] = true
self:SetScript('OnUpdate', self.OnUpdate)
end
end
function Timer:GetTexts() return pairs(self.Texts) end
function Timer:RemoveText(fontString)
if fontString then
self.Texts[fontString] = nil
end
end
function Timer:OnTextFinished(fontString)
if fontString then
self:RemoveText(fontString)
if fontString.OnFinishedCallback then
fontString:OnFinishedCallback()
end
end
end
function Timer:OnUpdate(elapsed)
for text in self:GetTexts() do
if text:HasLine() then
-- if there's no text displayed, display the current line.
if not text:GetText() then
text:SetToCurrentLine()
end
-- deduct elapsed time since update from current timer
text:UpdateCurrentLineTime(-elapsed)
-- timer is below/equal to zero, move on to next line
if text:IsLineFinished() then
text:RemoveLine()
-- check if there's another line waiting
if text:HasLine() then
text:SetToCurrentLine()
else
text:OnFinished()
end
end
else
self:OnTextFinished(text)
end
end
if not next(self.Texts) then
self:SetScript('OnUpdate', nil)
end
end