Name | Type | is_array | initial_value |
footman | unit | No | |
grunt | unit | No | |
peon | unit | No | |
speakactive | boolean | No |
-- "Speak" JRPG Dialogue System Overview:
speak:initscene()
--[[
Run this command to initialize the scene camera and prepare the dialogue box.
In this function, you may want to add/remove map-wide settings as needed for your use case.
*Note: if you init scenes instead of playing them immediately, you might want to manually position the camera
using 'camtargetx' and 'camtargety' (see below).
Properties:
.scenecam = the camera object settings to use (to change before scenes, use 'speak.scenecam = gg_cam_yourCustomCamera')
.camtargetx = position the camera here (if unit pan is enabled, it will be automated while items play).
.camtargety = ``
--]]
speak:startscene(chain[, ...])
--[[
@chain = the chain object containing the sequence of dialogue items, aka the scene, to run.
You can pass multiple chains as arguments and they will merge in their passed order (e.g. chain01, chain02, chain 03[, ...])
Run this command to begin the chain of dialogue items.
You can reference the cached chain object with 'speak.currentchain', which is a temporary clone of @chain.
To retrieve the current position in the chain, one can use 'speak.currentindex'. This could be useful to check for scene progress
inside of other functions if needed (an idea that comes to mind is cleaning up eye-candy effects or units made in the area).
--]]
speak:endscene()
--[[
This command runs automatically when a scene's current chain object is exhausted (i.e. last item is played).
If desired, you could call this automatically under specified conditions or via hotkeys to cut scenes short.
--]]
speak:pause()
--[[
Pause a scene, hiding the dialogue box and preventing continue actions.
Useful if you want to hide the box, run a cinematic sequence, then resume social interactions.
--]]
speak:resume()
--[[
Resume a scene from the stored speech chain point, showing the dialogue box once more.
--]]
speak.actor:new()
--[[
@returns table.
Create a new actor object.
Properties:
.unit = unit; actor's assigned unit (flash indicator and for referencing in functions).
.portrait = table; the file paths for displaying character's reaction portrait.
.name = string; defaults to assigned unit's name.
--]]
speak.actor:assign(unit, portrait)
--[[
@unit = the owner of the speech item.
@portrait = the portrait object for the actor.
This function fulfills the requirements of an actor, and will also accept anonymous tables as arguments for @portrait,
allowing you to quickly build new actors with terse code.
See the 'screenplay' demo script file for an explicit example.
--]]
speak.item:new()
--[[
@returns table.
Create a new speech item object.
Properties
.text = string; the characters that render in the dialogue box when called.
.actor = table; the actor object that owns the speech item.
.emotion = string; the emotion string for looking up the actor's matching portrait.
.anim = string; string animation to play when called.
.sound = sound; the sound to play when called.
.func = function; the function to play when called.
See the 'screenplay' demo script file for an explicit example.
--]]
speak.chain:new()
--[[
@returns table.
Create a new speech item chain object.
Chains are simple table classes and should be index-value paired, by which they are called in ascending index order when
injected into 'speak:startscene(chain)'. Chain objects have no config properties other than their indexed item content.
--]]
speak.chain:add(item, index)
--[[
@item = speech item object to add.
@index = [optional] the location to insert (pops into table, pushing other values up if present).
Add an item to a chain (provide no @index to insert at the top i.e. new index).
example: my_scene01:add(my_item)
--]]
speak.chain:remove(item)
--[[
@item = speech item to remove.
Remove an item from a chain.
example: my_scene01:remove(my_item)
--]]
speak.chain:build(t)
--[[
@t = a table used as a simple constructor for dialogue items, to allow for vertical reading like you would a real screenplay.
@returns table.
Example:
my_build_table = {
[1] = { "Some kind of dialogue.", actor1 , emotion1, anim1, sound1, func1 }
[2] = { "Some kind of dialogue.", actor2 , emotion2, anim2, sound2, func2 }
[3] = { "Some kind of dialogue.", actor3 , emotion3, anim3, sound3, func3 }
}
my_new_scene = speak.chain:build(my_build_table)
See the 'screenplay' demo script file for an explicit example.
--]]
utils = {}
utils.debug = false
utils.hotkeyt = {} -- store player hotkey trigs.
fp = { -- shorthand 'FRAMEPOINT' for terser code.
tl = FRAMEPOINT_TOPLEFT, t = FRAMEPOINT_TOP,
tr = FRAMEPOINT_TOPRIGHT, r = FRAMEPOINT_RIGHT,
br = FRAMEPOINT_BOTTOMRIGHT, b = FRAMEPOINT_BOTTOM,
bl = FRAMEPOINT_BOTTOMLEFT, l = FRAMEPOINT_LEFT,
c = FRAMEPOINT_CENTER,
}
utils.lorem = "Space, the final frontier. These are the voyages of the Starship"
.." Enterprise. Its five-year mission: to explore strange new worlds, to seek out new"
.." life and new civilizations, to boldly go where no man has gone before. Many say"
.." exploration is part of our destiny, but it’s actually our duty to future generations"
.." and their quest to ensure the survival of the human species."
function utils.newclass(t)
local t = t
t.__index = t
t.lookupt = {}
t.new = function()
local o = {}
setmetatable(o, t)
return o
end
t.destroy = function()
t.lookupt[t] = nil
end
if utils.debug then print("made new class for "..tostring(t)) end
end
function utils.timed(dur, func)
local tmr = NewTimer()
TimerStart(tmr, dur, false, function() func() ReleaseTimer() end)
return tmr
end
function utils.timedrepeat(dur, count, func)
local t, c = count, 0
local tmr = NewTimer()
if t == nil then
TimerStart(tmr, dur, true, function() func() end)
else
TimerStart(tmr, dur, true, function() func() c = c + 1 if c >= t then ReleaseTimer() end end)
end
return tmr
end
function utils.tablecollapse(t)
for index, value in pairs(t) do
if value == nil then
table.remove(t, index)
end
end
end
-- :: clones a table and any child tables (setting metatables)
-- @t = table to copy
function utils.deepcopy(t)
local t2 = {}
if getmetatable(t) then
setmetatable(t2, getmetatable(t))
end
for k,v in pairs(t) do
if type(v) == "table" then
local newt = {}
if getmetatable(v) then
setmetatable(newt, getmetatable(v))
end
for k2, v2 in pairs(v) do
newt[k2] = v2
end
t2[k] = newt
else
t2[k] = v
end
end
return t2
end
function utils.destroytable(t)
for i,v in pairs(t) do
if type(v) == "table" then
for i2,v2 in pairs(v) do
v2 = nil
i2 = nil
end
else
v = nil
i = nil
end
end
end
-- @bool = true to fade out (hide); false to fade in (show).
function utils.fadeframe(bool, fh, dur)
BlzFrameSetVisible(fh, true)
local bool = bool
local fh = fh
local alpha = 255
local int = math.floor(255/math.floor(dur/0.03))
-- show:
if bool then
BlzFrameSetVisible(fh, true)
BlzFrameSetAlpha(fh, 255)
utils.timedrepeat(0.03, nil, function()
if BlzFrameGetAlpha(fh) > 0 and BlzFrameGetAlpha(fh) > int then
alpha = alpha - int
BlzFrameSetAlpha(fh, alpha)
else
BlzFrameSetAlpha(fh, 0)
BlzFrameSetVisible(fh, false)
ReleaseTimer()
end
end)
-- hide:
else
BlzFrameSetVisible(fh, true)
BlzFrameSetAlpha(fh, 0)
utils.timedrepeat(0.03, nil, function()
if BlzFrameGetAlpha(fh) ~= 255 and BlzFrameGetAlpha(fh) < 255 - int then
alpha = alpha + int
BlzFrameSetAlpha(fh, alpha)
else
BlzFrameSetAlpha(fh, 255)
BlzFrameSetVisible(fh, true)
ReleaseTimer()
end
end)
end
end
function utils.debugfunc( func, name )
local name = name or ""
local passed, data = pcall( function() func() return "func " .. name .. " passed" end )
if not passed then
print(name, passed, data)
end
passed = nil
data = nil
end
function utils.playsound(snd, p)
local p = p or GetTriggerPlayer()
if p == GetLocalPlayer() then
StopSound(snd, false, false)
StartSound(snd)
end
end
function utils.playsoundall(snd)
utils.looplocalp(function()
StopSound(snd, false, false)
StartSound(snd)
end)
end
-- @func = run this for all players, but local only.
function utils.looplocalp(func)
ForForce(bj_FORCE_ALL_PLAYERS, function()
if GetEnumPlayer() == GetLocalPlayer() then
func(GetEnumPlayer())
end
end)
end
function utils.frameaddevent(fh, func, frameeventtype)
local trig = CreateTrigger()
local fh = fh
BlzTriggerRegisterFrameEvent(trig, fh, frameeventtype)
TriggerAddCondition(trig, Condition(function()
return BlzGetTriggerFrameEvent() == frameeventtype and BlzGetTriggerFrame() == fh
end) )
TriggerAddAction(trig, func)
return trig
end
function utils.speechindicator(unit)
UnitAddIndicatorBJ(unit, 0.00, 100, 0.00, 0)
end
function utils.fixfocus(fh)
BlzFrameSetEnable(fh, false)
BlzFrameSetEnable(fh, true)
end
function utils.tablelength(t)
local count = 0
for _ in pairs(t) do count = count + 1 end
return count
end
--Global Initialization 1.1 also hooks the InitCustomTriggers and RunInitializationTriggers functions
do
local iFuncs = {}
function onInitialization(func) -- Runs once all Map Initialization triggers are executed
iFuncs[func] = func
end
local function runInitialization()
for k, f in pairs(iFuncs) do f() end
iFuncs = nil
end
local tFuncs = {}
function onTriggerInit(func) -- Runs once all InitTrig_ functions are called
tFuncs[func] = func
end
local function runTriggerInit()
for k, f in pairs(tFuncs) do f() end
tFuncs = nil
local old = RunInitializationTriggers
if old then
function RunInitializationTriggers()
old()
runInitialization()
end
else
runInitialization()
end
end
local gFuncs = {}
function onGlobalInit(func) --Runs once all udg_ globals are set.
gFuncs[func] = func --Simplification thanks to TheReviewer and Zed on Hive Discord
end
local function runGlobalInit()
for k, f in pairs(gFuncs) do f() end
gFuncs = nil
local old = InitCustomTriggers
if old then
function InitCustomTriggers()
old()
runTriggerInit()
end
else
runTriggerInit()
end
end
local oldBliz = InitBlizzard
function InitBlizzard()
oldBliz()
local old = InitGlobals
if old then
function InitGlobals()
old()
runGlobalInit()
end
else
runGlobalInit()
end
end
end
do
local data = {}
function SetTimerData(whichTimer, dat)
data[whichTimer] = dat
end
--GetData functionality doesn't even require an argument.
function GetTimerData(whichTimer)
if not whichTimer then whichTimer = GetExpiredTimer() end
return data[whichTimer]
end
--NewTimer functionality includes optional parameter to pass data to timer.
function NewTimer(...)
local t = CreateTimer()
local arg = {...}
data[t] = {}
if arg then
for i,v in ipairs(arg) do
data[t][i] = v
end
end
return t
end
--Release functionality doesn't even need for you to pass the expired timer.
--as an arg. It also returns the user data passed.
function ReleaseTimer(whichTimer)
if not whichTimer then whichTimer = GetExpiredTimer() end
local dat = data[whichTimer]
data[whichTimer] = nil
PauseTimer(whichTimer)
DestroyTimer(whichTimer)
return dat
end
end
speak = { -- main dialogue class.
-- start of config settings (caution when editing durations):
hideui = true, -- should the default game ui be hidden when scenes run?
unitflash = true, -- should transmission indicators flash on the speaking unit?
showskip = true, -- should scenes be entirely skippable? (may have consequences if internal scene functions run)
fade = false, -- should dialogue components have fading eye candy effects?
fadedur = 0.81, -- how fast to fade if fade is enabled.
unitpan = true, -- should the camera pan to the speaking actor's unit?
camspeed = 0.75, -- how fast the camera pans when scenes start/end/shift.
anchorx = 0.4, -- x-offset for dialogue box's center framepoint.
anchory = 0.12, -- y-offset ``.
width = 0.37, -- x-width of dialogue frame.
height = 0.11, -- y-width ``.
nextbtntxt = "Continue",
skipbtntxt = "Skip",
fdfbackdrop = "BattleNetControlBackdropTemplate",
fdfportrait = "BattleNetControlBackdropTemplate",
fdfbutton = "ScriptDialogButton",
fdftitle = "CustomText", -- from imported .fdf
fdftextarea = "CustomTextArea", -- ``
hextitle = "|cffffce22", -- character title text color.
hextext = "|cffffffff", -- character speech text color.
debug = false, -- print debug messages for certain functions.
-- end of config settings.
}
speak.item = { -- sub class for dialogue strings and how they play.
-- required inputs:
text = nil, -- the dialogue string to display.
actor = nil, -- the actor that owns this speech item.
-- optional inputs:
emotion = nil, -- the portrait to display (if left empty, the default will be shown play).
anim = nil, -- play a string animation when the speech item is played.
sound = nil, -- play a sound when this item begins.
func = nil, -- call this function when the speech item is played (e.g. move a unit).
-- optional config:
speed = 0.04, -- cadence to show new string characters (you could increase for dramatic effect).
}
speak.portrait = { -- sub class for storing actor portrait files and rendering them.
none = '', -- portrait file path.
angry = '', -- ``
blush = '', -- ``
happy = '', -- ``
}
speak.actor = { -- sub class for storing actor settings.
unit = nil, -- the unit which owns the actor object.
name = nil, -- the name of the actor (defaults to unit name).
portrait = nil, -- the portrait object to use for the unit.
}
speak.chain = {} -- sub class for chaining speech items in order.
-- initialize classes and class specifics:
function speak:init()
utils.newclass(speak.actor)
utils.newclass(speak.portrait)
utils.newclass(speak.item)
utils.newclass(speak.chain)
self.worldui = BlzGetOriginFrame(ORIGIN_FRAME_WORLD_FRAME, 0)
self.prevactor = nil -- control for previous actor if frames animate on change.
self.itemdone = false -- flag for controlling quick-complete vs. next speech item.
self.currentindex = 1 -- the item currently being played from an item queue.
self.camtargetx = 0 -- X coord to pan camera to.
self.camtargety = 0 -- Y coord ``
self.paused = false -- a flag for dealing with paused scenes.
self.initialized = false -- flag for if the system was already set up manually.
self.scenecam = gg_cam_sceneCam
self.camtmr = NewTimer()
end
-- generate ui frames.
function speak:initframes()
self.fr = {}
self.fr.backdrop = BlzCreateFrame(self.fdfbackdrop, self.gameui, 0, 0)
BlzFrameSetSize(self.fr.backdrop, self.width, self.height)
BlzFrameSetAbsPoint(self.fr.backdrop, fp.c, self.anchorx, self.anchory)
-- portrait border:
self.fr.portraitbg = BlzCreateFrame(self.fdfportrait, self.fr.backdrop, 0, 0)
BlzFrameSetSize(self.fr.portraitbg, self.height*0.94, self.height*0.94)
BlzFrameSetPoint(self.fr.portraitbg, fp.bl, self.fr.backdrop, fp.bl, self.height*0.05, self.height*0.05)
-- character potrait:
self.fr.portrait = BlzCreateFrameByType("BACKDROP", "SpeakFillPortrait", self.fr.backdrop, "", 0)
BlzFrameSetSize(self.fr.portrait, self.height*0.78, self.height*0.78)
BlzFrameSetPoint(self.fr.portrait, fp.c, self.fr.portraitbg, fp.c, 0, 0)
BlzFrameSetTexture(self.fr.portrait, "ReplaceableTextures\\CommandButtons\\BTNFootman.blp", 0, true)
-- character title:
self.fr.title = BlzCreateFrame(self.fdftitle, self.fr.backdrop, 0, 0)
BlzFrameSetPoint(self.fr.title, fp.tl, self.fr.portraitbg, fp.tr, self.height*0.12, -self.height*0.1)
BlzFrameSetText(self.fr.title, "CHARACTER TITLE")
-- dialogue string:
self.fr.text = BlzCreateFrame(self.fdftextarea, self.fr.backdrop, 0, 0)
BlzFrameSetSize(self.fr.text, self.width - BlzFrameGetWidth(self.fr.portraitbg), self.height*0.65)
BlzFrameSetPoint(self.fr.text, fp.tl, self.fr.title, fp.bl, 0, -self.height*0.01)
BlzFrameSetText(self.fr.text, utils.lorem)
-- next button:
self.fr.nextbtn = BlzCreateFrame(self.fdfbutton, self.fr.backdrop, 0, 0)
BlzFrameSetSize(self.fr.nextbtn, 0.09, 0.033)
BlzFrameSetPoint(self.fr.nextbtn, fp.tr, self.fr.backdrop, fp.br, 0, 0)
BlzFrameSetText(self.fr.nextbtn, self.nextbtntxt)
-- skip button:
self.fr.skipbtn = BlzCreateFrame(self.fdfbutton, self.fr.backdrop, 0, 0)
BlzFrameSetSize(self.fr.skipbtn, 0.09, 0.033)
BlzFrameSetPoint(self.fr.skipbtn, fp.tl, self.fr.backdrop, fp.bl, 0, 0)
BlzFrameSetText(self.fr.skipbtn, self.skipbtntxt)
--
utils.frameaddevent(self.fr.nextbtn, function() speak:clicknext() end, FRAMEEVENT_CONTROL_CLICK)
utils.frameaddevent(self.fr.skipbtn, function() speak:endscene() end, FRAMEEVENT_CONTROL_CLICK)
self:show(false, true)
end
-- initialize the scene interface (e.g. typically if you are running a cinematic component first).
function speak:initscene()
self:clear()
if self.hideui then
BlzHideOriginFrames(true)
BlzFrameSetVisible(self.consolebd, false)
end
BlzEnableSelections(false, true)
PauseAllUnitsBJ(true)
self:enablecamera(true)
-- set flag for any GUI triggers that might need it:
udg_speakactive = true
self.initialized = true
end
-- @chain = the chain object to play items from.
-- @... = [optional] merge together additional chain objects (ordered by argument position).
function speak:startscene(chain, ...)
-- index increment is controlled in speak.chain:playnext.
self.currentindex = 0
self.currentchain = utils.deepcopy(chain)
self.paused = false
if not self.initialized then
self:initscene()
end
if ... then
for _,v in pairs({...}) do
if type(v) == "table" then
for i,itemval in pairs(v) do
table.insert(self.currentchain, utils.tablelength(self.currentchain) + 1, itemval)
end
else
assert(false, "speak:startscene was passed a non-table argument. (tip: was it a scene object?)")
end
end
end
self:hideshowskip()
if self.fade then
self:fadeout(false)
else
BlzFrameSetVisible(self.fr.backdrop, true)
end
BlzFrameSetEnable(self.fr.nextbtn, true)
self.currentchain:playnext()
-- run debug if enabled:
if self.debug then self.currentchain:debug("startscene") end
end
function speak:hideshowskip()
if self.showskip then
BlzFrameSetVisible(self.fr.skipbtn, true)
BlzFrameSetEnable(self.fr.skipbtn, true)
else
BlzFrameSetVisible(self.fr.skipbtn, false)
BlzFrameSetEnable(self.fr.skipbtn, false)
end
end
-- end the dialogue sequence.
function speak:endscene()
if self.hideui then
BlzHideOriginFrames(false)
BlzFrameSetVisible(self.consolebd, true)
end
BlzEnableSelections(true, true)
if self.fade then
self:fadeout(true)
else
BlzFrameSetVisible(self.fr.backdrop, false)
end
PauseAllUnitsBJ(false)
self:enablecamera(false)
utils.fixfocus(self.fr.skipbtn)
BlzFrameSetEnable(self.fr.nextbtn, false)
BlzFrameSetVisible(self.fr.nextbtn, false)
BlzFrameSetEnable(self.fr.skipbtn, false)
BlzFrameSetVisible(self.fr.skipbtn, false)
-- disable flag for any GUI triggers that might need it:
udg_speakactive = false
self.initialized = false
-- clear cache:
if not self.paused then
utils.destroytable(self.currentchain)
end
end
-- @bool = true to enter dialogue camera; false to exit.
function speak:enablecamera(bool)
utils.debugfunc(function()
if bool then
ClearTextMessagesBJ(bj_FORCE_ALL_PLAYERS)
TimerStart(self.camtmr, 0.03, true, function()
utils.looplocalp(function(p)
CameraSetupApplyForPlayer(true, self.scenecam, p, self.camspeed)
PanCameraToTimedForPlayer(p, self.camtargetx, self.camtargety, self.camspeed)
end)
end)
else
PauseTimer(self.camtmr)
utils.looplocalp(function(p)
ResetToGameCameraForPlayer(p, self.camspeed)
end)
end
end,"enablecamera")
end
-- function to run when the "next" button is clicked.
function speak:clicknext()
-- fix focus:
utils.fixfocus(self.fr.nextbtn)
if self.currentchain then
if speak.itemdone then
self.currentchain:playnext()
else
speak.itemdone = true
end
else
self:endscene()
end
end
-- @bool = true to show, false to hide.
-- @skipeffectsbool = [optional] set to true skip fade animation.
function speak:show(bool, skipeffectsbool)
if bool then
if self.fade and not skipeffectsbool then
self:fadeout(bool)
else
for _,fh in pairs(self.fr) do
if fh ~= self.fr.skipbtn then
BlzFrameSetVisible(fh, true)
end
end
end
else
for _,fh in pairs(self.fr) do
BlzFrameSetVisible(fh, false)
end
end
end
-- @bool = true to animate out (hide), false to animate in (show).
function speak:fadeout(bool)
utils.fadeframe(bool, self.fr.backdrop, self.fadedur)
end
-- pause the dialogue frame, hiding and clearing it in order to run a cinematic sequence, etc.
function speak:pause()
self:reset()
self:show(false)
self.resumeindex = self.currentindex + 1
self.pause = true
end
-- resume after initiating a pause event.
function speak:resume()
self:show(true)
self.currentchain:start(self.resumeindex)
self.resumeindex = nil
self.pause = false
end
-- when a new chain is being played, initialize the default display.
function speak:clear()
BlzFrameSetText(self.fr.text, "")
BlzFrameSetText(self.fr.title, "")
if self.fade then
BlzFrameSetAlpha(self.fr.portrait, 0)
BlzFrameSetAlpha(self.fr.portraitbg, 0)
BlzFrameSetAlpha(self.fr.text, 0)
BlzFrameSetAlpha(self.fr.title, 0)
end
end
-- @unit = assign the unit responsible for @portrait.
-- @portrait = portrait object for @unit.
function speak.actor:assign(unit, portrait)
self.unit = unit
self.portrait = portrait
self.name = GetUnitName(unit)
if not getmetatable(portrait) then
setmetatable(portrait, speak.portrait)
end
end
-- play a speech item, rendering its string characters over time and displaying actor details.
function speak.item:play()
-- a flag for skipping the text animation:
speak.itemdone = false
-- initialize timer settings:
if speak.tmr then
ReleaseTimer(speak.tmr)
speak.tmr = nil
end
local count = string.len(self.text)
local pos = 1
local dur = speak.fadedur
local ahead = ''
-- render string characters:
BlzFrameSetText(speak.fr.title, speak.hextitle..self.actor.name.."|r")
speak.tmr = utils.timedrepeat(self.speed, count, function()
if pos < count and not speak.itemdone then
ahead = string.sub(self.text, pos, pos+1)
-- scan for formatting patterns:
if ahead == '|c' then
pos = pos + 10
elseif ahead == '|r' or ahead == '|n' then
pos = pos + 2
else
pos = pos + 1
end
BlzFrameSetText(speak.fr.text, speak.hextext..string.sub(self.text, 1, pos))
utils.fixfocus(speak.fr.text)
else
speak.itemdone = true
BlzFrameSetText(speak.fr.text, speak.hextext..self.text)
ReleaseTimer()
end
end)
-- run additional speech inputs if present:
if speak.unitflash then
utils.speechindicator(self.actor.unit)
end
if self.anim then
ResetUnitAnimation(self.actor.unit)
QueueUnitAnimation(self.actor.unit, self.anim)
QueueUnitAnimation(self.actor.unit, "stand")
end
if self.sound then
utils.playsoundall(self.sound)
end
if self.func then
self.func()
end
-- manage frames:
self.actor.portrait:render(self.emotion)
speak:hideshowskip()
BlzFrameSetVisible(speak.fr.nextbtn, true)
BlzFrameSetVisible(speak.fr.title, true)
BlzFrameSetVisible(speak.fr.text, true)
BlzFrameSetVisible(speak.fr.portraitbg, true)
if speak.fade and speak.prevactor ~= self.actor then
utils.fadeframe(false, speak.fr.portrait, speak.fadedur)
utils.fadeframe(false, speak.fr.title, speak.fadedur)
utils.fadeframe(false, speak.fr.text, speak.fadedur)
utils.fadeframe(false, speak.fr.portraitbg, speak.fadedur)
else
BlzFrameSetVisible(speak.fr.portrait, true)
end
-- manage units:
if speak.unitpan then
speak.camtargetx = GetUnitX(self.actor.unit)
speak.camtargety = GetUnitY(self.actor.unit)
end
speak.prevactor = self.actor
end
-- after a speech item completes, see what needs to happen next (load next item or close, etc.)
function speak.chain:playnext()
utils.debugfunc(function()
speak.currentindex = speak.currentindex + 1
if speak.currentindex > utils.tablelength(speak.currentchain) + 1 then
speak:clear()
speak:endscene()
else
if not self[speak.currentindex] then
-- if next item was set to nil or is empty, try to skip over:
self:playnext()
else
if speak.debug then
print("trying to play index item: "..speak.currentindex)
print(self[speak.currentindex].actor.name)
end
self[speak.currentindex]:play()
end
end
end, "playnext")
end
-- @t = provide a table of items to automatically build; item's value ordering will follow index-values.
-- item format reminder: { text, actor, emotion, anim, sound, func } (if jumping a space, leave it nil)
function speak.chain:build(t)
assert(t and t[1], "error: speak.chain:build is missing an index-value table argument.")
local o = speak.chain:new()
-- we do pairs instead of ipairs so we can skip over nil values
-- that the end user may want to fill out later:
for i,v in pairs(t) do
if type(v) == "table" and type(i) == "number" then
o[i] = speak.item:new()
o[i].text = v[1]
o[i].actor = v[2]
if v[3] then
o[i].emotion = v[3]
end
if v[4] then
o[i].anim = v[4]
end
if v[5] then
o[i].sound = v[5]
end
if v[6] then
o[i].func = v[6]
end
end
end
if speak.debug then o:debug("build") end
return o
end
-- @item = add this item as the next item in the chain.
-- @index = [optional] insert @item into this index location instead.
function speak.chain:add(item, index)
if index then
table.insert(self, index, item)
else
self[#self + 1] = item
end
end
-- @item = remove this item from the chain (accepts item object or the index location in the chain).
function speak.chain:remove(item)
-- remove by index:
if type(item) == "number" then
table.remove(self, item)
-- remove by value:
else
for i,v in ipairs(self) do
if v == item then
table.remove(self, i)
end
end
end
end
-- @str = label this debug print source (e.g. "manual debug call" or "scene build", etc.).
-- @startindex, @endindex = [optional] index range to loop through (for large scene objects).
function speak.chain:debug(str, startindex, endindex)
-- if you need to debug an object, print its values via index range.
local i, startindex, endindex = 1, startindex or 1, endindex or utils.tablelength(self)
for i,v in pairs(self) do
if type(i) == "number" then
if i >= startindex then
print(" ")
print("debug: reading chain item from call: "..str)
print(i.." = "..tostring(v))
print("text = " ..tostring(v.text))
if v.actor then
print("actor = " ..tostring(v.actor.name))
print("unit = " ..tostring(v.actor.unit))
else
print("actor = nil")
end
print("emotion = " ..tostring(v.emotion))
print("anim = " ..tostring(v.anim))
print("sound = " ..tostring(v.sound))
print("func = " ..tostring(v.func))
end
else
assert(false, "speak.chain:debug read error: scene objects should be index-value pairs.")
end
i = i + 1
if i > endindex then
break
end
end
end
-- @emotion = string value used to look up an actor's portrait and render it in the dialogue box.
function speak.portrait:render(emotion)
if self[emotion] then
BlzFrameSetTexture(speak.fr.portrait, self[emotion], 0, true)
elseif self.none then
BlzFrameSetTexture(speak.fr.portrait, self.none, 0, true)
else
assert(false, "speak.portrait:render could not find a file path.")
end
end
--[[
init after GUI:
--]]
onInitialization(function()
speak:init()
end)
-- time elapsed init.
utils.timed(0.0, function()
utils.debugfunc(function()
if not BlzLoadTOCFile('war3mapImported\\CustomFrameTOC.toc') then
print("error: .fdf file failed to load")
print("tip: are you missing a curly brace in the fdf?")
print("tip: does the .toc file have the correct file paths?")
print("tip: .toc files require an empty newline at the end")
end
speak.consolebd = BlzGetFrameByName("ConsoleUIBackdrop",0)
speak.gameui = BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0)
speak:initframes()
end, "elapsed init")
end)
function buildactors()
--[[
there are three methods to building a portrait object:
1) simply declare a table and speak.actor:assign
will handle initializing the portrait object.
2) if you want to be more formal, you can declare
the new object then assign emotions as needed.
3) if you're a terse-junky, you can build a new
portrait object by passing in an anonymous table.
--]]
-- example 01) simple table - you just really like tables.
portrait_grunt = {
none = 'war3mapImported\\portrait-orc.blp',
angry = 'war3mapImported\\portrait-orc_anger.blp',
blush = 'war3mapImported\\portrait-orc_blush.blp',
happy = 'war3mapImported\\portrait-orc_happy.blp',
}
actor_grunt = speak.actor:new()
actor_grunt:assign(udg_grunt, portrait_grunt)
-- example 02) formal - you may be wanting to modify portrait objects or change an actor's portrait kit.
portrait_footman = speak.portrait:new()
portrait_footman.none = 'war3mapImported\\portrait-hu.blp'
portrait_footman.angry = 'war3mapImported\\portrait-hu_anger.blp'
portrait_footman.blush = 'war3mapImported\\portrait-hu_blush.blp'
portrait_footman.happy = 'war3mapImported\\portrait-hu_happy.blp'
actor_footman = speak.actor:new()
actor_footman:assign(udg_footman, portrait_footman)
-- example 03) terse-junky - keep the code really tight.
actor_peon = speak.actor:new()
actor_peon:assign(
udg_peon,
{ none = 'war3mapImported\\portrait-orc_peon.blp', angry = 'war3mapImported\\portrait-orc_peon.blp',
blush = 'war3mapImported\\portrait-orc_peon.blp', happy = 'war3mapImported\\portrait-orc_peon.blp', })
-- (in the future, you could change actor_peon's portrait with 'actor_peon.portrait.angry', etc.)
end
function buildscreenplay()
--[[
there are two ways to build chained dialogue items:
1) simple:
using a table with index-values, build a dialogue chain
by inserting the text and actor (auto generating
each speech item automatically).
item format:
{ text, actor [, emotion, anim, sound, func] }
{ "My dialogue text.", actor_object [, "angry", "stand victory", sound_var, func_var] }
example:
my_table = {
[1] = { "Your character's dialogue", my_actor_object_a, "angry" },
[2] = { "Your other character's dialogue", my_actor_object_b },
}
my_scene = scene.chain:build(my_table)
2) specified:
build each speech item separately. this can be useful
if you are going to add and remove speech items
yourself i.e. if user actions influence dialogue.
example:
my_dynamic_item = speak.item:new()
my_dynamic_item.actor = my_actor_object_a
my_dynamic_item.text = "This is my dynamic dialogue."
scene_01 = speak.chain:new()
scene_01:add(some_other_item)
scene_01:add(my_dynamic_item)
scene_01:add(some_other_item)
(...)
you decide to remove it later based on a player's action and insert a different item:
scene_01:remove(my_dynamic_item)
--]]
--[[
simple build example:
this method is useful for visually building screenplays,
allowing you to traverse them vertically as you would
pages from a normal screenplay in real life.
--]]
local spawngrunts = function()
-- create some grunts for the scene when the grunt calls for his squad:
local unit
unit = CreateUnit(Player(0), FourCC('ogru'), 700, 400, 0)
SetUnitInvulnerable(unit, true)
IssuePointOrderById(unit, 851986, 230, 75)
unit = CreateUnit(Player(0), FourCC('ogru'), 700, 380, 0)
SetUnitInvulnerable(unit, true)
IssuePointOrderById(unit, 851986, 465, -300)
unit = CreateUnit(Player(0), FourCC('ogru'), 700, 360, 0)
SetUnitInvulnerable(unit, true)
IssuePointOrderById(unit, 851986, 500, 0)
end
scene_01 = {
[1] = {
"|cffff0000Who goes there!?|r",
actor_footman,
},
[2] = {
"...",
actor_grunt,
"blush",
"stand two",
soundt.grunt01,
},
[3] = {
"What are you doing in Lordaeron, |cffff0000Horde|r filth?",
actor_footman,
"angry"
},
[4] = nil, -- left empty to demonstrate :add functionality.
[5] = {
"Wait, if there's one of you, then that means...",
actor_footman,
"blush",
},
[6] = {
"Form ranks!",
actor_grunt,
"angry",
"spell",
soundt.grunt02,
spawngrunts,
},
[7] = {
"Hah!|n|nYou think you've outwitted the Alliance with sheer numbers? We Alliance are never alone!",
actor_footman,
"happy",
},
[8] = {
"...",
actor_footman,
"blush",
},
[9] = {
"Garland? Erik? Where are you fools?",
actor_footman,
"blush",
},
[10] = {
"It looks like your men have forsaken you, human. Typical Alliance cowardice!",
actor_grunt,
"happy",
},
[11] = {
"Fine! I'll take you all on myself!",
actor_footman,
"angry",
"stand defend",
soundt.footman01,
},
[12] = {
"Others would call you a fool, human. But, you have the courage of a warrior. This is no honorable way to die."
.." Stand back troops; we meet tomorrow on the battlefield. Lok'tar ogar!",
actor_grunt,
},
[13] = {
"So be it. Tomorrow we meet on the hills of steel and bone and flesh. And now I've run out of things to say so I'm"
.." just meandering on and on and on to test this dialogue system. Yea, I'm still going. And going, and going. Wow,"
.." can you believe this scrolling text frame even works? It probably could use a footprint though. I think you can"
.." even use your mousewheel to scroll up when it's done spamming you with awesome lore content, just in case you"
.." missed something important. Really, really important. You might not understand this map without it. If it ever"
.." finishes, that is. You better not click that continue button. Hey! How DARE you!",
actor_footman,
},
}
-- we take the table outline above and convert it to a chain of speech items:
scene_01 = speak.chain:build(scene_01)
--[[
specified example where we fill the empty [4] slot:
--]]
peon_flee01 = speak.item:new()
peon_flee01.text = "Aaaggh!" -- (required)
peon_flee01.actor = actor_peon -- (required)
peon_flee01.emotion = "none" -- (optional)
peon_flee01.sound = soundt.peon01 -- (optional)
peon_flee01.func = function() -- (optional)
-- in this example, we'll also add a function that runs when the dialogue item is played.
PauseUnit(peon_flee01.actor.unit, false)
IssuePointOrderById(peon_flee01.actor.unit, 851986, 735, 750)
UnitApplyTimedLifeBJ( 6.0, FourCC('BTLF'), peon_flee01.actor.unit )
end
-- insert into the nil [4] slot in scene_01:
scene_01:add(peon_flee01, 4)
end
--[[
time elapsed init:
--]]
utils.timed(0.33, function()
utils.debugfunc(function()
--[[
sounds we play in the screenplay example:
--]]
soundt = {}
soundt.grunt01 = gg_snd_PeonWhat2
soundt.grunt02 = gg_snd_GruntWarcry1
soundt.peon01 = gg_snd_PeonPissed4
soundt.footman01 = gg_snd_FootmanYesAttack1
-- build after map init so we have a cached log with F12:
buildactors()
buildscreenplay()
end, "time elapsed init")
end)