Name | Type | is_array | initial_value |
actors | unit | Yes | |
projects | destructable | Yes | |
speakactive | boolean | No | |
testunit | unit | Yes |
-- fetch top left corner of a rect (how grid layout is determined).
function getrectcorner(rect)
return GetRectMinX(rect), GetRectMaxY(rect)
end
function xpcall( func )
local status = xpcall( func, printerror )
print(status)
end
function fixfocus(fh)
BlzFrameSetEnable(fh, false)
BlzFrameSetEnable(fh, true)
end
function printerror( err )
print( "error:", err )
end
function 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
end
-- :: helper function to check if table has index value.
-- @t = table to search.
-- @v = value to search for, returns true if it exists; false if not.
function tableindexof(t,v)
for index, value in pairs(t) do
if index == v then
return index
end
end
return false
end
function removefromtable(t,val)
for i,v in pairs(t) do
if v == val then
table.remove(t, i)
end
end
end
-- :: count number of tables within a table.
-- @t = table to search; returns number of tables.
function counttables(t)
local count = 0
for t in pairs(t) do
if type(t) == "table" then
count = count +1
end
end
return count
end
function tablelength(t)
local count = 0
for _ in pairs(t) do count = count + 1 end
return count
end
-- :: helper function to see if table has value
-- @t = table to search
-- @v = value to search for, returns true if it exists; false if not
function tablehasvalue(t,v)
for index, value in pairs(t) do
if value == v then
return true
end
end
return false
end
function tablehasindex(t,v)
for index, value in pairs(t) do
if index == v then
return true
end
end
return false
end
-- :: helper function to remove table index by searching for value
-- @t = table to search
-- @v = value to search for, returns index of that value where it exists in @t, returns false if it doesn't exist
function tablevalueindex(t,v)
for index, value in pairs(t) do
if value == v then
return index
end
end
return nil
end
-- :: helper function to collapse a table or array (remove empty spaces) e.g. [0, nil, 2] becomes [0,2]
-- @t = table to collapse
function tablecollapse(t)
for index, value in pairs(t) do
if value == nil then
table.remove(t, index)
end
end
end
function tabledeleteifempty(t)
for index, value in pairs(t) do
if value ~= nil then
return false
end
end
t = nil
return true
end
function distxy(x1,y1,x2,y2)
local dx = x2 - x1
local dy = y2 - y1
return SquareRoot(dx * dx + dy * dy)
end
function distanglexy(x1,y1,x2,y2)
return distxy(x1,y1,x2,y2), anglexy(x1,y1,x2,y2)
end
function distunits(unit1,unit2)
return distxy(unitx(unit1), unity(unit1), unitx(unit2), unity(unit2))
end
-- :: (alternative name that always causes headaches, so make it work) get facing angle from A to B
-- @x1,y1 = point A (facing from)
-- @x2,y2 = point B (facing towards)
function anglexy(x1,y1,x2,y2)
return bj_RADTODEG * Atan2(y2 - y1, x2 - x1)
end
function angleunits(unit1, unit2)
return bj_RADTODEG * Atan2(unity(unit2) - unity(unit1), unitx(unit2) - unitx(unit1))
end
-- :: PolarProjectionBJ converted to x,y
-- @x1,y1 = origin coord
-- @d = distance between origin and direction
-- @a = angle to project
function projectxy(x1,y1,d,a)
local x = x1 + d * Cos(a * bj_DEGTORAD)
local y = y1 + d * Sin(a * bj_DEGTORAD)
return x,y
end
function unitprojectxy(unit,d,a)
local x = unitx(unit) + d * Cos(a * bj_DEGTORAD)
local y = unity(unit) + d * Sin(a * bj_DEGTORAD)
return x,y
end
-- :: clones a table
-- @t = table to copy
function shallow_copy(t)
local t2 = {}
for k,v in pairs(t) do
t2[k] = v
end
return t2
end
-- :: clones a table and any child tables
-- @t = table to copy
function deep_copy(t)
if type(t) == "table" then
local t2 = {}
for k,v in pairs(t) do
if type(v) == "table" then
t2[k] = deep_copy(v)
if getmetatable(v) then
setmetatable(t2[k], getmetatable(v))
end
else
t2[k] = v
end
end
if getmetatable(t) then
setmetatable(t2, getmetatable(t))
end
return t2
else
return t
end
end
-- safest destroy loop.
function shallow_destroy(t)
for k,v in pairs(t) do
v = nil
k = nil
end
t = nil
end
-- :: loop through a parent table to find child tables, and delete them.
-- *note: will overflow the stack if used on tables that have self references.
-- @t = search this table.
function deep_destroy(t)
if t and type(t) == "table" then
for k,v in pairs(t) do
if k and v and type(v) == "table" then
deep_destroy(v)
else
v = nil
end
end
elseif t then
t = nil
end
end
-- :: get the length of a table.
-- @t = table to measure
function tablelength(t)
local count = 0
for _ in pairs(t) do count = count + 1 end
return count
end
-- :: round a number to a decimal place.
-- @num = round this number
-- @numdecplaces = to this many decimal places (use 0 for a whole number)
function round(num, numdecplaces)
local mult = 10^numDecimalPlaces
return math.floor(num * mult + 0.5) / mult
end
-- :: shorthand for getting a player number
function pid(player)
return GetConvertedPlayerId(player)
end
function powner(unit)
return GetOwningPlayer(unit)
end
-- :: shorthand for getting a player number based on a unit
function pidunit(unit)
return GetConvertedPlayerId(GetOwningPlayer(unit))
end
-- @snd = sound to play
-- @p = [optional] for this player (defaults to trig player)
function soundstop(snd, p)
if not p then p = GetTriggerPlayer() end
if GetLocalPlayer() == p then
StopSound(snd, false, false)
end
end
function triggeron(trig, bool)
if bool then
EnableTrigger(trig)
else
DisableTrigger(trig)
end
end
function localp()
return GetLocalPlayer()
end
function islocalp(p)
return GetLocalPlayer() == p
end
function getp(pnum)
return Player(pnum-1)
end
function getpnum(p)
return GetConvertedPlayerId(p)
end
function trigpnum()
return GetConvertedPlayerId(GetTriggerPlayer())
end
function unitp()
return GetOwningPlayer(GetTriggerUnit())
end
function trigp()
return GetTriggerPlayer()
end
function trigu()
return GetTriggerUnit()
end
function killu()
return GetKillingUnit()
end
function triguid()
return GetUnitTypeId(trigu())
end
function islocaltrigp()
return GetLocalPlayer() == GetTriggerPlayer()
end
function playsound(snd, p)
local p = p or GetTriggerPlayer()
if localp() == p then
StopSound(snd, false, false)
StartSound(snd)
end
end
function playsoundall(snd)
looplocalp(function()
StopSound(snd, false, false)
StartSound(snd)
end)
end
function stopsoundall(snd)
looplocalp(function()
StopSound(snd, false, false)
end)
end
-- @rect = rect to get center of.
-- @p = owning player
-- @id = unit raw code to create.
function unitinrect(rect, p, id)
-- try to capture most use cases with a short function.
return CreateUnit(p, FourCC(id), GetRectCenterX(rect), GetRectCenterY(rect), 255.0)
end
function unitatxy(p, x, y, id, _face)
return CreateUnit(p, FourCC(id), x, y, _face or 270.0)
end
-- @p = this player
-- @x,y = coords.
-- @dur = [optional] over seconds.
function panto(p, x, y, dur)
-- map camera to x,y instantly
local dur = dur or 0.0
PanCameraToTimedForPlayer(p, x, y, dur)
end
-- @p = this player.
-- @unit = this unit.
-- @dur = [optional] over seconds.
function pantounit(p, unit, dur)
-- map camera to x,y instantly
local dur = dur or 0.0
PanCameraToTimedForPlayer(p, GetUnitX(unit), GetUnitY(unit), dur)
end
-- @p = this player.
-- @r = this rect center.
-- @dur = [optional] over seconds.
function pantorect(p, r, dur)
-- map camera to x,y instantly
local dur = dur or 0.0
PanCameraToTimedForPlayer(p, GetRectCenterX(r), GetRectCenterY(r), dur)
end
function fadeblack(bool, _dur)
-- turn screen black
looplocalp(function()
if bool then
CinematicFadeBJ( bj_CINEFADETYPE_FADEOUT, _dur or 0.0, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
else
CinematicFadeBJ( bj_CINEFADETYPE_FADEIN, _dur or 0.0, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
end
end)
end
-- @unit = follow this unit
-- @p = for this player
function camlockunit(unit, p)
SetCameraTargetControllerNoZForPlayer(p, unit, 0, 0, false)
end
-- @unit = select this unit
-- @p = for this player
function selectu(unit, p)
if localp() == p then
if not IsUnitSelected(unit, p) then
SelectUnitForPlayerSingle(unit, p)
end
SetCameraFieldForPlayer(p, CAMERA_FIELD_TARGET_DISTANCE, 2200.0, 0)
SetCameraFieldForPlayer(p, CAMERA_FIELD_ANGLE_OF_ATTACK, 304.0, 0)
SetCameraFieldForPlayer(p, CAMERA_FIELD_ZOFFSET, 75.0, 0)
end
end
-- @unit = select this unit
-- @p = for this player, every 0.03 sec
-- @t = store the timer on this table as .permtimer
function permselect(unit, p, t)
local unit,p = unit,p
local func = function()
if not screenplay.pause_camera then
pantounit(p, unit, 0.23)
selectu(unit, p)
end
end
t.permtimer = NewTimer()
TimerStart(t.permtimer, 0.03, true, func)
SetTimerData(t.permtimer, func)
end
function temphide(unit, bool)
-- temporarily remove a unit (but maintaining their location presence).
SetUnitPathing(unit, not bool)
PauseUnit(unit, bool)
setinvul(unit, bool)
vertexhide(unit, bool)
end
function stop(unit)
-- order a unit to stop
IssueImmediateOrderById(unit, 851972)
end
function issmoverect(unit, rect)
IssuePointOrder(unit, 'move', GetRectCenterX(rect), GetRectCenterY(gg_rct_expeditionExit))
end
function issmovexy(unit, x, y)
IssuePointOrder(unit, 'move', x, y)
end
function smartmove(unit, target)
IssueTargetOrderById(unit, 851971, target)
end
function issatkxy(unit, x, y)
IssuePointOrderById(unit, 851983, x, y)
end
function issatkunit(unit, target)
IssueTargetOrderById(unit, 851983, target)
end
-- @bool = var to switch.
-- @dur = after x sec.
-- @flag = timer switch result (true or false)
function booltimer(bool, dur, flag)
-- a simple flag switch timer
bool = not flag
TimerStart(NewTimer(), dur, false, function()
bool = flag ReleaseTimer()
end)
end
function umoverect(unit, rect)
-- move unit to center of rect
SetUnitPosition(unit, GetRectCenterX(rect), GetRectCenterY(rect))
end
function umovexy(unit, x, y)
-- move unit to center of rect
SetUnitPosition(unit, x, y)
end
-- displays a red message followed by an error sound, or a green message with no sound if @_ispositive is passed as true
function palert(p, str, dur, _ispositive)
local dur = dur or 2.5
if p == localp() then
if _ispositive then
alert:new(color:wrap(color.tooltip.good, str), dur)
else
alert:new(color:wrap(color.tooltip.bad, str), dur)
playsound(kui.sound.error, p)
end
end
end
function palertall(str, dur, _soundsuppress)
playerloop(function(p) palert(p, str, dur, _soundsuppress) end)
end
function pname(p)
return GetPlayerName(p)
end
-- @func = run this for all players, but local only.
function looplocalp(func)
for p,_ in pairs(kobold.playing) do
if p == localp() then
func()
end
end
end
function textall(str)
DisplayTextToForce(GetPlayersAll(), str)
end
function unitx(taru)
return GetUnitX(taru)
end
function unity(taru)
return GetUnitY(taru)
end
function unitxy(taru)
return GetUnitX(taru), GetUnitY(taru)
end
function setxy(unit, x, y)
SetUnitX(unit,x) SetUnitY(unit,y)
end
function setheight(unit, z)
SetUnitFlyHeight(unit, z, 10000.0)
end
function face(unit, a)
SetUnitFacing(unit, a)
end
function faceunit(unit1, unit2)
SetUnitFacing(unit1, angleunits(unit1, unit2))
end
-- make both units face eachother.
function eachface(unit1, unit2)
SetUnitFacing(unit1, anglexy(unitx(unit1), unity(unit1), unitx(unit2), unity(unit2)))
SetUnitFacing(unit2, anglexy(unitx(unit2), unity(unit2), unitx(unit1), unity(unit1)))
end
function getface(unit)
return GetUnitFacing(unit)
end
function data(unit)
return GetUnitUserData(unit)
end
function spellxy()
return GetSpellTargetX(), GetSpellTargetY()
end
function rectxy(rect)
return GetRectCenterX(rect), GetRectCenterY(rect)
end
function rectx(rect)
return GetRectCenterX(rect)
end
function recty(rect)
return GetRectCenterY(rect)
end
function setcambounds(rect, _singlep)
if not _singlep then -- set for all players
for p,_ in pairs(kobold.playing) do
SetCameraBoundsToRectForPlayerBJ(p, rect)
end
else -- target a single player
SetCameraBoundsToRectForPlayerBJ(_singlep, rect)
end
end
function playerloop(func)
-- run an action on all player objects (make sure function has 'p' as arg e.g. 'myfunc(p [,...])')
for pnum = 1,kk.maxplayers do
local p = Player(pnum-1)
if kobold.player[p] then
func(p)
end
end
end
function moveallp(rect)
-- move all players to this rect center
playerloop(function(p) umoverect(kobold.player[p].unit, rect) end)
end
function moveallpxy(x,y)
-- move all players to this rect center
playerloop(function(p) umovexy(kobold.player[p].unit, x, y) port_yellowtp:play(x, y) end)
end
function panallp(x, y, _dur)
-- pan camera for all players to x,y.
local dur = _dur or 0
playerloop(function(p) panto(p, x, y, dur) end)
end
function timed(dur, func)
-- one shot timer
local t = NewTimer()
TimerStart(t, dur, false, function() func() ReleaseTimer() end)
return t
end
function timedrepeat(dur, count, func, _releasefunc)
local t, c, r = count, 0, _releasefunc or nil
if t == nil then
local tmr = TimerStart(NewTimer(), dur, true, function()
debugfunc(func,"timer") end)
return tmr
else
local tmr = TimerStart(NewTimer(), dur, true, function()
debugfunc(function()
func(c)
c = c + 1
if c >= t then
ReleaseTimer()
if r then r() end
end
end, "timer")
end)
return tmr
end
end
function newhotkey(p, oskey, func, islocal, ondown, meta)
local p, pnum = p, pnum(p)
local meta = meta or 0
local ondown = ondown or true
local islocal = islocal or true
if not hotkey_table[pnum] then
hotkey_table[pnum] = CreateTrigger()
end
BlzTriggerRegisterPlayerKeyEvent(hotkey_table[pnum], p, oskey, meta, ondown)
TriggerAddAction(hotkey_table[pnum], function()
if ishotkey(p, oskey) then
if islocal and p == GetLocalPlayer() then
func()
else
func()
end
end
end)
return trig
end
function ishotkey(p, oskey)
if p == GetLocalPlayer() then
if BlzGetTriggerPlayerKey() == oskey then
return true
else
return false
end
end
end
function disableuicontrol(bool)
looplocalp(function() EnableUserControl(not bool) EnableUserUI(not bool) end)
end
function mana(unit)
return GetUnitState(unit, UNIT_STATE_MANA)
end
function maxmana(unit)
return GetUnitState(unit, UNIT_STATE_MAX_MANA)
end
function life(unit)
return GetUnitState(unit, UNIT_STATE_LIFE)
end
function maxlife(unit)
return GetUnitState(unit, UNIT_STATE_MAX_LIFE)
end
function setlifep(unit, val)
-- 100-based.
SetUnitLifePercentBJ(unit, val)
end
function setmanap(unit, val)
-- 100-based.
SetUnitManaPercentBJ(unit, val)
end
function getlifep(unit)
-- 100-based.
return GetUnitLifePercent(unit)
end
function getmanap(unit)
-- 100-based.
return GetUnitManaPercent(unit)
end
-- @_owner = owner of the heal
function addlifep(unit, val, _arctextbool, _owner)
-- 100-based
local amt = maxlife(unit)*val/100
if _owner and kobold.player[powner(_owner)] then
local p = powner(_owner)
amt = amt*(1+kobold.player[p][p_stat_healing]/100)
kobold.player[p].score[4] = kobold.player[p].score[4] + amt
end
SetUnitState(unit, UNIT_STATE_LIFE, life(unit) + amt)
if _arctextbool and amt > 1.0 then
ArcingTextTag(color:wrap(color.tooltip.good, "+"..math.floor(amt)), unit, 2.0)
end
end
function addmanap(unit, val, _arctextbool)
-- 100-based
local amt = maxmana(unit)*val/100
SetUnitState(unit, UNIT_STATE_MANA, mana(unit) + amt)
if _arctextbool and amt > 1.0 then
ArcingTextTag(color:wrap(color.tooltip.alert, "+"..math.floor(amt)), unit, 2.0)
end
end
function addlifeval(unit, val, _arctextbool, _owner)
-- add a raw life value to a unit.
local val, p = val, powner(unit)
if _owner and kobold.player[powner(_owner)] then
local p = powner(_owner)
val = val*(1+kobold.player[p][p_stat_healing]/100)
kobold.player[p].score[4] = kobold.player[p].score[4] + val
end
SetUnitState(unit, UNIT_STATE_LIFE, life(unit) + val)
if _arctextbool and val > 1.0 then
ArcingTextTag(color:wrap(color.tooltip.good, "+"..math.floor(val)), unit, 2.0)
end
end
function addmanaval(unit, val, _arctextbool)
-- add a raw life value to a unit.
SetUnitState(unit, UNIT_STATE_MANA, RMaxBJ(mana(unit) + val))
if _arctextbool and val > 1.0 then
ArcingTextTag(color:wrap(color.tooltip.alert, "+"..math.floor(val)), unit, 2.0)
end
end
function setlifep(unit, val)
SetUnitState(unit, UNIT_STATE_LIFE, math.floor(GetUnitState(unit, UNIT_STATE_MAX_LIFE)*(val/100)))
end
function setmanap(unit, val)
SetUnitState(unit, UNIT_STATE_MANA, math.floor(GetUnitState(unit, UNIT_STATE_MAX_MANA)*(val/100)))
end
function restorestate(unit)
setlifep(unit, 100)
setmanap(unit, 100)
UnitRemoveBuffs(unit, true, true)
end
function setnewdesthp(dest, val)
SetDestructableMaxLife(dest, val)
SetDestructableLifePercentBJ(dest, 100)
end
function setnewunithp(unit, val, _resethp)
BlzSetUnitMaxHP(unit, math.floor(val))
if _resethp then
setlifep(unit, 100)
end
end
function setnewunitmana(unit, val, _donotreset)
BlzSetUnitMaxMana(unit, math.floor(val))
if not _donotreset and not map.manager.activemap then
setmanap(unit, 100)
end
end
function passivep()
return Player(PLAYER_NEUTRAL_PASSIVE)
end
function tempt()
local t = {}
weakt[t] = t
return t
end
function isalive(unit)
return not (GetUnitTypeId(unit) == 0 or IsUnitType(unit, UNIT_TYPE_DEAD) or GetUnitState(unit, UNIT_STATE_LIFE) < 0.405)
end
function ishero(unit)
return IsUnitType(unit, UNIT_TYPE_HERO)
end
function isancient(unit)
return IsUnitType(unit, UNIT_TYPE_ANCIENT)
end
function speffect(effect, x, y, _scale, _perm)
local e = AddSpecialEffect(effect, x, y)
BlzSetSpecialEffectScale(e, _scale or 1.0)
if not _perm then DestroyEffect(e) end
return e
end
function setinvul(unit, bool)
SetUnitInvulnerable(unit, bool)
end
function setinvis(unit, bool)
if bool then
UnitAddAbility(unit, FourCC('Apiv'))
else
UnitRemoveAbility(unit, FourCC('Apiv'))
end
end
function vertexhide(unit, bool)
if bool then
SetUnitVertexColor(unit, 255, 255, 255, 0)
else
SetUnitVertexColor(unit, 255, 255, 255, 255)
end
end
-- @x,y = reposition an effect.
function seteffectxy(effect, x, y, _z)
BlzSetSpecialEffectX(effect, x)
BlzSetSpecialEffectY(effect, y)
if _z then
BlzSetSpecialEffectZ(effect, _z)
end
end
function getcliffz(x, y, _height)
return GetTerrainCliffLevel(x, y)*128 - 256 + (_height or 0)
end
function dmgdestinrect(rect, dmg)
EnumDestructablesInRect(rect, nil, function()
if GetDestructableTypeId(GetEnumDestructable()) ~= b_bedrock.code then
SetWidgetLife(GetEnumDestructable(), GetWidgetLife(GetEnumDestructable()) - dmg)
end
end)
end
-- move a rect to @x,y then set its size to @w/@_h based on a radius value.
function setrectradius(rect, x, y, w, _h)
local w, h = w, _h or w
SetRect(rect, x - w, y - h, x + w, y + h)
end
function stunned(unit)
return UnitHasBuffBJ(unit, FourCC('BSTN')) or UnitHasBuffBJ(unit, FourCC('BPSE'))
end
function slowed(unit)
return UnitHasBuffBJ(unit, FourCC('Bslo')) or UnitHasBuffBJ(unit, FourCC('Bfro'))
end
function scaleatk(unit, val)
BlzSetUnitBaseDamage(unit, math.ceil( BlzGetUnitBaseDamage(unit, 0)*val), 0 )
end
function 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 debug then print("made new class for "..tostring(t)) end
end
-- :: clones a table and any child tables (setting metatables)
-- @t = table to copy
function 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 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 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))
local tmr
-- show:
if bool then
BlzFrameSetVisible(fh, true)
BlzFrameSetAlpha(fh, 255)
tmr = 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)
tmr = 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
return tmr
end
function 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 speechindicator(unit)
UnitAddIndicatorBJ(unit, 100, 100, 0.00, 0)
end
-- for index-value table, get a random value.
function trandom(t)
return t[math.random(1,#t)]
end
-- requires v1 to be a negative number
function math.randomneg(v1,v2)
if v1 >= 0 then print("math.randomneg only supports negative first argument") return end
return math.random(0, v2+(-1*v1)) - v1
end
function isinvul(u)
return BlzIsUnitInvulnerable(u)
end
function timedlife(u, dur)
UnitApplyTimedLife(u, FourCC('BTLF'), dur)
end
-- order all units of @player within @radius of @x,y to attack move to position of @unit
function issueatkmoveall(player, radius, x, y, unit)
local grp = g:newbyxy(player, g_type_ally, x, y, radius)
grp:action(function() if not isancient(grp.unit) then issatkxy(grp.unit, unitxy(unit)) end end)
grp:destroy()
end
function shakecam(p, dur, _mag)
CameraSetEQNoiseForPlayer(p, _mag or 3.0)
timed(dur, function() CameraClearNoiseForPlayer(p) end)
end
function booltobit(value)
if value == true then return 1 else return 0 end
end
function purgetrailing(str, char)
local str = str
if string.sub(str, -1) == char then str = string.sub(str,1,-2) end -- purge trailing character (last char).
return str
end
function tablesize(t)
local count = 0
for i,v in pairs(t) do
count = count + 1
end
return count
end
kkcreds = {
["Hearthstone Interface Assets"] = "©2014 Blizzard Entertainment, Inc. All rights reserved. Heroes of Warcraft is a trademark, and Hearthstone is a registered trademark of Blizzard Entertainment, Inc. in the U.S. and/or other countries.",
["World of Warcraft Interface Assets"] = "©2004 Blizzard Entertainment, Inc. All rights reserved. World of Warcraft, Warcraft and Blizzard Entertainment are trademarks or registered trademarks of Blizzard Entertainment, Inc. in the U.S. and/or other countries.",
["Open Source RPG Icons"] = "Ravenmore @ https://opengameart.org/",
["Hero Glow"] = "assasin_lord",
["Gold Ore, Silver Ore, Iron Ore"] = "Ergius",
["Pixel to DPI"] = "Quilnez/Kazeon",
["Devilsaur Pack, Genesaur"] = "Explobomb",
["Magnaron"] = "Maldiran",
["Confirmation"] = "Epsilon",
["Ground Explosion, Explosion Big, Sparks Explosion, Dirt Explosion"] = "WILL THE ALMIGHTY",
["Generic Canister, Orb Elements, Item Powerups"] = "General Frank",
["Lightning Strike, Blue/Green Fungus, Ratorg Rider, Void Lord"] = "Callahan",
["Orcish Runes"] = "frostwhisper",
["Purified Maw Giant"] = "Draconian",
["Naga Lord Pack"] = "Tier10Trash",
["Naga Brute Pack"] = "BardBord",
["Running Flame Aura"] = "sPy",
["Magic Shield"] = "JesusHipster",
["Simple White Bag"] = "Captain Bacon, ILH",
["Cannon Turret"] = "arnelbigstonepd33r",
["Troll Den, Gold Pile"] = "HerrDave",
["Global Init, Timer Utils"] = "Bribe",
["Arcing Text Tag"] = "Maker",
["Sleet Storm"] = "Hate",
["Kobold Slavemaster, Kobold Basher"] = "-Grendel",
["HQ Gold Chest"] = "HammerFist132",
["Kobold Stalker"] = "levigeorge1617",
["Kobold Hero"] = "Direfury",
["Rune Golem"] = "Alpain",
["Sentinel Shrine"] = "xYours Trulyx",
["Icon Model Packs"] = "Kenathorn",
["Bless"] = "Thrikodius",
["Candle"] = "bisnar13",
["Morgul Blade"] = "Uncle Fester",
["Nature Staff"] = "Blood Raven",
["Akama's Kama"] = "Systemfre1",
["Handheld Lantern"] = "smithyjones",
["Backpack"] = "dioris",
["Void Missiles, Void Rifts, Void Teleport, Fire Missiles, Conflagrate, Missile I, Missile II, Shot I, Shot II, Malevolence Aura, Pillar of Flame, Rain of Fire II, Judgement, "
.."Ember, Storm Bolt, Soul Beam, Soul Armor, Runic Rocket, Singularity I, Singularity II, Righteous Guard, Uber Shield, Climax, Winder Blast, Blink I, Blizzard II, Bondage, "
.."Flamestrike, Eldritch Scroll, Chain Grenade, Carrion Bats, Burning Rage, Athelas, Armor Pen/Stimulus, Firebrand Shots, Frostcraft Crystal, Psionic Shot, Heaven's Gate "
.."Heal, Ice Shard, Sacred Storm, Shining Flare, Soul Armor, Soul Beam, Soul Discharge, Stormfall, Valiant Charge, Shock Blast, Windwalk, Reaper Claws, Starfall Empire"] = "Mythic",
}
--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
-- Arcing Text Tag v1.0.0.3 by Maker encoded to Lua
DEFINITION = 1.0/60.0
SIZE_MIN = 0.012 -- Minimum size of text
SIZE_BONUS = 0.006 -- Text size increase
TIME_LIFE = 1.0 -- How long the text lasts
TIME_FADE = 1.0 -- When does the text start to fade
Z_OFFSET = -15 -- Height above unit
Z_OFFSET_BON = 25 -- How much extra height the text gains
VELOCITY = 1.0 -- How fast the text move in x/y plane
TMR = CreateTimer()
ANGLE_RND = true -- Is the angle random or fixed
if not ANGLE_RND then
ANGLE = bj_PI/2.0 -- If fixed, specify the Movement angle of the text.
end
tt = {}
as = {} -- angle, sin component
ac = {} -- angle, cos component
ah = {} -- arc height
t = {} -- time
x = {} -- origin x
y = {} -- origin y
str = {} -- text
ic = 0 -- Instance count
rn = {} ; rn[0] = 0
next = {} ; next[0] = 0
prev = {} ; prev[0] = 0 --Needed due to Lua not initializing them.
function ArcingTextTag(s, u)
local this = rn[0]
if this == 0 then
ic = ic + 1
this = ic
else
rn[0] = rn[this]
end
next[this] = 0
prev[this] = prev[0]
next[prev[0]] = this
prev[0] = this
str[this] = s
x[this] = GetUnitX(u)
y[this] = GetUnitY(u)
t[this] = TIME_LIFE
local a
if ANGLE_RND then
a = GetRandomReal(0, 2*bj_PI)
else
a = ANGLE
end
as[this] = Sin(a)*VELOCITY
ac[this] = Cos(a)*VELOCITY
ah[this] = 0.
if IsUnitVisible(u, GetLocalPlayer()) then
tt[this] = CreateTextTag()
SetTextTagPermanent(tt[this], false)
SetTextTagLifespan(tt[this], TIME_LIFE)
SetTextTagFadepoint(tt[this], TIME_FADE)
SetTextTagText(tt[this], s, SIZE_MIN)
SetTextTagPos(tt[this], x[this], y[this], Z_OFFSET)
end
if prev[this] == 0 then
TimerStart(TMR, DEFINITION, true, function()
local this = next[0]
local p
while (this ~= 0) do
p = Sin(bj_PI*t[this])
t[this] = t[this] - DEFINITION
x[this] = x[this] + ac[this]
y[this] = y[this] + as[this]
SetTextTagPos(tt[this], x[this], y[this], Z_OFFSET + Z_OFFSET_BON * p)
SetTextTagText(tt[this], str[this], SIZE_MIN + SIZE_BONUS * p)
if t[this] <= 0.0 then
tt[this] = null
next[prev[this]] = next[this]
prev[next[this]] = prev[this]
rn[this] = rn[0]
rn[0] = this
if next[0] == 0 then
PauseTimer(TMR)
end
end
this = next[this]
end
end)
end
return this
end
-- TriggerRegisterVariableEvent hook to convert these old school events into something more useful.
do
events = {}
function onRegisterVar(func)
events[func] = func
end
local oldEvent = TriggerRegisterVariableEvent
function TriggerRegisterVariableEvent(trig, var, op, val)
for k, func in pairs(events) do
if func(trig, var, val) then return end
end
oldEvent(trig, var, op, val)
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
if whichTimer then
local dat = data[whichTimer] or nil
if data[whichTimer] then
data[whichTimer] = nil
end
PauseTimer(whichTimer)
DestroyTimer(whichTimer)
return dat
end
end
end
speffect = {
scale = 1.0,
effect = '',
timed = false,
dur = 3.0,
death = true,
point = 'origin',
loadx = 18000,
loady = 17000,
stack = {},
vertex = { 255, 255, 255 },
}
function speffect:new(effectstring, scale, point, _vertext)
-- create a canned speffect that can be referenced over and over.
o = {}
setmetatable(o, self)
o.effect = effectstring
o.scale = scale or self.scale
o.point = point
o.vertex = _vertext or nil
-- capture preload needs in one spot:
self.__index = self
speffect.stack[#speffect.stack+1] = o
return o
end
function speffect:preload()
-- stagger to prevent any chance of desync from competing data overload:
for i = 1,#self.stack do
Preload(speffect.stack[i].effect)
speffect.stack[i]:play(speffect.loadx, speffect.loady)
end
end
-- @x,y = play canned effect at this location (e.g. myeffect:play(x,y) )
-- [@_dur] = destroy after this many seconds.
function speffect:play(x, y, _dur, _scale)
local e = AddSpecialEffect(self.effect, x, y)
local s = _scale or self.scale
if self.vertex then
BlzSetSpecialEffectColor(e, self.vertex[1], self.vertex[2], self.vertex[3])
end
BlzSetSpecialEffectScale(e, s)
if _dur then
TimerStart(NewTimer(), _dur, false, function()
self:removalcheck(e)
DestroyEffect(e)
ReleaseTimer()
end)
return e
elseif self.timed then
TimerStart(NewTimer(), self.dur, false, function()
self:removalcheck(e)
DestroyEffect(e)
ReleaseTimer()
end)
return e
else
DestroyEffect(e)
end
end
-- @x,y = create a non-obj canned effect at this location.
function speffect:create(x, y, _scale)
local e = AddSpecialEffect(self.effect, x, y)
if _scale then
BlzSetSpecialEffectScale(e, _scale)
end
if self.vertex then
BlzSetSpecialEffectColor(e, self.vertex[1], self.vertex[2], self.vertex[3])
end
return e
end
-- @unit = play at unit x,y.
function speffect:playu(unit)
self:play(GetUnitX(unit), GetUnitY(unit))
end
-- @unit = attach to this unit.
-- @dur = [optional] give it a duration.
-- @point = [optional] override the special effect point.
-- @scale = [optional] set new scale (e.g. 1.75).
-- @returns special effect
function speffect:attachu(unit, dur, point, scale)
local attach = point or self.point
local e = AddSpecialEffectTarget(self.effect, unit, attach)
if scale then BlzSetSpecialEffectScale(e, scale) end
if dur then
TimerStart(NewTimer(), dur, false, function()
utils.debugfunc(function()
if e then
self:removalcheck(e, attach)
end
end, "effect timer")
DestroyEffect(e) ReleaseTimer()
end)
end
return e
end
-- if effect has no death anim, hide it instantly to prevent lingering.
function speffect:removalcheck(e, _isattach)
-- *NOTE: in 1.32 game *crashes* when Z is set for attached effects.
if not self.death then
utils.playerloop(function(p)
if p == utils.localp() then
BlzSetSpecialEffectScale(e, 0.01)
BlzSetSpecialEffectAlpha(e, 0)
if not _isattach then
BlzSetSpecialEffectZ(e, 3500.0)
end
end
end)
end
end
function speffect:addz(e, z)
if utils.localp() then
BlzSetSpecialEffectZ(e, BlzGetLocalSpecialEffectZ(e) + z)
end
end
-- @d = diameter to play in (circular)
-- @c = number of effects to play in @d
-- @x,y = location to play at
function speffect:playradius(d, c, x, y, _dur)
local r = math.floor(d/2) -- convert to radius
local a = math.random(0,360) -- starting angle
local inc = 360/c -- angle increment
local x2,y2 -- effect point
for i = 1,c do
x2,y2 = utils.projectxy(x, y, r, a)
a = a + inc
self:play(x2, y2, _dur or nil)
end
end
do
ctm = {}
ctm.sys = {}
function ctm.sys:create()
ctm.__index = ctm
ctm.sys.__index = ctm.sys
-- primary config:
ctm.enabled = true -- manually disable system.
ctm.debug = false -- print debug info.
ctm.min_player_number = 1 -- max player number to enable system for.
ctm.max_player_number = 3 -- `` min player number.
-- options:
ctm.exclusion_radius = 48. -- radius around the unit where movement is prevented (stops in-place jittering and infinite run).
ctm.pathing_block_radius = 64. -- how far to check for pathability (prevents jittering near pathing blockers)
ctm.quick_turn_angle = 30. -- change in angle at which a move update is forced when click move is held.
ctm.tmr_cadence = 0.03 -- drag detection update frequency.
ctm.move_update_pause_ticks = 8 -- the minimum timer ticks before a new move update can be issued (while mouse held down).
ctm.move_pause_drag_ticks = 14 -- prevents drag mechanics when a non-move order is fired.
-- temporary variables:
ctm.calc_x = 0.
ctm.calc_y = 0.
ctm.calc_a = 0.
ctm.id = 0
-- player current mouse x,y:
ctm.mouse_x = {}
ctm.mouse_y = {}
-- player previous x,y during mouse held:
ctm.mouse_prev_x = {}
ctm.mouse_prev_y = {}
ctm.mouse_prev_angle = {}
-- player hero unit:
ctm.unit = {}
ctm.unit_x = {}
ctm.unit_y = {}
ctm.unit_angle = {}
-- mouse fields:
ctm.mouse_btn_is_down = {}
ctm.mouse_being_dragged = {}
ctm.update_allowed = {}
ctm.update_ticks = {}
ctm.drag_allowed = {}
ctm.drag_ticks = {}
ctm.stop_next_mouse_up = {}
ctm.tmr = CreateTimer()
-- initialize mouse triggers:
ctm.mouse_up_trig = CreateTrigger()
ctm.mouse_down_trig = CreateTrigger()
ctm.mouse_drag_trig = CreateTrigger()
for n = ctm.min_player_number, ctm.max_player_number do
local p = Player(n-1)
ctm.unit[n] = udg_click_move_unit[n] -- controlled in GUI init.
ctm.drag_allowed[n] = true
ctm.update_ticks[n] = 0
ctm.drag_ticks[n] = 0
ctm.mouse_prev_angle[n] = getface(ctm.unit[n])
TriggerRegisterPlayerEvent(ctm.mouse_up_trig, p, EVENT_PLAYER_MOUSE_UP)
TriggerRegisterPlayerEvent(ctm.mouse_down_trig, p, EVENT_PLAYER_MOUSE_DOWN)
TriggerRegisterPlayerEvent(ctm.mouse_drag_trig, p, EVENT_PLAYER_MOUSE_MOVE)
trg:new("order", Player(n-1)):regaction(function()
ctm.id = GetIssuedOrderId()
if ctm.id ~= 851986 then
ctm.sys:pause_mouse_drag(n)
ctm.stop_next_mouse_up[n] = true
else
ctm.sys:allow_drag_update(n)
end
end)
trg:new("startcast", Player(n-1)):regaction(function()
ctm.sys:pause_move_update(n, ctm.move_pause_drag_ticks)
end)
end
TriggerAddAction(ctm.mouse_up_trig, function() debugfunc(function() ctm.sys:mouse_up(trigpnum()) end, "mouse up") end)
TriggerAddAction(ctm.mouse_down_trig, function() debugfunc(function() ctm.sys:mouse_down(trigpnum()) end, "mouse down") end)
TriggerAddAction(ctm.mouse_drag_trig, function() debugfunc(function() ctm.sys:mouse_drag(trigpnum()) end, "mouse move") end)
ctm.sys:update_timer(n)
end
function ctm.sys:update_timer(n)
TimerStart(ctm.tmr, ctm.tmr_cadence, true, function() debugfunc(function()
if ctm.enabled then
for n = ctm.min_player_number, ctm.max_player_number do
-- mouse held down:
if ctm.mouse_btn_is_down[n] and not kobold.player[Player(n-1)].downed then
-- check if updates are allowed:
ctm.sys:update_timer_ticks(n)
-- mouse auto click to move:
if ctm.drag_allowed[n] then
ctm.sys:get_previous_mouse_xy(n)
if ctm.sys:is_mouse_within_exclusion_radius(n) then
-- mouse is within exclusion radius, stop:
IssueImmediateOrderById(ctm.unit[n], 851972)
elseif ctm.update_allowed[n] and ctm.sys:is_mouse_held_in_same_xy(n) then
-- mouse is held down in same x/y:
ctm.mouse_being_dragged[n] = false
ctm.calc_x, ctm.calc_y = ctm.sys:get_current_unit_xy(n)
ctm.calc_x, ctm.calc_y = projectxy(ctm.calc_x, ctm.calc_y, 256.0, ctm.unit_angle[n])
ctm.sys:issue_movement_update(n, ctm.calc_x, ctm.calc_y)
ctm.sys:pause_move_update(n)
-- fake where the mouse position is:
ctm.mouse_x[n], ctm.mouse_y[n] = ctm.calc_x, ctm.calc_y
else
-- mouse moved to new x/y:
ctm.mouse_being_dragged[n] = true
if ctm.sys:is_unit_eligible_for_quick_turn(n) then
-- if unit to mouse angle greatly changes, force a move update for responsive feeling:
ctm.sys:issue_movement_update(n)
elseif ctm.update_allowed[n] and ctm.sys:get_dist_from_prev_xy(n) >= ctm.exclusion_radius then
-- mouse is at a new x,y and a move command was not recently given, update move:
ctm.sys:issue_movement_update(n)
ctm.sys:pause_move_update(n)
end
end
end
end
end
end
if ctm.debug then
ClearTextMessages()
print("mouse btn down? = ", ctm.mouse_btn_is_down[1])
print("mouse dragged? = ", ctm.mouse_being_dragged[1])
print("drag allowed? = ", ctm.drag_allowed[1])
print("drag ticks = ", ctm.drag_ticks[1])
print("update allowed? = ", ctm.update_allowed[1])
print("update ticks = ", ctm.update_ticks[1])
print("mouse x = ", ctm.mouse_x[1])
print("mouse y = ", ctm.mouse_y[1])
print("mouse prev x = ", ctm.mouse_prev_x[1])
print("mouse prev y = ", ctm.mouse_prev_y[1])
print("unit x = ", ctm.unit_x[1])
print("unit y = ", ctm.unit_y[1])
end
end, "update_timer") end)
end
function ctm.sys:update_timer_ticks(n)
if ctm.update_ticks[n] > 0 then
ctm.update_ticks[n] = ctm.update_ticks[n] - 1
else
ctm.sys:allow_move_update(n)
end
if ctm.drag_ticks[n] > 0 then
ctm.drag_ticks[n] = ctm.drag_ticks[n] - 1
else
ctm.sys:allow_drag_update(n)
end
end
function ctm.sys:issue_movement_update(n, _x, _y)
SetUnitFacingTimed(ctm.unit[n], anglexy(unitx(ctm.unit[n]), unity(ctm.unit[n]), _x or ctm.mouse_x[n], _y or ctm.mouse_y[n]), 0.0)
issmovexy(ctm.unit[n], _x or ctm.mouse_x[n], _y or ctm.mouse_y[n])
end
function ctm.sys:get_current_mouse_xy(n)
ctm.mouse_x[n], ctm.mouse_y[n] = BlzGetTriggerPlayerMouseX(), BlzGetTriggerPlayerMouseY()
return ctm.mouse_x[n], ctm.mouse_y[n]
end
function ctm.sys:get_current_unit_xy(n)
ctm.unit_x[n], ctm.unit_y[n] = unitxy(ctm.unit[n])
return ctm.unit_x[n], ctm.unit_y[n]
end
function ctm.sys:get_dist_from_mouse_to_unit(n)
return distxy(ctm.mouse_x[n], ctm.mouse_y[n], ctm.sys:get_current_unit_xy(n))
end
function ctm.sys:get_dist_from_prev_xy(n)
return distxy(ctm.mouse_x[n], ctm.mouse_y[n], ctm.mouse_prevx, ctm.mouse_prevy)
end
function ctm.sys:get_angle_from_unit_to_mouse(n)
ctm.unit_angle[n] = anglexy(unitx(ctm.unit[n]), unity(ctm.unit[n]), ctm.mouse_x[n], ctm.mouse_y[n])
return ctm.unit_angle[n]
end
function ctm.sys:get_previous_mouse_xy(n)
ctm.mouse_prev_x[n], ctm.mouse_prev_y[n] = ctm.mouse_x[n], ctm.mouse_y[n]
return ctm.mouse_prev_x[n], ctm.mouse_prev_y[n]
end
function ctm.sys:cmod(a, n)
return a - math.floor(a/n) * n
end
function ctm.sys:delta_angle(a1, a2)
return math.abs(ctm.sys:cmod((a1 - a2) + 180, 360) - 180)
end
function ctm.sys:is_unit_eligible_for_quick_turn(n)
-- check if within quick turn angle:
if ctm.sys:delta_angle(ctm.sys:get_angle_from_unit_to_mouse(n), getface(ctm.unit[n])) > ctm.quick_turn_angle then
-- check if a turn was recently submitted (prevents spamming move orders):
if ctm.sys:delta_angle(ctm.mouse_prev_angle[n], ctm.unit_angle[n]) > 3. then
ctm.mouse_prev_angle[n] = ctm.unit_angle[n]
return true
end
end
return false
end
function ctm.sys:is_mouse_held_in_same_xy(n)
return (ctm.mouse_prev_x[n] == ctm.mouse_x[n] and ctm.mouse_prev_y[n] == ctm.mouse_y[n])
end
function ctm.sys:is_mouse_offset_pathable(n)
-- prototype function, not in use
ctm.calc_a = anglexy(unitx(ctm.unit[n]), unity(ctm.unit[n]), ctm.mouse_x[n], ctm.mouse_y[n])
ctm.calc_x, ctm.calc_y = projectxy(unitx(ctm.unit[n]), unity(ctm.unit[n]), ctm.pathing_block_radius, ctm.calc_a)
return not IsTerrainPathable(ctm.calc_x, ctm.calc_y, PATHING_TYPE_WALKABILITY)
end
function ctm.sys:is_mouse_within_exclusion_radius(n)
return distxy(ctm.mouse_x[n], ctm.mouse_y[n], unitxy(ctm.unit[n])) < ctm.exclusion_radius
end
function ctm.sys:mouse_down(n)
if ctm.enabled and ctm.sys:is_mouse_btn(1) then
ctm.mouse_btn_is_down[n] = true
ctm.sys:get_current_mouse_xy(n)
ctm.sys:get_angle_from_unit_to_mouse(n)
-- to prevent a fast click from triggering a false drag, temporarily pause updates:
ctm.sys:pause_move_update(n, ctm.move_cmd_order_ticks)
else
ctm.mouse_btn_is_down[n] = false
end
end
function ctm.sys:mouse_up(n)
if ctm.sys:is_mouse_btn(1) then
ctm.mouse_btn_is_down[n] = false
if not ctm.stop_next_mouse_up[n] then
if ctm.enabled and ctm.mouse_being_dragged[n] then
ctm.sys:get_current_mouse_xy(n)
ctm.sys:get_angle_from_unit_to_mouse(n)
ctm.sys:issue_movement_update(n)
ctm.sys:allow_move_update(n)
end
ctm.mouse_being_dragged[n] = false
else
ctm.stop_next_mouse_up[n] = false
end
end
end
function ctm.sys:mouse_drag(n)
if ctm.enabled and ctm.mouse_btn_is_down[n] then
ctm.sys:get_current_mouse_xy(n)
ctm.sys:get_angle_from_unit_to_mouse(n)
end
end
function ctm.sys:pause_move_update(n, _ticks)
ctm.update_allowed[n] = false
ctm.update_ticks[n] = _ticks or ctm.move_update_pause_ticks
end
function ctm.sys:pause_mouse_drag(n, _ticks)
ctm.drag_allowed[n] = false
ctm.drag_ticks[n] = _ticks or ctm.move_pause_drag_ticks
end
function ctm.sys:allow_move_update(n)
ctm.update_allowed[n] = true
ctm.update_ticks[n] = 0
end
function ctm.sys:allow_drag_update(n)
ctm.drag_allowed[n] = true
ctm.drag_ticks[n] = 0
end
function ctm.sys:is_mouse_btn(bit_is_right_btn)
if bit_is_right_btn == 0 then
return BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_LEFT
elseif bit_is_right_btn == 1 then
return BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_RIGHT
else
print("error: is_mouse_btn 'bit_is_right_btn' should be 0 or 1")
end
end
end
utils = {} -- class.
utils.weakt = {} -- table for storing weak tables to gc (temp tables).
utils.debug = false
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."
utils.hotkeyt = {} -- store player hotkey trigs.
setmetatable(utils.weakt, { __mode = 'k' })
function utils.init()
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, }
end
-- fetch top left corner of a rect (how grid layout is determined).
function utils.getrectcorner(rect)
return GetRectMinX(rect), GetRectMaxY(rect)
end
function utils.xpcall( func )
local status = xpcall( func, utils.printerror )
-- print(status)
end
function utils.fixfocus(fh)
BlzFrameSetEnable(fh, false)
BlzFrameSetEnable(fh, true)
end
function utils.printerror( err )
print( "error:", err )
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
-- global debug options:
-- print("debug:"..tostring(name))
end
function commaformat(amount)
local formatted = tostring(amount)
local k
while true do
formatted, k = string.gsub(formatted, "^(-?\x25d+)(\x25d\x25d\x25d)", '\x251,\x252')
if (k==0) then
break
end
end
return formatted
end
-- :: helper function to check if table has index value.
-- @t = table to search.
-- @v = value to search for, returns true if it exists; false if not.
function utils.tableindexof(t,v)
for index, value in pairs(t) do
if index == v then
return index
end
end
return false
end
function utils.removefromtable(t,val)
for i,v in pairs(t) do
if v == val then
table.remove(t, i)
end
end
end
-- :: count number of tables within a table.
-- @t = table to search; returns number of tables.
function utils.counttables(t)
local count = 0
for t in pairs(t) do
if type(t) == "table" then
count = count +1
end
end
return count
end
function utils.tablelength(t)
local count = 0
for _ in pairs(t) do count = count + 1 end
return count
end
-- :: helper function to see if table has value
-- @t = table to search
-- @v = value to search for, returns true if it exists; false if not
function utils.tablehasvalue(t,v)
for index, value in pairs(t) do
if value == v then
return true
end
end
return false
end
function utils.tablehasindex(t,v)
for index, value in pairs(t) do
if index == v then
return true
end
end
return false
end
-- :: helper function to remove table index by searching for value
-- @t = table to search
-- @v = value to search for, returns index of that value where it exists in @t, returns false if it doesn't exist
function utils.tablevalueindex(t,v)
for index, value in pairs(t) do
if value == v then
return index
end
end
return nil
end
-- :: helper function to collapse a table or array (remove empty spaces) e.g. [0, nil, 2] becomes [0,2]
-- @t = table to collapse
function utils.tablecollapse(t)
for index, value in pairs(t) do
if value == nil then
table.remove(t, index)
end
end
end
function utils.tabledeleteifempty(t)
for index, value in pairs(t) do
if value ~= nil then
return false
end
end
t = nil
return true
end
function utils.distxy(x1,y1,x2,y2)
local dx = x2 - x1
local dy = y2 - y1
return SquareRoot(dx * dx + dy * dy)
end
function utils.distanglexy(x1,y1,x2,y2)
return utils.distxy(x1,y1,x2,y2), utils.anglexy(x1,y1,x2,y2)
end
function utils.distunits(unit1,unit2)
return utils.distxy(utils.unitx(unit1), utils.unity(unit1), utils.unitx(unit2), utils.unity(unit2))
end
-- :: (alternative name that always causes headaches, so make it work) get facing angle from A to B
-- @x1,y1 = point A (facing from)
-- @x2,y2 = point B (facing towards)
function utils.anglexy(x1,y1,x2,y2)
return bj_RADTODEG * Atan2(y2 - y1, x2 - x1)
end
function utils.angleunits(unit1, unit2)
return bj_RADTODEG * Atan2(utils.unity(unit2) - utils.unity(unit1), utils.unitx(unit2) - utils.unitx(unit1))
end
-- :: PolarProjectionBJ converted to x,y
-- @x1,y1 = origin coord
-- @d = distance between origin and direction
-- @a = angle to project
function utils.projectxy(x1,y1,d,a)
local x = x1 + d * Cos(a * bj_DEGTORAD)
local y = y1 + d * Sin(a * bj_DEGTORAD)
return x,y
end
function utils.unitprojectxy(unit,d,a)
local x = utils.unitx(unit) + d * Cos(a * bj_DEGTORAD)
local y = utils.unity(unit) + d * Sin(a * bj_DEGTORAD)
return x,y
end
-- :: clones a table
-- @t = table to copy
function utils.shallow_copy(t)
local t2 = {}
for k,v in pairs(t) do
t2[k] = v
end
return t2
end
-- :: clones a table and any child tables
-- @t = table to copy
function utils.deep_copy(t)
if type(t) == "table" then
local t2 = {}
for k,v in pairs(t) do
if type(v) == "table" then
t2[k] = utils.deep_copy(v)
if getmetatable(v) then
setmetatable(t2[k], getmetatable(v))
end
else
t2[k] = v
end
end
if getmetatable(t) then
setmetatable(t2, getmetatable(t))
end
return t2
else
return t
end
end
-- safest destroy loop.
function utils.shallow_destroy(t)
for k,v in pairs(t) do
v = nil
k = nil
end
t = nil
end
-- :: loop through a parent table to find child tables, and delete them.
-- *note: will overflow the stack if used on tables that have self references.
-- @t = search this table.
function utils.deep_destroy(t)
if t and type(t) == "table" then
for k,v in pairs(t) do
if k and v and type(v) == "table" then
utils.deep_destroy(v)
else
v = nil
end
end
elseif t then
t = nil
end
end
-- :: get the length of a table.
-- @t = table to measure
function utils.tablelength(t)
local count = 0
for _ in pairs(t) do count = count + 1 end
return count
end
-- :: round a number to a decimal place.
-- @num = round this number
-- @numdecplaces = to this many decimal places (use 0 for a whole number)
function utils.round(num, numdecplaces)
local mult = 10^numDecimalPlaces
return math.floor(num * mult + 0.5) / mult
end
-- :: shorthand for getting a player number
function utils.pid(player)
return GetConvertedPlayerId(player)
end
function utils.powner(unit)
return GetOwningPlayer(unit)
end
-- :: shorthand for getting a player number based on a unit
function utils.pidunit(unit)
return GetConvertedPlayerId(GetOwningPlayer(unit))
end
-- @snd = sound to play
-- @p = [optional] for this player (defaults to trig player)
function utils.soundstop(snd, p)
if not p then p = GetTriggerPlayer() end
if GetLocalPlayer() == p then
StopSound(snd, false, false)
end
end
function utils.triggeron(trig, bool)
if bool then
EnableTrigger(trig)
else
DisableTrigger(trig)
end
end
function utils.localp()
return GetLocalPlayer()
end
function utils.islocalp(p)
return GetLocalPlayer() == p
end
function utils.getp(pnum)
return Player(pnum-1)
end
function utils.pnum(p)
return GetConvertedPlayerId(p)
end
function utils.trigpnum()
return GetConvertedPlayerId(GetTriggerPlayer())
end
function utils.unitp()
return GetOwningPlayer(GetTriggerUnit())
end
function utils.trigp()
return GetTriggerPlayer()
end
function utils.trigu()
return GetTriggerUnit()
end
function utils.killu()
return GetKillingUnit()
end
function utils.triguid()
return GetUnitTypeId(utils.trigu())
end
function utils.islocaltrigp()
return GetLocalPlayer() == GetTriggerPlayer()
end
function utils.playsound(snd, p)
local p = p or GetTriggerPlayer()
if utils.localp() == p then
StopSound(snd, false, false)
StartSound(snd)
end
end
function utils.playsoundall(snd)
utils.looplocalp(function()
StopSound(snd, false, false)
StartSound(snd)
end)
end
function utils.stopsoundall(snd)
utils.looplocalp(function()
StopSound(snd, false, false)
end)
end
-- @rect = rect to get center of.
-- @p = owning player
-- @id = unit raw code to create.
function utils.unitinrect(rect, p, id)
-- try to capture most use cases with a short function.
return CreateUnit(p, FourCC(id), GetRectCenterX(rect), GetRectCenterY(rect), 255.0)
end
function utils.unitatxy(p, x, y, id, _face)
return CreateUnit(p, FourCC(id), x, y, _face or 270.0)
end
-- @p = this player
-- @x,y = coords.
-- @dur = [optional] over seconds.
function utils.panto(p, x, y, dur)
-- map camera to x,y instantly
local dur = dur or 0.0
PanCameraToTimedForPlayer(p, x, y, dur)
end
-- @p = this player.
-- @unit = this unit.
-- @dur = [optional] over seconds.
function utils.pantounit(p, unit, dur)
-- map camera to x,y instantly
local dur = dur or 0.0
PanCameraToTimedForPlayer(p, GetUnitX(unit), GetUnitY(unit), dur)
end
-- @p = this player.
-- @r = this rect center.
-- @dur = [optional] over seconds.
function utils.pantorect(p, r, dur)
-- map camera to x,y instantly
local dur = dur or 0.0
PanCameraToTimedForPlayer(p, GetRectCenterX(r), GetRectCenterY(r), dur)
end
function utils.fadeblack(bool, _dur)
-- turn screen black
utils.looplocalp(function()
if bool then
CinematicFadeBJ( bj_CINEFADETYPE_FADEOUT, _dur or 0.0, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
else
CinematicFadeBJ( bj_CINEFADETYPE_FADEIN, _dur or 0.0, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
end
end)
end
-- @unit = follow this unit
-- @p = for this player
function utils.camlockunit(unit, p)
SetCameraTargetControllerNoZForPlayer(p, unit, 0, 0, false)
end
-- @unit = select this unit
-- @p = for this player
function utils.selectu(unit, p)
if utils.localp() == p then
if not IsUnitSelected(unit, p) then
SelectUnitForPlayerSingle(unit, p)
end
SetCameraFieldForPlayer(p, CAMERA_FIELD_TARGET_DISTANCE, 2200.0, 0)
SetCameraFieldForPlayer(p, CAMERA_FIELD_ANGLE_OF_ATTACK, 304.0, 0)
SetCameraFieldForPlayer(p, CAMERA_FIELD_ZOFFSET, 75.0, 0)
end
end
-- @unit = select this unit
-- @p = for this player, every 0.03 sec
-- @t = store the timer on this table as .permtimer
function utils.permselect(unit, p, t)
local unit,p = unit,p
local func = function()
if not screenplay.pause_camera then
utils.pantounit(p, unit, 0.23)
utils.selectu(unit, p)
end
end
t.permtimer = NewTimer()
TimerStart(t.permtimer, 0.03, true, func)
SetTimerData(t.permtimer, func)
end
function utils.temphide(unit, bool)
-- temporarily remove a unit (but maintaining their location presence).
SetUnitPathing(unit, not bool)
PauseUnit(unit, bool)
utils.setinvul(unit, bool)
utils.vertexhide(unit, bool)
end
function utils.stop(unit)
-- order a unit to stop
IssueImmediateOrderById(unit, 851972)
end
function utils.issmoverect(unit, rect)
IssuePointOrder(unit, 'move', GetRectCenterX(rect), GetRectCenterY(gg_rct_expeditionExit))
end
function utils.issmovexy(unit, x, y)
IssuePointOrder(unit, 'move', x, y)
end
function utils.smartmove(unit, target)
IssueTargetOrderById(unit, 851971, target)
end
function utils.issatkxy(unit, x, y)
IssuePointOrderById(unit, 851983, x, y)
end
function utils.issatkunit(unit, target)
IssueTargetOrderById(unit, 851983, target)
end
-- @bool = var to switch.
-- @dur = after x sec.
-- @flag = timer switch result (true or false)
function utils.booltimer(bool, dur, flag)
-- a simple flag switch timer
bool = not flag
TimerStart(NewTimer(), dur, false, function()
bool = flag ReleaseTimer()
end)
end
function utils.umoverect(unit, rect)
-- move unit to center of rect
SetUnitPosition(unit, GetRectCenterX(rect), GetRectCenterY(rect))
end
function utils.umovexy(unit, x, y)
-- move unit to center of rect
SetUnitPosition(unit, x, y)
end
-- displays a red message followed by an error sound, or a green message with no sound if @_ispositive is passed as true
function utils.palert(p, str, dur, _ispositive)
local dur = dur or 2.5
if p == utils.localp() then
if _ispositive then
alert:new(color:wrap(color.tooltip.good, str), dur)
else
alert:new(color:wrap(color.tooltip.bad, str), dur)
utils.playsound(kui.sound.error, p)
end
end
end
function utils.palertall(str, dur, _soundsuppress)
utils.playerloop(function(p) utils.palert(p, str, dur, _soundsuppress) end)
end
function utils.pname(p)
return GetPlayerName(p)
end
-- @func = run this for all players, but local only.
function utils.looplocalp(func)
for p,_ in pairs(kobold.playing) do
if p == utils.localp() then
func()
end
end
end
function utils.textall(str)
DisplayTextToForce(GetPlayersAll(), str)
end
function utils.unitx(taru)
return GetUnitX(taru)
end
function utils.unity(taru)
return GetUnitY(taru)
end
function utils.unitxy(taru)
return GetUnitX(taru), GetUnitY(taru)
end
function utils.setxy(unit, x, y)
SetUnitX(unit,x) SetUnitY(unit,y)
end
function utils.setheight(unit, z)
SetUnitFlyHeight(unit, z, 10000.0)
end
function utils.face(unit, a)
SetUnitFacing(unit, a)
end
function utils.faceunit(unit1, unit2)
SetUnitFacing(unit1, utils.angleunits(unit1, unit2))
end
-- make both units face eachother.
function utils.eachface(unit1, unit2)
SetUnitFacing(unit1, utils.anglexy(utils.unitx(unit1), utils.unity(unit1), utils.unitx(unit2), utils.unity(unit2)))
SetUnitFacing(unit2, utils.anglexy(utils.unitx(unit2), utils.unity(unit2), utils.unitx(unit1), utils.unity(unit1)))
end
function utils.getface(unit)
return GetUnitFacing(unit)
end
function utils.data(unit)
return GetUnitUserData(unit)
end
function utils.spellxy()
return GetSpellTargetX(), GetSpellTargetY()
end
function utils.rectxy(rect)
return GetRectCenterX(rect), GetRectCenterY(rect)
end
function utils.rectx(rect)
return GetRectCenterX(rect)
end
function utils.recty(rect)
return GetRectCenterY(rect)
end
function utils.setcambounds(rect, _singlep)
if not _singlep then -- set for all players
for p,_ in pairs(kobold.playing) do
SetCameraBoundsToRectForPlayerBJ(p, rect)
end
else -- target a single player
SetCameraBoundsToRectForPlayerBJ(_singlep, rect)
end
end
function utils.playerloop(func)
-- run an action on all player objects (make sure function has 'p' as arg e.g. 'myfunc(p [,...])')
for pnum = 1,kk.maxplayers do
local p = Player(pnum-1)
if kobold.player[p] then
func(p)
end
end
end
function utils.moveallp(rect)
-- move all players to this rect center
utils.playerloop(function(p) utils.umoverect(kobold.player[p].unit, rect) end)
end
function utils.moveallpxy(x,y)
-- move all players to this rect center
utils.playerloop(function(p) utils.umovexy(kobold.player[p].unit, x, y) port_yellowtp:play(x, y) end)
end
function utils.panallp(x, y, _dur)
-- pan camera for all players to x,y.
local dur = _dur or 0
utils.playerloop(function(p) utils.panto(p, x, y, dur) end)
end
function utils.timed(dur, func)
-- one shot timer
TimerStart(NewTimer(), dur, false, function() func() ReleaseTimer() end)
end
function utils.timedrepeat(dur, count, func, _releasefunc)
local t, c, r = count, 0, _releasefunc or nil
if t == nil then
local tmr = TimerStart(NewTimer(), dur, true, function()
utils.debugfunc(func,"timer") end)
return tmr
else
local tmr = TimerStart(NewTimer(), dur, true, function()
utils.debugfunc(function()
func(c)
c = c + 1
if c >= t then
ReleaseTimer()
if r then r() end
end
end, "timer")
end)
return tmr
end
end
function utils.newhotkey(p, oskey, func, islocal, ondown, meta)
local p, pnum = p, utils.pnum(p)
local meta = meta or 0
local ondown = ondown or true
local islocal = islocal or true
if not utils.hotkeyt[pnum] then
utils.hotkeyt[pnum] = CreateTrigger()
end
BlzTriggerRegisterPlayerKeyEvent(utils.hotkeyt[pnum], p, oskey, meta, ondown)
TriggerAddAction(utils.hotkeyt[pnum], function()
if utils.ishotkey(p, oskey) then
if islocal and p == GetLocalPlayer() then
func()
else
func()
end
end
end)
return trig
end
function utils.ishotkey(p, oskey)
if p == GetLocalPlayer() then
if BlzGetTriggerPlayerKey() == oskey then
return true
else
return false
end
end
end
function utils.disableuicontrol(bool)
utils.looplocalp(function() EnableUserControl(not bool) EnableUserUI(not bool) end)
end
function utils.mana(unit)
return GetUnitState(unit, UNIT_STATE_MANA)
end
function utils.maxmana(unit)
return GetUnitState(unit, UNIT_STATE_MAX_MANA)
end
function utils.life(unit)
return GetUnitState(unit, UNIT_STATE_LIFE)
end
function utils.maxlife(unit)
return GetUnitState(unit, UNIT_STATE_MAX_LIFE)
end
function utils.setlifep(unit, val)
-- 100-based.
SetUnitLifePercentBJ(unit, val)
end
function utils.setmanap(unit, val)
-- 100-based.
SetUnitManaPercentBJ(unit, val)
end
function utils.getlifep(unit)
-- 100-based.
return GetUnitLifePercent(unit)
end
function utils.getmanap(unit)
-- 100-based.
return GetUnitManaPercent(unit)
end
-- @_owner = owner of the heal
function utils.addlifep(unit, val, _arctextbool, _owner)
-- 100-based
local amt = utils.maxlife(unit)*val/100
if _owner and kobold.player[utils.powner(_owner)] then
local p = utils.powner(_owner)
amt = amt*(1+kobold.player[p][p_stat_healing]/100)
kobold.player[p].score[4] = kobold.player[p].score[4] + amt
end
SetUnitState(unit, UNIT_STATE_LIFE, utils.life(unit) + amt)
if _arctextbool and amt > 1.0 then
ArcingTextTag(color:wrap(color.tooltip.good, "+"..math.floor(amt)), unit, 2.0)
end
end
function utils.addmanap(unit, val, _arctextbool)
-- 100-based
local amt = utils.maxmana(unit)*val/100
SetUnitState(unit, UNIT_STATE_MANA, utils.mana(unit) + amt)
if _arctextbool and amt > 1.0 then
ArcingTextTag(color:wrap(color.tooltip.alert, "+"..math.floor(amt)), unit, 2.0)
end
end
function utils.addlifeval(unit, val, _arctextbool, _owner)
-- add a raw life value to a unit.
local val, p = val, utils.powner(unit)
if _owner and kobold.player[utils.powner(_owner)] then
local p = utils.powner(_owner)
val = val*(1+kobold.player[p][p_stat_healing]/100)
kobold.player[p].score[4] = kobold.player[p].score[4] + val
end
SetUnitState(unit, UNIT_STATE_LIFE, utils.life(unit) + val)
if _arctextbool and val > 1.0 then
ArcingTextTag(color:wrap(color.tooltip.good, "+"..math.floor(val)), unit, 2.0)
end
end
function utils.addmanaval(unit, val, _arctextbool)
-- add a raw life value to a unit.
SetUnitState(unit, UNIT_STATE_MANA, RMaxBJ(utils.mana(unit) + val))
if _arctextbool and val > 1.0 then
ArcingTextTag(color:wrap(color.tooltip.alert, "+"..math.floor(val)), unit, 2.0)
end
end
function utils.setlifep(unit, val)
SetUnitState(unit, UNIT_STATE_LIFE, math.floor(GetUnitState(unit, UNIT_STATE_MAX_LIFE)*(val/100)))
end
function utils.setmanap(unit, val)
SetUnitState(unit, UNIT_STATE_MANA, math.floor(GetUnitState(unit, UNIT_STATE_MAX_MANA)*(val/100)))
end
function utils.restorestate(unit)
utils.setlifep(unit, 100)
utils.setmanap(unit, 100)
UnitRemoveBuffs(unit, true, true)
end
function utils.setnewdesthp(dest, val)
SetDestructableMaxLife(dest, val)
SetDestructableLifePercentBJ(dest, 100)
end
function utils.setnewunithp(unit, val, _resethp)
BlzSetUnitMaxHP(unit, math.floor(val))
if _resethp then
utils.setlifep(unit, 100)
end
end
function utils.setnewunitmana(unit, val, _donotreset)
BlzSetUnitMaxMana(unit, math.floor(val))
if not _donotreset and not map.manager.activemap then
utils.setmanap(unit, 100)
end
end
function utils.passivep()
return Player(PLAYER_NEUTRAL_PASSIVE)
end
function utils.tempt()
local t = {}
utils.weakt[t] = t
return t
end
function utils.isalive(unit)
return not (GetUnitTypeId(unit) == 0 or IsUnitType(unit, UNIT_TYPE_DEAD) or GetUnitState(unit, UNIT_STATE_LIFE) < 0.405)
end
function utils.ishero(unit)
return IsUnitType(unit, UNIT_TYPE_HERO)
end
function utils.isancient(unit)
return IsUnitType(unit, UNIT_TYPE_ANCIENT)
end
function utils.speffect(effect, x, y, _scale, _perm)
local e = AddSpecialEffect(effect, x, y)
BlzSetSpecialEffectScale(e, _scale or 1.0)
if not _perm then DestroyEffect(e) end
return e
end
function utils.setinvul(unit, bool)
SetUnitInvulnerable(unit, bool)
end
function utils.setinvis(unit, bool)
if bool then
UnitAddAbility(unit, FourCC('Apiv'))
else
UnitRemoveAbility(unit, FourCC('Apiv'))
end
end
function utils.vertexhide(unit, bool)
if bool then
SetUnitVertexColor(unit, 255, 255, 255, 0)
else
SetUnitVertexColor(unit, 255, 255, 255, 255)
end
end
-- @x,y = reposition an effect.
function utils.seteffectxy(effect, x, y, _z)
BlzSetSpecialEffectX(effect, x)
BlzSetSpecialEffectY(effect, y)
if _z then
BlzSetSpecialEffectZ(effect, _z)
end
end
function utils.getcliffz(x, y, _height)
return GetTerrainCliffLevel(x, y)*128 - 256 + (_height or 0)
end
function utils.dmgdestinrect(rect, dmg)
EnumDestructablesInRect(rect, nil, function()
if GetDestructableTypeId(GetEnumDestructable()) ~= b_bedrock.code then
SetWidgetLife(GetEnumDestructable(), GetWidgetLife(GetEnumDestructable()) - dmg)
end
end)
end
-- move a rect to @x,y then set its size to @w/@_h based on a radius value.
function utils.setrectradius(rect, x, y, w, _h)
local w, h = w, _h or w
SetRect(rect, x - w, y - h, x + w, y + h)
end
function utils.stunned(unit)
return UnitHasBuffBJ(unit, FourCC('BSTN')) or UnitHasBuffBJ(unit, FourCC('BPSE'))
end
function utils.slowed(unit)
return UnitHasBuffBJ(unit, FourCC('Bslo')) or UnitHasBuffBJ(unit, FourCC('Bfro'))
end
function utils.scaleatk(unit, val)
BlzSetUnitBaseDamage(unit, math.ceil( BlzGetUnitBaseDamage(unit, 0)*val), 0 )
end
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
-- :: 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))
local tmr
-- show:
if bool then
BlzFrameSetVisible(fh, true)
BlzFrameSetAlpha(fh, 255)
tmr = 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)
tmr = 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
return tmr
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, 100, 100, 0.00, 0)
end
function utils.trim(str)
return string.gsub(str, "\x25s+", "")
end
-- for index-value table, get a random value.
function utils.trandom(t)
return t[math.random(1,#t)]
end
-- requires v1 to be a negative number
function math.randomneg(v1,v2)
if v1 >= 0 then print("math.randomneg only supports negative first argument") return end
return math.random(0, v2+(-1*v1)) - v1
end
function utils.isinvul(u)
return BlzIsUnitInvulnerable(u)
end
function utils.timedlife(u, dur)
UnitApplyTimedLife(u, FourCC('BTLF'), dur)
end
-- order all units of @player within @radius of @x,y to attack move to position of @unit
function utils.issueatkmoveall(player, radius, x, y, unit)
local grp = g:newbyxy(player, g_type_ally, x, y, radius)
grp:action(function() if not utils.isancient(grp.unit) then utils.issatkxy(grp.unit, utils.unitxy(unit)) end end)
grp:destroy()
end
function utils.shakecam(p, dur, _mag)
CameraSetEQNoiseForPlayer(p, _mag or 3.0)
utils.timed(dur, function() CameraClearNoiseForPlayer(p) end)
end
function utils.booltobit(value)
if value == true then return 1 else return 0 end
end
function utils.purgetrailing(str, char)
local str = str
if string.sub(str, -1) == char then str = string.sub(str,1,-2) end -- purge trailing character (last char).
return str
end
-- split a string into a table based on a separator. returns @table
function utils.split(str, sep)
sep = sep or "\x25s"
local t = {}
for field, s in string.gmatch(str, "([^" .. sep .. "]*)(" .. sep .. "?)") do
table.insert(t, field)
if s == "" then
return t
end
end
end
function utils.tablesize(t)
local count = 0
for i,v in pairs(t) do
count = count + 1
end
return count
end
alert = {}
function alert:init()
self.__index = self
self.dur = 4
self.time = 0
self.displaytext = ''
self.tmr = NewTimer()
-- create alert frame:
self.fr = kui.frame:newbytype("BACKDROP", kui.canvas.gameui) -- parent frame
self.fr:setsize(kui:px(770), kui:px(40))
self.fr:setbgtex(kui.tex.invis)
self.fr:setfp(fp.b, fp.b, kui.worldui, 0, kui:px(307))
self.fr:setlvl(9)
self.fr:show()
self.textfr = self.fr:createheadertext("", 0.58) -- text frame
self.textfr:setparent(self.fr.fh)
self.textfr:setallfp(self.fr)
self.textfr:setlvl(9)
self.textfr:show()
self.fr:hide()
BlzFrameSetTextAlignment(self.textfr.fh, TEXT_JUSTIFY_TOP, TEXT_JUSTIFY_CENTER)
end
function alert:new(text, _dur)
utils.debugfunc(function()
self.time = _dur or self.dur
BlzFrameSetText(self.textfr.fh, text)
if not self.fr:isvisible() then
-- reveal text frame.
utils.fadeframe(false, self.fr.fh, 0.66)
end
TimerStart(self.tmr, 1.0, true, function()
-- to prevent overlapping timers, we manually control a single timer:
self.time = self.time - 1
if self.time <= 0 then
utils.fadeframe(true, self.fr.fh, 0.66)
PauseTimer(self.tmr)
end
end)
print(text) -- not multiplayer safe
ClearTextMessages()
end, "alert:new")
end
ability = {}
ability.template = {}
function ability:init()
self.__index = self
ability.template:init()
utils.debugfunc(function() ability.template:buildall() end, "ability.template:buildall")
end
function ability:new(id, iconpath)
local o = {}
setmetatable(o, self)
o.code = FourCC(id)
o.icon = iconpath
return o
end
function ability:initfields(fields)
if fields then
for i,v in pairs(fields) do
self[i] = v
end
else
print("error: ability initfields requires a 'fields' table")
end
end
function ability:initdescript(name, descript)
-- print(name)
self.name = name
self.dorigin = descript
self.descript = self:parsedescript()
end
function ability:parsedescript()
local str = self.dorigin
--print("old descript = "..self.dorigin)
for field,value in pairs(self) do
if type(field) == "string" then
if type(value) == "table" then
str = string.gsub(str, "(#"..field..")", color:wrap(value.color, tostring(value.name)))
else
if string.find(field, "range") or string.find(field, "radius") then
str = string.gsub(str, "(#"..field..")", color:wrap(color.tooltip.good, tostring(value).."m"))
elseif string.find(field, "dur") then
str = string.gsub(str, "(#"..field..")", color:wrap(color.tooltip.good, tostring(value).." sec"))
elseif string.find(field, "perc") then
str = string.gsub(str, "(#"..field..")", color:wrap(color.tooltip.good, tostring(value).."%%%%"))
str = string.gsub(str, "(%)", "" )
else
str = string.gsub(str, "(#"..field..")", color:wrap(color.tooltip.good, tostring(value)))
end
end
end
end
self.dorigin = nil
return str
end
function ability:buildpanel()
fr = kui:createmassivepanel("ABILITIES")
local startx,starty = kui:px(kui.meta.abilpanel[1].x),kui:px(kui.meta.abilpanel[1].y)
local x,y = startx,starty
local offx = kui:px(kui.meta.abilpanel[1].offx)
local offy = kui:px(kui.meta.abilpanel[1].offy)
local abilslot = 0
fr.slot = {} -- ability cards.
fr.header = {} -- QWER headers.
for row = 1,3 do
fr.slot[row] = {}
for col = 1,4 do
abilslot = abilslot + 1
fr.slot[row][col] = kui.frame:newbytype("BACKDROP", fr)
fr.slot[row][col]:addbgtex(kui.meta.abilpanel[1].tex)
fr.slot[row][col]:setsize(kui:px(kui.meta.abilpanel[1].w), kui:px(kui.meta.abilpanel[1].h))
fr.slot[row][col]:setfp(fp.tl, fp.tl, fr, x, y)
fr.slot[row][col]:addtext("btn", "Ability Name", fp.t)
fr.slot[row][col].txt:setfp(fp.c, fp.t, fr.slot[row][col], 0.0, kui:px(-16))
-- ability backdrop dec:
fr.slot[row][col].iconbd = kui.frame:newbytype("BACKDROP", fr.slot[row][col])
fr.slot[row][col].iconbd:addbgtex(kui.meta.abilpanel[2].tex)
fr.slot[row][col].iconbd:setsize(kui:px(kui.meta.abilpanel.btnwh), kui:px(kui.meta.abilpanel.btnwh))
fr.slot[row][col].iconbd:setfp(fp.c, fp.l, fr.slot[row][col], 0.0, kui:px(kui.meta.abilpanel.btnoffy) + kui:px(8))
-- ability icon:
fr.slot[row][col].icon = kui.frame:newbytype("BACKDROP", fr.slot[row][col])
fr.slot[row][col].icon:setsize(kui:px(kui.meta.abilpanel.btnwh*0.54), kui:px(kui.meta.abilpanel.btnwh*0.54))
fr.slot[row][col].icon:setfp(fp.c, fp.c, fr.slot[row][col].iconbd, 0.0, kui:px(2))
-- ability description:
fr.slot[row][col].descript = fr.slot[row][col]:createtext("")
fr.slot[row][col].descript:setsize(kui:px(208), kui:px(95))
fr.slot[row][col].descript:setfp(fp.tl, fp.tl, fr.slot[row][col], kui:px(43), -kui:px(44))
BlzFrameSetTextAlignment(fr.slot[row][col].descript.fh, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_LEFT)
-- btn fill:
fr.slot[row][col].btn = kui.frame:newbytype("BUTTON", fr.slot[row][col])
fr.slot[row][col].btn:assigntooltip("learnabil")
fr.slot[row][col].btn:setallfp(fr.slot[row][col].iconbd)
fr.slot[row][col].btn:addhoverscale(fr.slot[row][col].icon, 1.1)
-- abil costs:
local costwh = 24
fr.slot[row][col].mana = kui.frame:newbytype("BACKDROP", fr.slot[row][col])
fr.slot[row][col].mana:addbgtex('war3mapImported\\icon-abil_costs_02.blp')
fr.slot[row][col].mana:setsize(kui:px(costwh), kui:px(costwh))
fr.slot[row][col].mana:addtext("btn", "0", fp.c)
fr.slot[row][col].mana.txt:assigntooltip("manacost")
fr.slot[row][col].mana.txt:setscale(0.85)
fr.slot[row][col].mana.txt:setsize(kui:px(costwh), kui:px(costwh))
fr.slot[row][col].mana:setfp(fp.tr, fp.b, fr.slot[row][col].iconbd, 0.0, kui:px(3))
fr.slot[row][col].cd = kui.frame:newbytype("BACKDROP", fr.slot[row][col])
fr.slot[row][col].cd:setsize(kui:px(costwh), kui:px(costwh))
fr.slot[row][col].cd:addbgtex('war3mapImported\\icon-abil_costs_01.blp')
fr.slot[row][col].cd:addtext("btn", "0", fp.c)
fr.slot[row][col].cd.txt:assigntooltip("cooldown")
fr.slot[row][col].cd.txt:setscale(0.9)
fr.slot[row][col].cd.txt:setsize(kui:px(costwh), kui:px(costwh))
fr.slot[row][col].cd:setfp(fp.tl, fp.b, fr.slot[row][col].iconbd, 0.0, kui:px(3))
-- progression lock icon after 1st abil:
if row == 1 and col == 1 then
-- do nothing, these abilities are unlocked by default.
else
fr.slot[row][col].lock = kui.frame:newbtntemplate(fr, "war3mapImported\\panel-ability-lock_icon.blp")
fr.slot[row][col].lock:setfp(fp.c, fp.c, fr.slot[row][col])
fr.slot[row][col].lock:setsize(kui:px(62), kui:px(79))
fr.slot[row][col].lock:addtext("btn", "LVL. "..((abilslot - 1)*3 + 1), fp.c) -- level req. text.
fr.slot[row][col].lock.btn:addhoverhl(fr.slot[row][col].lock)
fr.slot[row][col].lock.btn:clearallfp()
fr.slot[row][col].lock.btn:setfp(fp.c, fp.c, fr.slot[row][col].lock)
fr.slot[row][col].lock.btn:setsize(kui:px(kui.meta.abilpanel[1].w), kui:px(kui.meta.abilpanel[1].h))
fr.slot[row][col].lock.btn:addevents(nil, nil, function() end, nil) -- add baked-in focus fix.
fr.slot[row][col].lock.alpha = 255
fr.slot[row][col].lock.alphahl = 0
fr.slot[row][col].lock:resetalpha()
fr.slot[row][col].lock.txt:setfp(fp.c, fp.c, fr.slot[row][col].lock, 0.0, -kui:px(6))
fr.slot[row][col].lock.txt:setlvl(2)
fr.slot[row][col].lock.btn:assigntooltip("abilitylvl")
fr.slot[row][col].lock.btn:setlvl(3)
fr.slot[row][col].lock.btn.hoverarg = abilslot - 1
fr.slot[row][col].btn:hide() -- hide the ability learn button to prevent clicks.
fr.slot[row][col].btn.disabled = true
fr.slot[row][col]:setnewalpha(150) -- initialize default alpha for locked abils.
end
-- QWER header:
if row == 1 then
fr.header[col] = fr:createheadertext(kui.meta.abilpanel.headers[col])
fr.header[col]:setfp(fp.c, fp.t, fr.slot[row][col], 0.0, kui:px(22))
fr.header[col]:setsize(kui:px(60), kui:px(60))
end
fr.slot[row][col].click = function()
-- FIXME: DESYNCS
local p = utils.trigp()
if not map.manager.loading and not map.manager.activemap then
kobold.player[p].ability:learn(row, col)
else
utils.palert(p, "You cannot change abilities during a dig!")
end
end
fr.slot[row][col].btn:addevents(nil,nil,fr.slot[row][col].click)
x = x + offx
end
x = startx
y = y - offy
end
fr.panelid = 6
fr.statesnd = true
fr:hide()
return fr
end
function ability.template:createbase()
local o = {}
o.spells = {}
for i = 1,4 do
o.spells[i] = {}
end
o.spells[9] = {}
o.spells[10] = {}
for i = 1,10 do o[i] = {} end
setmetatable(o, self)
return o
end
function ability.template:buildall()
-- locals for readability:
local Q_id,W_id,E_id,R_id = 1,2,3,4
-- init template base object:
for charid = 1,4 do
ability.template[charid] = ability.template:createbase()
end
--------------------------------------------------------------------------------------------------
------------------ TUNNELER ----------------------------------------------------------------------
--------------------------------------------------------------------------------------------------
--[[
row 1:
--]]
-- Q
ability.template[1][Q_id][1] = ability:new("A010", "war3mapImported\\class_ability_icons_01.blp")
ability.template[1][Q_id][1]:initfields({type1=dmg.type[dmg_phys], dmg1=52, radius1=3, cost=5, cd=nil, perc1=75})
ability.template[1][Q_id][1]:initdescript(
"Tunnel Bomb",
"Throw dynamite at a target area, dealing #dmg1 #type1 damage in a #radius1 radius. Deals #perc1 damage to blocks.")
-- W
ability.template[1][W_id][1] = ability:new("A013", "war3mapImported\\class_ability_icons_02.blp")
ability.template[1][W_id][1]:initfields({type1=dmg.type[dmg_phys], dmg1=21, count1=3, range1=3, cost=25, cd=16, dur1=8, perc1=75})
ability.template[1][W_id][1]:initdescript(
"Rocksaw",
"Mount #count1 spinning blades to the ground for #dur1, each dealing #dmg1 #type1 damage per sec in a #range1 radius."
.." Deals #perc1 damage to blocks.")
-- E
ability.template[1][E_id][1] = ability:new("A014", "war3mapImported\\class_ability_icons_03.blp")
ability.template[1][E_id][1]:initfields({type1=dmg.type[dmg_phys], dmg1=76, range1=3, cost=25, cd=14, dur1=3})
ability.template[1][E_id][1]:initdescript(
"Mole Hole",
"Dig into the ground and move around freely, becoming immune to all effects. Emerge after #dur1 to deal #dmg1 #type1"
.." damage in a #range1 radius.")
-- R
ability.template[1][R_id][1] = ability:new("A015", "war3mapImported\\class_ability_icons_04.blp")
ability.template[1][R_id][1]:initfields({type1=dmg.type[dmg_phys], dmg1=64, cost=60, cd=45, dur1=8, range1=2, range2=3, perc1=75, perc2=75})
ability.template[1][R_id][1]:initdescript(
"Wrecking Ball",
"Turn into a wrecking ball for #dur1, dealing #dmg1 #type1 damage to targets in a #range1 radius and knocking them back."
.." While active, take #perc2 less damage.")
--[[
row 2:
--]]
-- Q
ability.template[1][Q_id][2] = ability:new("A011", "war3mapImported\\class_ability_icons_05.blp")
ability.template[1][Q_id][2]:initfields({type1=dmg.type[dmg_phys], dmg1=32, range1=8, radius1=2, cost=25, cd=nil, perc1=25, dur1=6})
ability.template[1][Q_id][2]:initdescript(
"Pickerang",
"Throw two spinning pickaxes that arc for #dur1, each dealing #dmg1 #type1 damage. Deals #perc1 bonus damage to slowed targets."
.." Can strike targets multiple times.")
-- W
ability.template[1][W_id][2] = ability:new("A012", "war3mapImported\\class_ability_icons_06.blp")
ability.template[1][W_id][2]:initfields({type1=dmg.type[dmg_phys], dmg1=48, range1=8, radius1=2, cost=15, cd=nil, dur1=1, dur2=2.5})
ability.template[1][W_id][2]:initdescript(
"Hammerang",
"Throw two precise hammers that travel for #dur1, each dealing #dmg1 #type1 damage and slowing targets for #dur2."
.." Can strike targets once.")
-- E
ability.template[1][E_id][2] = ability:new("A016", "war3mapImported\\class_ability_icons_07.blp")
ability.template[1][E_id][2]:initfields({type1=dmg.type[dmg_phys], dmg1=42, range1=15, radius1=2, cost=40, cd=12, perc1=100, dur1=2.5})
ability.template[1][E_id][2]:initdescript(
"'Kobolt' Auto-Miner",
"Deploy a robotic assistant which travels in a line for #range1, dealing #dmg1 #type1 damage to targets its path and slowing them for #dur1."
.." Deals #perc1 damage to blocks.")
-- R
ability.template[1][R_id][2] = ability:new("A017", "war3mapImported\\class_ability_icons_08.blp")
ability.template[1][R_id][2]:initfields({type1=dmg.type[dmg_phys], dmg1=348, dur1=4, dur2=3, radius1=8, cost=40, cd=60, perc1=75})
ability.template[1][R_id][2]:initdescript(
"The Big One",
"Deploy a bomb which detonates after #dur1, dealing #dmg1 #type1 damage in a #radius1 radius."
.." Deals #perc1 damage to blocks.")
--[[
row 3:
--]]
-- Q
ability.template[1][Q_id][3] = ability:new("A018", "war3mapImported\\class_ability_icons_09.blp")
ability.template[1][Q_id][3]:initfields({type1=dmg.type[dmg_fire], type2=dmg.type[dmg_frost], type3=dmg.type[dmg_nature],
dmg1=32, range1=6, radius1=2, cost=10, cd=nil, count1=3})
ability.template[1][Q_id][3]:initdescript(
"Primal Candle",
"Unleash #count1 random bursts of candle magic in a #range1 line, each dealing #dmg1 #type1, #type2, or #type3 damage twice in quick succession.")
-- W
ability.template[1][W_id][3] = ability:new("A019", "war3mapImported\\class_ability_icons_10.blp")
ability.template[1][W_id][3]:initfields({type1=dmg.type[dmg_fire], type2=dmg.type[dmg_frost], type3=dmg.type[dmg_nature],
dmg1=84, range1=10, cost=20, cd=10, dur1=3})
ability.template[1][W_id][3]:initdescript(
"Rift Rocket",
"Launch a pair of tethered missiles which travel for #range1, rooting targets for #dur1"
.." and dealing #dmg1 #type1, #type2, and #type3 damage.")
-- E
ability.template[1][E_id][3] = ability:new("A01A", "war3mapImported\\class_ability_icons_11.blp")
ability.template[1][E_id][3]:initfields({type1=dmg.type[dmg_fire], type2=dmg.type[dmg_frost], type3=dmg.type[dmg_nature],
dmg1=24, cost=25, cd=1, dur1=30, range1=8, radius1=8})
ability.template[1][E_id][3]:initdescript(
"'Kobolt' Auto-Turret",
"Deploy a robotic turret for #dur1 that lobs random elementium missiles at nearby targets, dealing"
.." #dmg1 #type1, #type2, or #type3 damage with each shot.")
-- R
ability.template[1][R_id][3] = ability:new("A01B", "war3mapImported\\class_ability_icons_12.blp")
ability.template[1][R_id][3]:initfields({type1=dmg.type[dmg_fire], type2=dmg.type[dmg_frost], type3=dmg.type[dmg_nature],
count1=3, dmg1=34, dur1=12, cost=45, cd=30, range1=10})
ability.template[1][R_id][3]:initdescript(
"Triumvirate",
"Spawn #count1 wax orbs which orbit you for #dur1, launching random elemental missiles in a continuous spiral which deal"
.." #dmg1 #type1, #type2, or #type3 damage.")
--------------------------------------------------------------------------------------------------
------------------ GEOMANCER ---------------------------------------------------------------------
--------------------------------------------------------------------------------------------------
--[[
row 1:
--]]
-- Q
ability.template[2][Q_id][1] = ability:new("A00O", "war3mapImported\\class_ability_icons_13.blp")
ability.template[2][Q_id][1]:initfields({type1=dmg.type[dmg_phys], dmg1=72, range1=8, radius1=3, cost=10, cd=nil, dur1=2.5})
ability.template[2][Q_id][1]:initdescript(
"Geo-Bolt",
"Launch a missile that explodes on contact, dealing #dmg1 #type1 damage in a #radius1 radius and slowing targets for #dur1.")
-- W
ability.template[2][W_id][1] = ability:new("A00R", "war3mapImported\\class_ability_icons_14.blp")
ability.template[2][W_id][1]:initfields({type1=dmg.type[dmg_phys], count1=3, dmg1=32, range1=6, radius1=1.5, cost=15, cd=nil, dur1=3.5})
ability.template[2][W_id][1]:initdescript(
"Tremor",
"Launch #count1 tremoring paths toward a target area, dealing #dmg1 #type1 damage in each quake's #radius1 radius and slowing targets for #dur1.")
-- E
ability.template[2][E_id][1] = ability:new("A00T", "war3mapImported\\class_ability_icons_15.blp")
ability.template[2][E_id][1]:initfields({range1=12, cost=15, cd=30, dur1=10, radius1=1})
ability.template[2][E_id][1]:initdescript(
"Geo-Portal",
"Create a magical portal in front of you and at a target location up to #range1 away, allowing Kobolds to travel between either for #dur1.")
-- R
ability.template[2][R_id][1] = ability:new("A00U", "war3mapImported\\class_ability_icons_16.blp")
ability.template[2][R_id][1]:initfields({type1=dmg.type[dmg_nature], dmg1=176, radius1=10, cost=75, cd=30, dur1=3, dur2=2})
ability.template[2][R_id][1]:initdescript(
"Fulmination",
"Charge yourself with energy for #dur1. When complete, strike each target within #radius1 for #dmg1 #type1 damage and stun them for #dur2.")
--[[
row 2:
--]]
-- Q
ability.template[2][Q_id][2] = ability:new("A00P", "war3mapImported\\class_ability_icons_17.blp")
ability.template[2][Q_id][2]:initfields({type1=dmg.type[dmg_nature], dmg1=16, heal1=12, range1=8, radius1=3, cost=10, cd=nil, dur1=6,
shroom="Shroom"})
ability.template[2][Q_id][2]:initdescript(
"Shroom Bolt",
"Launch a missile that explodes to summon a #shroom for #dur1. The shroom deals #dmg1 #type1 damage to targets every sec while "
.." healing Kobolds for #heal1.")
-- W
ability.template[2][W_id][2] = ability:new("A00S", "war3mapImported\\class_ability_icons_18.blp")
ability.template[2][W_id][2]:initfields({type1=dmg.type[dmg_nature], dmg1=160, range1=10, cost=20, cd=12, dur1=6, dur2=3, perc1=20})
ability.template[2][W_id][2]:initdescript(
"Fungal Infusion",
"Become infused with shroom magic, absorbing #dmg1 damage for #dur1. Additionally, restore #perc1 of your health over #dur2.")
-- E
ability.template[2][E_id][2] = ability:new("A00V", "war3mapImported\\class_ability_icons_19.blp")
ability.template[2][E_id][2]:initfields({type1=dmg.type[dmg_nature], dmg1=16, dmg2=48, cost=75, cd=30, dur1=3, golem="Bog Golem", bolt="Bolt", leaps="leaps"})
ability.template[2][E_id][2]:initdescript(
"Bog Golem",
"Summon a #golem to attack your foes for #dmg1 #type1 damage. The Golem #leaps to #bolt cast locations for #dmg2 #type1 damage (#dur1 cooldown).")
-- R
ability.template[2][R_id][2] = ability:new("A00W", "war3mapImported\\class_ability_icons_20.blp")
ability.template[2][R_id][2]:initfields({type1=dmg.type[dmg_nature], dmg1=16, range1=5, cost=nil, cd=45, dur1=12, dur2=6, perc1=100, heal1=12,
shroom="Shroom", radius1=3})
ability.template[2][R_id][2]:initdescript(
"Shroompocalypse",
"Imbibe the ultimate concoction, restoring #perc1 of your health and mana over #dur1. While active, using a spell causes you to"
.." leave behind a #shroom.")
--[[
row 3:
--]]
-- Q
ability.template[2][Q_id][3] = ability:new("A00Q", "war3mapImported\\class_ability_icons_21.blp")
ability.template[2][Q_id][3]:initfields({type1=dmg.type[dmg_arcane], dmg1=64, dmg2=34, range1=8, radius1=2, cost=10, cd=nil, perc1=3})
ability.template[2][Q_id][3]:initdescript(
"Arcane Bolt",
"Launch a missile that pierces targets in a #radius1 radius, dealing #dmg1 #type1 damage. Each target hit increases the missile's damage by #perc1.")
-- W
ability.template[2][W_id][3] = ability:new("A00X", "war3mapImported\\class_ability_icons_22.blp")
ability.template[2][W_id][3]:initfields({type1=dmg.type[dmg_arcane], dmg1=12, range1=10, cost=40, cd=30, dur1=3, dup="duplicate", bolt="Bolt", golem="Arcane Golem"})
ability.template[2][W_id][3]:initdescript(
"Arcane Golem",
"Summon a passive #golem to follow you. The Golem will #dup any #bolt ability you use (#dur1 cooldown).")
-- E
ability.template[2][E_id][3] = ability:new("A00Y", "war3mapImported\\class_ability_icons_23.blp")
ability.template[2][E_id][3]:initfields({type1=dmg.type[dmg_arcane], dmg1=176, cost=60, cd=3, radius1=3, dur1=2, dur2=3})
ability.template[2][E_id][3]:initdescript(
"Gravity Burst",
"Target an area which detonates after #dur1, dealing #dmg1 #type1 damage in a #radius1 radius and slowing targets for #dur2.")
-- R
ability.template[2][R_id][3] = ability:new("A00Z", "war3mapImported\\class_ability_icons_24.blp")
ability.template[2][R_id][3]:initfields({type1=dmg.type[dmg_arcane], dmg1=32, cost=60, cd=45, radius1=6, dur1=12, perc1=25})
ability.template[2][R_id][3]:initdescript(
"Curse of Doom",
"Curse targets in a #radius1 radius for #dur1, dealing #dmg1 #type1 damage per sec, slowing them, and reducing their primary damage"
.." resistance by #perc1.")
--------------------------------------------------------------------------------------------------
------------------ RASCAL-------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------
--[[
row 1:
--]]
-- Q
ability.template[3][Q_id][1] = ability:new("A01C", "war3mapImported\\class_ability_icons_25.blp")
ability.template[3][Q_id][1]:initfields({type1=dmg.type[dmg_phys], range1=3, dmg1=48, cost=5, cd=nil, dur1=3, perc1=15, count1=3})
ability.template[3][Q_id][1]:initdescript(
"Gambit",
"Strike targets within #range1 for #dmg1 #type1 damage."
.." Each use of this ability increases its damage by #perc1 for #dur1.")
-- W
ability.template[3][W_id][1] = ability:new("A01D", "war3mapImported\\class_ability_icons_26.blp")
ability.template[3][W_id][1]:initfields({type1=dmg.type[dmg_fire], cost=15, cd=8, dur1=3, dmg1=32, radius1=5, radius2=3})
ability.template[3][W_id][1]:initdescript(
"Candleblight",
"Curse targets within #radius1, causing them to explode in fiery wax for #dmg1 #type1 damage in a #radius2 radius after #dur1.")
-- E
ability.template[3][E_id][1] = ability:new("A01E", "war3mapImported\\class_ability_icons_27.blp")
ability.template[3][E_id][1]:initfields({type1=dmg.type[dmg_fire], dmg1=64, range1=10, range2=3, cost=25, cd=10, dur1=3, perc1=25})
ability.template[3][E_id][1]:initdescript(
"Rocket Jump",
"Blast off to a location up to #range1 away, dealing #dmg1 #type1 damage in a #range2 radius from the launch point and gaining"
.." #perc1 movespeed for #dur1.")
-- R
ability.template[3][R_id][1] = ability:new("A01F", "war3mapImported\\class_ability_icons_28.blp")
ability.template[3][R_id][1]:initfields({type1=dmg.type[dmg_phys], cost=0, cd=10, perc1=60, perc2=20, perc3=10, count1=2, radius1=3})
ability.template[3][R_id][1]:initdescript(
"Devitalize",
"Strike targets within #radius1 for #perc1 of their missing health as #type1 damage and restore #perc2 mana. If a target dies, restore #perc3 bonus mana."
.." Less effective versus bosses.")
--[[
row 2:
--]]
-- Q
ability.template[3][Q_id][2] = ability:new("A01G", "war3mapImported\\class_ability_icons_29.blp")
ability.template[3][Q_id][2]:initfields({type1=dmg.type[dmg_shadow], type2=dmg.type[dmg_shadow], dmg1=38, dmg2=76, cost=5, cd=nil, perc1=200, radius1=3})
ability.template[3][Q_id][2]:initdescript(
"Backstab",
"Strike targets within #radius1 for #dmg1 #type1 damage. If you are behind a target, deal #perc1 bonus #type2 damage to them.")
-- W
ability.template[3][W_id][2] = ability:new("A01H", "war3mapImported\\class_ability_icons_30.blp")
ability.template[3][W_id][2]:initfields({dmg1=38, count1=6, range1=8, cost=20, cd=nil, perc1="50", count2=3})
ability.template[3][W_id][2]:initdescript(
"Gamble",
"Throw #count2 magical blades which deal #dmg1 damage of a random type to the first target they strike. #perc1 chance to throw #count1 blades.")
-- E
ability.template[3][E_id][2] = ability:new("A01I", "war3mapImported\\class_ability_icons_31.blp")
ability.template[3][E_id][2]:initfields({type1=dmg.type[dmg_shadow], dmg1=36, radius1=3, cost=30, cd=14, dur1=6})
ability.template[3][E_id][2]:initdescript(
"Nether Bomb",
"Toss a grenade which creates a #radius1 void puddle, pulling targets towards its impact center for #dur1 and dealing #dmg1 #type1 damage per sec.")
-- R
ability.template[3][R_id][2] = ability:new("A01V", "war3mapImported\\class_ability_icons_32.blp")
ability.template[3][R_id][2]:initfields({type1=dmg.type[dmg_shadow], dmg1=48, cost=40, cd=40, dur1=6, perc1=50})
ability.template[3][R_id][2]:initdescript(
"Deadly Shadows",
"Become invisible and invulnerable for #dur1. While stealthed, your abilities deal #perc1 bonus #type1 damage and you move at max movespeed.")
--[[
row 3:
--]]
-- Q
ability.template[3][Q_id][3] = ability:new("A01K", "war3mapImported\\class_ability_icons_33.blp")
ability.template[3][Q_id][3]:initfields({type1=dmg.type[dmg_frost], type2=dmg.type[dmg_frost], dmg1=32, dmg2=64, range1=1, range2=3, cost=5,
cd=nil, perc1=200, frozen="frozen", radius1=3})
ability.template[3][Q_id][3]:initdescript(
"Frost Strike",
"Strike targets within #radius1 for #dmg1 #type1 damage. If a target is #frozen, deal #perc1 bonus #type2 damage in a #range2 radius.")
-- W
ability.template[3][W_id][3] = ability:new("A01L", "war3mapImported\\class_ability_icons_34.blp")
ability.template[3][W_id][3]:initfields({type1=dmg.type[dmg_frost], dmg1=72, range1=3, radius1=2.33, cost=15, cd=nil,
dur1=2.25, freezes="freezes"})
ability.template[3][W_id][3]:initdescript(
"Frigid Dagger",
"Throw a dagger that deals #dmg1 #type1 damage in a #range1 line. The dagger detonates at the end of its path for #dmg1 #type1"
.." damage and #freezes targets for #dur1.")
-- E
ability.template[3][E_id][3] = ability:new("A01M", "war3mapImported\\class_ability_icons_35.blp")
ability.template[3][E_id][3]:initfields({type1=dmg.type[dmg_frost], dmg1=132, range1=3, cost=30, cd=12, dur1=4, dur2=6, freezing="freezing"})
ability.template[3][E_id][3]:initdescript(
"Glacial Grenade",
"Toss a grenade which deals #dmg1 #type1 damage in a #range1 radius, #freezing targets for #dur1.")
-- R
ability.template[3][R_id][3] = ability:new("A01N", "war3mapImported\\class_ability_icons_36.blp")
ability.template[3][R_id][3]:initfields({type1=dmg.type[dmg_frost], dmg1=82, dur1=12, dur2=3, range1=6, cost=60, cd=45, freezing="freezing"})
ability.template[3][R_id][3]:initdescript(
"Abominable",
"Turn into an abominable snobold for #dur1. While active, abilities create rolling snowballs which deal #dmg1 #type1 damage"
.." to targets, #freezing them for #dur2.")
--------------------------------------------------------------------------------------------------
------------------ WICKFIGHTER -------------------------------------------------------------------
--------------------------------------------------------------------------------------------------
--[[
row 1:
--]]
-- Q
ability.template[4][Q_id][1] = ability:new("A002", "war3mapImported\\class_ability_icons_37.blp")
ability.template[4][Q_id][1]:initfields({type1=dmg.type[dmg_fire], dmg1=42, radius1=3, cost=5, cd=nil, perc1=1, dur1=4})
ability.template[4][Q_id][1]:initdescript(
"Wickfire",
"Deal #dmg1 #type1 damage in a #radius1 radius and taunt targets for #dur1. Restore #perc1 of your max health for each target hit.")
-- W
ability.template[4][W_id][1] = ability:new("A003", "war3mapImported\\class_ability_icons_38.blp")
ability.template[4][W_id][1]:initfields({type1=dmg.type[dmg_fire], dmg1=196, dmg2=14, radius1=3, cost=25, cd=12, dur1=6})
ability.template[4][W_id][1]:initdescript(
"Molten Shield",
"Absorb the next #dmg1 damage taken for #dur1. While active, deal #dmg2 #type1 damage per sec in a #radius1 radius.")
-- E
ability.template[4][E_id][1] = ability:new("A004", "war3mapImported\\class_ability_icons_39.blp")
ability.template[4][E_id][1]:initfields({type1=dmg.type[dmg_fire], dmg1=64, range1=10, radius1=3, cost=25, cd=8, dur1=6})
ability.template[4][E_id][1]:initdescript(
"Ember Charge",
"Charge a target area within #range1 and combust, dealing #dmg1 #type1 damage in a #radius1 radius and slowing targets for #dur1.")
-- R
ability.template[4][R_id][1] = ability:new("A005", "war3mapImported\\class_ability_icons_40.blp")
ability.template[4][R_id][1]:initfields({type1=dmg.type[dmg_fire], radius1=6, cost=40, cd=45, dur1=10, dmg1=24})
ability.template[4][R_id][1]:initdescript(
"Candlepyre",
"Become a living candle for #dur1. While active, abilities lob wax globules in a #radius1 radius which deal #dmg1 #type1 damage.")
--[[
row 2:
--]]
-- Q
ability.template[4][Q_id][2] = ability:new("A00G", "war3mapImported\\class_ability_icons_41.blp")
ability.template[4][Q_id][2]:initfields({type1=dmg.type[dmg_phys], type2=dmg.type[dmg_fire], dmg1=44, dmg2=32, dmg3=32, cost=5, cd=nil, radius1=3})
ability.template[4][Q_id][2]:initdescript(
"Slag Slam",
"Bash targets within #radius1 for #dmg1 #type1 damage plus #dmg2 #type2 damage. If a target dies, they combust to"
.." deal #dmg3 #type2 damage in a #radius1 radius.")
-- W
ability.template[4][W_id][2] = ability:new("A00H", "war3mapImported\\class_ability_icons_42.blp")
ability.template[4][W_id][2]:initfields({type1=dmg.type[dmg_phys], dmg1=64, radius1=10, cost=20, cd=20, dur1=12, perc1=15, perc2=20})
ability.template[4][W_id][2]:initdescript(
"Waxshell",
"Become encased in defensive wax, gaining #perc1 elemental resistance for #dur1 and restoring #perc2 life.")
-- E
ability.template[4][E_id][2] = ability:new("A00I", "war3mapImported\\class_ability_icons_43.blp")
ability.template[4][E_id][2]:initfields({type1=dmg.type[dmg_phys], dmg1=104, radius1=3, range1=3, cost=20, cd=10, dur1=0.50})
ability.template[4][E_id][2]:initdescript(
"Wickfire Leap",
"Leap to a target point over #dur1, dealing #dmg1 #type1 damage to targets in a #radius1 radius upon landing"
.." and knocking them back #range1.")
-- R
ability.template[4][R_id][2] = ability:new("A00J", "war3mapImported\\class_ability_icons_44.blp")
ability.template[4][R_id][2]:initfields({type1=dmg.type[dmg_fire], dmg1=94, dur1=4, radius1=3, range1=10, cost=40, cd=45, count1=8})
ability.template[4][R_id][2]:initdescript(
"Basalt Mortar",
"Hurl a volley of #count1 fireballs at a target area over #dur1, each dealing #dmg1 #type1 damage in a #radius1 radius.")
--[[
row 3:
--]]
-- Q
ability.template[4][Q_id][3] = ability:new("A00K", "war3mapImported\\class_ability_icons_45.blp")
ability.template[4][Q_id][3]:initfields({type1=dmg.type[dmg_phys], dmg1=64, radius1=10, cost=5, cd=nil, perc1=25, count1=3, radius2=3})
ability.template[4][Q_id][3]:initdescript(
"Rallying Clobber",
"Pummel targets within #radius1, dealing #dmg1 #type1 damage and increasing the movespeed and physical resistance of Kobolds within"
.." #radius1 by #perc1 for 3 sec.")
-- W
ability.template[4][W_id][3] = ability:new("A00L", "war3mapImported\\class_ability_icons_46.blp")
ability.template[4][W_id][3]:initfields({type1=dmg.type[dmg_fire], dmg1=36, radius1=4, cost=20, cd=nil, dur1=4, perc1=25})
ability.template[4][W_id][3]:initdescript(
"Molten Roar",
"Roar with molten waxfury, dealing #dmg1 #type1 damage to targets in a #radius1 radius and reducing their"
.." attack damage by #perc1 for #dur1.")
-- E
ability.template[4][E_id][3] = ability:new("A00M", "war3mapImported\\class_ability_icons_47.blp")
ability.template[4][E_id][3]:initfields({type1=dmg.type[dmg_phys], dmg1=148, cost=20, cd=6, dur1=3, perc1=50, range1=4.25, count1=2, radius1=3, range1=3})
ability.template[4][E_id][3]:initdescript(
"Seismic Toss",
"Toss targets within #radius1 away from you, dealing #dmg1 #type1 damage and stunning them for #dur1. Deals #perc1 bonus damage to toss-immune targets.")
-- R
ability.template[4][R_id][3] = ability:new("A00N", "war3mapImported\\class_ability_icons_48.blp")
ability.template[4][R_id][3]:initfields({type1=dmg.type[dmg_fire], dmg1=28, dur1=15, cost=40, cd=20, radius1=2.5, perc1=25})
ability.template[4][R_id][3]:initdescript(
"Dreadfire",
"Become engulfed in fiery wax armor for #dur1, reducing all damage taken by #perc1 and dealing #dmg1 #type1 damage to nearby targets each sec.")
ability.template:buildmasteryabils()
end
function ability.template:buildmasteryabils()
ability.template.mastery = setmetatable({}, self)
-- Forcewave
ability.template.mastery[1] = ability:new("A02N", "war3mapImported\\mastery_ability-icons_01.blp")
ability.template.mastery[1]:initfields({type1=dmg.type[dmg_arcane], cd=3, cost=15, dmg1=124, radius1=3, range1=4, dur1=6})
ability.template.mastery[1]:initdescript(
"Forcewave",
"Create a shockwave at your feet, dealing #dmg1 #type1 damage to targets in a #radius1 radius, knocking them back #range1,"
.." and slowing them for #dur1.")
ability.template.mastery[2] = ability:new("A02X", "war3mapImported\\mastery_ability-icons_02.blp")
ability.template.mastery[2]:initfields({type1=dmg.type[dmg_arcane], dmg1=24, cd=70, cost=60, dur1=15, perc1=5, radius1=3, third="3rd", every="every element"})
ability.template.mastery[2]:initdescript(
"Unstable Arcana",
"Become power hungry, gaining #perc1 added damage of #every for #dur1. Additionally, every #third ability unleashes a #radius1 nova of missiles for"
.." #dmg1 #type1 damage.")
ability.template.mastery[3] = ability:new("A02P", "war3mapImported\\mastery_ability-icons_03.blp")
ability.template.mastery[3]:initfields({type1=dmg.type[dmg_frost], cd=8, cost=50, dmg1=196, radius1=3, range1=8, freezing="freezing", dur1=3, targetimgsize=200})
ability.template.mastery[3]:initdescript(
"Icebound Globe",
"Launch an orb that explodes on the first target it strikes, dealing #dmg1 #type1 damage in a #radius1 radius and #freezing targets for #dur1.")
ability.template.mastery[4] = ability:new("A02V", "war3mapImported\\mastery_ability-icons_04.blp")
ability.template.mastery[4]:initfields({type1=dmg.type[dmg_frost], cd=30, cost=30, dmg1=248, freeze="freeze", dur1=6, perc1=50, radius1=3})
ability.template.mastery[4]:initdescript(
"Glaciate",
"Deal #dmg1 #type1 damage to targets within #radius1 and #freeze them for #dur1. Deals #perc1 bonus damage to freeze-immune targets.")
ability.template.mastery[5] = ability:new("A02K", "war3mapImported\\mastery_ability-icons_05.blp")
ability.template.mastery[5]:initfields({type1=dmg.type[dmg_nature], cd=10, cost=30, dmg1=128, radius1=4, dur1=3})
ability.template.mastery[5]:initdescript(
"Magnet Rod",
"Strike all targets within #radius1 for #dmg1 #type1 damage and pull them to you. Pulled targets"
.." are stunned for #dur1.")
ability.template.mastery[6] = ability:new("A02Q", "war3mapImported\\mastery_ability-icons_06.blp")
ability.template.mastery[6]:initfields({type1=dmg.type[dmg_nature], cd=3, cost=75, dmg1=182, dur1=3, perc1=50, perc2=3, radius1=3, targetimgsize=300})
ability.template.mastery[6]:initdescript(
"Thunderbolt",
"Begin charging a target #radius1 area with energy. After #dur1, strike the area with lightning for #dmg1 #type1 damage. "
.."If an enemy dies, restore #perc2 of your max mana.")
ability.template.mastery[7] = ability:new("A02O", "war3mapImported\\mastery_ability-icons_07.blp")
ability.template.mastery[7]:initfields({type1=dmg.type[dmg_fire], cd=10, cost=25, dmg1=264, radius1=3, radius2=3, dur1=8, dur2=3})
ability.template.mastery[7]:initdescript(
"Ignite",
"Burn all targets within #radius1 for #dmg1 #type1 damage over #dur1. If an enemy dies, they combust and stun targets within #radius2 for #dur2.")
ability.template.mastery[8] = ability:new("A02W", "war3mapImported\\mastery_ability-icons_08.blp")
ability.template.mastery[8]:initfields({type1=dmg.type[dmg_fire], cd=4, cost=35, dmg1=164, radius1=3, dur1=12, perc1=50, frost="Frost", targetimgsize=300})
ability.template.mastery[8]:initdescript(
"Rain of Fire",
"Deal #dmg1 #type1 damage to targets in a #radius1 radius over #dur1. Deals #perc1 bonus damage to targets with #frost resistance.")
ability.template.mastery[9] = ability:new("A02J", "war3mapImported\\mastery_ability-icons_09.blp")
ability.template.mastery[9]:initfields({type1=dmg.type[dmg_shadow], cd=6, cost=30, dmg1=14, range1=8, radius1=3, dur1=12, count1=2, targetimgsize=50})
ability.template.mastery[9]:initdescript(
"Demon Bolt",
"Launch a missile which explodes on the first target it strikes, summoning #count1 demons for #dur1 which attack nearby targets"
.." for #dmg1 #type1 damage.")
ability.template.mastery[10] = ability:new("A02R", "war3mapImported\\mastery_ability-icons_10.blp")
ability.template.mastery[10]:initfields({type1=dmg.type[dmg_shadow], type2=dmg.type[dmg_fire], dmg1=48, dmg2=36, cd=nil, cost=20, radius1=3, range1=8})
ability.template.mastery[10]:initdescript(
"Shadowflame",
"Launch fel magic which travels #range1, dealing #dmg1 #type1 and #dmg2 #type2 damage. When the wave reaches max range, it returns"
.." to you, damaging targets again.")
ability.template.mastery[11] = ability:new("A02A", "war3mapImported\\mastery_ability-icons_11.blp")
ability.template.mastery[11]:initfields({type1=dmg.type[dmg_phys], dmg1=196, cost=50, cd=30, perc1=15, perc2=300, castrange=125, radius1=3})
ability.template.mastery[11]:initdescript(
"Eradicate",
"Deal #dmg1 #type1 damage to targets within #radius1. If they are under #perc1 health, they take #perc2 more damage. "
.."If an enemy dies, this ability's cooldown is reset.")
ability.template.mastery[12] = ability:new("A02S", "war3mapImported\\mastery_ability-icons_12.blp")
ability.template.mastery[12]:initfields({type1=dmg.type[dmg_phys], cost=30, cd=4, perc1=400, perc2=200, armor="armor rating", castrange=125, radius1=3})
ability.template.mastery[12]:initdescript(
"Blood Rite",
"Deal #perc1 of your #armor as #type1 damage to targets within #radius1 and restore health equal to #perc2 of the damage dealt.")
ability.template.mastery[13] = ability:new("A02M", "war3mapImported\\mastery_ability-icons_13.blp")
ability.template.mastery[13]:initfields({type1=dmg.type[dmg_phys], cost=25, cd=45, perc1=10, perc2=25, perc3=40, dur1=6, revive="revive", castrange=125, radius1=3})
ability.template.mastery[13]:initdescript(
"Bull Rat",
"Reduce all damage taken by #perc1 for #dur1. If you fall below #perc2 health while active, instantly heal for #perc3 health and consume the effect. "
.." While active, nearby enemies are knocked back #radius1.")
ability.template.mastery[14] = ability:new("A02T", "war3mapImported\\mastery_ability-icons_14.blp")
ability.template.mastery[14]:initfields({type1=dmg.type[dmg_phys], cost=20, cd=6, perc1=10, perc3=100, dur1=3, radius1=10})
ability.template.mastery[14]:initdescript(
"Rejuvenate",
"Restore #perc1 health instantly, plus an additional #perc1 of your max health over #dur1.")
ability.template.mastery[15] = ability:new("A02L", "war3mapImported\\mastery_ability-icons_15.blp")
ability.template.mastery[15]:initfields({dmg1=48, cost=30, cd=30, dur1=15, dur2=3})
ability.template.mastery[15]:initdescript(
"Energy Shield",
"Gain a protective shield for #dur1. While active, the shield is powered every #dur2 to absorb #dmg1 damage for #dur2.")
ability.template.mastery[16] = ability:new("A02U", "war3mapImported\\mastery_ability-icons_16.blp")
ability.template.mastery[16]:initfields({cost=0, cd=8, radius1=6, dur1=1.0, dmg1=124, castrange=600, targetimgsize=80})
ability.template.mastery[16]:initdescript(
"Rat Dash",
"Quickly dash to a target area up to #radius1 away and absorb the next #dmg1 damage taken for #dur1.")
end
function ability.template:buildmasteryspells(p)
local p = p
self.mastery.p = p
-- "Forcewave"
ability.template.mastery[1].spell = spell:new(casttype_instant, p, self.mastery:mastabilid(1))
ability.template.mastery[1].spell.orderstr = 'cloudoffog'
ability.template.mastery[1].spell.id = 1
ability.template.mastery[1].spell:addaction(function()
local s = ability.template.mastery[1].spell
s.caster = kobold.player[p].unit
local grp = g:newbyunitloc(p, g_type_dmg, s.caster, self.mastery:getmastradius(1, "radius1"))
speff_fspurp:playu(s.caster)
spell:gdmg(p, grp, self.mastery:getmastfield(1, "type1"), self.mastery:getmastfield(1, "dmg1"))
spell:gbuff(grp, bf_slow, self.mastery:getmastfield(1, "dur1"))
kb:new_group(grp, s.casterx, s.castery, self.mastery:getmastradius(1, "range1"))
grp:destroy()
end)
-- "Unstable Arcana"
ability.template.mastery[2].spell = spell:new(casttype_instant, p, self.mastery:mastabilid(2))
ability.template.mastery[2].spell.active = false
ability.template.mastery[2].spell.casts = 0
ability.template.mastery[2].spell.trig = trg:newspelltrig(kobold.player[p].unit, function()
if ability.template.mastery[2].spell.active then
ability.template.mastery[2].spell.casts = ability.template.mastery[2].spell.casts + 1
if ability.template.mastery[2].spell.casts >= 3 then -- every 3rd cast
ability.template.mastery[2].spell.casts = 0
missile:create_in_radius(utils.unitx(kobold.player[p].unit), utils.unity(kobold.player[p].unit), kobold.player[p].unit,
mis_shot_arcane.effect, self.mastery:getmastfield(2, "dmg1"), self.mastery:getmastfield(2, "type1"), 8, 600.0, nil, 125.0, 90, true, false)
end
end
end)
ability.template.mastery[2].spell.orderstr = 'wispharvest'
ability.template.mastery[2].spell.id = 2
ability.template.mastery[2].spell:addaction(function()
local s = ability.template.mastery[2].spell
s.caster = kobold.player[p].unit
local dur1 = self.mastery:getmastfield(2, "dur1")
ability.template.mastery[2].spell.casts = 0
ability.template.mastery[2].spell.active = true
for dmgtypeid = 1,6 do -- enhance all ids except physical.
if dmgtypeid ~= 6 then
s:enhancedmgtype(dmgtypeid, self.mastery:getmastfield(2, "perc1"), dur1, p, true)
end
end
speff_bluglow:attachu(s.caster, dur1, 'chest')
utils.timed(dur1, function() ability.template.mastery[2].spell.active = false end)
buffy:add_indicator(p, "Unstable Arcana", "ReplaceableTextures\\CommandButtons\\BTNStarfall.blp", dur1,
"Added elemental damage, abilities unleash arcane missiles")
end)
-- "Icebound Globe"
ability.template.mastery[3].spell = spell:new(casttype_point, p, self.mastery:mastabilid(3))
ability.template.mastery[3].spell.orderstr = 'controlmagic'
ability.template.mastery[3].spell.id = 3
ability.template.mastery[3].spell:addaction(function()
local s = ability.template.mastery[3].spell
s.caster = kobold.player[p].unit
speff_frostnova:playu(s.caster)
local dur = self.mastery:getmastfield(3, "dur1")
local r = self.mastery:getmastradius(3, "radius1")
local mis = s:pmissiletargetxy(self.mastery:getmastrange(3, "range1"), orb_eff_frost, self.mastery:getmastfield(3, "dmg1"))
mis.dmgtype = self.mastery:getmastfield(3, "type1")
mis.vel = 17
mis.expl = true
mis.explfunc = function()
local grp = g:newbyxy(p, g_type_dmg, mis.x, mis.y, r)
spell:gbuff(grp, bf_freeze, dur)
speff_frostnova:playradius(r/2, 3, mis.x, mis.y)
speff_frostnova:playradius(r, 5, mis.x, mis.y)
grp:destroy()
end
end)
-- "Glaciate"
ability.template.mastery[4].spell = spell:new(casttype_instant, p, self.mastery:mastabilid(4))
ability.template.mastery[4].spell.orderstr = 'doom'
ability.template.mastery[4].spell.id = 4
ability.template.mastery[4].spell:addaction(function()
local s = ability.template.mastery[4].spell
s.caster = kobold.player[p].unit
speff_frostnova:playu(s.caster)
local dmg1 = self.mastery:getmastfield(4, "dmg1")
local dur1 = self.mastery:getmastfield(4, "dur1")
local grp = g:newbyxy(p, g_type_dmg, s.casterx, s.castery, self.mastery:getmastradius(4, "radius1"))
grp:action(function()
if utils.isancient(grp.unit) then
s:tdmg(p, grp.unit, self.mastery:getmastfield(4, "type1"), dmg1*(1 + self.mastery:getmastfield(4, "perc1")/100))
else
bf_freeze:apply(grp.unit, dur1)
bf_stun:apply(grp.unit, dur1)
speff_iceblock:attachu(grp.unit, dur1, 'origin')
s:tdmg(p, grp.unit, self.mastery:getmastfield(4, "type1"), dmg1)
end
end)
grp:destroy()
end)
-- "Magnet Rod"
ability.template.mastery[5].spell = spell:new(casttype_instant, p, self.mastery:mastabilid(5))
ability.template.mastery[5].spell.orderstr = 'creepdevour'
ability.template.mastery[5].spell.id = 5
ability.template.mastery[5].spell:addaction(function()
local s = ability.template.mastery[5].spell
s.caster = kobold.player[p].unit
local dur1 = self.mastery:getmastfield(5, "dur1")
local grp = g:newbyxy(p, g_type_dmg, s.casterx, s.castery, self.mastery:getmastradius(5, "radius1"))
mis_hammerred:playu(s.caster)
spell:gbuff(grp, bf_stun, dur1)
spell:gdmg(p, grp, self.mastery:getmastfield(5, "type1"), self.mastery:getmastfield(5, "dmg1"))
-- grp:action(function() buffy:gen_cast_unit(s.caster, FourCC('A03A'), 'chainlightning', grp.unit) end)
kb:new_pullgroup(grp, s.casterx, s.castery, 0.84)
grp:destroy()
end)
-- "Thunderbolt"
ability.template.mastery[6].spell = spell:new(casttype_point, p, self.mastery:mastabilid(6))
ability.template.mastery[6].spell.orderstr = 'cyclone'
ability.template.mastery[6].spell.id = 6
ability.template.mastery[6].spell:addaction(function()
local s = ability.template.mastery[6].spell
s.caster = kobold.player[p].unit
speff_charging:play(s.targetx, s.targety, self.mastery:getmastfield(6, "dur1"), 1.33)
utils.timed(self.mastery:getmastfield(6, "dur1"), function()
utils.debugfunc(function()
local grp = g:newbyxy(p, g_type_dmg, s.targetx, s.targety, self.mastery:getmastradius(6, "radius1"))
grp:action(function()
if utils.isalive(grp.unit) then
s:tdmg(p, grp.unit, self.mastery:getmastfield(6, "type1"), self.mastery:getmastfield(6, "dmg1"))
if not utils.isalive(grp.unit) then -- refund partial manacost if death by dmg.
utils.addmanap(s.caster, 3.0, true)
end
end
end)
speff_stormlght:play(s.targetx, s.targety)
grp:destroy()
end, "Thunderbolt")
end)
end)
-- "Ignite"
ability.template.mastery[7].spell = spell:new(casttype_instant, p, self.mastery:mastabilid(7))
ability.template.mastery[7].spell.teffect = {}
ability.template.mastery[7].spell.orderstr = 'coupletarget'
ability.template.mastery[7].spell.id = 7
ability.template.mastery[7].spell:addaction(function()
ability.template.mastery[7].spell.caster = kobold.player[p].unit
local s = ability.template.mastery[7].spell
local dur1, dur2 = self.mastery:getmastfield(7, "dur1"), self.mastery:getmastfield(7, "dur2")
local dmg1 = math.floor(self.mastery:getmastfield(7, "dmg1")/dur1)
local r = self.mastery:getmastradius(7, "radius1")
local grp = g:newbyunitloc(p, g_type_dmg, s.caster, self.mastery:getmastradius(1, "radius1"))
grp:action(function()
if utils.isalive(grp.unit) then
local e = speff_burning:attachu(s.target, dur1, 'overhead')
local grpunit = grp.unit
utils.timedrepeat(1.0, dur1, function()
if grpunit and utils.isalive(grpunit) then
s:tdmg(p, grpunit, self.mastery:getmastfield(7, "type1"), dmg1)
else
speff_fireroar:playu(grpunit)
local grp2 = g:newbyxy(utils.powner(s.caster), g_type_dmg, utils.unitx(grpunit), utils.unity(grpunit), r)
grp2:action(function()
s:gbuff(grp2, bf_stun, dur2)
end)
grp2:destroy()
DestroyEffect(e)
ReleaseTimer()
end
end, function() if e then DestroyEffect(e) end end)
end
end)
speff_fireroar:playu(s.caster)
grp:destroy()
end)
-- "Rain of Fire"
ability.template.mastery[8].spell = spell:new(casttype_point, p, self.mastery:mastabilid(8))
ability.template.mastery[8].spell.orderstr = 'creepheal'
ability.template.mastery[8].spell.id = 8
ability.template.mastery[8].spell:addaction(function()
local s = ability.template.mastery[8].spell
s.caster = kobold.player[p].unit
local x,y = s.targetx, s.targety
local radius = self.mastery:getmastradius(8, "radius1")
local dur1 = self.mastery:getmastfield(8, "dur1")
local dmg1 = self.mastery:getmastfield(8, "dmg1")/3/dur1
local dmgtype= self.mastery:getmastfield(8, "type1")
local a,r,x2,y2
utils.timedrepeat(0.33, dur1*3, function()
a = math.random() * 2 * 3.141
r = radius * math.sqrt(math.random())
x2 = x + (r * math.cos(a))
y2 = y + (r * math.sin(a))
speff_rainfire:play(x2, y2)
mis_fireball:play(x2, y2)
s:gdmgxy(p, dmgtype, dmg1, x, y, radius)
local grp = g:newbyxy(p, g_type_dmg, x, y, radius)
grp:action(function()
if BlzGetUnitIntegerField(grp.unit, UNIT_IF_DEFENSE_TYPE) == GetHandleId(DEFENSE_TYPE_NORMAL) then -- has frost defense type.
s:tdmg(p, grp.unit, dmgtype, dmg1*1.5)
else
s:tdmg(p, grp.unit, dmgtype, dmg1)
end
end)
grp:destroy()
end, function() s = nil dmgtype = nil end)
end)
-- "Demon Bolt"
ability.template.mastery[9].spell = spell:new(casttype_point, p, self.mastery:mastabilid(9))
ability.template.mastery[9].spell.orderstr = 'blizzard'
ability.template.mastery[9].spell.id = 9
ability.template.mastery[9].spell:addaction(function()
local s = ability.template.mastery[9].spell
s.caster = kobold.player[p].unit
local dmg1 = self.mastery:getmastfield(9, "dmg1")
local mis = ability.template.mastery[9].spell:pmissiletargetxy(self.mastery:getmastrange(9, "range1"), mis_boltpurp, dmg1)
mis.dmgtype = self.mastery:getmastfield(9, "type1")
mis.vel = 16
mis.expl = true
mis.explfunc = function()
speff_explpurp:play(mis.x, mis.y)
for i = 1,self.mastery:getmastfield(9, "count1") do
local x,y = utils.projectxy(mis.x, mis.y, 128, math.random(0,360))
local unit = utils.unitatxy(p, x, y, 'n00J', math.random(0,360))
BlzSetUnitBaseDamage(unit, dmg1, 0)
UnitApplyTimedLife(unit, FourCC('BTLF'), self.mastery:getmastfield(9, "dur1"))
speff_deathpact:play(x, y)
end
end
end)
-- "Shadowflame"
ability.template.mastery[10].spell = spell:new(casttype_point, p, self.mastery:mastabilid(10))
ability.template.mastery[10].spell.orderstr = 'creepthunderclap'
ability.template.mastery[10].spell.id = 10
ability.template.mastery[10].spell:addaction(function()
local s = ability.template.mastery[10].spell
s.caster = kobold.player[p].unit
local x,y = s.casterx, s.castery
local dmg1 = self.mastery:getmastfield(10, "dmg1")
local dmg2 = self.mastery:getmastfield(10, "dmg2")
local smis = s:pmissiletargetxy(self.mastery:getmastrange(9, "range1"), mis_voidballhge, dmg1)
smis.vel = 24
smis.x, smis.y = utils.projectxy(utils.unitx(s.caster), utils.unity(s.caster), 50.0, s:angletotarget()-90.0)
smis.dmgtype = self.mastery:getmastfield(10, "type1")
smis.reflect = true
smis:initpiercing()
local fmis = s:pmissiletargetxy(self.mastery:getmastrange(9, "range1"), mis_fireballhge, dmg2)
fmis.vel = 24
fmis.dmgtype = self.mastery:getmastfield(10, "type2")
fmis.x, fmis.y = utils.projectxy(utils.unitx(s.caster), utils.unity(s.caster), 50.0, s:angletotarget()+90.0)
fmis.reflect = true
fmis:initpiercing()
end)
-- "Eradicate"
ability.template.mastery[11].spell = spell:new(casttype_instant, p, self.mastery:mastabilid(11))
ability.template.mastery[11].spell.orderstr = 'devourmagic'
ability.template.mastery[11].spell.id = 11
ability.template.mastery[11].spell.dmgfunc = function(unit)
if utils.getlifep(unit) < 15 then
speff_beamred:playu(unit)
spell:tdmg(p, unit, self.mastery:getmastfield(11, "type1"), self.mastery:getmastfield(11, "dmg1")*3.0)
if not utils.isalive(unit) then
utils.timed(0.21, function() BlzEndUnitAbilityCooldown(s.caster, s.dummycode) s.cdactive = false end)
end
end
end
ability.template.mastery[11].spell:addaction(function()
local s = ability.template.mastery[11].spell
s.caster = kobold.player[p].unit
local grp = g:newbyunitloc(p, g_type_dmg, s.caster, self.mastery:getmastradius(1, "radius1"))
speff_redburst:playu(s.caster)
spell:gdmg(p, grp, self.mastery:getmastfield(11, "type1"), self.mastery:getmastfield(1, "dmg1"), ability.template.mastery[11].spell.dmgfunc)
kb:new_group(grp, s.casterx, s.castery, self.mastery:getmastradius(1, "range1"))
grp:destroy()
end)
-- "Blood Rite"
ability.template.mastery[12].spell = spell:new(casttype_instant, p, self.mastery:mastabilid(12))
ability.template.mastery[12].spell.orderstr = 'deathcoil'
ability.template.mastery[12].spell.id = 12
ability.template.mastery[12].spell:addaction(function()
local s = ability.template.mastery[12].spell
s.caster = kobold.player[p].unit
local dmg1 = math.floor(((kobold.player[p][p_stat_armor]-(kobold.player[p].level-1)*0.8)))
if dmg1 > 0 then
local grp = g:newbyxy(p, g_type_dmg, s.casterx, s.castery , self.mastery:getmastradius(12, "radius1"))
grp:action(function()
s:tdmg(p, grp.unit, self.mastery:getmastfield(12, "type1"), dmg1)
utils.addlifeval(s.caster, dmg1*2, true, s.caster)
speff_redburst:playu(grp.unit)
end)
speff_bloodsplat:playu(s.caster)
grp:destroy()
end
end)
-- "Bull Rat"
ability.template.mastery[13].spell = spell:new(casttype_instant, p, self.mastery:mastabilid(13))
ability.template.mastery[13].spell.orderstr = 'cannibalize'
ability.template.mastery[13].spell.id = 13
ability.template.mastery[13].spell:addaction(function()
local s = ability.template.mastery[13].spell
s.caster = kobold.player[p].unit
local dur1 = self.mastery:getmastfield(13, "dur1")
local e = sp_enchant_eff[4]:attachu(s.caster, dur1, 'chest', 0.8)
local perc2,perc3 = self.mastery:getmastfield(13, "perc2"),self.mastery:getmastfield(13, "perc3")
local c = 0
local consumedbool = false
speff_roar:playu(s.caster)
kobold.player[p][p_stat_dmg_reduct] = kobold.player[p][p_stat_dmg_reduct] + self.mastery:getmastfield(13, "perc1")
tooltip:updatecharpage(p)
utils.timedrepeat(0.03, dur1*33, function()
c = c + 1
if c >= 5 then
c = 0
local grp = g:newbyxy(p, g_type_dmg, utils.unitx(s.caster), utils.unity(s.caster), 185.0)
kb:new_group(grp, utils.unitx(s.caster), utils.unity(s.caster), self.mastery:getmastradius(13, "radius1"), 0.66)
grp:destroy()
end
if not kobold.player[p].downed and utils.getlifep(s.caster) <= perc2 and not consumedbool then -- below 10% detected
consumedbool = true
kobold.player[p][p_stat_dmg_reduct] = kobold.player[p][p_stat_dmg_reduct] - self.mastery:getmastfield(13, "perc1")
tooltip:updatecharpage(p)
utils.addlifep(s.caster, perc3, true, kobold.player[p].unit)
speff_resurrect:playu(s.caster)
DestroyEffect(e)
ReleaseTimer()
end
end, function()
if not consumedbool then
kobold.player[p][p_stat_dmg_reduct] = kobold.player[p][p_stat_dmg_reduct] - self.mastery:getmastfield(13, "perc1")
tooltip:updatecharpage(p)
end
end)
buffy:add_indicator(p, "Bull Rat", "ReplaceableTextures\\CommandButtons\\BTNReincarnation.blp", dur1,
"Reduces damage taken, restores health when low")
end)
-- "Rejuvenate"
ability.template.mastery[14].spell = spell:new(casttype_instant, p, self.mastery:mastabilid(14))
ability.template.mastery[14].spell.orderstr = 'rejuvination'
ability.template.mastery[14].spell.id = 14
ability.template.mastery[14].spell:addaction(function()
local s = ability.template.mastery[14].spell
s.caster = kobold.player[p].unit
local dur1 = self.mastery:getmastfield(14, "dur1")
local amt = (utils.maxlife(s.caster)*0.10)/dur1
local caster = s.caster -- leave this, have to instantiate or timer bugs.
speff_rejuv:attachu(caster, dur1, 'origin')
utils.addlifep(caster, 10.0, true, caster)
utils.timedrepeat(1.0, dur1, function()
utils.addlifeval(caster, amt, true, caster)
end, function() s = nil end)
buffy:add_indicator(p, "Rejuvenate", "ReplaceableTextures\\CommandButtons\\BTNRejuvenation.blp", dur1,
"Recovering "..tostring(math.floor(amt)).." health per sec")
end)
-- "Energy Shield"
ability.template.mastery[15].spell = spell:new(casttype_instant, p, self.mastery:mastabilid(15))
ability.template.mastery[15].spell.orderstr = 'breathoffire'
ability.template.mastery[15].spell.id = 15
ability.template.mastery[15].spell:addaction(function()
local s = ability.template.mastery[15].spell
s.caster = kobold.player[p].unit
local dmg1 = self.mastery:getmastfield(15, "dmg1")
local dur1 = self.mastery:getmastfield(15, "dur1")
local dur2 = self.mastery:getmastfield(15, "dur2")
local e1 = mis_lightnorb:attachu(s.caster, dur1, 'hand,left')
local e2 = mis_lightnorb:attachu(s.caster, dur1, 'hand,right')
local e3 = mis_lightnorb:attachu(s.caster, dur1, 'overhead')
dmg.absorb:new(s.caster, dur2, { all = dmg1 }, speff_uberblu)
utils.timedrepeat(dur2, math.floor(dur1/dur2), function()
utils.debugfunc(function()
if not kobold.player[p].downed then
dmg.absorb:new(s.caster, dur2, { all = dmg1 }, speff_uberblu)
buffy:add_indicator(p, "Energy Shield Absorb", "ReplaceableTextures\\CommandButtons\\BTNOrbOfLightning.blp", dur2,
"Absorbs up to "..tostring(dmg1).." damage")
else
DestroyEffect(e1) DestroyEffect(e2) DestroyEffect(e3)
ReleaseTimer()
end
end, "energy shield")
end)
buffy:add_indicator(p, "Energy Shield", "ReplaceableTextures\\CommandButtons\\BTNLightningShield.blp", dur1,
"Absorbs damage every "..tostring(dur2).." sec")
end)
-- "Rat Dash"
ability.template.mastery[16].spell = spell:new(casttype_point, p, self.mastery:mastabilid(16))
ability.template.mastery[16].spell.orderstr = 'blink'
ability.template.mastery[16].spell.id = 16
ability.template.mastery[16].spell:addaction(function()
local s = ability.template.mastery[16].spell
s.caster = kobold.player[p].unit
local e = sp_enchant_eff[2]:attachu(s.caster, 0.24, 'chest')
if not spell:slide(s.caster, s:disttotarget(), 0.24, s.targetx, s.targety, nil, function()
dmg.absorb:new(s.caster, self.mastery:getmastfield(16, "dur1"), { all = self.mastery:getmastfield(16, "dmg1") }) end) then DestroyEffect(e) end
buffy:add_indicator(p, "Rat Dash", "ReplaceableTextures\\CommandButtons\\BTNEvasion.blp", self.mastery:getmastfield(16, "dur1"),
"Absorbs up to "..tostring(self.mastery:getmastfield(16, "dmg1")).." damage")
end)
end
--[[
mastery lookup helpers:
--]]
function ability.template:mastabilid(id)
return self[id].code
end
function ability.template:getmastfield(id, fieldname)
return self[id][fieldname]
end
function ability.template:getmastradius(id, fieldname)
-- *note: this converts meters to in-game units (x * 100).
return math.floor(self[id][fieldname]*(1+kobold.player[self.p][p_stat_abilarea]/100))*100
end
function ability.template:getmastrange(id, fieldname)
return math.floor(self[id][fieldname]*100) -- *note, we get missile range stat in spell helpers!
end
--[[
standard lookup helpers:
--]]
function ability.template:abilid(hotkeyid, colid)
return self[hotkeyid][colid].code -- ability code
end
function ability.template:getfield(hotkeyid, colid, fieldname)
return self[hotkeyid][colid][fieldname]
end
function ability.template:getradius(hotkeyid, colid, fieldname)
-- *note: this converts meters to in-game units (x * 100).
return math.floor(self[hotkeyid][colid][fieldname]*(1+kobold.player[self.p][p_stat_abilarea]/100))*100
end
function ability.template:getrange(hotkeyid, colid, fieldname)
return math.floor(self[hotkeyid][colid][fieldname]*(1+kobold.player[self.p][p_stat_mislrange]/100))*100 -- *note, we get missile range stat in spell helpers!
end
function ability.template:loadtunneler(p)
local Q_id,W_id,E_id,R_id,p = 1,2,3,4,p
-----------------------------------Q-----------------------------------
-- TUNNEL BOMB --
self.spells[Q_id][1] = spell:new(casttype_point, p, self:abilid(Q_id, 1))
self.spells[Q_id][1].rect = Rect(0,0,0,0)
self.spells[Q_id][1]:uiregcooldown(self:getfield(Q_id, 1, "cd"), Q_id)
self.spells[Q_id][1]:addaction(function()
local s = self.spells[Q_id][1]
local r = self:getradius(Q_id, 1, "radius1")
local dmg = self:getfield(Q_id, 1, "dmg1")
local x,y = s.targetx, s.targety -- *note: need to instantiate for spammable abils.
local landfunc = function()
speff_explode2:play(x, y)
s:gdmgxy(p, self:getfield(Q_id, 1, "type1"), dmg, x, y, r)
utils.setrectradius(s.rect, x, y, r)
utils.dmgdestinrect(s.rect, math.floor(dmg*(self:getfield(Q_id, 1, "perc1")/100)))
end
local mis = s:pmissilearc(mis_grenadeoj, 0)
mis.explfunc = landfunc
end)
-- PICKERANG --
self.spells[Q_id][2] = spell:new(casttype_point, p, self:abilid(Q_id, 2))
self.spells[Q_id][2].rect = Rect(0,0,0,0)
self.spells[Q_id][2]:uiregcooldown(self:getfield(Q_id, 2, "cd"), Q_id)
self.spells[Q_id][2]:addaction(function()
local s = self.spells[Q_id][2]
local d = self:getrange(Q_id, 2, "range1")
local r = self:getradius(Q_id, 2, "radius1")
local dmg = self:getfield(Q_id, 2, "dmg1")/2
local x,y = s.targetx, s.targety -- *note: need to instantiate for spammable abils.
for i = 1,2 do
local mis = s:pmissileangle(d, mis_hammergreen, 0)
local i = i
mis.vel = 28
mis.x, mis.y = utils.projectxy(mis.x, mis.y, 48.0, mis.angle)
if i == 1 then mis.angle = mis.angle + 45 else mis.angle = mis.angle - 45 end
mis.time = self:getfield(Q_id, 2, "dur1")
mis:initpiercing()
mis:initduration()
speff_defend:playu(s.caster)
local hitfunc = function(unit)
utils.debugfunc(function()
if utils.isalive(unit) and utils.slowed(unit) then
-- NOTE: this will give a false positive of being "broken" on Ymmud because he ignores buffs.
self:getfield(Q_id, 2, "type1"):pdeal(p, dmg*0.25, unit)
end
end, "hit")
end
mis.func = function(mis)
utils.debugfunc(function()
if math.fmod(mis.elapsed, 8) == 0 then
s:gdmgxy(p, self:getfield(Q_id, 2, "type1"), dmg, mis.x, mis.y, r, hitfunc)
end
if i == 1 then mis.angle = mis.angle - 5.33 else mis.angle = mis.angle + 5.33 end
end, "mis")
end
end
end)
-- PRIMAL CANDLE --
self.spells[Q_id][3] = spell:new(casttype_point, p, self:abilid(Q_id, 3))
self.spells[Q_id][3].rect = Rect(0,0,0,0)
self.spells[Q_id][3]:uiregcooldown(self:getfield(Q_id, 3, "cd"), Q_id)
self.spells[Q_id][3]:addaction(function()
local s = self.spells[Q_id][3]
local r = self:getradius(Q_id, 3, "radius1")
local x1,y1= utils.projectxy(s.casterx, s.castery, r, s:angletotarget())
local x2,y2= utils.projectxy(s.casterx, s.castery, r*2, s:angletotarget())
local x3,y3= utils.projectxy(s.casterx, s.castery, r*3, s:angletotarget())
local t1,t2,t3 = math.random(2,4), math.random(2,4), math.random(2,4)
speff_expl_stack[t1]:play(x1, y1)
speff_expl_stack[t2]:play(x2, y2)
speff_expl_stack[t3]:play(x3, y3)
s:gdmgxy(p, dmg.type.stack[t1], self:getfield(Q_id, 3, "dmg1"), x1, y1, r)
s:gdmgxy(p, dmg.type.stack[t2], self:getfield(Q_id, 3, "dmg1"), x2, y2, r)
s:gdmgxy(p, dmg.type.stack[t3], self:getfield(Q_id, 3, "dmg1"), x3, y3, r)
utils.timed(0.21, function() -- repeat dmg
s:gdmgxy(p, dmg.type.stack[t1], self:getfield(Q_id, 3, "dmg1"), x1, y1, r)
s:gdmgxy(p, dmg.type.stack[t2], self:getfield(Q_id, 3, "dmg1"), x2, y2, r)
s:gdmgxy(p, dmg.type.stack[t3], self:getfield(Q_id, 3, "dmg1"), x3, y3, r)
end)
end)
-----------------------------------W-----------------------------------
-- ROCKSAW --
self.spells[W_id][1] = spell:new(casttype_point, p, self:abilid(W_id, 1))
self.spells[W_id][1].rect = Rect(0,0,0,0)
self.spells[W_id][1]:uiregcooldown(self:getfield(W_id, 1, "cd"), W_id)
self.spells[W_id][1]:addaction(function()
local s = self.spells[W_id][1]
local r = self:getradius(W_id, 1, "range1")
local dur = self:getfield(W_id, 1, "dur1")
local dmg = math.floor(self:getfield(W_id, 1, "dmg1")/3)
local ddmg = math.floor(dmg*(self:getfield(W_id, 1, "perc1")/100))
local x1,y1,e = 0,0,{}
local angleoffset = 90
local startangle = utils.anglexy(utils.unitx(s.caster), utils.unity(s.caster), s.targetx, s.targety) - angleoffset
for i = 1,tonumber(self:getfield(W_id, 1, "count1")) do
e[i] = {}
e[i].x, e[i].y = utils.projectxy(s.targetx, s.targety, 190.0, startangle + ((i-1)*angleoffset))
e[i].e = speff_arcglaive:play(e[i].x, e[i].y, dur)
speff_dustcloud:play(e[i].x, e[i].y)
end
utils.timedrepeat(0.03, dur*33 - 1, function(c)
utils.debugfunc(function()
if math.fmod(c, 10) == 0 then
for _,t in ipairs(e) do
s:gdmgxy(p, self:getfield(W_id, 1, "type1"), dmg, t.x, t.y, r)
if math.random(0,1) == 1 then speff_phys:play(t.x, t.y) else mis_rock:play(t.x, t.y) end
utils.setrectradius(s.rect, t.x, t.y, r)
utils.dmgdestinrect(s.rect, ddmg)
end
end
end, "rocksaw timer")
end, function()
for i,t in ipairs(e) do
if t.e then DestroyEffect(t.e) end
e[i] = nil
end
e = nil
end)
end)
-- HAMMERANG --
self.spells[W_id][2] = spell:new(casttype_point, p, self:abilid(W_id, 2))
self.spells[W_id][2].rect = Rect(0,0,0,0)
self.spells[W_id][2]:uiregcooldown(self:getfield(W_id, 2, "cd"), W_id)
self.spells[W_id][2]:addaction(function()
local s = self.spells[W_id][2]
local d = self:getrange(W_id, 2, "range1")
local r = self:getradius(W_id, 2, "radius1")
-- local dmg = self:getfield(W_id, 2, "dmg1")/2
local x,y = s.targetx, s.targety -- *note: need to instantiate for spammable abils.
for i = 1,2 do
local mis = s:pmissileangle(d, mis_hammerred, self:getfield(W_id, 2, "dmg1"))
local i = i
mis.vel = 28
mis.dmgtype = self:getfield(W_id, 2, "type1")
mis.x, mis.y = utils.projectxy(mis.x, mis.y, 48.0, mis.angle)
mis.time = self:getfield(W_id, 2, "dur1")
if i == 1 then mis.angle = mis.angle + 6 else mis.angle = mis.angle - 6 end
-- mis:initpiercing()
mis:initduration()
speff_defend:playu(s.caster)
mis.colfunc = function(mis)
if utils.isalive(mis.hitu) and not utils.stunned(mis.hitu) then
bf_slow:apply(mis.hitu, self:getfield(W_id, 2, "dur2"))
end
end
mis.func = function(mis) if i == 1 then mis.angle = mis.angle - 1.0 else mis.angle = mis.angle + 1.0 end end
end
end)
-- RIFT ROCKET --
self.spells[W_id][3] = spell:new(casttype_point, p, self:abilid(W_id, 3))
self.spells[W_id][3].rect = Rect(0,0,0,0)
self.spells[W_id][3]:uiregcooldown(self:getfield(W_id, 3, "cd"), W_id)
self.spells[W_id][3]:addaction(function()
local s = self.spells[W_id][3]
local dur = self:getfield(W_id, 3, "dur1")
local mis = s:pmissileangle(self:getrange(W_id, 3, "range1"), speff_gatearcane, 0)
local mis2 = s:pmissileangle(self:getrange(W_id, 3, "range1"), mis_runic, 0)
local mis3 = s:pmissileangle(self:getrange(W_id, 3, "range1"), mis_runic, 0)
local vel = 12.0
local dmg1 = self:getfield(W_id, 3, "dmg1")
local dmgtype1 = self:getfield(W_id, 3, "type1")
local dmgtype2 = self:getfield(W_id, 3, "type2")
local dmgtype3 = self:getfield(W_id, 3, "type3")
speff_explblu:playu(s.caster)
mis2.collide = false
mis3.collide = false
mis.x, mis.y = utils.projectxy(mis.x, mis.y, 24.0, mis.angle)
mis2.x, mis2.y = utils.projectxy(mis.x, mis.y, 164.0, mis.angle - 90)
mis3.x, mis3.y = utils.projectxy(mis.x, mis.y, 164.0, mis.angle + 90)
mis.dmgtype = self:getfield(W_id, 3, "type1")
mis2.vel, mis3.vel, mis.vel = vel, vel, vel
mis2.height, mis3.height = 145.0, 145.0
mis.radius = 328.0
mis.colfunc = function(mis)
utils.debugfunc(function()
bf_root:apply(mis.hitu, dur)
speff_lightn:playu(mis.hitu)
s:tdmg(p, mis.hitu, dmgtype1, dmg1)
s:tdmg(p, mis.hitu, dmgtype2, dmg1)
s:tdmg(p, mis.hitu, dmgtype3, dmg1)
end, "col")
end
mis:initpiercing()
end)
-----------------------------------E-----------------------------------
-- MOLE HOLE --
self.spells[E_id][1] = spell:new(casttype_instant, p, self:abilid(E_id, 1))
self.spells[E_id][1]:uiregcooldown(self:getfield(E_id, 1, "cd"), E_id)
self.spells[E_id][1]:addaction(function()
local s = self.spells[E_id][1]
local r = self:getradius(E_id, 1, "range1")
local dur = self:getfield(E_id, 1, "dur1")
local dmg = self:getfield(E_id, 1, "dmg1")
local e = speff_unitcirc:play(s.casterx, s.castery, dur)
bf_silence:apply(s.caster, dur)
utils.setinvul(s.caster, true)
utils.vertexhide(s.caster, true)
speff_dustcloud:playu(s.caster)
speff_nature:playu(s.caster)
utils.timedrepeat(0.03, dur*33, function()
if e then
utils.seteffectxy(e, utils.unitx(s.caster), utils.unity(s.caster))
else
ReleaseTimer()
end
end)
utils.timed(dur, function()
s:gdmgxy(p, self:getfield(W_id, 1, "type1"), dmg, utils.unitx(s.caster), utils.unity(s.caster), r)
utils.vertexhide(s.caster, false)
speff_dustcloud:playu(s.caster)
speff_quake:playu(s.caster)
if not map.manager.objiscomplete or scoreboard_is_active then
utils.setinvul(s.caster, false)
end
end)
buffy:add_indicator(p, "Mole Hole", "ReplaceableTextures\\CommandButtons\\BTNCryptFiendBurrow.blp", dur,
"Untouchable while underground")
end)
-- KOBOLT AUTO-MINER --
self.spells[E_id][2] = spell:new(casttype_point, p, self:abilid(E_id, 2))
self.spells[E_id][2].rect = Rect(0,0,0,0)
self.spells[E_id][2]:uiregcooldown(self:getfield(E_id, 2, "cd"), E_id)
self.spells[E_id][2]:addaction(function()
local s = self.spells[E_id][2]
local r = self:getradius(E_id, 2, "radius1")
local dmg = self:getfield(E_id, 2, "dmg1")/3
local bdmg = dmg*(self:getfield(E_id, 2, "perc1")/100)*3
local a = s:angletotarget()
local vel = 8.0
local c = math.floor(self:getrange(E_id, 2, "range1")/vel)
local unit = utils.unitatxy(p, s.casterx, s.castery, 'h009', a)
local i = 0
local x,y = s.casterx, s.castery
SetUnitAnimation(unit, "attack")
utils.timedrepeat(0.03, nil, function()
utils.debugfunc(function()
i = i + 1
if utils.isalive(unit) and i < c then
x,y = utils.projectxy(x, y, vel, a)
utils.setxy(unit, x, y)
if math.fmod(i, 10) == 0 then
s:gdmgxy(p, self:getfield(E_id, 2, "type1"), dmg, x, y, r, function(u) bf_slow:apply(u, self:getfield(E_id, 2, "dur1")) end)
QueueUnitAnimation(unit, "attack")
speff_dustcloud:playu(unit)
speff_quake:playu(unit)
utils.setrectradius(s.rect, x, y, r)
utils.dmgdestinrect(s.rect, bdmg)
end
else
ReleaseTimer()
RemoveUnit(unit)
end
end, "tmr")
end)
end)
-- KOBOLT AUTO TURRET --
self.spells[E_id][3] = spell:new(casttype_point, p, self:abilid(E_id, 3))
self.spells[E_id][3]:uiregcooldown(self:getfield(E_id, 3, "cd"), E_id)
self.spells[E_id][3]:addaction(function()
local s = self.spells[E_id][3]
local r = self:getradius(E_id, 3, "radius1")
local d = self:getrange(E_id, 3, "range1")
local dmg1 = kobold.player[p]:calcminiondmg(self:getfield(E_id, 3, "dmg1"))
local dur = self:getfield(E_id, 3, "dur1")
local unit = utils.unitatxy(p, s.targetx, s.targety, 'h00A', s:angletotarget())
speff_feral:play(s.targetx, s.targety)
utils.timedrepeat(0.5, nil, function()
if unit then
local x,y = utils.unitxy(unit)
local grp = g:newbyxy(p, g_type_dmg, x, y , r)
if grp:getsize() > 0 then
local rgrp = g:newbyrandom(1, grp)
rgrp:action(function()
local angle = utils.anglexy(x, y, utils.unitxy(rgrp.unit))
local r = math.random(2,4)
local mis = s:pmissile_custom_unitxy(unit, d, utils.unitx(rgrp.unit), utils.unity(rgrp.unit), mis_ele_stack[r], dmg1)
mis.dmgtype = dmg.type.stack[r]
mis.vel = 38
mis:initpiercing()
utils.face(unit, angle)
SetUnitAnimation(unit, 'attack')
QueueUnitAnimation(unit, 'stand')
end)
rgrp:destroy()
end
grp:destroy()
else
ReleaseTimer()
end
end)
utils.timed(dur, function()
KillUnit(unit)
unit = nil
end)
buffy:add_indicator(p, "'Kobolt' Turret", "ReplaceableTextures\\CommandButtons\\BTNHumanMissileUpTwo.blp", dur,
"Accompanied by a turret which shoots targets")
end)
-----------------------------------R-----------------------------------
-- WRECKING BALL --
self.spells[R_id][1] = spell:new(casttype_instant, p, self:abilid(R_id, 1))
self.spells[R_id][1].rect = Rect(0,0,0,0)
self.spells[R_id][1]:uiregcooldown(self:getfield(R_id, 1, "cd"), R_id)
self.spells[R_id][1]:addaction(function()
local s = self.spells[R_id][1]
local r = self:getradius(R_id, 1, "range1")
local dur = self:getfield(R_id, 1, "dur1")
local dmg = self:getfield(R_id, 1, "dmg1")/3
local e = mis_rock:create(s.casterx, s.castery, 3.33)
local c, i, x, y = dur*33, 0, s.casterx, s.castery
speff_quake:playu(s.caster)
utils.vertexhide(s.caster, true)
bf_silence:apply(s.caster, dur)
bf_ms:apply(s.caster, dur)
spell:enhanceresistpure(75.0, dur, p, false)
utils.timedrepeat(0.03, nil, function()
utils.debugfunc(function()
i = i + 1
x,y = utils.unitx(s.caster), utils.unity(s.caster)
if not kobold.player[s.p].downed and i < c then
utils.seteffectxy(e, x, y, utils.getcliffz(x, y, 75.0))
BlzSetSpecialEffectYaw(e, utils.getface(s.caster)*bj_DEGTORAD)
if math.fmod(i, 10) == 0 then
s:gdmgxy(p, self:getfield(R_id, 1, "type1"), dmg, x, y, r)
speff_quake:playu(s.caster)
kb:new_pgroup(p, x, y, r, dist, 1.0)
utils.setrectradius(s.rect, x, y, r)
utils.dmgdestinrect(s.rect, math.floor(dmg*(self:getfield(R_id, 1, "perc1")/100)))
end
else
s.active = false
DestroyEffect(e)
utils.vertexhide(s.caster, false)
ReleaseTimer()
end
end, "wrecking")
end)
buffy:add_indicator(p, "Wrecking Ball", "ReplaceableTextures\\CommandButtons\\BTNGolemStormBolt.blp", dur,
"Knocks back and damages nearby targets")
end)
-- THE BIG ONE --
self.spells[R_id][2] = spell:new(casttype_point, p, self:abilid(R_id, 2))
self.spells[R_id][2].rect = Rect(0,0,0,0)
self.spells[R_id][2]:uiregcooldown(self:getfield(R_id, 2, "cd"), R_id)
self.spells[R_id][2]:addaction(function()
local s = self.spells[R_id][2]
local r = self:getradius(R_id, 2, "radius1")
local r2 = r*0.33
local dur = self:getfield(R_id, 2, "dur1")
local dmg = self:getfield(R_id, 2, "dmg1")
local sc = 0.5
local e = speff_mine:create(s.targetx, s.targety, sc)
local i,c = 0, dur*33
local x,y = s.targetx, s.targety
speff_dustcloud:play(s.targetx, s.targety)
speff_feral:play(s.targetx, s.targety)
utils.timedrepeat(0.03, nil, function()
i = i + 1
if i < c then
sc = sc + 0.015
BlzSetSpecialEffectScale(e, sc)
else
s:gdmgxy(p, self:getfield(R_id, 2, "type1"), dmg, x, y, r)
utils.setrectradius(s.rect, x, y, r)
utils.dmgdestinrect(s.rect, dmg*(self:getfield(R_id, 2, "perc1")/100))
speff_explode:playradius(r2, 4, x, y)
utils.timed(0.12, function()
speff_explode2:playradius(r2+r2, 6, x, y)
utils.timed(0.12, function()
speff_explode2:playradius(r2+r2+r2, 8, x, y)
utils.timed(0.12, function()
speff_dirtexpl:playradius(r2+r2+r2+r2, 10, x, y)
end)
end)
end)
DestroyEffect(e)
ReleaseTimer()
end
end)
end)
-- TRIUMVIRATE --
self.spells[R_id][3] = spell:new(casttype_instant, p, self:abilid(R_id, 3))
self.spells[R_id][3]:uiregcooldown(self:getfield(R_id, 3, "cd"), R_id)
self.spells[R_id][3]:addaction(function()
local x,y,eff,a,inc,t,rand = {},{},{},{},0,0,0
local s = self.spells[R_id][3]
local d = self:getrange(R_id, 3, "range1")
local dur = self:getfield(R_id, 3, "dur1")
local dmg1 = self:getfield(R_id, 3, "dmg1")
local c = dur*25
for i = 1,3 do
a[i] = utils.getface(s.caster) + inc
x[i], y[i] = utils.projectxy(s.casterx, s.castery, 100.0, a[i])
eff[i] = speff_orbfire:create(x[i],y[i])
speff_exploj:play(x[i],y[i])
inc = inc + 120.0
end
local cx, cy = s.casterx, s.castery
utils.timedrepeat(0.04, nil, function()
t = t + 1
if t < c and utils.isalive(s.caster) then
cx,cy = utils.unitxy(s.caster)
for i = 1,3 do
a[i] = a[i] + 4
x[i], y[i] = utils.projectxy(cx, cy, 100.0, a[i])
utils.seteffectxy(eff[i], x[i], y[i])
if math.fmod(t,4) == 0 then
rand = math.random(2,4)
local mis = s:pmissileangle(d, mis_ele_stack2[rand], dmg1, a[i])
mis.x, mis.y = x[i], y[i]
mis.dmgtype = dmg.type.stack[rand]
mis.vel = 32
mis:initpiercing()
end
end
else
for i = 1,3 do DestroyEffect(eff[i]) end
x = nil y = nil eff = nil a = nil
ReleaseTimer()
end
end)
buffy:add_indicator(p, "Triumvirate", "ReplaceableTextures\\CommandButtons\\BTNStormEarth&Fire.blp", dur,
"Launching elemental missiles in a radius")
end)
end
function ability.template:loadgeomancer(p)
local Q_id,W_id,E_id,R_id,p = 1,2,3,4,p
-- helpers:
self.aihelper = function(self, x, y, _unit)
if self.spells[E_id][2].ai then
self.spells[E_id][2].ai:engage(x, y, _unit or nil)
end
end
-----------------------------------Q-----------------------------------
-- GEO-BOLT --
self.spells[Q_id][1] = spell:new(casttype_point, p, self:abilid(Q_id, 1))
self.spells[Q_id][1]:addaction(function()
local s = self.spells[Q_id][1]
local dur = self:getfield(Q_id, 1, "dur1")
local r = self:getradius(Q_id, 1, "radius1")
local mis = s:pmissiletargetxy(self:getrange(Q_id, 1, "range1"), mis_boltoj, self:getfield(Q_id, 1, "dmg1"))
mis.dmgtype = self:getfield(Q_id, 1, "type1")
mis.vel = 20
mis.expl = true
mis.explfunc = function()
local grp = g:newbyxy(p, g_type_dmg, mis.x, mis.y, r)
spell:gbuff(grp, bf_slow, dur, true)
speff_exploj:play(mis.x, mis.y, nil, 0.75)
grp:destroy()
self:aihelper(mis.x, mis.y)
end
end)
-- SHROOM BOLT --
self.spells[Q_id][2] = spell:new(casttype_point, p, self:abilid(Q_id, 2))
self.spells[Q_id][2]:addaction(function()
local s = self.spells[Q_id][2]
local dur = self:getfield(Q_id, 2, "dur1")
local heal1 = self:getfield(Q_id, 2, "heal1")
local dmg1 = self:getfield(Q_id, 2, "dmg1")
local r = self:getradius(Q_id, 2, "radius1")
local mis = s:pmissiletargetxy(self:getrange(Q_id, 2, "range1"), mis_boltgreen, self:getfield(Q_id, 2, "dmg1"))
local dmgtype = self:getfield(Q_id, 2, "type1")
mis.dmgtype = dmgtype
mis.vel = 20
mis.expl = true
mis.explfunc = function()
local x,y = mis.x, mis.y
speff_shroom:play(x, y, dur, 0.3)
speff_explgrn:play(x, y, nil, 0.75)
utils.timedrepeat(1.0, dur, function()
s:ghealxy(p, heal1, x, y, r)
s:gdmgxy(p, dmgtype, dmg1, x, y, r)
end, function()
dmgtype = nil
end)
self:aihelper(x, y)
end
end)
-- ARCANE BOLT --
self.spells[Q_id][3] = spell:new(casttype_point, p, self:abilid(Q_id, 3))
self.spells[Q_id][3]:addaction(function()
local s = self.spells[Q_id][3]
local mis = s:pmissiletargetxy(self:getrange(Q_id, 3, "range1"), mis_boltblue, self:getfield(Q_id, 3, "dmg1"))
mis.radius = self:getradius(Q_id, 3, "radius1")
mis.dmgtype = self:getfield(Q_id, 3, "type1")
mis.vel = 20
mis:initpiercing()
mis.colfunc = function()
mis.dmg = mis.dmg*1.03
BlzSetSpecialEffectScale(mis.effect, BlzGetSpecialEffectScale(mis.effect) + 0.04 )
self:aihelper(mis.x, mis.y)
end
end)
-----------------------------------W-----------------------------------
-- TREMOR --
self.spells[W_id][1] = spell:new(casttype_point, p, self:abilid(W_id, 1))
self.spells[W_id][1]:uiregcooldown(self:getfield(W_id, 1, "cd"), W_id)
self.spells[W_id][1]:addaction(function()
local x,y,a = {},{},{}
local s = self.spells[W_id][1]
local r = self:getradius(W_id, 1, "radius1")
local d = self:getrange(W_id, 1, "range1")
local dmg1 = self:getfield(W_id, 1, "dmg1")
local vel = 75.0
a[1] = s:angletotarget()
a[2] = a[1] - 35
a[3] = a[1] + 35
for i = 1,3 do
x[i], y[i] = s.casterx, s.castery
end
utils.timedrepeat(0.15, math.floor(d/vel), function()
utils.debugfunc(function()
for i = 1,3 do
local grp = g:newbyxy(p, g_type_dmg, x[i], y[i], r)
s:gbuff(grp, bf_slow, self:getfield(W_id, 1, "dur1"), true)
s:gdmg(p, grp, self:getfield(W_id, 1, "type1"), dmg1)
a[i] = a[i] + math.random(1,30)
a[i] = a[i] - math.random(1,30)
x[i], y[i] = utils.projectxy(x[i], y[i], vel, a[i])
speff_cataexpl:play(x[i], y[i], nil, 0.35)
grp:destroy()
end
end, "tmr")
end, function()
a = nil x = nil y = nil
end)
end)
-- FUNGAL INFUSION --
self.spells[W_id][2] = spell:new(casttype_instant, p, self:abilid(W_id, 2))
self.spells[W_id][2]:uiregcooldown(self:getfield(W_id, 2, "cd"), W_id)
self.spells[W_id][2]:addaction(function()
local s = self.spells[W_id][2]
local caster= s.caster
speff_radgreen:attachu(s.caster, self:getfield(W_id, 2, "dur2"), 'chest')
local tmr = utils.timedrepeat(1.0, self:getfield(W_id, 2, "dur2"), function()
if not kobold.player[p].downed then
utils.addlifep(caster, self:getfield(W_id, 2, "perc1")/3, true, caster)
else
ReleaseTimer(tmr)
end
end)
dmg.absorb:new(caster, self:getfield(W_id, 2, "dur1"), { all = self:getfield(W_id, 2, "dmg1"), }, speff_ubergrn)
buffy:new_absorb_indicator(p, "Fungal", "ReplaceableTextures\\CommandButtons\\BTNOrbOfVenom.blp", self:getfield(W_id, 2, "dmg1"), self:getfield(W_id, 2, "dur1"))
end)
-- ARCANE GOLEM --
self.spells[W_id][3] = spell:new(casttype_instant, p, self:abilid(W_id, 3))
self.spells[W_id][3]:uiregcooldown(self:getfield(W_id, 3, "cd"), W_id)
self.spells[W_id][3]:addaction(function()
end)
self.spells[W_id][3].ai = nil -- unit ai container
self.spells[W_id][3].pauseclone = false
self.spells[W_id][3].func = function() -- bolt listener
if self.spells[W_id][3].ai and self.spells[W_id][3].ai.unit and not self.spells[W_id][3].pauseclone then
local abilid = GetSpellAbilityId()
if abilid == FourCC('A00O') or abilid == FourCC('A00P') or abilid == FourCC('A00Q') --[[or abilid == FourCC('A02J')--]] then
self.spells[W_id][3].pauseclone = true
if abilid == FourCC('A00O') then
buffy:gen_cast_point(self.spells[W_id][3].ai.unit, self:abilid(Q_id, 1), 'shockwave', GetSpellTargetX(), GetSpellTargetY())
elseif abilid == FourCC('A00P') then
buffy:gen_cast_point(self.spells[W_id][3].ai.unit, self:abilid(Q_id, 2), 'shockwave', GetSpellTargetX(), GetSpellTargetY())
elseif abilid == FourCC('A00Q') then
buffy:gen_cast_point(self.spells[W_id][3].ai.unit, self:abilid(Q_id, 3), 'shockwave', GetSpellTargetX(), GetSpellTargetY())
-- elseif abilid == FourCC('A02J') then -- NOTE: bugged
-- buffy:gen_cast_point(self.spells[W_id][3].ai.unit, self.mastery:mastabilid(11), 'blizzard', GetSpellTargetX(), GetSpellTargetY())
end
PauseUnit(self.spells[W_id][3].ai.unit, true)
SetUnitAnimation(self.spells[W_id][3].ai.unit,'attack')
utils.timed(1.0, function()
if self.spells[W_id][3].ai.unit then PauseUnit(self.spells[W_id][3].ai.unit, false) ResetUnitAnimation(self.spells[W_id][3].ai.unit) end
end)
utils.timed(self:getfield(W_id, 3, "dur1"), function() self.spells[W_id][3].pauseclone = false end)
end
end
end
self.spells[W_id][3].trig = trg:newspelltrig(kobold.player[p].unit, self.spells[W_id][3].func)
self.spells[W_id][3]:uiregcooldown(self:getfield(W_id, 2, "cd"), W_id)
self.spells[W_id][3]:addaction(function()
local s = self.spells[W_id][3]
local dmg1 = self:getfield(W_id, 3, "dmg1")
local unit = utils.unitatxy(p, s.casterx, s.castery, 'h00C', math.random(0,360))
if self.spells[W_id][3].ai then self.spells[W_id][3].ai:releasecompanion() end
self.spells[W_id][3].ai = ai:new(unit)
self.spells[W_id][3].ai:initcompanion(1000.0, dmg1, p_stat_nature, self:abilid(W_id, 3))
BlzSetUnitBaseDamage(unit, dmg1, 0)
utils.smartmove(unit, s.caster)
speff_explgrn:play(utils.unitxy(unit))
end)
-- ARCANE INFUSION (old) --
-- self.spells[W_id][3] = spell:new(casttype_instant, p, self:abilid(W_id, 3))
-- self.spells[W_id][3]:uiregcooldown(self:getfield(W_id, 3, "cd"), W_id)
-- self.spells[W_id][3]:addaction(function()
-- local s = self.spells[W_id][3]
-- local dmg1 = self:getfield(W_id, 3, "dmg1")
-- local dur1 = self:getfield(W_id, 3, "dur1")
-- local perc = self:getfield(W_id, 3, "perc1")
-- local grp = g:newbyxy(p, g_type_ally, s.casterx, s.castery , self:getradius(W_id, 3, "radius1"))
-- grp:action(function()
-- if utils.ishero(grp.unit) and kobold.player[utils.powner(grp.unit)] then
-- local grpunit = grp.unit
-- kobold.player[utils.powner(grpunit)][p_stat_elels] = kobold.player[utils.powner(grpunit)][p_stat_elels] + perc
-- kobold.player[utils.powner(grpunit)][p_stat_physls] = kobold.player[utils.powner(grpunit)][p_stat_physls] + perc
-- dmg.absorb:new(grpunit, self:getfield(W_id, 3, "dur1"), { arcane = dmg1 }, speff_radblue, function()
-- kobold.player[utils.powner(grpunit)][p_stat_elels] = kobold.player[utils.powner(grpunit)][p_stat_elels] - perc
-- kobold.player[utils.powner(grpunit)][p_stat_physls] = kobold.player[utils.powner(grpunit)][p_stat_physls] - perc
-- end)
-- end
-- end)
-- grp:destroy()
-- end)
-----------------------------------E-----------------------------------
-- GEO-PORTAL --
self.spells[E_id][1] = spell:new(casttype_point, p, self:abilid(E_id, 1))
self.spells[E_id][1]:uiregcooldown(self:getfield(E_id, 1, "cd"), E_id)
self.spells[E_id][1]:addaction(function()
local x,y,eff,trig,dum,tpt = {},{},{},{},{},{}
local s = self.spells[E_id][1]
local r = self:getfield(E_id, 1, "radius1")*100 -- fixed value, no stat bonus
local d = self:getrange(E_id, 1, "range1")
local dur = self:getfield(E_id, 1, "dur1")
-- starting portal:
x[1], y[1] = utils.projectxy(s.casterx, s.castery, 160.0, s:angletotarget())
-- arrival portal:
x[2], y[2] = s.targetx, s.targety
-- generate triggers:
local enterfunc = function(unit, dumunit)
utils.debugfunc(function()
if utils.pnum(utils.powner(unit)) <= kk.maxplayers and IsUnitType(unit, UNIT_TYPE_HERO) and not tpt[utils.data(unit)] then
local dat = utils.data(unit)
utils.setxy(unit, utils.unitxy(dumunit))
port_yellowtp:playu(unit)
tpt[dat] = true
utils.stop(unit)
utils.timed(1.0, function() if tpt then tpt[dat] = false end end)
end
end, "trig")
end
for i = 1,2 do
speff_exploj:play(x[i], y[i])
dum[i] = utils.unitatxy(p, x[i], y[i], 'h008', 270.0)
trig[i] = CreateTrigger()
eff[i] = port_yellow:create(x[i], y[i])
end
TriggerRegisterUnitInRange(trig[1], dum[1], r, Condition(function() enterfunc(utils.trigu(), dum[2]) return false end))
TriggerRegisterUnitInRange(trig[2], dum[2], r, Condition(function() enterfunc(utils.trigu(), dum[1]) return false end))
utils.timed(dur, function()
for i = 1,2 do
DestroyEffect(eff[i])
TriggerClearConditions(trig[i])
DestroyTrigger(trig[i])
RemoveUnit(dum[i])
end
x = nil y = nil eff = nil trig = nil dum = nil tpt = nil enterfunc = nil
end)
end)
-- BOG GOLEM --
self.spells[E_id][2] = spell:new(casttype_instant, p, self:abilid(E_id, 2))
self.spells[E_id][2].ai = nil -- unit ai container
self.spells[E_id][2].pauseleap = false
self.spells[E_id][2].func = function() -- bolt listener
if self.spells[E_id][2].ai and self.spells[E_id][2].ai.unit and not self.spells[E_id][2].pauseleap then
local abilid = GetSpellAbilityId()
if abilid == FourCC('A00O') or abilid == FourCC('A00P') or abilid == FourCC('A00Q') or abilid == FourCC('A02J') then
self.spells[E_id][2].pauseleap = true
local x,y = utils.unitxy(self.spells[E_id][2].ai.unit)
local d,a = utils.distanglexy(x,y,GetSpellTargetX(), GetSpellTargetY())
utils.face(self.spells[E_id][2].ai.unit,a)
kbeff = kb:new(self.spells[E_id][2].ai.unit, a, d, 0.72) -- use getradius for throw dist.
kbeff.effect = nil
kbeff.arc = true
kbeff.destroydest = false
kbeff.terraincol = false
kbeff.endfunc = function()
speff_explgrn:playu(self.spells[E_id][2].ai.unit)
self.spells[E_id][2]:gdmgxy(p, self:getfield(E_id, 2, "type1"), self:getfield(E_id, 2, "dmg2"),
utils.unitx(self.spells[E_id][2].ai.unit), utils.unity(self.spells[E_id][2].ai.unit), 300.0)
end
utils.timed(self:getfield(E_id, 2, "dur1"), function() self.spells[E_id][2].pauseleap = false end)
end
end
end
self.spells[E_id][2].trig = trg:newspelltrig(kobold.player[p].unit, self.spells[E_id][2].func)
self.spells[E_id][2]:uiregcooldown(self:getfield(E_id, 2, "cd"), E_id)
self.spells[E_id][2]:addaction(function()
local s = self.spells[E_id][2]
local dmg1 = self:getfield(E_id, 2, "dmg1")
local unit = utils.unitatxy(p, s.casterx, s.castery, 'h00B', math.random(0,360))
if self.spells[E_id][2].ai then self.spells[E_id][2].ai:releasecompanion() end
self.spells[E_id][2].ai = ai:new(unit)
self.spells[E_id][2].ai:initcompanion(1000.0, dmg1, p_stat_nature, self:abilid(E_id, 2))
BlzSetUnitBaseDamage(unit, dmg1, 0)
utils.smartmove(unit, s.caster)
speff_explgrn:play(utils.unitxy(unit))
end)
-- GRAVITY BURST --
self.spells[E_id][3] = spell:new(casttype_point, p, self:abilid(E_id, 3))
self.spells[E_id][3]:uiregcooldown(self:getfield(E_id, 3, "cd"), E_id)
self.spells[E_id][3]:addaction(function()
local s = self.spells[E_id][3]
local r = self:getradius(E_id, 3, "radius1")
local dur1 = self:getfield(E_id, 3, "dur1")
local dur2 = self:getfield(E_id, 3, "dur2")
local dmg1 = self:getfield(E_id, 3, "dmg1")
local x,y = s.targetx, s.targety
speff_marker:play(x, y, 1.87)
utils.timed(dur1, function()
speff_gravblue:play(x, y)
speff_explblu:play(x, y)
s:gdmgxy(p, self:getfield(E_id, 3, "type1"), dmg1, x, y, r, function(unit) bf_slow:apply(unit, dur2) end)
end)
end)
-----------------------------------R-----------------------------------
-- FULMINATION --
self.spells[R_id][1] = spell:new(casttype_instant, p, self:abilid(R_id, 1))
self.spells[R_id][1]:uiregcooldown(self:getfield(R_id, 1, "cd"), R_id)
self.spells[R_id][1]:addaction(function()
local s = self.spells[R_id][1]
local r = self:getradius(R_id, 1, "radius1")
local dur1 = self:getfield(R_id, 1, "dur1")
local dur2 = self:getfield(R_id, 1, "dur2")
local dmg1 = self:getfield(R_id, 1, "dmg1")
local c = s.caster
speff_charging:attachu(c, dur1, 'origin', 0.8)
utils.timed(dur1, function()
local grp = g:newbyxy(p, g_type_dmg, utils.unitx(c), utils.unity(c), r)
s:gbuff(grp, bf_stun, 0.75)
s:gdmg(p, grp, self:getfield(R_id, 1, "type1"), dmg1, function(unit) speff_ltstrike:playu(unit) end)
grp:destroy()
end)
end)
-- SHROOMPOCALYPSE --
self.spells[R_id][2] = spell:new(casttype_instant, p, self:abilid(R_id, 2))
self.spells[R_id][2]:uiregcooldown(self:getfield(R_id, 2, "cd"), R_id)
self.spells[R_id][2]:addaction(function()
local s = self.spells[R_id][2]
local r = self:getradius(R_id, 2, "radius1")
local dur1 = self:getfield(R_id, 2, "dur1")
local dur2 = self:getfield(R_id, 2, "dur2")
local heal1 = self:getfield(R_id, 2, "heal1")
local dmg1 = self:getfield(R_id, 2, "dmg1")
local trig = trg:new("spell", p)
speff_explgrn:play(s.casterx, s.castery)
speff_shroom:attachu(s.caster, dur1, 'chest', 0.23)
speff_shroom:attachu(s.caster, dur1, 'hand,left', 0.23)
speff_shroom:attachu(s.caster, dur1, 'hand,right', 0.23)
trig:regaction(function()
local x,y = utils.unitxy(s.caster)
x,y = utils.projectxy(x, y, math.random(50,200), math.random(0,360))
if utils.trigu() == s.caster then
local dmgtype = self:getfield(R_id, 2, "type1")
speff_shroom:play(x, y, dur2, 0.3)
speff_explgrn:play(x, y, nil, 0.75)
utils.timedrepeat(1.0, dur2, function()
s:ghealxy(p, heal1, x, y, r)
s:gdmgxy(p, dmgtype, dmg1, x, y, r)
end, function() dmgtype = nil end)
end
end)
utils.timedrepeat(0.25, dur1*4, function()
utils.addmanap(s.caster, 2.08, true)
utils.addlifep(s.caster, 2.08, true)
end, function()
trig:destroy()
end)
buffy:add_indicator(p, "Shroompocalypse", "ReplaceableTextures\\CommandButtons\\BTNHealingSpray.blp", dur1,
"Recovering mana, abilities spawn Shrooms")
end)
-- CURSE OF DOOM --
self.spells[R_id][3] = spell:new(casttype_point, p, self:abilid(R_id, 3))
self.spells[R_id][3]:uiregcooldown(self:getfield(R_id, 3, "cd"), R_id)
self.spells[R_id][3]:addaction(function()
local s = self.spells[R_id][3]
local r = self:getradius(R_id, 3, "radius1")
local dur1 = self:getfield(R_id, 3, "dur1")
local dmg1 = self:getfield(R_id, 3, "dmg1")/3
local perc1 = self:getfield(R_id, 3, "perc1")
local grp = g:newbyxy(p, g_type_dmg, s.targetx, s.targety, r)
local efft = {} -- store effects to remove if unit died.
local dmgtype = self:getfield(R_id, 3, "type1")
speff_eldritch:play(s.targetx, s.targety, 1.33, nil, 1.1)
speff_voidaura:playradius(550, 6, s.targetx, s.targety, 0.51)
grp:action(function()
spell:armorpenalty(grp.unit, perc1, dur1)
efft[utils.data(grp.unit)] = speff_weakened:attachu(grp.unit, dur1, 'overhead')
end)
s:gbuff(grp, bf_slow, dur1)
utils.timedrepeat(0.33, dur1*3, function()
grp:action(function()
if utils.isalive(grp.unit) then
s:tdmg(p, grp.unit, dmgtype, dmg1)
else
DestroyEffect(efft[utils.data(grp.unit)])
grp:remove(grp.unit)
end
end)
end, function()
grp:destroy() efft = nil dmgtype = nil
end)
end)
end
function ability.template:loadrascal(p)
local Q_id,W_id,E_id,R_id,p = 1,2,3,4,p
-----------------------------------Q-----------------------------------
-- GAMBIT --
self.spells[Q_id][1] = spell:new(casttype_instant, p, self:abilid(Q_id, 1))
self.spells[Q_id][1].stackbonus = 0
self.spells[Q_id][1]:addaction(function()
local s = self.spells[Q_id][1]
local dmg = self:getfield(Q_id, 1, "dmg1")
local grp = g:newbyunitloc(p, g_type_dmg, s.caster, self:getradius(Q_id, 1, "range1"))
spell:gdmg(p, grp, self:getfield(Q_id, 1, "type1"), dmg + s.stackbonus)
utils.timed(self:getfield(Q_id, 1, "dur1"), function()
s.stackbonus = s.stackbonus - dmg*0.15
if s.stackbonus < 0 then s.stackbonus = 0 end
end)
grp:playeffect(speff_cleave)
s.stackbonus = s.stackbonus + dmg*0.15
grp:destroy()
end)
-- BACKSTAB --
self.spells[Q_id][2] = spell:new(casttype_instant, p, self:abilid(Q_id, 2))
self.spells[Q_id][2]:addaction(function()
local s = self.spells[Q_id][2]
local x,y = utils.unitxy(s.caster)
local grp = g:newbyxy(p, g_type_dmg, s.casterx, s.castery, self:getradius(Q_id, 2, "radius1"))
grp:action(function()
local x2,y2 = utils.unitxy(grp.unit)
spell:tdmg(p, grp.unit, self:getfield(Q_id, 2, "type1"), self:getfield(Q_id, 2, "dmg1"))
if CosBJ(utils.anglexy(x2, y2, x, y) - utils.getface(grp.unit)) <= CosBJ(180.0-90.0) then
spell:tdmg(p, grp.unit, self:getfield(Q_id, 2, "type2"), self:getfield(Q_id, 2, "dmg2"))
speff_cleave:attachu(grp.unit, 0.0, 'chest', 1.25)
end
end)
grp:destroy()
end)
-- FROST STRIKE --
self.spells[Q_id][3] = spell:new(casttype_instant, p, self:abilid(Q_id, 3))
self.spells[Q_id][3]:addaction(function()
local s = self.spells[Q_id][3]
local grp = g:newbyxy(p, g_type_dmg, s.casterx, s.castery, self:getradius(R_id, 1, "radius1"))
grp:action(function()
spell:tdmg(p, grp.unit, self:getfield(Q_id, 3, "type1"), self:getfield(Q_id, 3, "dmg1"))
DestroyEffect(mis_iceball:attachu(grp.unit, nil, 'chest', 0.6))
if UnitHasBuffBJ(grp.unit, FourCC('B001')) then
speff_frostnova:playu(grp.unit)
local grp = g:newbyunitloc(p, g_type_dmg, s.caster, self:getradius(Q_id, 3, "range2"))
spell:gdmg(p, grp, self:getfield(Q_id, 3, "type2"), self:getfield(Q_id, 3, "dmg2"))
grp:destroy()
end
end)
grp:destroy()
end)
-----------------------------------W-----------------------------------
-- CANDLEBLIGHT --
self.spells[W_id][1] = spell:new(casttype_instant, p, self:abilid(W_id, 1))
self.spells[W_id][1]:uiregcooldown(self:getfield(W_id, 1, "cd"), W_id)
self.spells[W_id][1]:addaction(function()
local s = self.spells[W_id][1]
s.caster = kobold.player[p].unit
local dur1 = self:getfield(W_id, 1, "dur1")
local grp = g:newbyxy(p, g_type_dmg, s.casterx, s.castery, self:getradius(W_id, 1, "radius1"))
speff_fireroar:playu(s.caster)
grp:action(function()
local grpunit = grp.unit
speff_steamtank:playu(grpunit)
local e = speff_ember:attachu(grp.unit, dur1, 'overhead')
utils.timed(dur1, function()
utils.debugfunc(function()
s:gdmgxy(p, self:getfield(W_id, 1, "type1"), self:getfield(W_id, 1, "dmg1"), utils.unitx(grpunit), utils.unity(grpunit), self:getradius(W_id, 1, "radius2"))
mis_fireballbig:playu(grpunit)
DestroyEffect(e)
end, "candleblight")
end)
end)
grp:destroy()
end)
-- GAMBLE --
self.spells[W_id][2] = spell:new(casttype_point, p, self:abilid(W_id, 2))
self.spells[W_id][2]:addaction(function()
local s = self.spells[W_id][2]
local c = self:getfield(W_id, 2, "count2")
if math.random(0,100) < 50 then
c = self:getfield(W_id, 2, "count1")
end
utils.timedrepeat(0.21, c, function()
local r = math.random(1,6)
local mis = s:pmissiletargetxy(self:getrange(W_id, 2, "range1"), mis_ele_stack[r], self:getfield(W_id, 2, "dmg1"))
mis.dmgtype = dmg.type.stack[r]
mis.vel = 32
end)
end)
-- FRIGID DAGGER --
self.spells[W_id][3] = spell:new(casttype_point, p, self:abilid(W_id, 3))
self.spells[W_id][3]:uiregcooldown(self:getfield(W_id, 3, "cd"), W_id)
self.spells[W_id][3]:addaction(function()
local s = self.spells[W_id][3]
local mis = s:pmissilepierce(self:getrange(W_id, 3, "range1"), mis_icicle, self:getfield(W_id, 3, "dmg1"))
mis.dmgtype = dmg.type.stack[dmg_frost]
mis.vel = 32
mis.expl = true
mis.explr = self:getradius(W_id, 3, "radius1")
mis.explfunc= function()
utils.debugfunc(function()
speff_frostnova:play(mis.x, mis.y)
local grp = g:newbyxy(p, g_type_dmg, mis.x, mis.y, self:getradius(W_id, 3, "radius1"))
spell:gbuff(grp, bf_freeze, self:getfield(W_id, 3, "dur1"))
grp:destroy()
end)
end
end)
-----------------------------------E-----------------------------------
-- ROCKET JUMP --
self.spells[E_id][1] = spell:new(casttype_point, p, self:abilid(E_id, 1))
self.spells[E_id][1]:uiregcooldown(self:getfield(E_id, 1, "cd"), E_id)
self.spells[E_id][1]:addaction(function()
local s = self.spells[E_id][1]
local x,y = s.targetx, s.targety
local d = self:getradius(E_id, 1, "range2")
local grp = g:newbyunitloc(p, g_type_dmg, s.caster, d)
spell:gdmg(p, grp, self:getfield(E_id, 1, "type1"), self:getfield(E_id, 1, "dmg1"))
speff_fire:playradius(d/2, 10, s.casterx, s.castery)
speff_fire:playradius(d, 10, s.casterx, s.castery)
grp:destroy()
kbeff = kb:new(s.caster, s:angletotarget(), s:disttotarget(), 0.75) -- use getradius for throw dist.
kbeff.effect = mis_mortar_fir.effect
kbeff.arc = true
kbeff.destroydest = false
kbeff.terraincol = false
speff_chargeoj:attachu(s.caster, 0.75, 'origin')
kbeff.endfunc = function() bf_ms:apply(s.caster, 3.0) end
end)
-- NETHER BOMB --
self.spells[E_id][2] = spell:new(casttype_point, p, self:abilid(E_id, 2))
self.spells[E_id][2]:uiregcooldown(self:getfield(E_id, 2, "cd"), E_id)
self.spells[E_id][2]:addaction(function()
local s = self.spells[E_id][2]
local r = self:getradius(E_id, 2, "radius1")
local dur = self:getfield(E_id, 2, "dur1")
local landfunc = function()
speff_voidpool:play(s.targetx, s.targety, dur - 0.25)
utils.timedrepeat(0.25, math.floor(dur/0.25), function()
local grp = g:newbyxy(p, g_type_dmg, s.targetx, s.targety, r)
kb:new_pullgroup(grp, s.targetx, s.targety, dur)
spell:gdmg(p, grp, self:getfield(E_id, 2, "type1"), math.floor(self:getfield(E_id, 2, "dmg1")/4))
grp:destroy()
dur = dur - 0.25
end)
end
local mis = s:pmissilearc(mis_voidball, 0)
mis.explfunc = landfunc
end)
-- GLACIAL GRENADE --
self.spells[E_id][3] = spell:new(casttype_point, p, self:abilid(E_id, 3))
self.spells[E_id][3]:uiregcooldown(self:getfield(E_id, 3, "cd"), E_id)
self.spells[E_id][3]:addaction(function()
local s = self.spells[E_id][3]
local r = self:getfield(E_id, 3, "range1")*100
local landfunc = function()
speff_iceburst2:play(s.targetx, s.targety)
local grp = g:newbyxy(p, g_type_dmg, s.targetx, s.targety, r)
spell:gdmg(p, grp, self:getfield(E_id, 3, "type1"), math.floor(self:getfield(E_id, 3, "dmg1")))
spell:gbuff(grp, bf_freeze, self:getfield(E_id, 3, "dur1"))
grp:destroy()
end
local mis = s:pmissilearc(mis_grenadeblue, 0)
mis.explfunc = landfunc
end)
-----------------------------------R-----------------------------------
-- DEVITALIZE --
self.spells[R_id][1] = spell:new(casttype_instant, p, self:abilid(R_id, 1))
self.spells[R_id][1]:uiregcooldown(self:getfield(R_id, 1, "cd"), R_id)
self.spells[R_id][1]:addaction(function()
local s = self.spells[R_id][1]
local grp = g:newbyxy(p, g_type_dmg, s.casterx, s.castery, self:getradius(R_id, 1, "radius1"))
grp:action(function()
if utils.isalive(grp.unit) then
local dmg = (utils.maxlife(grp.unit) - utils.life(grp.unit))*(self:getfield(R_id, 1, "perc1")/100)
if utils.isancient(grp.unit) then -- is a boss.
dmg = math.floor(dmg*0.01)
end
spell:tdmg(p, grp.unit, self:getfield(R_id, 1, "type1"), dmg)
utils.addmanap(s.caster, 20, true)
if not utils.isalive(grp.unit) then -- separate for visual indicator
utils.addmanap(s.caster, 10, true)
end
speff_cleave:attachu(grp.unit, 0.0, 'chest', 1.25)
end
end)
grp:destroy()
end)
-- DEADLY SHADOWS --
self.spells[R_id][2] = spell:new(casttype_instant, p, self:abilid(R_id, 2))
self.spells[R_id][2]:uiregcooldown(self:getfield(R_id, 2, "cd"), R_id)
self.spells[R_id][2]:addaction(function()
local s = self.spells[R_id][2]
local dur = self:getfield(R_id, 2, "dur1")
local bonus = self:getfield(R_id, 2, "perc1")
utils.setinvul(s.caster, true)
utils.setinvis(s.caster, true)
spell:addenchant(s.caster, dmg_shadow, dur)
speff_embersha:attachu(s.caster, dur, 'chest', 1.15)
bf_msmax:apply(s.caster, dur)
SetUnitVertexColor(s.caster, 175, 0, 175, 135)
kobold.player[s.p][p_stat_dmg_shadow] = kobold.player[s.p][p_stat_dmg_shadow] + bonus
utils.timed(dur, function()
kobold.player[s.p][p_stat_dmg_shadow] = kobold.player[s.p][p_stat_dmg_shadow] - bonus
utils.setinvis(s.caster, false)
SetUnitVertexColor(s.caster, 255, 255, 255, 255)
if not map.manager.objiscomplete or scoreboard_is_active then
utils.setinvul(s.caster, false)
end
end)
buffy:add_indicator(p, "Deadly Shadows", "ReplaceableTextures\\CommandButtons\\BTNDoom.blp", dur,
"+"..tostring(self:getfield(R_id, 2, "perc1")).."%% Shadow damage")
end)
-- ABOMINABLE --
self.spells[R_id][3] = spell:new(casttype_instant, p, self:abilid(R_id, 3))
self.spells[R_id][3]:uiregcooldown(self:getfield(R_id, 3, "cd"), R_id)
self.spells[R_id][3]:addaction(function()
local s = self.spells[R_id][3]
s.caster = kobold.player[p].unit
local snowball = function()
-- hook the cast xy for the temp trigger:
local a = math.random(0,360)
local x,y = 0,0
for i = 1,6 do
x,y = utils.projectxy(utils.unitx(utils.trigu()), utils.unity(utils.trigu()), 600, a + i*60)
local mis = missile:create_piercexy(x, y, utils.trigu(), mis_iceball.effect, self:getfield(R_id, 3, "dmg1"))
mis.dist = self:getrange(R_id, 3, "range1")
mis.dmgtype = dmg.type.stack[dmg_frost]
mis.vel = 20
mis.colfunc = function()
bf_freeze:apply(mis.hitu, self:getfield(R_id, 3, "dur2"))
mis_iceball:playu(mis.hitu)
end
end
a,x,y = nil,nil,nil
end
local trig = trg:newspelltrig(s.caster, snowball)
speff_sleet:attachu(s.caster, self:getfield(R_id, 3, "dur1"), 'chest', 0.7)
speff_conflagbl:playu(s.caster)
SetUnitVertexColor(s.caster, 100, 100, 255, 255)
utils.timed(self:getfield(R_id, 3, "dur1"), function()
trig:destroy()
SetUnitVertexColor(s.caster, 255, 255, 255, 255)
s = nil
end)
buffy:add_indicator(p, "Abominable", "ReplaceableTextures\\CommandButtons\\BTNIceShard.blp", self:getfield(R_id, 3, "dur1"),
"Using an ability launches snowballs in a radius")
end)
end
function ability.template:loadwickfighter(p)
local Q_id,W_id,E_id,R_id,p = 1,2,3,4,p
-----------------------------------Q-----------------------------------
-- WICKFIRE --
self.spells[Q_id][1] = spell:new(casttype_instant, p, self:abilid(Q_id, 1))
self.spells[Q_id][1].tnt = {} -- track recently issued orders to prevent sticking order spam.
self.spells[Q_id][1]:addaction(function()
local s = self.spells[Q_id][1]
local perc = self:getfield(Q_id, 1, "perc1")
local grp = g:newbyunitloc(p, g_type_dmg, s.caster, self:getradius(Q_id, 1, "radius1"))
spell:gdmg(p, grp, self:getfield(Q_id, 1, "type1"), self:getfield(Q_id, 1, "dmg1"))
grp:action(function()
utils.addlifep(s.caster, perc, true, s.caster)
if GetUnitTypeId(grp.unit) ~= kk.ymmudid and not utils.isancient(grp.unit) then
utils.issatkunit(grp.unit, s.caster) -- run taunt command.
end
end)
utils.timed(4.0, function()
grp:action(function()
if not utils.isancient(grp.unit) then
if s.tnt[utils.data(grp.unit)] and GetUnitTypeId(grp.unit) ~= kk.ymmudid then
utils.issatkxy(grp.unit, utils.projectxy(s.casterx, s.castery, math.random(100,600), math.random(0,360))) -- reset from taunt.
s.tnt[utils.data(grp.unit)] = nil
end
end
end)
grp:destroy()
end)
end)
-- SLAG SLAM --
self.spells[Q_id][2] = spell:new(casttype_instant, p, self:abilid(Q_id, 2))
self.spells[Q_id][2]:addaction(function()
local s = self.spells[Q_id][2]
local dmg1 = self:getfield(Q_id, 2, "dmg1")
local dmg2 = self:getfield(Q_id, 2, "dmg2")
local dmg3 = self:getfield(Q_id, 2, "dmg3")
local r = self:getradius(Q_id, 2, "radius1")
local dur1 = self:getfield(Q_id, 2, "dur1")
local grp = g:newbyxy(p, g_type_dmg, s.casterx, s.castery, r)
grp:action(function()
if utils.isalive(grp.unit) then
s:tdmg(p, grp.unit, self:getfield(Q_id, 2, "type1"), dmg1)
s:tdmg(p, grp.unit, self:getfield(Q_id, 2, "type2"), dmg2)
speff_demoexpl:play(utils.unitxy(grp.unit))
if not utils.isalive(grp.unit) then
s:gdmgxy(p, self:getfield(Q_id, 2, "type2"), dmg3, utils.unitx(grp.unit), utils.unity(grp.unit), r)
speff_exploj:play(utils.unitx(grp.unit), utils.unity(grp.unit), nil, 0.5)
end
end
end)
grp:destroy()
end)
-- RALLYING CLOBBER --
self.spells[Q_id][3] = spell:new(casttype_instant, p, self:abilid(Q_id, 3))
self.spells[Q_id][3]:addaction(function()
local s = self.spells[Q_id][3]
local dmg1 = self:getfield(Q_id, 3, "dmg1")
local dur1 = self:getfield(Q_id, 3, "dur1")
local grp = g:newbyxy(p, g_type_dmg, s.casterx, s.castery, self:getradius(Q_id, 3, "radius2"))
grp:action(function()
s:tdmg(p, grp.unit, self:getfield(Q_id, 3, "type1"), dmg1)
speff_demoexpl:play(utils.unitxy(grp.unit))
end)
grp:destroy()
local grp2 = g:newbyxy(p, g_type_ally, s.casterx, s.castery, self:getradius(Q_id, 3, "radius1"))
grp2:action(function()
if utils.ishero(grp2.unit) then
bf_ms:apply(grp2.unit, 3.0)
bf_armor:apply(grp2.unit, 3.0)
speff_boosted:attachu(grp2.unit, 0.0, 'origin', 0.85)
end
end)
grp2:destroy()
end)
-----------------------------------W-----------------------------------
-- MOLTEN SHIELD --
self.spells[W_id][1] = spell:new(casttype_instant, p, self:abilid(W_id, 1))
self.spells[W_id][1]:uiregcooldown(self:getfield(W_id, 1, "cd"), W_id)
self.spells[W_id][1]:addaction(function()
local s = self.spells[W_id][1]
local dmg1 = self:getfield(W_id, 1, "dmg1")
local dmg2 = math.floor(self:getfield(W_id, 1, "dmg2")/3)
local dex = utils.data(s.caster)
local r = self:getradius(W_id, 1, "radius1")
dmg.absorb:new(s.caster, self:getfield(W_id, 1, "dur1"), { all = dmg1 }, speff_uberorj, endfunc)
utils.timedrepeat(0.33, self:getfield(W_id, 1, "dur1")*3, function()
if dmg.absorb.stack[dex] then
local grp = g:newbyunitloc(p, g_type_dmg, s.caster, r)
spell:gdmg(p, grp, self:getfield(W_id, 1, "type1"), dmg2)
grp:destroy()
else
ReleaseTimer()
end
end)
buffy:new_absorb_indicator(p, "Molten", "ReplaceableTextures\\CommandButtons\\BTNOrbOfFire.blp", dmg1, self:getfield(W_id, 1, "dur1"))
end)
-- WAXSHELL --
self.spells[W_id][2] = spell:new(casttype_instant, p, self:abilid(W_id, 2))
self.spells[W_id][2]:uiregcooldown(self:getfield(W_id, 2, "cd"), W_id)
self.spells[W_id][2]:addaction(function()
local s = self.spells[W_id][2]
s:enhanceresistall(self:getfield(W_id, 2, "perc1"), self:getfield(W_id, 2, "dur1"), p, true)
speff_radglow:attachu(s.caster, self:getfield(W_id, 2, "dur1"), 'chest', 0.8)
utils.addlifep(s.caster, self:getfield(W_id, 2, "perc2"), true, s.caster)
buffy:add_indicator(p, "Waxshell", "ReplaceableTextures\\CommandButtons\\BTNInnerFire.blp", self:getfield(W_id, 2, "dur1"),
"+"..tostring(self:getfield(W_id, 2, "perc1")).."%% Elemental Resistance")
end)
-- MOLTEN ROAR --
self.spells[W_id][3] = spell:new(casttype_instant, p, self:abilid(W_id, 3))
self.spells[W_id][3]:uiregcooldown(self:getfield(W_id, 3, "cd"), W_id)
self.spells[W_id][3]:addaction(function()
local s = self.spells[W_id][3]
local dur1 = self:getfield(W_id, 3, "dur1")
s:gdmgxy(p, self:getfield(W_id, 3, "type1"), self:getfield(W_id, 3, "dmg1"), s.casterx, s.castery, self:getradius(W_id, 3, "radius1"))
speff_fireroar:play(s.casterx, s.castery, nil, 1.5)
bf_a_atk:applyaoe(s.caster, 6.0, self:getradius(W_id, 3, "radius1"))
end)
-----------------------------------E-----------------------------------
-- EMBER CHARGE --
self.spells[E_id][1] = spell:new(casttype_point, p, self:abilid(E_id, 1))
self.spells[E_id][1]:uiregcooldown(self:getfield(E_id, 1, "cd"), E_id)
self.spells[E_id][1]:addaction(function()
local s = self.spells[E_id][1]
local x,y = utils.projectxy(s.targetx, s.targety, 80.0, s:angletotarget() - 180.0)
local e = speff_chargeoj:attachu(s.caster, nil, 'origin')
local land = function()
local r = self:getradius(E_id, 1, "radius1")
local grp = g:newbyunitloc(p, g_type_dmg, s.caster, r)
spell:gdmg(p, grp, self:getfield(E_id, 1, "type1"), self:getfield(E_id, 1, "dmg1"))
speff_explfire:playradius(r/2, 10, x, y)
speff_explfire:playradius(r, 10, x, y)
spell:gbuff(grp, bf_slow, self:getfield(E_id, 1, "dur1"))
grp:destroy()
DestroyEffect(e)
end
if not spell:slide(s.caster, s:disttotarget(), 0.72, x, y, nil, land) then DestroyEffect(e) end
end)
-- WICKFIRE LEAP --
self.spells[E_id][2] = spell:new(casttype_point, p, self:abilid(E_id, 2))
self.spells[E_id][2]:uiregcooldown(self:getfield(E_id, 2, "cd"), E_id)
self.spells[E_id][2]:addaction(function()
local s = self.spells[E_id][2]
local r = self:getradius(E_id, 2, "radius1")
local d = self:getradius(E_id, 2, "range1") -- kb distance
local dur = self:getfield(E_id, 2, "dur1")
local vel = s:disttotarget()/dur/20
local a = s:angletotarget()
local x,y = s.casterx, s.castery
utils.temphide(s.caster, true)
speff_stormfire:play(utils.unitxy(s.caster))
utils.timedrepeat(0.05, dur*20, function()
x,y = utils.projectxy(x, y, vel, a)
utils.setxy(s.caster, x, y)
end, function()
speff_stormfire:play(utils.unitxy(s.caster))
local grp = g:newbyunitloc(p, g_type_dmg, s.caster, r)
spell:gdmg(p, grp, self:getfield(E_id, 2, "type1"), self:getfield(E_id, 2, "dmg1"))
kb:new_group(grp, utils.unitx(s.caster), utils.unity(s.caster), d)
utils.temphide(s.caster, false)
grp:destroy()
end)
end)
-- SEISMIC TOSS --
self.spells[E_id][3] = spell:new(casttype_instant, p, self:abilid(E_id, 3))
self.spells[E_id][3]:uiregcooldown(self:getfield(E_id, 3, "cd"), E_id)
self.spells[E_id][3]:addaction(function()
local s = self.spells[E_id][3]
local dur1 = self:getfield(E_id, 3, "dur1")
local kbeff
local grp = g:newbyxy(p, g_type_dmg, s.casterx, s.castery, self:getradius(E_id, 3, "radius1"))
grp:action(function()
local grpunit = grp.unit
if utils.isalive(grpunit) then
if not utils.isancient(grpunit) then
-- use getradius for throw dist.
kbeff = kb:new(grpunit, utils.anglexy(utils.unitx(s.caster), utils.unity(s.caster), utils.unitxy(grpunit)), self:getradius(E_id, 3, "range1"), 0.66)
kbeff.arc = true
kbeff.endfunc = function()
utils.debugfunc(function()
speff_warstomp:playu(grpunit)
bf_stun:apply(grpunit, dur1)
s:tdmg(p, grpunit, self:getfield(E_id, 3, "type1"), self:getfield(E_id, 3, "dmg1"))
utils.timed(dur1 + 0.21, function()
if utils.isalive(grpunit) then
utils.issatkxy(grpunit, utils.unitxy(s.caster)) -- prevent stuck elites exploit, etc.
end
end)
end)
end
else -- if stoic target, deal bonus dmg instead.
speff_warstomp:playu(grpunit)
s:tdmg(p, grp.unit, self:getfield(E_id, 3, "type1"), self:getfield(E_id, 3, "dmg1")*1.5)
end
end
end)
grp:destroy()
end)
-----------------------------------R-----------------------------------
-- CANDLEPYRE --
self.spells[R_id][1] = spell:new(casttype_instant, p, self:abilid(R_id, 1))
self.spells[R_id][1].active = false
self.spells[R_id][1].trig = trg:newspelltrig(kobold.player[p].unit, function()
if self.spells[R_id][1].active then
local dist = self:getradius(R_id, 1, "radius1")/2
local px,py = 0,0
-- lob missiles:
utils.timedrepeat(0.44, 2, function()
px,py = utils.unitxy(kobold.player[p].unit)
missile:create_arc_in_radius(px, py, kobold.player[p].unit, mis_fireball.effect, self:getfield(R_id, 1, "dmg1"), self:getfield(R_id, 1, "type1"),
8, dist, 0.66, 148, 100)
dist = dist + dist
end)
end
end)
self.spells[R_id][1]:uiregcooldown(self:getfield(R_id, 1, "cd"), R_id)
self.spells[R_id][1]:addaction(function()
local s = self.spells[R_id][1]
local e = speff_radglow:attachu(s.caster, self:getfield(R_id, 1, "dur1"), 'chest')
self.spells[R_id][1].active = true
utils.timedrepeat(1.0, self:getfield(R_id, 1, "dur1"), function()
if not utils.isalive(s.caster) then
self.spells[R_id][1].active = false
DestroyEffect(e)
ReleaseTimer()
end
end, function()
self.spells[R_id][1].active = false
DestroyEffect(e)
end)
buffy:add_indicator(p, "Candlepyre", "ReplaceableTextures\\CommandButtons\\BTNIncinerate.blp", self:getfield(R_id, 1, "dur1"),
"Using an ability launches fireballs in a radius")
end)
-- BASALT MORTAR --
self.spells[R_id][2] = spell:new(casttype_point, p, self:abilid(R_id, 2))
self.spells[R_id][2]:uiregcooldown(self:getfield(R_id, 2, "cd"), R_id)
self.spells[R_id][2]:addaction(function()
local s = self.spells[R_id][2]
local launchfunc = function(x, y)
local x,y = x,y
local mis = s:pmissilearc(mis_fireballbig, self:getfield(R_id, 2, "dmg1"), x, y)
mis.explfunc = function() speff_explode2:play(mis.x, mis.y, nil, 1.2) end
mis.dmgtype = self:getfield(R_id, 2, "type1")
mis.explr = self:getradius(R_id, 2, "radius1")
end
launchfunc(s.targetx, s.targety)
local x,y
utils.timedrepeat(0.51, self:getfield(R_id, 2, "count1")-1, function()
x,y = utils.projectxy(s.targetx, s.targety, math.random(0,150), math.random(0.360))
launchfunc(x, y)
end)
end)
-- DREADFIRE --
self.spells[R_id][3] = spell:new(casttype_instant, p, self:abilid(R_id, 3))
self.spells[R_id][3]:uiregcooldown(self:getfield(R_id, 3, "cd"), R_id)
self.spells[R_id][3]:addaction(function()
local s = self.spells[R_id][3]
local dur1 = self:getfield(R_id, 3, "dur1")
local dmg1 = self:getfield(R_id, 3, "dmg1")
local r = self:getradius(R_id, 3, "radius1")
local e = speff_blazing:attachu(s.caster, dur1)
speff_fireroar:play(s.casterx, s.castery, nil, 1.25)
s:enhanceresistpure(self:getfield(R_id, 3, "perc1"), dur1, p, true)
utils.timedrepeat(0.33, dur1*3, function()
if not kobold.player[p].downed then
s:gdmgxy(p, self:getfield(R_id, 3, "type1"), dmg1/3, utils.unitx(s.caster), utils.unity(s.caster), r)
else
s = nil
DestroyEffect(e)
ReleaseTimer()
end
end)
buffy:add_indicator(p, "Dreadfire", "ReplaceableTextures\\CommandButtons\\BTNImmolationOn.blp", dur1,
"Reduces damage taken and damages nearby targets")
end)
end
function ability.template:loaditemspells(p)
-- HEALING POTION (F) --
self.spells[9][1] = spell:new(casttype_instant, p, FourCC('A01O'))
self.spells[9][1]:uiregcooldown(15.0, 9, 63, 75)
self.spells[9][1]:addaction(function()
local unit = self.spells[9][1].caster
local val = 30.0 + kobold.player[self.p][p_stat_potionpwr]
SetUnitLifePercentBJ(unit, GetUnitLifePercent(unit) + val)
ArcingTextTag(color:wrap(color.tooltip.good, "+"..math.floor(val).."%%"), unit, 2.0)
StopSound(kui.sound.drinkpot, false, false)
utils.playsound(kui.sound.drinkpot, self.p)
loot.ancient:raiseevent(ancient_event_potion, kobold.player[p], true)
end)
-- MANA POTION (G) --
self.spells[10][1] = spell:new(casttype_instant, p, FourCC('A01P'))
self.spells[10][1]:uiregcooldown(15.0, 10, 63, 75)
self.spells[10][1]:addaction(function()
local unit = self.spells[10][1].caster
local val = 30.0 + kobold.player[self.p][p_stat_artifactpwr]
SetUnitManaPercentBJ(unit, GetUnitManaPercent(unit) + val)
ArcingTextTag(color:wrap(color.tooltip.alert, "+"..math.floor(val).."%%"), unit, 2.0)
StopSound(kui.sound.drinkpot, false, false)
utils.playsound(kui.sound.drinkpot, self.p)
loot.ancient:raiseevent(ancient_event_potion, kobold.player[p], false)
end)
end
function ability.template:load(charid)
-- replace icons and descriptions in the ability pane.
utils.debugfunc(function()
if self.p == utils.localp() then
for row = 1,3 do
for col = 1,4 do
-- *note: template tables have col/row reversed from kui skill slots:
kui.canvas.game.abil.slot[row][col].icon:setbgtex(self[col][row].icon)
kui.canvas.game.abil.slot[row][col].descript:settext(self[col][row].descript)
kui.canvas.game.abil.slot[row][col].txt:settext(self[col][row].name)
if not self[col][row].cost then self[col][row].cost = 0 end
if not self[col][row].cd then self[col][row].cd = 0 end
kui.canvas.game.abil.slot[row][col].mana.txt:settext(self[col][row].cost)
kui.canvas.game.abil.slot[row][col].cd.txt:settext(self[col][row].cd)
end
end
end
-- load character spells:
if charid == 4 then
kobold.player[self.p].ability:loadwickfighter(self.p)
elseif charid == 3 then
kobold.player[self.p].ability:loadrascal(self.p)
elseif charid == 2 then
kobold.player[self.p].ability:loadgeomancer(self.p)
elseif charid == 1 then
kobold.player[self.p].ability:loadtunneler(self.p)
end
-- initialize base items:
self:loaditemspells(self.p)
-- instantiate mastery spells:
self:buildmasteryspells(self.p)
end, "load")
end
-- learn a standard ability.
function ability.template:learn(row, col)
-- NOTE: this desyncs in multiplayer
-- loosely translated: row == ability option, col == skill slot id on skill bar (hotkey id).
for abilid = 1,3 do
if self.spells[col][abilid].cdactive then
-- if column is on cooldown, don't allow a switch:
utils.palert(self.p, "You cannot change an ability while it's on cooldown!")
return
end
end
if not self.map[row][col] then -- do nothing if already learned.
kobold.player[self.p]:updatecastspeed()
for abilrow = 1,3 do
-- *NOTE: template has col and row reversed.
UnitRemoveAbility(kobold.player[self.p].unit, self[col][abilrow].code)
self.map[abilrow][col] = false -- reset abil learned bool.
end
UnitAddAbility(kobold.player[self.p].unit, self[col][row].code)
self:updateabilstats(BlzGetUnitAbility(kobold.player[self.p].unit, self[col][row].code), col, row)
-- store abil learned for that slot (for other logic):
self.map[row][col] = true
-- update ui components:
if self.p == utils.localp() then
for abilrow = 1,3 do
kui.canvas.game.abil.slot[abilrow][col].iconbd:setbgtex(kui.meta.abilpanel[2].tex)
end
kui.canvas.game.abil.slot[row][col].iconbd:setbgtex(kui.meta.abilpanel[2].texsel)
kui.canvas.game.skill.skill[col].fill:setbgtex(self[col][row].icon) -- skill bar icon
kui.canvas.game.skill.skill[col].cdtxt.advtip[1] = self[col][row].icon
kui.canvas.game.skill.skill[col].cdtxt.advtip[2] =
color:wrap(color.tooltip.alert, self[col][row].name)
..color:wrap(color.txt.txtdisable, "|nMana Cost: "..tostring(self[col][row].cost or 0))
..color:wrap(color.txt.txtdisable, "|nCooldown: "..tostring(self[col][row].cd or 0).." sec")
kui.canvas.game.skill.skill[col].cdtxt.advtip[3] = self[col][row].descript
utils.playsound(kui.sound.selnode, self.p)
end
end
end
-- function specific for talent tree abilities (slots 5-8).
function ability.template:learnmastery(abilid)
-- *NOTE: abilid == the mastery code we created, not actual ability id (e.g. "na1")
if not self.savedmastery then self.savedmastery = {} end
-- loosely translated: row == ability option, col == skill slot id on skill bar (hotkey id).
if map.manager.activemap then
utils.palert(self.p, "You cannot change masteries during a dig!", 2.0)
return
end
local foundabil = false
if self.spells[col] and self.spells[col][abilid] and self.spells[col][abilid].cdactive then
-- if column is on cooldown, don't allow a switch:
utils.palert(self.p, "You cannot change an ability while it's on cooldown!")
return
end
-- see if ability is learned already:
for col = 5,8 do
if self.spells[col] and self.spells[col][abilid] then -- already learned, remove from bar.
-- check if slot is on cooldown first:
for casttype,rawcode in pairs(hotkey.map.codet[hotkey.map.intmap[col]]) do
if BlzGetUnitAbility(kobold.player[self.p].unit, rawcode) and BlzGetUnitAbilityCooldownRemaining(kobold.player[self.p].unit, rawcode) > 0 then
utils.palert(self.p, "You cannot remove a mastery ability while it's on cooldown!")
return
end
end
-- update key before clearing:
kobold.player[self.p].keymap:clearkey(hotkey.map.intmap[col], self.spells[col][abilid].casttype)
self.spells[col][abilid] = nil
self.spells[col] = nil
self[col][1] = nil
kui.canvas.game.skill.skill[col].cdtxt.advtip = nil
if self.p == utils.localp() then
kui.canvas.game.skill.skill[col].fill:setbgtex(kui.color.black)
utils.playsound(kui.sound.closebtn, self.p)
end
foundabil = true
-- save data:
self.savedmastery[col-4] = nil
break
end
end
if foundabil then
return
else
for col = 5,8 do
if not self.spells[col] then
local row, pobj = 1, kobold.player[self.p]
-- assign empty slot:
if not self.spells[col] then self.spells[col] = {} end
self[col][row] = self.mastery[mastery.abillist[abilid].spell.id]
self.spells[col][abilid] = nil -- destroy previous table pointer.
self.spells[col][abilid] = self.mastery[mastery.abillist[abilid].spell.id].spell
local id = self.spells[col][abilid].id
-- update hotkey map return dummyabil so we can reference it on the player:
local dummyabil = pobj.keymap:updatekey(hotkey.map.intmap[col], self.spells[col][abilid].code,
self.spells[col][abilid].casttype, self.spells[col][abilid].orderstr)
self.spells[col][abilid]:uiregcooldown(self.mastery:getmastfield(id, "cd"), col, nil, nil, pobj.unit, dummyabil)
self.spells[col][abilid].dummycode = dummyabil -- for referencing the dummy abil in spell code.
self:updatemasterystats(BlzGetUnitAbility(pobj.unit, dummyabil), id)
pobj:updatecastspeed()
-- update ui components:
if self.p == utils.localp() then
kui.canvas.game.skill.skill[col].fill:setbgtex(self[col][row].icon) -- skill bar icon
kui.canvas.game.skill.skill[col].cdtxt.advtip = nil -- gc original advtip table.
kui.canvas.game.skill.skill[col].cdtxt.advtip = {}
kui.canvas.game.skill.skill[col].cdtxt.advtip[1] = self[col][row].icon
kui.canvas.game.skill.skill[col].cdtxt.advtip[2] =
color:wrap(color.tooltip.alert, self[col][row].name)
..color:wrap(color.txt.txtdisable, "|nMana Cost: "..tostring(self[col][row].cost or 0))
..color:wrap(color.txt.txtdisable, "|nCooldown: "..tostring(self[col][row].cd or 0).." sec")
kui.canvas.game.skill.skill[col].cdtxt.advtip[3] = self[col][row].descript
utils.playsound(kui.sound.selnode, self.p)
end
-- save data:
self.savedmastery[col-4] = abilid
pobj = nil
break
end
end
end
end
function ability.template:updatemasterystats(abil, id)
-- match mana cost and cooldown to ability template field:
if self.mastery:getmastfield(id, "cost") then
BlzSetAbilityIntegerLevelField(abil, ABILITY_ILF_MANA_COST, 0, math.floor(self.mastery:getmastfield(id, "cost")))
else -- field is nil, which is also 0.
BlzSetAbilityIntegerLevelField(abil, ABILITY_ILF_MANA_COST, 0, 0)
end
if self.mastery:getmastfield(id, "cd") then
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_COOLDOWN, 0, self.mastery:getmastfield(id, "cd"))
else -- field is nil, which is also 0.
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_COOLDOWN, 0, 0)
end
if self.mastery:getmastfield(id, "castrange") then
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_CAST_RANGE, 0, self.mastery:getmastfield(id, "castrange"))
else
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_CAST_RANGE, 0, 800.0)
end
if self.mastery:getmastfield(id, "targetimgsize") then
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_AREA_OF_EFFECT, 0, self.mastery:getmastfield(id, "targetimgsize"))
else
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_AREA_OF_EFFECT, 0, 500.0)
end
end
function ability.template:updateabilstats(abil, col, row)
-- match mana cost and cooldown to ability template field:
if self:getfield(col, row, "cost") then
BlzSetAbilityIntegerLevelField(abil, ABILITY_ILF_MANA_COST, 0, math.floor(self:getfield(col, row, "cost")))
else -- field is nil, which is also 0.
BlzSetAbilityIntegerLevelField(abil, ABILITY_ILF_MANA_COST, 0, 0)
end
if self:getfield(col, row, "cd") then
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_COOLDOWN, 0, self:getfield(col, row, "cd"))
else -- field is nil, which is also 0.
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_COOLDOWN, 0, 0)
end
end
function ability.template:new(p, charid)
local o
if ability.template[charid] then
o = utils.deep_copy(ability.template[charid])
-- .map stores a bool for learned slots for logical use:
o.map = {}
o.mastmap = {}
for row = 1,3 do
o.map[row] = {}
for col = 1,4 do
o.map[row][col] = false -- QWER
o.mastmap[col+4] = false -- 1234
end
end
setmetatable(o, self)
o.p = p
return o
end
end
function ability.template:init()
-- holds ability metadata.
self.__index = self
end
ai = {}
ai.miniboss = {}
ai.miniboss.__index = ai.miniboss
ai.stack = {}
ai.__index = ai
function ai:new(unit)
local o = setmetatable({}, self)
o.unit = unit
o.p = utils.powner(unit)
o.owner = kobold.player[o.p].unit
self.stack[o] = o
return o
end
-- initialize a simple follow timer for a companion unit.
-- @_dmg, = base weapon damage or spell damage to modify
-- @p_stat = if the unit should scale with player stats, link it.
-- @_abilid = if owner loses this ability, unsummon the unit.
function ai:initcompanion(leashrange, _dmg, _p_stat, _abilid)
if self.unit then
local aiunit = self.unit -- new handle for timer logic
self.leashr = leashrange or 800.0 -- forced move range (guaranteed positioning)
self.leashm = self.leashr*0.8 -- quick move range (standard movement)
self.dmg = _dmg or nil
self.p_stat = _p_stat or nil
self.code = _abilid or nil
self.combat = false -- controls attack-move ordering on spell damage.
self.leashed = false -- set to true when unit is returning to owner.
SetUnitMoveSpeed(self.unit, 522.0) -- for now, always make companions have max MS.
self:update_damage() -- init unit damage
self.tmr = utils.timedrepeat(4.00, nil, function()
utils.debugfunc(function()
if aiunit and utils.isalive(aiunit) then
-- check for unit updates and ability swaps:
if self.code and not BlzGetUnitAbility(self.owner, self.code) then
self:releasecompanion()
ReleaseTimer()
return
end
-- check for leash if outside leash range:
if not self.leashed and self:getownerdist() > self.leashr then
self.leashed = true
utils.issmovexy(self.unit, utils.unitxy(self.owner))
self:follow(3.33)
-- check if companion should be moved (very far away or stuck):
self.leashtmr = utils.timed(2.63, function()
utils.debugfunc(function()
if aiunit and utils.isalive(aiunit) then
self.leashed = false
if self:getownerdist() > self.leashr then
utils.setxy(self.unit, utils.unitxy(self.owner))
speff_feral:play(utils.unitxy(self.owner))
self:follow(1.66)
end
else
ReleaseTimer()
return
end
end, "ai leash")
end)
elseif not self.combat and self:getownerdist() > self.leashm then
utils.issmovexy(self.unit, utils.unitxy(self.owner))
self:follow(2.5)
elseif not self.combat then
-- to prevent idle ranged units, issue random attack command near player
utils.issatkxy(self.unit, utils.unitprojectxy(self.owner, math.random(100, 300), math.random(0,360)))
end
else
self:releasecompanion()
ReleaseTimer()
return
end
end, "ai leash")
end)
return self
else
assert(false, "error: did not run ai:initcompanion because self.unit did not exist")
end
end
-- initiate follow order after a delay period (sec).
function ai:follow(delay)
local aiunit = self.unit -- handle for timer.
if self.followtmr then ReleaseTimer(self.followtmr) end
self.followtmr = utils.timed(delay, function()
if aiunit then
if not self.combat then
utils.smartmove(self.unit, self.owner)
end
else
ReleaseTimer()
end
end)
end
function ai:update_damage()
if self.dmg and self.p_stat then
BlzSetUnitBaseDamage(self.unit, math.floor(self.dmg*(1+kobold.player[self.p][self.p_stat]/100)), 0)
end
end
-- destroy an ai after a duration
function ai:timed(duration)
utils.timed(duration, function()
self:releasecompanion()
end)
end
-- send a companion to attack on spell damage, etc.
function ai:engage(x, y, _unit)
-- check if already engaged.
if not self.combat then
if _unit then
utils.issatkunit(self.unit, _unit)
else
utils.issatkxy(self.unit, x, y)
end
self.combat = true
utils.timed(6.0, function() self.combat = false end)
end
end
function ai:getownerdist()
return utils.distxy(utils.unitx(self.owner), utils.unity(self.owner), utils.unitx(self.unit), utils.unity(self.unit))
end
function ai:releasecompanion()
speff_feral:play(utils.unitxy(self.unit))
if self.followtmr then ReleaseTimer(self.followtmr) self.followtmr = nil end
if self.leashtmr then ReleaseTimer(self.leashtmr) self.leashtmr = nil end
if self.tmr then ReleaseTimer(self.tmr) self.tmr = nil end
RemoveUnit(self.unit)
for i,v in pairs(self) do v = nil i = nil end
self.stack[self] = nil
end
function ai.miniboss:new(unit, castcadence, castanim, dropfrag, fragcount)
local o = setmetatable({}, self)
o.spellfunc = nil -- assign outside of scope
o.unit = unit
o.castcadence = castcadence
o.castanim = castanim
o.spellelapsed = 0
o.moveelapsed = 0
o.fidgetcad = 2.72
o.fidgetdur = 1.54
o.castdur = 1.03
o.moving = false
o.casting = false
o.trackedunit = kobold.player[Player(0)].unit
local fragcount = fragcount
utils.timedrepeat(0.33, nil, function(c)
utils.debugfunc(function()
if o and o.unit and utils.isalive(o.unit) and map.manager.activemap then
if o:hero_within_range(1200) then
o.spellelapsed = o.spellelapsed + 0.33
o.moveelapsed = o.moveelapsed + 0.33
if not o.moving and not o.casting then
if o.spellelapsed >= o.castcadence then
o.spellelapsed = 0
o.casting = true
PauseUnit(o.unit, true)
SetUnitAnimation(o.unit, o.castanim)
o.spellfunc(o.unit)
utils.timed(o.castdur, function() PauseUnit(o.unit, false) o.casting = false end)
elseif o.moveelapsed > o.fidgetcad then
o.moveelapsed = 0
o.moving = true
local a,x,y = utils.angleunits(o.unit, o.trackedunit), utils.unitxy(o.unit)
x,y = utils.projectxy(x, y, math.random(150,300), a + math.randomneg(-30,30))
utils.issatkxy(o.unit, x, y)
utils.timed(o.fidgetdur, function() o.moving = false end)
end
end
end
elseif not map.manager.activemap and utils.isalive(o.unit) then
RemoveUnit(o.unit)
else
if map.manager.activemap and dropfrag and not utils.isalive(o.unit) and map.manager.downp ~= map.manager.totalp then
local fragcount = math.max(1,fragcount + map.manager.diffid - 2)
if map.manager.diffid == 5 then fragcount = fragcount + 3 end
loot:generatelootmissile(utils.unitx(o.unit), utils.unity(o.unit), "I00F", fragcount or 1, nil, speff_fireroar, speff_fragment.effect)
end
o:release()
ReleaseTimer()
end
end, "miniboss timer")
end)
-- boss units have no pathing:
SetUnitPathing(o.unit, false)
o:scale()
ai.stack[o] = o
return o
end
function ai.miniboss:hero_within_range(r)
local check, grp = false, g:newbyxy(Player(PLAYER_NEUTRAL_AGGRESSIVE), g_type_dmg, utils.unitx(self.unit), utils.unity(self.unit), r)
grp:action(function()
if utils.ishero(grp.unit) then
check = true
grp.brk = true
end
end)
grp:destroy()
return check
end
function ai.miniboss:scale()
BlzSetUnitMaxHP(self.unit, math.floor(600*(1 + (map.mission.cache.level^1.12)*0.035) ) )
BlzSetUnitBaseDamage(self.unit, math.floor(22*(1 + (map.mission.cache.level^1.12)*0.025) ), 0)
if map.mission.cache and map.mission.cache.setting then
BlzSetUnitMaxHP(self.unit, math.ceil( BlzGetUnitMaxHP(self.unit)*map.mission.cache.setting[m_stat_health]) )
BlzSetUnitBaseDamage(self.unit, math.ceil( BlzGetUnitBaseDamage(self.unit, 0)*map.mission.cache.setting[m_stat_attack]), 0 )
BlzSetUnitIntegerField(self.unit, UNIT_IF_LEVEL, math.floor(map.mission.cache.level or 10))
end
utils.restorestate(self.unit)
end
function ai.miniboss:release()
ai.stack[self] = nil
end
badge = {}
badge.__index = badge
function badge:buildpanel()
fr = kui:createmassivepanel("BADGES")
fr.panelid = 7
fr.statesnd = true
local x,y = 0,0
local w,h = 196,164
local start_xoff,start_yoff = -592,245
local xoff, yoff = start_xoff, start_yoff
for id = 1,18 do
if id < 10 then
badge[id] = kui.frame:newbtntemplate(fr, "war3mapImported\\achievement_icon_gallery_0"..tostring(id)..".blp")
else
badge[id] = kui.frame:newbtntemplate(fr, "war3mapImported\\achievement_icon_gallery_"..tostring(id)..".blp")
end
badge[id].btn:assigntooltip("badgetip"..tostring(id))
badge[id].btn:event(nil, nil, nil, nil)
badge[id]:setsize(kui:px(w), kui:px(h))
badge[id]:setfp(fp.tl, fp.c, fr, kui:px(xoff), kui:px(yoff))
badge[id]:setnewalpha(90)
xoff = xoff + w
if math.fmod(id, 6) == 0 then
xoff = start_xoff
yoff = yoff - h
xoff = start_xoff
end
end
-- add class indicators
for id = 1,18 do
badge[id].classicon = {}
for classid = 1,4 do
badge[id].classicon[classid] = kui.frame:newbtntemplate(badge[id], kui.meta.tipcharselect[classid][1])
badge[id].classicon[classid]:setsize(kui:px(20), kui:px(20))
badge[id].classicon[classid]:hide()
badge[id].classicon[classid].btn:assigntooltip("badgetipclass"..tostring(classid))
badge[id].classicon[classid].btn:event(nil, nil, nil, nil)
if classid == 1 then
badge[id].classicon[classid]:setfp(fp.c, fp.c, badge[id], kui:px(-70), kui:px(55))
elseif classid == 2 then
badge[id].classicon[classid]:setfp(fp.c, fp.c, badge[id], kui:px(-40), kui:px(68))
elseif classid == 3 then
badge[id].classicon[classid]:setfp(fp.c, fp.c, badge[id], kui:px(40), kui:px(68))
elseif classid == 4 then
badge[id].classicon[classid]:setfp(fp.c, fp.c, badge[id], kui:px(70), kui:px(55))
end
-- glow effect:
badge[id].classicon[classid].glow = kui.frame:newbytype("BACKDROP", badge[id].classicon[classid])
badge[id].classicon[classid].glow:addbgtex("war3mapImported\\panel-shop-selection-circle.blp", 200)
badge[id].classicon[classid].glow:setfp(fp.c, fp.c, badge[id].classicon[classid], 0, 0)
badge[id].classicon[classid].glow:setsize(kui:px(34), kui:px(34))
badge[id].classicon[classid].glow:hide()
end
end
fr:hide()
return fr
end
function badge:earn(pobj, id, _classid)
local classid = _classid or pobj.charid
-- earn badge:
if pobj.badge[id] == 0 then
pobj.badge[id] = 1
kui.canvas.game.skill.alerticon[5]:show()
end
pobj.badgeclass[badge:get_class_index(id, classid)] = 1 -- 4 per badge, index 1, calc offset.
badge:validate_all(pobj)
end
function badge:validate_all(pobj)
if utils.islocalp(pobj.p) then
for id = 1,18 do
if pobj.badge[id] == 1 then
badge[id]:setnewalpha(255)
end
for classid = 1,4 do
if pobj.badgeclass[badge:get_class_index(id, classid)] == 1 then
badge[id].classicon[classid]:show()
badge[id].classicon[classid].glow:show()
badge[id].classicon[classid]:setnewalpha(255)
else
-- only show alpha class icon if that achieve is already unlocked (reduce clutter):
if pobj.badge[id] == 1 then
badge[id].classicon[classid]:setnewalpha(90)
badge[id].classicon[classid]:show()
else
badge[id].classicon[classid]:hide()
end
end
end
end
end
end
function badge:get_class_index(id, classid)
-- return the 1-72 position for this combo (player data)
return (id-1)*4 + classid
end
function badge:save_data(pobj)
badge.data = ""
for id = 1,#pobj.badge do
badge.data = badge.data..tostring(pobj.badge[id])
end
for classid = 1,#pobj.badgeclass do
badge.data = badge.data..tostring(pobj.badgeclass[classid])
end
PreloadGenClear()
PreloadGenStart()
badge.data = char:basic_encrypt(char.newseed, badge.data, true)
Preload("\")\ncall BlzSetAbilityTooltip('AOws',\""..badge.data.."\",".."0"..")\n//")
PreloadGenEnd("KoboldKingdom\\badges.txt")
badge.data = nil
end
function badge:load_data(pobj)
PreloadStart()
Preload("")
Preloader("KoboldKingdom\\badges.txt")
Preload("")
PreloadEnd(0.0)
utils.debugfunc(function()
badge.data = BlzGetAbilityTooltip(FourCC("AOws"), 0)
if badge.data and badge.data ~= "" then
local badge_length, class_length = #pobj.badge, #pobj.badgeclass
local decrypted = char:basic_encrypt(char.newseed, badge.data, false)
for pos = 1,(badge_length + class_length) do
if string.sub(decrypted, pos, pos) == "1" then
if pos <= badge_length then
-- load badge data:
pobj.badge[pos] = 1
else
-- load class button data:
pobj.badgeclass[pos - badge_length] = 1
end
end
end
end
badge:validate_all(pobj)
badge.data = nil
end, "load badges")
end
function badge:sync_past_achievements(pobj)
-- for retroactive awards (old save codes):
if quests_total_completed then
if quests_total_completed >= 2 then
badge:earn(pobj, 1, pobj.charid)
end
if quests_total_completed >= 3 then
badge:earn(pobj, 3, pobj.charid)
end
if quests_total_completed >= 4 then
badge:earn(pobj, 4, pobj.charid)
end
if quests_total_completed >= 5 then
badge:earn(pobj, 5, pobj.charid)
end
if quests_total_completed >= 6 then
badge:earn(pobj, 7, pobj.charid)
end
if quests_total_completed >= 9 then
badge:earn(pobj, 8, pobj.charid)
end
if quests_total_completed >= 12 then
badge:earn(pobj, 9, pobj.charid)
end
if quests_total_completed >= 14 then
badge:earn(pobj, 10, pobj.charid)
end
if quests_total_completed >= 15 then
badge:earn(pobj, 11, pobj.charid)
badge:earn(pobj, 12, pobj.charid)
end
end
badge:save_data(pobj)
end
boss = {}
function boss:init()
-- globals:
self.__index = self
self.stack = {}
-- build boss metadata (used for ALL bosses!)
self:buildbosses()
-- assign audio from init table:
self:init_sounds()
-- build boss health bar frames:
bossfr = kui.frame:newbytype("BACKDROP", bossfr)
bossfr:setbgtex("war3mapImported\\boss-panel-icon-header.blp")
bossfr:setsize(kui:px(315), kui:px(103))
bossfr:setfp(fp.t, fp.t, kui.worldui, 0, -kui:px(12))
bossfr.hpbar = kui.frame:newbysimple("BossHealthBar", kui.canvas.gameui)
bossfr.hpbar:setfp(fp.c, fp.t, kui.worldui, 0.0, -kui:px(124))
bossfr.hpbar:setlvl(2)
bossfr.hptxt = bossfr:createtext("0/0", nil, true)
bossfr.hptxt:setallfp(bossfr.hpbar)
bossfr.bossname = bossfr:createbtntext("Test Boss Name")
bossfr.bossname:setsize(kui:px(300), kui:px(28))
bossfr.bossname:setfp(fp.c, fp.c, bossfr.hpbar, 0, kui:px(28))
-- simple bars don't follow parent visibility:
bossfr.hidefunc = function() bossfr.hpbar:hide() end
bossfr.showfunc = function() bossfr.hpbar:show() end
-- `` intro slide-in frames:
bossslidetext1 = kui.canvas:createheadertext("Test Sliding Text", nil, true)
bossslidetext1:setsize(kui:px(500), kui:px(40))
bossslidetext1:setfp(fp.t, fp.c, kui.canvas, 0, -kui:px(50))
bossslidetext1:hide()
bossslidetext2 = kui.canvas:createbtntext("Test Sliding Text", nil, true)
bossslidetext2:setsize(kui:px(500), kui:px(28))
bossslidetext2:setfp(fp.t, fp.c, kui.canvas, 0, -kui:px(100))
bossslidetext2:hide()
bossfr:hide()
--[[
**********************
******boss class******
**********************
--]]
-- two spell groups:
-- 'basic' are spammed, should not be too threatening.
-- 'power' are threatening, happen less often.
self.spellbook = { -- stores spell functions.
basic = {}, power = {}, channel = {}, ultimate = {}
}
self.spellticks = { -- stores timer position to run certain spells.
basic = 0, power = 0, channel = 0, ultimate = 0
}
self.spellcadence = { -- when to cast specific spell types.
basic = 2.5, power = 12.5, channel = 16.0, ultimate = 24.0
}
-- intro can be any sound, basic and power should be an audible combat noise, channel should be a charge-up dialogue, ultimate is controlled globally.
self.bossaudio = { -- play this audio for certain events.
intro = nil, basic = nil, power = nil, channel = nil
}
-- general features:
self.trackedunit = nil -- move and cast spells in conjunction with this unit.
self.centerx = 0 -- x center of where boss leashes to.
self.centery = 1000 -- `` y.
self.casteffect = '' -- cast special effect.
self.casteffectsc = 1.0 -- `` scale
self.waxchunk = 20.0 -- x% health to award wax canisters.
self.waxcount = 3 -- `` how many wax canisters drop.
-- spells features:
self.casttime = 1.0 -- speed to cast an ability and prevent other actions.
self.iscasting = false -- tracks whether cast is in progress
self.pausespells = false -- pause abilities?
self.ultdelayspells = true -- does the ultimate delay other spell timers (to prevent overlap)?
self.randomspells = true -- are abilities randomly picked?
-- movement features:
self.collides = false -- does boss collide with objects (kept updated within ai timer).
self.moves = true -- boss is able to move at all.
self.leash = 1200 -- move toward player if outside of this range.
self.movecadence = 0.77 -- how often to check for movement updates (lower = more erratic).
self.ismoving = false -- tracks if currently moving.
self.moveduration = 1.45 -- how long the boss spends moving before re-evaluating.
self.moveticks = 0 -- track timer ticks
self.melee = false -- boss tries to stay in melee range.
-- evasive movement features:
self.evasive = true -- does ai move about randomly?
self.evasivecadence = 3.0 -- how often to check for evasion.
self.evasiveangle = 80 -- angle to evade from tracked unit.
self.evadetime = 1.42 -- how long an evade lasts.
-- minion features:
self.hasminions = true -- enables a move check in the ai timer for idle minions.
end
function boss:new(unit, bossid)
local o = setmetatable({}, self)
-- override any features assigned to the boss via the metadata table:
for id,t in pairs(self.bosst) do
if id == bossid then
for field,val in pairs(t) do
o[field] = val
end
end
end
o.shadowspells = {} -- store unlocked darkness spells that are triggered by candle light.
o.shadowticks = 0.33 -- `` track how often to cast
o.castindex = { basic = 0, power = 0, channel = 0, ultimate = 0 } -- tracks spell category order when randomspells is false.
o.unit = unit
o.bossid = bossid
SetHeroLevel(o.unit, math.max(10,map.mission.cache.level))
BlzFrameSetText(bossfr.bossname.fh, o.name)
o:scale_health()
o:start_update_hp_bar()
if o.playercolor then SetUnitColor(o.unit, GetPlayerColor(o.playercolor)) end
self.stack[o] = o
return o
end
function boss:run_intro_scene()
local dur = 4.9
local alpha = 0
local swoopdur = 0.9
local swoopend = math.ceil(swoopdur/0.03)
local alphainc = math.ceil(255/(swoopdur/0.03)) -- load to full alpha over x sec.
local ystart1, ystart2 = kui.position.centery - kui:px(60), kui.position.centery - kui:px(95)
local xstart1, xstart2, xspeed = kui.position.centerx - kui:px(400), kui.position.centerx + kui:px(400), kui:px(12)
BlzEnableSelections(false, false)
PauseAllUnitsBJ(true)
-- grant vision of boss to players:
utils.playerloop(function(p) UnitShareVisionBJ(true, self.unit, p) end)
utils.timed(1.0, function()
utils.debugfunc(function()
utils.playerloop(function(p)
ReleaseTimer(kobold.player[p].permtimer)
kui:hidegameui(p)
map.manager.candlebar:hide()
end)
bossslidetext1:show()
bossslidetext1:settext(self.name)
bossslidetext1:setalpha(0)
bossslidetext2:show()
bossslidetext2:settext(self.subtitle)
bossslidetext2:setalpha(0)
utils.playsoundall(kui.sound.bossintro)
utils.timedrepeat(0.03, dur/0.03, function(c)
utils.debugfunc(function()
utils.playerloop(function(p) CameraSetupApplyForPlayer(true, gg_cam_bossViewCam, p, 0.9) end)
alpha = alpha + alphainc
BlzFrameSetAlpha(bossslidetext1.fh, math.min(255, alpha))
BlzFrameSetAlpha(bossslidetext2.fh, math.min(255, alpha))
xstart1, xstart2 = xstart1 + xspeed, xstart2 - xspeed
bossslidetext1:setabsfp(fp.c, xstart1, ystart1)
bossslidetext2:setabsfp(fp.c, xstart2, ystart2)
if c == swoopend then xspeed = kui:px(0.6) map.mission.cache.boss:play_sound('intro') end
end, "boss intro 2nd timer")
end, function()
utils.debugfunc(function()
PauseAllUnitsBJ(false)
BlzEnableSelections(true, false)
utils.playerloop(function(p)
kui:showgameui(p, true)
map.manager.candlebar:show()
CameraSetupApplyForPlayer(true, gg_cam_gameCameraTemplate, p, 0.66)
utils.permselect(kobold.player[p].unit, p, kobold.player[p])
SelectUnitForPlayerSingle(kobold.player[p].unit, p)
end)
candle.current = candle.total + candle.bossb -- set wax to boss duration.
bossslidetext1:hide()
bossslidetext2:hide()
bossfr:show()
self:init_boss_ai()
end, "boss intro wrap-up")
end)
end, "boss intro 1st timer")
end)
end
function boss:init_boss_ai()
for i,v in pairs(self.spellticks) do
self.spellticks[i] = 0
end
self.debug = false
self.trackedunit = kobold.player[Player(0)].unit
self.waxthresh = 100.0
self.castcheckrate = 0.33
if not boss_dev_mode then candle:load() end
if map.manager.diffid == 5 then
self.castcheckrate = 0.45
self.channeltime = self.channeltime*0.8
self.casttime = self.casttime*0.8
end
-- -- --
if self.moves then
self.tmr = TimerStart(NewTimer(), 0.33, true, function()
utils.debugfunc(function()
if self.unit and utils.isalive(self.unit) and (boss_dev_mode or map.manager.activemap) then
self.moveticks = self.moveticks + 0.33
self:checkwax()
self:checkshadow()
-- check move conditions every full second:
if self.moveticks > self.movecadence and not self.ismoving and not self.iscasting then
-- update collision detection:
if self.collides then
SetUnitPathing(self.unit, true)
else
SetUnitPathing(self.unit, false)
end
-- check leash range
if self.debug then print("?: checking leash") end
if utils.distunits(self.trackedunit, self.unit) > self.leash then
if self.debug then print("-> leash range exceeded") end
self:move_to_xy(utils.unitxy(self.trackedunit))
self.moveticks = 0
return
-- if evasive, make ai move about.
elseif self.evasive and self.moveticks > self.evasivecadence then
if self.debug then print("?: checking if should evade") end
-- get random angle direction:
local a = self.evasiveangle
if math.random(1,2) == 1 then a = -1*a end
local a2 = utils.angleunits(self.unit, self.trackedunit) + a
local x,y = utils.unitprojectxy(self.unit, 1000, a2)
-- see if outside of arena, if so, reverse angle of movement toward center:
-- if utils.distxy(x, y, self.centerx, self.centery) > 1536 then
-- if self.debug then print("-> outside of arena detected") end
-- self:move_to_xy(self.centerx, self.centery)
-- else
if self.debug then print("-> evade") end
-- occasionally do a longer evade:
if math.random(1,4) == 1 then
self:move_to_xy(x, y, self.evadetime*1.33)
else
self:move_to_xy(x, y, self.evadetime)
end
-- end
-- if minions, move them toward tracked unit so they don't idle:
if self.hasminions then
local grp = g:newbyunitloc(Player(24), g_type_ally, self.unit, 3000.0)
grp:action(function() if grp.unit ~= self.unit then utils.issatkxy(grp.unit, utils.unitxy(self.trackedunit)) end end)
grp:destroy()
end
self.moveticks = 0
return
end
end
for abilitytype,tick in pairs(self.spellticks) do
if not self.iscasting then
self.spellticks[abilitytype] = self.spellticks[abilitytype] + self.castcheckrate
end
end
-- check spell conditions:
if self.debug then print("?: checking if should cast") end
if not self.iscasting and not self.ismoving then
-- see if timer has reached spell cast point for each spell category:
for abilitytype,tick in pairs(self.spellticks) do
if tick > self.spellcadence[abilitytype] then
-- cast basic ability:
self:start_casting(abilitytype)
self.spellticks[abilitytype] = 0
return
end
end
else
if self.debug then print("-> should not cast") end
end
else
-- if boss was defeated:
if not utils.isalive(self.unit) and map.manager.downp ~= map.manager.totalp then
-- clear area of boss units:
map.grid:clearunits(0.0, 0.0, 6400.0, true)
-- run victory items:
ResetUnitAnimation(self.unit)
SetUnitAnimation(self.unit, 'death')
self:play_death_effect()
utils.playsoundall(kui.sound.bossdefeat)
PauseTimer(self.candletmr)
map.manager.candlebar:hide()
alert:new(color:wrap(color.tooltip.good,self.name).." has been vanquished!", 6.0)
map.mission.cache:objstep(12.5)
if self.bossid == "N01R" then
map.block:spawnore(4*map.manager.diffid, 7, utils.unitx(self.unit), utils.unity(self.unit))
else
map.block:spawnore(3*map.manager.diffid, self.dmgtypeid, utils.unitx(self.unit), utils.unity(self.unit))
end
-- unlock badges:
badge:earn(kobold.player[Player(0)], self.badge_id)
if map.manager.diffid == 5 and kobold.player[Player(0)].level >= 60 then
badge:earn(kobold.player[Player(0)], self.badge_id_tyran)
local total_defeated = 0
for id = 13,17 do
if kobold.player[Player(0)].badge[id] == 1 and kobold.player[Player(0)].badgeclass[badge:get_class_index(id, kobold.player[Player(0)].charid)] == 1 then
total_defeated = total_defeated + 1
if total_defeated == 5 then
badge:earn(kobold.player[Player(0)], 18)
end
end
end
end
end
-- cleanup:
bossfr:hide()
StopSound(kui.sound.bossmusic, false, true)
-- delete after all map wrapup logic has referenced this object:
utils.timed(20.0, function() self.castindex = nil self.shadowspells = nil self.stack[self] = nil end)
ReleaseTimer()
end
end, "boss ai timer")
end)
else
-- TODO: what if boss is a stationary unit?
end
end
function boss:play_death_effect()
local x,y = utils.unitxy(self.unit)
utils.timedrepeat(0.24, 18, function()
utils.speffect(area_storm_stack[self.bosst[self.bossid].dmgtypeid].effect, utils.projectxy(x, y, math.random(100,260), math.random(0,360)))
end)
end
function boss:move_to_xy(x, y, _dur)
if self.debug then print("-> move to",x,y,"over",_dur or self.moveduration,"sec") end
self.ismoving = true
utils.issmovexy(self.unit, x, y)
utils.timed(_dur or self.moveduration, function() utils.stop(self.unit) self.ismoving = false end)
end
function boss:start_update_hp_bar()
if self.tmr then ReleaseTimer(self.tmr) end
self.tmr = TimerStart(NewTimer(),0.15,true,function()
utils.debugfunc(function()
if self.unit and utils.isalive(self.unit) then
BlzFrameSetValue(bossfr.hpbar.fh, math.ceil(GetUnitLifePercent(self.unit)))
BlzFrameSetText(bossfr.hptxt.fh, math.floor(GetUnitState(self.unit, UNIT_STATE_LIFE)) .."/" ..math.floor(GetUnitState(self.unit, UNIT_STATE_MAX_LIFE)))
else
BlzFrameSetValue(bossfr.hpbar.fh, 0)
ReleaseTimer()
end
end, "update bars")
end)
end
-- @abilitytype = "basic" or "power" or "channel"
-- @_castdur = override default cast duration.
function boss:start_casting(abilitytype, _castdur)
utils.debugfunc(function()
local ct = _castdur or self.casttime
if self.debug then print("issued to cast",abilitytype,"ability with cast time",ct) end
self.iscasting = true
utils.stop(self.unit)
utils.faceunit(self.unit, self.trackedunit)
if abilitytype == "channel" then
SetUnitAnimation(self.unit, 'channel')
ct = self.channeltime
speff_channel:attachu(self.unit, self.channeltime, 'origin', 2.0)
elseif abilitytype == "basic" then
SetUnitAnimation(self.unit, 'attack')
else
SetUnitAnimation(self.unit, 'spell')
end
utils.timed(ct, function()
utils.debugfunc(function()
self:end_casting(abilitytype)
self:spell_cast(abilitytype)
end, "start_casting timer")
end)
-- channel finish specific effects:
if abilitytype == 'channel' then
self:play_sound(abilitytype)
end
-- if ultimate, delay other abilities so they do not overlap:
if abilitytype == 'ultimate' and self.ultdelayspells then
self.spellticks['basic'] = self.spellticks['basic'] - self.spellcadence['basic']
self.spellticks['power'] = self.spellticks['power'] - self.spellcadence['power']
self.spellticks['channel'] = self.spellticks['channel'] - self.channeltime
end
end, "start_casting")
end
-- @abilitytype = 'basic', 'power', or 'channel'
function boss:spell_cast(abilitytype)
if self.debug then print('attempting to self:spell_cast for ability type',abilitytype) end
if not self.pausespells then
if self.randomspells then
self.spellbook[abilitytype][math.random(1,#self.spellbook[abilitytype])](self)
else -- ordered
if self.spellbook[abilitytype][self.castindex[abilitytype] + 1] then
self.castindex[abilitytype] = self.castindex[abilitytype] + 1
else
self.castindex[abilitytype] = 1
end
self.spellbook[abilitytype][self.castindex[abilitytype]](self)
end
end
end
function boss:end_casting(abilitytype)
if self.debug then print("finished casting") end
self.iscasting = false
ResetUnitAnimation(self.unit)
if self.casteffect then
self.casteffect:play(utils.unitx(self.unit), utils.unity(self.unit), nil, self.casteffectsc)
end
if abilitytype ~= 'channel' then
self:play_sound(abilitytype)
else
-- self:play_sound('basic')
end
end
function boss:init_sounds()
for bossid,_ in pairs(boss.bosst) do
boss.bosst[bossid].bossaudio = {}
for soundkey,sound in pairs(kui.sound.boss[bossid]) do
boss.bosst[bossid].bossaudio[soundkey] = sound
end
end
end
function boss:play_sound(soundkey)
if soundkey == 'intro' then
utils.playsoundall(self.bosst[self.bossid].bossaudio[soundkey])
elseif soundkey == 'ultimate' then
utils.playsoundall(kui.sound.boss.ultimate)
else
StopSound(self.bosst[self.bossid].bossaudio[soundkey], false, false)
PlaySoundOnUnitBJ(self.bosst[self.bossid].bossaudio[soundkey], 80.0, self.unit)
end
end
function boss:get_dmg(damage)
if map.manager.activemap then
return map.mission.cache.setting[m_stat_attack]*damage*(1 + (map.mission.cache.level^1.125)*0.03)
else
return damage
end
end
function boss:heal_value(amount)
utils.addlifeval(self.unit, amount, false)
end
function boss:heal_percent(percent)
utils.addlifep(self.unit, percent, false)
end
function boss:deal_area_dmg(damage, x, y, r, _hitfunc)
local grp = g:newbyxy(Player(24), g_type_dmg, x, y, r)
local damage = self:get_dmg(damage)
grp:action(function()
if utils.isalive(grp.unit) then
dmg.type.stack[self.dmgtypeid]:dealdirect(self.unit, grp.unit, damage)
if _hitfunc then
_hitfunc(grp.unit)
end
end
end)
grp:destroy()
end
function boss:scale_health()
local newhp = utils.life(self.unit)*map.mission.cache.setting[m_stat_health]
newhp = newhp*(1 + (map.mission.cache.level^1.1)*0.02)
if map.mission.cache.level >= 60 then
newhp = newhp*1.1
end
utils.setnewunithp(self.unit, math.floor(newhp), true)
utils.restorestate(self.unit)
BlzSetUnitArmor(self.unit, 5)
if map.mission.cache then
BlzSetUnitArmor(self.unit, math.ceil( BlzGetUnitArmor(self.unit)+((map.mission.cache.setting[m_stat_enemy_res]-1)*100)) )
end
end
-- create a visual rune marker at @x,@y that lasts for @dur to highlight landing locations etc.
function boss:marker(x, y, dur)
local e = speff_bossmark3:play(x, y, dur or nil, 1.15)
BlzSetSpecialEffectTimeScale(e, 300)
BlzSetSpecialEffectZ(e, BlzGetLocalSpecialEffectZ(e))
BlzSetSpecialEffectColorByPlayer(e, Player(0)) -- make it red.
return e
end
function boss:awardbosschest()
utils.looplocalp(function()
kui.canvas.game.bosschest:show()
for i = 1,5 do kui.canvas.game.bosschest.chest.gem[i]:hide() end
kui.canvas.game.bosschest.chest.gem[map.manager.prevbiomeid]:show()
end)
end
-- check if boss has lost a percentage of health, then drop wax.
function boss:checkwax()
if utils.getlifep(self.unit) < self.waxthresh - self.waxchunk and candle.current > 0 then
self.waxthresh = self.waxthresh - self.waxchunk
self:spawnwax(self.waxcount)
end
end
-- spawn wax canisters in a radius around the boss.
function boss:spawnwax(count)
local x,y = utils.unitxy(self.unit)
local size,tangle,incangle,x2,y2 = nil,math.random(0,360),360/count,0,0
if map.manager.diffid == 5 then size = "medium" end
for i = 1,count do
x2,y2 = utils.projectxy(x, y, math.random(650,750), tangle)
local mis = missile:create_arc(x2, y2, self.unit, 'Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl', 0)
mis.explfunc = function(mis) if map.manager.activemap then candle:spawnwax(mis.x, mis.y, size) end end
tangle = tangle + incangle
end
end
--[[
********************************************************
******************** darkness powers *******************
********************************************************
--]]
-- see if a shadow spell should be cast
function boss:checkshadow()
utils.debugfunc(function()
if #self.shadowspells > 0 and self.shadowticks > 15.0 then
for i = 1,3 do
if self.shadowspells[i] then utils.timed(i, function() self.shadowspells[i](self) end) end
end
self.shadowticks = 0
else
self.shadowticks = self.shadowticks + 0.33
end
end, "checkshadow")
end
function boss:assign_darkness_spell(waveid)
if waveid == 1 then
self.shadowspells[1] = function(bossobj) bossobj:darkness_shadow_mortar() end
elseif waveid == 2 then
self.shadowspells[2] = function(bossobj) bossobj:darkness_shadow_minions() end
elseif waveid == 3 then
self.shadowspells[3] = function(bossobj) bossobj:darkness_shadow_bomb() end
end
end
-- establish origin point of a darkness spell from center of map to tracked unit location:
function boss:init_darkness_spell()
local x1,y1 = utils.unitxy(self.trackedunit)
local a = utils.anglexy(0, 0, x1, y1)
local x2,y2 = utils.projectxy(0, 0, math.random(150,1000), a + math.randomneg(-4,4))
return x1, y1, x2, y2, a
end
function boss:darkness_shadow_mortar()
local x,y = utils.unitxy(self.unit)
speff_stormshad:play(x,y)
self:spell_artillery(34, mis_voidball, 1.0, 1.72, 0.18, math.random(16,18), 260.0, 120.0, nil, 300)
end
-- summons a void well which does light area damage while tossing shadow minions out of it.
function boss:darkness_shadow_minions()
local x1,y1,x2,y2,a = self:init_darkness_spell()
speff_voidpool:play(x2, y2, 12.0, 1.33)
speff_stormshad:play(x2,y2)
utils.timedrepeat(0.33, math.floor(12/0.33), function(c)
utils.debugfunc(function()
if utils.isalive(self.unit) then
if math.fmod(c, 3) == 0 then
x1,y1 = utils.projectxy(x2, y2, math.random(100,300), math.random(0,360))
local mis = missile:create_arc(x1, y1, self.unit, speff_embersha.effect, self:get_dmg(16))
mis.x, mis.y = x1, y1
mis.angle = utils.anglexy(x2, y2, x1, y1)
mis.explfunc = function(mis) utils.issatkxy(bm_darknessdrop:create(mis.x, mis.y), utils.unitx(self.trackedunit), utils.unity(self.trackedunit)) end
end
else
ReleaseTimer()
end
end, "minions timer")
end)
end
-- spawns an orb which chases the player, dealing massive damage if it hits them.
function boss:darkness_shadow_bomb()
local x1,y1,x2,y2,a = self:init_darkness_spell()
local vel = 3
local e = mis_voidballhge:play(x2, y2, 20.0, 1.1)
BlzSetSpecialEffectYaw(e, a*bj_DEGTORAD)
utils.seteffectxy(e, x2, y2, utils.getcliffz(x2, y2, 115))
speff_stormshad:play(x2,y2)
utils.timed(2.0, function()
utils.timedrepeat(0.03, math.floor(20/0.03), function(c)
utils.debugfunc(function()
if utils.isalive(self.unit) then
a = utils.anglexy(x2, y2, utils.unitx(self.trackedunit), utils.unity(self.trackedunit))
BlzSetSpecialEffectYaw(e, a*bj_DEGTORAD)
x2, y2 = utils.projectxy(x2, y2, vel, a)
utils.seteffectxy(e, x2, y2, utils.getcliffz(x2, y2, 215.0))
if math.fmod(c, 10) == 0 and utils.distxy(x2, y2, utils.unitx(self.trackedunit), utils.unity(self.trackedunit)) < 150.0 then
self:deal_area_dmg(self:get_dmg(124), x2, y2, 300.0)
speff_fspurp:play(x2, y2, nil, 1.5)
DestroyEffect(e)
ReleaseTimer()
end
else
DestroyEffect(e)
ReleaseTimer()
end
end, "bomb timer")
end)
end)
end
--[[
********************************************************
******************** boss abilities ********************
********************************************************
--]]
-- launches arcing missile(s) toward a player location.
function boss:spell_artillery(damage, effect, scale, traveltime, cadence, count, spread, _hitradius, _hitfunc, _startheight, _archeight)
utils.debugfunc(function()
local damage = self:get_dmg(damage)
if self.debug then print('-> trying to: spell_artillery') end
local x,y = utils.unitxy(self.trackedunit)
local x2,y2 = 0,0
utils.timedrepeat(cadence, count, function()
x2,y2 = utils.projectxy(x, y, math.random(math.floor(spread*0.5),spread), math.random(0,360))
local mis = missile:create_arc(x2, y2, self.unit, effect.effect, damage)
mis:updatescale(scale)
mis.dmgtype = dmg.type.stack[self.dmgtypeid]
mis.explr = _hitradius or 80.0
mis.offset = 120
mis.arch = _archeight or 600
-- initialize timed:
mis.time = traveltime
mis.height = _startheight or 100.0
mis.colfunc = _hitfunc or nil
boss:marker(x2, y2)
mis:initduration()
end)
end, "spell_artillery")
end
-- creates a radial spread of mines which explode after @detonatedelay seconds.
function boss:spell_mines(damage, spawneffect, explrad, mineeffect, scale, detonatedelay, detonateeffect, wavecount, wavedelay, spread, minecount, mineincrement)
utils.debugfunc(function()
local startangle,spawnangle,w = math.random(0,360),math.floor(360/minecount),0
local centerx,centery = utils.unitxy(self.unit)
local currentspread,spread,explrad,mineincrement,minecount,scale = spread,spread,explrad,mineincrement,minecount,scale
bossminet = {}
utils.timedrepeat(wavedelay, wavecount, function()
w = w + 1 -- wave number.
local wc = w -- instantiate wave.
bossminet[w] = {}
for i = 1,minecount do
bossminet[w][i] = {}
bossminet[w][i].x, bossminet[w][i].y = utils.projectxy(centerx, centery, currentspread, startangle)
spawneffect:play(bossminet[w][i].x, bossminet[w][i].y)
bossminet[w][i].e = mineeffect:play(bossminet[w][i].x, bossminet[w][i].y, detonatedelay, scale) -- mine effect (stored)
speffect:addz(bossminet[w][i].e, 16) -- raise it a bit to prevent clipping.
startangle = startangle + spawnangle
end
minecount = minecount + mineincrement
currentspread = currentspread + spread
spawnangle = spawnangle + 8
utils.timed(detonatedelay, function()
utils.debugfunc(function()
for i,t in ipairs(bossminet[wc]) do
if t.x and t.y then
detonateeffect:play(t.x, t.y)
self:deal_area_dmg(damage, t.x, t.y, explrad)
bossminet[wc][i] = nil
end
end
bossminet[wc] = nil
end, "spell_mines detonate")
end)
end)
end, "spell_mines")
end
function boss:spell_missile_circle(damage, effect, scale, distance, count, velocity, _duration, _hitradius, _height, _collides, _explodes)
-- launch a fan of missiles in every direction around the boss.
local x,y = utils.unitxy(self.unit)
local damage = self:get_dmg(damage)
missile:create_in_radius(x, y, self.unit, effect.effect, damage, dmg.type.stack[self.dmgtypeid], count,
distance, _duration or nil, _hitradius or nil, _height or nil, _collides or nil, _explodes or nil)
end
function boss:spell_wall_of_fire(damage, walleffect, moveeffect, scale, velocity, duration, hitradius, startx, starty, endx, endy, chunkspread, length, _hitfunc)
utils.debugfunc(function()
local e,x,y,t,a,d,hr,v,cr = {},0,0,1,utils.anglexy(startx, starty, endx, endy),damage,hitradius,velocity,chunkspread/2
-- build random gap:
-- build "up":
for i = 1,math.ceil(length/2) do
e[i] = {}
e[i].x, e[i].y = utils.projectxy(startx, starty, cr, a - 90)
e[i].e = walleffect:play(x,y,duration,scale or 1.0)
cr = cr + chunkspread
t = t + 1
end
cr = chunkspread/2
-- build "down":
for i = t,t+math.floor(length/2) do
e[i] = {}
e[i].x, e[i].y = utils.projectxy(startx, starty, cr, a + 90)
e[i].e = walleffect:play(x,y,duration,scale or 1.0)
cr = cr + chunkspread
t = t + 1
end
-- run timer:
local tick = 0
utils.timedrepeat(0.03,math.floor(duration/0.03),function()
utils.debugfunc(function()
if map.manager.activemap then
tick = tick + 1
-- move effects forward:
for i,t in ipairs(e) do
t.x, t.y = utils.projectxy(t.x, t.y, v, a)
utils.seteffectxy(t.e, t.x, t.y)
end
-- deal damage every 0.33 sec:
if math.fmod(tick,10) == 0 then
for i,t in ipairs(e) do
self:deal_area_dmg(d, t.x, t.y, hr)
if math.fmod(tick,12) == 0 then
moveeffect:playradius(hr, 2, t.x, t.y)
end
end
end
else
for i,t in ipairs(e) do if t.e then DestroyEffect(t.e) end e[i] = nil end e = nil ReleaseTimer()
end
end, "spell_wall_of_fire timer")
end, function()
if e then
for i,t in ipairs(e) do if t.e then DestroyEffect(t.e) end e[i] = nil end e = nil
end
end)
end, "spell_wall_of_fire")
end
function boss:spell_summon_minions(miseffect, creatureobj, spawneffect, centerx, centery, distance, spreaddist, staggerdist, count, wavecount, cadence)
-- launches missiles in a radius with a @stagger distance over @cadence time, generating minions upon landing.
local cx, cy, dist, c, wc, ca = centerx, centery, distance, count, wavecount, cadence
local a, ao, sp, spo, stag = 0, 360/count, distance, spreaddist, staggerdist
local landfunc = function(x,y)
if utils.isalive(self.unit) and (boss_dev_mode or map.manager.activemap) then
utils.issatkxy(creatureobj:create(x,y), utils.unitxy(self.trackedunit))
spawneffect:play(x,y)
end
end
utils.timedrepeat(ca, wc, function()
utils.debugfunc(function()
a, sp = math.random(0,360), sp + spo -- get random start angle, increase wave distance from center.
-- summon minions:
for i = 1,count do
tarx,tary = utils.projectxy(cx, cy, sp, a)
boss:marker(tarx, tary)
tarx,tary = utils.projectxy(tarx, tary, math.random(math.floor(stag/2),stag), math.random(0,360))
local mis = missile:create_arc(tarx, tary, self.unit, miseffect.effect, 0)
mis.explfunc = function() landfunc(mis.x, mis.y) end
a = a + ao
end
end, "spell_summon_minions timer")
end)
end
function boss:spell_trample(damage, velocity, delay, hitradius, tarx, tary, animindex, exhaustdur)
self.iscasting = true
local exhaustdur = exhaustdur or 0.03
local dist = utils.distxy(utils.unitx(self.unit), utils.unity(self.unit), tarx, tary)
local angle = utils.anglexy(utils.unitx(self.unit), utils.unity(self.unit), tarx, tary)
local markd = dist/5 -- number of markers to place
local x,y = utils.projectxy(utils.unitx(self.unit), utils.unity(self.unit), markd, angle)
for i = 1,5 do
x,y = utils.projectxy(utils.unitx(self.unit), utils.unity(self.unit), markd*i, angle)
boss:marker(x, y)
end
utils.face(self.unit, angle)
utils.timed(delay, function()
utils.debugfunc(function()
sp_enchant_eff[2]:attachu(self.unit, dist/velocity*0.03, 'origin')
PauseUnit(self.unit, true)
ResetUnitAnimation(self.unit)
SetUnitAnimationByIndex(self.unit, animindex)
utils.timedrepeat(0.03,dist/velocity,function(c)
utils.debugfunc(function()
if utils.isalive(self.unit) then
x,y = utils.projectxy(utils.unitx(self.unit), utils.unity(self.unit), velocity, angle)
utils.setxy(self.unit, x, y)
if math.fmod(c, 11) == 0 then
self:deal_area_dmg(damage, x, y, hitradius, nil)
speff_dustcloud:playradius(hitradius, 3, x, y)
speff_quake:playradius(hitradius, 3, x, y)
end
else
ReleaseTimer()
end
end, "spell_trample timer 2")
end, function()
SetUnitVertexColor(self.unit, 125, 125, 125, 255)
ArcingTextTag(color:wrap(color.tooltip.bad, "Exhausted!"), self.unit, 3.0)
ResetUnitAnimation(self.unit)
utils.timed(exhaustdur, function()
SetUnitVertexColor(self.unit, 255, 255, 255, 255)
self.iscasting = false
PauseUnit(self.unit, false)
end)
end)
end, "spell_trample timer 1")
end)
end
function boss:spell_shockwave(damage, effect, scale, waveeffect, startx, starty, angle, velocity, traveltime, hitradius, _misheight)
local x,y = utils.projectxy(startx, starty, 1000, angle)
local d = self:get_dmg(damage)
local mis = missile:create_piercexy(x, y, self.unit, effect.effect, 0)
local tick = 0
mis.time = traveltime
mis.radius = hitradius
mis.vel = velocity
mis.height = _misheight or mis.height
mis.dmgtype = dmg.type.stack[self.dmgtypeid]
mis:initduration()
mis:updatescale(scale)
utils.timedrepeat(0.12, math.floor(traveltime/0.12), function()
tick = tick + 1
if mis then
if tick >= 3 then
waveeffect:playradius(mis.radius, 3, mis.x, mis.y)
self:deal_area_dmg(d, mis.x, mis.y, mis.radius, nil)
tick = 0
end
end
end)
end
function boss:spell_spinning_turret(damage, turreteffect, turretscale, height, miseffect, spawnx, spawny, duration, spinrate, radius, misvelocity, misdist, _isartillery,
_dmgtype)
-- creates a special effect shoots missiles in a radius over time.
local d,a,ao,x,y,spawnx,spawny,isartillery = self:get_dmg(damage),math.random(0,360),360*spinrate,0,0,spawnx,spawny,_isartillery
local r,v = radius,misvelocity
utils.seteffectxy(turreteffect:play(spawnx, spawny, duration, turretscale), spawnx, spawny, utils.getcliffz(spawnx, spawny, height))
utils.timed(1.5, function()
utils.timedrepeat(spinrate, math.floor(duration/spinrate), function()
utils.debugfunc(function()
x,y = utils.projectxy(spawnx, spawny, r, a) -- missile landing point
local mis = missile:create_point(spawnx, spawny, utils.anglexy(spawnx, spawny, x, y), misdist, self.unit, miseffect.effect, d, isartillery, 0.90)
mis.dmgtype = _dmgtype or dmg.type.stack[self.dmgtypeid]
mis.explr = 80.0
mis.radius = 90.0
mis.vel = v
a = a + ao
end, "spell_spinning_turret timer")
end)
end)
end
function boss:buildbosses()
-- store UI meta details by unit code:
self.bosst = {}
self.bosst['N01B'] = {name = "The Slag King", subtitle = "Is it getting hotter in here?", code = 'N01B', biomeid = biome_id_slag, dmgtypeid = dmg_fire}
self.bosst['N01B'].badge_id = 7
self.bosst['N01B'].badge_id_tyran = 13
self.bosst['N01B'].casteffect = speff_conflagoj
self.bosst['N01B'].evadetime = 0.86
self.bosst['N01B'].casttime = 1.21
self.bosst['N01B'].casteffectsc = 2.25
self.bosst['N01B'].channeltime = 4.25
self.bosst['N01B'].hasminions = false
self.bosst['N01B'].playercolor = Player(5) -- orange
self.bosst['N01B'].spellcadence = { basic = 1.66, power = 6.0, channel = 8.0, ultimate = 16.0 }
self.bosst['N01B'].spellbook = {
basic = {
[1] = function(bossobj) -- launches a volley of fireballs at the tracktarget.
bossobj:spell_artillery(42, mis_fireballbig, 0.81, 1.9, 0.19, math.random(9,15), 330.0, 120.0, nil, 300)
end,
},
power = {
[1] = function(bossobj) -- spews mines over the arena that explode in waves.
bossobj:spell_mines(64, speff_conflagoj, 142.0, mis_grenadeoj, 1.33, 4.0, speff_redburst, 8, 0.33, 260.0, 6, 3)
end,
},
channel = {
[1] = function(bossobj) -- launches repeat fiery missiles in a radius that increase in travel range per wave.
local c,d = 10,450
kb:new_pgroup(Player(24), utils.unitx(bossobj.unit), utils.unity(bossobj.unit), 350.0, 400.0, 0.75)
utils.timedrepeat(0.54, 8, function()
bossobj:spell_missile_circle(24, mis_fireball, 1.15, d, c, 7, nil, 60.0, 95.0, true)
c,d = c+1,d+425
end)
end
},
ultimate = {
[1] = function(bossobj) -- creates a wall of fire that tracks across the arena.
local x,y = utils.unitx(bossobj.unit),utils.unity(bossobj.unit)
local x1,y1 = utils.projectxy(x, y, 2500, utils.angleunits(bossobj.unit, bossobj.trackedunit) - 180)
local x2,y2 = utils.projectxy(x, y, 2500, utils.angleunits(bossobj.unit, bossobj.trackedunit))
bossobj:spell_wall_of_fire(86, speff_sacstorm, speff_flare, 1.5, 10.5, 24.0, 280, x1, y1, x2, y2, 725, 8)
end,
}
}
self.bosst['N01F'] = {name = "Marsh Mutant", subtitle = "*Inaudible gurgling noises.*", code = 'N01F', biomeid = biome_id_mire, dmgtypeid = dmg_nature}
self.bosst['N01F'].badge_id = 8
self.bosst['N01F'].badge_id_tyran = 14
self.bosst['N01F'].casteffect = speff_waterexpl
self.bosst['N01F'].casteffectsc = 2.5
self.bosst['N01F'].channeltime = 2.75
self.bosst['N01F'].randomspells = false
self.bosst['N01F'].playercolor = Player(6) -- green
self.bosst['N01F'].spellcadence = { basic = 4.87, power = 9.21, channel = 14.33, ultimate = 21.00 }
self.bosst['N01F'].spellbook = {
basic = {
[1] = function(bossobj) -- launches missiles in a radius to spawn a horde of minions.
local x,y = utils.unitxy(bossobj.unit)
bossobj:spell_summon_minions(speff_embernat, bm_swampling, speff_water, utils.unitx(bossobj.unit), utils.unity(bossobj.unit), 800.0, 200.0, 200.0, 4, 3, 0.33)
end,
[2] = function(bossobj) -- launches 3 waves of sludge missiles over 3 sec
local misfunc = function(bossobj)
local centerx,centery = utils.unitxy(bossobj.trackedunit)
local x,y,starta,offa = 0, 0, utils.angleunits(bossobj.unit, bossobj.trackedunit), 90
starta = -offa
for i = 1,3 do
x,y = utils.projectxy(centerx, centery, 225, starta)
local mis = missile:create_arc(x, y, bossobj.unit, mis_acid.effect, boss:get_dmg(24))
mis.bounces = 0
mis.explr = 96
mis.time = 1.78
mis.dmgtype = dmg.type.stack[dmg_nature]
mis:updatescale(1.33)
mis:initduration()
starta = starta + offa
end
end
misfunc(bossobj)
utils.timedrepeat(1.0, 3, function()
misfunc(bossobj)
end, function() misfunc = nil end)
end
},
power = {
[1] = function(bossobj) -- launches a single torrent of water in a line at trackedunit.
local startangle = utils.angleunits(bossobj.unit, bossobj.trackedunit) + math.randomneg(-4,4)
bossobj:spell_shockwave(24, mis_waterwave, 0.7, speff_water, utils.unitx(bossobj.unit), utils.unity(bossobj.unit), startangle, 7.0, 12.0, 150)
end
},
channel = {
[1] = function(bossobj) -- generate turrets that lob water artillery in a circle.
local x,y,a,ao = 0,0,math.random(0,360),360/6
utils.timedrepeat(0.45, 8, function()
x,y = utils.projectxy(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), math.random(1200,1300), a - math.random(0,4))
speff_conflaggn:play(x, y, nil, 1.5)
bossobj:spell_spinning_turret(24, mis_boltgreen, 0.7, 124.0, mis_shot_nature, x, y, 13.5, 0.09, 260.0, 10, 333.0, false)
a = a + ao
end)
end
},
ultimate = {
[1] = function(bossobj) -- launches multiple torrents of water in a line at trackedunit.
bossobj.iscasting = true
PauseUnit(bossobj.unit, true)
local startangle, angleoffset = 0,0
utils.timed(1.33, function()
utils.timedrepeat(1.21, 6, function()
if utils.isalive(bossobj.unit) then
angleoffset = math.random(30,40)
startangle = utils.angleunits(bossobj.unit, bossobj.trackedunit) - angleoffset
SetUnitAnimation(bossobj.unit, 'spell')
speff_waterexpl:play(utils.unitxy(bossobj.unit))
for i = 1,3 do
bossobj:spell_shockwave(24, mis_waterwave, 0.7, speff_water, utils.unitx(bossobj.unit), utils.unity(bossobj.unit), startangle, 6.5, 13.0, 150, 100)
startangle = startangle + angleoffset
end
else
ReleaseTimer()
end
end, function()
bossobj.iscasting = false
PauseUnit(bossobj.unit, false)
end)
end)
end
}
}
self.bosst['N01H'] = {name = "Megachomp", subtitle = "Nobody move a muscle.", code = 'N01H', biomeid = biome_id_foss, dmgtypeid = dmg_phys}
self.bosst['N01H'].badge_id = 9
self.bosst['N01H'].badge_id_tyran = 15
self.bosst['N01H'].casteffect = speff_roar
self.bosst['N01H'].casteffectsc = 2.5
self.bosst['N01H'].evadetime = 0.91
self.bosst['N01H'].casttime = 0.66
self.bosst['N01H'].channeltime = 1.25
self.bosst['N01H'].randomspells = false
self.bosst['N01H'].playercolor = Player(0) -- red
self.bosst['N01H'].spellcadence = { basic = 3.81, power = 9.27, channel = 5.21, ultimate = 14.00 }
self.bosst['N01H'].spellbook = {
basic = {
[1] = function(bossobj) -- launch a huge boulder which summons an invincible golem which gets turned into artillery missiles by power ability.
utils.timedrepeat(0.33, 3, function()
local x,y = utils.projectxy(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), math.random(800, 1000),
math.random(math.floor(utils.angleunits(bossobj.unit, bossobj.trackedunit)-30),math.floor(utils.angleunits(bossobj.unit, bossobj.trackedunit)+30)))
local mis = missile:create_timedxy(x, y, 2, bossobj.unit, mis_rock.effect, bossobj:get_dmg(48))
mis.x,mis.y = utils.projectxy(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), 175.0, utils.getface(bossobj.unit))
mis.dmgtype = dmg.type.stack[bossobj.dmgtypeid]
mis.vel = 10.5
mis.radius = 96
mis.expl = true
mis.height = 133
mis.explfunc = function(mis)
if utils.isalive(bossobj.unit) then
local u = bm_livfossil:create(mis.x, mis.y)
utils.setinvul(u, true)
PauseUnit(u, true)
utils.timedrepeat(2.0, nil, function()
if utils.isalive(u) and utils.isalive(bossobj.unit) then
SetUnitAnimation(u, 'spell slam')
utils.faceunit(u, bossobj.trackedunit)
local mis = missile:create_timedxy(utils.unitx(bossobj.trackedunit), utils.unity(bossobj.trackedunit),
12.0, u, mis_rock.effect, bossobj:get_dmg(16))
mis.vel = math.random(14,18)
mis.dmgtype = dmg.type.stack[bossobj.dmgtypeid]
else
if u then KillUnit(u) end
ReleaseTimer()
end
end)
utils.issatkunit(u, bossobj.trackedunit)
end
end
mis:updatescale(2.3)
end)
end,
[2] = function(bossobj) -- stomps the ground to create a tremor that slowly moves in target direction.
bossobj.iscasting = true
PauseUnit(bossobj.unit, true)
SetUnitAnimationByIndex(bossobj.unit, 4)
utils.timedrepeat(0.78, 3, function(c)
local c = c + 1 -- prevent 0 for random angle negative function.
if utils.isalive(bossobj.unit) then
SetUnitAnimationByIndex(bossobj.unit, 4)
local a,vel = utils.angleunits(bossobj.unit, bossobj.trackedunit), 90.0
local x,y = utils.unitprojectxy(bossobj.unit, 100.0, a)
speff_quake:play(x,y)
if utils.isalive(bossobj.unit) then
utils.timedrepeat(0.36, 62, function()
if utils.isalive(bossobj.unit) then
x,y = utils.projectxy(x, y, vel, a)
speff_quake:play(x,y)
bossobj:deal_area_dmg(12, x, y, 180.0, function(u) bf_slow:apply(u, 6.0) end)
local grp = g:newbyxy(Player(24), g_type_none, x, y, 180.0)
grp:action(function()
if GetUnitTypeId(grp.unit) == FourCC('n01I') and utils.isalive(grp.unit) then -- convert golems into artillery missiles.
local x,y = utils.unitxy(grp.unit)
missile:create_arc_in_radius(x, y, grp.unit, mis_rock.effect, bossobj:get_dmg(36),
dmg.type.stack[bossobj.dmgtypeid], math.random(5,7), math.random(200,300), 2.21, 90.0, 350.0)
KillUnit(grp.unit)
end
end)
grp:destroy()
end
end)
end
else
ReleaseTimer()
end
end, function()
ResetUnitAnimation(bossobj.unit)
bossobj.iscasting = false
PauseUnit(bossobj.unit, false)
end)
end
},
power = {
[1] = function(bossobj) -- slams the ground, slowing all Kobolds in a huge radius and breaking golems.
local d,c = 225, 6
local x,y = utils.unitxy(bossobj.unit)
utils.timedrepeat(0.15, 18, function(count)
utils.debugfunc(function()
speff_dustcloud:playradius(d, c, x, y)
speff_quake:playradius(d, c, x, y)
d,c = d + 225, c + 1
if math.fmod(count, 4) == 0 then
local grp = g:newbyxy(Player(24), g_type_none, x, y, d)
grp:action(function()
if GetUnitTypeId(grp.unit) == FourCC('n01I') and utils.isalive(grp.unit) then -- convert golems into artillery missiles.
local x,y = utils.unitxy(grp.unit)
missile:create_arc_in_radius(x, y, grp.unit, mis_rock.effect, bossobj:get_dmg(36),
dmg.type.stack[bossobj.dmgtypeid], math.random(5,7), math.random(200,300), 2.21, 90.0, 350.0)
KillUnit(grp.unit)
end
end)
bossobj:deal_area_dmg(6, x, y, d/2, function(u) bf_slow:apply(u, 8.0, true) end)
grp:destroy()
end
end, "megachomp power timer")
end)
end
},
channel = {
[1] = function(bossobj) -- charges the focus target.
local delay = 1.51
if map.manager.diffid and map.manager.diffid == 5 then delay = delay*0.6 end
kb:new_pgroup(Player(24), utils.unitx(bossobj.unit), utils.unity(bossobj.unit), 400.0, 500.0, 0.75)
local x,y = utils.projectxy(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), 1600, utils.angleunits(bossobj.unit, bossobj.trackedunit))
bossobj:spell_trample(64, 24, delay, 264, x, y, 0, 3.78)
end
},
ultimate = {
[1] = function(bossobj) -- slows boss, chases player, launches artillery. if they get close, player loses 50% health and is stunned. boss heals for 10% health.
utils.playsoundall(gg_snd_boss_megachomp_ultimate)
speff_followmark:attachu(bossobj.trackedunit, 0.33*16, 'overhead')
speff_ragered:attachu(bossobj.unit, 0.33*16, 'overhead')
bossobj.iscasting = true
SetUnitMoveSpeed(bossobj.unit, 330.0)
utils.timed(1.33, function()
utils.timedrepeat(0.33, 18, function()
local x1, y1, x2, y2
local angle = utils.angleunits(bossobj.unit, bossobj.trackedunit)
local d = 164
-- launch rocks:
utils.timedrepeat(0.33, 4, function()
x1,y1 = utils.projectxy(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), d, angle-30)
x2,y2 = utils.projectxy(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), d, angle+30)
boss:marker(x1,y1)
boss:marker(x2,y2)
local mis1 = missile:create_arc(x1, y1, bossobj.unit, mis_rock.effect, bossobj:get_dmg(28))
local mis2 = missile:create_arc(x2, y2, bossobj.unit, mis_rock.effect, bossobj:get_dmg(28))
mis1.dmgtype, mis1.explr = dmg.type.stack[bossobj.dmgtypeid], 60.0
mis2.dmgtype, mis2.explr = dmg.type.stack[bossobj.dmgtypeid], 60.0
d = d + 164
end)
-- move unit:
utils.issmovexy(bossobj.unit, utils.unitxy(bossobj.trackedunit))
speff_dustcloud:playradius(156, 4, utils.unitxy(bossobj.unit))
if utils.distunits(bossobj.unit, bossobj.trackedunit) < 260.0 then
speff_bloodsplat:playradius(156, 4, utils.unitxy(bossobj.unit))
speff_pothp:play(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), nil, 2.25)
dmg.type.stack[bossobj.dmgtypeid]:dealdirect(bossobj.unit, bossobj.trackedunit, utils.maxlife(bossobj.trackedunit)*0.5)
bf_stun:apply()
bossobj.iscasting = false
bossobj:heal_percent(10.0)
SetUnitMoveSpeed(bossobj.unit, 522.0)
bossobj.iscasting = true
PauseUnit(bossobj.unit, true)
SetUnitAnimationByIndex(bossobj.unit, 11)
utils.timed(1.44, function() bossobj.iscasting = false PauseUnit(bossobj.unit, false) end)
ReleaseTimer()
end
end, function()
if utils.isalive(bossobj.unit) then
PauseUnit(bossobj.unit, true) -- exhaust so melee can get hits in
SetUnitVertexColor(bossobj.unit, 125, 125, 125, 255)
ArcingTextTag(color:wrap(color.tooltip.bad, "Exhausted!"), bossobj.unit, 3.0)
utils.timed(6.0, function()
SetUnitVertexColor(bossobj.unit, 255, 255, 255, 255)
PauseUnit(bossobj.unit, false)
bossobj.iscasting = false
SetUnitMoveSpeed(bossobj.unit, 522.0)
end)
end
end)
end)
end
}
}
self.bosst['N01J'] = {name = "Thawed Experiment", subtitle = "It's time to chill out.", code = 'N01J', biomeid = biome_id_ice, dmgtypeid = dmg_frost}
self.bosst['N01J'].badge_id = 10
self.bosst['N01J'].badge_id_tyran = 16
self.bosst['N01J'].casteffect = speff_conflagbl
self.bosst['N01J'].casteffectsc = 2.25
self.bosst['N01J'].evadetime = 1.45
self.bosst['N01J'].casttime = 1.05
self.bosst['N01J'].channeltime = 1.50
self.bosst['N01J'].randomspells = false
self.bosst['N01J'].playercolor = Player(1) -- blue
self.bosst['N01J'].spellcadence = { basic = 3.31, power = 6.3, channel = 16.65, ultimate = 12.1 }
self.bosst['N01J'].spellbook = {
basic = {
[1] = function(bossobj) -- launches a cluster of frozen bolts which explode to create impassable ice blocks.
local x,y = 0,0
for i = 1,6 do
x,y = utils.unitxy(bossobj.trackedunit)
x,y = utils.projectxy(x, y, math.random(30, 300), math.random(0,360))
local mis = missile:create_arc(x, y, bossobj.unit, mis_blizzard.effect, boss:get_dmg(32))
mis.time = 1.75
mis.radius = 84.0
mis.explr = 150.0
mis.vel = 16.0
mis.dmgtype = dmg.type.stack[bossobj.dmgtypeid]
mis.explfunc = function(mis)
if utils.isalive(bossobj.unit) then
utils.timedlife(utils.unitatxy(Player(24), mis.x, mis.y, 'h00I'), 24.0)
speff_frostnova:play(mis.x, mis.y)
end
end
mis:initduration()
end
end,
[2] = function(bossobj) -- launches frozen orbs which summon minions.
local x,y = 0,0
for i = 1,9 do
x,y = utils.unitxy(bossobj.trackedunit)
x,y = utils.projectxy(x, y, math.random(10, 600), math.random(0,360))
local mis = missile:create_arc(x, y, bossobj.unit, speff_frost.effect, boss:get_dmg(6))
mis.time = 1.75
mis.radius = 76.0
mis.vel = 17.0
mis.dmgtype = dmg.type.stack[bossobj.dmgtypeid]
mis.explfunc = function(mis)
if utils.isalive(bossobj.unit) then
utils.issatkxy(bm_discexperim:create(mis.x, mis.y), utils.unitxy(bossobj.trackedunit))
speff_conflagbl:play(mis.x, mis.y)
end
end
mis:initduration()
end
end
},
power = {
[1] = function(bossobj) -- launches a line of crystals which act as movement blockers. slows on hit.
bossobj.iscasting = true
PauseUnit(bossobj.unit, true)
utils.timedrepeat(1.0, 3, function()
if utils.isalive(bossobj.unit) then
SetUnitAnimation(bossobj.unit, 'spell')
local a,dt,d,x,y = 0,89.0,89.0,0,0
a = utils.angleunits(bossobj.unit, bossobj.trackedunit)
utils.face(bossobj.unit, a)
utils.timedrepeat(0.21, 18, function()
if utils.isalive(bossobj.unit) then
x,y = utils.unitxy(bossobj.unit)
x,y = utils.projectxy(x, y, dt, a)
x,y = utils.projectxy(x, y, math.random(0,16), math.random(90,180))
utils.timedlife(utils.unitatxy(Player(24), x, y, 'h00I'), 24.0)
bossobj:deal_area_dmg(32, x, y, 112.0, function(u) bf_slow:apply(u, 3.00, true) end)
speff_frostnova:play(x, y)
dt = dt + d
end
end)
end
end, function() bossobj.iscasting = false PauseUnit(bossobj.unit, false) end)
end
},
channel = {
[1] = function(bossobj) -- launches a torrent of slow, spinning missiles which make a full loop around the arena.
bossobj.iscasting = true
PauseUnit(bossobj.unit, true)
utils.timedrepeat(0.55, 12, function(c)
if utils.isalive(bossobj.unit) then
if math.fmod(c, 3) == 0 then
SetUnitAnimation(bossobj.unit, 'spell slam')
speff_iceblast:play(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), nil, 2.25)
end
utils.timedrepeat(0.11, 5, function()
local mis = missile:create_angle(math.random(0,360), 9600, bossobj.unit, speff_icecrystal.effect, bossobj:get_dmg(48))
mis.dmgtype= dmg.type.stack[bossobj.dmgtypeid]
mis.vel = 8.3
mis.explr = 124.0
mis.radius = 84.0
mis.expl = true
mis.func = function(mis)
mis.angle = mis.angle - 0.36
if not utils.isalive(bossobj.unit) then
mis:destroy()
end
end
mis.explfunc = function(mis)
speff_iceblast:play(mis.x, mis.y)
end
end)
end
end, function() bossobj.iscasting = false PauseUnit(bossobj.unit, false) end)
end
},
ultimate = {
[1] = function(bossobj) -- summons a mana storm which follows the player.
local dur,vel = 60.0, math.random(1,3)
local a = utils.angleunits(bossobj.unit, bossobj.trackedunit)
local x,y = utils.projectxy(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), 300.0, a)
local e = speff_manastorm:play(x, y, dur)
BlzSetSpecialEffectColor(e, 0, 0, 0)
utils.timed(1.87, function()
BlzSetSpecialEffectColor(e, 255, 255, 255)
utils.timedrepeat(0.03, dur/0.03, function(c)
if utils.isalive(bossobj.unit) then
a = utils.anglexy(x, y, utils.unitxy(bossobj.trackedunit))
x,y = utils.projectxy(x, y, vel, a)
utils.seteffectxy(e, x, y, BlzGetLocalSpecialEffectZ(e))
if math.fmod(c, 21) == 0 then
speff_iceblast:play(x, y)
bossobj:deal_area_dmg(64, x, y, 200.0, nil)
end
else
DestroyEffect(e)
ReleaseTimer()
end
end)
end)
end
}
}
self.bosst['N01R'] = {name = "Amalgam of Greed", subtitle = "Taking one coin won't hurt... right?", code = 'N01R', biomeid = biome_id_vault, dmgtypeid = dmg_phys}
self.bosst['N01R'].badge_id = 11
self.bosst['N01R'].badge_id_tyran = 17
self.bosst['N01R'].casteffect = speff_edict
self.bosst['N01R'].casteffectsc = 1.33
self.bosst['N01R'].evadetime = 1.42
self.bosst['N01R'].casttime = 1.14
self.bosst['N01R'].channeltime = 4.50
self.bosst['N01R'].randomspells = false
self.bosst['N01R'].playercolor = Player(4) -- yellow
self.bosst['N01R'].spellcadence = { basic = 3.27, power = 7.3, channel = 23.33, ultimate = 30.0 }
self.bosst['N01R'].spellbook = {
basic = {
[1] = function(bossobj) -- launches a line of shining flares at the player, which then return back toward the boss after meeting max range.
local flaret = {}
local a = utils.angleunits(bossobj.unit, bossobj.trackedunit)
local count, vel, startx, starty = 0, 120, utils.unitprojectxy(bossobj.unit, 150.0, a)
boss:marker(startx, starty)
utils.timedrepeat(0.84, 3, function(c)
count = #flaret+1
flaret[count] = {}
flaret[count].x = startx
flaret[count].y = starty
end)
utils.timedrepeat(0.42, 24, function(c2)
if flaret[1] then
for idex = 1,#flaret do
flaret[idex].x, flaret[idex].y = utils.projectxy(flaret[idex].x, flaret[idex].y, vel, a)
if math.random(0,1) == 1 then
speff_flare:play(flaret[idex].x, flaret[idex].y)
else
speff_edict:play(flaret[idex].x, flaret[idex].y)
end
bossobj:deal_area_dmg(16, flaret[idex].x, flaret[idex].y, 160.0)
end
end
end, function()
a = a - 180.0 -- invert the traveled path.
utils.timedrepeat(0.42, 24, function(c3)
if flaret[1] then
for idex = 1,#flaret do
flaret[idex].x, flaret[idex].y = utils.projectxy(flaret[idex].x, flaret[idex].y, vel, a)
if math.random(0,1) == 1 then
speff_flare:play(flaret[idex].x, flaret[idex].y)
else
speff_edict:play(flaret[idex].x, flaret[idex].y)
end
bossobj:deal_area_dmg(32, flaret[idex].x, flaret[idex].y, 160.0)
end
end
end, function()
utils.deep_destroy(flaret)
end)
end)
end,
[2] = function(bossobj) -- launches shadow bolts which spawn dark kobolds upon impact.
local x,y = 0,0
utils.timedrepeat(0.33, math.random(3,5), function(c)
x,y = utils.projectxy(utils.unitx(bossobj.trackedunit), utils.unity(bossobj.trackedunit), math.random(35,135), math.random(0,360))
local mis = missile:create_arc(x, y, bossobj.unit, mis_voidball.effect, boss:get_dmg(6))
mis.time = 2.0
mis.radius = 70
mis.height = 175
mis:updatescale(1.4)
mis:initduration()
mis.explfunc= function()
if utils.isalive(bossobj.unit) then
orb_prolif_stack[5]:play(mis.x, mis.y)
cr_dkoboldmel:create(mis.x, mis.y)
end
end
mis.dmgtype = dmg.type.stack[dmg_shadow]
end)
end,
},
power = {
[1] = function(bossobj) -- marks the ground and begins launching a rapid volley of yellow lightning, tracking the player for the duration.
local vel,dur = 11.5, 12.0
local x1,y1 = utils.unitxy(bossobj.trackedunit)
local a,x2,y2 = 0, utils.unitxy(bossobj.trackedunit)
local e = speff_bossmark:play(x2, y2, dur)
utils.timed(1.5, function()
utils.timedrepeat(0.10, dur/0.10, function(c)
if utils.isalive(bossobj.unit) then
-- move center point and marker effect:
a = utils.anglexy(x1, y1, utils.unitxy(bossobj.trackedunit))
x1,y1 = utils.projectxy(x1, y1, vel, a)
utils.seteffectxy(e, x1, y1)
-- create missile:
x2,y2 = utils.projectxy(x1, y1, math.random(33,120), c*(math.random(12,18)))
local mis = missile:create_arc(x2, y2, bossobj.unit, speff_psiyel.effect, boss:get_dmg(18))
boss:marker(x2, y2)
mis.time = 1.84
mis.radius = 80
mis.height = 425
mis.dmgtype = dmg.type.stack[dmg_fire]
mis:initduration()
if math.fmod(c, 14) == 0 then -- run animation, set facing.
mis_boltyell:play(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), nil, 1.5)
end
-- move missile center toward player slowly:
else
DestroyEffect(e)
ReleaseTimer()
end
end)
end)
end
},
channel = {
[1] = function(bossobj) -- becomes immune and summons 3 different manifestations - yellow, black, red (excess, ego, envy), each launching different damage type missiles.
-- init boss animation and pause:
if utils.distxy(0, 0, utils.unitxy(bossobj.unit)) > 1725 then -- move closer to center if near edge of arena.
utils.setxy(bossobj.unit, utils.unitprojectxy(bossobj.unit, 768, utils.anglexy(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), 0, 0)))
port_yellowtp:play(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), nil, 1.65)
end
bossobj.iscasting = true
PauseUnit(bossobj.unit, true)
utils.setinvul(bossobj.unit, true)
SetUnitAnimationByIndex(bossobj.unit, 14)
SetUnitVertexColor(bossobj.unit, 175, 175, 175, 175)
bondeff = speff_bondgold:attachu(bossobj.unit, nil, 'chest', 1.5)
bossobj.slainmanifs = 0
-- missile launch function from each manifestation:
if manifestation_init then manifestation_init = nil end -- destroy previous handles.
manifestation_init = function(bossobj, u, miseffect, landeffect)
local u = u
local waves = 2
if map.manager.diffid == 5 then waves = waves + 1 end
utils.timedrepeat(3.81, nil, function()
if u and utils.isalive(u) and utils.isalive(bossobj.unit) then
landeffect:playu(u)
local x2,y2,starta = 0,0,math.random(0,360)
for b = 1,waves do
for i = 1,7 do
x2,y2 = utils.unitprojectxy(u, b*305, starta + i*51.5)
local mis = missile:create_arc(x2, y2, u, miseffect.effect, boss:get_dmg(24), function(m) landeffect:play(m.x, m.y) end)
mis.time = 2.33
mis.explr = 115.0
mis:initduration()
mis.dmgtype = dmg.type.stack[dmg_nature]
end
end
else
if utils.isalive(bossobj.unit) then
bossobj.slainmanifs = bossobj.slainmanifs + 1
if bossobj.slainmanifs >= 3 then -- are all 3 manifestations dead?
-- if so, queue effect, delay reactivation:
bossobj.slainmanifs = nil
speff_heavgate:play(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), 7.0)
utils.setinvul(bossobj.unit, false)
SetUnitVertexColor(bossobj.unit, 255, 0, 0, 255)
SetUnitAnimationByIndex(bossobj.unit, 18)
ArcingTextTag(color:wrap(color.tooltip.bad, "Exhausted!"), bossobj.unit, 3.0)
utils.timed(10.0, function()
speff_resurrect:play(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), nil, 2.0)
ResetUnitAnimation(bossobj.unit)
bossobj.iscasting = false
PauseUnit(bossobj.unit, false)
SetUnitVertexColor(bossobj.unit, 255, 255, 255, 255)
if bondeff then DestroyEffect(bondeff) end
end)
end
else
if bondeff then DestroyEffect(bondeff) end
end
ReleaseTimer()
end
end)
end
-- launch dark kobolds from the boss position while manifestations are active:
utils.timedrepeat(1.0, nil, function(c)
if bossobj.slainmanifs and bossobj.slainmanifs < 3 and utils.isalive(bossobj.unit) then
local x2,y2 = utils.unitprojectxy(bossobj.unit, math.random(100,400), math.random(0,360))
local mis = missile:create_arc(x2, y2, bossobj.unit, mis_voidball.effect, boss:get_dmg(6))
mis.time = 2.21
mis.radius = 70
mis.height = 175
mis.arch = 700
mis:updatescale(1.3)
mis:initduration()
mis.explfunc= function()
if utils.isalive(bossobj.unit) then
orb_prolif_stack[5]:play(mis.x, mis.y)
cr_dkoboldmel:create(mis.x, mis.y)
end
end
mis.dmgtype = dmg.type.stack[dmg_shadow]
else
ReleaseTimer()
end
end)
utils.timedrepeat(0.78, 3, function(c)
local c = c
local manifmaxlife = 0.125 -- %% of boss HP
if map.manager.diffid == 5 then manifmaxlife = 0.1 end
local x,y = utils.unitprojectxy(bossobj.unit, 612, c*120)
speff_heavgate:play(x, y, 0.78)
speff_judge:play(x, y)
utils.timed(0.78, function()
if c == 0 then
-- excess
bj_lastCreatedUnit = utils.unitatxy(Player(24), x, y, 'N01W', utils.anglexy(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), x, y))
manifestation_init(bossobj, bj_lastCreatedUnit, mis_grenadegrn, speff_grnburst)
speff_stormnat:play(x, y)
elseif c == 1 then
-- envy
bj_lastCreatedUnit = utils.unitatxy(Player(24), x, y, 'N01X', utils.anglexy(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), x, y))
manifestation_init(bossobj, bj_lastCreatedUnit, mis_grenadeblue, speff_iceburst2)
speff_stormfrost:play(x, y)
elseif c == 2 then
-- ego
bj_lastCreatedUnit = utils.unitatxy(Player(24), x, y, 'N01Y', utils.anglexy(utils.unitx(bossobj.unit), utils.unity(bossobj.unit), x, y))
manifestation_init(bossobj, bj_lastCreatedUnit, mis_grenadeoj, speff_redburst)
speff_stormfire:play(x, y)
end
utils.setnewunithp(bj_lastCreatedUnit, utils.maxlife(bossobj.unit)*manifmaxlife, true)
SetUnitAnimationByIndex(bj_lastCreatedUnit, 22)
PauseUnit(bj_lastCreatedUnit, true)
end)
end)
end
},
ultimate = {
[1] = function(bossobj) -- creates a line of chests facing a direction with a gap. every few sec, they launch deadly gold coins that explode.
local centerx, centery = utils.unitprojectxy(bossobj.unit, 2200, math.random(0,360))
local originx, originy = centerx, centery
local spacing = 156
local spacingt = 0
local a = utils.anglexy(centerx, centery, utils.unitxy(bossobj.trackedunit))
local origina = a -- cache default shooting direction of chests.
-- place rows of chests:
local row = {}
local num = 1
row[1], row[2] = {}, {}
a = a - 90
for i = 1,32 do
if i == 12 then
centerx, centery = originx, originy -- reset to center, then reverse angle.
a = a + 180 -- swap to +90
num = 2 -- change to row 2.
spacingt = -spacing -- reset starting projection distance, skip first by setting negative so no mid gap.
end
spacingt = spacingt + spacing
row[num][i] = {}
row[num][i].x, row[num][i].y = utils.projectxy(centerx, centery, spacingt, a)
row[num][i].e = speff_goldchest:create(row[num][i].x, row[num][i].y)
BlzSetSpecialEffectScale(row[num][i].e, 1.66)
BlzSetSpecialEffectYaw(row[num][i].e, origina*bj_DEGTORAD)
port_yellowtp:play(row[num][i].x, row[num][i].y)
end
-- missile timer:
utils.timed(3.0, function()
utils.timedrepeat(2.54, 12, function()
if map.manager.activemap then
for rownum,rowt in pairs(row) do
for i,t in pairs(rowt) do
if t.e then -- chest exists, shoot missile.
mis_boltyell:play(t.x, t.y)
local mis = missile:create_angle(origina, 300.0, bossobj.unit, speff_coin.effect, boss:get_dmg(24))
mis.x,mis.y = t.x, t.y -- move away from unit origin point
mis.time = 16.0
mis.vel = math.random(3, 14)
mis.radius = 80.0
mis.dmgtype = dmg.type.stack[dmg_fire]
mis.colfunc = function(m) speff_edict:play(m.x, m.y) end
mis.func = function(m) if not map.manager.activemap then m:destroy() end end
mis:updatescale(1.5)
mis:initduration()
end
end
end
end
end, function() -- cleanup after timer expires.
for rownum,rowt in pairs(row) do
for i,t in pairs(rowt) do
mis_boltyell:play(t.x, t.y)
if t.e then DestroyEffect(t.e) end
end
end
utils.deep_destroy(row)
end)
end)
end
}
}
end
buffy = {}
function buffy:buildbuffs()
-- order id list: https://github.com/nestharus/JASS/blob/master/jass/Systems/OrderIds/script.j
bf_slow = buffy:new('A01S', 'Bslo', 852075, false, true)
bf_slow.name = 'Slowed'
bf_slow.icon = 'ReplaceableTextures\\CommandButtons\\BTNSlow.blp'
bf_slow.descript = 'Reduced movespeed'
bf_ms = buffy:new('A01T', 'B000', 852101, false, false) -- NOTE: these use empty buff effects
bf_ms.name = 'Sprint'
bf_ms.icon = 'ReplaceableTextures\\CommandButtons\\BTNBoots.blp'
bf_ms.descript = 'Increased movespeed'
bf_msmax = buffy:new('A03C', 'B000', 852101, false, false) -- NOTE: these use empty buff effects
bf_msmax.name = 'Max Sprint'
bf_msmax.icon = 'ReplaceableTextures\\CommandButtons\\BTNBootsOfSpeed.blp'
bf_msmax.descript = 'Movespeed increased to maximum'
bf_armor = buffy:new('A01R', 'Binf', 852066, false, false)
bf_armor.name = 'Armored'
bf_armor.icon = 'ReplaceableTextures\\CommandButtons\\BTNHumanArmorUpOne.blp'
bf_armor.descript = 'Increased physical resistance'
bf_invis = buffy:new('A01Q', 'Binv', 852069, false, false)
bf_invis.name = 'Invisible'
bf_invis.icon = 'ReplaceableTextures\\CommandButtons\\BTNInvisibility.blp'
bf_invis.descript = 'Enemies cannot detect you'
bf_stun = buffy:new('A01U', 'BPSE', 852095, false, false)
bf_stun.name = 'Stunned'
bf_stun.icon = 'ReplaceableTextures\\CommandButtons\\BTNStun.blp'
bf_stun.descript = 'Unable to take action'
bf_freeze = buffy:new('A01J', 'B001', 852171, false, true)
bf_freeze.name = 'Frozen'
bf_freeze.icon = 'ReplaceableTextures\\CommandButtons\\BTNFreezingBreath.blp'
bf_freeze.descript = 'Unable to move or attack'
bf_root = buffy:new('A01X', 'B002', 852171, false, true)
bf_root.name = 'Rooted'
bf_root.icon = 'ReplaceableTextures\\CommandButtons\\BTNEntanglingRoots.blp'
bf_root.descript = 'Unable to move'
bf_silence = buffy:new('A01W', 'B000', 852668, false, false) -- NOTE: these use empty buff effects
bf_silence.name = 'Silenced'
bf_silence.icon = 'ReplaceableTextures\\CommandButtons\\BTNSilence.blp'
bf_silence.descript = 'Cannot cast spells'
-- AoE:
bf_a_atk = buffy:new('A01Y', 'BNht', 852588, true)
bf_a_atk.name = 'Hasty Hands'
bf_a_atk.icon = 'ReplaceableTextures\\CommandButtons\\BTNGlove.blp'
bf_a_atk.descript = 'Increased mining speed'
end
function buffy:preload()
local tar = CreateUnit(Player(PLAYER_NEUTRAL_AGGRESSIVE), FourCC("hfoo"), 18000, 17000, 270)
local debugtar
if self.debug then debugtar = CreateUnit(Player(0), FourCC("hfoo"), 18000, 17000, 270) end
for i = 1,#self.stack do
if not self.stack[i].aoe then
self.stack[i]:apply(tar, 1.0)
else
self.stack[i]:applyaoe(tar, 1.0)
end
if self.debug then self.stack[i]:apply(debugtar, i+1.0) end
end
self.preloading = nil
RemoveUnit(tar)
end
-- NOTE: currently has zero multiplayer support.
function buffy:buildpanel()
self.fr = kui.frame:newbytype("BACKDROP", kui.canvas.game.skill)
self.fr:setbgtex(kui.tex.invis)
self.fr:setfp(fp.bl, fp.b, kui.worldui, -kui:px(368), kui:px(185))
self.fr:setsize(kui:px(284), kui:px(25))
self.fr.stack = {}
local startx, starty, xoff, pad = -368, 185, 0, 4
for buffid = 1,20 do
if buffid == 11 then
startx, starty, xoff = -368, 185 + 25 + 4, 0
end
self.fr.stack[buffid] = kui.frame:newbtntemplate(kui.canvas.game.skill, kui.tex.invis)
self.fr.stack[buffid]:setfp(fp.bl, fp.b, kui.worldui, kui:px(startx + xoff), kui:px(starty))
self.fr.stack[buffid]:setsize(kui:px(25), kui:px(25))
self.fr.stack[buffid].btn:assigntooltip("buff")
self.fr.stack[buffid].btn.hoverarg = buffid
self.fr.stack[buffid]:hide()
xoff = xoff + 25 + pad
end
end
function buffy:add_indicator(p, name, icon, dur, descript, _effect, _attachpoint)
-- find first open slot:
self:start_indicator_timer()
if _effect then _effect:attachu(kobold.player[p].unit, dur, _attachpoint) end
if utils.islocalp(p) then
if not self:check_for_duplicate(name, dur) then
for buffid = 1,20 do
if not self.fr.stack[buffid].data then
self.fr.stack[buffid]:setbgtex(icon)
self.fr.stack[buffid].data = {}
self.fr.stack[buffid].data.descript = descript
self.fr.stack[buffid].data.name = name
self.fr.stack[buffid].data.dur = dur
self.fr.stack[buffid].data.icon = icon
self.fr.stack[buffid].data.elapsed = 0
self.fr.stack[buffid]:show()
self.fr.stack[buffid]:setalpha(255)
break
end
end
end
end
end
function buffy:check_for_duplicate(name, dur)
for buffid = 1,20 do
if self.fr.stack[buffid].data and self.fr.stack[buffid].data.name == name then
self.fr.stack[buffid].data.dur = dur
self.fr.stack[buffid].data.elapsed = 0
return true
end
end
return false
end
function buffy:start_indicator_timer()
buffy_dur_check = 0
-- init timer:
if not self.tmr then
self.tmr = NewTimer()
TimerStart(self.tmr, 0.21, true, function()
utils.debugfunc(function()
buffy_total_check = 20
for buffid = 1,20 do
if self.fr.stack[buffid].data then
self.fr.stack[buffid].data.elapsed = self.fr.stack[buffid].data.elapsed + 0.21
-- check for depletion:
if self.fr.stack[buffid].data.elapsed >= self.fr.stack[buffid].data.dur then
self.fr.stack[buffid].data = nil
-- sort empty slots to beginning:
for currentid = buffid,20 do
if self.fr.stack[currentid + 1] and self.fr.stack[currentid + 1].data then
self.fr.stack[currentid].data = self.fr.stack[currentid + 1].data
self.fr.stack[currentid + 1].data = nil
end
end
end
end
end
-- filter icons to correct positions:
for buffid = 1,20 do
if self.fr.stack[buffid].data then
-- decorate transparency for "almost over" indication:
buffy_dur_check = self.fr.stack[buffid].data.elapsed/self.fr.stack[buffid].data.dur
if buffy_dur_check > 0.8 then
self.fr.stack[buffid]:setalpha(90)
elseif buffy_dur_check > 0.65 then
self.fr.stack[buffid]:setalpha(150)
elseif buffy_dur_check > 0.5 then
self.fr.stack[buffid]:setalpha(200)
else
self.fr.stack[buffid]:setalpha(255)
end
-- re-show if hidden:
self.fr.stack[buffid]:setbgtex(self.fr.stack[buffid].data.icon)
self.fr.stack[buffid]:show()
else
self.fr.stack[buffid]:setbgtex(kui.tex.invis)
self.fr.stack[buffid]:hide()
buffy_total_check = buffy_total_check - 1
end
end
if buffy_total_check <= 0 then ReleaseTimer() self.tmr = nil end
end, "buffy timer")
end)
end
end
function buffy:init()
utils.debugfunc(function()
self.__index = self
self.unit = {}
self.stack = {}
self.dummyid = FourCC("h008")
self.isbuff = true -- NOTE: not currently in use, but differentiates positive/negative for targeting purposes.
self.debug = false
self.playeff = false -- play a negative eye candy effect on application (targeted only).
self.eff = speff_debuffed
self.deflvl = 0 -- cast this ability level by default
self.dummy = CreateUnit(Player(PLAYER_NEUTRAL_AGGRESSIVE), self.dummyid, 19000, 18000, 270)
self.pdummy = {} -- for dummy abilities that have casting time, cast per player.
self.pdummy[25] = self.dummy -- in case gen_ suite is used with neutral hostile.
self:buildbuffs()
self.preloading = true
self:preload()
end)
end
function buffy:new(rawcode, buffcode, orderid, isaoe, playeffect)
local o = setmetatable({}, self)
o.code = FourCC(rawcode)
o.buffcode = FourCC(buffcode)
o.orderid = orderid
o.playeff = playeffect or false
o.aoe = isaoe or false
self.stack[#self.stack+1] = o
return o
end
function buffy:verify_dummy()
if not self.dummy or not utils.isalive(self.dummy) then
self.dummy = CreateUnit(Player(PLAYER_NEUTRAL_AGGRESSIVE), self.dummyid, 19000, 18000, 270)
end
end
function buffy:new_absorb_indicator(p, name, icon, amount, dur, _descript)
buffy:add_indicator(p, name.." Shield", icon, dur, _descript or "Absorbs up to "..tostring(amount).." damage")
end
function buffy:new_p_stat_indicator(p, p_stat, dur)
if loot.statmod.pstatstack[p_stat] then
local statname, icon = "", nil
if p_stat == p_epic_hit_crit then
statname = "Critical Hit Chance"
icon = "ReplaceableTextures\\CommandButtons\\BTNCriticalStrike.blp"
elseif p_stat == p_epic_demon then
statname = "Chance to Summon Demon"
icon = "ReplaceableTextures\\CommandButtons\\BTNRevenant.blp"
elseif loot.statmod.pstatstack[p_stat].buffname then
statname = loot.statmod.pstatstack[p_stat].buffname
icon = loot.statmod.pstatstack[p_stat].icon
else
statname = loot.statmod.pstatstack[p_stat].name
icon = loot.statmod.pstatstack[p_stat].icon
end
buffy:add_indicator(p, "+"..statname, icon or "ReplaceableTextures\\CommandButtons\\BTNSelectHeroOn.blp",
dur, "Temporarily increased "..statname)
end
end
function buffy:new_buff_indicator(p, dur)
buffy:add_indicator(p, self.name, self.icon, dur, self.descript)
end
-- apply a buff to a @unit for @dur sec.
function buffy:apply(unit, dur, _preventstacking, _ignorebuff)
utils.debugfunc(function()
self:verify_dummy()
if _preventstacking and UnitHasBuffBJ(unit, self.buffcode) then
return
else
self:updatesettings(unit, dur)
self:cast(unit)
if self.playeff then self.eff:play(utils.unitxy(unit)) end
UnitRemoveAbility(self.dummy, self.code)
if not _ignorebuff and not self.preloading and utils.pnum(utils.powner(unit)) <= kk.maxplayers then
self:new_buff_indicator(utils.powner(unit), dur)
end
end
end)
end
-- cast an area-effect buff from @unit's location for @dur sec.
-- use @ownerunit to decide the spell's targets (ally or enemy).
function buffy:applyaoe(ownerunit, dur, _diameter)
utils.debugfunc(function()
self:verify_dummy()
SetUnitOwner(self.dummy, utils.powner(ownerunit), false)
self:updatesettings(ownerunit, dur, _diameter)
self:castaoe()
UnitRemoveAbility(self.dummy, self.code)
SetUnitOwner(self.dummy, Player(PLAYER_NEUTRAL_AGGRESSIVE), false)
end)
end
function buffy:updatesettings(unit, dur, _diameter)
utils.setxy(self.dummy, utils.unitxy(unit))
UnitAddAbility(self.dummy, self.code)
local abil = BlzGetUnitAbility(self.dummy, self.code)
local dur = dur or 3.0
-- 0-based:
if BlzGetAbilityRealLevelField(abil, ABILITY_RLF_DURATION_NORMAL, self.deflvl) then
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_DURATION_NORMAL, self.deflvl, dur)
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_DURATION_HERO, self.deflvl, dur)
else
if self.debug then print("error: no real field for "..tostring(abil).." at lvl "..self.deflvl) end
end
if _diameter then
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_AREA_OF_EFFECT, self.deflvl, _diameter or 500.0)
end
end
function buffy:cast(unit)
IssueTargetOrderById(self.dummy, self.orderid, unit)
end
function buffy:castaoe()
if self.debug then
print(tostring(IssueImmediateOrderById(self.dummy, self.orderid)))
else
IssueImmediateOrderById(self.dummy, self.orderid)
end
end
function buffy:gen_cast_instant(unit, abilcode, orderstr)
local pnum, bool = utils.pnum(utils.powner(unit)), false
self:gen_init(abilcode, unit, orderstr, pnum)
bool = IssueImmediateOrder(self.pdummy[pnum], orderstr)
self:gen_cleanup(abilcode, pnum)
return bool
end
function buffy:gen_cast_point(unit, abilcode, orderstr, x, y)
local pnum, bool = utils.pnum(utils.powner(unit)), false
self:gen_init(abilcode, unit, orderstr, pnum)
SetUnitFacing(self.pdummy[pnum], utils.anglexy(utils.unitx(self.pdummy[pnum]), utils.unity(self.pdummy[pnum]), x, y))
bool = IssuePointOrder(self.pdummy[pnum], orderstr, x, y)
self:gen_cleanup(abilcode, pnum)
return bool
end
function buffy:gen_cast_unit(unit, abilcode, orderstr, target)
local pnum, bool = utils.pnum(utils.powner(unit)), false
self:gen_init(abilcode, unit, orderstr, pnum)
SetUnitFacing(self.pdummy[pnum], utils.anglexy(utils.unitx(self.pdummy[pnum]), utils.unity(self.pdummy[pnum]), utils.unitxy(target)))
bool = IssueTargetOrder(self.pdummy[pnum], orderstr, target)
self:gen_cleanup(abilcode, pnum)
return bool
end
function buffy:create_dummy(p, x, y, id, dur)
local u = utils.unitatxy(p, x, y, id, 270.0)
UnitApplyTimedLife(u, FourCC('BTLF'), dur)
return u
end
function buffy:gen_init(abilcode, unit, orderstr, pnum)
if not self.pdummy[pnum] then self.pdummy[pnum] = CreateUnit(Player(pnum-1), self.dummyid, 19000, 18000, 270) end
utils.setxy(self.pdummy[pnum], utils.unitxy(unit))
UnitAddAbility(self.pdummy[pnum], abilcode)
end
function buffy:gen_cleanup(abilcode, pnum)
UnitRemoveAbility(self.pdummy[pnum], abilcode)
end
function buffy:gen_reset(unit, abilcode)
local unit, abilcode = unit, abilcode
utils.palert(utils.powner(unit), "Ability failed to cast (invalid target)!", 0.25)
utils.timed(0.21, function() BlzEndUnitAbilityCooldown(unit, abilcode) end)
utils.addmanaval(unit, BlzGetAbilityIntegerLevelField(BlzGetUnitAbility(unit, abilcode), ABILITY_ILF_MANA_COST, 0), false)
end
candle = {}
candle.__index = candle
candle.thresh = { -- at what percent candle light does a wave and commander spawn?
[1] = 0.60,
[2] = 0.30,
[3] = 0.0,
}
candle.size = { -- how many creatures to spawn with the darkness elite?
[1] = 3,
[2] = 5,
[3] = 7,
}
candle.scale = { -- how much more stronger are the creatures spawned?
[1] = 1.00,
[2] = 1.15,
[3] = 1.30,
}
candle.cadence = 1.25 -- how fast candle light burns (1 per cadence in sec).
candle.despawn = 21.0 -- sec until wax canisters despawn.
candle.precalc = {} -- pre-calculated wave breakpoints (performance).
candle.spawned = {} -- store bool if the wave was already triggered.
candle.canister = {} -- collectible wax.
candle.canister[FourCC('I00A')] = 5
candle.canister[FourCC('I000')] = 10
candle.canister[FourCC('I001')] = 15
candle.canister[FourCC('I002')] = 30
function candle:init()
local o = {}
setmetatable(o, self)
if not self.trig then
self.trig = CreateTrigger()
utils.playerloop(function(p) TriggerRegisterPlayerUnitEventSimple(self.trig, p, EVENT_PLAYER_UNIT_PICKUP_ITEM) end)
TriggerAddAction(self.trig, function()
utils.debugfunc(function() candle:collectwax() map.block:collectore() loot:collectloot() shrine:collectfragment() end, "collect wax or ore")
end)
end
return o
end
function candle:load()
for i = 1,3 do self.spawned[i] = false end
self.default = 180 -- default wax total (1 pt == 1 x cadence).
self.bossb = 60 -- bonus wax added for boss fights.
self.current = 0 -- the current candle light value.
self.total = self.default
self.pobj = kobold.player[Player(0)] -- NOTE: how to fix in multiplayer?
-- only enable candle for non-boss fights:
self:setcapacity()
self.current = self.total -- do before ui.
self:updateui()
if self.exhaustmr then ReleaseTimer(self.exhaustmr) end
-- figure out what light levels trigger what waves:
for waveid = 1,3 do
self.precalc[waveid] = math.floor(self.thresh[waveid] * self.total)
self.spawned[waveid] = false
utils.looplocalp(function(p) BlzFrameSetVertexColor(map.manager.candlewave[waveid].fh, BlzConvertColor(255, 255, 255, 255)) end)
end
end
function candle:updateui()
utils.looplocalp(function()
BlzFrameSetText(map.manager.candletxt, self.current.."/"..self.total)
BlzFrameSetValue(map.manager.candlebar.fh, math.floor(100*self.current/self.total))
end)
end
function candle:add(val, _u)
-- add to a player's current wax.
self.current = self.current + val
if self.current > self.total then self.current = self.total end
self:updateui()
if _u then
ArcingTextTag(color:wrap(color.ui.wax, "+"..tostring(val).." Wax"), _u)
end
end
function candle:burn(val)
-- reduce a player's candle capacity per 1 sec.
if self.current > 0 then
self.current = math.floor(self.current - val)
else
self.current = 0
end
self:updateui()
self:check()
end
function candle:setcapacity()
if map.mission.cache.id == m_type_boss_fight then
self.total = math.floor(self.default*(1 + self.pobj[p_stat_wax]/100)) + self.bossb -- TODO: how to merge in multiplayer?
else
self.total = math.floor(self.default*(1 + self.pobj[p_stat_wax]/100))
end
end
function candle:check()
utils.debugfunc(function()
for waveid = 1,3 do
if not self.spawned[waveid] and self.current <= self.precalc[waveid] then
self.spawned[waveid] = true
if self.pobj:islocal() then
BlzFrameSetVertexColor(map.manager.candlewave[waveid].fh, BlzConvertColor(100, 255, 255, 255))
end
if map.mission.cache and map.mission.cache.id == m_type_boss_fight and map.mission.cache.boss then
alert:new(color:wrap(color.tooltip.alert, "The Darkness consumes your foe..."), 3.33)
map.mission.cache.boss:assign_darkness_spell(waveid)
else
self:spawnwave(waveid)
end
end
end
end, "spawnwave")
end
function candle:spawnwave(waveid)
-- create a cluster of darkness minions under wave 3:
if waveid < 3 then
local biomeid = map.manager.biome.id
local px,py = utils.unitxy(self.pobj.unit)
local x,y = utils.projectxy(px, py, 1200, utils.anglexy(px, py, 0, 1000) + math.randomneg(-3,3))
local rect = Rect(0,0,0,0)
-- darkness minions:
local units = creature.cluster.biome[biomeid][math.random(1, #creature.cluster.biome[biomeid])]:spawn(x, y, self.size[waveid], self.size[waveid]+2)
for _,unit in ipairs(units) do self:updateunit(unit, waveid) end
local grp = g:newbytable(units)
grp.temp = true
-- darkness elite:
local elite = creature.darkt[math.random(1, #creature.darkt)]:create(x, y)
grp:add(elite)
self:updateunit(elite, waveid)
UnitAddAbility(elite, FourCC('A03D')) -- aura effect
-- run effect and clear blocks:
utils.timedrepeat(0.21, 7, function(c)
x,y = utils.projectxy(x, y, c*121, utils.anglexy(x, y, px, py))
mis_voidballhge:playradius(300, 3, x, y)
utils.setrectradius(rect, x, y, 500)
utils.dmgdestinrect(rect, 99999)
end, function() RemoveRect(rect) end)
alert:new(color:wrap(color.tooltip.alert, "Your light is fading... minions of The Darkness approach..."), 3.33)
utils.timedrepeat(6.0, nil, function()
utils.debugfunc(function()
grp:verifyalive()
if grp and grp:getsize() > 0 then
if map.manager.activemap then
grp:attackmove(utils.unitxy(self.pobj.unit))
else
grp:completepurge()
end
else
ReleaseTimer()
end
end, "timer")
end)
-- spawn The Darkness for wave 3:
elseif waveid == 3 then
self:spawndarkness()
end
return elite, grp
end
function candle:spawndarkness()
-- if a player's candle light hit 0, begin sending recurring waves that get stronger.
utils.playsoundall(kui.sound.darkwarning)
alert:new(color:wrap(color.tooltip.alert, "Strange sounds can be heard... The Darkness approaches..."), 3.33)
utils.timed(2.75, function()
if map.manager.downp < map.manager.totalp then -- make sure players didn't die during the delay.
local rect = Rect(0,0,0,0)
local x,y = utils.unitxy(self.pobj.unit)
x,y = utils.projectxy(x, y, 1000, utils.anglexy(x, y, 0, 1000) + math.randomneg(-3,3))
self.darkness = utils.unitatxy(Player(24), x, y, 'n01O', math.random(0,360))
UnitAddAbility(self.darkness, FourCC('A03D')) -- aura effect
SetUnitPathing(self.darkness, false)
utils.playerloop(function(p) UnitShareVisionBJ(true, self.darkness, p) end)
-- spawn in effect:
local x2,y2 = 0,0
utils.timedrepeat(0.21, 12, function()
if map.manager.activemap and self.darkness and rect then
x2,y2 = utils.projectxy(utils.unitx(self.darkness), utils.unity(self.darkness), math.random(300,900), math.random(0,360))
utils.setrectradius(rect, x2, y2, 400)
utils.dmgdestinrect(rect, 1000)
mis_voidballhge:playradius(600, 5, x, y)
end
end)
-- constant attack move timer:
self.exhaustmr = utils.timedrepeat(2.75, nil, function()
-- spawn recurring wave and increment strength (wave changes reset on map load).
if map.manager.activemap and self.darkness then
local x,y = utils.unitxy(self.darkness)
utils.setrectradius(rect, x, y, 400)
utils.dmgdestinrect(rect, 1000)
mis_voidballhge:playradius(600, 5, x, y)
utils.issatkxy(self.darkness, utils.unitxy(self.pobj.unit))
else
if self.darkness then RemoveUnit(self.darkness) end
RemoveRect(rect)
ReleaseTimer()
end
end)
end
end)
end
function candle:updateunit(unit, waveid)
SetUnitVertexColor(unit, 0, 0, 0, 255) -- make dark.
SetUnitMoveSpeed(unit, math.ceil( GetUnitDefaultMoveSpeed(unit)*0.75) ) -- slower units due to strength
BlzSetUnitMaxHP(unit, math.ceil( BlzGetUnitMaxHP(unit)*self.scale[waveid]) )
utils.scaleatk(unit, self.scale[waveid])
-- SetUnitPathing(unit, false)
UnitAddAbility(unit, FourCC('A009'))
utils.restorestate(unit)
utils.issatkxy(unit, utils.unitxy(self.pobj.unit))
dmg:convertdmgtype(unit, dmg_shadow)
end
function candle:collectwax()
local id = GetItemTypeId(GetManipulatedItem())
if candle.canister[id] and map.manager.activemap then
candle:add(candle.canister[id], utils.trigu())
end
end
function candle:spawnwax(x, y, _size)
local itm,inc
if _size and _size == "large" then
itm = CreateItem(FourCC('I002'), x, y)
elseif _size and _size == "medium" then
itm = CreateItem(FourCC('I001'), x, y)
elseif _size and _size == "tiny" then
itm = CreateItem(FourCC('I00A'), x, y)
else
itm = CreateItem(FourCC('I000'), x, y)
end
utils.timedrepeat(0.21, self.despawn/0.21, function(c)
inc = math.floor((255*(c/(self.despawn/0.21))))
-- BlzSetItemIntegerFieldBJ(itm, ITEM_IF_TINTING_COLOR_RED, 255)
BlzSetItemIntegerFieldBJ(itm, ITEM_IF_TINTING_COLOR_GREEN, 255 - inc)
BlzSetItemIntegerFieldBJ(itm, ITEM_IF_TINTING_COLOR_BLUE, 255 - inc)
end, function() if itm then RemoveItem(itm) end end)
end
do
color = {}
color.rarity = {
common = "5bff45",
rare = "4568ff",
epic = "c445ff",
ancient = "ff9c00",
}
color.tooltip = {
alert = "47c9ff",
good = "00f066",
bad = "ff3e3e",
}
color.txt = {
txtwhite = "ffffff",
txtgrey = "777777",
txtdisable = "aaaaaa",
}
color.dmg = {
arcane = "0b39ff",
frost = "00eaff",
nature = "00ff12",
fire = "ff9c00",
shadow = "7214ff",
physical = "ff2020",
}
color.dmgid = {
[1] = "0b39ff",
[2] = "00eaff",
[3] = "00ff12",
[4] = "ff9c00",
[5] = "7214ff",
[6] = "ff2020",
}
color.ui = {
hotkey = "47c9ff",
wax = "fdff6f",
candlelight = "fdff6f",
gold = "fff000",
xp = "d45bfb",
}
end
function color:test()
-- loop thru color table
for i,t in pairs(color) do
-- grab each child table's stored color and prepend WC3 hex code (always |cff, does nothing in-game but is needed)
if type(t) == "table" then
for colorname,colorvalue in pairs(t) do
print(colorvalue.."test|r")
end
end
end
end
function color:wrap(hex, str)
-- build an RGBA hex code.
return "|cff"..hex..str.."|r"
end
-- project name: {dest code to place, excavation dest to replace, facing angle, scale}
function initprojects()
constructiont = {}
placedconst = {}
-- shinykeeper shop upgrades:
constructiont['shiny1'] = {'B00B', udg_projects[1], 270.0, 1.0} -- hammer -- _0032
constructiont['shiny2'] = {'B00D', udg_projects[2], 23.0, 1.0} -- shinies, for dummies -- _0109
constructiont['shiny3'] = {'B00C', udg_projects[3], 270.0, 0.75} -- shiny super-forge -- _0110
constructiont['shiny4'] = {'B00G', udg_projects[4], 270.0, 0.75} -- scrapomatic -- _0029
-- elementalist shop upgrades:
constructiont['ele1'] = {'B00F', udg_projects[5], 166.0, 1.5} -- enchanted stash -- _0024
constructiont['ele2'] = {'B00E', udg_projects[6], 270.0, 0.75} -- infusion crystal 1 -- _0141
constructiont['ele3'] = {'B00E', udg_projects[7], 270.0, 0.75} -- `` 2 -- _0140
constructiont['ele4'] = {'B00E', udg_projects[8], 270.0, 0.75} -- `` 3 -- _0209
-- boss trophies:
constructiont['boss1'] = {'B00H', udg_projects[9], 270.0, 1.75} -- slag king -- _0214
constructiont['boss2'] = {'B00I', udg_projects[10], 270.0, 1.75} -- marsh mutant -- _0137
constructiont['boss3'] = {'B00J', udg_projects[11], 270.0, 1.75} -- megachomp -- _0215
constructiont['boss4'] = {'B00K', udg_projects[12], 270.0, 1.75} -- thawed experiment -- _0217
constructiont['boss5'] = {'B00L', udg_projects[13], 270.0, 1.00} -- ancient portal
-- greywhisker:
constructiont['grey1'] = {'B00M', udg_projects[14], 315.0, 0.8} -- greywhisker crafting station _0394
end
function placeproject(projectname)
utils.debugfunc(function()
local x, y = GetDestructableX(constructiont[projectname][2]), GetDestructableY(constructiont[projectname][2])
KillDestructable(constructiont[projectname][2])
placedconst[projectname] = CreateDestructable(FourCC(constructiont[projectname][1]), x, y, constructiont[projectname][3], constructiont[projectname][4], 1)
SetDestructableInvulnerable(placedconst[projectname], true)
end, 'placeproject')
end
function mergeancientrelics()
-- KillDestructable(placedconst['boss1'])
-- KillDestructable(placedconst['boss2'])
-- KillDestructable(placedconst['boss3'])
-- KillDestructable(placedconst['boss4'])
placeproject('boss5')
utils.playerloop(function(p) utils.shakecam(p, 1.75, 2.5) end)
utils.playsoundall(kui.sound.portalmerge)
end
function testprojects()
for name,t in pairs(constructiont) do
placeproject(name)
end
end
creature = {} -- creature metadata class.
creature.cluster = {} -- sub class for generating groupings of similar creatures + an elite.
creature.boss = {} -- store possible bosses by biomeid.
creature.cluster.biome = {} -- store possible clusters by biomeid.
function creature:init()
EnableCreepSleepBJ(false)
-- globals:
self.__index = self
self.basehp = 42 -- base hp value.
self.multhp = 1.0 -- hp multiplier based on creature strength.
self.baseatk = 5 -- `` atk value.
self.multatk = 1.0 -- ``
self.spelldmg = 36 -- ``
self.multspell = 1.0 -- ``
self.lvlmulthp = 0.04 -- multiplier based on map level.
self.lvlmultatk = 0.04 -- ``
self.pmulthp = 0.15 -- multiplier based on party size.
self.pmultatk = 0.12 -- ``
self.eliteratio = 0.08 -- how often elites spawn.
self.darkt = {} -- store darkness commanders.
creature.boss:init()
creature.cluster:init()
creature.cluster.biome:init()
-- enchantment ids:
-- TODO: use dmg and check for unit ability
self.enchant = { -- eye-candy effects to attach.
[1] = FourCC('A00F'), -- arcane
[2] = FourCC('A00D'), -- frost
[3] = FourCC('A00A'), -- nature
[4] = FourCC('A00B'), -- fire
[5] = FourCC('A00C'), -- shadow
[6] = FourCC('A00E'), -- physical
}
-- weak creatures:
cr_skele_orc = creature:new( 'n007', {multhp = 0.80, multatk = 0.85} )
cr_elefirelesr = creature:new( 'n00C', {multhp = 0.60, multatk = 0.60} )
cr_firerev = creature:new( 'n009', {multhp = 0.85, multatk = 0.70} )
cr_arachnathid = creature:new( 'n00K', {multhp = 0.60, multatk = 0.60} )
cr_gnoll = creature:new( 'n00O', {multhp = 0.85, multatk = 0.60} )
cr_spider = creature:new( 'n00S', {multhp = 0.70, multatk = 0.50} )
cr_shroomlocmag = creature:new( 'n011', {multhp = 0.85, multatk = 0.65} )
cr_ogreminion = creature:new( 'n01A', {multhp = 0.80, multatk = 0.60} )
cr_conslooter = creature:new( 'u001', {multhp = 0.85, multatk = 0.55} )
cr_dkoboldmel = creature:new( 'n01T', {multhp = 0.80, multatk = 0.80} )
-- normal creatures:
cr_rarachnathid = creature:new( 'n00X', {multhp = 0.80, multatk = 0.65} )
cr_gnollwarden = creature:new( 'n00Q', {multhp = 1.25, multatk = 0.60} )
cr_gnollnature = creature:new( 'n00P', {multhp = 1.00, multatk = 0.80} )
cr_skele_arch = creature:new( 'n003', {multhp = 0.90, multatk = 0.60} )
cr_lava_spawn = creature:new( 'n006', {multhp = 1.20, multatk = 0.40} )
cr_sludgefling = creature:new( 'n00A', {multhp = 1.10, multatk = 0.60} )
cr_marshfiend = creature:new( 'n000', {multhp = 1.00, multatk = 0.80} )
cr_geodegolem = creature:new( 'n00N', {multhp = 1.70, multatk = 0.80} )
cr_hydra = creature:new( 'n00U', {multhp = 1.60, multatk = 0.80} )
cr_lightnliz = creature:new( 'n00W', {multhp = 1.30, multatk = 0.60} )
cr_magmaliz = creature:new( 'n00Z', {multhp = 1.40, multatk = 0.70} )
cr_shroomloc = creature:new( 'n010', {multhp = 1.10, multatk = 0.70} )
cr_magnataur = creature:new( 'n017', {multhp = 1.60, multatk = 0.90} )
cr_wendigo = creature:new( 'n015', {multhp = 1.70, multatk = 0.80} )
cr_rollingt = creature:new( 'n01S', {multhp = 1.50, multatk = 0.90} )
cr_dkoboldrng = creature:new( 'n01U', {multhp = 1.20, multatk = 0.90} )
-- tough creatures:
cr_mirehydra = creature:new( 'n00T', {multhp = 2.80, multatk = 2.10} )
cr_gnollseer = creature:new( 'n00R', {multhp = 2.70, multatk = 2.10} )
cr_rockgolem = creature:new( 'n00M', {multhp = 2.80, multatk = 1.70} )
cr_elefiregrtr = creature:new( 'n00D', {multhp = 3.00, multatk = 2.25} )
cr_sludgemonst = creature:new( 'n00B', {multhp = 3.00, multatk = 2.25} )
cr_war_cons = creature:new( 'n008', {multhp = 3.30, multatk = 1.70} )
cr_magmalizbig = creature:new( 'n00L', {multhp = 3.10, multatk = 1.90} )
cr_stormliz = creature:new( 'n00V', {multhp = 2.70, multatk = 1.70} )
cr_broodmother = creature:new( 'n00Y', {multhp = 2.60, multatk = 2.30} )
cr_shroomlocbig = creature:new( 'n012', {multhp = 2.50, multatk = 2.10} )
cr_caveogre = creature:new( 'n019', {multhp = 2.80, multatk = 1.60} )
cr_magnataurbig = creature:new( 'n018', {multhp = 3.20, multatk = 1.60} )
cr_wendigobig = creature:new( 'n016', {multhp = 3.00, multatk = 2.00} )
cr_greedsymb = creature:new( 'u002', {multhp = 2.70, multatk = 2.20} )
-- shrine minions:
sh_dragbrood = creature:new( 'n028', {multhp = 1.50, multatk = 0.90} )
sh_dragfire = creature:new( 'n027', {multhp = 1.50, multatk = 0.90} )
sh_dragfrost = creature:new( 'n026', {multhp = 1.50, multatk = 0.90} )
sh_arcshard = creature:new( 'n029', {multhp = 1.85, multatk = 0.60} )
sh_fossloot = creature:new( 'n02B', {multhp = 1.85, multatk = 0.60} )
sh_arcana = creature:new( 'n02C', {multhp = 1.75, multatk = 0.75} )
-- boss minions:
bm_swampling = creature:new( 'n01G', {multhp = 1.70, multatk = 1.15} )
bm_livfossil = creature:new( 'n01I', {multhp = 1.70, multatk = 3.50} )
bm_discexperim = creature:new( 'n01K', {multhp = 1.60, multatk = 2.10} )
bm_darknessdrop = creature:new( 'n01P', {multhp = 1.15, multatk = 1.65} )
--[[
slag:
--]]
cl_slag_fire = creature.cluster:new(
{cr_elefirelesr},
{cr_lava_spawn},
{cr_elefiregrtr, cr_sludgemonst},
{biome_id_slag} )
cl_slag_shadow = creature.cluster:new(
{cr_firerev},
{cr_sludgefling},
{cr_sludgemonst},
{biome_id_slag} )
cl_slag_liz = creature.cluster:new(
nil,
{cr_magmaliz},
{cr_magmalizbig},
{biome_id_slag} )
cl_slag_liz.thresh = { weak = 0.0, normal = 0.4, tough = 0.2 }
--[[
fossile:
--]]
cl_skele = creature.cluster:new(
{cr_skele_orc},
{cr_skele_arch},
{cr_war_cons},
{biome_id_foss} )
cl_foss_gnoll = creature.cluster:new(
{cr_gnoll},
{cr_gnollwarden, cr_gnollnature},
{cr_gnollseer},
{biome_id_foss} )
cl_foss_golem = creature.cluster:new(
nil,
{cr_geodegolem},
{cr_rockgolem},
{biome_id_foss} )
cl_foss_golem.thresh = { weak = 0.0, normal = 0.4, tough = 0.2 }
cl_foss_liz = creature.cluster:new(
nil,
{cr_lightnliz},
{cr_stormliz},
{biome_id_foss} )
cl_foss_liz.thresh = { weak = 0.0, normal = 0.6, tough = 0.1 }
cl_foss_arach = creature.cluster:new(
{cr_arachnathid},
{cr_rarachnathid},
nil,
{biome_id_foss} )
cl_foss_arach.thresh = { weak = 0.4, normal = 0.4, tough = 0.0 }
--[[
mire:
--]]
cl_mire_hydra = creature.cluster:new(
nil,
{cr_hydra},
{cr_mirehydra},
{biome_id_mire} )
cl_mire_hydra.thresh = { weak = 0.0, normal = 0.4, tough = 0.2 }
cl_mire_spider = creature.cluster:new(
{cr_spider},
nil,
{cr_broodmother},
{biome_id_mire} )
cl_mire_spider.thresh = { weak = 0.9, normal = 0.0, tough = 0.1 }
cl_mire_loc = creature.cluster:new(
{cr_shroomlocmag},
{cr_shroomloc},
{cr_shroomlocbig},
{biome_id_mire} )
--[[
glacier:
--]]
cl_ice_wendigo = creature.cluster:new(
nil,
{cr_wendigo},
{cr_wendigobig},
{biome_id_ice} )
cl_ice_wendigo.thresh = { weak = 0.0, normal = 0.4, tough = 0.1 }
cl_ice_magna = creature.cluster:new(
nil,
{cr_magnataur},
{cr_magnataurbig},
{biome_id_ice} )
cl_ice_magna.thresh = { weak = 0.0, normal = 0.4, tough = 0.1 }
cl_ice_ogre = creature.cluster:new(
{cr_ogreminion},
nil,
{cr_caveogre},
{biome_id_ice} )
cl_ice_ogre.thresh = { weak = 0.9, normal = 0.0, tough = 0.1 }
--[[
vault:
--]]
cl_vault_treasure = creature.cluster:new(
{cr_conslooter},
{cr_rollingt},
{cr_greedsymb},
{biome_id_vault} )
cl_vault_treasure.thresh = { weak = 0.6, normal = 0.3, tough = 0.1 }
cl_vault_dkobold = creature.cluster:new(
{cr_dkoboldmel},
{cr_dkoboldrng},
nil,
{biome_id_vault} )
cl_vault_dkobold.thresh = { weak = 0.5, normal = 0.4, tough = 0.0 }
cl_shrine_arcana = creature.cluster:new(
nil,
{sh_arcana},
nil,
{biome_id_vault} )
cl_vault_dkobold.thresh = { weak = 0.0, normal = 0.7, tough = 0.0 }
--[[
boss objectives:
--]]
bs_hoard_drag = creature:new('n00E', {multhp = 6.15, multatk = 3.15})
bs_hoard_amalgam = creature:new('n013', {multhp = 5.75, multatk = 3.25})
bs_hoard_qngreed = creature:new('n014', {multhp = 5.75, multatk = 3.33})
bs_hoard_drag:addbosstype({biome_id_foss, biome_id_slag})
bs_hoard_amalgam:addbosstype({biome_id_mire, biome_id_slag, biome_id_ice, biome_id_vault})
bs_hoard_qngreed:addbosstype({biome_id_slag, biome_id_mire, biome_id_ice, biome_id_vault})
--[[
darkness wave elites:
--]]
drk_widow = creature:new( 'n00F', {multhp = 3.33, multatk = 3.00}, true )
drk_terror = creature:new( 'n00G', {multhp = 3.00, multatk = 3.50}, true )
drk_dragon = creature:new( 'n00H', {multhp = 3.15, multatk = 2.33}, true )
end
function creature.cluster.biome:init()
self[biome_id_foss] = {}
self[biome_id_slag] = {}
self[biome_id_mire] = {}
self[biome_id_ice] = {}
self[biome_id_vault] = {}
end
function creature.boss:init()
self[biome_id_foss] = {}
self[biome_id_slag] = {}
self[biome_id_mire] = {}
self[biome_id_ice] = {}
self[biome_id_vault] = {}
end
function creature:new(rawcode, statst, _darkness)
local o = {}
setmetatable(o, self)
o.code = FourCC(rawcode)
for stat,val in pairs(statst) do
o[stat] = val
end
if _darkness then
self.darkt[#self.darkt + 1] = o
end
return o
end
function creature:create(x, y)
local unit = CreateUnit(Player(PLAYER_NEUTRAL_AGGRESSIVE), self.code, x, y, 0)
IssuePointOrderById(unit, 851986, x, y) -- move in place to auto-shift the unit outside of blocks.
utils.stop(unit) -- stop order to stop pathfinding.
SetUnitFacing(unit, math.random(0,360))
BlzSetUnitIntegerField(unit, UNIT_IF_LEVEL, math.floor(map.mission.cache.level))
-- init from creature template:
BlzSetUnitMaxHP(unit, math.floor(self.basehp*self.multhp*(1 + (map.mission.cache.level^1.12)*self.lvlmulthp) ) )
BlzSetUnitBaseDamage(unit, math.floor(self.baseatk*self.multatk*(1 + (map.mission.cache.level^1.12)*self.lvlmultatk) ), 0)
-- modify based on difficulty:
if map.mission.cache and map.mission.setting then
BlzSetUnitMaxHP(unit, math.ceil( BlzGetUnitMaxHP(unit)*map.mission.cache.setting[m_stat_health]) )
BlzSetUnitBaseDamage(unit, math.ceil( BlzGetUnitBaseDamage(unit, 0)*map.mission.cache.setting[m_stat_attack]), 0 )
SetUnitMoveSpeed(unit, math.ceil( GetUnitDefaultMoveSpeed(unit)*map.mission.cache.setting[m_stat_ms]) )
BlzSetUnitArmor(unit, math.ceil( BlzGetUnitArmor(unit)+((map.mission.cache.setting[m_stat_enemy_res]-1)*100)) )
end
-- modify based on players present:
if map.manager.totalp > 1 then
BlzSetUnitMaxHP(unit, math.ceil( BlzGetUnitMaxHP(unit)*(1 + map.manager.totalp*self.pmulthp)) )
BlzSetUnitBaseDamage(unit, math.ceil( BlzGetUnitBaseDamage(unit, 0)*(1 + map.manager.totalp*self.pmultatk)), 0 )
end
utils.restorestate(unit)
return unit
end
function creature:addbosstype(biomeid)
if type(biomeid) == "table" then
for _,id in pairs(biomeid) do
creature.boss[id][#creature.boss[id]+1] = self
end
else
creature.boss[biomeid][#creature.boss[biomeid]+1] = self
end
self.boss = true
end
function creature.cluster:init()
self.__index = self
self.size = { min = 2, max = 4 } -- controls clustering size (min and max * thresh).
self.dist = { min = 128, max = 896 } -- controls randomized distance between spawns.
self.thresh = { weak = 0.7, normal = 0.2, tough = 0.1 } -- thresholds for creatures to spawn based on strength.
self.creatures = { weak = {}, normal = {}, tough = {} } -- store possible creatures to randomly spawn for this squad.
self.eliteratio = 0.33 -- chance for tough cluster creatures to roll an elite pack.
self.lvlscale = 0.01 -- cluster size multiplier per mission level.
end
function creature.cluster:new(weakt, normalt, tought, biomest)
-- add creatures to a new squad type. assign biomeids with {...}.
local o = setmetatable({}, self)
o.creatures = utils.deep_copy(self.creatures)
o.thresh = utils.deep_copy(self.thresh)
o.size = utils.deep_copy(self.size)
o.dist = utils.deep_copy(self.dist)
if weakt then
for _,cr in pairs(weakt) do
o.creatures.weak[#o.creatures.weak+1] = cr
end
end
if normalt then
for _,cr in pairs(normalt) do
o.creatures.normal[#o.creatures.normal+1] = cr
end
end
if tought then
for _,cr in pairs(tought) do
o.creatures.tough[#o.creatures.tough+1] = cr
end
end
for _,biomeid in pairs(biomest) do
creature.cluster.biome[biomeid][#creature.cluster.biome[biomeid]+1] = o
end
return o
end
function creature.cluster:spawn(centerx, centery, min, max, _spawneffect)
-- spawn a randomized cluster of creatures at this point.
-- strength randomizers:
local id, x, y, mn, mx = 1, 1, 1, min or self.size.min, max or self.size.max
local eliteratio = self.eliteratio
if map.manager.diffid == 5 then -- far more elites on tyrannical
eliteratio = eliteratio*2
end
local ut = {} -- return the unit table.
if map.mission.cache then -- merge density modifier.
mn,mx = mn*map.mission.cache.setting[m_stat_density],mx*map.mission.cache.setting[m_stat_density]
mn,mx = math.floor(mn*(1+map.mission.cache.level*self.lvlscale)), math.floor(mx*(1+map.mission.cache.level*self.lvlscale))
end
if #self.creatures.weak > 0 and self.thresh.weak > 0.0 then
local threshw = math.random(math.floor(mn*self.thresh.weak), math.ceil(mx*self.thresh.weak))
for i = 1,threshw do
id = math.random(1, #self.creatures.weak)
x,y = utils.projectxy(centerx, centery, math.random(self.dist.min, self.dist.max), math.random(0,360))
ut[#ut+1] = self.creatures.weak[id]:create(x, y)
if _spawneffect then _spawneffect:play(x,y) end
end
end
if #self.creatures.normal > 0 and self.thresh.normal > 0.0 then
local threshn = math.random(math.floor(mn*self.thresh.normal), math.ceil(mx*self.thresh.normal))
for i = 1,threshn do
id = math.random(1, #self.creatures.normal)
x,y = utils.projectxy(centerx, centery, math.random(self.dist.min, self.dist.max), math.random(0,360))
ut[#ut+1] = self.creatures.normal[id]:create(x, y)
if _spawneffect then _spawneffect:play(x,y) end
end
end
if #self.creatures.tough > 0 and self.thresh.tough > 0.0 then
local thresht = math.random(math.floor(mn*self.thresh.tough), math.ceil(mx*self.thresh.tough))
for i = 1,thresht do
id = math.random(1, #self.creatures.tough)
x,y = utils.projectxy(centerx, centery, math.random(self.dist.min, self.dist.max), math.random(0,360))
ut[#ut+1] = self.creatures.tough[id]:create(x, y)
if _spawneffect then _spawneffect:play(x,y) end
if map.mission.cache.level > 4 then -- elites only spawn at level 5+
if math.random(0,100) < eliteratio*100 then
elite:new(ut[#ut], math.random(1,6), self)
end
end
end
end
return ut
end
dmg = {}
dmg.event = {}
dmg.type = {}
dmg.absorb = {}
-- default constants mapping:
-- DEFENSE_TYPE_DIVINE ATTACK_TYPE_MAGIC DAMAGE_TYPE_MAGIC -- arcane dmgid: 1
-- DEFENSE_TYPE_NORMAL ATTACK_TYPE_MELEE DAMAGE_TYPE_COLD -- frost dmgid: 2 -- TYPE_MELEE = 'Normal'
-- DEFENSE_TYPE_LARGE ATTACK_TYPE_PIERCE DAMAGE_TYPE_PLANT -- nature dmgid: 3
-- DEFENSE_TYPE_LIGHT ATTACK_TYPE_CHAOS DAMAGE_TYPE_FIRE -- fire dmgid: 4
-- DEFENSE_TYPE_FORT ATTACK_TYPE_SIEGE DAMAGE_TYPE_DEATH -- shadow dmgid: 5
-- DEFENSE_TYPE_MEDIUM ATTACK_TYPE_HERO DAMAGE_TYPE_FORCE -- physical dmgid: 6
-- unused:
-- DEFENSE_TYPE_HERO ATTACK_TYPE_NORMAL
function dmg:init()
self.debug = false
--
self.trig = CreateTrigger()
self.history = {} -- store events
self.txtdur = 1.66 -- text duration
self.histmax = 3 -- how many?
self.buffer = 5 -- when do we start culling?
self.overage = self.histmax + self.buffer
self.passive = Player(PLAYER_NEUTRAL_PASSIVE)
self.cacheon = false -- cache damage values
self.pdmg = false -- flag for working with players on dealing damage.
self.phit = false -- `` on taking damage.
self.crit = false -- `` on crit damage.
self.thorns = false -- `` returned damage.
self.convert = false -- `` converted damage.
self.hexp = 0
self.hext = {
[0] = "|cffffffff", -- universal
[1] = "|cff0b39ff", -- arcane
[2] = "|cff00eaff", -- frost
[3] = "|cff00ff12", -- nature
[4] = "|cffff9c00", -- fire
[5] = "|cff7214ff", -- shadow
[6] = "|cffff2020", -- physical
}
dmg.event:init()
dmg.absorb:init()
-- initialize damage type lookup table:
dtypelookup = {
[DAMAGE_TYPE_MAGIC] = 1,
[DAMAGE_TYPE_FIRE] = 2,
[DAMAGE_TYPE_FORCE] = 3,
[DAMAGE_TYPE_PLANT] = 4,
[DAMAGE_TYPE_DEATH] = 5,
[DAMAGE_TYPE_COLD] = 6,
}
atypelookup = {
[ATTACK_TYPE_MAGIC] = 1,
[ATTACK_TYPE_MELEE] = 2,
[ATTACK_TYPE_PIERCE] = 3,
[ATTACK_TYPE_CHAOS] = 4,
[ATTACK_TYPE_SIEGE] = 5,
[ATTACK_TYPE_HERO] = 6,
}
deftypelookup = {
[DEFENSE_TYPE_DIVINE] = 1,
[DEFENSE_TYPE_NORMAL] = 2,
[DEFENSE_TYPE_LARGE] = 3,
[DEFENSE_TYPE_LIGHT] = 4,
[DEFENSE_TYPE_FORT] = 5,
[DEFENSE_TYPE_MEDIUM] = 6,
}
atypenamet = {
[ATTACK_TYPE_MAGIC] = "arcane",
[ATTACK_TYPE_MELEE] = "frost",
[ATTACK_TYPE_PIERCE] = "nature",
[ATTACK_TYPE_CHAOS] = "fire",
[ATTACK_TYPE_SIEGE] = "shadow",
[ATTACK_TYPE_HERO] = "physical",
}
-- create damage event listener:
TriggerRegisterAnyUnitEventBJ(self.trig, EVENT_PLAYER_UNIT_DAMAGING)
TriggerAddCondition(self.trig, Filter(function()
utils.debugfunc(function()
local e = dmg.event:new()
if e.amount ~= 0 then
local pobj
-----------------------------------------------
-- player-specific calc for taking damage:
-----------------------------------------------
if IsUnitType(e.target, UNIT_TYPE_HERO) and utils.pnum(e.targetp) <= kk.maxplayers then
if map.manager.isactive and map.manager.success then -- if map was won, zero out lingering damage.
e.amount = 0
else -- do normal damage
pobj = kobold.player[e.targetp]
pobj.score[3] = pobj.score[3] + math.floor(e.amount)
self.phit = true
-- see if pure damage reduction should be applied (a separate percentage reduction stat).
if pobj[p_stat_dmg_reduct] ~= 0 then
e.amount = math.max(e.amount*(1-pobj[p_stat_dmg_reduct]/100), e.amount*0.1) -- max 90% reduction.
end
-- see if thorns should be applied:
if pobj[p_stat_thorns] > 0 then
self.thorns = true
dmg.type.stack[dmg_phys]:pdeal(e.targetp, pobj[p_stat_thorns], e.source)
end
-- resistances calc:
if dtypelookup[e.dmgtype] then
e.amount = dmg.type.stack[dtypelookup[e.dmgtype]]:calctake(pobj, e.amount)
elseif atypelookup[e.atktype] then
e.amount = dmg.type.stack[atypelookup[e.atktype]]:calctake(pobj, e.amount)
end
-- player lethal check:
if e.amount >= utils.life(e.target) and not pobj.downed then
e:void()
pobj:down(true)
ArcingTextTag(color:wrap(color.tooltip.bad, "Downed"), e.target, self.txtdur)
elseif e.dmgtype == DAMAGE_TYPE_NORMAL then
-- armor reduction for non-spell attacks:
if not e:dodgecalc(pobj) then
e:armorcalc(pobj)
else
e.dodged = true
end
end
-- wrap up with ancient effects:
loot.ancient:raiseevent(ancient_event_damagetake, pobj, false, e)
end
end
-----------------------------------------------
-- player-specific calc for dealing damage:
-----------------------------------------------
if not self.thorns and IsUnitType(e.source, UNIT_TYPE_HERO) and utils.pnum(e.sourcep) <= kk.maxplayers then
pobj = kobold.player[e.sourcep]
self.pdmg = true
-- see if spell damage; if not, calc dmgtype bonus to auto attacks:
if atypelookup[e.atktype] == dmg_phys or atypelookup[e.dmgtype] == dmg_phys then -- is physical attack.
if not self.bonusdmg and pobj[p_stat_physls] > 0 then
local h = e.amount*(pobj[p_stat_physls]/100)
utils.addlifeval(e.source, h, true, e.source)
pobj.score[4] = pobj.score[4] + h
end
if e.weptype ~= WEAPON_TYPE_WHOKNOWS then -- is melee attack, apply physical modifier.
e.amount = d_lookup_atk[e.atktype]:calcdeal(e.sourcep, e.amount, e.target)
end
else -- is spell.
if not self.bonusdmg and pobj[p_stat_elels] > 0 then
local h = e.amount*(pobj[p_stat_elels]/100)
utils.addlifeval(e.source, h, true, e.source)
pobj.score[4] = pobj.score[4] + h
end
end
-- see if bonus damage should be applied:
if not self.bonusdmg and e.targetp ~= self.passive then
for typeid,pstat in ipairs(p_dmg_lookup) do
if pobj[pstat] > 0 then
self.bonusdmg = true
dmg.type.stack[typeid]:pdeal(e.sourcep, e.amount*pobj[pstat]/100, e.target)
end
end
-- see if non-bonus damage should be type converted:
if not self.convert then
for convid = p_epic_arcane_conv, p_epic_phys_conv do
if pobj[convid] > 0 then
e.amount = e.amount - e.amount*pobj[convid]/100
if e.amount > 0 then
self.convert = true
dmg.type.stack[p_conv_lookup[convid]]:pdeal(e.sourcep, e.amount*pobj[convid]/100, e.target)
end
end
end
end
else
self.bonusdmg = false
end
-- see if an ele proc should occur:
-- proliferation calc:
if not self.overtime and GetUnitTypeId(e.source) ~= FourCC("n00J") then -- is not a recursive-prone DoT effect (e.g. fire profilerate); is not a prolif demon.
if not self.eleproc and not self.bonusdmg and not self.pmin and not self.pminrep and not self.thorns and not self.convert then -- see if ele proc should roll.
if not self.prolifcd then
if e.amount > 10 and pobj[p_stat_eleproc] > 0 and (e.amount/utils.maxlife(e.target) >= 0.05
or GetUnitTypeId(e.target) == kk.ymmudid or IsUnitType(e.target, UNIT_TYPE_ANCIENT)) then
if math.random(0,100) < math.min(75, pobj:calceleproc()) then -- 75% max chance to proc.
self.eleproc = true
ArcingTextTag(color:wrap(color.dmgid[atypelookup[e.atktype]], "Proc!"), e.target, 1.0)
proliferate[atypelookup[e.atktype]](e) -- proc effect.
orb_prolif_stack[atypelookup[e.atktype]]:playu(e.target) -- special effect.
self.prolifcd = true
TimerStart(NewTimer(), 0.33, false, function() self.prolifcd = false end)
end
end
end
end
end
if not self.crit and pobj[p_epic_hit_crit] > 0 then
if math.random(1,100) <= pobj[p_epic_hit_crit] then
self.crit = true
e.amount = e.amount *1.50
end
end
-- wrap up with ancient effects:
loot.ancient:raiseevent(ancient_event_damagedeal, pobj, true, e)
elseif not utils.ishero(e.source) and utils.pnum(e.sourcep) <= kk.maxplayers then
-- is a player minion (consume player modifiers):
self.pmin = true
self.bonusdmg = false
end
-----------------------------------------------
-- elite ability helper:
-----------------------------------------------
if not self.pdmg and elite.stack[utils.data(e.target)] and utils.ishero(e.source) then
if utils.getmanap(e.target) == 100 then IssueTargetOrderById(e.target, 852075, e.source) end
end
-----------------------------------------------
-- absorb calc:
-----------------------------------------------
if not e.dodged and dmg.absorb.stack[utils.data(e.target)] and e.amount > 0 then
dmg.absorb.stack[utils.data(e.target)]:absorb(e)
end
-----------------------------------------------
-- wrap-up and damage control override:
-----------------------------------------------
if self.pdmg then
e.amount = math.random(math.floor(e.amount*0.98), math.floor(e.amount*1.02))
end
if self.pmin and not self.pminrep and e.sourcep then -- replace minion damage with player-calculated damage.
-- override with a new player-sourced event (to roll up p_stat modifiers).
self.pmin = false
self.pminrep = true
dmg.type.stack[atypelookup[e.atktype]]:pdeal(e.sourcep, e.amount*(1+kobold.player[e.sourcep][p_stat_miniondmg]/100), e.target)
e.amount = 0
end
BlzSetEventDamage(e.amount)
if dmg.cacheon then dmg:cache(e) end
if dmg.debug then e:debug() end
-----------------------------------------------
-- floating combat text:
-----------------------------------------------
if not e.dodged and e.amount > 0 then
if e.dmgtype == DAMAGE_TYPE_MAGIC or e.atktype == ATTACK_TYPE_MAGIC then
self.hexp = 1 -- arcane
elseif e.dmgtype == DAMAGE_TYPE_COLD or e.atktype == ATTACK_TYPE_MELEE then -- == ATTACK_TYPE_MELEE is ATTACK_TYPE_NORMAL
self.hexp = 2 -- frost
elseif e.dmgtype == DAMAGE_TYPE_PLANT or e.atktype == ATTACK_TYPE_PIERCE then
self.hexp = 3 -- nature
elseif e.dmgtype == DAMAGE_TYPE_FIRE or e.atktype == ATTACK_TYPE_CHAOS then
self.hexp = 4 -- fire
elseif e.dmgtype == DAMAGE_TYPE_DEATH or e.atktype == ATTACK_TYPE_SIEGE then
self.hexp = 5 -- shadow
elseif e.dmgtype == DAMAGE_TYPE_FORCE or e.atktype == ATTACK_TYPE_HERO then
self.hexp = 6 -- physical
else
self.hexp = 0
end
if self.crit then
ArcingTextTag(self.hext[self.hexp]..math.floor(e.amount).."|cffff3e3e!", e.target, self.txtdur)
else
ArcingTextTag(self.hext[self.hexp]..math.floor(e.amount), e.target, self.txtdur)
end
if self.pdmg and map.manager.activemap then pobj.score[1] = pobj.score[1] + e.amount end
elseif e.dodged then
ArcingTextTag(color:wrap(color.txt.txtwhite, "Dodge"), e.target, self.txtdur)
if self.phit and map.manager.activemap then pobj.score[2] = pobj.score[2] + e.amount end
elseif e.amount < 0 then
-- colorize healing text:
ArcingTextTag(color:wrap(color.tooltip.good, "+"..math.ceil(e.amount)), e.target, self.txtdur)
if self.pdmg and map.manager.activemap then pobj.score[4] = pobj.score[4] + e.amount end
end
-- flag cleanup:
self.pdmg = false
self.phit = false
self.thorns = false
self.convert = false
self.crit = false
self.pmin = false
self.eleproc = false
self.pminrep = false
self.overtime = false
pobj = nil
end
end, "dmg")
return true
end))
--
self.attacktypestring = {
[0] = "ATTACK_TYPE_NORMAL",
[1] = "ATTACK_TYPE_MELEE",
[2] = "ATTACK_TYPE_PIERCE",
[3] = "ATTACK_TYPE_SIEGE",
[4] = "ATTACK_TYPE_MAGIC",
[5] = "ATTACK_TYPE_CHAOS",
[6] = "ATTACK_TYPE_HERO"
}
self.damagetypestring = {
[0] = "DAMAGE_TYPE_UNKNOWN",
[4] = "DAMAGE_TYPE_NORMAL",
[5] = "DAMAGE_TYPE_ENHANCED",
[8] = "DAMAGE_TYPE_FIRE",
[9] = "DAMAGE_TYPE_COLD",
[10] = "DAMAGE_TYPE_LIGHTNING",
[11] = "DAMAGE_TYPE_POISON",
[12] = "DAMAGE_TYPE_DISEASE",
[13] = "DAMAGE_TYPE_DIVINE",
[14] = "DAMAGE_TYPE_MAGIC",
[15] = "DAMAGE_TYPE_SONIC",
[16] = "DAMAGE_TYPE_ACID",
[17] = "DAMAGE_TYPE_FORCE",
[18] = "DAMAGE_TYPE_DEATH",
[19] = "DAMAGE_TYPE_MIND",
[20] = "DAMAGE_TYPE_PLANT",
[21] = "DAMAGE_TYPE_DEFENSIVE",
[22] = "DAMAGE_TYPE_DEMOLITION",
[23] = "DAMAGE_TYPE_SLOW_POISON",
[24] = "DAMAGE_TYPE_SPIRIT_LINK",
[25] = "DAMAGE_TYPE_SHADOW_STRIKE",
[26] = "DAMAGE_TYPE_UNIVERSAL"
}
self.weapontypestring = {
[0] = "WEAPON_TYPE_WHOKNOWS",
[1] = "WEAPON_TYPE_METAL_LIGHT_CHOP",
[2] = "WEAPON_TYPE_METAL_MEDIUM_CHOP",
[3] = "WEAPON_TYPE_METAL_HEAVY_CHOP",
[4] = "WEAPON_TYPE_METAL_LIGHT_SLICE",
[5] = "WEAPON_TYPE_METAL_MEDIUM_SLICE",
[6] = "WEAPON_TYPE_METAL_HEAVY_SLICE",
[7] = "WEAPON_TYPE_METAL_MEDIUM_BASH",
[8] = "WEAPON_TYPE_METAL_HEAVY_BASH",
[9] = "WEAPON_TYPE_METAL_MEDIUM_STAB",
[10] = "WEAPON_TYPE_METAL_HEAVY_STAB",
[11] = "WEAPON_TYPE_WOOD_LIGHT_SLICE",
[12] = "WEAPON_TYPE_WOOD_MEDIUM_SLICE",
[13] = "WEAPON_TYPE_WOOD_HEAVY_SLICE",
[14] = "WEAPON_TYPE_WOOD_LIGHT_BASH",
[15] = "WEAPON_TYPE_WOOD_MEDIUM_BASH",
[16] = "WEAPON_TYPE_WOOD_HEAVY_BASH",
[17] = "WEAPON_TYPE_WOOD_LIGHT_STAB",
[18] = "WEAPON_TYPE_WOOD_MEDIUM_STAB",
[19] = "WEAPON_TYPE_CLAW_LIGHT_SLICE",
[20] = "WEAPON_TYPE_CLAW_MEDIUM_SLICE",
[21] = "WEAPON_TYPE_CLAW_HEAVY_SLICE",
[22] = "WEAPON_TYPE_AXE_MEDIUM_CHOP",
[23] = "WEAPON_TYPE_ROCK_HEAVY_BASH"
}
self.defensetypestring = {
[0] = "LIGHT",
[1] = "MEDIUM",
[2] = "HEAVY",
[3] = "FORTIFIED",
[4] = "NORMAL",
[5] = "HERO",
[6] = "DIVINE",
[7] = "UNARMORED",
}
dmg.type:init()
end
function dmg:cache(event)
self.history[#self.history+1] = event
if #self.history > self.overage then
for i = 1,self.buffer do
self.history[i] = nil
end
utils.tablecollapse(self.history)
end
end
function dmg:recent()
print("::: "..self.buffer.." most recent damage events :::")
print(" ")
for i = #self.history-self.buffer,#self.history do
if self.history[i] then
print(" ")
print("::: event " .. i .. " :::")
self.history[i]:debug()
else
print("caution: no event data was found in the cache for index "..i)
end
end
end
function dmg:getdef(unit)
return BlzGetUnitIntegerField(unit, UNIT_IF_DEFENSE_TYPE)
end
function dmg:convertdmgtype(unit, dmgtypeid)
for atktype,dmgid in pairs(atypelookup) do
if dmgid == dmgtypeid then
BlzSetUnitWeaponIntegerField(unit, UNIT_WEAPON_IF_ATTACK_ATTACK_TYPE, 0, GetHandleId(atktype))
break
end
end
for deftype,dmgid in pairs(deftypelookup) do
if dmgid == dmgtypeid then
BlzSetUnitIntegerField(unit, UNIT_IF_DEFENSE_TYPE, GetHandleId(deftype))
break
end
end
end
function dmg.event:init()
self.__index = self
end
function dmg.event:new()
local o = {}
o.source = GetEventDamageSource()
o.amount = GetEventDamage()
o.target = BlzGetEventDamageTarget()
o.atktype = BlzGetEventAttackType()
o.dmgtype = BlzGetEventDamageType()
o.weptype = BlzGetEventWeaponType()
o.sourcep = utils.powner(o.source)
o.targetp = utils.powner(o.target)
setmetatable(o, self)
self.__index = self
return o
end
function dmg.absorb:init()
-- initialize absorb listener
self.debug = false -- print debug messages.
self.stack = {}
self.hex = "|cffffffff"
self.instance = 0
self.endfuncs = {}
self.__index = self
return o
end
-- @unit = attach absorbs to this unit.
-- @dur = how long does the effect last?
-- @dmgtypet = pass in dmg types to absorb as a table using dmgtype names e.g. {physical = 500, fire = 500}.
-- @custeffect = [optional] override the default shield special effect.
-- example: { nature = 100, fire = 50 }
-- for ALL damage types: { all = 50 }
function dmg.absorb:new(unit, dur, dmgtypet, custeffect)
utils.debugfunc(function()
-- absorb damage for this unit based on index, add shield effect.
local dex = utils.data(unit)
local t = dmgtypet
if kobold.player[utils.powner(unit)] then
for dmgname,absorbval in pairs(t) do
t[dmgname] = absorbval*(1+(kobold.player[utils.powner(unit)][p_stat_absorb]/400)) -- each point is worth 0.25 perc bonus.
end
end
if not self.stack[dex] then
-- build absorb object:
self.stack[dex] = {}
self.stack[dex].absorbed = 0
self.stack[dex].vals = utils.shallow_copy(t)
self.stack[dex].dex = dex
self.stack[dex].p = utils.powner(unit)
self.stack[dex].unit = unit
setmetatable(self.stack[dex], self)
if self.debug then self.stack[dex]:debugprint('self.stack[dex] init') end
else
if self.debug then
print("attempting to merge t: ")
for i,v in pairs(t) do
print("i = ",i)
print("v = ",v)
end
end
self.stack[dex]:merge(t)
end
self.stack[dex]:addeffect(dur, custeffect)
TimerStart(NewTimer(), dur, false, function()
utils.debugfunc(function()
if self.stack[dex] then
if self.stack[dex].vals then
for n,val in pairs(t) do
if self.stack[dex].vals[n] > 0 then
if self.stack[dex].vals[n] > val then
self.stack[dex].vals[n] = self.stack[dex].vals[n] - (self.stack[dex].vals[n] - val)
elseif self.stack[dex].vals[n] <= val then
self.stack[dex].vals[n] = 0
end
end
end
t = nil -- destroy dmgtypet
end
if self.debug then self.stack[dex]:debugprint('timer before verify') end
self.stack[dex]:verify()
end
end, "absorb timer")
end)
end,"absorb new")
end
function dmg.absorb:addeffect(dur, custeffect)
if custeffect == speff_shield and self.effect then BlzSetSpecialEffectAlpha(self.effect, 0) DestroyEffect(self.effect) end
if custeffect then
self.effect = custeffect:attachu(self.unit, dur)
else
self.effect = speff_shield:attachu(self.unit, dur)
end
end
-- @newdmgtypet = merge this table with the existing absorb table.
function dmg.absorb:merge(newdmgtypet)
-- merge a new absorb event with an existing one.
for n,val in pairs(newdmgtypet) do
-- if value not yet initialized, add it:
if not self.vals[n] then
self.vals[n] = 0
else
self.vals[n] = self.vals[n] + val
end
if self.debug then
print("new merged absorb:")
print("new damage type found = "..n)
print("added val = "..val)
print("new self.vals[n] = "..self.vals[n])
end
end
if self.debug then self:debugprint('merge') end
end
function dmg.absorb:verify()
-- check if trigger should be enabled or not
self.emptycheck = true
for name,val in pairs(self.vals) do
if val > 0 then self.emptycheck = false else self.vals[name] = 0 end
if self.debug then print("verify "..name.." "..val) end
end
if self.emptycheck then
self:destroy()
end
end
function dmg.absorb:debugprint(str)
print('debugprint for:',str)
for n,v in pairs(self.vals) do
print(self.vals)
print(n.." = "..v)
end
end
function dmg.absorb:absorb(event)
-- absorb any damage type first:
local atype = atypenamet[event.atktype]
-- check if absorb table has matching damage type > 0 or absorbs all damage:
-- "all" dmg type is consumed first.
if (self.vals.all and self.vals.all > 0) or self.vals[atype] then
local amount = event.amount
local absorbed = 0
local remainder = 0
if self.debug then
print(GetHandleId(event.atktype))
print(dmg.attacktypestring[GetHandleId(event.atktype)])
print(self.vals[atype])
end
if self.vals.all then
self.vals.all = self.vals.all - amount
if self.vals.all < 0 then
remainder = -self.vals.all
self.vals.all = 0
end
absorbed = amount - remainder
end
-- for dealing with remainders, run individual element check separately:
if absorbed ~= amount and self.vals[atype] and self.vals[atype] > 0 then
-- separately calculate if a remainder should be deducted if "all" absorbed most of it first:
if remainder > 0 then
self.vals[atype] = self.vals[atype] - remainder
else
self.vals[atype] = self.vals[atype] - amount
end
if self.vals[atype] < 0 then
remainder = -self.vals[atype]
self.vals[atype] = 0
end
absorbed = amount - remainder
end
-- total absorbed amount:
self.absorbed = self.absorbed + absorbed
self:verify()
if absorbed > 0 then
ArcingTextTag(self.hex.."("..math.floor(self.absorbed)..")", event.target, dmg.txtdur)
if dmg.phit and map.manager.activemap then
kobold.player[event.targetp].score[2] = kobold.player[event.targetp].score[2] + absorbed - remainder
end
event.amount = amount - absorbed + remainder
end
end
end
function dmg.absorb:destroy()
DestroyEffect(self.effect)
self.stack[self.dex].val = nil
self.stack[self.dex] = nil
if self.debug then print("destroyed absorb instance") end
end
function dmg.event:void()
self.amount = 0
end
------------------------------------------------------------------------
function dmg.event:armorcalc(pobj)
self.armorval = pobj:calcarmor()
self.amount = math.floor(self.amount*math.max(1-self.armorval/100, 0.25)) -- armor has a 75 perc cap.
end
function dmg.event:dodgecalc(pobj)
if pobj[p_stat_dodge] > 0 then
self.dodgeval = pobj:calcdodge()
if math.random(0,1000) < math.min(self.dodgeval, 750) then -- dodge has a 75 perc cap.
self.amount = 0
return true
else -- failed to dodge
return false
end
else
return false
end
end
------------------------------------------------------------------------
function dmg.event:debug()
print("source = " .. GetHandleId(self.source) .. " (" .. GetUnitName(self.source) .. " - Player " .. utils.pidunit(self.source) .. ")")
print("target = " .. GetHandleId(self.target) .. " (" .. GetUnitName(self.target) .. " - Player " .. utils.pidunit(self.target) .. ")")
print("atktype = " .. dmg.attacktypestring[GetHandleId(self.atktype)] )
print("dmgtype = " .. dmg.damagetypestring[GetHandleId(self.dmgtype)] )
print("weptype = " .. dmg.weapontypestring[GetHandleId(self.weptype)] )
print("amount = " .. self.amount)
print(" ")
end
function dmg.type:init()
self.debug = false
--
self.__index = self
self.stack = {}
self.weptype = WEAPON_TYPE_WHOKNOWS
-- group cosntants:
dmg_g_elemental = 1
dmg_g_physical = 2
-- type contants:
dmg_arcane = 1
dmg_frost = 2
dmg_nature = 3
dmg_fire = 4
dmg_shadow = 5
dmg_phys = 6
-- globals:
dmg_max_res = 0.25 -- minimum damage that can be taken (75% = 0.25)
self[dmg_arcane] = dmg.type:new("Arcane", DAMAGE_TYPE_MAGIC, dmg_g_elemental, p_stat_arcane, p_stat_arcane_res, color.dmg.arcane)
self[dmg_arcane]:inittype (ATTACK_TYPE_MAGIC, DEFENSE_TYPE_DIVINE)
self[dmg_arcane].id = dmg_arcane
self[dmg_frost] = dmg.type:new("Frost", DAMAGE_TYPE_COLD, dmg_g_elemental, p_stat_frost, p_stat_frost_res, color.dmg.frost)
self[dmg_frost]:inittype (ATTACK_TYPE_MELEE, DEFENSE_TYPE_NORMAL)
self[dmg_frost].id = dmg_frost
self[dmg_nature] = dmg.type:new("Nature", DAMAGE_TYPE_PLANT, dmg_g_elemental, p_stat_nature, p_stat_nature_res, color.dmg.nature)
self[dmg_nature]:inittype (ATTACK_TYPE_PIERCE, DEFENSE_TYPE_LARGE)
self[dmg_nature].id = dmg_nature
self[dmg_fire] = dmg.type:new("Fire", DAMAGE_TYPE_FIRE, dmg_g_elemental, p_stat_fire, p_stat_fire_res, color.dmg.fire)
self[dmg_fire]:inittype (ATTACK_TYPE_CHAOS, DEFENSE_TYPE_LIGHT)
self[dmg_fire].id = dmg_fire
self[dmg_shadow] = dmg.type:new("Shadow", DAMAGE_TYPE_DEATH, dmg_g_elemental, p_stat_shadow, p_stat_shadow_res, color.dmg.shadow)
self[dmg_shadow]:inittype (ATTACK_TYPE_SIEGE, DEFENSE_TYPE_FORT)
self[dmg_shadow].id = dmg_shadow
self[dmg_phys] = dmg.type:new("Physical", DAMAGE_TYPE_FORCE, dmg_g_physical, p_stat_phys, p_stat_phys_res, color.dmg.physical)
self[dmg_phys]:inittype (ATTACK_TYPE_HERO, DEFENSE_TYPE_MEDIUM)
self[dmg_phys].id = dmg_phys
dmg.type:buildlookupt()
spell:assigndmgtypes()
end
function dmg.type:new(strname, dmgtype, dmggroup, p_stat, p_stat_res, txtcolor)
local o = {}
setmetatable(o, self)
o.name = strname
o.dmgtype = dmgtype
o.group = dmggroup
o.p_stat = p_stat
o.p_res = p_stat_res
o.color = txtcolor
o.effect = nil
self.stack[#self.stack+1] = o
return o
end
function dmg.type:inittype(atktype, deftype)
-- init mapped gameplay constant types.
self.atktype = atktype
self.deftype = deftype
end
function dmg.type:buildlookupt()
d_lookup_atk = {}
d_lookup_def = {}
for id,o in pairs(self) do
if type(o) == "table" and o.atktype and o.deftype then
d_lookup_atk[o.atktype] = o
d_lookup_def[o.deftype] = o
end
end
end
function dmg.type:basecalc(amount)
-- returns hypothetical damage for descriptions.
return math.floor(amount*(1+self.p_stat/100))
end
-- *NOTE: this returns a value, it doesn't deal damage.
function dmg.type:calcdeal(p, amount, target)
-- player deals damage to mobs formula.
local resistmul = 1
-- *note: we pass in a cached armor val beacuse we override the default armor system.
if BlzGetUnitIntegerField(target, UNIT_IF_DEFENSE_TYPE) == GetHandleId(self.deftype) then -- has resist type
resistmul = 1 - BlzGetUnitArmor(target)*0.01
end
if kobold.player[p] then
if self.debug then print("player calcdeal:",math.floor(amount*(1+kobold.player[p][self.p_stat]/100)*resistmul)) end
return math.floor(amount*(1+kobold.player[p][self.p_stat]/100)*resistmul)
else
if self.debug then print("non-player calcdeal:",math.floor(amount*resistmul)) end
return math.floor(amount*resistmul)
end
end
function dmg.type:calctake(pobj, amount)
-- player takes damage from mobs formula.
if pobj[self.p_res] ~= 0 then
if self.debug then print("calctake:",math.floor(math.max(amount*(1-pobj[self.p_res]/100), amount*dmg_max_res))) end
return math.floor(math.max(amount*(1-pobj[self.p_res]/100), amount*dmg_max_res))
end
return amount
end
function dmg.type:pdeal(p, amount, target)
if not utils.isinvul(target) then
-- damage a target with this damage type.
if self.debug then print("dmg.type:pdeal calc: "..self:calcdeal(p, amount, target)) end
local amt = self:calcdeal(p, amount, target)
-- because we use a custom damage formula, we need to ignore the default armor calc:
local cachearmor = BlzGetUnitArmor(target)
BlzSetUnitArmor(target, 0)
UnitDamageTarget(kobold.player[p].unit, target, amt, false, false, self.atktype, self.dmgtype, self.weptype)
BlzSetUnitArmor(target, cachearmor)
end
end
function dmg.type:ptake(p, amount, dealer)
if not utils.isinvul(kobold.player[p].unit) then
-- damage a player's kobold with this damage type.
UnitDamageTarget(dealer, kobold.player[p].unit, self:calctake(p, amount), false, false, self.atktype, self.dmgtype, self.weptype)
end
end
function dmg.type:dealdirect(dealer, target, amount)
if not utils.isinvul(target) then
UnitDamageTarget(dealer, target, amount, true, false, self.atktype, self.dmgtype, self.weptype)
end
end
function dmg.type:getrandomtype()
return self.stack[math.random(1, #self.stack)]
end
dev = {}
-- dev command listener:
onGlobalInit(function()
boss_dev_mode = false
dev.trig = CreateTrigger()
TriggerRegisterPlayerChatEvent(dev.trig, Player(0), "-", false)
TriggerAddAction(dev.trig, function()
utils.debugfunc(function()
dev_last_command = GetEventPlayerChatString()
local p = utils.trigp()
-- load quest
if string.sub(dev_last_command,2,2) == "q" and string.sub(dev_last_command,3,3) == " " then
-- clear current quest if it exists
if quest.current then
quest.current.nextquest = nil
if not quest.current.triggered then
quest.current:begin(true)
end
quest.current:finish(true)
end
local qstid = utils.trim(string.sub(dev_last_command, 4))
print("trying to load "..qstid)
quest.lineage[tonumber(qstid)]:activate()
quest.lineage[tonumber(qstid)]:begin()
quest.lineage[tonumber(qstid)].triggered = true
print("quest loaded with id: "..qstid)
end
if dev_last_command == "-proj" then
testprojects()
end
if dev_last_command == "-dk" then
kui.canvas.game.dig.mappiece:show()
kui.canvas.game.dig.card[5]:show()
for i = 1,5 do
loot:generatedigkey(p, kobold.player[p].level, i)
end
end
if dev_last_command == '-kb' then
KillUnit(map.mission.cache.boss.unit)
end
if dev_last_command == '-sk' then
boss_dev_mode = true
utils.setcambounds(GetPlayableMapRect())
DisableTrigger(kk.boundstrig)
local x,y = 0, 0
utils.setxy(kobold.player[Player(0)].unit, x, y)
map.mission:build(m_type_boss_fight)
map.mission.cache.boss = boss:new(utils.unitatxy(Player(24), x, y, 'N01B', 270.0), 'N01B')
map.mission.cache.boss.centerx, map.mission.cache.boss.centery = x,y
map.mission.cache.boss:init_boss_ai()
map.mission.cache.bossid = 'N01B'
map.mission.cache.boss:play_sound('intro')
-- test spell category:
--[[map.mission.cache.boss.spellcadence = {}
map.mission.cache.boss.spellcadence.basic = 999.9
map.mission.cache.boss.spellcadence.power = 999.9
map.mission.cache.boss.spellcadence.channel = 6.0
map.mission.cache.boss.spellcadence.ultimate = 999.9--]]
bossfr:show()
end
if dev_last_command == '-mm' then
boss_dev_mode = true
utils.setcambounds(GetPlayableMapRect())
DisableTrigger(kk.boundstrig)
local x,y = 0, 0
utils.setxy(kobold.player[Player(0)].unit, x, y)
map.mission:build(m_type_boss_fight)
map.mission.cache.boss = boss:new(utils.unitatxy(Player(24), x, y, 'N01F', 270.0), 'N01F')
map.mission.cache.boss.centerx, map.mission.cache.boss.centery = x,y
map.mission.cache.boss:init_boss_ai()
map.mission.cache.bossid = 'N01F'
map.mission.cache.boss:play_sound('intro')
bossfr:show()
end
if dev_last_command == '-mc' then
boss_dev_mode = true
utils.setcambounds(GetPlayableMapRect())
DisableTrigger(kk.boundstrig)
local x,y = 0, 0
utils.setxy(kobold.player[Player(0)].unit, x, y)
map.mission:build(m_type_boss_fight)
map.mission.cache.boss = boss:new(utils.unitatxy(Player(24), x, y, 'N01H', 270.0), 'N01H')
map.mission.cache.boss.centerx, map.mission.cache.boss.centery = x,y
map.mission.cache.boss:init_boss_ai()
map.mission.cache.bossid = 'N01H'
map.mission.cache.boss:play_sound('intro')
bossfr:show()
end
if dev_last_command == '-te' then
boss_dev_mode = true
utils.setcambounds(GetPlayableMapRect())
DisableTrigger(kk.boundstrig)
local x,y = 0, 0
utils.setxy(kobold.player[Player(0)].unit, x, y)
map.mission:build(m_type_boss_fight)
map.mission.cache.boss = boss:new(utils.unitatxy(Player(24), x, y, 'N01J', 270.0), 'N01J')
map.mission.cache.boss.centerx, map.mission.cache.boss.centery = x,y
map.mission.cache.boss:init_boss_ai()
map.mission.cache.bossid = 'N01J'
map.mission.cache.boss:play_sound('intro')
bossfr:show()
end
if dev_last_command == '-ag' then
boss_dev_mode = true
utils.setcambounds(GetPlayableMapRect())
DisableTrigger(kk.boundstrig)
local x,y = 0, 0
utils.setxy(kobold.player[Player(0)].unit, x, y)
map.mission:build(m_type_boss_fight)
map.mission.cache.boss = boss:new(utils.unitatxy(Player(24), x, y, 'N01R', 270.0), 'N01R')
map.mission.cache.boss.centerx, map.mission.cache.boss.centery = x,y
map.mission.cache.boss:init_boss_ai()
map.mission.cache.bossid = 'N01R'
map.mission.cache.boss:play_sound('intro')
bossfr:show()
end
if dev_last_command == '-bspell' then
-- test spell category:
map.mission.cache.boss.spellcadence = {}
map.mission.cache.boss.spellcadence.basic = 999.9
map.mission.cache.boss.spellcadence.power = 999.9
map.mission.cache.boss.spellcadence.channel = 999.0
map.mission.cache.boss.spellcadence.ultimate = 10.0
end
if dev_last_command == '-ai' then
for aid = 1,36 do
loot:generateancient(Player(0), kobold.player[Player(0)].level, nil, nil, aid)
end
end
if dev_last_command == '-ai2' then
for aid = 37,48 do
loot:generateancient(Player(0), kobold.player[Player(0)].level, nil, nil, aid)
end
end
if dev_last_command == '-pai' then
for i,v in pairs(loot.ancienttable) do
print('ancientid stored in ancienttable:', i)
end
end
if dev_last_command == '-eai' then
for i,v in pairs(kobold.player[Player(0)].ancients) do
print("id", i, "=")
for i2, v2 in pairs(v) do
print(i2, "=", v2)
end
end
end
if dev_last_command == '-can' then
local x,y = utils.unitxy(kobold.player[Player(0)].unit)
x,y = x + math.random(0,100), y + math.random(0,100)
candle:spawnwax(x, y)
candle:spawnwax(x, y, "medium")
candle:spawnwax(x, y, "large")
end
if dev_last_command == '-relics' then
placeproject('boss1')
placeproject('boss2')
placeproject('boss3')
placeproject('boss4')
end
if dev_last_command == '-cons' then
for i,v in pairs(constructiont) do
placeproject(i)
end
end
if dev_last_command == '-ele' then
kobold.player[Player(0)]:modstat(p_stat_eleproc, true, 1000)
end
if dev_last_command == '-h10' then
utils.setlifep(kobold.player[Player(0)].unit, 10)
end
if dev_last_command == '-fp' then
kobold.player[Player(0)]:freeplaymode()
end
if dev_last_command == '-verifyq' then
utils.timedrepeat(1.0, 14, function()
if quest.current then
quest.current:finish()
utils.timed(1.0, function() speak:endscene() end)
else
ReleaseTimer()
end
end)
end
if dev_last_command == '-save' then
kobold.player[p].char.fileslot = 1
kobold.player[p].char:save_character()
end
if dev_last_command == '-load' then
kui:hidesplash(p, true)
kobold.player[p].isloading = true
kobold.player[p].char = char:new(p)
kobold.player[p].char:read_file(1)
kobold.player[p].char:get_file_data()
kobold.player[p].char:load_character()
end
if dev_last_command == '-saveprint' then
kobold.player[p].char:print_data()
end
if dev_last_command == '-frags' then
kobold.player[p]:awardfragment(10)
end
if dev_last_command == '-fshop' then
shop.gwfr:show()
end
if dev_last_command == '-chest1' then
map.manager.prevbiomeid = math.random(1,5)
map.manager.prevdiffid = 1
boss:awardbosschest()
end
if dev_last_command == '-chest2' then
map.manager.prevbiomeid = math.random(1,5)
map.manager.prevdiffid = 2
boss:awardbosschest()
end
if dev_last_command == '-chest3' then
map.manager.prevbiomeid = math.random(1,5)
map.manager.prevdiffid = 3
boss:awardbosschest()
end
if dev_last_command == '-chest4' then
map.manager.prevbiomeid = math.random(1,5)
map.manager.prevdiffid = 4
boss:awardbosschest()
end
if dev_last_command == '-chest5' then
map.manager.prevbiomeid = math.random(1,5)
map.manager.prevdiffid = 5
boss:awardbosschest()
end
if dev_last_command == '-chestv1' then
map.manager.prevbiomeid = 1
map.manager.prevdiffid = 5
boss:awardbosschest()
end
if dev_last_command == '-chestv2' then
map.manager.prevbiomeid = 2
map.manager.prevdiffid = 5
boss:awardbosschest()
end
if dev_last_command == '-chestv3' then
map.manager.prevbiomeid = 3
map.manager.prevdiffid = 5
boss:awardbosschest()
end
if dev_last_command == '-chestv4' then
map.manager.prevbiomeid = 4
map.manager.prevdiffid = 5
boss:awardbosschest()
end
if dev_last_command == '-chestv5' then
map.manager.prevbiomeid = 5
map.manager.prevdiffid = 5
boss:awardbosschest()
end
if dev_last_command == '-ts' then
shrine_dev_mode = true
map.manager.activemap = true
map.mission.cache = map.mission:generate(m_type_monster_hunt)
map.mission.cache.level = 60
candle:load()
utils.setcambounds(GetPlayableMapRect())
DisableTrigger(kk.boundstrig)
local x,y = -1300, 2300
utils.setxy(kobold.player[Player(0)].unit, 0, 1000)
for i,s in ipairs(shrine.devstack) do
s:create(x + (i-1)*326, y - (i-1)*326)
end
end
if dev_last_command == '-ts1' then
shrine_dev_mode = true
map.manager.activemap = true
map.mission.cache = map.mission:generate(m_type_monster_hunt)
map.mission.cache.level = 60
candle:load()
utils.setcambounds(GetPlayableMapRect())
DisableTrigger(kk.boundstrig)
local x,y = -1300, 2300
utils.setxy(kobold.player[Player(0)].unit, 0, 1000)
for i = 1,3 do
shrine_arcprison:create(x, y)
end
end
if dev_last_command == '-buff' then
for i = 1,20 do
if i < 10 then
buffy:add_indicator(p, "Test Title "..tostring(i), 'war3mapImported\\class_ability_icons_0'..tostring(i)..'.blp', i+3, "Test description")
else
buffy:add_indicator(p, "Test Title "..tostring(i), 'war3mapImported\\class_ability_icons_'..tostring(i)..'.blp', i+3, "Test description")
end
end
end
if dev_last_command == '-debuff' then
for i,b in ipairs(buffy.stack) do
b:apply(kobold.player[p].unit, math.random(3,10))
end
end
if dev_last_command == '-movespeed' then
print(BlzGetUnitRealField(kobold.player[p].unit, UNIT_RF_SPEED))
end
if dev_last_command == '-badge' then
kui.canvas.game.badge:show()
end
if dev_last_command == '-badgetest' then
for id = 1,18 do
if math.random(1,2) == 1 then
for classid = 1,math.random(1,4) do
badge:earn(kobold.player[Player(0)], id, classid)
end
end
end
end
if dev_last_command == '-debugloot' then
loot.debug = true
end
-- force set a mission:
if string.sub(dev_last_command,2,2) == "m" and string.sub(dev_last_command,3,3) == " " then
local misid = utils.trim(string.sub(dev_last_command, 4))
print("forced mission id set to "..misid)
if misid == 0 then
dev_mission_id = nil
else
dev_mission_id = tonumber(misid)
end
end
end, "dev command")
end)
end)
function dev:fakeplayers()
for pnum = 2,4 do
local p = Player(pnum-1)
SetPlayerAlliance(p, Player(0), ALLIANCE_SHARED_CONTROL, true)
kobold.player[p] = kobold.player:new(p)
kobold.player[p].unit = CreateUnit(p, FourCC(kui.meta.char04raw), utils.rectx(gg_rct_charspawn), utils.recty(gg_rct_charspawn), 270.0)
kobold.player[p].charid = 4
kobold.player[p].mastery = mastery:new()
kobold.player[p].permtimer = NewTimer()
SetTimerData(kobold.player[p].permtimer, function() end) -- dummy func to prevent error raise
kui.canvas.game.party.pill[pnum] = kui:createpartypill(kui.canvas.game.party, pnum, kobold.player[p].charid)
kui:attachupdatetimer(p, pnum)
end
end
function dev:completequest()
quest.current:markprogress(true)
end
function dev:showshop()
quest_elementalist_unlocked = true
quest_shinykeeper_unlocked = true
quest.socialfr.eleshop:show()
quest.socialfr.shinshop:show()
for i = 1,6 do
kobold.player[Player(0)]:awardoretype(i, 100)
end
kobold.player[Player(0)]:awardgold(10000)
end
function dev:toggleshop()
if not shop.fr.shinypane:isvisible() then
shop_vendor_type_open[utils.trigpnum()] = 0
shop.fr.shinypane:show()
shop.fr.elepane:hide()
else
shop_vendor_type_open[utils.trigpnum()] = 1
shop.fr.shinypane:hide()
shop.fr.elepane:show()
end
end
function dev:devscreenplay1()
screenplay:run(screenplay.chains.intro_1)
end
function dev:devscreenplay2()
screenplay:run(screenplay.chains.intro_2)
end
function dev:devscreenplay3()
screenplay:run(screenplay.chains.intro_3)
end
function dev:candle()
candle.current = candle.current - 75
end
function dev:fakeleave()
kobold.player[Player(2)]:leftgame()
end
function dev:fakedown()
for pnum = 2,4 do
if kobold.player[Player(pnum-1)] then kobold.player[Player(pnum-1)]:down(true) end
end
end
function dev:loot()
-- dev space:
if loot:isbagfull(Player(0)) then
for slotid = 1,42 do
kobold.player[Player(0)].items[slotid] = nil
end
loot:cleanupempty(Player(0))
end
for i = 1,42 do
utils.debugfunc(function()
loot:generate(Player(0),
kobold.player[Player(0)].level,
loot:getrandomslottype())
end, "loot:generate")
end
end
function dev:devgear()
for slotid = 1001,1010 do
local item = loot:generate(Player(0), kobold.player[Player(0)].level, slotid, rarity_epic)
kobold.player[Player(0)].selslotid = 1
item:equip(Player(0))
end
end
function dev:devgeardestroy()
for slotid = 1001,1010 do
kobold.player[Player(0)].selslotid = slotid
loot.item:unequip(Player(0))
end
dev:clearitems()
end
function dev:clearitems()
for slotid = 1,42 do
kobold.player[Player(0)].items[slotid] = nil
end
loot:cleanupempty(Player(0))
end
function dev:loot60()
-- dev space:
if loot:isbagfull(Player(0)) then
for slotid = 1,42 do
kobold.player[Player(0)].items[slotid] = nil
end
loot:cleanupempty(Player(0))
end
for i = 1,42 do
utils.debugfunc(function()
loot:generate(Player(0),
i+1,
loot:getrandomslottype())
end, "loot:generate")
end
end
function dev:level(x)
utils.debugfunc( function()
for _ = 1,x do
if kobold.player[Player(0)].level < 60 then
kobold.player[Player(0)]:addlevel()
end
end
end, "dev")
end
function dev:addallattr(_attr)
for i = 1,300 do
if _attr then
kobold.player[Player(0)]:applyattrpoint(_attr)
else
kobold.player[Player(0)]:applyattrpoint(p_stat_strength)
kobold.player[Player(0)]:applyattrpoint(p_stat_wisdom)
kobold.player[Player(0)]:applyattrpoint(p_stat_alacrity)
kobold.player[Player(0)]:applyattrpoint(p_stat_vitality)
end
end
end
function dev:learn()
kobold.player[Player(0)].ability:learnmastery("na1")
end
elite = {
manamax = 100.0, -- reach this cap to cast an elite spell.
managen = 4.00, -- determines how fast mana is capped.
packmin = 1,
packmax = 3,
spelldmg = 21, -- base spell damage.
debug = false,
debugsp = false, -- disable ability adding to test maually.
}
elite.goon = {} -- an elite's empowered minions.
elite.abils = {} -- store possible abilities to add (controlled by AI script).
elite.goon.abils = {} -- store possible abilities to add to goons.
function elite:init()
self.__index = self
self.stack = {}
self.colort = {
[1] = PLAYER_COLOR_BLUE,
[2] = PLAYER_COLOR_CYAN,
[3] = PLAYER_COLOR_GREEN,
[4] = PLAYER_COLOR_ORANGE,
[5] = PLAYER_COLOR_PURPLE,
[6] = PLAYER_COLOR_LIGHT_GRAY,
}
elite.goon.__index = elite.goon
setmetatable(elite.goon, elite)
self:buildspells()
self:deathtrig()
end
function elite:deathtrig()
self.trig = trg:new("death", Player(PLAYER_NEUTRAL_AGGRESSIVE)) -- ondeath trig
self.trig:regaction(function()
-- award ore on elite death based on elite's enhanced element id:
if elite.stack[utils.data(utils.trigu())] and not elite.stack[utils.data(utils.trigu())].isgoon then
local u = utils.trigu()
local oreid = map.block.oretypet[elite.stack[utils.data(u)].dmgtypeid]
local count = math.ceil((1*map.mission.cache.setting[m_stat_treasure] + map.manager.diffid)/2)
map.block:spawnore(1*map.manager.diffid, oreid, utils.unitxy(u))
if math.random(0,10) < math.ceil(map.manager.diffid/2) then
loot:generatelootpile(utils.unitx(u), utils.unity(u), math.random(1,2), Player(0))
end
-- chance to drop wax:
candle:spawnwax(utils.unitx(u), utils.unity(u))
utils.palertall(color:wrap(color.rarity.rare, "Empowered creature defeated!"), 4.0, true)
end
end)
elite.deathtrig = nil
end
-- @unit = the unit to roll into an elite.
-- @dmgtypeid = the element type to use.
-- @cluster = creature cluster table used to determine goon types (e.g. creature.cluster.creatures.normal).
function elite:new(unit, dmgtypeid, cluster)
local packmin, packmax = self.packmin, self.packmax
local o = setmetatable({}, self)
o.unit = unit
o.dmgtypeid = dmgtypeid -- element id.
o.creaturet = creaturet -- store in case we want abils to spawn more units etc.
o.dex = utils.data(unit)
o.goons = {} -- where goons are stored for AI purposes.
-- create goon squad:
for _ = packmin, math.random(packmin, packmax) do
local packmax = packmax
if map.manager.diffid > 3 then
-- -- Heroic or higher difficulty, density is increased:
packmax = packmax*map.mission.cache.setting[m_stat_elite_pack]
end
local x,y = utils.projectxy(utils.unitx(unit), utils.unity(unit), math.random(128,256), math.random(0,360))
-- clear destructables in the way to keep pack inside a workable arena:
map.grid:excavate(x, y, 256)
if map.manager.diffid > 3 then
-- Heroic or higher difficulty, add passives:
local abilroll = math.random(1, #elite.goon.abils[dmgtypeid])
if cluster.creatures.normal and #cluster.creatures.normal > 0 then
elite.goon:new(cluster.creatures.normal[math.random(1,#cluster.creatures.normal)]:create(x, y), dmgtypeid, o):addpassive(abilroll)
elseif cluster.creatures.weak and #cluster.creatures.weak > 0 then
elite.goon:new(cluster.creatures.weak[math.random(1,#cluster.creatures.weak)]:create(x, y), dmgtypeid, o):addpassive(abilroll)
end
else
-- Greenwhisker or Normal difficulty, no passives:
if cluster.creatures.normal and #cluster.creatures.normal > 0 then
elite.goon:new(cluster.creatures.normal[math.random(1,#cluster.creatures.normal)]:create(x, y), dmgtypeid, o)
elseif cluster.creatures.weak and #cluster.creatures.weak > 0 then
elite.goon:new(cluster.creatures.weak[math.random(1,#cluster.creatures.weak)]:create(x, y), dmgtypeid, o)
end
end
end
-- init elite unit:
o:addglow()
o:updateatkdef()
o:addeliteabil(dmgtypeid)
-- spell:addenchant(unit, dmgtypeid)
if map.manager.diffid > 3 then
-- Heroic or higher difficulty, elites cast more often:
o.managen = self.managen*map.mission.cache.setting[m_stat_elite_str] -- pull in lethality modifier.
end
utils.scaleatk(unit, map.mission.cache.setting[m_stat_elite_str])
utils.setnewunitmana(unit, self.manamax)
BlzSetUnitRealField(unit, UNIT_RF_MANA_REGENERATION, o.managen)
SetUnitScale(unit, 1.25, 1.25, 1.25)
elite.stack[o.dex] = o
return o
end
function elite:destroy()
utils.shallow_destroy(self)
elite.stack[self.dex] = nil
end
-- after a map ends, destroy all elite objects
function elite:cleanup()
for _,e in pairs(self.stack) do
if e.isgoon then
e:destroygoon()
else
e:destroy()
end
end
end
function elite:addglow()
UnitAddAbility(self.unit, sp_enchant_elite[self.dmgtypeid])
SetUnitColor(self.unit, self.colort[self.dmgtypeid])
end
function elite:addeliteabil()
if not elite.debugsp then
UnitAddAbility(self.unit, elite.abils[math.random(1, #elite.abils)].code)
end
end
-- update an elite unit to have its attack and defense match its assigned dmgtypeid.
function elite:updateatkdef(_isgoon)
dmg:convertdmgtype(self.unit, self.dmgtypeid)
BlzSetUnitWeaponStringField(self.unit, UNIT_WEAPON_SF_ATTACK_PROJECTILE_ART, 0, mis_ele_stack2[self.dmgtypeid].effect) -- attack art.
BlzSetUnitArmor(self.unit, math.floor(BlzGetUnitArmor(self.unit)+(5*(map.manager.diffid-1))))
if _isgoon then
utils.setnewunithp(self.unit, math.ceil(BlzGetUnitMaxHP(self.unit)*2.50), true)
utils.scaleatk(self.unit, 1.50)
else
utils.setnewunithp(self.unit, math.ceil(BlzGetUnitMaxHP(self.unit)*1.50), true)
utils.scaleatk(self.unit, 1.10)
end
end
function elite:buildspells()
local p
if elite.debug then
p = Player(0)
print("caution: elite debug mode is on (spells activate for Player 1)")
else
p = Player(PLAYER_NEUTRAL_AGGRESSIVE)
end
--[[
ELITE ABILS -----------------------------------------------------------------------------
--]]
elite.abils = {}
-- ELEMENTAL ORBS --
-- spawns orbs which create swirling elemental tethers, damaging players caught in the tether.
elite.abils[1] = spell:new(casttype_unit, p, FourCC('A02B'))
elite.abils[1]:addaction(function()
local caster = elite.abils[1].caster
local dmgtypeid = elite.stack[utils.data(caster)].dmgtypeid
local dps = elite.spelldmg*map.mission.cache.setting[m_stat_attack]
local dmgtype = dmg.type.stack[dmgtypeid]
local x,y,x2,y2,vel,a,a2,e,e2,start,sc,t = {},{},{},{},{},{},{},{},{},math.random(0,360),0.49,0
for i = 1,3 do
a[i] = start+((i-1)*120)
a2[i] = a[i]
x[i], y[i] = utils.projectxy(elite.abils[1].casterx, elite.abils[1].castery, math.random(32,128), a[i])
e[i] = orb_eff_stack[dmgtypeid]:create(x[i], y[i])
vel[i] = math.random(5,8)
BlzSetSpecialEffectScale(e[i], sc)
end
-- begin moving orbs into position
utils.timedrepeat(0.03, 51, function()
utils.debugfunc(function()
for i = 1,3 do
x[i], y[i] = utils.projectxy(x[i], y[i], vel[i], a[i])
utils.seteffectxy(e[i], x[i], y[i])
end
end)
end, function()
-- queue charge effect indicator:
for i = 1,3 do
speff_boosted:play(x[i], y[i])
end
utils.timedrepeat(0.03, 51, function()
utils.debugfunc(function()
-- scale the orbs until they activate:
for i = 1,3 do
sc = sc + 0.01
BlzSetSpecialEffectScale(e[i], sc)
end
end)
end, function()
utils.debugfunc(function()
-- create tethers and queue activation indicator:
for i = 1,3 do
speff_generate:play(x[i], y[i])
e2[i] = teth_gate_stack[dmgtypeid]:create(x[i], y[i])
a2[i] = math.random(0,360)
end
utils.timedrepeat(0.03, 300*map.mission.cache.setting[m_stat_elite_str], function()
utils.debugfunc(function()
t = t + 1
-- spin the tethers and deal damage:
for i = 1,3 do
a2[i] = a2[i] + 1.15
x2[i], y2[i] = utils.projectxy(x[i], y[i], 164, a2[i])
utils.seteffectxy(e2[i], x2[i], y2[i])
BlzSetSpecialEffectYaw(e2[i], (a2[i]+90.0)*bj_DEGTORAD)
if math.fmod(t, 12) == 0 then
spell:creepgdmgxy(caster, dmgtype, dps, x2[i], y2[i], 128)
end
end
end)
end, function()
for i = 1,3 do DestroyEffect(e[i]) DestroyEffect(e2[i]) end
x,y,x2,y2,vel,a,a2,e,e2,dmgtype = nil,nil,nil,nil,nil,nil,nil,nil,nil,nil
end)
end)
end)
end)
end)
-- MORTAR SHELLS --
-- queues mortars to fire over time in a line at each player nearby.
elite.abils[2] = spell:new(casttype_unit, p, FourCC('A029'))
elite.abils[2]:addaction(function()
local rand = {}
local grp = g:newbyxy(p, g_type_dmg, elite.abils[2].casterx, elite.abils[2].castery, 1200.0)
grp:action(function()
if utils.ishero(grp.unit) then
rand[#rand+1] = grp.unit
end
end)
grp:destroy()
-- select random player to target if present:
if #rand > 0 then
local miscount = 6
local caster = elite.abils[2].caster
local dmgtypeid = elite.stack[utils.data(caster)].dmgtypeid
local dmgtype = dmg.type.stack[dmgtypeid]
local spelldmg = elite.spelldmg*1.66*map.mission.cache.setting[m_stat_attack]
utils.timedrepeat(0.66, #rand, function()
local lastmis = miscount+1
local x,y,e = {},{},{}
local index = math.random(1,#rand)
local selunit = rand[index]
local a = utils.anglexy(utils.unitx(caster), utils.unity(caster), utils.unitxy(selunit))
table.remove(rand, index)
for i = 1,miscount do
a = a - math.random(1,18)
a = a + math.random(1,18)
x[i], y[i] = utils.projectxy(utils.unitx(caster), utils.unity(caster), i*math.random(75,150)+256, a)
e[i] = area_marker_stack[dmgtypeid]:create(x[i], y[i])
BlzSetSpecialEffectScale(e[i],0.75)
end
-- place 1 shell directly on the player (force them to react):
x[lastmis], y[lastmis] = utils.unitxy(selunit)
e[lastmis] = area_marker_stack[dmgtypeid]:create(x[lastmis], y[lastmis])
BlzSetSpecialEffectScale(e[lastmis],0.75)
utils.timed(1.72, function()
local inc = miscount+1 -- add the direct mortar.
-- begin launching mortar missiles:
utils.timedrepeat(0.51, miscount+1, function()
DestroyEffect(e[inc])
if utils.isalive(caster) then
local mis = spell:pmissile_custom_xy(
caster, utils.unitx(caster), utils.unity(caster), x[inc], y[inc], mis_bolt_stack[dmgtypeid], spelldmg)
mis:initarcing()
mis.explr = 128
mis.dmgtype = dmgtype
end
inc = inc - 1
end, function()
x,y,e,s = nil,nil,nil,nil
end)
end)
end, function()
dmgtype,rand = nil,nil,nil
end)
else
rand,s = nil,nil
end
end)
-- STORM STRIKE --
-- sends out moving markers that create a lightning strike every 2 sec for 10 sec.
elite.abils[3] = spell:new(casttype_unit, p, FourCC('A02I'))
elite.abils[3]:addaction(function()
local caster = elite.abils[3].caster
local count = 4
local dmgtypeid = elite.stack[utils.data(caster)].dmgtypeid
local spelldmg = elite.spelldmg*2.33*map.mission.cache.setting[m_stat_attack]
local dmgtype = dmg.type.stack[dmgtypeid]
local vel = 6
local x,y,e,a,d,t,start = {},{},{},{},{},0,math.random(0,360)
for i = 1,count do
if math.random(1,2) == 1 then d[i] = 1 else d[i] = -1 end
a[i] = start+(i-1)*(360/count)
x[i], y[i] = utils.projectxy(utils.unitx(caster), utils.unity(caster), (i*48)+math.random(196,512), a[i])
e[i] = area_marker_stack[dmgtypeid]:create(x[i], y[i])
BlzSetSpecialEffectScale(e[i],1.15)
end
utils.timed(1.00, function()
utils.timedrepeat(0.03, 330, function()
t = t + 1
for i = 1,count do
a[i] = a[i] + d[i]*1.5
x[i], y[i] = utils.projectxy(x[i], y[i], vel, a[i])
utils.seteffectxy(e[i], x[i], y[i])
if math.fmod(t, 66) == 0 then
area_storm_stack[dmgtypeid]:play(x[i], y[i])
spell:creepgdmgxy(caster, dmgtype, spelldmg, x[i], y[i], 176)
end
end
end, function()
for i = 1,count do DestroyEffect(e[i]) end
x,y,e,a,d = nil,nil,nil,nil,nil
end)
end)
end)
--[[
GOON ABILS --------------------------------------------------------------------------------
--]]
elite.goon.abils = {}
elite.goon.abils[dmg_arcane] = {}
elite.goon.abils[dmg_arcane][#elite.goon.abils[dmg_arcane]+1] = FourCC('A024') -- energy shield
elite.goon.abils[dmg_arcane][#elite.goon.abils[dmg_arcane]+1] = FourCC('A022') -- manathirst
elite.goon.abils[dmg_frost] = {}
elite.goon.abils[dmg_frost][#elite.goon.abils[dmg_frost]+1] = FourCC('A027') -- chilled touch
elite.goon.abils[dmg_nature] = {}
elite.goon.abils[dmg_nature][#elite.goon.abils[dmg_nature]+1] = FourCC('A020') -- overcharged
elite.goon.abils[dmg_fire] = {}
elite.goon.abils[dmg_fire][#elite.goon.abils[dmg_fire]+1] = FourCC('A028') -- immolated
elite.goon.abils[dmg_fire][#elite.goon.abils[dmg_fire]+1] = FourCC('A023') -- infernal weapons
elite.goon.abils[dmg_shadow] = {}
elite.goon.abils[dmg_shadow][#elite.goon.abils[dmg_shadow]+1] = FourCC('A026') -- envenomed
elite.goon.abils[dmg_shadow][#elite.goon.abils[dmg_shadow]+1] = FourCC('A025') -- undeath
elite.goon.abils[dmg_phys] = {}
elite.goon.abils[dmg_phys][#elite.goon.abils[dmg_phys]+1] = FourCC('A007') -- critical wounds
elite.goon.abils[dmg_phys][#elite.goon.abils[dmg_phys]+1] = FourCC('A006') -- staggering blows
elite.goon.abils[dmg_phys][#elite.goon.abils[dmg_phys]+1] = FourCC('A01Z') -- thorns
elite.buildspells = nil
end
-- @unit = the unit to roll into a goon.
-- @dmgtypeid = the element rolled for this elite.
-- @eliteowner = the leader of the elite pack.
function elite.goon:new(unit, dmgtypeid, eliteowner)
local o = setmetatable({}, self)
o.unit = unit
o.isgoon = true
o.dmgtypeid = dmgtypeid
o.chief = eliteowner
o.dex = utils.data(unit)
o:updateatkdef(true)
spell:addenchant(unit, dmgtypeid)
SetUnitColor(self.unit, self.colort[self.dmgtypeid])
elite.stack[o.dex] = o
eliteowner.goons[#eliteowner.goons+1] = o
return o
end
function elite.goon:destroygoon()
utils.shallow_destroy(self)
elite.stack[self.dex] = nil
end
-- @rollid = pass in the rolled index for the ability so each goon has the same effect (allowing predictability for players).
function elite.goon:addpassive(rollid)
UnitAddAbility(self.unit, elite.goon.abils[self.dmgtypeid][rollid])
end
indexer = {} -- indexer class.
function indexer:preplaced()
for i = bj_MAX_PLAYER_SLOTS - 1, 0, -1 do
GroupEnumUnitsOfPlayer(bj_lastCreatedGroup, Player(i), Filter(function()
if self.enabled then indexer:addindex(GetFilterUnit(), true) end
return false
end))
end
GroupClear(bj_lastCreatedGroup)
end
-- @unit = index this unit and set its custom value
-- @bool = was it a prelaced unit?
function indexer:addindex(unit, bool)
self.index = self.index + 1
self.placed[self.index] = bool or false
SetUnitUserData(unit, self.index)
end
function indexer:init()
self.debug = false -- print data lookups when true.
self.region = CreateRegion()
self.map = GetWorldBounds()
self.trig = CreateTrigger()
self.index = 0
self.enabled = true
self.placed = {}
self.__index = self
RegionAddRect(self.region, self.map)
RemoveRect(self.map)
TriggerRegisterEnterRegion(self.trig, self.region, Condition(function() if self.enabled then indexer:addindex(GetFilterUnit(), false) end return false end))
end
do
onTriggerInit(function()
indexer:init()
indexer:preplaced()
end)
end
kk = {}
kk.devmode = false -- disable certain features for expediency.
onGlobalInit(function()
utils.debugfunc(function()
if ReloadGameCachesFromDisk() then is_single_player = true else is_single_player = false end
kk.packageinit()
speak:init()
end, "onGlobalInit")
end)
function kk.init()
kk.maxplayers = 1
kk.boundstrig = CreateTrigger()
kk.dusteff = speffect:new('Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl')
kk.ymmudid = FourCC('n004')
BlzEnableSelections(true, false) -- disable sel circles.
SetPlayerState(Player(PLAYER_NEUTRAL_AGGRESSIVE), PLAYER_STATE_GIVES_BOUNTY, 0)
-- set up eyecandy units:
local tempg = CreateGroup()
GroupEnumUnitsInRange(tempg, 16300.0, -16900.00, 3000.0, Condition(function()
local f = GetFilterUnit()
if GetOwningPlayer(f) == Player(11) then
SetUnitInvulnerable(f, true)
PauseUnit(f, true)
if GetUnitTypeId(f) == FourCC('nkot') then -- tunneler
UnitAddAbility(f, FourCC('A008'))
IssueImmediateOrderBJ(f, "channel")
else
PauseUnit(f,true)
end
if GetUnitTypeId(f) == FourCC('n001') then -- shinykeeper
SetUnitColor(f, GetPlayerColor(Player(4)))
elseif GetUnitTypeId(f) == FourCC('n002') then -- elementalist
SetUnitColor(f, GetPlayerColor(Player(3)))
elseif GetUnitTypeId(f) == FourCC('n000') then -- dig master
SetUnitColor(f, GetPlayerColor(Player(5)))
end
end
return true
end))
DestroyGroup(tempg)
-- make excavation sites invulnerable:
EnumDestructablesInRect(gg_rct_expeditionVision, nil, function()
if GetDestructableTypeId(GetEnumDestructable()) == FourCC('B00A') then
SetDestructableMaxLife(GetEnumDestructable(), 100000)
SetDestructableLifePercentBJ(GetEnumDestructable(), 100)
SetDestructableInvulnerable(GetEnumDestructable(), true)
end
end)
-- catch starting area escapees:
TriggerRegisterLeaveRectSimple(kk.boundstrig, gg_rct_expeditionVision, nil)
TriggerAddAction(kk.boundstrig, function()
local p = utils.unitp()
if utils.pnum(p) < kk.maxplayers then -- is player
utils.setxy(GetTriggerUnit(), GetRectCenterX(gg_rct_charspawn), GetRectCenterY(gg_rct_charspawn))
end
end)
-- kobold enter/exit eyecandy:
expeditionExitTrig = CreateTrigger()
TriggerRegisterEnterRectSimple(expeditionExitTrig, gg_rct_expeditionExit)
TriggerAddCondition(expeditionExitTrig, Condition(function() return GetUnitTypeId(GetTriggerUnit()) == FourCC('nkot') end))
TriggerAddAction(expeditionExitTrig, function()
kk.dusteff:playu(GetTriggerUnit())
RemoveUnit(GetTriggerUnit()) end)
TimerStart(NewTimer(), 4.33, true, function()
if not map.manager.activemap then
local unit = utils.unitinrect(gg_rct_expeditionEnter, Player(11), 'nkot')
kk.dusteff:playu(unit)
utils.issmoverect(unit, gg_rct_expeditionExit)
UnitAddAbility(unit, FourCC('Aloc'))
end
end)
-- clear placed preload units:
local pregrp = g:newbyrect(nil, nil, gg_rct_preloadclear)
pregrp:completepurge()
-- visibility:
pvisvis = {}
pvigfog = {}
pvisblk = {}
pvisscr = {}
for pnum = 1,kk.maxplayers do
pvisvis[pnum] = CreateFogModifierRectBJ( true, Player(pnum-1), FOG_OF_WAR_VISIBLE, gg_rct_expeditionVision )
pvisscr[pnum] = CreateFogModifierRectBJ( true, Player(pnum-1), FOG_OF_WAR_VISIBLE, gg_rct_scorebounds )
pvisblk[pnum] = CreateFogModifierRectBJ( true, Player(pnum-1), FOG_OF_WAR_MASKED, bj_mapInitialPlayableArea )
pvigfog[pnum] = CreateFogModifierRectBJ( true, Player(pnum-1), FOG_OF_WAR_FOGGED, bj_mapInitialPlayableArea )
end
-- credits:
kk.creds = CreateQuest()
QuestSetDescription(kk.creds, kk.bakecreds() )
QuestSetTitle(kk.creds, "Credits")
QuestSetIconPath(kk.creds, "ReplaceableTextures\\CommandButtons\\BTNRepair.blp")
QuestSetRequired(kk.creds, false)
kk.badges = CreateQuest()
QuestSetDescription(kk.badges, "Access Badges (J) to view your achievements. This progress is account-wide and will load independent of your character slot." )
QuestSetTitle(kk.badges, "Badges")
QuestSetIconPath(kk.badges, "ReplaceableTextures\\CommandButtons\\BTNMedalionOfCourage.blp")
QuestSetRequired(kk.badges, false)
kk.author = CreateQuest()
QuestSetDescription(kk.author, "User interface, systems, component scripts, and map design by Planetary @hiveworkshop.com.|n|nThis map is unprotected and open source.")
QuestSetTitle(kk.author, "Author")
QuestSetIconPath(kk.author, "war3mapImported\\icon_planetary_author_credit.blp")
QuestSetRequired(kk.author, true)
kk.saving = CreateQuest()
QuestSetDescription(kk.saving, "You can have up to 4 save slots at any given time. Save your current character by opening the Equipment panel ('V') and clicking the save button in the"
.." lower left.|n|nYou can only load from the menu. To load a different save, use the restart map option in the options menu ('F10').|n|n"
.."Save slots will be managed automatically unless you already have 4 files and attempt to save a new character. At that point, a prompt will occur to choose a file to delete."
.." |n|nIf you wish to have more than 4 save slots at once, navigate to your /CustomMapData directory and cut any desired saves into a sub folder of any name. You may wish to"
.." rename moved files to keep track of them. You can then drag and drop files between the main /KoboldKingdom directory and the sub folder when you wish to load different"
.." characters. However, keep in mind that placed file names MUST be 'char1.txt', 'char2.txt', 'char3.txt', or 'char4.txt' in order to properly load."
.."|n|nNote: items stored in the Item Overflow feature at the bottom of your inventory are NOT saved.")
QuestSetTitle(kk.saving, "Save/Load")
QuestSetIconPath(kk.saving, "ReplaceableTextures\\CommandButtons\\BTNTransmute.blp")
QuestSetRequired(kk.saving, true)
StopMusicBJ(false)
-- we want to freeze time but randomseed needs a value, so let time of day cycle:
TimerStart(NewTimer(), 60.0, true, function() SetTimeOfDay(24.00) end)
TimerStart(NewTimer(), 1.0, true, function() StopMusicBJ(false) end) -- 1.33 troubleshooting.
end
function kk.bakecreds()
local str = "Thanks to all those who make great models on The Hive. See official map page @ hiveworkshop.com for asset details."
for asset,name in pairs(kkcreds) do str = str..name.."|n" end
return str
end
function kk.packageinit()
local funcs = {}
-- *note: careful with reordering this:
funcs[#funcs+1] = function() utils.debugfunc(function() utils.init() end, 'utils.init') end
funcs[#funcs+1] = function() utils.debugfunc(function() spell:buildeffects() end, 'buildeffects')end
funcs[#funcs+1] = function() utils.debugfunc(function() g:init() end, 'g') end
funcs[#funcs+1] = function() utils.debugfunc(function() missile:init() end, 'missile') end
funcs[#funcs+1] = function() utils.debugfunc(function() kk.init() end, 'kk.init') end
funcs[#funcs+1] = function() utils.debugfunc(function() trg:init() end, 'trg:init') end
funcs[#funcs+1] = function() utils.debugfunc(function() buffy:init() end, 'buffy:init') end
funcs[#funcs+1] = function() utils.debugfunc(function() spell:init() end, 'spell') end
funcs[#funcs+1] = function() utils.debugfunc(function() sync:init() end, 'sync:init') end
funcs[#funcs+1] = function() utils.debugfunc(function() kobold:init() end, 'kobold') end
funcs[#funcs+1] = function() utils.debugfunc(function() dmg:init() end, 'dmg') end
funcs[#funcs+1] = function() utils.debugfunc(function() ability:init() end, 'ability') end
funcs[#funcs+1] = function() utils.debugfunc(function() mastery:init() end, 'mastery') end
funcs[#funcs+1] = function() utils.debugfunc(function() candle:init() end, 'candle') end
funcs[#funcs+1] = function() utils.debugfunc(function() tooltip:init() end, 'tooltip') end
funcs[#funcs+1] = function() utils.debugfunc(function() terrain:init() end, 'terrain') end
funcs[#funcs+1] = function() utils.debugfunc(function() map:init() end, 'map') end
funcs[#funcs+1] = function() utils.debugfunc(function() shrine:init() end, 'shrine') end
funcs[#funcs+1] = function() utils.debugfunc(function() kui:init() end, 'kui') end -- hotkey:init() runs here.
for i = 1,#funcs do
funcs[i]()
end
funcs = nil
end
---A simple sync mechanism for using a true randomseed based on os.time().
---This should be called once on init as it is destroyed to free up global namespace.
---Requires: DetectPlayers
function generate_global_randomseed() ---@type function|nil
local t = CreateTrigger()
AllCurrentPlayers(function(p)
BlzTriggerRegisterPlayerSyncEvent(t, p, "seed", false)
end)
TriggerAddAction(t, function()
math.randomseed(math.floor(tonumber(BlzGetTriggerSyncData())))
DestroyTrigger(t)
end)
for id = 0,bj_MAX_PLAYER_SLOTS do
-- finds the first player in the game:
if table.has_value(DetectPlayers.player_user_table, id) then
if GetLocalPlayer() == Player(id) and os and os.time() then
BlzSendSyncData("seed", tostring(os.time()*2560))
else
BlzSendSyncData("seed", tostring(GetTimeOfDay()*2560*2560))
end
break
end
end
generate_global_randomseed = nil
end
TimerStart(NewTimer(),0.0,false,function()
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- elapsed init for things that require it.
kui.gameui = BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0)
kui.chatlog = BlzFrameGetChild(kui.gameui, 7)
BlzFrameSetLevel(kui.chatlog, 6)
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- init global sounds
-- play main panel sounds easily with kui.sound.panel[id]
kui.sound = {
closebtn = gg_snd_BigButtonClick,
clickstr = gg_snd_MouseClick1,
clicksft = gg_snd_MouseClick2,
hoversft = gg_snd_OverViewSelectionChange1,
hoverbtn = gg_snd_PickUpItem,
digsel = gg_snd_Switch,
digstart = gg_snd_BattleNetDoorsStereo2,
digend = gg_snd_audio_loading_complete,
splashclk = gg_snd_QuestActivateWhat1,
char01new = gg_snd_audio_tunl_finder_looook_new_shinies,
char02new = gg_snd_audio_geom_fungal_mushroom_power,
char03new = gg_snd_audio_scout_knight_onward,
char04new = gg_snd_audio_wickf_kobold_smash,
loadend = gg_snd_QuestNew,
itemsel = gg_snd_audio_item_selected,
selnode = gg_snd_audio_mastery_select_node,
runenode = gg_snd_FarseerMissile,
scorebcard = gg_snd_audio_mastery_open,
complete = gg_snd_QuestCompleted,
objstep = gg_snd_GoodJob,
failure = gg_snd_QuestFailed,
completebig = gg_snd_NewTournament,
error = gg_snd_Error,
sell = gg_snd_AlchemistTransmuteDeath1,
darkwarning = gg_snd_GraveYardWhat1,
menumusic = gg_snd_audio_music_main_menu_kobolds_catacombs_gather,
bossmusic = nil,
caveambient = nil,
bossdefeat = gg_snd_boss_defeated_cheer,
mumble = {
[1] = gg_snd_mumbling_1,
[2] = gg_snd_mumbling_2,
[3] = gg_snd_mumbling_3,
[4] = gg_snd_mumbling_4,
[5] = gg_snd_mumbling_direct,
[6] = gg_snd_mumbling_positive,
},
goldcoins = gg_snd_forge_item_shinykeeper,
eleforge = gg_snd_forge_item_elementalist,
questdone = gg_snd_wow_quest_complete,
queststart = gg_snd_wow_quest_start,
tutorialpop = gg_snd_tutorial_popup,
panel = { -- panelid
[1] = { -- char
close = gg_snd_audio_char_page_close,
open = gg_snd_audio_char_page_open,
},
[2] = { -- equip
close = gg_snd_audio_equipment_close,
open = gg_snd_audio_equipment_open,
},
[3] = { -- inv
close = gg_snd_audio_inventory_close,
open = gg_snd_audio_inventory_open,
},
[4] = { -- dig site map
close = gg_snd_audio_map_open_close,
open = gg_snd_audio_char_page_close,
},
[5] = { -- mastery
close = gg_snd_audio_abilities_panel_close,
open = gg_snd_audio_abilities_panel_open,
},
[6] = { -- abilities
close = gg_snd_audio_abilities_panel_close,
open = gg_snd_audio_abilities_panel_open,
},
[7] = { -- badges
close = gg_snd_audio_abilities_panel_close,
open = gg_snd_audio_abilities_panel_open,
},
},
boss = {
['ultimate'] = gg_snd_boss_ability_warning,
['N01B'] = { -- slag king
intro = gg_snd_boss_slag_king_intro,
basic = gg_snd_boss_slag_king_spell_basic,
power = gg_snd_boss_slag_king_spell_power,
channel = gg_snd_boss_slag_king_spell_channel,
},
['N01F'] = { -- marsh mutant
intro = gg_snd_boss_marsh_mutant_intro,
basic = gg_snd_boss_marsh_mutant_cast,
power = gg_snd_boss_marsh_mutant_cast,
channel = gg_snd_boss_marsh_mutant_cast,
},
['N01H'] = { -- megachomp
intro = gg_snd_boss_megachomp_ultimate,
basic = gg_snd_boss_megachomp_basic,
power = gg_snd_boss_megachomp_power,
channel = gg_snd_boss_megachomp_basic,
},
['N01J'] = { -- thawed experiment
intro = gg_snd_boss_thawed_experiment_power,
basic = gg_snd_boss_thawed_experiment_basic,
power = gg_snd_boss_thawed_experiment_basic,
channel = gg_snd_boss_thawed_experiment_channel,
},
['N01R'] = { -- amalgam of greed
intro = gg_snd_boss_amalgam_greed_intro,
basic = gg_snd_boss_amalgam_greed_basic,
power = gg_snd_boss_amalgam_greed_basic,
channel = gg_snd_boss_amalgam_greed_channel,
},
},
drinkpot = gg_snd_audio_potion_drink,
openchest = gg_snd_boss_chest_open,
hoverchest = gg_snd_boss_chest_hover,
bossintro = gg_snd_boss_intro,
shrine = gg_snd_audio_shrine_click,
footsteps = {
[1] = gg_snd_audio_foostep_1, [2] = gg_snd_audio_foostep_2, [3] = gg_snd_audio_foostep_3, [4] = gg_snd_audio_foostep_4,
[5] = gg_snd_audio_foostep_5, [6] = gg_snd_audio_foostep_6, [7] = gg_snd_audio_foostep_7, [8] = gg_snd_audio_foostep_8
},
portalmerge = gg_snd_BarrelExplosion2,
reliccraft = gg_snd_audio_relic_craft,
}
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- begin UI build
if not BlzLoadTOCFile('war3mapImported\\CustomFrames.toc') then print("error: main .fdf file failed to load") end
utils.debugfunc(function() initprojects() end, "initprojects")
utils.debugfunc(function() tooltip:bake() end, "tooltip:bake")
utils.debugfunc(function() kui:uigen() end, "kui:uigen")
utils.debugfunc(function() boss:init() end, "boss:init")
utils.debugfunc(function() loot:init() end, "loot:init")
utils.debugfunc(function() hotkey:init() end, "hotkey:init")
utils.debugfunc(function() hotkey:assignkuipanels() end, "hotkey:assignkuipanels")
utils.debugfunc(function() kui:hidecmdbtns() end, "kui:hidecmdbtns")
utils.debugfunc(function() creature:init() end, "creature")
utils.debugfunc(function() elite:init() end, "elite")
utils.debugfunc(function() speffect:preload() end, "speffect:preload")
utils.debugfunc(function() alert:init() end, "alert:init")
utils.debugfunc(function() shop:init() end, "shop:init")
utils.debugfunc(function() screenplay:build() end, "screenplay:build")
utils.debugfunc(function() quest:init() end, "quest:init")
-- scan for files and update character slots on main screen:
utils.timed(0.33, function()
utils.playerloop(function(p)
utils.debugfunc(function()
kobold.player[p].char = char:new(p)
if kobold.player[p]:islocal() then
kobold.player[p].char:validate_fileslots()
end
end, "file validation")
end)
end)
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- unblock screen after loading complete:
utils.fadeblack(false) -- ***note: async; cannot run locally!
-- misc.
StopSound(bj_nightAmbientSound, true, true)
StopSound(bj_dayAmbientSound, true, true)
VolumeGroupSetVolumeBJ( SOUND_VOLUMEGROUP_AMBIENTSOUNDS, 15 )
utils.playsoundall(kui.sound.menumusic) -- init menu music.
BlzChangeMinimapTerrainTex('war3mapImported\\minimap-hub.blp')
utils.timed(1.0, function()
if not BlzLoadTOCFile('war3mapImported\\DialogueFrameTOC.toc') then print("error: speak .fdf file failed to load") end
speak.consolebd = kui.consolebd
speak.gameui = kui.canvas.fh
speak.customgameui = kui.canvas.game.fh
utils.debugfunc(function() speak:initframes() end, "speak:initframes")
end)
-- not sync-safe; fix editor's fixed seed for test map:
generate_global_randomseed()
end)
g = {}
function g:init()
self.debug = false
self.__index = self
self.stack = {}
self.temp = false
self.tempg = CreateGroup()
g_type_none = 0
g_type_dmg = 1
g_type_heal = 2
g_type_ally = 3
g_type_buff = 4
g_type_debuff = 5
end
function g:new(p, gtype)
local o = {}
setmetatable(o, self)
o.size = 0 -- group size when loooping.
o.loop = 0 -- index position when looping.
o.grp = CreateGroup()
o.p = p or nil
if gtype then
o:settype(gtype)
else
o.type = g_type_none
end
g.stack[o] = o
if g.debug then print(utils.tablelength(self.stack)) end
return o
end
function g:newbyrect(p, gtype, rect)
local o = g:new(p, gtype)
if o.filter then
GroupEnumUnitsInRect(o.grp, rect, Filter(function() return o.filter() end))
else
GroupEnumUnitsInRect(o.grp, rect, nil)
end
return o
end
function g:newbyunitloc(p, gtype, unit, r)
local o = g:new(p, gtype)
if o.filter then
GroupEnumUnitsInRange(o.grp, GetUnitX(unit), GetUnitY(unit), r, Filter(function() return o.filter() end))
else
GroupEnumUnitsInRange(o.grp, GetUnitX(unit), GetUnitY(unit), r, Filter(function() return true end))
end
return o
end
function g:newbyxy(p, gtype, x, y, r)
local o = g:new(p, gtype)
if o.filter then
GroupEnumUnitsInRange(o.grp, x, y, r, Filter(function() return o.filter() end))
else
GroupEnumUnitsInRange(o.grp, x, y, r, Filter(function() return true end))
end
return o
end
function g:newbyrandom(count, sourcegrp)
local grp = g:new()
DestroyGroup(grp.grp)
grp.grp = GetRandomSubGroup(count, sourcegrp.grp)
return grp
end
function g:newbytable(t)
local o = g:new()
for _,unit in ipairs(t) do
o:add(unit)
end
return o
end
-- @typestr = "dmg" for only enemies, "heal" or "ally" for only allies, "none" for no filter.
function g:settype(gtype)
self.type = gtype
if self.type == g_type_dmg then
self.filter = function() return g:fenemy(self.p) end
elseif self.type == g_type_heal then
self.filter = function() return g:fally(self.p) end
elseif self.type == g_type_ally then
self.filter = function() return g:fally(self.p) end
elseif self.type == g_type_buff then
self.filter = function() return g:fally(self.p) end
elseif self.type == g_type_debuff then
self.filter = function() return g:fenemy(self.p) end
elseif self.type == g_type_none then
self.filter = function() return true end
end
end
function g:action(func)
self:sort()
self.brk = false -- passable flag to end loop.
self.size = self:getsize()
if self.size > 0 then
for i = 0, self.size-1 do
if not self.brk then
self.unit = BlzGroupUnitAt(self.grp, i)
self.loop = i
func() -- run action on self.unit
else
break
end
end
end
return self
end
function g:sort()
self.size = self:getsize()
BlzGroupAddGroupFast(self.grp, self.tempg)
GroupClear(self.grp)
BlzGroupAddGroupFast(self.tempg, self.grp)
GroupClear(self.tempg)
end
function g:attackmove(x, y)
self:action(function()
utils.issatkxy(self.unit, x, y)
end)
end
function g:add(unit)
GroupAddUnit(self.grp, unit)
end
function g:remove(unit)
GroupRemoveUnit(self.grp, unit)
end
function g:clear()
self:action(function()
self:remove(self.unit)
end)
end
function g:destroy()
DestroyGroup(self.grp)
if self.stack[self] then self.stack[self] = nil end
self.grp = nil
utils.tablecollapse(self.stack)
if g.debug then print(utils.tablelength(self.stack)) end
end
function g:indexunit(x)
return BlzGroupUnitAt(self.grp, x)
end
function g:verifyalive()
utils.debugfunc(function()
self:action(function()
if self.unit and (IsUnitType(self.unit, UNIT_TYPE_DEAD) or GetUnitTypeId(self.unit) == 0) then
self:remove(self.unit)
end
end)
if self.temp and self:getsize() == 0 then
self:destroy()
end
end, "verifyalive")
end
function g:getsize()
return BlzGroupGetSize(self.grp)
end
function g:completepurge()
self:action(function()
if not (utils.pnum(utils.powner(self.unit)) < kk.maxplayers+1
and IsUnitType(self.unit, UNIT_TYPE_HERO)) and not GetUnitTypeId(self.unit) ~= buffy.dummyid then
if map.mission.cache and map.mission.cache.boss and GetUnitTypeId(self.unit) == FourCC(map.mission.cache.boss.bossid) then
if map.manager.success then
-- do nothing to preserve death of unit.
else
-- player failed, allow boss removal during map cleanup.
RemoveUnit(self.unit)
end
else
RemoveUnit(self.unit)
end
end
end)
self:destroy()
end
function g:hasunit(unit)
bool = false
self:action(function()
if self.unit == unit then
self.brk = true
bool = true
end
end)
return bool
end
function g:e()
return GetEnumUnit()
end
-- @return filter unit.
function g:f()
return GetFilterUnit()
end
-- @p = does this player own the filter unit.
function g:fp(p)
return p == GetOwningPlayer(GetFilterUnit())
end
-- @p = is filter unit an enemy of this player.
function g:fenemy(p)
return IsPlayerEnemy(p, GetOwningPlayer(GetFilterUnit())) and utils.isalive(GetFilterUnit())
end
-- @p = is filter unit an ally of this player.
function g:fally(p)
return IsPlayerAlly(p, GetOwningPlayer(GetFilterUnit())) and utils.isalive(GetFilterUnit()) and utils.powner(GetFilterUnit()) ~= Player(25)
end
-- @effect = play this effect on every unit in the group.
function g:playeffect(speffect)
self:action(function()
if utils.isalive(self.unit) then
speffect:playu(self.unit)
end
end)
end
hotkey = {}
hotkey.map = {} -- sub class for custom 1-4 hotkeys.
-- *note: this must be done after kui:uigen().
function hotkey:init()
hotkey.map:init()
self.hotkeyc = color.ui.hotkey
self.trigger = CreateTrigger()
-- self.movetrig = CreateTrigger()
self.movetimer = {}
self.timeout = {} -- only allow events every x sec to prevent spam.
self.timeoutdur = 0.03
self.closeall = OSKEY_ESCAPE -- btn used to close all visible tabs.
self.masterykeys = {
[OSKEY_1] = "Z",
[OSKEY_2] = "X",
[OSKEY_3] = "T",
[OSKEY_4] = "Y",
}
self.panel = {
char = { str = "C", key = OSKEY_C, tar = kui.canvas.game.char }, -- panelid: 1
equip = { str = "V", key = OSKEY_V, tar = kui.canvas.game.equip }, -- panelid: 2
inv = { str = "B", key = OSKEY_B, tar = kui.canvas.game.inv }, -- panelid: 3
dig = { str = "Tab", key = OSKEY_TAB, tar = kui.canvas.game.dig }, -- panelid: 4
mast = { str = "N", key = OSKEY_N, tar = kui.canvas.game.mast }, -- panelid: 5
abil = { str = "K", key = OSKEY_K, tar = kui.canvas.game.abil }, -- panelid: 6
badg = { str = "J", key = OSKEY_J, tar = kui.canvas.game.badge }, -- panelid: 7
}
--------------------------------------
for panel,kt in pairs(self.panel) do kt.str = color:wrap(self.hotkeyc, kt.str) end
--self:createclicktomove()
end
-- *note: this must be done after kui and kk packages init.
function hotkey:assignkuipanels()
utils.playerloop(function(p)
local p = p
if kobold.player[p] then
self.timeout[p] = false
for panelname,ktab in pairs(self.panel) do
local func = function()
if not map.manager.loading then
if utils.islocaltrigp() then
if not ktab.tar:isvisible() and not scoreboard_is_active then
ktab.tar:show()
if ktab.tar.alerticon then
-- hide the eyecandy alert for this panel button.
ktab.tar.alerticon:hide()
end
else
-- we have to do a hacky fix to make sure opening panels refreshes selected slot:
if panelname == "inv" and kobold.player[p].selslotid and kobold.player[p].selslotid < 1000 then
ktab.tar:hide(function() kobold.player[p].selslotid = nil end)
elseif panelname == "equip" and kobold.player[p].selslotid and kobold.player[p].selslotid > 1000 then
ktab.tar:hide(function() kobold.player[p].selslotid = nil end)
elseif panelname == "dig" and kobold.player[p].selslotid and kobold.player[p].selslotid == 1011 then
ktab.tar:hide(function() kobold.player[p].selslotid = nil end)
else
ktab.tar:hide()
end
end
end
end
end
hotkey:regkeyevent(p, ktab.key, func, true, true)
end
local escfunc = function()
if utils.islocaltrigp() then
shop.fr:hide()
shop.gwfr:hide()
for panelname,ktab in pairs(self.panel) do
if ktab.tar:isvisible() then ktab.tar:hide(function() kobold.player[utils.trigp()].selslotid = nil end) end
end
end
end
hotkey:regkeyevent(p, self.closeall, escfunc, true, false)
end
end)
end
-- @istimeout,@ondown, and @meta are optional and have defaults.
function hotkey:regkeyevent(p, oskey, func, islocal, istimeout, ondown, meta)
-- create an OSKEY event for a player.
local meta = meta or 0
local ondown = ondown or true
local istimeout = istimeout or true
BlzTriggerRegisterPlayerKeyEvent(self.trigger, p, oskey, meta, ondown)
local action = TriggerAddAction(self.trigger, function()
if not self.timeout[p] and hotkey:iskey(p, oskey) then
if islocal and p == utils.localp() then
func()
else
func()
end
end
if istimeout then self:timeout(p) end
end)
return action
end
-- @p = for this player
-- @typestr = "down", "up" or "move"
-- @localfunc = local player function
-- @netfunc = net-safe function
-- @leftorright = 0 for left, 1 for right
function hotkey:regmouseevent(p, typestr, localfunc, netfunc, leftorright)
local meta
if typestr == "down" then
TriggerRegisterPlayerEvent(self.movetrig, p, EVENT_PLAYER_MOUSE_DOWN)
meta = 0
elseif typestr == "up" then
TriggerRegisterPlayerEvent(self.movetrig, p, EVENT_PLAYER_MOUSE_UP)
elseif typestr == "move" then
TriggerRegisterPlayerEvent(self.movetrig, p, EVENT_PLAYER_MOUSE_MOVE)
else
print("error: hotkey:regmouseevent 'typestr' is not valid.")
end
if typestr ~= "move" then
TriggerAddAction(self.movetrig, function()
if hotkey:ismousebtn(leftorright) then
if netfunc then
netfunc()
end
if p == utils.trigp() then
localfunc()
end
end
end)
else
TriggerAddAction(self.movetrig, function()
netfunc()
end)
end
end
function hotkey:timeout(p)
utils.booltimer(self.timeout[p], self.timeoutdur, false)
end
function hotkey:iskey(p, oskey)
if p == utils.localp() then
if BlzGetTriggerPlayerKey() == oskey then
return true
else
return false
end
end
end
function hotkey:ismousebtn(leftorright)
if leftorright == 0 then
return BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_LEFT
elseif leftorright == 1 then
return BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_RIGHT
else
print("error: ismousebtn 'leftorright' should be 0 or 1")
end
end
function hotkey:createclicktomove()
click_atk_trig = {}
click_cast_trig = {}
click_move_disabled = {}
click_move_bool = {}
click_move_x = {}
click_move_y = {}
click_move_tempx = {}
click_move_tempy = {}
click_move_tempa = {}
click_move_prevx = {}
click_move_prevy = {}
click_move_unit = {}
click_move_tmr = NewTimer()
click_move_uptrig = CreateTrigger()
click_move_dntrig = CreateTrigger()
click_move_mvtrig = CreateTrigger()
pause_click_to_move = function(p)
click_move_disabled[utils.pnum(p)] = true
if kobold.player[p].clickdistmr then ReleaseTimer(kobold.player[p].clickdistmr) kobold.player[p].clickdistmr = nil end
kobold.player[p].clickdistmr = utils.timed(0.18, function()
click_move_disabled[utils.pnum(p)] = false
end)
end
for pnum = 1,kk.maxplayers do
local p, pnum = Player(pnum-1), pnum
if kobold.player[p] then
click_move_bool[pnum] = false
-- add each player to event listener:
TriggerRegisterPlayerEvent(click_move_dntrig, p, EVENT_PLAYER_MOUSE_DOWN)
TriggerRegisterPlayerEvent(click_move_uptrig, p, EVENT_PLAYER_MOUSE_UP)
TriggerRegisterPlayerEvent(click_move_mvtrig, p, EVENT_PLAYER_MOUSE_MOVE)
click_move_unit[pnum] = function() return kobold.player[Player(pnum-1)].unit end
end
-- click to move spell pause helper:
click_cast_trig[pnum] = trg:new("startcast", Player(pnum-1))
click_cast_trig[pnum]:regaction(function()
pause_click_to_move(utils.unitp())
end)
-- click to move attack pause helper:
click_atk_trig[pnum] = trg:new("order", Player(pnum-1))
click_atk_trig[pnum]:regaction(function()
if GetIssuedOrderId() == 851971 or GetIssuedOrderId() == 851983 or GetIssuedOrderId() == 851985
or GetIssuedOrderId() == 851988 then
pause_click_to_move(utils.unitp())
end
end)
end
-- listen for player events and set arrays:
local mousedn = function()
if hotkey:ismousebtn(1) then
local pnum = utils.trigpnum()
click_move_bool[pnum] = true
click_move_x[pnum] = BlzGetTriggerPlayerMouseX()
click_move_y[pnum] = BlzGetTriggerPlayerMouseY()
click_move_prevx[pnum] = BlzGetTriggerPlayerMouseX()
click_move_prevy[pnum] = BlzGetTriggerPlayerMouseY()
click_move_tempa[pnum] = utils.anglexy(utils.unitx(click_move_unit[pnum]()), utils.unity(click_move_unit[pnum]()),
click_move_prevx[pnum], click_move_prevy[pnum])
end
end
local mouseup = function()
if hotkey:ismousebtn(1) then
click_move_bool[utils.trigpnum()] = false
local pnum = utils.trigpnum()
-- to prevent our hold-down fix from overriding on slight movement after release, issue a move if distance is greater than fix range:
if not IsTerrainPathable(click_move_x[pnum], click_move_y[pnum], PATHING_TYPE_WALKABILITY)
and utils.distxy(click_move_x[pnum], click_move_y[pnum], utils.unitx(click_move_unit[pnum]()), utils.unity(click_move_unit[pnum]()))
> BlzGetUnitRealField(click_move_unit[pnum](), UNIT_RF_SPEED) then
if not click_move_disabled[pnum] then
utils.issmovexy(kobold.player[Player(pnum-1)].unit, click_move_x[pnum], click_move_y[pnum])
end
end
end
end
local mousemv = function()
if click_move_bool[utils.trigpnum()] then
local pnum = utils.trigpnum()
if click_move_disabled[pnum] and kobold.player[Player(pnum-1)].clickdistmr then
if utils.distxy(click_move_prevx[pnum], click_move_prevy[pnum], click_move_x[pnum], click_move_y[pnum]) > 40.0 then
ReleaseTimer(kobold.player[Player(pnum-1)].clickdistmr)
kobold.player[Player(pnum-1)].clickdistmr = nil
click_move_disabled[pnum] = false
SetUnitFacingTimed(click_move_unit[pnum](),
utils.anglexy(click_move_prevx[pnum], click_move_prevy[pnum], click_move_x[pnum], click_move_y[pnum]), 0.0)
end
end
click_move_x[pnum] = BlzGetTriggerPlayerMouseX()
click_move_y[pnum] = BlzGetTriggerPlayerMouseY()
click_move_tempa[pnum] = utils.anglexy(utils.unitx(click_move_unit[pnum]()), utils.unity(click_move_unit[pnum]()),
click_move_x[pnum], click_move_y[pnum])
end
end
TriggerAddAction(click_move_dntrig, mousedn)
TriggerAddAction(click_move_uptrig, mouseup)
TriggerAddAction(click_move_mvtrig, mousemv)
-- move hero when click arrays are valid:
TimerStart(click_move_tmr, 0.06, true, function()
utils.debugfunc(function()
for pnum = 1,kk.maxplayers do
if kobold.player[Player(pnum-1)].unit and utils.isalive(kobold.player[Player(pnum-1)].unit)then
if click_move_bool[pnum] and not click_move_disabled[pnum] and not kobold.player[Player(pnum-1)].downed then
-- check if minimum distance exceeded:
if utils.distxy(click_move_x[pnum], click_move_y[pnum], utils.unitxy(kobold.player[Player(pnum-1)].unit)) > 40.0 then
-- check for mouse held down on timer update:
if utils.distxy(click_move_x[pnum], click_move_y[pnum], click_move_prevx[pnum], click_move_prevy[pnum]) < 25.0 then
-- mouse is held down:
click_move_x[pnum], click_move_y[pnum] = BlzGetTriggerPlayerMouseX(), BlzGetTriggerPlayerMouseY()
local click_held_dist = utils.distxy(click_move_x[pnum], click_move_y[pnum], utils.unitxy(click_move_unit[pnum]()))
click_move_tempx[pnum], click_move_tempy[pnum] = utils.unitxy(click_move_unit[pnum]())
click_move_tempx[pnum], click_move_tempy[pnum] = utils.projectxy(click_move_tempx[pnum], click_move_tempy[pnum],
click_held_dist, click_move_tempa[pnum])
if click_held_dist > 80.0 then
if click_move_tempx[pnum] ~= 0.0 and click_move_tempy[pnum] ~= 0.0
and not IsTerrainPathable(click_move_tempx[pnum], click_move_tempy[pnum], PATHING_TYPE_WALKABILITY) then
utils.issmovexy(kobold.player[Player(pnum-1)].unit, click_move_tempx[pnum], click_move_tempy[pnum])
pause_click_to_move(Player(pnum-1))
end
end
-- standard on mouse move:
elseif click_move_x[pnum] ~= 0.0 and click_move_y[pnum] ~= 0.0
and not IsTerrainPathable(click_move_x[pnum], click_move_y[pnum], PATHING_TYPE_WALKABILITY) then
-- only issue a new command if updated to a new coordinate
if utils.distxy(click_move_prevx[pnum], click_move_prevy[pnum], click_move_x[pnum], click_move_y[pnum]) > 40.0 then
utils.issmovexy(kobold.player[Player(pnum-1)].unit, click_move_x[pnum], click_move_y[pnum])
pause_click_to_move(Player(pnum-1))
end
end
-- set previous values:
click_move_prevx[pnum], click_move_prevy[pnum] = click_move_x[pnum], click_move_y[pnum]
end
end
end
end
end, "click move")
end)
end
function hotkey.map:init()
self.__index = self
self.codet = {
[OSKEY_1] = {
[casttype_point] = FourCC('A02Z'),
[casttype_unit] = FourCC('A02Y'),
[casttype_instant] = FourCC('A030'),
},
[OSKEY_2] = {
[casttype_point] = FourCC('A032'),
[casttype_unit] = FourCC('A031'),
[casttype_instant] = FourCC('A033'),
},
[OSKEY_3] = {
[casttype_point] = FourCC('A035'),
[casttype_unit] = FourCC('A034'),
[casttype_instant] = FourCC('A036'),
},
[OSKEY_4] = {
[casttype_point] = FourCC('A038'),
[casttype_unit] = FourCC('A037'),
[casttype_instant] = FourCC('A039'),
},
}
self.ordert = {
[OSKEY_1] = 'ward',
[OSKEY_2] = 'vengeance',
[OSKEY_3] = 'forceofnature',
[OSKEY_4] = 'web',
}
self.intmap = {
[5] = OSKEY_1,
[6] = OSKEY_2,
[7] = OSKEY_3,
[8] = OSKEY_4,
}
end
function hotkey.map:new(p)
local o = setmetatable({}, self)
o.code = {} -- mapped ability code to cast.
o.orderstr = {} -- `` orderstr for spell dummy.
o.action = {} -- stored hotkey funcs.
o.p = p
for oskey,target in pairs(hotkey.masterykeys) do
o.action[oskey] = hotkey:regkeyevent(p, oskey, function()
ForceUIKey(target)
end, true, false)
end
o:mapabilkeys()
return o
end
function hotkey.map:getplayerunit()
return kobold.player[self.p].unit
end
function hotkey.map:updatekey(oskey, abilcode, casttype, orderstr)
self:clearkey(oskey, casttype)
self.code[oskey] = abilcode -- keep after clear key.
self.orderstr[oskey] = orderstr
UnitAddAbility(self:getplayerunit(), self.codet[oskey][casttype])
return self.codet[oskey][casttype]
end
function hotkey.map:clearkey(oskey, casttype)
self.code[oskey] = nil
for casttype,dummyabil in pairs(self.codet[oskey]) do
if BlzGetUnitAbility(self:getplayerunit(), dummyabil) then
UnitRemoveAbility(self:getplayerunit(), dummyabil)
end
end
end
function hotkey.map:mapabilkeys()
self.trig = trg:new("spell", self.p)
self.trig:regaction(function()
for oskey,t in pairs(self.codet) do
for casttype,dummyabil in pairs(t) do
-- note: trg package controls owning player.
if GetSpellAbilityId() == dummyabil then
if casttype == casttype_instant then
if not buffy:gen_cast_instant(
self:getplayerunit(), self.code[oskey], self.orderstr[oskey])
then
buffy:gen_reset(self:getplayerunit(), dummyabil)
end
elseif casttype == casttype_point then
if not buffy:gen_cast_point(
self:getplayerunit(), self.code[oskey], self.orderstr[oskey], GetSpellTargetX(), GetSpellTargetY())
then
buffy:gen_reset(self:getplayerunit(), dummyabil)
end
elseif casttype == casttype_unit then
if not buffy:gen_cast_unit(
self:getplayerunit(), self.code[oskey], self.orderstr[oskey], GetSpellTargetUnit())
then
buffy:gen_reset(self:getplayerunit(), dummyabil)
end
end
break
end
end
end
end)
end
loot = {} -- item management class.
loot.itemtype = {} -- item base type sub class.
loot.item = {} -- item object sub class.
loot.slottype = {} -- slot type sub class.
loot.kw = {} -- item attribute sub class.
loot.rarity = {} -- item rarity sub class.
loot.statmod = {} -- player modification sub class.
loot.box = {} -- treasure group sub class (decides which items are possible on roll).
loot.ancient = {} -- wrapper for dealing with ancient effects and metadata.
loot.ancient.allancients = {} -- global ancient rolls from any biome.
function loot:init()
global_statmod_multiplier = 2.25 -- makes all statmods more potent as player level increase.
self.debug = false -- print debug messages.
self.invmax = 42 -- total inv slots.
-- class init:
loot.itemtype:init()
loot.item:init()
loot.kw:init()
loot.statmod:init()
loot.rarity:init()
-- lootdata functions:
loot:globals()
loot:buildtables()
loot.missile = {
[rarity_common] = 'I00B',
[rarity_rare] = 'I00C',
[rarity_epic] = 'I00D',
[rarity_ancient] = 'I00E',
}
loot.pickup = {
[FourCC("I00B")] = rarity_common,
[FourCC("I00C")] = rarity_rare,
[FourCC("I00D")] = rarity_epic,
[FourCC("I00E")] = rarity_ancient,
}
end
function loot:isbagfull(p)
-- if player's bag is full, return true.
for slotid = 1,loot.invmax do
if kobold.player[p].items[slotid] == nil then
return false
end
end
return true
end
function loot:getfirstempty(p)
for slotid = 1,loot.invmax do
if kobold.player[p].items[slotid] == nil then
return slotid
end
end
return false
end
function loot:inventorysellall(p)
local founditem = false
if kobold.player[p].sellallclk then
for slotid = 1,42 do
if kobold.player[p].items[slotid] and kobold.player[p].items[slotid].itemtype.slottype.slotid ~= slot_digkey and not kobold.player[p].items[slotid].ancientid then
kobold.player[p].items[slotid]:sell(p, true, slotid)
founditem = true
end
end
loot:cleanupempty(p)
else
kobold.player[p].sellallclk = true
utils.timed(0.42, function() kobold.player[p].sellallclk = nil end)
end
if founditem then
utils.playsound(kui.sound.sell, p)
kui.canvas.game.equip.pane:hide()
kui.canvas.game.inv.pane:hide()
kobold.player[p].selslotid = nil
end
end
function loot:unequipall(p)
if kobold.player[p].unequipallclk then
for slotid = 1001,1010 do
if kobold.player[p].items[slotid] then
loot.item:unequip(p, slotid)
end
end
utils.playsound(kui.sound.itemsel, p)
kui.canvas.game.equip.pane:hide()
kui.canvas.game.inv.pane:hide()
kobold.player[p].selslotid = nil
else
kobold.player[p].unequipallclk = true
utils.timed(0.42, function() kobold.player[p].unequipallclk = nil end)
end
end
function loot:inventorysortall(p)
if not kobold.player[p].pausesort then
orderedt = {}
kobold.player[p].pausesort = true
for slotid = 1,42 do
if kobold.player[p].items[slotid] then
orderedt[#orderedt+1] = kobold.player[p].items[slotid]
end
end
table.sort(orderedt, function(a, b) return a.itemtype.slottype.slotid < b.itemtype.slottype.slotid end)
loot:clearitems(p) -- remove existing items
for slotid = 1,42 do
-- replace slots with the new ordering:
if orderedt[slotid] then
kobold.player[p].items[slotid] = nil
kobold.player[p].items[slotid] = orderedt[slotid]
end
end
loot:cleanupempty(p) -- refresh new frame icons etc.
utils.playsound(kui.sound.itemsel, p)
orderedt = nil
kui.canvas.game.equip.pane:hide()
kui.canvas.game.inv.pane:hide()
kobold.player[p].selslotid = nil
utils.timed(1.0, function() kobold.player[p].pausesort = false end)
else
utils.palert(p, "Sorting too fast!")
end
loot:updateallrarityicons(p)
end
function loot:clearitems(p)
for slotid = 1,42 do
kobold.player[p].items[slotid] = nil
end
loot:cleanupempty(p)
end
function loot:cleanupempty(p)
-- searches for nil item slots and resets any
-- lingering icons.
for slotid = 1,loot.invmax do
if kobold.player[p].items[slotid] == nil then
if p == utils.localp() then
kui.canvas.game.inv.space[slotid].icon:setbgtex(kui.tex.invis)
end
else
kui.canvas.game.inv.space[slotid].icon:setbgtex(kobold.player[p].items[slotid].itemtype.icon)
end
end
loot:updateallrarityicons(p)
end
function loot:updateallrarityicons(p)
local pobj = kobold.player[p]
if pobj:islocal() then
for slotid = 1,42 do
if pobj.items[slotid] and pobj.items[slotid].slottype.slotid ~= slot_digkey then
kui.canvas.game.inv.space[slotid].rareind:setbgtex(pobj.items[slotid].rarity.rareind)
else
kui.canvas.game.inv.space[slotid].rareind:setbgtex(kui.tex.invis)
end
end
for slotid = 1001,1010 do
if pobj.items[slotid] then
kui.canvas.game.equip.space[slotid].rareind:setbgtex(pobj.items[slotid].rarity.rareind)
else
kui.canvas.game.equip.space[slotid].rareind:setbgtex(kui.tex.invis)
end
end
end
pobj = nil
end
-- @p = for this player.
-- @level = the item level to determine stats.
-- @slotid = equipment piece to generate.
-- @rarityid= force this rarity.
-- @_iscrafted = [optional] default false; does custom things for shinykeeper purchases.
function loot:generate(p, level, slotid, rarityid, _iscrafted)
-- wrapper for generating an item.
if not loot:isbagfull(p) then
local level = math.max(1,level) -- level cannot be below 1.
local newitem = self.item:new(loot:getitemtypebylvl(slotid, level), p)
local rarityid = rarityid or loot:getrandomrarity(rarity_common, rarity_epic, kobold.player[p][p_stat_treasure]*100)
newitem.rarity = loot.raritytable[rarityid]
newitem.level = level
newitem.sellsfor = math.ceil(math.random(newitem.level^0.90,newitem.level^1.05)*newitem.rarity.sellfactor)
if slotid == slot_potion or slotid == slot_artifact or slotid == slot_backpack or slotid == slot_tool or slotid == slot_candle then
newitem.sellsfor = math.floor(newitem.sellsfor*1.15)
end
-- based on rarity, pick affixes to roll:
if not _iscrafted then
-- item dropped in world:
if rarityid == rarity_common then
if level < 10 then -- under level 10, commons sometimes only have 1 primary.
newitem:addrandomaffix(math.random(1,2))
elseif level < 30 then -- after 10, allow 2 primaries on commons.
newitem:addrandomaffix(2)
elseif level >= 30 and level < 40 then -- after 30, allow 2 primaries and a secondary on commons.
newitem:addsecondary()
newitem:addrandomaffix(2)
else -- at level 40, allow full common stats.
newitem:addsecondary()
newitem:addrandomaffix(3)
end
elseif rarityid == rarity_rare then
newitem:addsecondary()
if level < 10 then
newitem:addrandomaffix(2)
else
newitem:addrandomaffix(3)
end
newitem:addrarityaffix(slotid, rarityid)
elseif rarityid == rarity_epic then
newitem:addsecondary()
newitem:addrandomaffix(3)
newitem:addrarityaffix(slotid, rarityid)
newitem.sellsfor = math.floor(newitem.sellsfor*1.05)
elseif rarityid == rarity_ancient then
newitem:addsecondary()
newitem:addrandomaffix(3)
newitem:addrarityaffix(slotid, rarityid)
newitem.sellsfor = math.floor(newitem.sellsfor*1.1)
end
elseif _iscrafted then
-- item crafted by shinykeeper:
newitem.modifierid = 1
local affixcount = 2
if quest_shinykeeper_upgrade_1 then
affixcount = affixcount + 1
if math.random(0,2) == 1 then
newitem:addsecondary(true)
end
end
if quest_shinykeeper_upgrade_2 and rarityid == rarity_epic then
newitem:addrarityaffix(slotid, rarityid)
end
newitem:addrandomaffix(affixcount)
-- if rolled epic, do an effect at the forge construction doodad:
if rarityid == rarity_epic then
speff_explpurp:play(18500,-18800, nil, 1.5)
end
end
newitem:buildname()
newitem:giveto(p)
return newitem
else
loot:cacheloot(level, slotid, rarityid, kobold.player[p])
end
end
-- generates an item with forced affixes from a specific ore type id.
function loot:generateforcedelement(p, level, slotid, rarityid, oreid)
if loot.debug then
for kw_type,kw_table in pairs(loot.eletable[oreid]) do
for slotid,slottable in pairs(kw_table) do
if #slottable > 0 then
print("found match: ore table",oreid,"kw_type",kw_type,"slotid",slotid,"has table size",#slottable)
for slotid,kw in pairs(slottable) do
print("p_stat =",kw.statmod.p_stat)
end
end
end
end
end
local newitem = self.item:new(loot:getitemtypebylvl(slotid, level), p)
if loot.debug then print("trying to make elemental item for oreid ", oreid," rarity ", rarityid, " slotid ", slotid, " level ", level) end
newitem.level = level
newitem.rarity = loot.raritytable[rarityid]
newitem.modifierid = 2
newitem:addaffix(kw_type_prefix)
newitem:addaffix(kw_type_suffix)
newitem:addelementaffix(oreid, kw_type_typemod)
if quest_elementalist_upgrade_1 then -- a quest unlocks ability to craft secondary stats.
newitem:addsecondary(true)
end
-- add slotid/rarity specific features:
if slotid == slot_potion or slotid == slot_artifact then
newitem:addelementaffix(oreid, kw_type_potion)
end
if rarityid > rarity_rare and (slotid ~= slot_potion and slotid ~= slot_artifact) then
newitem:addelementaffix(oreid, kw_type_epic)
end
-- if rolled epic, do eyecandy at upgrade doodad:
if rarityid == rarity_epic then
speff_explpurp:play(18190,-16130, nil, 1.5)
end
-- as a crafted elemental item, increase all stats by 7 percent:
for kwt,roll in pairs(newitem.kw_roll) do
newitem.kw_roll[kwt] = math.floor(newitem.kw_roll[kwt]*1.07)
end
-- add the elementalist tag (crafted by):
newitem:buildname()
newitem:giveto(p)
return newitem
end
function loot:generatedigkey(p, level, biomeid)
if not loot:isbagfull(p) then
-- local diffid = diffid or map.manager.prevdiffid or 1
local newitem = self.item:new(loot.itemtable[slot_digkey][biomeid], p)
-- find boss details in boss table:
for bossid,t in pairs(boss.bosst) do
if t.biomeid == biomeid then
newitem.biomeid = t.biomeid
newitem.bossname = t.name
newitem.biomename = map.biomemap[t.biomeid]
newitem.bossid = bossid
break
end
end
newitem.rarity = loot.raritytable[4]
newitem.level = level
newitem.sellsfor = 50
newitem:buildname()
newitem.descript = "Summons "..color:wrap(color.tooltip.bad, newitem.bossname).." within "..color:wrap(color.tooltip.good, newitem.biomename).."."
.."|n|n"..color:wrap(color.txt.txtdisable,"Equipping this key will summon a powerful foe on your next adventure into "..newitem.biomename.."."
.."|n|n"..color:wrap(color.txt.txtdisable,"This item is consumed upon successful completion.") )
newitem:giveto(p)
return newitem
else
loot:cachedigkey(level, biomeid, kobold.player[p])
end
end
-- generates loot missiles around x,y and automates rarity selection.
function loot:generatelootpile(x, y, count, _p)
local oddsmod = 0
local minrarity, maxrarity = rarity_common, rarity_common
local level = map.mission.cache and map.mission.cache.level or kobold.player[Player(0)].level -- cheap hack for dev testing NOTE: how to fix for multiplayer?
if level > 10 then
maxrarity = rarity_rare
elseif level > 30 then
maxrarity = rarity_epic
elseif level > 50 then
maxrarity = rarity_ancient
end
if _p then oddsmod = kobold.player[_p]:getlootodds() end
local rarityid = loot:getrandomrarity(minrarity, maxrarity, oddsmod)
loot:generatelootmissile(x, y, loot.missile[rarityid], count)
end
function loot:generatelootmissile(x, y, itemid, _count, _distance, _effect, _miseffect)
local count, xorigin, yorigin = _count or 1, x, y
local dmy = buffy:create_dummy(Player(PLAYER_NEUTRAL_PASSIVE), x, y, 'h008', 3.0)
for i = 1,count do
x,y = utils.projectxy(xorigin, yorigin, _distance or math.random(140,240), math.random(0,360))
missile:create_arc(x, y, dmy, _miseffect or speff_item.effect, 0, function(mis)
local item = CreateItem(FourCC(itemid), mis.x, mis.y)
if loot.missile[FourCC(itemid)] and not _effect then
orb_prolif_stack[loot.pickup[FourCC(itemid)]]:play(mis.x, mis.y)
elseif _effect then
_effect:play(mis.x, mis.y)
end
if map.manager.activemap then
utils.timed(30.0, function() RemoveItem(item) end)
end
end, 1.0)
end
end
function loot:collectloot()
-- highjacked in the candle wax pickup trigger.
local id = GetItemTypeId(GetManipulatedItem())
local p = utils.unitp()
if loot.pickup[id] then
if loot.pickup[id] == rarity_ancient then
loot:generateancient(p, kobold.player[p].level, map.manager.prevbiomeid or map.manager.biome.id, map.manager.prevdiffid or map.manager.diffid)
else
loot:generate(p, kobold.player[p].level, loot:getrandomslottype(), loot.pickup[id])
end
ArcingTextTag("+"..color:wrap(color.tooltip.good, "Loot"), kobold.player[p].unit)
end
end
function loot:getancientdmgtype(biomeid)
-- fetch an appropriate dmgtype id for a biome when rolling ancient affixes.
return loot.biomedmgtypemap[biomeid][math.random(1,#loot.biomedmgtypemap[biomeid])]
end
function loot:generateancient(p, level, _biomeid, _diffid, _ancientid)
local biomeid = _biomeid or math.random(1,5)
if not loot:isbagfull(p) then
if #loot.ancient.biomes[biomeid] > 0 then
local ancientid = _ancientid or loot.ancient.biomes[biomeid][math.random(1, #loot.ancient.biomes[biomeid])]
local slotid = loot.ancienttable[ancientid].slotid
local newitem = self.item:new(loot.ancienttypes[ancientid], p)
if _diffid and _diffid > 3 then
-- modifierids: 4 == vicious item, 5 == tyrannical item.
newitem.modifierid = _diffid
end
newitem.ancientid = ancientid
newitem.rarity = loot.raritytable[rarity_ancient]
newitem.level = level
if slotid ~= slot_candle and slotid ~= slot_artifact and slotid ~= slot_backpack and math.random(1,100) < 33 then
newitem:addelementaffix(loot:getancientdmgtype(biomeid), kw_type_prefix)
newitem:addelementaffix(loot:getancientdmgtype(biomeid), kw_type_suffix)
newitem:addelementaffix(loot:getancientdmgtype(biomeid), kw_type_typemod)
else
newitem:addrandomaffix(3)
end
newitem:addsecondary(true)
-- enhancements - ancients on vicious/ tyrannical are 3.5perc/7perc stronger (matching elementalist quality on diffid 5):
newitem.sellsfor = math.ceil(math.random(newitem.level^0.90,newitem.level^1.05)*newitem.rarity.sellfactor*1.1)
if slotid == slot_potion or slotid == slot_artifact or slotid == slot_backpack or slotid == slot_tool or slotid == slot_candle then
newitem.sellsfor = math.floor(newitem.sellsfor*1.15)
end
if _diffid and _diffid > 3 then
for kwt,roll in pairs(newitem.kw_roll) do
newitem.kw_roll[kwt] = math.random(math.floor(newitem.kw_roll[kwt]*(1+((_diffid-3)*0.035))),math.floor(newitem.kw_roll[kwt]*(1+((_diffid-3)*0.035))))
end
end
-- build description:
newitem:buildancientname(ancientid, _diffid)
newitem:giveto(p)
return newitem
end
else
loot:cacheancient(kobold.player[p], level, biomeid, _diffid, _ancientid)
end
end
function loot.item:buildancientname(ancientid, _diffid)
self:buildname()
if _diffid and _diffid > 3 then
self.fullname = color:wrap(color.tooltip.alert, loot.ancienttable[ancientid].fullname)..self.namedetails.." "..loot.ancient.diffnames[_diffid]
else
self.fullname = color:wrap(color.tooltip.alert, loot.ancienttable[ancientid].fullname)..self.namedetails
end
self.descript = self.descript..'|n'..color:wrap(color.rarity.ancient, 'Ancient Power:')..'|n'..loot.ancienttable[ancientid].descript
end
function loot:getrandomrarity(min, max, oddsmod)
-- @oddsmod = treasure find modifier (each point = +0.25 chance of rarer items)
local rid = min
for id = max,min,-1 do
if math.random(0, 10000) <= rarity_odds[id] + oddsmod then
rid = id
break
end
end
return rid
end
function loot:getrandomslottype()
-- this will grab a random slotid
return slot_all_t[math.random(1,#slot_all_t)]
end
function loot:getitemtypebylvl(slotid, level)
-- this will pull a random item from the slottype table.
local r
for id = #loot.itemtable[slotid],1,-1 do
if level >= loot.itemtable[slotid][id].ilvl then
r = loot.itemtable[slotid][id]
break
end
end
return r
end
function loot:cacheloot(ilvl, slotid, rarityid, pobj)
-- cache randomly generated loot if bags are full.
pobj.tcache[#pobj.tcache+1] = function() loot:generate(pobj.p, ilvl, slotid, rarityid) end
-- utils.palert(pobj.p, "Your bags are full! Free up space to claim from your inventory.", 5.0)
if pobj.p == utils.localp() then kui.canvas.game.inv.tcache:show() end
end
function loot:cachedigkey(ilvl, biomeid, pobj)
-- cache randomly generated loot if bags are full.
pobj.tcache[#pobj.tcache+1] = function() loot:generatedigkey(pobj.p, ilvl, biomeid) end
-- utils.palert(pobj.p, "Your bags are full! Free up space to claim from your inventory.", 5.0)
if pobj.p == utils.localp() then kui.canvas.game.inv.tcache:show() end
end
function loot:cacheancient(pobj, level, _biomeid, _diffid, _ancientid)
-- cache randomly generated loot if bags are full.
pobj.tcache[#pobj.tcache+1] = function() loot:generateancient(pobj.p, level, _biomeid, _diffid, _ancientid) end
-- utils.palert(pobj.p, "Your bags are full! Free up space to claim from your inventory.", 5.0)
if pobj.p == utils.localp() then kui.canvas.game.inv.tcache:show() end
end
function loot:slot(id)
return loot.slottable[id]
end
function loot:debugprint()
for i = 1,loot.invmax do
print(kobold.player[Player(0)].inv[i])
end
end
function loot.item:init()
self.sellsfor = 0
self.__index = self
end
function loot.item:new(itemtype, p)
-- the player's actual item object.
local o = {}
o.diffid = 0 -- if earned on a difficulty that adds stats, 0 = none.
o.kw = {} -- array of keyword ids for looking up named strings, etc.
o.kw_roll = {} -- randomized calcs of keyword attachment values.
o.owner = p --
o.itemtype = itemtype -- base item, also controls slottype by default.
o.slottype = itemtype.slottype -- lazy inherit for context usage.
o.name = itemtype.name -- *unmodified root itemtype name.
o.fullname = itemtype.name -- *modified full name upon generation (apply keywords).
o.level = itemtype.level -- inherited item level.
o.descript = "" -- built based on keyword array.
o.modifierid = 0 -- modifies name in specific ways (diffid, crafted, etc.).
loot:updateallrarityicons(p)
setmetatable(o,self)
return o
end
function loot.item:buildname()
self:mergedupstats()
-- build header if affixes exist:
if self.kw[kw_type_typemod] or self.kw[kw_type_prefix] or self.kw[kw_type_suffix] then
self.descript =
self.descript
..color:wrap(color.txt.txtdisable, "Primary Bonus:|n")
end
-- prepend type mod to item name:
if self.kw[kw_type_typemod] then
self.fullname = self.kw[kw_type_typemod].name.." "..self.fullname
end
-- prepend prefix to item name:
if self.kw[kw_type_prefix] then
self.fullname = self.kw[kw_type_prefix].name.." "..self.fullname
if self.kw_roll[kw_type_prefix] then
self.descript =
self.descript
..color:wrap(self.kw[kw_type_prefix].statmod.modcol, self.kw[kw_type_prefix].statmod.moddir
..self.kw_roll[kw_type_prefix]..self.kw[kw_type_prefix].statmod.modsym)
.." "..self.kw[kw_type_prefix].statmod.name.."|n"
end
end
-- append suffix to item name:
if self.kw[kw_type_suffix] then
self.fullname = self.fullname.." "..self.kw[kw_type_suffix].name
if self.kw_roll[kw_type_suffix] then
self.descript =
self.descript
..color:wrap(self.kw[kw_type_suffix].statmod.modcol, self.kw[kw_type_suffix].statmod.moddir
..self.kw_roll[kw_type_suffix]..self.kw[kw_type_suffix].statmod.modsym)
.." "..self.kw[kw_type_suffix].statmod.name.."|n"
end
end
-- add typemod description last for perc bonus ordering:
if self.kw[kw_type_typemod] then
if self.kw_roll[kw_type_typemod] then
self.descript =
self.descript
..color:wrap(self.kw[kw_type_typemod].statmod.modcol, self.kw[kw_type_typemod].statmod.moddir
..self.kw_roll[kw_type_typemod]..self.kw[kw_type_typemod].statmod.modsym)
.." "..self.kw[kw_type_typemod].statmod.name.."|n"
end
end
if self.kw[kw_type_secondary] then
self.descript =
self.descript
..color:wrap(color.txt.txtdisable, "|nSecondary Bonus:|n")
if self.kw_roll[kw_type_secondary] then
self.descript =
self.descript
..color:wrap(self.kw[kw_type_secondary].statmod.modcol, self.kw[kw_type_secondary].statmod.moddir
..self.kw_roll[kw_type_secondary]..self.kw[kw_type_secondary].statmod.modsym)
.." "..self.kw[kw_type_secondary].statmod.name.."|n"
end
end
if self.kw[kw_type_epic] then
self.descript =
self.descript
..color:wrap(color.txt.txtdisable, "|nEpic Bonus:|n")
if self.kw_roll[kw_type_epic] then
self.descript =
-- because of a dumb gsub percent escape quirk, concat double percent symbol.
self.descript
..string.gsub(string.gsub(string.gsub(self.kw[kw_type_epic].statmod.descript, "(#v)",
color:wrap(self.kw[kw_type_epic].statmod.modcol,
self.kw_roll[kw_type_epic]..(self.kw[kw_type_epic].statmod.modsym..self.kw[kw_type_epic].statmod.modsym))),
"(#perc)", "%%%%"), "(%)", "" )
end
end
if self.kw[kw_type_potion] then
self.descript =
self.descript
..color:wrap(color.txt.txtdisable, "|nPotion Bonus:|n")
if self.kw_roll[kw_type_potion] then
self.descript =
self.descript
..string.gsub(string.gsub(string.gsub(self.kw[kw_type_potion].statmod.descript, "(#v)",
color:wrap(self.kw[kw_type_potion].statmod.modcol,
self.kw_roll[kw_type_potion]..(self.kw[kw_type_potion].statmod.modsym..self.kw[kw_type_potion].statmod.modsym))),
"(#perc)", "%%%%"), "(%)", "" )
end
end
self.namedetails = "|n"
..color:wrap(self.rarity.color, self.rarity.name).."|n"
..color:wrap(color.txt.txtdisable, self.itemtype.slottype.name)
self.fullname = color:wrap(color.tooltip.alert, self.fullname..self.namedetails)
-- merge item modifier tags:
if self.modifierid == 1 then
self.fullname = self.fullname..color:wrap(color.ui.gold, " (Crafted)")
elseif self.modifierid == 2 then
self.fullname = self.fullname..color:wrap(color.ui.xp, " (Crafted)")
end
end
-- @p = triggering player.
-- @selslotid = selected inv space.
-- @tarslotid = target inv space.
function loot.item:slotswap(p, selslotid, tarslotid)
-- if the target slot is empty:
if kobold.player[p].items[selslotid] and kobold.player[p].items[tarslotid] == nil then
if selslotid < 1000 and tarslotid < 1000 then
if p == utils.localp() then
kui.canvas.game.inv.space[tarslotid].icon:setbgtex(kobold.player[p].items[selslotid].itemtype.icon)
kui.canvas.game.inv.space[selslotid].icon:setbgtex(kui.tex.invis)
end
kobold.player[p].items[tarslotid] = kobold.player[p].items[selslotid] -- move to target slot
kobold.player[p].items[selslotid] = nil -- clear original slot
end
end
loot:updateallrarityicons(p)
end
function loot.item:unequip(p, _slotid)
local sid = _slotid or kobold.player[p].selslotid
if not loot:isbagfull(p) then
if not map.manager.loading and not map.manager.activemap and kobold.player[p].items[sid] then
local snd
utils.debugfunc(function() kobold.player[p].items[sid]:equipstats(p, false) end, "equipstats")
utils.debugfunc(function() kobold.player[p].items[sid]:giveto(p) end, "giveto")
kobold.player[p].items[sid] = nil
if sid == slot_digkey then
snd = kui.sound.panel[5].open
kui.canvas.game.dig.space[sid].icon:setbgtex(kui.tex.invis)
kui.canvas.game.dig.digkeyglow:hide()
kui.canvas.game.dig.boss:hide()
else
snd = kui.sound.panel[3].open
kui.canvas.game.equip.space[sid].icon:setbgtex(kui.tex.invis)
end
if p == utils.localp() then
if sid == slot_digkey then
kui.canvas.game.dig.pane:hide()
else
kui.canvas.game.equip.pane:hide()
end
utils.playsound(snd, p)
end
else
if map.manager.loading or map.manager.activemap then
utils.palert(p, "You cannot change items during a dig!")
end
end
else
utils.palert(p, "Cannot unequip because your inventory is full!")
end
loot:updateallrarityicons(p)
end
function loot.item:equip(p, _slotid)
local sid = _slotid or kobold.player[p].selslotid -- currently selected item id.
local eid = kobold.player[p].items[sid].slottype.slotid -- equipment slot id targeted for equipping.
if kobold.player[p].level >= kobold.player[p].items[sid].level then
if not map.manager.loading and not map.manager.activemap then
if kobold.player[p].items[eid] == nil then -- nothing already equipped
if p == utils.localp() then
if eid == slot_digkey then -- dig site key
kui.canvas.game.dig.space[eid].icon:setbgtex(kobold.player[p].items[sid].itemtype.icon)
-- kui.canvas.game.dig:show()
kui.canvas.game.dig.digkeyglow:show()
kui.canvas.game.dig.boss:setfp(fp.c, fp.c, kui.canvas.game.dig.card[kobold.player[p].items[sid].biomeid].bd, -kui:px(8), -kui:px(10))
kui.canvas.game.dig.boss:show()
else
kui.canvas.game.equip.space[eid].icon:setbgtex(kobold.player[p].items[sid].itemtype.icon)
end
kui.canvas.game.inv.space[sid].icon:setbgtex(kui.tex.invis)
kui.canvas.game.inv.pane:hide()
utils.playsound(kui.sound.panel[3].close)
end
kobold.player[p].items[sid]:equipstats(p, true)
kobold.player[p].items[eid] = kobold.player[p].items[sid] -- move to target slot
kobold.player[p].items[sid] = nil -- clear original slot
kobold.player[p].selslotid = nil
else -- swap with an equipped item
local equippeditem = kobold.player[p].items[eid] -- the item already equipped.
if p == utils.localp() then
if eid == slot_digkey then -- dig site key
kui.canvas.game.dig.space[eid].icon:setbgtex(kobold.player[p].items[sid].itemtype.icon)
-- kui.canvas.game.dig:show()
kui.canvas.game.dig.digkeyglow:show()
kui.canvas.game.dig.boss:setfp(fp.c, fp.c, kui.canvas.game.dig.card[kobold.player[p].items[sid].biomeid].bd, -kui:px(8), -kui:px(10))
kui.canvas.game.dig.boss:show()
else
kui.canvas.game.equip.space[eid].icon:setbgtex(kobold.player[p].items[sid].itemtype.icon)
end
kui.canvas.game.inv.space[sid].icon:setbgtex(equippeditem.itemtype.icon)
kui.canvas.game.inv.pane:hide()
utils.playsound(kui.sound.panel[3].close)
end
kobold.player[p].items[eid]:equipstats(p, false) -- remove prev equip stats
kobold.player[p].items[sid]:equipstats(p, true) -- add new item stats
kobold.player[p].items[eid] = kobold.player[p].items[sid] -- equipped becomes selected item
kobold.player[p].items[sid] = equippeditem -- selected item becomes equipped
kobold.player[p].selslotid = nil
end
kobold.player[p]:updatecastspeed()
else
utils.palert(p, "You cannot change items during a dig!")
end
else
utils.palert(p, "You do not meet that item's level requirement")
end
loot:updateallrarityicons(p)
end
-- this will automatically pick up the player's selected item in inventory unless @_selslotid is passed to override (i.e. external logic, sorting, etc.)
function loot.item:sell(p, _sndsuppress, _selslotid)
local sid = _selslotid or kobold.player[p].selslotid
if kobold.player[p].items[sid].itemtype.slottype.slotid ~= slot_digkey then
local p = p
local total = kobold.player[p].items[sid].sellsfor*(1+kobold.player[p][p_stat_vendor]/100)
kobold.player[p]:awardgold(total)
if not _sndsuppress then utils.playsound(kui.sound.sell, p) end
if p == utils.localp() then
kui.canvas.game.inv.space[sid].icon:setbgtex(kui.tex.invis)
kui.canvas.game.inv.pane:hide()
end
kobold.player[p].items[sid] = nil
kobold.player[p].selslotid = nil
shop:updateframes(kobold.player[p])
else
utils.palert(p, color:wrap(color.tooltip.bad,"You cannot sell dig keys!"))
end
loot:updateallrarityicons(p)
end
-- @p = give to this player
function loot.item:giveto(p)
-- place loot into a player's inventory
for slotid = 1,loot.invmax do
if kobold.player[p].items[slotid] == nil then
kobold.player[p].items[slotid] = self
loot_last_slotid = slotid -- global for easy referencing.
if p == utils.localp() then
kui.canvas.game.skill.alerticon[2]:show() -- show new items alert eyecandy on menu button.
kui.canvas.game.inv.space[slotid].icon:setbgtex(self.itemtype.icon)
end
loot:updateallrarityicons(p)
return true
end
end
return false
end
-- update function for item tooltip on mouseover (used in kui).
function loot.item:updatetooltip(p)
-- background texture:
if self.itemtype.slottype.slotid == slot_digkey then
BlzFrameSetTexture(tooltip.itemtipfh[1], 'war3mapImported\\tooltip-bg_scroll_items_digkey.blp', 0, true)
else
BlzFrameSetTexture(tooltip.itemtipfh[1], self.rarity.tooltipbg, 0, true)
end
-- icon:
BlzFrameSetTexture(tooltip.itemtipfh[2], self.itemtype.icon, 0, true)
-- rarity gem:
BlzFrameSetTexture(tooltip.itemtipfh[3], self.rarity.icon, 0, true)
-- name:
BlzFrameSetText(tooltip.itemtipfh[4], self.fullname)
-- description:
BlzFrameSetText(tooltip.itemtipfh[5], self.descript)
-- level:
if kobold.player[p].level >= self.level then
BlzFrameSetText(tooltip.itemtipfh[6], color:wrap(color.txt.txtdisable, "Level "..self.level))
else -- doesn't meet req.
BlzFrameSetText(tooltip.itemtipfh[6], color:wrap(color.tooltip.bad, "Level "..self.level))
end
-- gold value:
BlzFrameSetText(tooltip.itemtipfh[7], self.sellsfor)
end
-- update function for item tooltip on mouseover (used in kui).
function loot.item:updatecomptooltip(p)
-- background texture:
if self.itemtype.slottype.slotid == slot_digkey then
BlzFrameSetTexture(tooltip.itemcompfh[1], 'war3mapImported\\tooltip-bg_scroll_items_digkey.blp', 0, true)
else
BlzFrameSetTexture(tooltip.itemcompfh[1], self.rarity.tooltipbg, 0, true)
end
BlzFrameSetAlpha(tooltip.itemcompfh[1], 230)
-- icon:
BlzFrameSetTexture(tooltip.itemcompfh[2], self.itemtype.icon, 0, true)
-- rarity gem:
BlzFrameSetTexture(tooltip.itemcompfh[3], self.rarity.icon, 0, true)
-- name:
BlzFrameSetText(tooltip.itemcompfh[4], self.fullname)
-- description:
BlzFrameSetText(tooltip.itemcompfh[5], self.descript)
-- level:
if kobold.player[p].level >= self.level then
BlzFrameSetText(tooltip.itemcompfh[6], color:wrap(color.txt.txtdisable, "Level "..self.level))
else -- doesn't meet req.
BlzFrameSetText(tooltip.itemcompfh[6], color:wrap(color.tooltip.bad, "Level "..self.level))
end
-- gold value:
BlzFrameSetText(tooltip.itemcompfh[7], self.sellsfor)
BlzFrameSetAlpha(tooltip.itemcompfh[8], 140)
end
function loot.item:mergedupstats()
if loot.debug then print("running loot.item:mergedupstats for slot:",self.itemtype.slottype.name) end
if self.itemtype.slottype.slotid ~= slot_digkey and self.kw and utils.tablesize(self.kw) > 1 then
for kw_id,kw_t in pairs(self.kw) do
for kw_id2,kw_t2 in pairs(self.kw) do
if kw_t ~= kw_t2 and kw_t.statmod.id == kw_t2.statmod.id then
self.kw_roll[kw_id] = self.kw_roll[kw_id] + self.kw_roll[kw_id2] -- merge dup values.
self.kw[kw_id2] = nil -- destroy duplicate.
self.kw_roll[kw_id2] = nil
return
end
end
end
end
end
function loot.slottype:new(slotid, name)
-- matches item objects to equipment slots.
local o = {}
o.count = 0 -- total items that exist for this slot (design tracking).
o.slotid = slotid
o.name = name
setmetatable(o,self)
self.__index = self
return o
end
function loot.itemtype:init()
-- the base item or root word (e.g. "dagger", "hatchet", "cludgel", "star pendant")
-- keywords are then applied to this base item.
self.stack = {}
self.__index = self
end
-- @name = the name of the item.
-- @slottype = slot class to control slot label and UI equip location.
-- @icon = path to icon .blp.
-- @ilvl = minimum item level.
function loot.itemtype:new(name, slottype, ilvl, icon)
-- create a new base item type that item objects inherit.
local o = {}
o.name = name
o.slottype = slottype
o.icon = icon
o.ilvl = ilvl
o.id = #self.stack + 1
self.stack[o.id] = o
setmetatable(o,self)
return o
end
function loot.kw:init()
-- keywords.
self.__index = self
self.stack = {}
end
-- @id = kw id.
-- @name = kw in-game name.
-- @kw_type = constant: kw_type_prefix, kw_type_typemod, or kw_type_suffix.
-- @statmod = the player stat to modify.
-- @kw_group = applicable slots
function loot.kw:new(name, kw_type, statmod, kw_group)
local o = {}
setmetatable(o,self)
o.name = name
o.kw_type = kw_type -- prefix, typemod, or suffix
o.statmod = statmod
-- build kw lookups:
for _,slotid in pairs(kw_group) do
-- place kw in slotid lookup tables (loot generation performance):
if loot.affixt[kw_type][slotid] then
loot.affixt[kw_type][slotid][#loot.affixt[kw_type][slotid]+1] = o
end
-- place kw in element lookup tables:
for oreid = 1,6 do
if utils.tablehasvalue(loot.oret[oreid], statmod.p_stat) then
-- print("oreid ",oreid," added kw_type ", kw_type, " p_stat ",statmod.p_stat," slotid ", slotid)
loot.eletable[oreid][kw_type][slotid][#loot.eletable[oreid][kw_type][slotid]+1] = o
end
end
end
-- store an id for save/load system:
o.id = #self.stack + 1
self.stack[o.id] = o
return o
end
function loot.rarity:init()
-- the rarity attachment to an item.
self.iconw = 24 -- width and height
self.iconh = 24
self.anchor = fp.c -- point on rarity icon to attach to item icon.
self.taranchor = fp.b -- point on item icon to attach rarity gem.
self.__index = self
end
function loot.rarity:new(id, icon, name)
local o = {}
o.id = id
o.icon = icon
o.tooltipbg = nil
o.name = name
o.sellfactor = 1.0 -- added sell value after generation.
o.maxfactor = 0.0 -- the minimum roll is increased by this factor amount based on rarity.
o.rollfactor = 1.0
o.color = rarity_color[id]
setmetatable(o,self)
return o
end
function loot.statmod:init()
self.__index = self
self.modtype = modtype_relative
self.modsym = "%%"
self.moddir = "+"
self.modcol = color.tooltip.good
self.stack = {}
self.pstatstack = {}
end
-- @name = name of the p_stat.
-- @p_stat = the player stat to modify.
-- @minval = minimum stat roll value.
-- @maxmult = max ``.
-- @lvlmult = total item level multiplier for this stat.
-- @absbool = [optional] when true, convert from relative perc-based to absolute value.
-- @negbool = [optional] when true, this statmod is a negative value.
function loot.statmod:new(name, p_stat, minval, maxval, lvlmult, absbool, negbool)
if type(p_stat) == "number" then
local o = {}
setmetatable(o, self)
o.name = name
o.p_stat = p_stat
o.minval = minval
o.maxval = maxval
o.lvlmult = lvlmult * global_statmod_multiplier
if absbool then
o.modsym = ""
o.modtype = modtype_absolute
end
if negbool then
o.moddir = "-"
o.modcol = color.tooltip.bad
end
o.id = #self.stack + 1
self.stack[o.id] = o
self.pstatstack[p_stat] = o -- store statmod lookup by player stat for external features.
return o
else
print("error: statmod:new() was not passed a valid 'p_stat' id; p_stat = "..p_stat)
end
end
function loot.statmod:newlong(descript, p_stat, minval, maxval, lvlmult, absbool, negbool)
local o = loot.statmod:new(descript, p_stat, minval, maxval, lvlmult, absbool, negbool)
o.long = true
o.descript = descript
o.id = #self.stack + 1
self.stack[o.id] = o
return o
end
-- @p = for this player.
-- @isaddbool = true to add stats, false to remove stats.
function loot.item:equipstats(p, isaddbool)
utils.debugfunc(function()
local statval
for kw_type,keyword in pairs(self.kw) do
if self.kw_roll[kw_type] ~= 0 then
statval = self.kw_roll[kw_type]
if isaddbool then
kobold.player[p]:modstat(keyword.statmod.p_stat, true, statval)
else
kobold.player[p]:modstat(keyword.statmod.p_stat, false, statval)
end
end
end
-- add ancient id to array of equipped ancient effects for external logic:
if self.ancientid then
if isaddbool then -- add ancient id to table.
badge:earn(kobold.player[p], 6)
local size = #kobold.player[p].ancients[loot.ancienttable[self.ancientid].eventtype]
kobold.player[p].ancients[loot.ancienttable[self.ancientid].eventtype][size + 1] = self.ancientid
loot.ancient:raiseevent(ancient_event_equip, kobold.player[p], true, nil, self.ancientid)
else -- remove ``.
if loot.ancienttable[self.ancientid].unequip then loot.ancienttable[self.ancientid].unequip(kobold.player[p]) end
loot.ancient:raiseevent(ancient_event_equip, kobold.player[p], false, nil, self.ancientid) -- NOTE: keep ABOVE table removal so func can be found first!
utils.removefromtable(kobold.player[p].ancients[loot.ancienttable[self.ancientid].eventtype], self.ancientid)
end
end
kobold.player[p]:updateallstats()
end, "equipstats")
end
function loot.item:addrandomaffix(count)
-- adds a prefix or suffix to an item.
self.sellsfor = math.floor(self.sellsfor*(1+(count*0.1)))
if count == 1 then -- pick a prefix or suffix.
local r = math.random(0,1)
if r == 1 then
self:addaffix(kw_type_prefix)
else
self:addaffix(kw_type_suffix)
end
elseif count == 2 then -- add prefix and suffix.
self:addaffix(kw_type_prefix)
self:addaffix(kw_type_suffix)
elseif count == 3 then
self:addaffix(kw_type_prefix)
self:addaffix(kw_type_typemod)
self:addaffix(kw_type_suffix)
else
print("error: addrandomaffix was passed an invalid 'count' or it was nil")
end
end
function loot.item:addsecondary(_guaranteed)
-- only add secondary stats on occasion unless @_guaranteed is passed:
local r = 0
if not _guaranteed then
r = math.random(1,100)
else
r = 100
end
if r > 20 then
self:addaffix(kw_type_secondary)
end
end
function loot.item:addrarityaffix(slotid, rarityid)
if loot.debug then print("loot.item:addrarityaffix: running for slotid",slotid,"rarityid",rarityid) end
-- only add secondary stats on occasion:
if rarityid == rarity_epic and (slotid ~= slot_potion and slotid ~= slot_artifact) then
self:addaffix(kw_type_epic)
elseif rarityid == rarity_ancient and (slotid ~= slot_potion and slotid ~= slot_artifact) then
self:addaffix(kw_type_epic)
elseif (slotid == slot_potion or slotid == slot_artifact) and (rarityid == rarity_rare or rarityid == rarity_epic) then
self:addaffix(kw_type_potion)
end
end
function loot.item:addaffix(kw_type)
if loot.debug then print("loot.item:addaffix: running for kw_type",kw_type) end
if not self.kw[kw_type] then -- will not override if already exists.
if #loot.affixt[kw_type][self.slottype.slotid] > 0 then
local roll = math.random(1,#loot.affixt[kw_type][self.slottype.slotid])
if loot.debug then print("kw_type "..kw_type.." rolled tableid "..roll) end
self.kw[kw_type] = loot.affixt[kw_type][self.slottype.slotid][roll]
self:rollvalue(kw_type, self.rarity)
end
elseif loot.debug then
print("loot.item:addaffix: 'self.kw[kw_type]' not found, no affix added")
end
end
function loot.item:addelementaffix(oreid, kw_type)
if #loot.eletable[oreid][kw_type][self.slottype.slotid] > 0 then
local roll = math.random(1,#loot.eletable[oreid][kw_type][self.slottype.slotid])
if loot.debug then print("kw_type "..kw_type.." rolled tableid "..roll) end
self.kw[kw_type] = loot.eletable[oreid][kw_type][self.slottype.slotid][roll]
self:rollvalue(kw_type, self.rarity)
end
end
function loot.item:rollvalue(kw_type, rarity)
if loot.debug then print("trying to add kw_type: "..kw_type) end
if rarity.id == rarity_common then
self.kw_roll[kw_type] =
math.random(
self.kw[kw_type].statmod.minval,
self.kw[kw_type].statmod.maxval)
else
self.kw_roll[kw_type] =
math.random(
-- rare and above items always have a guaranteed
-- roll value as a perc of the max roll possible.
math.floor(self.kw[kw_type].statmod.maxval*rarity.maxfactor),
self.kw[kw_type].statmod.maxval)
-- alongside the max factor, higher rarities
-- roll higher values in general.
self.kw_roll[kw_type] = self.kw_roll[kw_type]*rarity.rollfactor
end
-- increment the stat value based on item level:
self.kw_roll[kw_type] = math.ceil(self.kw_roll[kw_type]*(1.0+(self.level*self.kw[kw_type].statmod.lvlmult)))
end
ancient_event_damagedeal = 1
ancient_event_damagetake = 2
ancient_event_ability = 3
ancient_event_equip = 4
ancient_event_potion = 5
ancient_event_movement = 6
loot.ancient.diffnames = {
[1] = '|cffebce5a(Greenwhisker)|r',
[2] = '|cffd1d8eb(Standard)|r',
[3] = '|cff2fceeb(Heroic)|r',
[4] = '|cffb250d8(Vicious)|r',
[5] = '|cffed6d00(Tyrannical)|r',
}
function loot.ancient:newplayertable()
-- ancient storage table for event lookups (add new event types here as needed):
local o = {}
o[ancient_event_damagedeal] = {}
o[ancient_event_damagetake] = {}
o[ancient_event_ability] = {}
o[ancient_event_equip] = {}
o[ancient_event_potion] = {}
o[ancient_event_movement] = {}
return o
end
function loot.ancient:new(ancientid, biomeid, slotid, eventtype, fullname, description, func)
-- wrapper object for actual item to control special interactions.
local o = setmetatable({}, o)
o.fullname = fullname
o.descript = description
o.biomeid = biomeid
o.eventtype = eventtype
o.slotid = slotid
o.func = func
o.ancientid = ancientid
if biomeid then
self.biomes[biomeid][#self.biomes[biomeid] + 1] = ancientid -- store for biome-specific drops.
else
for bid = 1,5 do self.biomes[bid][#self.biomes[bid] + 1] = ancientid end -- store as a globally available ancient.
end
self.allancients[#self.allancients + 1] = ancientid -- store for global drops.
return o
end
function loot.ancient:raiseevent(eventtype, pobj, _flag, _eventobj, _ancientid)
-- print("raising ancient event", eventtype, _flag, _eventobj)
-- raise the event being triggered, then search the player's ancient table for eligible effects to run based on ancient ids equipped.
utils.debugfunc(function()
if #pobj.ancients[eventtype] > 0 then
for _,ancientid in ipairs(pobj.ancients[eventtype]) do
if not pobj.ancientscd[ancientid]then
if eventtype ~= ancient_event_equip or (_ancientid and eventtype == ancient_event_equip and ancientid == _ancientid) then
loot.ancienttable[ancientid].func(ancientid, pobj, _flag, _eventobj)
end
end
end
end
end, "ancient raiseevent")
end
function loot.ancient:registercooldown(ancientid, dur, pobj)
if not pobj.ancientscd[ancientid] then
pobj.ancientscd[ancientid] = true
utils.timed(dur, function() pobj.ancientscd[ancientid] = nil end)
end
end
function loot:builtancienttable()
loot.ancienttable = {}
loot.ancient.biomes = {}
loot.ancient.biomes[biome_id_slag] = {}
loot.ancient.biomes[biome_id_mire] = {}
loot.ancient.biomes[biome_id_foss] = {}
loot.ancient.biomes[biome_id_ice] = {}
loot.ancient.biomes[biome_id_vault]= {}
-- used to map ore/damagetype ids to the appropriate biome when rolling ancient affixes.
loot.biomedmgtypemap = {}
loot.biomedmgtypemap[biome_id_slag] = {}
loot.biomedmgtypemap[biome_id_mire] = {}
loot.biomedmgtypemap[biome_id_foss] = {}
loot.biomedmgtypemap[biome_id_ice] = {}
loot.biomedmgtypemap[biome_id_vault] = {}
loot.biomedmgtypemap[biome_id_slag][1] = dmg_fire
loot.biomedmgtypemap[biome_id_slag][2] = dmg_shadow
loot.biomedmgtypemap[biome_id_mire][1] = dmg_nature
loot.biomedmgtypemap[biome_id_mire][2] = dmg_shadow
loot.biomedmgtypemap[biome_id_foss][1] = dmg_phys
loot.biomedmgtypemap[biome_id_foss][2] = dmg_nature
loot.biomedmgtypemap[biome_id_ice][1] = dmg_frost
loot.biomedmgtypemap[biome_id_ice][2] = dmg_arcane
loot.biomedmgtypemap[biome_id_vault][1] = dmg_phys
loot.biomedmgtypemap[biome_id_vault][2] = dmg_shadow
local a_id = 1
-- ancients will not have secondary stats, and can only roll two other affixes (but they are 1.5x (?) as strong)
-- instantiated for each player, allowing reads for equipped effects on ability use, loot equip, and damage event.
-- event types: 'ability', 'equip', 'damagetake', 'damagedeal', 'potion', 'other'
-- even better idea: have an array for players where we insert/remove ancient ids upon equip and then loop those ids in each event (limited processing done)
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_slag, slot_boots, ancient_event_ability,
'Doomwalkers', 'Using an ability ignites your feet for 6 sec, maxing your movespeed and damaging nearby targets for 6%% of your max health as Fire damage '
..'per sec. (10 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
local damage = utils.maxlife(pobj.unit)*0.06/3
bf_msmax:apply(pobj.unit, 6.0)
speff_liqfire:attachu(pobj.unit, 6.0)
utils.timedrepeat(0.33, 6*3, function()
spell:gdmgxy(pobj.p, dmg.type.stack[dmg_fire], damage, utils.unitx(pobj.unit), utils.unity(pobj.unit), 133.0)
end)
loot.ancient:registercooldown(ancientid, 10.0, pobj)
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_01.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_slag, slot_candle, ancient_event_damagetake,
"Slag King's Torch", "If you take damage while under 50%% health, restore 10 Candle Light, 10%% health, and 10%% mana. (10 sec cooldown)")
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if event.target == pobj.unit and utils.getlifep(pobj.unit) < 50 then
loot.ancient:registercooldown(ancientid, 10.0, pobj)
speff_conflagoj:play(utils.unitxy(pobj.unit))
speff_pothp:play(utils.unitx(pobj.unit), utils.unity(pobj.unit), nil, 0.6)
if map.manager.activemap and candle.current > 0 then candle.current = candle.current + 10 end
utils.addlifep(pobj.unit, 10, true, pobj.unit)
utils.addmanap(pobj.unit, 10, true)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_02.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_slag, slot_outfit, ancient_event_ability,
'Cuirass of the Flame', 'Using an ability increases your Fire damage by 10%% for 8 sec, but reduces all other damage by 5%% for the duration.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if pobj.ancientsdata.cuirasseff then DestroyEffect(pobj.ancientsdata.cuirasseff) end
pobj.ancientsdata.cuirasseff = sp_armor_boost[4]:attachu(pobj.unit, 8.0, 'overhead')
for i = p_stat_fire, p_stat_phys do
if i == p_stat_fire then spell:enhancepstat(p_stat_fire, 10.0, 8.0, pobj.p) else spell:enhancepstat(i, -5.0, 8.0, pobj.p) end
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_03.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_slag, slot_helmet, ancient_event_equip,
'Phoenix Crown', 'Summons a Phoenix companion which launches fiery bolts at up to 3 targets at a time for 24 Fire damage.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if flag then
if not pobj.ancientsdata.phxunit then
pobj.ancientsdata.phxunit = utils.unitatxy(pobj.p, utils.unitx(pobj.unit), utils.unity(pobj.unit), 'n01L', math.random(0,360))
pobj.ancientsdata.phxunitai = ai:new(pobj.ancientsdata.phxunit)
pobj.ancientsdata.phxunitai:initcompanion(900.0, 24.0, p_stat_fire)
end
elseif not flag then
if pobj.ancientsdata.phxunit then RemoveUnit(pobj.ancientsdata.phxunit) pobj.ancientsdata.phxunit = nil end
if pobj.ancientsdata.phxunitai then pobj.ancientsdata.phxunitai:releasecompanion() end
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_04.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_slag, slot_outfit, ancient_event_damagetake,
'Battlescarred Vestments', 'Taking damage restores 3%% of your mana and 1%% of your health. (1 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
utils.addmanap(pobj.unit, 3, true)
utils.addlifep(pobj.unit, 1, true)
loot.ancient:registercooldown(ancientid, 1, pobj)
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_05.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_slag, slot_gloves, ancient_event_damagedeal,
'Broiling Mittens', 'Dealing damage has a 20%% chance to lob molten slag in a 6m radius for 54 Fire damage. (3 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if event.amount > 10 and math.random(0,10) <= 1 then
local x,y = utils.unitxy(pobj.unit)
missile:create_in_radius(x, y, pobj.unit, mis_fireballbig.effect, 54, dmg.type.stack[dmg_fire], 8, 600, 1.25, 96.0, 128.0, true, nil, true)
loot.ancient:registercooldown(ancientid, 3.0, pobj)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_06.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_mire, slot_outfit, ancient_event_ability,
'Mutant Husk', 'Using an ability causes you to spit up 3 murlocs which deal 10 Nature damage per sec for 15 sec. (6 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
for i = 1,3 do
local x,y = utils.projectxy(utils.unitx(pobj.unit), utils.unity(pobj.unit), math.random(200,360), math.random(0,360))
local mis = missile:create_arc(x, y, pobj.unit, speff_embernat.effect, 0)
mis.explfunc = function(mis)
ai:new(utils.unitatxy(pobj.p, mis.x, mis.y, 'n01M', math.random(0,360)))
:initcompanion(900.0, 11.0, p_stat_nature)
:timed(15.0)
speff_water:play(mis.x, mis.y)
end
end
loot.ancient:registercooldown(ancientid, 6.0, pobj)
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_07.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_mire, slot_boots, ancient_event_movement,
'Bog Waders', 'While moving, gain 30%% Nature resistance. While standing still, deal 30%% more Nature damage.')
loot.ancienttable[a_id].unequip = function(pobj)
if pobj.ancientsdata.bogwaderbonus1 then
pobj.ancientsdata.bogwaderbonus1 = false
pobj:modstat(p_stat_nature_res, false, 30)
end
if pobj.ancientsdata.bogwaderbonus2 then
pobj.ancientsdata.bogwaderbonus2 = false
pobj:modstat(p_stat_nature, false, 30)
end
end
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if flag then
if not pobj.ancientsdata.bogwaderbonus1 then
pobj.ancientsdata.bogwaderbonus1 = true
pobj:modstat(p_stat_nature_res, true, 30)
end
if pobj.ancientsdata.bogwaderbonus2 then
pobj.ancientsdata.bogwaderbonus2 = false
pobj:modstat(p_stat_nature, false, 30)
end
else
if pobj.ancientsdata.bogwaderbonus1 then
pobj.ancientsdata.bogwaderbonus1 = false
pobj:modstat(p_stat_nature_res, false, 30)
end
if not pobj.ancientsdata.bogwaderbonus2 then
pobj.ancientsdata.bogwaderbonus2 = true
pobj:modstat(p_stat_nature, true, 30)
end
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_08.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_mire, slot_tool, ancient_event_damagedeal,
'Quagmirator', 'Dealing damage has a 15%% chance to unleash swamp bat missiles towards targets within 6m for 42 Nature damage. (3 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if event.amount > 10 and math.random(0,100) < 15 then
local grp = g:newbyxy(pobj.p, g_type_dmg, utils.unitx(pobj.unit), utils.unity(pobj.unit), 600)
grp:action(function()
local x,y = utils.unitxy(grp.unit)
local mis = missile:create_arc(x, y, pobj.unit, mis_bat.effect, 42)
mis.dmgtype = dmg.type.stack[dmg_nature]
mis.time = 1.03
mis:initduration()
end)
grp:destroy()
loot.ancient:registercooldown(ancientid, 3.0, pobj)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_09.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_mire, slot_helmet, ancient_event_damagetake,
'Forgotten Plate Helm', 'Taking damage has a 15%% chance to cause you to gain 10 Armor for 10 sec. (1.5 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if event.amount > 10 and math.random(1,100) < 15 then
if pobj.ancientsdata.platebeff then DestroyEffect(pobj.ancientsdata.platebeff) end
pobj.ancientsdata.platebeff = sp_armor_boost[6]:attachu(pobj.unit, 10.0, 'overhead')
spell:enhancepstat(p_stat_armor, 10, 10, pobj.p)
loot.ancient:registercooldown(ancientid, 1.5, pobj)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_10.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_mire, slot_potion, ancient_event_potion,
'Swampbrewer Potion', 'On Mana Potion use, summon a Shroom at your feet every sec for 3 sec.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if not flag then -- mpot only.
utils.timedrepeat(0.99, 3, function()
local x1,y1 = utils.unitxy(pobj.unit)
speff_shroom:play(x1, y1, 6, 0.3)
speff_explgrn:play(x1, y1, nil, 0.75)
utils.timedrepeat(1.0, 6, function()
utils.debugfunc(function()
spell:ghealxy(pobj.p, 16, x1, y1, 300.0)
spell:gdmgxy(pobj.p, dmg.type.stack[dmg_nature], 12, x1, y1, 300.0)
end, 'heal')
end)
end)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_11.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_mire, slot_backpack, ancient_event_damagetake,
'Sssnake Ssskin', 'Taking damage has a 15%% chance to grant you a shield that absorbs damage equal to 10%% of your health for 6 sec. (8 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if event.amount > 10 and math.random(1,100) < 15 then
local amt = math.floor(utils.maxlife(pobj.unit)*.10)
dmg.absorb:new(pobj.unit, 6.0, {all = amt}, speff_ubergrn)
buffy:add_indicator(pobj.p, "Sssnake Ssskin", "ReplaceableTextures\\CommandButtons\\BTNReinforcedHides.blp", 8.0,
"Absorbs up to "..tostring(amt).." damage")
loot.ancient:registercooldown(ancientid, 8.0, pobj)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_12.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_foss, slot_tool, ancient_event_damagedeal,
'Mawchomper', 'Dealing damage has a 15%% chance to crush targets in a 3m radius, stunning them for 1.5 sec and dealing 36 Physical damage. (3 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if event.amount > 10 and math.random(0,100) < 15 then
local x,y = utils.unitxy(pobj.unit)
spell:gdmgxy(pobj.p, dmg.type.stack[dmg_phys], 36, x, y, 300, function(u) bf_stun:apply(u, 1.5) end)
speff_dustcloud:playradius(200, 3, x, y)
speff_quake:playradius(200, 3, x, y)
loot.ancient:registercooldown(ancientid, 3.0, pobj)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_13.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_foss, slot_tool, ancient_event_damagedeal,
'Spinethrower', 'Targets farther than 10m from you take 15%% more damage.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if event.amount > 10 and utils.distunits(pobj.unit, event.target) > 1000.0 then
event.amount = event.amount*1.15
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_14.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_foss, slot_leggings, ancient_event_movement,
'Treasure Trousers', 'While moving, occasionally drop 3 enchanted gold coins which explode for 48 Physical damage in a 1.5m radius after 2 sec.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if flag and math.random(0,100) < 4 then
for i = 1,3 do
local x,y = utils.projectxy(utils.unitx(pobj.unit), utils.unity(pobj.unit), math.random(100,200), i*120 + math.randomneg(-15,15))
local mis = missile:create_arc(x, y, pobj.unit, speff_coin.effect, 0)
mis.explfunc = function(mis)
speff_coin:play(mis.x, mis.y, 2.0, 0.88)
local x,y = mis.x, mis.y
utils.timed(2.0, function()
speff_edict:play(x, y, nil, 0.3)
spell:gdmgxy(pobj.p, dmg.type.stack[dmg_phys], 48, x, y, 150.0)
end)
end
end
loot.ancient:registercooldown(ancientid, 2.0, pobj)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_15.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_foss, slot_gloves, ancient_event_equip,
"Archaeologist's Wraps", 'Increases your mining speed by 25%%.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if flag then
if not pobj.ancientsdata.archwrapsbonus then
pobj.ancientsdata.archwrapsbonus = true
pobj:modstat(p_stat_minespd, true, 25)
end
else
if pobj.ancientsdata.archwrapsbonus then
pobj.ancientsdata.archwrapsbonus = false
pobj:modstat(p_stat_minespd, false, 25)
end
end
pobj:updateattackspeed()
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_16.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_foss, slot_backpack, ancient_event_ability,
'Fossilized Turboshell', 'Using an ability causes you to launch 3 spinning turtles which trample targets for 24 Physical damage. (3 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
for i = 1,3 do
local x,y = utils.projectxy(utils.unitx(pobj.unit), utils.unity(pobj.unit), 600.0, i*120 + math.randomneg(-15,15))
local mis = missile:create_piercexy(x, y, pobj.unit, 'Units\\Creeps\\DragonSeaTurtle\\DragonSeaTurtle', 0)
mis.dmgtype = dmg.type.stack[dmg_phys]
mis.cangle = math.random(0,360)
mis.vel = 17
mis.time = 6.0
mis:updatescale(0.27)
mis:initduration()
mis.func = function(mis)
mis.cangle = mis.cangle + 12
mis.angle = mis.angle + 2
if math.fmod(mis.elapsed, 7) == 0 then -- every 0.21 sec
spell:gdmgxy(pobj.p, dmg.type.stack[dmg_phys], 24, mis.x, mis.y, 124.0)
speff_phys:play(mis.x, mis.y)
end
end
end
loot.ancient:registercooldown(ancientid, 3.0, pobj)
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_17.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_foss, slot_gloves, ancient_event_ability,
'Skeleton Grasps', 'Using an ability has a 15%% chance to summon a skeletal warrior to fight for you for 12 sec, striking nearby targets for 24 Physical damage.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if math.random(0,100) < 15 then
local x,y = utils.projectxy(utils.unitx(pobj.unit), utils.unity(pobj.unit), 100, math.random(0,360))
speff_animate:play(x, y)
ai:new(utils.unitatxy(pobj.p, x, y, 'n01N', math.random(0,360)))
:initcompanion(700.0, 25.0, p_stat_phys)
:timed(12.0)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_18.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_ice, slot_tool, ancient_event_damagedeal,
'Reaping Picksickle', 'Frost, Shadow, and Arcane damage combusts, dealing 100%% bonus Frost damage and Freezing the target for 1.5 sec (3 sec cooldown).')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if event.amount > 16.0
and (dtypelookup[event.dmgtype] == dmg_frost or atypelookup[event.atktype] == dmg_frost or
dtypelookup[event.dmgtype] == dmg_shadow or atypelookup[event.atktype] == dmg_shadow or
dtypelookup[event.dmgtype] == dmg_arcane or atypelookup[event.atktype] == dmg_arcane) then
loot.ancient:registercooldown(ancientid, 3.0, pobj)
speff_frostnova:play(utils.unitx(event.target), utils.unity(event.target), nil, 0.75)
bf_freeze:apply(event.target, 1.5)
dmg.type[dmg_frost]:pdeal(pobj.p, event.amount, event.target)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_19.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_ice, slot_leggings, ancient_event_ability,
'Frostfire Pantaloons', 'Using an ability increases your Frost or Fire damage by 5%% for 10 sec, cycling back and forth.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if pobj.ancientsdata.frostfirepants then
spell:enhancepstat(p_stat_frost, 5.0, 10.0, pobj.p)
pobj.ancientsdata.frostfirepants = false
else
spell:enhancepstat(p_stat_fire, 5.0, 10.0, pobj.p)
pobj.ancientsdata.frostfirepants = true
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_20.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_ice, slot_candle, ancient_event_movement,
'Congealed Candlelight', 'While moving, deal 35%% more Fire damage.')
loot.ancienttable[a_id].unequip = function(pobj)
if pobj.ancientsdata.congealedbonus then
pobj.ancientsdata.congealedbonus = false
pobj:modstat(p_stat_fire, false, 35)
end
end
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if flag then
if not pobj.ancientsdata.congealedbonus then
pobj.ancientsdata.congealedbonus = true
pobj:modstat(p_stat_fire, true, 35)
end
else
if pobj.ancientsdata.congealedbonus then
pobj.ancientsdata.congealedbonus = false
pobj:modstat(p_stat_fire, false, 35)
end
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_21.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_ice, slot_boots, ancient_event_ability,
'Reversed Frost-Steppers', 'Using an ability leaves behind ice which explodes after 3 sec, Freezing targets in a 3m radius for 1.5 sec and dealing 48 Frost damage.'
..' (3 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
local x,y = utils.unitx(pobj.unit), utils.unity(pobj.unit)
speff_frost:play(x, y)
BlzSetSpecialEffectAlpha(speff_icestep:play(x, y, 3.0, 0.23), 200)
utils.timed(3.0, function()
speff_iceshard:playradius(300, 5, x, y, 1.0)
spell:gdmgxy(pobj.p, dmg.type.stack[dmg_frost], 48, x, y, 300.0, function(u) bf_freeze:apply(u, 1.5) end)
end)
loot.ancient:registercooldown(ancientid, 3.0, pobj)
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_22.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_ice, slot_helmet, ancient_event_damagetake,
'Bluewyrm Skullhelm', 'If you take damage while under 30%% health, reduce all damage taken by 25%% for 6 sec. (12 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if utils.getlifep(pobj.unit) < 50.0 then
mis_blizzard:attachu(pobj.unit, 6.0, 'hand,left', 0.75)
mis_blizzard:attachu(pobj.unit, 6.0, 'hand,right', 0.75)
spell:enhanceresistpure(25, 6.0, pobj.p)
loot.ancient:registercooldown(ancientid, 12.0, pobj)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_23.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, biome_id_ice, slot_potion, ancient_event_potion,
'Frozen Chalice', 'On Health Potion use, become encased in invulnerable ice for 2.5 sec.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if flag then -- hpot only.
utils.setinvul(pobj.unit, true)
speff_iceblock:attachu(pobj.unit, 2.5, 'origin', 0.75)
utils.timed(2.5, function() if not pobj.downed then utils.setinvul(pobj.unit, false) end end)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_24.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
------ generic ancient pool ------
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, nil, slot_tool, ancient_event_damagedeal,
'Gravel Lob-o-matic', 'When you swing your pickaxe, lob 6 boulders in front of you which each deal 24 Physical damage in a 3m radius.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if event.source == pobj.unit and event.weptype ~= WEAPON_TYPE_WHOKNOWS and event.atktype == ATTACK_TYPE_HERO then
local x,y = utils.unitprojectxy(pobj.unit, 150.0, utils.getface(pobj.unit))
local x2,y2,a = 0, 0, math.random(0,360)
for i = 1,6 do
x2,y2 = utils.projectxy(x, y, math.random(0,150), a + (i-1)*60)
local mis = missile:create_arc(x2, y2, pobj.unit, mis_rock.effect, 24)
mis.dmgtype = dmg.type.stack[dmg_phys]
mis.time = 1.0
mis.height = 95.0
mis:initduration()
end
loot.ancient:registercooldown(ancientid, 1.0, pobj)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_25.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, nil, slot_potion, ancient_event_potion,
'Omni-Potion', 'On Mana Potion use, increase all non-physical damage by 25%% for 6 sec. On Health Potion use, increase your Armor Rating by 10%% for 6 sec.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if not flag then
for dmgid = p_stat_arcane,p_stat_shadow do spell:enhancepstat(dmgid, 25.0, 6.0, pobj.p) end
elseif flag then
spell:enhancepstat(p_stat_armor, math.ceil(10*(pobj[p_stat_armor]/100)), 6.0, pobj.p)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_26.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, nil, slot_backpack, ancient_event_damagetake,
"Fleeting Adventurer's Pack", 'If you take damage while below 50%% health, max your movespeed and gain 50%% bonus Dodge Rating for 6 sec. (8 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if utils.getlifep(pobj.unit) < 50.0 then
sp_enchant_eff[2]:attachu(pobj.unit, 8.0, 'origin', 0.7)
bf_msmax:apply(pobj.unit, 6.0)
spell:enhancepstat(p_stat_dodge, math.floor(50*(pobj[p_stat_dodge]/100)), 8.0, pobj.p)
loot.ancient:registercooldown(ancientid, 8.0, pobj)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_27.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, nil, slot_backpack, ancient_event_equip,
"Archaeologist's Travel Pack", 'Increases your mining speed by 25%%.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if flag then
if not pobj.ancientsdata.archpackbonus then
pobj.ancientsdata.archpackbonus = true
pobj:modstat(p_stat_minespd, true, 25)
end
else
if pobj.ancientsdata.archpackbonus then
pobj.ancientsdata.archpackbonus = false
pobj:modstat(p_stat_minespd, false, 25)
end
end
pobj:updateattackspeed()
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_28.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, nil, slot_artifact, ancient_event_equip,
'Deep Fathom Seashell', 'Summons 3 murloc warriors to melee nearby targets for 14 Nature damage.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if flag then
if not pobj.ancientsdata.murloct then
pobj.ancientsdata.murloct = {}
local x,y = 0,0
for i = 1,3 do
x,y = utils.projectxy(utils.unitx(pobj.unit), utils.unity(pobj.unit), 150.0, (i-1)*120)
pobj.ancientsdata.murloct[i] = {}
pobj.ancientsdata.murloct[i].unit = utils.unitatxy(pobj.p, x, y, 'n01Q', math.random(0,360))
pobj.ancientsdata.murloct[i].ai = ai:new(pobj.ancientsdata.murloct[i].unit)
pobj.ancientsdata.murloct[i].ai:initcompanion(900.0, 15.0, p_stat_nature)
end
end
elseif not flag then
if pobj.ancientsdata.murloct then
for i = 1,3 do
pobj.ancientsdata.murloct[i].ai:releasecompanion()
RemoveUnit(pobj.ancientsdata.murloct[i].unit)
pobj.ancientsdata.murloct[i] = nil
end
pobj.ancientsdata.murloct = nil
end
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_29.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, nil, slot_artifact, ancient_event_potion,
'Bauble of Endless Absorption', 'On Mana Potion use, gain 20 Absorb on Spell and increase your Absorb Power by 25%% for 6 sec.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if not flag then -- mpot
spell:enhancepstat(p_stat_shielding, 20, 6.0, pobj.p)
spell:enhancepstat(p_stat_absorb, math.floor(25*(pobj[p_stat_absorb]/100)), 6.0, pobj.p)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_30.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, nil, slot_candle, ancient_event_damagedeal,
'Mana-Hungry Spelltorch', 'Arcane damage is increased by 50%% while you are above 50%% mana, but is reduced by 25%% while below 50%% mana.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if atypelookup[event.atktype] == dmg_arcane then
if utils.getmanap(pobj.unit) >= 50 then
event.amount = event.amount*1.50
else
event.amount = event.amount*0.75
end
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_31.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, nil, slot_tool, ancient_event_damagedeal,
'Shadowy Ore Spiker', 'Targets within 3m of you take 25%% more damage and heal you for 25%% of the total damage done.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if utils.distunits(event.target, pobj.unit) <= 300.0 then
event.amount = event.amount*1.25
utils.addlifeval(pobj.unit, event.amount*0.25, true, pobj.unit)
speff_embersha:playu(event.target)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_32.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, nil, slot_gloves, ancient_event_damagedeal,
'Gauntlets of Impending Doom', 'You deal 5%% increased damage for every 10 Candle Wax burned during a dig.')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if map.manager.activemap then
event.amount = event.amount*(1+math.floor((candle.total - candle.current)/10)*0.05)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_33.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, nil, slot_outfit, ancient_event_ability,
'Darkness Hauberk', 'Using an ability gives you 5%% added Shadow damage for 6 sec. If used under 50%% health, also gain 10%% Elemental Lifesteal. (6 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if pobj.ancientsdata.darkhauberkeff then DestroyEffect(pobj.ancientsdata.darkhauberkeff) end
pobj.ancientsdata.darkhauberkeff = sp_armor_boost[5]:attachu(pobj.unit, 6.0, 'overhead')
spell:enhancedmgtype(dmg_shadow, 5.0, 6.0, pobj.p)
if not pobj.ancientsdata.darkhauberkcd and utils.getlifep(pobj.unit) < 50 then
pobj.ancientsdata.darkhauberkcd = true
utils.timed(6.0, function() pobj.ancientsdata.darkhauberkcd = false end)
spell:enhancepstat(p_stat_elels, 10.0, 6.0, pobj.p)
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_34.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, nil, slot_helmet, ancient_event_damagedeal,
'Looted Scourgehelm', 'Dealing damage has a 20%% chance to restore 3%% of your health. While under 35%% health, restore 6%% instead. (1.5 sec cooldown)')
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if event.amount > 10 then
if math.random(1,10) <= 2 then
if utils.getlifep(pobj.unit) >= 35.0 then
utils.addlifep(pobj.unit, 3.0, true, pobj.unit)
else
utils.addlifep(pobj.unit, 6.0, true, pobj.unit)
end
loot.ancient:registercooldown(ancientid, 1.5, pobj)
end
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_35.blp'
a_id = a_id + 1
----------------------------------
loot.ancienttable[a_id] = loot.ancient:new(a_id, nil, slot_boots, ancient_event_movement,
'Fusion Slipshoes', 'While moving, occasionally become invisible for 2.5 sec and absorb up to 84 damage for the duration. While standing still, deal 20%% more Arcane damage.')
loot.ancienttable[a_id].unequip = function(pobj)
if pobj.ancientsdata.fusionb then
pobj.ancientsdata.fusionb = false
pobj:modstat(p_stat_arcane, false, 20)
end
end
loot.ancienttable[a_id].func = function(ancientid, pobj, flag, event)
if flag then
if pobj.ancientsdata.fusionb then
pobj.ancientsdata.fusionb = false
pobj:modstat(p_stat_arcane, false, 20)
end
if math.random(1,100) < 3 and not pobj.ancientsdata.fusioncd then
pobj.ancientsdata.fusioncd = true
bf_invis:apply(pobj.unit, 2.5)
dmg.absorb:new(pobj.unit, 2.5, { all = 84 }, speff_uberblu)
utils.timed(2.5, function() pobj.ancientsdata.fusioncd = nil end)
end
else
if not pobj.ancientsdata.fusionb then
pobj.ancientsdata.fusionb = true
pobj:modstat(p_stat_arcane, true, 20)
end
end
end
loot.ancienttable[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_36.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
-------- itemtype builder --------
----------------------------------
loot.ancienttypes = {}
-- construct itemtypes with icon, ancientid (for use in save/load system):
for ancientid,t in ipairs(loot.ancienttable) do
loot.ancienttypes[ancientid] = loot.itemtype:new(t.fullname, loot:slot(t.slotid), 1, t.icon)
loot.ancienttypes[ancientid].ancientid = ancientid
end
end
function loot:builtancienttable_v1_5()
-- MUST start at the last ancient_id + 1 for save/load system!
-- MUST insert this function at the END of lootdata build functions!
local a_id = 37
loot.ancienttable_v_1_5 = {}
-- biome_id_slag Fire/Shadow
-- biome_id_mire Nature/Shadow
-- biome_id_foss Physical/Nature
-- biome_id_ice Frost/Arcane
-- biome_id_vault Physical/Shadow
loot.ancienttable_v_1_5[a_id] = loot.ancient:new(a_id, biome_id_slag, slot_artifact, ancient_event_ability,
'Pearlescent Prism', 'Using an ability increases all non-physical damage dealt by 3%% for 8 seconds.')
loot.ancienttable_v_1_5[a_id].func = function(ancientid, pobj, flag, event)
for dmgid = p_stat_arcane,p_stat_shadow do spell:enhancepstat(dmgid, 3.0, 8.0, pobj.p) end
end
loot.ancienttable_v_1_5[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_2_01.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
loot.ancienttable_v_1_5[a_id] = loot.ancient:new(a_id, biome_id_mire, slot_potion, ancient_event_potion,
'Summoning Elixir', 'On Mana Potion use, increase Minion damage dealt by 50%% for 10 seconds.')
loot.ancienttable_v_1_5[a_id].func = function(ancientid, pobj, flag, event)
if not flag then -- mpot
spell:enhancepstat(p_stat_miniondmg, 50.0, 10.0, pobj.p)
end
end
loot.ancienttable_v_1_5[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_2_02.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
loot.ancienttable_v_1_5[a_id] = loot.ancient:new(a_id, biome_id_foss, slot_tool, ancient_event_ability,
'Fossilized Wishbone', 'Using an ability increases your chance to critically strike by 3%% for 10 seconds.')
loot.ancienttable_v_1_5[a_id].func = function(ancientid, pobj, flag, event)
spell:enhancepstat(p_epic_hit_crit, 3.0, 10.0, pobj.p)
end
loot.ancienttable_v_1_5[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_2_03.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
loot.ancienttable_v_1_5[a_id] = loot.ancient:new(a_id, biome_id_ice, slot_leggings, ancient_event_damagedeal,
'Necromancer Strides', 'Dealing damage while above 75%% health gives you a 25%% chance to summon a demon to your side on ability use.')
loot.ancienttable_v_1_5[a_id].func = function(ancientid, pobj, flag, event)
if utils.getlifep(pobj.unit) >= 75 then
spell:enhancepstat(p_epic_demon, 25.0, 3.0, pobj.p)
loot.ancient:registercooldown(ancientid, 3.0, pobj)
end
end
loot.ancienttable_v_1_5[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_2_04.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
loot.ancienttable_v_1_5[a_id] = loot.ancient:new(a_id, biome_id_vault, slot_candle, ancient_event_equip,
"Archaeologist's Lantern", 'Increases your mining speed by 25%%.')
loot.ancienttable_v_1_5[a_id].func = function(ancientid, pobj, flag, event)
if flag then
if not pobj.ancientsdata.archlantbonus then
pobj.ancientsdata.archlantbonus = true
pobj:modstat(p_stat_minespd, true, 25)
end
else
if pobj.ancientsdata.archlantbonus then
pobj.ancientsdata.archlantbonus = false
pobj:modstat(p_stat_minespd, false, 25)
end
end
end
loot.ancienttable_v_1_5[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_2_05.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
loot.ancienttable_v_1_5[a_id] = loot.ancient:new(a_id, biome_id_slag, slot_candle, ancient_event_equip,
"Phoenix Flame", 'Summons a Phoenix companion which launches fiery bolts at up to 3 targets at a time for 24 Fire damage.')
loot.ancienttable_v_1_5[a_id].func = function(ancientid, pobj, flag, event)
if flag then
if not pobj.ancientsdata.phxunitB then
pobj.ancientsdata.phxunitB = utils.unitatxy(pobj.p, utils.unitx(pobj.unit), utils.unity(pobj.unit), 'n01L', math.random(0,360))
pobj.ancientsdata.phxunitBai = ai:new(pobj.ancientsdata.phxunitB)
pobj.ancientsdata.phxunitBai:initcompanion(900.0, 24.0, p_stat_fire)
end
elseif not flag then
if pobj.ancientsdata.phxunitB then RemoveUnit(pobj.ancientsdata.phxunitB) pobj.ancientsdata.phxunitB = nil end
if pobj.ancientsdata.phxunitBai then pobj.ancientsdata.phxunitBai:releasecompanion() end
end
end
loot.ancienttable_v_1_5[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_2_06.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
loot.ancienttable_v_1_5[a_id] = loot.ancient:new(a_id, biome_id_mire, slot_gloves, ancient_event_damagedeal,
"Mire Wranglers", 'Increases your Nature and Minion damage by 25%%.')
loot.ancienttable_v_1_5[a_id].func = function(ancientid, pobj, flag, event)
if dtypelookup[event.dmgtype] == dmg_nature or atypelookup[event.atktype] == dmg_nature then
event.amount = event.amount*1.25
end
if event.pmin or event.pminrep or event.target ~= pobj.unit then
event.amount = event.amount*1.25
end
end
loot.ancienttable_v_1_5[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_2_07.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
loot.ancienttable_v_1_5[a_id] = loot.ancient:new(a_id, biome_id_foss, slot_tool, ancient_event_equip,
"Skulltooth Channeling Rod", '50%% of your highest non-physical damage bonus is granted to all other non-physical damage bonuses.')
loot.ancienttable_v_1_5[a_id].func = function(ancientid, pobj, flag, event)
if flag then
pobj.ancientsdata.skulltoothbonusfunc = function()
if pobj.ancientsdata.skulltoothbonus then
for dmgid = p_stat_arcane,p_stat_shadow do
if dmgid ~= pobj.ancientsdata.skulltoothbonusid then
pobj:modstat(dmgid, false, pobj.ancientsdata.skulltoothbonus)
end
end
end
local highestid, highestvalue = 0, 0
for dmgid = p_stat_arcane,p_stat_shadow do
if pobj[dmgid] > highestvalue then
highestid = dmgid
highestvalue = pobj[dmgid]
end
end
pobj.ancientsdata.skulltoothbonus = math.floor(highestvalue*0.5)
pobj.ancientsdata.skulltoothbonusid = highestid
for dmgid = p_stat_arcane,p_stat_shadow do
if dmgid ~= highestid then
pobj:modstat(dmgid, true, pobj.ancientsdata.skulltoothbonus)
end
end
end
pobj.ancientsdata.skulltoothbonustmr = TimerStart(NewTimer(), 0.1, true, function()
if pobj.ancientsdata.skulltoothbonusfunc then pobj.ancientsdata.skulltoothbonusfunc() else ReleaseTimer() end
end)
else
for dmgid = p_stat_arcane,p_stat_shadow do
if dmgid ~= pobj.ancientsdata.skulltoothbonusid then
pobj:modstat(dmgid, false, pobj.ancientsdata.skulltoothbonus)
end
end
pobj.ancientsdata.skulltoothbonus = nil
pobj.ancientsdata.skulltoothbonusid = nil
pobj.ancientsdata.skulltoothbonusfunc = nil
end
end
loot.ancienttable_v_1_5[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_2_08.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
loot.ancienttable_v_1_5[a_id] = loot.ancient:new(a_id, biome_id_ice, slot_backpack, ancient_event_damagedeal,
"Portable Storage Cooler", 'Increases damage dealt to Frozen, Stunned or Rooted targets by 25%%.')
loot.ancienttable_v_1_5[a_id].func = function(ancientid, pobj, flag, event)
if UnitHasBuffBJ(event.target, bf_stun.buffcode) or UnitHasBuffBJ(event.target, bf_freeze.buffcode) or UnitHasBuffBJ(event.target, bf_root.buffcode) then
event.amount = event.amount *1.25
end
end
loot.ancienttable_v_1_5[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_2_09.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
loot.ancienttable_v_1_5[a_id] = loot.ancient:new(a_id, biome_id_vault, slot_artifact, ancient_event_damagedeal,
"Time-Lost Amalgam", 'Increases damage dealt to Bosses by 25%%.')
loot.ancienttable_v_1_5[a_id].func = function(ancientid, pobj, flag, event)
if IsUnitType(event.target, UNIT_TYPE_ANCIENT) then
event.amount = event.amount*1.25
end
end
loot.ancienttable_v_1_5[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_2_10.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
loot.ancienttable_v_1_5[a_id] = loot.ancient:new(a_id, biome_id_vault, slot_outfit, ancient_event_equip,
"Gleaming Vault-Plate", 'Increases your Treasure Find by 100%%.')
loot.ancienttable_v_1_5[a_id].func = function(ancientid, pobj, flag, event)
if flag then
pobj:modstat(p_stat_treasure, true, 100)
else
pobj:modstat(p_stat_treasure, false, 100)
end
end
loot.ancienttable_v_1_5[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_2_11.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
loot.ancienttable_v_1_5[a_id] = loot.ancient:new(a_id, biome_id_vault, slot_leggings, ancient_event_damagetake,
"Liquid Gold Legplates", 'Reduces the damage you receive from Bosses by 10%%.')
loot.ancienttable_v_1_5[a_id].func = function(ancientid, pobj, flag, event)
if event.target == pobj.unit and IsUnitType(event.source, UNIT_TYPE_ANCIENT) then
event.amount = event.amount*0.9
end
end
loot.ancienttable_v_1_5[a_id].icon = 'war3mapImported\\item_loot_icons_ancient_2_12.blp'
a_id = a_id + 1
----------------------------------
----------------------------------
for ancientid,t in pairs(loot.ancienttable_v_1_5) do
loot.ancienttypes[ancientid] = loot.itemtype:new(t.fullname, loot:slot(t.slotid), 1, t.icon)
loot.ancienttypes[ancientid].ancientid = ancientid
-- MUST add new ancients to the original ancient table:
loot.ancienttable[ancientid] = t
end
end
function loot:globals()
self.slottable = {}
self.raritytable = {}
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- base keyword assignment:
-- [Prefix] [TypeMod] [ItemType] [Suffix]
-- [1] [2] [n] [3]
kw_type_prefix = 1
kw_type_typemod = 2
kw_type_suffix = 3
kw_type_secondary = 4
kw_type_epic = 5
kw_type_ancient = 6
kw_type_digsite = 7
kw_type_potion = 8
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- statmod types:
modtype_relative = 1
modtype_absolute = 2
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- rarity data:
rarity_common = 1
rarity_rare = 2
rarity_epic = 3
rarity_ancient = 4
rarity_ids = {
[1] = rarity_common,
[2] = rarity_rare,
[3] = rarity_epic,
[4] = rarity_ancient,
}
rarity_color = {
[rarity_common] = "5bff45",
[rarity_rare] = "4568ff",
[rarity_epic] = "c445ff",
[rarity_ancient] = "ff9c00",
}
rarity_odds = {
[rarity_common] = 10000,
[rarity_rare] = 1500,
[rarity_epic] = 300,
[rarity_ancient] = 50,
}
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- equipment slot ids:
slot_helmet = 1001
slot_outfit = 1002
slot_leggings = 1003
slot_tool = 1004
slot_potion = 1005
slot_boots = 1006
slot_gloves = 1007
slot_backpack = 1008
slot_artifact = 1009
slot_candle = 1010
-- misc:
slot_digkey = 1011
slot_all_t = { -- used in fetch random slot id.
slot_helmet,slot_outfit,slot_leggings,
slot_tool,slot_potion,slot_boots,
slot_gloves,slot_backpack,slot_artifact,
slot_candle }
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- keyword groups:
kw_group_elemental = { slot_artifact, slot_candle, slot_tool }
kw_group_attack = { slot_artifact, slot_tool, slot_gloves }
kw_group_resist = { slot_artifact, slot_helmet, slot_outfit, slot_leggings, slot_boots, slot_gloves }
kw_group_resist_two = { slot_artifact, slot_helmet, slot_outfit, slot_leggings, slot_boots, slot_gloves, slot_backpack }
kw_group_wax = { slot_candle, slot_backpack, slot_artifact }
kw_group_utility = { slot_potion, slot_artifact, slot_candle, slot_backpack }
kw_group_movement = { slot_boots, slot_backpack }
kw_group_char = { slot_outfit, slot_helmet, slot_leggings, slot_potion, slot_gloves }
kw_group_defense = { slot_artifact, slot_outfit }
kw_group_castspeed = { slot_artifact, slot_candle, slot_gloves, slot_tool }
kw_group_equipment = { slot_helmet, slot_gloves, slot_leggings, slot_outfit, slot_boots }
kw_group_tool = { slot_tool }
kw_group_digsite = { slot_digkey }
kw_group_onpotion = { slot_artifact, slot_potion }
kw_group_potion = { slot_potion }
kw_group_artifact = { slot_artifact }
kw_group_all = { slot_helmet,slot_outfit,slot_leggings,slot_tool,slot_potion,slot_boots,slot_gloves,slot_backpack,slot_artifact,slot_candle }
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- initialize slot ids:
self.slottable[slot_helmet] = loot.slottype:new(slot_helmet, "Helmet")
self.slottable[slot_outfit] = loot.slottype:new(slot_outfit, "Outfit")
self.slottable[slot_leggings] = loot.slottype:new(slot_leggings, "Leggings")
self.slottable[slot_tool] = loot.slottype:new(slot_tool, "Tool")
self.slottable[slot_potion] = loot.slottype:new(slot_potion, "Potion")
self.slottable[slot_boots] = loot.slottype:new(slot_boots, "Boots")
self.slottable[slot_gloves] = loot.slottype:new(slot_gloves, "Gloves")
self.slottable[slot_backpack] = loot.slottype:new(slot_backpack, "Backpack")
self.slottable[slot_artifact] = loot.slottype:new(slot_artifact, "Artifact")
self.slottable[slot_candle] = loot.slottype:new(slot_candle, "Candle")
-- non-equipment types:
self.slottable[slot_digkey] = loot.slottype:new(slot_digkey, "Dig Site Key")
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- initialize rarities:
self.raritytable[rarity_common] = loot.rarity:new(rarity_common, "war3mapImported\\rarity-gem-icons_01.blp", "Common")
self.raritytable[rarity_common].tooltipbg = "war3mapImported\\tooltip-bg_scroll_common.blp"
self.raritytable[rarity_common].rareind = "war3mapImported\\item-rarity_indicator_01.blp"
self.raritytable[rarity_rare] = loot.rarity:new(rarity_rare, "war3mapImported\\rarity-gem-icons_02.blp", "Rare")
self.raritytable[rarity_rare].tooltipbg = "war3mapImported\\tooltip-bg_scroll_rare.blp"
self.raritytable[rarity_rare].rareind = "war3mapImported\\item-rarity_indicator_02.blp"
self.raritytable[rarity_rare].sellfactor = 1.1
self.raritytable[rarity_rare].maxfactor = 0.65
self.raritytable[rarity_rare].rollfactor = 1.1
self.raritytable[rarity_epic] = loot.rarity:new(rarity_epic, "war3mapImported\\rarity-gem-icons_03.blp", "Epic")
self.raritytable[rarity_epic].tooltipbg = "war3mapImported\\tooltip-bg_scroll_epic.blp"
self.raritytable[rarity_epic].rareind = "war3mapImported\\item-rarity_indicator_03.blp"
self.raritytable[rarity_epic].sellfactor = 1.2
self.raritytable[rarity_epic].maxfactor = 0.8
self.raritytable[rarity_epic].rollfactor = 1.3
self.raritytable[rarity_ancient] = loot.rarity:new(rarity_ancient, "war3mapImported\\rarity-gem-icons_04.blp", "Ancient")
self.raritytable[rarity_ancient].tooltipbg = "war3mapImported\\tooltip-bg_scroll_ancient.blp"
self.raritytable[rarity_ancient].rareind = "war3mapImported\\item-rarity_indicator_04.blp"
self.raritytable[rarity_ancient].sellfactor = 1.3
self.raritytable[rarity_ancient].maxfactor = 0.9
self.raritytable[rarity_ancient].rollfactor = 1.5
end
function loot:buildtables()
loot:buildtypetable()
loot:buildstatmodtable()
loot:buildkwtable()
loot:builtancienttable()
loot:builtancienttable_v1_5()
end
function loot:getslotname(slotid)
return self.slottable[slotid].name
end
function loot:buildtypetable()
loot.itemtable = {}
for slotid = slot_helmet,slot_digkey do
loot.itemtable[slotid] = {}
end
ilvl_map = {1,10,20,30,40,50,60}
-- builds item types for slotids based on index-ordered .blp icon file names.
loot:builditemtype(slot_boots, {"Slippers", "Boots", "Cavewaders", "Greaves", "Stompers", "Ghostcleats", "Slag Stompers"}, 1)
loot:builditemtype(slot_gloves, {"Mittons", "Wristwraps", "Gloves", "Gauntlets", "Rockpaws", "Ghostgrasps", "Grottogrips"}, 8)
loot:builditemtype(slot_artifact, {"Stone Bead", "Necklace", "Pendant", "Choker", "Jewel", "Carcanet", "Lavalliere"}, 15)
loot:builditemtype(slot_outfit, {"Ratwraps", "Vest", "Chainmail", "Chestplate", "Boneplate", "Ghostwraps", "Slag Garb"}, 22)
loot:builditemtype(slot_leggings, {"Slacks", "Pants", "Pantaloons", "Chainchaps", "Studjeans", "Ghostwalkers", "Slag Trousers"}, 29)
loot:builditemtype(slot_helmet, {"Cap", "Helm", "Chain Helm", "Hard Hat", "Plate Helm", "Bonehat", "Deephelm"}, 36)
loot:builditemtype(slot_tool, {"Shovel", "Rock Spade", "Stonecutter", "Ore-Chopper", "Ore-Cleaver", "Autoclaw", "Drillmatic"}, 43)
-- loot:builditemtype(slot_tool, {"Gnarlstaff", "Staff", "Moonstaff", "Stone Wand", "Molepole", "Energy Rod", "Terminus"}, 50)
loot:builditemtype(slot_candle, {"Candle", "Flambeau", "Waxtorch", "Waxwand", "Bonetorch", "Brazier", "Candlelight Lens"}, 57)
loot:builditemtype(slot_potion, {"Tonic", "Potion", "Elixir", "Spirit Beaker", "Spirit Bottle", "Spirit Flask", "Spirit Vessel"}, 64)
loot:builditemtype(slot_backpack, {"Rucksack", "Backpack", "Ore Sack", "Satchel", "Duffel", "Ornate Duffel", "Ornate Backpack"}, 71)
-- dig keys:
loot.itemtable[slot_digkey][1] = loot.itemtype:new("Ruby Relic", loot:slot(slot_digkey), 1, "war3mapImported\\icon_bosskey_chomp.blp")
loot.itemtable[slot_digkey][2] = loot.itemtype:new("Topaz Relic", loot:slot(slot_digkey), 1, "war3mapImported\\icon_bosskey_slag.blp")
loot.itemtable[slot_digkey][3] = loot.itemtype:new("Emerald Relic", loot:slot(slot_digkey), 1, "war3mapImported\\icon_bosskey_mutant.blp")
loot.itemtable[slot_digkey][4] = loot.itemtype:new("Sapphire Relic", loot:slot(slot_digkey), 1, "war3mapImported\\icon_bosskey_thawed.blp")
loot.itemtable[slot_digkey][5] = loot.itemtype:new("Corrupt Relic", loot:slot(slot_digkey), 1, "war3mapImported\\icon_bosskey_amalgam.blp")
-- lookup table for dig key itemtypes (used in save/load):
loot.digkeyt = {
[loot.itemtable[slot_digkey][1].id] = 1,
[loot.itemtable[slot_digkey][2].id] = 2,
[loot.itemtable[slot_digkey][3].id] = 3,
[loot.itemtable[slot_digkey][4].id] = 4,
[loot.itemtable[slot_digkey][5].id] = 5,
}
end
function loot:builditemtype(slotid, namet, iconindex, _count)
local count = _count or 7
for id = 1,count do
local idex,iconstr = iconindex-1+id,""
if idex < 10 then
iconstr = "war3mapImported\\item_loot_icons_0"..idex..".blp"
else
iconstr = "war3mapImported\\item_loot_icons_"..idex..".blp"
end
loot.itemtable[slotid][#loot.itemtable[slotid]+1] = loot.itemtype:new(namet[id], loot:slot(slotid), ilvl_map[id], iconstr)
end
end
function loot:buildstatmodtable()
loot.smt = {} -- item stat mods
loot.digmt = {} -- dig site stat mods
-- primary:
loot.smt[p_stat_strength_b] = loot.statmod:new("Strength", p_stat_strength_b, 2, 6, 0.03, true)
loot.smt[p_stat_strength_b].icon= "ReplaceableTextures\\CommandButtons\\BTNGauntletsOfOgrePower.blp"
loot.smt[p_stat_wisdom_b] = loot.statmod:new("Wisdom", p_stat_wisdom_b, 2, 6, 0.03, true)
loot.smt[p_stat_wisdom_b].icon = "ReplaceableTextures\\CommandButtons\\BTNPendantOfMana.blp"
loot.smt[p_stat_alacrity_b] = loot.statmod:new("Alacrity", p_stat_alacrity_b, 2, 6, 0.03, true)
loot.smt[p_stat_alacrity_b].icon= "ReplaceableTextures\\CommandButtons\\BTNDaggerOfEscape.blp"
loot.smt[p_stat_vitality_b] = loot.statmod:new("Vitality", p_stat_vitality_b, 2, 6, 0.03, true)
loot.smt[p_stat_vitality_b].icon= "ReplaceableTextures\\CommandButtons\\BTNPeriapt.blp"
loot.smt[p_stat_wax] = loot.statmod:new("Wax Efficiency", p_stat_wax, 2, 6, 0.005)
loot.smt[p_stat_wax].icon = "ReplaceableTextures\\CommandButtons\\BTNPotionOfRestoration.blp"
loot.smt[p_stat_bms] = loot.statmod:new("Bonus Movespeed", p_stat_bms, 2, 10, 0.010)
loot.smt[p_stat_bms].icon = "ReplaceableTextures\\CommandButtons\\BTNBootsOfSpeed.blp"
loot.smt[p_stat_bhp] = loot.statmod:new("Bonus Health", p_stat_bhp, 2, 10, 0.005)
loot.smt[p_stat_bhp].icon = "ReplaceableTextures\\CommandButtons\\BTNPeriapt.blp"
loot.smt[p_stat_bmana] = loot.statmod:new("Bonus Mana", p_stat_bmana, 2, 10, 0.005)
loot.smt[p_stat_bmana].icon = "ReplaceableTextures\\CommandButtons\\BTNPendantOfMana.blp"
loot.smt[p_stat_fear] = loot.statmod:new("Fear", p_stat_fear, 1, 10, 0.025)
loot.smt[p_stat_cowardice] = loot.statmod:new("Cowardice", p_stat_cowardice, 1, 10, 0.025)
loot.smt[p_stat_paranoia] = loot.statmod:new("Paranoia", p_stat_paranoia, 1, 10, 0.025)
loot.smt[p_stat_arcane] = loot.statmod:new(color:wrap(color.dmg.arcane, "Arcane").." Damage", p_stat_arcane, 4, 8, 0.015)
loot.smt[p_stat_arcane].icon = "ReplaceableTextures\\CommandButtons\\BTNManaBurn.blp"
loot.smt[p_stat_frost] = loot.statmod:new(color:wrap(color.dmg.frost, "Frost").." Damage", p_stat_frost, 4, 8, 0.015)
loot.smt[p_stat_frost].icon = "ReplaceableTextures\\CommandButtons\\BTNDarkRitual.blp"
loot.smt[p_stat_nature] = loot.statmod:new(color:wrap(color.dmg.nature, "Nature").." Damage", p_stat_nature, 4, 8, 0.015)
loot.smt[p_stat_nature].icon = "ReplaceableTextures\\CommandButtons\\BTNRegenerate.blp"
loot.smt[p_stat_fire] = loot.statmod:new(color:wrap(color.dmg.fire, "Fire").." Damage", p_stat_fire, 4, 8, 0.015)
loot.smt[p_stat_fire].icon = "ReplaceableTextures\\CommandButtons\\BTNImmolationOn.blp"
loot.smt[p_stat_shadow] = loot.statmod:new(color:wrap(color.dmg.shadow, "Shadow").." Damage", p_stat_shadow, 4, 8, 0.015)
loot.smt[p_stat_shadow].icon = "ReplaceableTextures\\CommandButtons\\BTNFaerieFire.blp"
loot.smt[p_stat_phys] = loot.statmod:new(color:wrap(color.dmg.physical, "Physical").." Damage", p_stat_phys, 4, 8, 0.015)
loot.smt[p_stat_phys].icon = "ReplaceableTextures\\CommandButtons\\BTNDeathPact.blp"
loot.smt[p_stat_arcane_res] = loot.statmod:new(color:wrap(color.dmg.arcane, "Arcane").." Resistance", p_stat_arcane_res, 3, 7, 0.01)
loot.smt[p_stat_arcane_res].icon= "ReplaceableTextures\\CommandButtons\\BTNReplenishMana.blp"
loot.smt[p_stat_frost_res] = loot.statmod:new(color:wrap(color.dmg.frost, "Frost").." Resistance", p_stat_frost_res, 3, 7, 0.01)
loot.smt[p_stat_frost_res].icon = "ReplaceableTextures\\CommandButtons\\BTNBreathOfFrost.blp"
loot.smt[p_stat_nature_res] = loot.statmod:new(color:wrap(color.dmg.nature, "Nature").." Resistance", p_stat_nature_res, 3, 7, 0.01)
loot.smt[p_stat_nature_res].icon= "ReplaceableTextures\\CommandButtons\\BTNMonsoon.blp"
loot.smt[p_stat_fire_res] = loot.statmod:new(color:wrap(color.dmg.fire, "Fire").." Resistance", p_stat_fire_res, 3, 7, 0.01)
loot.smt[p_stat_fire_res].icon = "ReplaceableTextures\\CommandButtons\\BTNFire.blp"
loot.smt[p_stat_shadow_res] = loot.statmod:new(color:wrap(color.dmg.shadow, "Shadow").." Resistance", p_stat_shadow_res, 3, 7, 0.01)
loot.smt[p_stat_shadow_res].icon= "ReplaceableTextures\\CommandButtons\\BTNPossession.blp"
loot.smt[p_stat_phys_res] = loot.statmod:new(color:wrap(color.dmg.physical, "Physical").." Resistance", p_stat_phys_res, 3, 7, 0.01)
loot.smt[p_stat_phys_res].icon = "ReplaceableTextures\\CommandButtons\\BTNImpale.blp"
loot.smt[p_stat_healing] = loot.statmod:new("Healing Power", p_stat_healing, 2, 8, 0.0125)
loot.smt[p_stat_healing].icon = "ReplaceableTextures\\CommandButtons\\BTNHealingWard.blp"
loot.smt[p_stat_absorb] = loot.statmod:new("Absorb Power", p_stat_absorb, 2, 8, 0.015)
loot.smt[p_stat_absorb].icon = "ReplaceableTextures\\CommandButtons\\BTNPurge.blp"
loot.smt[p_stat_minepwr] = loot.statmod:new("Mining Power", p_stat_minepwr, 3, 5, 0.025)
loot.smt[p_stat_minepwr].icon = "ReplaceableTextures\\CommandButtons\\BTNGatherGold.blp"
loot.smt[p_stat_eleproc] = loot.statmod:new("Proliferation", p_stat_eleproc, 1, 5, 0.010)
loot.smt[p_stat_eleproc].icon = "ReplaceableTextures\\CommandButtons\\BTNScatterRockets.blp"
loot.smt[p_stat_thorns] = loot.statmod:new("Thorns", p_stat_thorns, 1, 2, 0.005, true)
loot.smt[p_stat_thorns].icon = "ReplaceableTextures\\CommandButtons\\BTNMetamorphosis.blp"
loot.smt[p_stat_shielding] = loot.statmod:new("Absorb on Spell Use", p_stat_shielding, 2, 5, 0.010, true)
loot.smt[p_stat_shielding].icon = "ReplaceableTextures\\CommandButtons\\BTNAbsorbMagic.blp"
loot.smt[p_stat_dodge] = loot.statmod:new("Dodge Rating", p_stat_dodge, 2, 5, 0.015, true)
loot.smt[p_stat_dodge].icon = "ReplaceableTextures\\CommandButtons\\BTNCloudOfFog.blp"
loot.smt[p_stat_armor] = loot.statmod:new("Armor Rating", p_stat_armor, 3, 6, 0.015, true)
loot.smt[p_stat_armor].icon = "ReplaceableTextures\\CommandButtons\\BTNThoriumArmor.blp"
loot.smt[p_stat_miniondmg] = loot.statmod:new("Minion Damage", p_stat_miniondmg, 2, 5, 0.010)
loot.smt[p_stat_miniondmg].icon = "ReplaceableTextures\\CommandButtons\\BTNDarkSummoning.blp"
-- secondary:
loot.smt[p_stat_treasure] = loot.statmod:new("Treasure Find", p_stat_treasure, 2, 5, 0.010)
loot.smt[p_stat_treasure].icon = "ReplaceableTextures\\CommandButtons\\BTNMagicalSentry.blp"
loot.smt[p_stat_digxp] = loot.statmod:new("Dig Site XP", p_stat_digxp, 2, 5, 0.010)
loot.smt[p_stat_digxp].icon = "ReplaceableTextures\\CommandButtons\\BTNSorceressAdept.blp"
loot.smt[p_stat_mislrange] = loot.statmod:new("Missile Range", p_stat_mislrange, 3, 6, 0.005)
loot.smt[p_stat_mislrange].icon = "ReplaceableTextures\\CommandButtons\\BTNDwarvenLongRifle.blp"
loot.smt[p_stat_abilarea] = loot.statmod:new("Ability Radius", p_stat_abilarea, 3, 6, 0.005)
loot.smt[p_stat_abilarea].icon = "ReplaceableTextures\\CommandButtons\\BTNUpgradeMoonGlaive.blp"
loot.smt[p_stat_physls] = loot.statmod:new("Physical Lifesteal", p_stat_physls, 1, 3, 0.005)
loot.smt[p_stat_physls].icon = "ReplaceableTextures\\CommandButtons\\BTNVampiricAura.blp"
loot.smt[p_stat_elels] = loot.statmod:new("Elemental Lifesteal", p_stat_elels, 1, 3, 0.005)
loot.smt[p_stat_elels].icon = "ReplaceableTextures\\CommandButtons\\BTNDevourMagic.blp"
loot.smt[p_stat_potionpwr] = loot.statmod:new("Health Potion Power", p_stat_potionpwr, 1, 5, 0.005)
loot.smt[p_stat_potionpwr].icon = "ReplaceableTextures\\CommandButtons\\BTNPotionGreenSmall.blp"
loot.smt[p_stat_artifactpwr] = loot.statmod:new("Mana Potion Power", p_stat_artifactpwr, 1, 5, 0.005)
loot.smt[p_stat_artifactpwr].icon = "ReplaceableTextures\\CommandButtons\\BTNPotionBlueSmall.blp"
loot.smt[p_stat_vendor] = loot.statmod:new("Item Sell Value", p_stat_vendor, 5, 6, 0.005)
loot.smt[p_stat_vendor].icon = "ReplaceableTextures\\CommandButtons\\BTNChestOfGold.blp"
loot.smt[p_stat_castspeed] = loot.statmod:new("Casting Speed", p_stat_castspeed, 1, 4, 0.010)
loot.smt[p_stat_castspeed].icon = "ReplaceableTextures\\CommandButtons\\BTNWandOfCyclone.blp"
loot.smt[p_stat_dmg_reduct] = loot.statmod:new("Damage Reduction", p_stat_dmg_reduct, 1, 1, 0.005)
loot.smt[p_stat_dmg_reduct].icon= "ReplaceableTextures\\CommandButtons\\BTNDefend.blp"
-- dig site:
loot.digmt[m_stat_health] = loot.statmod:new("Monster Health", m_stat_health, 5, 12, 0.01)
loot.digmt[m_stat_health].icon = ""
loot.digmt[m_stat_attack] = loot.statmod:new("Monster Strength", m_stat_attack, 5, 12, 0.01)
loot.digmt[m_stat_attack].icon = ""
loot.digmt[m_stat_enemy_res] = loot.statmod:new("Monster Resistances", m_stat_enemy_res, 5, 12, 0.0025)
loot.digmt[m_stat_enemy_res].icon = ""
loot.digmt[m_stat_density] = loot.statmod:new("Monster Density", m_stat_density, 5, 12, 0.01)
loot.digmt[m_stat_density].icon = ""
loot.digmt[m_stat_treasure] = loot.statmod:new("Treasure Find", m_stat_treasure, 5, 12, 0.01)
loot.digmt[m_stat_treasure].icon= ""
loot.digmt[m_stat_xp] = loot.statmod:new("Dig Site XP", m_stat_xp, 5, 12, 0.005)
loot.digmt[m_stat_xp].icon = ""
-- potion:
loot.smt[p_potion_life] = loot.statmod:newlong("On Health Potion use, restore #v bonus life over 10 sec.", p_potion_life, 10, 20, 0.005)
loot.smt[p_potion_mana] = loot.statmod:newlong("On Mana Potion use, restore #v bonus mana over 10 sec.", p_potion_mana, 4, 16, 0.005)
loot.smt[p_potion_arcane_dmg] = loot.statmod:newlong("On Mana Potion use, gain #v added "..color:wrap(color.dmg.arcane, "Arcane").." damage for 6 sec.", p_potion_arcane_dmg, 5, 10, 0.0125)
loot.smt[p_potion_frost_dmg] = loot.statmod:newlong("On Mana Potion use, gain #v added "..color:wrap(color.dmg.frost, "Frost").." damage for 6 sec.", p_potion_frost_dmg, 5, 10, 0.0125)
loot.smt[p_potion_nature_dmg] = loot.statmod:newlong("On Mana Potion use, gain #v added "..color:wrap(color.dmg.nature, "Nature").." damage for 6 sec.", p_potion_nature_dmg, 5, 10, 0.0125)
loot.smt[p_potion_fire_dmg] = loot.statmod:newlong("On Mana Potion use, gain #v added "..color:wrap(color.dmg.fire, "Fire").." damage for 6 sec.", p_potion_fire_dmg, 5, 10, 0.0125)
loot.smt[p_potion_shadow_dmg] = loot.statmod:newlong("On Mana Potion use, gain #v added "..color:wrap(color.dmg.shadow, "Shadow").." damage for 6 sec.", p_potion_shadow_dmg, 5, 10, 0.0125)
loot.smt[p_potion_phys_dmg] = loot.statmod:newlong("On Mana Potion use, gain #v added "..color:wrap(color.dmg.physical, "Physical").." damage for 6 sec.", p_potion_phys_dmg, 5, 10, 0.0125)
loot.smt[p_potion_dmgr] = loot.statmod:newlong("On any Potion use, reduce all damage taken by #v for 6 sec.", p_potion_dmgr, 5, 10, 0.009)
loot.smt[p_potion_arcane_res] = loot.statmod:newlong("On Health Potion use, gain #v "..color:wrap(color.dmg.arcane, "Arcane").." resistance for 6 sec.", p_potion_arcane_res, 10, 20, 0.015)
loot.smt[p_potion_frost_res] = loot.statmod:newlong("On Health Potion use, gain #v "..color:wrap(color.dmg.frost, "Frost").." resistance for 6 sec.", p_potion_frost_res, 10, 20, 0.015)
loot.smt[p_potion_nature_res] = loot.statmod:newlong("On Health Potion use, gain #v "..color:wrap(color.dmg.nature, "Nature").." resistance for 6 sec.", p_potion_nature_res, 10, 20, 0.015)
loot.smt[p_potion_fire_res] = loot.statmod:newlong("On Health Potion use, gain #v "..color:wrap(color.dmg.fire, "Fire").." resistance for 6 sec.", p_potion_fire_res, 10, 20, 0.015)
loot.smt[p_potion_shadow_res] = loot.statmod:newlong("On Health Potion use, gain #v "..color:wrap(color.dmg.shadow, "Shadow").." resistance for 6 sec.", p_potion_shadow_res, 10, 20, 0.015)
loot.smt[p_potion_phys_res] = loot.statmod:newlong("On Health Potion use, gain #v "..color:wrap(color.dmg.physical, "Physical").." resistance for 6 sec.", p_potion_phys_res, 10, 20, 0.015)
loot.smt[p_potion_aoe_stun] = loot.statmod:newlong("On Health Potion use, stun all targets within 3m for #v sec.", p_potion_aoe_stun, 1, 3, 0.0025, true)
loot.smt[p_potion_aoe_slow] = loot.statmod:newlong("On Health Potion use, slow all targets within 3m for #v sec.", p_potion_aoe_slow, 2, 3, 0.0075, true)
loot.smt[p_potion_absorb] = loot.statmod:newlong("On any Potion use, absorb damage equal to #v of your max health for 6 sec.", p_potion_absorb, 3, 4, 0.015)
loot.smt[p_potion_armor] = loot.statmod:newlong("On any Potion use, increase your armor rating by #v for 8 sec.", p_potion_armor, 15, 25, 0.015)
loot.smt[p_potion_dodge] = loot.statmod:newlong("On any Potion use, increase your dodge rating by #v for 8 sec.", p_potion_dodge, 15, 25, 0.015)
loot.smt[p_potion_lifesteal] = loot.statmod:newlong("On any Potion use, gain #v lifesteal for 8 sec.", p_potion_lifesteal, 1, 10, 0.0015)
loot.smt[p_potion_thorns] = loot.statmod:newlong("On any Potion use, gain #v thorns and increase total thorns by 25#perc for 8 sec.", p_potion_thorns, 5, 10, 0.03, true)
--loot.smt[p_potion_aoe_heal] = loot.statmod:newlong("On Health Potion use, restore #v life to all other Kobolds within 10m.", p_potion_aoe_heal, 5, 10, 0.015)
--loot.smt[p_potion_aoe_mana] = loot.statmod:newlong("On Mana Potion use, restore #v mana to all other Kobolds within 10m.", p_potion_aoe_mana, 3, 7, 0.015)
-- epic:
loot.smt[p_epic_dmg_reduct] = loot.statmod:newlong("On ability use, reduce all damage taken by #v for 3 sec.", p_epic_dmg_reduct, 1, 2, 0.005)
loot.smt[p_epic_arcane_mis] = loot.statmod:newlong("Abilities have a 30#perc chance to launch a missile which pierces targets for #v "..color:wrap(color.dmg.arcane, "Arcane").." damage.", p_epic_arcane_mis, 5, 9, 0.015, true)
loot.smt[p_epic_frost_mis] = loot.statmod:newlong("Abilities have a 30#perc chance to launch a missile which pierces targets for #v "..color:wrap(color.dmg.frost, "Frost").." damage.", p_epic_frost_mis, 5, 9, 0.015, true)
loot.smt[p_epic_nature_mis] = loot.statmod:newlong("Abilities have a 30#perc chance to launch a missile which pierces targets for #v "..color:wrap(color.dmg.nature, "Nature").." damage.", p_epic_nature_mis, 5, 9, 0.015, true)
loot.smt[p_epic_fire_mis] = loot.statmod:newlong("Abilities have a 30#perc chance to launch a missile which pierces targets for #v "..color:wrap(color.dmg.fire, "Fire").." damage.", p_epic_fire_mis, 5, 9, 0.015, true)
loot.smt[p_epic_shadow_mis] = loot.statmod:newlong("Abilities have a 30#perc chance to launch a missile which pierces targets for #v "..color:wrap(color.dmg.shadow, "Shadow").." damage.", p_epic_shadow_mis, 5, 9, 0.015, true)
loot.smt[p_epic_phys_mis] = loot.statmod:newlong("Abilities have a 30#perc chance to launch a missile which pierces targets for #v "..color:wrap(color.dmg.physical, "Physical").." damage.", p_epic_phys_mis, 5, 9, 0.015, true)
loot.smt[p_epic_heal_aoe] = loot.statmod:newlong("Abilities have a #v chance to restore 3#perc life to all Kobolds within 10m.", p_epic_heal_aoe, 2, 5, 0.015)
loot.smt[p_epic_aoe_stun] = loot.statmod:newlong("Abilities have a #v chance to stun targets in a 3m radius for 2 sec.", p_epic_aoe_stun, 1, 3, 0.01)
loot.smt[p_epic_hit_crit] = loot.statmod:newlong("Abilities have a #v chance to critically strike, dealing 50#perc bonus damage.", p_epic_hit_crit, 2, 3, 0.005)
loot.smt[p_epic_arcane_conv] = loot.statmod:newlong("Convert #v of damage dealt to "..color:wrap(color.dmg.arcane, "Arcane")..".", p_epic_arcane_conv, 2, 5, 0.01)
loot.smt[p_epic_frost_conv] = loot.statmod:newlong("Convert #v of damage dealt to "..color:wrap(color.dmg.frost, "Frost")..".", p_epic_frost_conv, 2, 5, 0.01)
loot.smt[p_epic_nature_conv] = loot.statmod:newlong("Convert #v of damage dealt to "..color:wrap(color.dmg.nature, "Nature")..".", p_epic_nature_conv, 2, 5, 0.01)
loot.smt[p_epic_fire_conv] = loot.statmod:newlong("Convert #v of damage dealt to "..color:wrap(color.dmg.fire, "Fire")..".", p_epic_fire_conv, 2, 5, 0.01)
loot.smt[p_epic_shadow_conv] = loot.statmod:newlong("Convert #v of damage dealt to "..color:wrap(color.dmg.shadow, "Shadow")..".", p_epic_shadow_conv, 2, 5, 0.01)
loot.smt[p_epic_phys_conv] = loot.statmod:newlong("Convert #v of damage dealt to "..color:wrap(color.dmg.physical, "Physical")..".", p_epic_phys_conv, 2, 5, 0.01)
loot.smt[p_epic_arcane_aoe] = loot.statmod:newlong("Abilities have a 25#perc chance to unleash a nova that deals #v "..color:wrap(color.dmg.arcane, "Arcane").." damage in a 3m radius.", p_epic_arcane_aoe, 4, 8, 0.01, true)
loot.smt[p_epic_frost_aoe] = loot.statmod:newlong("Abilities have a 25#perc chance to unleash a nova that deals #v "..color:wrap(color.dmg.frost, "Frost").." damage in a 3m radius.", p_epic_frost_aoe, 4, 8, 0.01, true)
loot.smt[p_epic_nature_aoe] = loot.statmod:newlong("Abilities have a 25#perc chance to unleash a nova that deals #v "..color:wrap(color.dmg.nature, "Nature").." damage in a 3m radius.", p_epic_nature_aoe, 4, 8, 0.01, true)
loot.smt[p_epic_fire_aoe] = loot.statmod:newlong("Abilities have a 25#perc chance to unleash a nova that deals #v "..color:wrap(color.dmg.fire, "Fire").." damage in a 3m radius.", p_epic_fire_aoe, 4, 8, 0.01, true)
loot.smt[p_epic_shadow_aoe] = loot.statmod:newlong("Abilities have a 25#perc chance to unleash a nova that deals #v "..color:wrap(color.dmg.shadow, "Shadow").." damage in a 3m radius.", p_epic_shadow_aoe, 4, 8, 0.01, true)
loot.smt[p_epic_phys_aoe] = loot.statmod:newlong("Abilities have a 25#perc chance to unleash a nova that deals #v "..color:wrap(color.dmg.physical, "Physical").." damage in a 3m radius.", p_epic_phys_aoe, 4, 8, 0.01, true)
loot.smt[p_epic_demon] = loot.statmod:newlong("Abilities have a 25#perc chance to summon a demon for 12 sec to attack targets for #v "..color:wrap(color.dmg.shadow, "Shadow").." damage.", p_epic_demon, 3, 5, 0.01, true)
loot.smt[p_stat_dmg_arcane] = loot.statmod:newlong("Add #v bonus "..color:wrap(color.dmg.arcane, "Arcane").." damage to every attack and ability.", p_stat_dmg_arcane, 1, 3, 0.005)
loot.smt[p_stat_dmg_arcane].buffname = "Added "..color:wrap(color.dmg.arcane, "Arcane").." Damage"
loot.smt[p_stat_dmg_arcane].icon = "ReplaceableTextures\\CommandButtons\\BTNEtherealFormOn.blp"
loot.smt[p_stat_dmg_frost] = loot.statmod:newlong("Add #v bonus "..color:wrap(color.dmg.frost, "Frost").." damage to every attack and ability.", p_stat_dmg_frost, 1, 3, 0.005)
loot.smt[p_stat_dmg_frost].buffname = "Added "..color:wrap(color.dmg.frost, "Frost").." Damage"
loot.smt[p_stat_dmg_frost].icon = "ReplaceableTextures\\CommandButtons\\BTNFrostArmor.blp"
loot.smt[p_stat_dmg_nature] = loot.statmod:newlong("Add #v bonus "..color:wrap(color.dmg.nature, "Nature").." damage to every attack and ability.", p_stat_dmg_nature, 1, 3, 0.005)
loot.smt[p_stat_dmg_nature].buffname = "Added "..color:wrap(color.dmg.nature, "Nature").." Damage"
loot.smt[p_stat_dmg_nature].icon = "ReplaceableTextures\\CommandButtons\\BTNChainLightning.blp"
loot.smt[p_stat_dmg_fire] = loot.statmod:newlong("Add #v bonus "..color:wrap(color.dmg.fire, "Fire").." damage to every attack and ability.", p_stat_dmg_fire, 1, 3, 0.005)
loot.smt[p_stat_dmg_fire].buffname = "Added "..color:wrap(color.dmg.fire, "Fire").." Damage"
loot.smt[p_stat_dmg_fire].icon = "ReplaceableTextures\\CommandButtons\\BTNArcaniteMelee.blp"
loot.smt[p_stat_dmg_shadow] = loot.statmod:newlong("Add #v bonus "..color:wrap(color.dmg.shadow, "Shadow").." damage to every attack and ability.", p_stat_dmg_shadow, 1, 3, 0.005)
loot.smt[p_stat_dmg_shadow].buffname = "Added "..color:wrap(color.dmg.shadow, "Shadow").." Damage"
loot.smt[p_stat_dmg_shadow].icon = "ReplaceableTextures\\CommandButtons\\BTNUnsummonBuilding.blp"
loot.smt[p_stat_dmg_phys] = loot.statmod:newlong("Add #v bonus "..color:wrap(color.dmg.physical, "Physical").." damage to every attack and ability.", p_stat_dmg_phys, 1, 3, 0.005)
loot.smt[p_stat_dmg_phys].buffname = "Added "..color:wrap(color.dmg.physical, "Physical").." Damage"
loot.smt[p_stat_dmg_phys].icon = "ReplaceableTextures\\CommandButtons\\BTNClawsOfAttack.blp"
end
function loot:buildkwtable()
-- initial build table.
loot.kwt = {}
loot.kwt[kw_type_prefix] = {}
loot.kwt[kw_type_typemod] = {}
loot.kwt[kw_type_suffix] = {}
loot.kwt[kw_type_secondary] = {}
loot.kwt[kw_type_epic] = {}
loot.kwt[kw_type_ancient] = {}
loot.kwt[kw_type_digsite] = {}
loot.kwt[kw_type_potion] = {}
-- indexed lookup for selection/loading based on slotid:
loot.affixt = {}
loot.affixt[kw_type_prefix] = {}
loot.affixt[kw_type_typemod] = {}
loot.affixt[kw_type_suffix] = {}
loot.affixt[kw_type_secondary] = {}
loot.affixt[kw_type_epic] = {}
loot.affixt[kw_type_ancient] = {}
loot.affixt[kw_type_digsite] = {}
loot.affixt[kw_type_potion] = {}
for _,slotid in pairs(slot_all_t) do
loot.affixt[kw_type_prefix][slotid] = {}
loot.affixt[kw_type_typemod][slotid] = {}
loot.affixt[kw_type_suffix][slotid] = {}
loot.affixt[kw_type_secondary][slotid] = {}
loot.affixt[kw_type_epic][slotid] = {}
loot.affixt[kw_type_ancient][slotid] = {}
loot.affixt[kw_type_digsite][slotid] = {}
loot.affixt[kw_type_potion][slotid] = {}
end
-- indexed lookup for selection based on oreid crafting:
loot.oret = {
[ore_arcane] = {
p_stat_arcane,
p_stat_arcane_res,
p_stat_dmg_arcane,
p_epic_arcane_mis,
p_epic_arcane_conv,
p_epic_arcane_aoe,
p_potion_arcane_dmg,
p_potion_arcane_res,
},
[ore_frost] = {
p_stat_frost,
p_stat_frost_res,
p_stat_dmg_frost,
p_epic_frost_conv,
p_epic_frost_aoe,
p_potion_frost_dmg,
p_potion_frost_res,
},
[ore_nature] = {
p_stat_nature,
p_stat_nature_res,
p_stat_dmg_nature,
p_epic_heal_aoe,
p_epic_nature_conv,
p_epic_nature_aoe,
p_potion_nature_dmg,
p_potion_nature_res,
p_potion_thorns,
},
[ore_fire] = {
p_stat_fire,
p_stat_fire_res,
p_stat_dmg_fire,
p_epic_fire_mis,
p_epic_fire_conv,
p_epic_fire_aoe,
p_potion_fire_dmg,
p_potion_fire_res,
},
[ore_shadow] = {
p_stat_shadow,
p_stat_shadow_res,
p_stat_dmg_shadow,
p_epic_shadow_mis,
p_epic_shadow_conv,
p_epic_demon,
p_epic_shadow_aoe,
p_potion_shadow_dmg,
p_potion_shadow_res,
},
[ore_phys] = {
p_stat_phys,
p_stat_phys_res,
p_stat_dmg_phys,
p_epic_frost_mis,
p_epic_nature_mis,
p_epic_phys_mis,
p_epic_aoe_stun,
p_epic_hit_crit,
p_epic_phys_conv,
p_epic_phys_aoe,
p_potion_phys_dmg,
p_potion_phys_res,
}
}
loot.eletable = {}
for oreid,_ in pairs(loot.oret) do
loot.eletable[oreid] = {}
loot.eletable[oreid][kw_type_prefix] = {}
loot.eletable[oreid][kw_type_typemod] = {}
loot.eletable[oreid][kw_type_suffix] = {}
loot.eletable[oreid][kw_type_secondary] = {}
loot.eletable[oreid][kw_type_epic] = {}
loot.eletable[oreid][kw_type_ancient] = {}
loot.eletable[oreid][kw_type_digsite] = {}
loot.eletable[oreid][kw_type_potion] = {}
for _,slotid in pairs(slot_all_t) do
loot.eletable[oreid][kw_type_prefix][slotid] = {}
loot.eletable[oreid][kw_type_typemod][slotid] = {}
loot.eletable[oreid][kw_type_suffix][slotid] = {}
loot.eletable[oreid][kw_type_secondary][slotid] = {}
loot.eletable[oreid][kw_type_epic][slotid] = {}
loot.eletable[oreid][kw_type_ancient][slotid] = {}
loot.eletable[oreid][kw_type_digsite][slotid] = {}
loot.eletable[oreid][kw_type_potion][slotid] = {}
end
end
-----------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------PREFIX-----------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------
-- prefix main stat:
loot.kwt[kw_type_prefix][1] = loot.kw:new("Blunt", kw_type_prefix, loot.smt[p_stat_strength_b], kw_group_all)
loot.kwt[kw_type_prefix][2] = loot.kw:new("Magical", kw_type_prefix, loot.smt[p_stat_wisdom_b], kw_group_all)
loot.kwt[kw_type_prefix][3] = loot.kw:new("Agile", kw_type_prefix, loot.smt[p_stat_alacrity_b], kw_group_all)
loot.kwt[kw_type_prefix][4] = loot.kw:new("Tough", kw_type_prefix, loot.smt[p_stat_vitality_b], kw_group_all)
-- prefix movement:
loot.kwt[kw_type_prefix][5] = loot.kw:new("Speedy", kw_type_prefix, loot.smt[p_stat_bms], kw_group_movement)
-- prefix character:
loot.kwt[kw_type_prefix][6] = loot.kw:new("Vitalized", kw_type_prefix, loot.smt[p_stat_bhp], kw_group_char)
loot.kwt[kw_type_prefix][7] = loot.kw:new("Energized", kw_type_prefix, loot.smt[p_stat_bmana], kw_group_char)
loot.kwt[kw_type_prefix][8] = loot.kw:new("Acrobatic", kw_type_prefix, loot.smt[p_stat_dodge], kw_group_equipment)
loot.kwt[kw_type_prefix][9] = loot.kw:new("Stalwart", kw_type_prefix, loot.smt[p_stat_armor], kw_group_equipment)
-- prefix utility:
loot.kwt[kw_type_prefix][10] = loot.kw:new("Restorative", kw_type_prefix, loot.smt[p_stat_healing], kw_group_utility)
loot.kwt[kw_type_prefix][11] = loot.kw:new("Shielding", kw_type_prefix, loot.smt[p_stat_absorb], kw_group_utility)
loot.kwt[kw_type_prefix][12] = loot.kw:new("Tunneling", kw_type_prefix, loot.smt[p_stat_minepwr], kw_group_tool)
-- prefix resistances:
loot.kwt[kw_type_prefix][13]= loot.kw:new("Aqueous", kw_type_prefix, loot.smt[p_stat_fire_res], kw_group_resist_two)
loot.kwt[kw_type_prefix][14]= loot.kw:new("Congealing", kw_type_prefix, loot.smt[p_stat_frost_res], kw_group_resist_two)
loot.kwt[kw_type_prefix][15]= loot.kw:new("Ligneous", kw_type_prefix, loot.smt[p_stat_nature_res], kw_group_resist_two)
loot.kwt[kw_type_prefix][16]= loot.kw:new("Crepusculous", kw_type_prefix, loot.smt[p_stat_shadow_res], kw_group_resist_two)
loot.kwt[kw_type_prefix][17]= loot.kw:new("Anti-Mage", kw_type_prefix, loot.smt[p_stat_arcane_res], kw_group_resist_two)
-- dig site prefix
loot.kwt[kw_type_prefix][18]= loot.kw:new("Ravenous", kw_type_prefix, loot.digmt[m_stat_attack], kw_group_digsite)
loot.kwt[kw_type_prefix][19]= loot.kw:new("Adventurous", kw_type_prefix, loot.digmt[m_stat_xp], kw_group_digsite)
loot.kwt[kw_type_prefix][20]= loot.kw:new("Bountiful", kw_type_prefix, loot.digmt[m_stat_treasure], kw_group_digsite)
-----------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------SUFFIX-----------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------
-- suffix resistances:
loot.kwt[kw_type_suffix][1] = loot.kw:new("of the Infernal", kw_type_suffix, loot.smt[p_stat_fire_res], kw_group_resist)
loot.kwt[kw_type_suffix][2] = loot.kw:new("of the Lichborn", kw_type_suffix, loot.smt[p_stat_frost_res], kw_group_resist)
loot.kwt[kw_type_suffix][3] = loot.kw:new("of the Forest", kw_type_suffix, loot.smt[p_stat_nature_res], kw_group_resist)
loot.kwt[kw_type_suffix][4] = loot.kw:new("of the Damned", kw_type_suffix, loot.smt[p_stat_shadow_res], kw_group_resist)
loot.kwt[kw_type_suffix][5] = loot.kw:new("of the Magus", kw_type_suffix, loot.smt[p_stat_arcane_res], kw_group_resist)
loot.kwt[kw_type_suffix][6] = loot.kw:new("of the Brute", kw_type_suffix, loot.smt[p_stat_phys_res], kw_group_resist)
-- suffix attack:
loot.kwt[kw_type_suffix][7] = loot.kw:new("of the Warrior", kw_type_suffix, loot.smt[p_stat_phys], kw_group_attack)
-- suffix wax:
loot.kwt[kw_type_suffix][8] = loot.kw:new("of Longevity", kw_type_suffix, loot.smt[p_stat_wax], kw_group_wax)
-- potion/artifact suffix:
loot.kwt[kw_type_suffix][9] = loot.kw:new("of Vengeance", kw_type_suffix, loot.smt[p_stat_thorns], kw_group_utility)
loot.kwt[kw_type_suffix][10]= loot.kw:new("of Warding", kw_type_suffix, loot.smt[p_stat_shielding], kw_group_utility)
-- suffix main stat:
loot.kwt[kw_type_suffix][11] = loot.kw:new("of the Slayer", kw_type_suffix, loot.smt[p_stat_strength_b], kw_group_equipment)
loot.kwt[kw_type_suffix][12] = loot.kw:new("of the Oracle", kw_type_suffix, loot.smt[p_stat_wisdom_b], kw_group_equipment)
loot.kwt[kw_type_suffix][13] = loot.kw:new("of the Trickster", kw_type_suffix, loot.smt[p_stat_alacrity_b], kw_group_equipment)
loot.kwt[kw_type_suffix][14] = loot.kw:new("of the Veteran", kw_type_suffix, loot.smt[p_stat_vitality_b], kw_group_equipment)
loot.kwt[kw_type_suffix][15] = loot.kw:new("of the Summoner", kw_type_suffix, loot.smt[p_stat_miniondmg], kw_group_equipment)
-----------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------TYPE MOD---------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------
loot.kwt[kw_type_typemod][1] = loot.kw:new("Pyromaniac's", kw_type_typemod, loot.smt[p_stat_fire], kw_group_all)
loot.kwt[kw_type_typemod][2] = loot.kw:new("Lich's", kw_type_typemod, loot.smt[p_stat_frost], kw_group_all)
loot.kwt[kw_type_typemod][3] = loot.kw:new("Druid's", kw_type_typemod, loot.smt[p_stat_nature], kw_group_all)
loot.kwt[kw_type_typemod][4] = loot.kw:new("Warlock's", kw_type_typemod, loot.smt[p_stat_shadow], kw_group_all)
loot.kwt[kw_type_typemod][5] = loot.kw:new("Wizard's", kw_type_typemod, loot.smt[p_stat_arcane], kw_group_all)
loot.kwt[kw_type_typemod][6] = loot.kw:new("Bruiser's", kw_type_typemod, loot.smt[p_stat_phys], kw_group_all)
loot.kwt[kw_type_typemod][7] = loot.kw:new("Elementalist's", kw_type_typemod, loot.smt[p_stat_eleproc], kw_group_all)
-----------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------SECONDARY--------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------
loot.kwt[kw_type_secondary][1] = loot.kw:new("Treasure Hunter", kw_type_secondary, loot.smt[p_stat_treasure], kw_group_all)
loot.kwt[kw_type_secondary][2] = loot.kw:new("Veteran", kw_type_secondary, loot.smt[p_stat_digxp], kw_group_all)
loot.kwt[kw_type_secondary][3] = loot.kw:new("Artillery", kw_type_secondary, loot.smt[p_stat_mislrange], kw_group_attack)
loot.kwt[kw_type_secondary][4] = loot.kw:new("Cleaver", kw_type_secondary, loot.smt[p_stat_abilarea], kw_group_attack)
loot.kwt[kw_type_secondary][5] = loot.kw:new("Blood Magic", kw_type_secondary, loot.smt[p_stat_elels], kw_group_elemental)
loot.kwt[kw_type_secondary][6] = loot.kw:new("Steel Leech", kw_type_secondary, loot.smt[p_stat_physls], kw_group_elemental)
loot.kwt[kw_type_secondary][7] = loot.kw:new("Potion Mastery", kw_type_secondary, loot.smt[p_stat_potionpwr], kw_group_utility)
loot.kwt[kw_type_secondary][8] = loot.kw:new("Artifact Mastery",kw_type_secondary, loot.smt[p_stat_artifactpwr], kw_group_utility)
loot.kwt[kw_type_secondary][9] = loot.kw:new("Bartering", kw_type_secondary, loot.smt[p_stat_vendor], kw_group_utility)
loot.kwt[kw_type_secondary][10]= loot.kw:new("Battle Mage", kw_type_secondary, loot.smt[p_stat_castspeed], kw_group_castspeed)
-----------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------EPIC-------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------
loot.kwt[kw_type_epic][1] = loot.kw:new("Indestructible", kw_type_epic, loot.smt[p_epic_dmg_reduct], kw_group_all)
loot.kwt[kw_type_epic][2] = loot.kw:new("Missile", kw_type_epic, loot.smt[p_epic_fire_mis], kw_group_all)
loot.kwt[kw_type_epic][3] = loot.kw:new("Missile", kw_type_epic, loot.smt[p_epic_frost_mis], kw_group_all)
loot.kwt[kw_type_epic][4] = loot.kw:new("Missile", kw_type_epic, loot.smt[p_epic_nature_mis], kw_group_all)
loot.kwt[kw_type_epic][5] = loot.kw:new("Missile", kw_type_epic, loot.smt[p_epic_shadow_mis], kw_group_all)
loot.kwt[kw_type_epic][6] = loot.kw:new("Missile", kw_type_epic, loot.smt[p_epic_arcane_mis], kw_group_all)
loot.kwt[kw_type_epic][7] = loot.kw:new("Missile", kw_type_epic, loot.smt[p_epic_phys_mis], kw_group_all)
loot.kwt[kw_type_epic][8] = loot.kw:new("Healing Aura", kw_type_epic, loot.smt[p_epic_heal_aoe], kw_group_all)
loot.kwt[kw_type_epic][9] = loot.kw:new("Staggering Blow", kw_type_epic, loot.smt[p_epic_aoe_stun], kw_group_all)
loot.kwt[kw_type_epic][10] = loot.kw:new("Critical Blow", kw_type_epic, loot.smt[p_epic_hit_crit], kw_group_all)
loot.kwt[kw_type_epic][11] = loot.kw:new("Conversion", kw_type_epic, loot.smt[p_epic_fire_conv], kw_group_all)
loot.kwt[kw_type_epic][12] = loot.kw:new("Conversion", kw_type_epic, loot.smt[p_epic_frost_conv], kw_group_all)
loot.kwt[kw_type_epic][13] = loot.kw:new("Conversion", kw_type_epic, loot.smt[p_epic_nature_conv], kw_group_all)
loot.kwt[kw_type_epic][14] = loot.kw:new("Conversion", kw_type_epic, loot.smt[p_epic_shadow_conv], kw_group_all)
loot.kwt[kw_type_epic][15] = loot.kw:new("Conversion", kw_type_epic, loot.smt[p_epic_arcane_conv], kw_group_all)
loot.kwt[kw_type_epic][16] = loot.kw:new("Conversion", kw_type_epic, loot.smt[p_epic_phys_conv], kw_group_all)
loot.kwt[kw_type_epic][17] = loot.kw:new("Demonic", kw_type_epic, loot.smt[p_epic_demon], kw_group_all)
loot.kwt[kw_type_epic][18] = loot.kw:new("Nova", kw_type_epic, loot.smt[p_epic_fire_aoe], kw_group_all)
loot.kwt[kw_type_epic][19] = loot.kw:new("Nova", kw_type_epic, loot.smt[p_epic_frost_aoe], kw_group_all)
loot.kwt[kw_type_epic][20] = loot.kw:new("Nova", kw_type_epic, loot.smt[p_epic_nature_aoe], kw_group_all)
loot.kwt[kw_type_epic][21] = loot.kw:new("Nova", kw_type_epic, loot.smt[p_epic_shadow_aoe], kw_group_all)
loot.kwt[kw_type_epic][22] = loot.kw:new("Nova", kw_type_epic, loot.smt[p_epic_arcane_aoe], kw_group_all)
loot.kwt[kw_type_epic][23] = loot.kw:new("Nova", kw_type_epic, loot.smt[p_epic_phys_aoe], kw_group_all)
loot.kwt[kw_type_epic][24] = loot.kw:new("Proliferation", kw_type_epic, loot.smt[p_stat_dmg_arcane], kw_group_tool)
loot.kwt[kw_type_epic][25] = loot.kw:new("Proliferation", kw_type_epic, loot.smt[p_stat_dmg_frost], kw_group_tool)
loot.kwt[kw_type_epic][26] = loot.kw:new("Proliferation", kw_type_epic, loot.smt[p_stat_dmg_nature], kw_group_tool)
loot.kwt[kw_type_epic][27] = loot.kw:new("Proliferation", kw_type_epic, loot.smt[p_stat_dmg_fire], kw_group_tool)
loot.kwt[kw_type_epic][28] = loot.kw:new("Proliferation", kw_type_epic, loot.smt[p_stat_dmg_shadow], kw_group_tool)
loot.kwt[kw_type_epic][29] = loot.kw:new("Proliferation", kw_type_epic, loot.smt[p_stat_dmg_phys], kw_group_tool)
-----------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------POTION-----------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------
--loot.kwt[kw_type_potion][17] = loot.kw:new("Shared Healing", kw_type_potion, loot.smt[p_potion_aoe_heal], kw_group_onpotion)
--loot.kwt[kw_type_potion][18] = loot.kw:new("Shared Energy", kw_type_potion, loot.smt[p_potion_aoe_mana], kw_group_onpotion)
loot.kwt[kw_type_potion][1] = loot.kw:new("Living Essence", kw_type_potion, loot.smt[p_potion_life], kw_group_potion)
loot.kwt[kw_type_potion][2] = loot.kw:new("Energy Essence", kw_type_potion, loot.smt[p_potion_mana], kw_group_artifact)
loot.kwt[kw_type_potion][3] = loot.kw:new("Tortoise", kw_type_potion, loot.smt[p_potion_dmgr], kw_group_onpotion)
loot.kwt[kw_type_potion][4] = loot.kw:new("Elemental Shield", kw_type_potion, loot.smt[p_potion_fire_res], kw_group_potion)
loot.kwt[kw_type_potion][5] = loot.kw:new("Elemental Shield", kw_type_potion, loot.smt[p_potion_frost_res], kw_group_potion)
loot.kwt[kw_type_potion][6] = loot.kw:new("Elemental Shield", kw_type_potion, loot.smt[p_potion_nature_res], kw_group_potion)
loot.kwt[kw_type_potion][7] = loot.kw:new("Elemental Shield", kw_type_potion, loot.smt[p_potion_shadow_res], kw_group_potion)
loot.kwt[kw_type_potion][8] = loot.kw:new("Elemental Shield", kw_type_potion, loot.smt[p_potion_arcane_res], kw_group_potion)
loot.kwt[kw_type_potion][9] = loot.kw:new("Elemental Shield", kw_type_potion, loot.smt[p_potion_phys_res], kw_group_artifact)
loot.kwt[kw_type_potion][10] = loot.kw:new("Elemental Power", kw_type_potion, loot.smt[p_potion_fire_dmg], kw_group_artifact)
loot.kwt[kw_type_potion][11] = loot.kw:new("Elemental Power", kw_type_potion, loot.smt[p_potion_frost_dmg], kw_group_artifact)
loot.kwt[kw_type_potion][12] = loot.kw:new("Elemental Power", kw_type_potion, loot.smt[p_potion_nature_dmg], kw_group_artifact)
loot.kwt[kw_type_potion][13] = loot.kw:new("Elemental Power", kw_type_potion, loot.smt[p_potion_shadow_dmg], kw_group_artifact)
loot.kwt[kw_type_potion][14] = loot.kw:new("Elemental Power", kw_type_potion, loot.smt[p_potion_arcane_dmg], kw_group_artifact)
loot.kwt[kw_type_potion][15] = loot.kw:new("Elemental Power", kw_type_potion, loot.smt[p_potion_phys_dmg], kw_group_artifact)
loot.kwt[kw_type_potion][16] = loot.kw:new("Ground Slam", kw_type_potion, loot.smt[p_potion_aoe_stun], kw_group_potion)
loot.kwt[kw_type_potion][17] = loot.kw:new("Ground Tremor", kw_type_potion, loot.smt[p_potion_aoe_slow], kw_group_potion)
loot.kwt[kw_type_potion][18] = loot.kw:new("Shielding", kw_type_potion, loot.smt[p_potion_absorb], kw_group_onpotion)
loot.kwt[kw_type_potion][19] = loot.kw:new("Thick Hide", kw_type_potion, loot.smt[p_potion_armor], kw_group_onpotion)
loot.kwt[kw_type_potion][20] = loot.kw:new("Acrobatic", kw_type_potion, loot.smt[p_potion_dodge], kw_group_onpotion)
loot.kwt[kw_type_potion][21] = loot.kw:new("Vampiric", kw_type_potion, loot.smt[p_potion_lifesteal], kw_group_onpotion)
loot.kwt[kw_type_potion][22] = loot.kw:new("Vengeful", kw_type_potion, loot.smt[p_potion_thorns], kw_group_onpotion)
-----------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------v1.3-------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------
loot.kwt[kw_type_suffix][16] = loot.kw:new("of Arcane", kw_type_suffix, loot.smt[p_stat_arcane], kw_group_attack)
loot.kwt[kw_type_suffix][17] = loot.kw:new("of Frost", kw_type_suffix, loot.smt[p_stat_frost], kw_group_attack)
loot.kwt[kw_type_suffix][18] = loot.kw:new("of Nature", kw_type_suffix, loot.smt[p_stat_nature], kw_group_attack)
loot.kwt[kw_type_suffix][19] = loot.kw:new("of Fire", kw_type_suffix, loot.smt[p_stat_fire], kw_group_attack)
loot.kwt[kw_type_suffix][20] = loot.kw:new("of Shadow", kw_type_suffix, loot.smt[p_stat_shadow], kw_group_attack)
loot.kwt[kw_type_suffix][21] = loot.kw:new("of Rending", kw_type_suffix, loot.smt[p_stat_phys], kw_group_attack)
loot.kwt[kw_type_suffix][22] = loot.kw:new("of Proliferation", kw_type_suffix, loot.smt[p_stat_eleproc], kw_group_attack)
end
do
kb = {
--[[
configurable:
--]]
colradius = 32.0, -- detect terrain collision
tickrate = 0.03, -- timer cadence.
defaultvel = 20, -- default velocity for basic kb calls.
defaultdist = 500, -- default distance ``.
terraincol = true, -- set to false to ignore terrain.
destroydest = true, -- destroy dustructibles? (note: system collides with invul dest.).
destradius = 192.0, -- search this range for dest.
destplayeff = true, -- play effect on dest. collision?
arc = false, -- should unit do parabolic movement (toss or throw)?
arch = 375, -- if arc enabled, how high?
efftickrate = 6, -- stagger effect creation.
colfunc = nil, -- run on collision when set (NOTE: do not recursively run kb here, update existing one instead).
updfunc = nil, -- `` on update.
endfunc = nil, -- `` on end of kb path.
interrupt = false, -- pass this flag to force-cancel a kb instance.
effect = 'Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl',
desteff = 'Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl',
abil = FourCC('Arav'),
debug = false,
--[[
non-configurable:
--]]
total = 0,
stack = {},
}
kb.rect = Rect(0, 0, kb.destradius, kb.destradius)
kb.tmr = NewTimer()
kb.__index = kb
end
-- @unit = unit to knock back.
-- @angle = towards this angle.
-- [@_dist] = set a distance.
-- [@_dur] = set a duration (required with @_dist!).
function kb:new(unit, angle, _dist, _dur)
if unit and not IsUnitType(unit, UNIT_TYPE_STRUCTURE) and not IsUnitType(unit, UNIT_TYPE_ANCIENT) then
local o = setmetatable({}, self)
o.udex = GetUnitUserData(unit)
o.unit = unit
o.angle = angle
o.dist = _dist or kb.defaultdist
o.dur = _dur or nil
o.tick = 0
o.trav = 0
if kb.stack[o.udex] then
kb.stack[o.udex]:deletekb()
end
kb.stack[o.udex] = o
if kb.total == 0 then
TimerStart(kb.tmr, kb.tickrate, true, function()
utils.debugfunc(function()
if not map.manager.sbactive then
for _,obj in pairs(kb.stack) do
obj:update()
end
else
for _,obj in pairs(kb.stack) do
obj:deletekb()
end
end
end, "kb update")
if kb.debug then print("running timer") end
end)
end
kb.total = kb.total + 1
if kb.debug then print("total kb instances: "..kb.total) end
-- mark player being knocked back for external logic:
if utils.pnum(utils.powner(o.unit)) <= kk.maxplayers then
kobold.player[utils.powner(o.unit)].currentlykb = true
end
o:init()
return o
end
end
-- @unit = unit to knock back.
-- @x,y = from this origin coord.
-- [@_dist] = set a distance.
-- [@_dur] = set a duration (required with @_dist!).
function kb:new_origin(unit, x, y, _dist, _dur)
return kb:new(unit, utils.anglexy(x, y, GetUnitX(unit), GetUnitY(unit), _dist, _dur))
end
-- @grp = get units to knock back.
-- @x,y = from this origin coord.
-- [@_dist] = set a distance.
-- [@_dur] = set a duration (required with @_dist!).
function kb:new_group(grp, x, y, _dist, _dur)
grp:action(function()
kb:new(grp.unit, utils.anglexy(x, y, GetUnitX(grp.unit), GetUnitY(grp.unit)), _dist, _dur)
end)
end
-- @p = for this player.
-- @x,y = from this origin coord.
-- @r = radius to search for.
-- [@_dist] = set a distance.
-- [@_dur] = set a duration (required with @_dist!).
function kb:new_pgroup(p, x, y, r, _dist, _dur)
local grp = g:newbyxy(p, g_type_dmg, x, y, r)
grp:action(function()
kb:new(grp.unit, utils.anglexy(x, y, GetUnitX(grp.unit), GetUnitY(grp.unit)), _dist, _dur)
end)
grp:destroy()
end
-- @grp = get units to pull in.
-- @x,y = toward this origin coord.
-- [@_dur] = set a duration.
function kb:new_pullgroup(grp, x, y, _dur, _vel)
local d = 0
grp:action(function()
d = utils.distxy(x, y, utils.unitx(grp.unit), utils.unity(grp.unit))
kb:new(grp.unit, utils.anglexy(GetUnitX(grp.unit), GetUnitY(grp.unit), x, y), d, _dur)
end)
end
function kb:init()
if IsUnitType(self.unit, UNIT_TYPE_FLYING) then
self.isflying = true
end
if utils.pnum(utils.powner(self.unit)) < kk.maxplayers then
self.p = utils.powner(self.unit)
end
self.x = GetUnitX(self.unit)
self.y = GetUnitY(self.unit)
self.z = 0 -- only used when arc is set.
if self.dur and self.dist then
self.vel = (self.dist/self.dur)/(1.0/self.tickrate)
else
self.vel = self.defaultvel
end
self.tickmax = math.floor(self.dist/self.vel)
if self:collision() then
self:destroy()
return
end
if self.pause then
PauseUnit(self.unit, true)
end
if kb.debug then
print("init debug:")
print("self.unit = "..tostring(self.unit))
print("self.x = "..self.x)
print("self.y = "..self.y)
print("self.vel = "..self.vel)
print("self.dist = "..self.dist)
print("self.tickmax = "..self.tickmax)
end
end
function kb:update()
if self:collision() then
self:destroy()
return
else
self.tick = self.tick + 1
end
if self.updfunc then
self.updfunc(self)
end
if self.p and kobold.player[self.p].downed then
self:deletekb()
elseif not self.interrupt and utils.isalive(self.unit) and self.tick < self.tickmax and self.trav < self.dist then
self.trav = self.trav + self.vel
self.x, self.y = utils.projectxy(self.x, self.y, self.vel, self.angle)
-- special effect staggered play for performance/aesthetic:
if self.effect and ModuloInteger(self.tick, self.efftickrate) == 0 then
DestroyEffect(AddSpecialEffect(self.effect, self.x, self.y))
end
-- add/remove storm crow for pathing:
UnitAddAbility(self.unit, self.abil)
utils.setxy(self.unit, self.x, self.y)
if self.arc then
if not self.cachedh then self.cachedh = GetUnitFlyHeight(self.unit) end
if self.z == 0.0 then
self.z = self:getheight()
end
self.z = (4*self.arch/self.dist)*(self.dist - self.trav)*(self.trav/self.dist) -- + self:getheight()
utils.setheight(self.unit, self.z)
end
UnitRemoveAbility(self.unit, self.abil)
else
self:deletekb()
end
if kb.debug and ModuloInteger(self.tick, 21) then
print("attempting to update for unit index "..self.udex)
print("self.unit = "..tostring(self.unit))
print("self.x = "..self.x)
print("self.y = "..self.y)
print("self.vel = "..self.vel)
print("self.dist = "..self.dist)
print("self.tickmax = "..self.tickmax)
end
end
-- conditional destroy.
function kb:destroy()
-- hook self.delete to force remove or prevent removal (for advanced users):
if self.delete then
if self.arc then
UnitAddAbility(self.unit, self.abil)
utils.setheight(self.unit, self.cachedh)
UnitRemoveAbility(self.unit, self.abil)
end
if self.endfunc then
self.endfunc(self)
end
if self.pause then
PauseUnit(self.unit, false)
end
for i,v in pairs(self) do
v = nil
i = nil
end
kb.stack[self.udex] = nil
kb.total = kb.total - 1
if kb.total <= 0 then
kb.total = 0
PauseTimer(kb.tmr)
end
if kb.total < 10 then
-- recycle indeces when size is low:
utils.tablecollapse(kb.stack)
end
-- mark player being knocked back for external logic:
if utils.pnum(utils.powner(self.unit)) <= kk.maxplayers then
kobold.player[utils.powner(self.unit)].currentlykb = false
end
elseif kb.debug then
print("caution: self.delete hooked, instance not removed")
end
if kb.debug then print("total kb instances: "..kb.total) end
end
-- absolute destroy (use where finality is required).
function kb:deletekb()
self.delete = true
self:destroy()
end
function kb:collision()
if not self.isflying and (self:terraincollision() or self:destcollision()) then
-- hook delete in collision func to prevent deletion, etc.
self.delete = true
if self.colfunc then
self.colfunc(self)
end
return true
end
return false
end
function kb:terraincollision()
if self.terraincol then
self.offsetcheck = self.vel + self.colradius
self.offsetx, self.offsety = utils.projectxy(self.x, self.y, self.offsetcheck, self.angle)
if IsTerrainPathable(self.offsetx, self.offsety, PATHING_TYPE_WALKABILITY) then
return true
end
end
return false
end
function kb:destcollision()
self:updaterect(self.offsetx, self.offsety)
if self.destroydest then
EnumDestructablesInRect(kb.rect, nil, function() self:hitdestdestroy() end)
else
EnumDestructablesInRect(kb.rect, nil, function() self:hitdestcollide() end)
end
if self.destcol then
return true
else
return false
end
end
-- destroy destructables.
function kb:hitdestdestroy()
bj_destRandomCurrentPick = GetEnumDestructable()
-- check destructable life to ignore any that are set to invulnerable:
if GetWidgetLife(bj_destRandomCurrentPick) > 0.405 and GetWidgetLife(bj_destRandomCurrentPick) < 99999.0 then
if self.destplayeff and self.desteff then
DestroyEffect(AddSpecialEffect(self.desteff, GetWidgetX(bj_destRandomCurrentPick), GetWidgetY(bj_destRandomCurrentPick)))
end
SetWidgetLife(bj_destRandomCurrentPick, 0)
elseif GetWidgetLife(bj_destRandomCurrentPick) > 99998.0 then
self.destcol = true
end
end
function kb:updaterect(x, y)
local w = self.destradius/2
SetRect(kb.rect, x - w, y - w, x + w, y + w)
end
-- find out if there is a nearby destructable to collide with.
function kb:hitdestcollide()
bj_destRandomCurrentPick = GetEnumDestructable()
if bj_destRandomCurrentPick and GetWidgetLife(bj_destRandomCurrentPick) > 0.405 then
if IsUnitInRangeXY(
self.unit,
GetWidgetX(bj_destRandomCurrentPick),
GetWidgetY(bj_destRandomCurrentPick),
self.destradius)
then
self.destcol = true
end
end
end
function kb:getheight()
if self.arc then
return GetTerrainCliffLevel(self.x, self.y)*128 - 256
else
return GetTerrainCliffLevel(self.x, self.y)*128 - 256 + self.height
end
end
function kb:reset()
-- e.g. for recursive updates of a kb instance.
self.trav = 0
self.tickmax = 0
self.tick = 0
self.delete = false
end
kui = {}
kui.frame = {} -- frame object class.
-- TODO: store active mainframe in a focus variable or something, then have a hotkey which targets that context.
-- e.g. "ESC" calls the closebtn function attached to said mainframe.
function kui:init()
-- note: kui.gameui has layer priority over kui.worldui.
-- enable or disable debug event print() logging:
self.debug = false -- overrides all debug settings.
self.debugclick = false
self.debughover = false
self.debugenter = false
self.lockcam = true -- character perma screen/sel lock.
-- load meta data:
kui:dataload()
-- init other classes:
kui.frame:init()
-- run local-only code:
-- block screen until loading complete:
utils.fadeblack(true) -- ***note: async; cannot run locally!
for p,_ in pairs(kobold.playing) do
if p == utils.localp() then
BlzFrameClearAllPoints(self.ubertip)
BlzFrameSetPoint(self.ubertip, fp.tr, self.gameui, fp.tl, 0.0, -0.05) -- hide normal tooltip frame.
BlzFrameSetPoint(self.otip, fp.tr, self.gameui, fp.tl, 0.0, -0.05)
BlzFrameSetPoint(self.chatmsg, fp.bl, self.minimap, fp.tl, 0.1, 0.05)
BlzFrameSetAbsPoint(BlzGetFrameByName("ConsoleUI", 0), FRAMEPOINT_TOPLEFT, 0.0, 0.633)
BlzFrameSetSize(self.chatmsg, 0.5, 0.2)
BlzFrameSetVisible(self.consolebd, false)
BlzFrameSetVisible(self.unitmsg, true)
BlzFrameSetLevel(self.unitmsg,6)
BlzFrameSetParent(self.unitmsg, self.worldui)
BlzEnableUIAutoPosition(false)
BlzHideOriginFrames(true)
self.screenw = BlzGetLocalClientWidth()
self.screenh = BlzGetLocalClientHeight()
self.ratio = self.screenh/self.screenw
kui:pxdpiscale()
end
end
end
function kui:uigen() -- genui
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- o = master parent for custom ui frames.
-- o.game = skill bar, etc. ('gameplay' ui).
-- o.splash = main menu.
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
kui.canvas = kui.frame:newbytype("PARENT", self.gameui)
kui.canvas.game = kui.frame:newbytype("PARENT", kui.canvas)
-- because simple frames are awful, run thru each
-- child parent to fire their show/hide funcs.
kui.canvas.game.showfunc = function()
kui.canvas.game.inv:show()
kui.canvas.game.char:show()
kui.canvas.game.skill:show()
kui.canvas.game.dig:show()
end
kui.canvas.game.hidefunc = function()
kui.canvas.game.inv:hide()
kui.canvas.game.char:hide()
kui.canvas.game.skill:hide()
kui.canvas.game.dig:hide()
end
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
kui.canvas.game.party = kui:createpartypane(kui.canvas.game)
kui.canvas.game.inv = kui:createinventory(kui.canvas.game)
kui.canvas.game.equip = kui:createequipment(kui.canvas.game)
kui.canvas.game.char = kui:createcharpage(kui.canvas.game)
kui.canvas.game.skill = kui:createskillbar(kui.canvas.game)
kui.canvas.game.skill.obj = map.mission:buildpanel()
kui.canvas.game.dig = kui:createdigsitepage(kui.canvas.game)
kui.canvas.game.bosschest = kui:createbosschest(kui.canvas.game)
kui.canvas.game.abil = ability:buildpanel()
kui.canvas.game.mast = mastery:buildpanel()
kui.canvas.game.badge = badge:buildpanel()
kui.canvas.game.modal = kui:createmodal(kui.gameui)
kui.canvas.game.overwrite = kui:createoverwriteprompt(kui.gameui)
kui:linkskillbarbtns()
kui.canvas.game:hide()
kui.canvas.game.skill:hide()
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
utils.debugfunc(function() map.manager:initframes() end, "map.manager:initframes")
utils.debugfunc(function() buffy:buildpanel() end, "buffy:buildpanel")
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
utils.debugfunc(function() kui.canvas.splash = kui:createsplashscreen(kui.canvas) end, "kui:createsplashscreen")
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
end
function kui.frame:init()
self.fh = nil -- shorthand for object's framehandle.
self.distex = nil -- disabled display graphic (backdrop only).
self.normaltex = nil -- normal (backdrop only).
self.activetex = nil -- active (backdrop only).
self.hovertex = nil -- hover (backdrop only).
self.statesnd = false -- does the frame play open/close sounds?
self.suppress = false -- stop show/hide utility functions?
self.simple = false
self.disabled = false
self.ismf = false
self.alpharatio = 0.8 -- for hover highlights.
self.alpha = 255
self.alphahl = 255 -- altered when hoverhl() is set, else ignore.
self.__index = self
end
-- @simplename = the name of the frame as defined in .fdf.
-- @parentfr = set to this parent frame.
function kui.frame:newbysimple(simplename, parentfr)
-- these should typically be individual frames that
-- are updated via GetFrameName for a local player.
-- parentfr can be frame object or framehandle.
local fr = {}
setmetatable(fr, self)
if parentfr == nil then
fr.fh = BlzCreateSimpleFrame(simplename, kui.gameui, 0)
else
if type(parentfr) == "table" then
fr.fh = BlzCreateSimpleFrame(simplename, parentfr.fh, 0)
else
fr.fh = BlzCreateSimpleFrame(simplename, parentfr, 0)
end
end
if kui.debug then fr.debugn = simplename end
fr.simple = true
BlzFrameSetLevel(fr.fh, 0)
return fr
end
-- @framevalue = frame's text name.
-- @converttype = 0 or 1; 0 for framename, 1 for framehandle
-- @context = [optional] defaults to 0
function kui.frame:convert(framevalue, converttype, context)
-- create a frame object of a default frame
-- via its frame name lookup or origin frame.
local fr = {}
setmetatable(fr, self)
local context = context or 0
if converttype == 0 then
fr.fh = BlzGetFrameByName(framevalue,context)
else
fr.fh = framevalue
end
return fr
end
-- @frametype = frame type string: "FRAME", "BACKDROP", etc.
-- @parentfr = [optional] parent frame object
function kui.frame:newbytype(frametype, parentfr)
-- create a frame by frametype.
-- parentfr can be frame object or framehandle.
local fr = {}
local t = frametype
-- hacky custom frametype insert:
if t == "PARENT" then t = "BACKDROP" end
local parent
setmetatable(fr, self)
if parentfr == nil then
parent = kui.gameui
else
if type(parentfr) == "table" then
parent = parentfr.fh
else
parent = parentfr
end
end
fr.fh = BlzCreateFrameByType(t, "kuiframe"..#kui.framestack, parent, "", 0)
kui.framestack[#kui.framestack+1] = fr
BlzFrameSetLevel(fr.fh, 0) -- don't use :setlvl
if kui.debug then fr.debugn = frametype end
-- init custom frametype:
if frametype == "PARENT" then
fr:setsize(0.001,0.001)
fr:setfp(fp.tr, fp.tr, kui.gameui)
fr:setabsfp(fp.tr, 0.0, 0.0)
fr:addbgtex(kui.tex.invis)
end
return fr
end
-- @framename = frame name string: "FrameBaseText", etc.
-- @parentfr = [optional] parent frame object
function kui.frame:newbyname(framename, parentfr)
-- create a frame by name and inherit its frametype (as defined in .fdf files).
local fr = {}
local parent
setmetatable(fr, self)
if parentfr == nil then
parent = kui.gameui
else
if type(parentfr) == "table" then
parent = parentfr.fh
else
parent = parentfr
end
end
fr.fh = BlzCreateFrame(framename, parent, 0, 0)
BlzFrameSetLevel(fr.fh, 0) -- don't use :setlvl
kui.framestack[#kui.framestack+1] = fr
if kui.debug then fr.debugn = framename end
return fr
end
-- @parentfr = set as parent.
-- @texture = set btn backdrop texture.
function kui.frame:newbtntemplate(parentfr, texture)
-- create a frame with an invisible button over it (.btn)
-- *note: these will only work in the 4:3 ratio screen space.
-- *reminder: add events to .btn, not its wrapped backdrop.
local fr = self:newbytype("BACKDROP", parentfr)
fr.btn = self:newbytype("GLUEBUTTON", fr)
fr.btn:setallfp(fr)
fr:addbgtex(texture)
return fr
end
function kui.frame:newitempane(parentfr)
-- item selection pane template.
self.pane = kui.frame:newbytype("BACKDROP", parentfr)
self.pane:addbgtex('war3mapImported\\inventory-selected_box.blp')
self.pane:setsize(kui:px(52),kui:px(52))
self.pane:setlvl(3)
self.pane.btnbd = kui.frame:newbytype("BACKDROP", self.pane)
self.pane.btnbd:addbgtex("war3mapImported\\inventory-icon_unequip.blp")
self.pane.btnbd:setsize(kui:px(32),kui:px(32))
self.pane.btnbd:setfp(fp.bl, fp.tr, self.pane, 0.0, 0.0)
self.pane.btn = kui.frame:newbytype("BUTTON", self.pane)
self.pane.btn:setallfp(self.pane.btnbd)
self.pane.btn:assigntooltip("invbtnunequip")
local unequipfunc = function()
utils.debugfunc(function()
loot.item:unequip(utils.trigp()) -- selslotid is controlled by inventory pane selection.
end, "unequip")
end
self.pane.btn:addevents(nil, nil, unequipfunc)
end
-- @showfr = frame to show when clicked.
-- @hidefr = hide this frame.
-- @func = [optional] additional callback func to run after click event, if needed.
function kui.frame:createbackbtn(showfr, hidefr, func)
-- attach a back btn to this frame object, which hides it to
-- show the 'showfr' object it is led to. property: fr.btn
local fr = self:newbtntemplate(hidefr, kui.tex.backbtn)
fr.statesnd = true
fr:setsize(kui:px(kui.meta.backbtnw), kui:px(kui.meta.backbtnh))
fr.btn:addswapevent(showfr, hidefr)
fr.btn:addhoverhl(fr)
if func then fr.btn:addevents(nil,nil,func) end
return fr
end
-- @x = frame level.
function kui.frame:setlvl(x)
if type(self) == "table" and self.fh then -- fr obj.
BlzFrameSetLevel(self.fh, x)
else
BlzFrameSetLevel(self, x) -- framehandle.
end
end
-- @texturepath = texture (.tga or .blp, .blp only on simple frames) as the background.
-- @alphavalue = [optional] 0-255 (0 is invisible).
function kui.frame:addbgtex(texturepath, alphavalue)
-- this should be used on frame objects ( e.g. fr:addbgtex() ).
local alphavalue = alphavalue or self.alpha
BlzFrameSetTexture(self.fh, texturepath, 0, true)
BlzFrameSetAlpha(self.fh, math.ceil(alphavalue))
self.normaltex = texturepath
self.distex = texturepath -- disabled texture by default
end
function kui.frame:setbgtex(texturepath)
BlzFrameSetTexture(self.fh, texturepath, 0, true)
end
-- @entercallback,@leavecallback,@clickcallback = event func.
-- @clickoverridetype = [optional] to be used with SIMPLEBUTTON frames to detect clicks.
-- @islocalbool = [optional] should the code be ran locally? (true by default)
function kui.frame:addevents(entercallback, leavecallback, clickcallback, clickoverridetype, islocalbool)
-- *note: simplebuttons do not support enter or leave (handle that via textures).
-- *note: simplebuttons can only have *one* event click, a 2nd will erase the 1st.
-- this should be used on frame objects ( e.g. fr:addevents() ).
if entercallback ~= nil then
if self.mouseenter == nil then
self.mouseenter = self:event(entercallback, FRAMEEVENT_MOUSE_ENTER)
elseif self.mouseenter.trig then
TriggerAddAction(self.mouseenter.trig, entercallback, kui.debug, islocalbool)
else
print("error: addevents is missing a trigger for mouseenter")
end
end
if leavecallback ~= nil then
if self.mouseleave == nil then
self.mouseleave = self:event(leavecallback, FRAMEEVENT_MOUSE_LEAVE)
elseif self.mouseleave.trig then
TriggerAddAction(self.mouseleave.trig, leavecallback, kui.debug, islocalbool)
else
print("error: addevents is missing a trigger for mouseleave")
end
end
if clickcallback ~= nil then
local ct = FRAMEEVENT_CONTROL_CLICK
if self.simple then ct = FRAMEEVENT_CONTROL_CLICK end
if self.click == nil then
self.click = self:event(clickcallback, ct, kui.debug, islocalbool)
elseif self.click.trig then
TriggerAddAction(self.click.trig, clickcallback)
else
print("error: addevents is missing a trigger for click")
end
end
end
-- @eventcallback = run this func.
function kui.frame:addnetclickevent(eventcallback)
-- add an event which requires being non-local e.g.
-- create a unit, change or compare handles, etc.
self:event(eventcallback, FRAMEEVENT_CONTROL_CLICK, false, false)
end
-- @eventcallback = action to run on event.
-- @frameeventtype = event which runs callback.
-- @debugbool = [optional] set to false to individually disable debug.
-- @localbool = [optional] default true. set to false to run net-code funcs. *important.
function kui.frame:event(eventcallback, frameeventtype, debugbool, localbool)
-- this should be used on frame objects ( e.g. fr:event() )
local localbool = localbool or true
local eventfunc
if localbool then
eventfunc = function()
if utils.islocaltrigp() then
utils.debugfunc(function() eventcallback() end, kui.debugstr[frameeventtype])
self:focusfix()
end
end
else
eventfunc = function()
utils.debugfunc(function() eventcallback() end, kui.debugstr[frameeventtype])
if utils.islocaltrigp() then self:focusfix() end
end
end
local event = {
trig = CreateTrigger(),
event = eventcallback,
}
BlzTriggerRegisterFrameEvent(event.trig, self.fh, frameeventtype)
TriggerAddCondition(event.trig, Condition(function()
return BlzGetTriggerFrameEvent() == frameeventtype and self:istrigger()
end) )
TriggerAddAction(event.trig, eventfunc)
if kui.debug and debugbool ~= false then self:addeventdebug(event.trig, kui.debugstr[frameeventtype]) end
return event
end
-- @closefr = the frame which gets closed by this frame object.
function kui.frame:linkcloseevent(closefr)
if self.fh and closefr.fh then
local onclick = function()
if utils.islocaltrigp() then
self:focusfix()
closefr:hide()
end
end
self:addevents(nil, nil, onclick)
else
print("error: linkcloseevent found no eligible framehandles")
end
end
-- @linkedfr = the frame which is controlled by the open/show btn.
function kui.frame:linkshowhidebtn(linkedfr)
-- self (fr) will show/hide the linkedfr
if self.fh and linkedfr.fh then
local onclick = function()
if utils.islocaltrigp() then
self:focusfix()
if linkedfr:isvisible() then
linkedfr:hide()
else
-- if the button has an alert notification assigned, hide it.
if self.alerticon then
self.alerticon:hide()
end
linkedfr:show() -- show frame.
end
end
end
self:addevents(nil, nil, onclick)
else
print("error: linkshowhidebtn found no eligible framehandles")
end
end
-- @targetfr = show this frame and hide the triggering frame.
-- @groupparentfr = [optional] hide a group frame instead of the target frame.
function kui.frame:addswapevent(targetfr, groupparentfr)
if self.fh and targetfr.fh then
local onclick = function()
if utils.islocaltrigp() and not self.disabled then
targetfr:show()
if groupparentfr then groupparentfr:hide() else self:hide() end
utils.playsound(kui.sound.clickstr)
end
end
self:addevents(nil, nil, onclick)
else
print("error: addswapevent found no eligible framehandles on 'targetfr'")
end
end
-- @sndtype = 0 for close, 1 for open.
function kui.frame:statesound(sndtype)
-- checks for panelid and plays stored sound.
if self.panelid and self.statesnd then
if sndtype == 1 then
utils.playsound(kui.sound.panel[self.panelid].open)
else
utils.playsound(kui.sound.panel[self.panelid].close)
end
end
end
function kui.frame:enableeventsall(bool)
-- this should be used on frame objects ( e.g. fr:enableeventsall() )
if self.mouseenter then utils.triggeron(self.mouseenter.trig, bool) end
if self.mouseleave then utils.triggeron(self.mouseleave.trig, bool) end
if self.click then utils.triggeron(self.click.trig, bool) end
end
function kui.frame:enableevent(enterbool, leavebool, clickbool)
-- this should be used on frame objects ( e.g. fr:enableevent() )
if self.mouseenter then utils.triggeron(self.mouseenter.trig, enterbool) end
if self.mouseleave then utils.triggeron(self.mouseleave.trig, leavebool) end
if self.click then utils.triggeron(self.click.trig, clickbool) end
end
-- @texttype = "header", "base", "tip", or "btn"
-- @str = btn display text.
-- @anchor = anchor point or "all"
function kui.frame:addtext(texttype, str, anchor)
-- **note: this is used in our tooltip class.
if texttype == "base" then self.txt = kui.frame:newbyname("FrameBaseText", self) elseif -- consolas reg, justify left, top
texttype == "btn" then self.txt = kui.frame:newbyname("FrameBtnText", self) elseif -- tarzan sml, justify center, middle
texttype == "tip" then self.txt = kui.frame:newbyname("FrameTooltipText",self) elseif -- consolas reg, justify center, middle
texttype == "header" then self.txt = kui.frame:newbyname("FrameHeaderText", self) -- tarzan lrg, justify center, middle
end
BlzFrameSetText(self.txt.fh, str)
if anchor == "all" then
BlzFrameSetAllPoints(self.txt.fh, self.fh)
else
BlzFrameSetPoint(self.txt.fh, anchor, self.fh, anchor, 0.0, 0.0)
end
end
-- @tipstr = tooltip lookup string (kui.tooltip)
function kui.frame:assigntooltip(tipstr)
self.tip = tooltip:get(tipstr) -- fetch stored tooltip table w/ settings.
-- check which frame to show/update on hover:
if self.tip.fn then
if self.tip.fn == "FrameItemTooltip" then
self.tip.fr = tooltip.itemtipfr
elseif self.tip.fn == "FrameLongformTooltip" then
self.tip.fr = tooltip.advtipfr
end
else
self.tip.fr = tooltip.simpletipfr
end
-- assign hover event:
if self.tip.fr and self.tip.fr.fh then
local enter = function()
utils.debugfunc(function()
if not self.disabled then
if self.tip.simple then -- simple frame, 1-line.
local c,w,_,h
if self.tip.hoverset then
local hovertext = self.tip.hoverset(self.hoverarg or nil)
if hovertext == nil then return end -- stop if something happened and text is nil.
_,h = string.gsub(hovertext,"|c","|c")
c = string.len(hovertext)
if utils.islocaltrigp() then
BlzFrameSetText(self.tip.fr.txt.fh, hovertext)
end
else
_,h = string.gsub(self.tip.txt,"|c","|c")
c = string.len(self.tip.txt)
if utils.islocaltrigp() then
BlzFrameSetText(self.tip.fr.txt.fh, self.tip.txt)
end
end
c = c - (h*12) -- hex codes are 12 chars.
w = tooltip.setting.simplew+(c*tooltip.setting.simplec)
self.tip.fr.alpha = 240
self.tip.fr:setsize(w, tooltip.setting.simpleh)
self.tip.fr:setlvl(tooltip.setting.flevel)
self.tip.fr:resetalpha()
self.tip.fr:show()
self.tip.fr:clearallfp()
self.tip.fr:setfp(self.tip.tipanchor, self.tip.attachanchor, self.fh, 0, 0)
end
else
self.tip.fr:hide()
end
end, "simple tip hover")
end
local leave = function()
self.tip.fr:hide()
end
self:addevents(enter, leave, nil)
self.tip.fr:hide()
else
assert(false, "error: assigntooltip failed to find a tooltip frame for '"..tipstr.."'")
end
end
function kui.frame:initializeadvtip()
-- on hover, show a text area tooltip.
self.tip = tooltip:get("advtip")
self.tip.fr = tooltip.advtipfr
if not self.advtipanchor then self.advtipanchor = self.tip.tipanchor end
if not self.advattachanchor then self.advattachanchor = self.tip.attachanchor end
local enter = function()
if utils.trigp() == utils.localp() then
if self:isvisible() and self.advtip and self.advtip[3] then
BlzFrameSetTexture(tooltip.advtipfh[1], self.advtip[1], 0, true) -- icon texture
BlzFrameSetText(tooltip.advtipfh[2], self.advtip[2]) -- name
BlzFrameSetText(tooltip.advtipfh[3], self.advtip[3]) -- description
self.tip.fr:show()
self.tip.fr:clearallfp()
self.tip.fr:setfp(self.advtipanchor, self.advattachanchor, self, 0.0, 0.0)
else
self.tip.fr:hide()
end
end
end
local leave = function()
if utils.trigp() == utils.localp() then
self.tip.fr:hide()
end
end
self.tip.fr:hide()
self:addevents(enter, leave, nil)
end
function kui.frame:initializeitemslot(slotid)
-- hover-over functionality for item tooltips.
self.tip = tooltip:get("itemtip")
self.tip.fr = tooltip.itemtipfr
local enter = function()
local p = utils.trigp()
if kobold.player[p]:getitem(slotid) ~= nil then
local targetslotid = kobold.player[p].items[slotid].itemtype.slottype.slotid
-- update item comp tooltip:
if slotid < 1000 and kobold.player[p].items[targetslotid] then
kobold.player[p].items[targetslotid]:updatecomptooltip(p)
tooltip.itemcompfr:show()
tooltip.itemcompfr:setfp(fp.r, fp.l, self.tip.fr, kui:px(-10), 0.0)
else
tooltip.itemcompfr:hide()
end
-- update item hover tooltip:
if kobold.player[p]:islocal() then
kobold.player[p]:getitem(slotid):updatetooltip(p)
self.tip.fr:show()
self.tip.fr:setfp(self.tip.tipanchor, self.tip.attachanchor, self, 0, 0)
end
else
if kobold.player[p]:islocal() then self.tip.fr:hide() end
end
end
local leave = function()
if kobold.player[utils.trigp()]:islocal() then
self.tip.fr:hide()
tooltip.itemcompfr:hide()
end
end
local click = function()
-- understanding this event: .selslotid == currently selected slot if not nil, slotid == target slot.
local p = utils.trigp()
kui.canvas.game.inv.pane:hide()
kui.canvas.game.equip.pane:hide()
kui.canvas.game.dig.pane:hide()
-- if no slot already selected and item exists in slot, or if clicking on another item that exists already:
if (not kobold.player[p].selslotid and kobold.player[p].items[slotid])
or (kobold.player[p].selslotid and kobold.player[p].items[slotid] and kobold.player[p].selslotid ~= slotid) then
kobold.player[p].selslotid = slotid
if p == utils.localp() then
if slotid < 1000 then -- is inventory slot.
kui.canvas.game.inv.pane:show()
kui.canvas.game.inv.pane:setfp(fp.c, fp.c, self)
utils.playsound(kui.sound.itemsel)
elseif slotid ~= 1011 then -- is equipment slot.
kui.canvas.game.equip.pane:show()
kui.canvas.game.equip.pane:setfp(fp.c, fp.c, self)
utils.playsound(kui.sound.itemsel)
elseif slotid == 1011 then -- is dig key
kui.canvas.game.dig.pane:show()
kui.canvas.game.dig.pane:setfp(fp.c, fp.c, self)
utils.playsound(kui.sound.itemsel)
end
end
-- if slot already selected, and next slot is empty:
elseif kobold.player[p].selslotid and not kobold.player[p].items[slotid] then
if p == utils.localp() then
-- inventory slot to empty inventory slot:
if kobold.player[p].selslotid < 1000 and slotid < 1000 then
loot.item:slotswap(p, kobold.player[p].selslotid, slotid)
utils.playsound(kui.sound.itemsel)
-- equipment slot or dig key to empty inventory slot:
elseif kobold.player[p].selslotid > 1000 and slotid < 1000 then
local firstslotid = loot:getfirstempty(p)
loot.item:unequip(p, kobold.player[p].selslotid) -- (plays sound internally)
if slotid ~= firstslotid then
loot.item:slotswap(p, firstslotid, slotid)
end
-- inventory slot to empty equipment slot:
elseif kobold.player[p].selslotid < 1000 and slotid > 1000 then
loot.item:equip(p, kobold.player[p].selslotid) -- (plays sound internally)
end
end
kobold.player[p].selslotid = nil
-- cleanup in all other situations:
else
kobold.player[p].selslotid = nil
end
end
self.tip.fr:hide()
self:addevents(enter, leave, click)
end
function kui.frame:isvisible()
return BlzFrameIsVisible(self.fh)
end
function kui.frame:isenabled()
return BlzFrameGetEnable(self.fh)
end
function kui.frame:istrigger()
return BlzGetTriggerFrame() == self.fh
end
function kui.frame:suppress(bool)
-- prevent a frame's sound, showfunc, and hidefunc actions
-- after they are shown or hidden. Should be for temp use.
self.suppress = bool
end
function kui.frame:suppressall(bool)
-- run suppress for all canvas.game frames.
for _,fr in pairs(kui.canvas.game) do
if type(fr) == "table" then
fr.suppress = bool
end
end
end
function kui.frame:show(_datafunc)
if self.fh then
BlzFrameSetVisible(self.fh, true)
if self.showfunc and not self.suppress then self.showfunc() end
if not suppress_all_panel_sounds and self.statesnd and not self.suppress then self:statesound(1) end
if _datafunc then _datafunc() end
else
print("error: attempted to show frame with no 'fh' for "..self.fh)
end
end
function kui.frame:hide(_datafunc)
if self.fh then
BlzFrameSetVisible(self.fh, false)
if self.hidefunc and not self.suppress then self.hidefunc() end
if not suppress_all_panel_sounds and self.statesnd and not self.suppress then self:statesound(0) end
if _datafunc then _datafunc() end
else
print("error: attempted to hide frame with no 'fh' for "..self.fh)
end
end
function kui.frame:enable()
self.disabled = false
self:resettex()
end
function kui.frame:active()
-- for alternative functionality and graphics
-- e.g. a button has multiple states.
self.activated = true
self.disabled = false
self:resettex()
end
function kui.frame:inactive()
-- for alternative functionality and graphics
-- e.g. a button has multiple states.
self.activated = false
self.disabled = false
self:resettex()
end
function kui.frame:disable()
self.disabled = true
self:resettex()
end
function kui.frame:focusfix()
-- remove keyboard focus hack:
-- *note this needs to run for local p only.
if utils.islocaltrigp() then
BlzFrameSetEnable(self.fh, false)
BlzFrameSetEnable(self.fh, true)
end
end
function kui.frame:settext(str)
BlzFrameSetText(self.fh, str)
end
-- @targetfr = [optional] when provided, the hovered frame will instead alter this frame object.
-- (useful for when backdrops are used behind buttons)
function kui.frame:addhoverhl(targetfr)
-- makes a frame light up on hover.
local targetfr = targetfr or nil
if not targetfr then
self.alphahl = self.alpha
self.alpha = math.floor(self.alpha*self.alpharatio)
self:resetalpha()
else
targetfr.alphahl = targetfr.alpha
targetfr.alpha = math.floor(targetfr.alpha*targetfr.alpharatio)
targetfr:resetalpha()
end
local enter,leave
if targetfr ~= nil then
enter = function()
if utils.islocaltrigp() then
BlzFrameSetAlpha(targetfr.fh,targetfr.alphahl)
if targetfr and targetfr.statesnd and targetfr:isnotpreviousframesoundfh() then
utils.playsound(kui.sound.hoverbtn)
end
end
end
leave = function() targetfr:resetalpha() end
else
enter = function()
if utils.islocaltrigp() then
BlzFrameSetAlpha(self.fh,self.alphahl)
if targetfr and targetfr.statesnd and targetfr:isnotpreviousframesoundfh() then
utils.playsound(kui.sound.hoverbtn)
end
end
end
leave = function() self:resetalpha() end
end
self:event(enter, FRAMEEVENT_MOUSE_ENTER)
self:event(leave, FRAMEEVENT_MOUSE_LEAVE)
end
function kui.frame:isnotpreviousframesoundfh()
-- 1.33 tacky fix for enter/exit loop bug:
if kui.previousframesoundfh and kui.previousframesoundfh == self.fh then
return false -- stop sound from repeating
else
kui.previousframesoundfh = self.fh
return true -- allow first sound
end
end
function kui.frame:addtexhl(targetfr)
if targetfr.normaltex and targetfr.hovertex then
enter = function()
if utils.islocaltrigp() then
if not self.disabled then targetfr:setbgtex(targetfr.hovertex) end
if self.statesnd and self:isnotpreviousframesoundfh() then
utils.playsound(kui.sound.hoverbtn)
end
end
end
leave = function()
if utils.islocaltrigp() then
if not self.disabled then
if self.activated then
targetfr:setbgtex(targetfr.activetex)
else
targetfr:setbgtex(targetfr.normaltex)
end
else
targetfr:setbgtex(targetfr.distex)
end
end
end
self:event(enter, FRAMEEVENT_MOUSE_ENTER)
self:event(leave, FRAMEEVENT_MOUSE_LEAVE)
else
print("error: addtexhl could not find a normal or hover texture")
end
end
-- @targetfr = [optional] when provided, the hovered frame will instead alter this frame object.
-- (useful for when backdrops are used behind buttons)
-- @scale = [optional] override default scale
function kui.frame:addhoverscale(targetfr, scale)
-- makes a frame grow in scale when hovered.
local scale = scale or kui.meta.hoverscale
local enter,leave
if targetfr ~= nil then
enter = function()
if utils.islocaltrigp() and not self.disablehoverscale then
BlzFrameSetScale(targetfr.fh,scale)
if self.statesnd and self:isnotpreviousframesoundfh() then utils.playsound(kui.sound.hoversft) end
end
end
leave = function() targetfr:resetscale() end
else
enter = function()
if utils.islocaltrigp() and not self.disablehoverscale then
BlzFrameSetScale(self.fh,scale)
if self.statesnd and self:isnotpreviousframesoundfh() then utils.playsound(kui.sound.hoversft) end
end
end
leave = function() self:resetscale() end
end
self:event(enter, FRAMEEVENT_MOUSE_ENTER, kui.debughover)
self:event(leave, FRAMEEVENT_MOUSE_LEAVE, kui.debughover)
end
-- @id = unit to create.
-- @charid = class id to map custom assets.
-- @selsnd = [optional] character selected sound to play.
function kui.frame:addselectclassevent(id, charid, selsnd)
-- create a new unit, using smart handling (trig player or trig unit owner).
local trig = CreateTrigger()
kui:addclasslistener(trig)
local click = function()
utils.debugfunc(function()
local p = utils.trigp()
local pnum = utils.pnum(p)
if not kobold.player[p].unit then -- events dup sometimes; see if unit was made already.
BlzSendSyncData("classpick", tostring(pnum)..tostring(charid)..tostring(id))
if p == utils.localp() then
if selsnd then utils.playsound(selsnd, p) end
end
end
end,"selectclass")
end
self:addnetclickevent(click)
end
function kui:addclasslistener(trig)
for p,_ in pairs(kobold.playing) do
BlzTriggerRegisterPlayerSyncEvent(trig, p, "classpick", false)
end
TriggerAddAction(trig, function()
utils.debugfunc(function()
local d = BlzGetTriggerSyncData()
local pid = string.sub(d,1,1)
local charid = string.sub(d,2,2)
local id = string.sub(d,3)
charid = tonumber(charid)
pid = tonumber(pid)
local p = Player(pid-1)
if not kobold.player[p].unit then
utils.setcambounds(gg_rct_expeditionVision, p)
kobold.player[p].unit = utils.unitinrect(kui.meta.rectspawn, p, id)
kobold.player[p].charid = charid
kui.effects.char[charid]:playu(kobold.player[p].unit)
utils.restorestate(kobold.player[p].unit)
kui.canvas.game.equip:initequipment(charid, p)
kobold.player[p].ability = ability.template:new(p, charid)
kobold.player[p].ability:load(charid) -- keep below template:new
kobold.player[p].ability:learn(1, 1)
kobold.player[p].keymap = hotkey.map:new(p)
kobold.player[p]:updateallstats()
utils.pantorect(p, kui.meta.rectspawn, 0.0)
kui:initializefootsteps(kobold.player[p])
--
if not kobold.player[p].loadchar then
quest:load_current()
end
kui:hidesplash(p, true)
-- TODO: how would this work if we implement multiplayer?:
if not kk.devmode then
if freeplay_mode then
utils.timed(1.21, function() screenplay:run(screenplay.chains.narrator_intro_freeplay) end)
elseif not kobold.player[p].loadchar then
utils.timed(1.21, function() screenplay:run(screenplay.chains.narrator_intro) end)
else
kui:showgameui(p, true)
end
else
kui:showgameui(p, true)
end
if kui.lockcam then utils.permselect(kobold.player[p].unit, p, kobold.player[p]) end -- cam and select lock.
kui.canvas.game.party.pill[pid] = kui:createpartypill(kui.canvas.game.party, pid, charid)
kui:attachupdatetimer(p, pid) -- hp/mana bar and party pill timer.
-- enable freeplay features if selected:
if freeplay_mode and not kobold.player[p].loadchar then
kobold.player[p]:freeplaymode()
end
-- badges:
badge:load_data(kobold.player[p])
-- click to move:
udg_click_move_unit = {}
udg_click_move_unit[utils.pid(p)] = kobold.player[p].unit
ctm.sys:create()
-- recolor and add attachments:
if charid == 1 then -- tunneler
SetUnitColor(kobold.player[p].unit, GetPlayerColor(Player(4)))
AddSpecialEffectTargetUnitBJ("right hand", kobold.player[p].unit, "war3mapImported\\Akamas Kama3.mdx")
BlzSetSpecialEffectScale(GetLastCreatedEffectBJ(), 0.7)
AddSpecialEffectTargetUnitBJ("left hand", kobold.player[p].unit, "war3mapImported\\SpiritLantern_MyriadFungiForHive.mdx")
BlzSetSpecialEffectScale(GetLastCreatedEffectBJ(), 0.75)
elseif charid == 2 then -- geomancer
SetUnitColor(kobold.player[p].unit, GetPlayerColor(Player(3)))
AddSpecialEffectTargetUnitBJ("right hand", kobold.player[p].unit, "war3mapImported\\NatureStaff - Corrupted.mdx")
BlzSetSpecialEffectScale(GetLastCreatedEffectBJ(), 0.7)
AddSpecialEffectTargetUnitBJ("left hand", kobold.player[p].unit, "war3mapImported\\SpiritLanternBlueForHive.mdx")
BlzSetSpecialEffectScale(GetLastCreatedEffectBJ(), 0.75)
elseif charid == 3 then -- rascal
SetUnitColor(kobold.player[p].unit, GetPlayerColor(Player(0)))
AddSpecialEffectTargetUnitBJ("right hand", kobold.player[p].unit, "war3mapImported\\MorgulBlade.mdx")
BlzSetSpecialEffectScale(GetLastCreatedEffectBJ(), 0.6)
AddSpecialEffectTargetUnitBJ("left hand", kobold.player[p].unit, "war3mapImported\\SpiritLanternRedForHive.mdx")
BlzSetSpecialEffectScale(GetLastCreatedEffectBJ(), 0.75)
elseif charid == 4 then -- wickfighter
SetUnitColor(kobold.player[p].unit, GetPlayerColor(Player(5)))
AddSpecialEffectTargetUnitBJ("right hand", kobold.player[p].unit, "war3mapImported\\Fire Sword.mdx")
BlzSetSpecialEffectScale(GetLastCreatedEffectBJ(), 0.7)
AddSpecialEffectTargetUnitBJ("left hand", kobold.player[p].unit, "war3mapImported\\HandHeldLantern.mdx")
BlzSetSpecialEffectScale(GetLastCreatedEffectBJ(), 0.75)
end
AddSpecialEffectTargetUnitBJ("chest", kobold.player[p].unit, "war3mapImported\\Backpack_by_dioris.mdx")
BlzSetSpecialEffectScale(GetLastCreatedEffectBJ(), 0.9)
AddSpecialEffectTargetUnitBJ("head", kobold.player[p].unit, "war3mapImported\\Candle1.mdx")
BlzSetSpecialEffectScale(GetLastCreatedEffectBJ(), 0.9)
end
end, "select class")
end)
end
function kui:attachupdatetimer(p, pid)
local p, pid = p, pid
-- attach state bar update timer:
TimerStart(NewTimer(),0.2,true,function()
if utils.localp() == p then -- interface bars (local only).
if kui.canvas.game.skill:isvisible() then
if not kobold.player[p].downed then
utils.debugfunc(function()
BlzFrameSetValue( kui.canvas.game.skill.hpbar.fh,
math.floor(100-GetUnitLifePercent(kobold.player[p].unit)) ) -- hp bar is inversed
BlzFrameSetValue( kui.canvas.game.skill.manabar.fh,
math.ceil(GetUnitManaPercent(kobold.player[p].unit)) )
BlzFrameSetText( kui.canvas.game.skill.hptxt.fh,
math.floor(GetUnitState(kobold.player[p].unit, UNIT_STATE_LIFE))
.."/"
..math.floor(GetUnitState(kobold.player[p].unit, UNIT_STATE_MAX_LIFE)))
BlzFrameSetText( kui.canvas.game.skill.manatxt.fh,
math.floor(GetUnitState(kobold.player[p].unit, UNIT_STATE_MANA))
.."/"
..math.floor(GetUnitState(kobold.player[p].unit, UNIT_STATE_MAX_MANA)))
end, "update bars")
else
BlzFrameSetText( kui.canvas.game.skill.hptxt.fh, color:wrap(color.tooltip.bad, "Incapacitated"))
BlzFrameSetValue( kui.canvas.game.skill.hpbar.fh, 100)
BlzFrameSetText( kui.canvas.game.skill.manatxt.fh, color:wrap(color.tooltip.bad, "Incapacitated"))
BlzFrameSetValue( kui.canvas.game.skill.manabar.fh, 0)
end
end
end
if not kobold.player[p].downed then -- party pill bars (non-local).
BlzFrameSetValue( kui.canvas.game.party.pill[pid].hpbar.fh, math.ceil(GetUnitLifePercent(kobold.player[p].unit)) )
BlzFrameSetValue( kui.canvas.game.party.pill[pid].manabar.fh, math.ceil(GetUnitManaPercent(kobold.player[p].unit)) )
else
BlzFrameSetValue( kui.canvas.game.party.pill[pid].hpbar.fh, 0 )
BlzFrameSetValue( kui.canvas.game.party.pill[pid].manabar.fh, 0 )
end
end )
end
-- @charid = matching class id.
-- @p = for this local player.
function kui.frame:initequipment(charid, p)
-- since we use custom graphics, base equip is empty,
-- we then update local player textures.
if p == utils.localp() then
self:addbgtex(kui.tex.char[charid].panelequip)
self:hide()
end
end
function kui.frame:resetalpha()
if self.fh then BlzFrameSetAlpha(self.fh, self.alpha)
else print("error: resetalpha found no 'fh' for frame object.") end
end
function kui.frame:setalpha(val)
if self.fh then BlzFrameSetAlpha(self.fh, math.floor(val)) end
end
function kui.frame:setnewalpha(val)
if self.fh then self.alpha = math.floor(val) BlzFrameSetAlpha(self.fh, self.alpha) end
end
function kui.frame:setscale(scale)
if self.fh then BlzFrameSetScale(self.fh, scale) end
end
function kui.frame:resetscale()
if self.fh then BlzFrameSetScale(self.fh,1.0)
else print("error: resetscale found no 'fh' for frame object.") end
end
function kui.frame:resettex()
if self.fh then
if self.disabled and self.distex then
BlzFrameSetTexture(self.fh, self.distex, 0, true)
elseif self.activated and self.activetex then
BlzFrameSetTexture(self.fh, self.activetex, 0, true)
elseif self.normaltex then
BlzFrameSetTexture(self.fh, self.normaltex, 0, true)
end
else print("error: resettex found no 'fh' for frame object.") end
end
function kui.frame:reset()
self:resetscale()
self:resetalpha()
self:resettex()
end
-- @w,h = width, height (% of screen)
function kui.frame:setsize(w, h)
-- set the height, width, or both of a frame with ease.
-- pass nil for the one to ignore when using single arg.
if w and h then
BlzFrameSetSize(self.fh, w, h)
elseif w then
BlzFrameSetSize(self.fh, w, BlzFrameGetHeight(self.fh))
elseif h then
BlzFrameSetSize(self.fh, BlzFrameGetWidth(self.fh), h)
else
print("error: setsize found no width and height args")
end
end
function kui.frame:w()
return BlzFrameGetWidth(self.fh)
end
function kui.frame:h()
return BlzFrameGetHeight(self.fh)
end
-- @anchor = frame point of owner frame.
-- @targetanchor = frame point of the target frame.
-- @targetfr = attach frame object to this frame.
-- @x,y = [optional] screen percentage offset from anchor.
function kui.frame:setfp(anchor, targetanchor, targetfr, x, y)
-- supports frame handle or frame object.
local x = x or 0.0
local y = y or 0.0
if type(targetfr) == "table" then -- object
if targetfr.fh then
BlzFrameSetPoint(self.fh, anchor, targetfr.fh, targetanchor, x, y)
else
print("error: setfp() could not find a framehandle on 'targetfr'.")
end
else -- framehandle
BlzFrameSetPoint(self.fh, anchor, targetfr, targetanchor, x, y)
end
end
-- @anchor = frame point of owner frame.
-- @x,y = abs screen percentage offset.
function kui.frame:setabsfp(anchor, x, y)
-- supports frame handle or frame object.
BlzFrameSetAbsPoint(self.fh, anchor, x, y)
end
-- @targetfr = frame to match points.
function kui.frame:setallfp(targetfr)
-- supports frame handle or frame object.
if type(targetfr) == "table" then -- object
self:clearallfp()
BlzFrameSetAllPoints(self.fh, targetfr.fh)
else -- framehandle
BlzFrameSetAllPoints(self.fh, targetfr)
end
end
function kui.frame:clearallfp()
if self.fh then
BlzFrameClearAllPoints(self.fh)
end
end
-- @parentfr = set this as parent.
function kui.frame:setparent(parentfr)
-- supports frame handle or frame object.
if type(targetfr) == "table" then -- object
BlzFrameSetParent(self.fh, parentfr.fh)
else -- framehandle
BlzFrameSetParent(self.fh, parentfr)
end
end
function kui.frame:clearallfp()
BlzFrameClearAllPoints(self.fh)
end
function kui.frame:verifytables()
if not self.string then self.string = {} end
end
function kui.frame:addeventdebug(trig, eventnamestr)
-- print frame events and frame names when testing
-- frame event components.
if not self.suppressdebug then
TriggerAddAction(trig, function()
print("::: new debug event :::")
if self.debugn then print("'"..self.debugn.."'") end
print("'"..eventnamestr.."' : trig: "..GetHandleId(trig).." fr: "..GetHandleId(self.fh).." fh: "..GetHandleId(BlzGetTriggerFrame()))
end)
end
end
function kui:pxdpiscale()
-- for screens that have smaller than standard 0.6 max Y size
-- since pixel values were pulled from a 1080px screen, use
-- that as the baseline denominator.
self.dpiratio = 0.6*(self.screenh/1080.0)
end
function kui:pxtodpi()
return self.dpiratio/self.screenh
end
function kui:px(pixels)
return pixels*kui:pxtodpi()
end
function kui:setfullscreen(fh)
-- pass a framehandle and set it to cover the entire screen.
BlzFrameSetSize(fh, kui:px(1920), kui:px(1080))
BlzFrameSetPoint(fh, fp.c, self.worldui, fp.c, 0, 0)
end
-- @parentfr = assigned parent frame (e.g. the canvas frame).
-- @texture = use this texture as the backdrop
-- @panelid = determines positioning (ordering 1, 2, 3 on screen)
function kui:createmainframeparent(texture, parentfr, panelid)
-- create a large container for main UI components (inventory, equipment, char panel, etc.).
local fr = kui.frame:newbytype("BACKDROP", parentfr)
local xpos
if panelid == 1 then -- leftmost panel
xpos = self.position.centerx-(kui:px(kui.meta.mfw)+kui:px(kui.meta.mfp))
elseif panelid == 2 then -- center panel
xpos = self.position.centerx
elseif panelid == 3 then -- rightmost panel
xpos = self.position.centerx+(kui:px(kui.meta.mfw)+kui:px(kui.meta.mfp))
end
fr.ismf = true
fr.statesnd = true
fr.panelid = panelid
fr:addbgtex(texture)
fr:setsize(kui:px(kui.meta.mfw), kui:px(kui.meta.mfh))
fr:setabsfp(fp.c, xpos, 0.32 + kui:px(kui.meta.skillh*0.5))
fr:createmainframeclosebtn()
return fr
end
function kui:createmassivepanel(headerstr, texture)
-- build scroll panel that takes up the whole screen:
local texture = texture or kui.meta.massivescroll.tex
local fr = kui.frame:newbytype("BACKDROP", kui.canvas.game)
fr:addbgtex(texture)
fr:setsize(kui:px(kui.meta.massivescroll.w), kui:px(kui.meta.massivescroll.h))
fr:setabsfp(fp.c, kui.position.centerx, 0.32 + kui:px(kui.meta.skillh*0.5))
fr:addtext("btn",headerstr,fp.c)
fr.txt:setfp(fp.c, fp.c, fr, kui:px(kui.meta.massivescroll.headerx), kui:px(kui.meta.massivescroll.headery) - kui:px(38))
fr:createmainframeclosebtn()
return fr
end
function kui:createcharpage(parentfr)
-- this frame is complicated so we are going the hardcoded route.
local ltext, rtext, lval, rval = {},{},{},{}
ltext[1], ltext[2], rtext[1], rtext[2], lval[1], lval[2], rval[1], rval[2] = tooltip:charcleanse(kobold.player)
local fr = self:createmainframeparent(kui.tex.panelchar, parentfr, 1)
local offx = kui:px(97) -- gap between attr values.
local funcs = {}
-- hidden parent for group control:
fr.attr = self.frame:newbytype("PARENT", parentfr)
fr.attr:setlvl(1)
-- 1 = str, 2 = wis, 3 = ala, 4 = vit
for i = 1,4 do
local attrid = p_attr_map[i]
funcs[i] = function() utils.debugfunc(function() kobold.player[utils.trigp()]:applyattrpoint(attrid) end, "applyattrpoint") end
-- start attribute values:
fr.attr[i] = fr:createtext("0", 1.0, true)
fr.attr[i]:setsize(kui:px(54), kui:px(20))
fr.attr[i]:setfp(fp.c, fp.tl, fr, kui:px(90) + ((i-1)*offx), -kui:px(216))
-- start "add attribute +" button:
fr.attr[i].addbd = kui.frame:newbytype("BACKDROP", fr)
fr.attr[i].addbd:addbgtex(kui.tex.addatr)
fr.attr[i].addbd:setsize(kui:px(43), kui:px(25))
fr.attr[i].addbd:setfp(fp.c, fp.tl, fr, kui:px(90) + ((i-1)*offx), -kui:px(239))
fr.attr[i].addbd:setlvl(1)
fr.attr[i].addbtn = kui.frame:newbytype("BUTTON", fr.attr[i].addbd)
fr.attr[i].addbtn:setallfp(fr.attr[i].addbd)
fr.attr[i].addbtn:assigntooltip("attrzero")
fr.attr[i].addbtn:addevents(nil, nil, funcs[i], nil, false)
-- start invisible attribute wrapper for hovering tooltip
fr.attr[i].tiptxt = fr:createheadertext(color:wrap(color.tooltip.good, ""))
fr.attr[i].tiptxt:setfp(fp.c, fp.c, fr.attr[i].addbd, 0.0, kui:px(70))
fr.attr[i].tiptxt:setsize(kui:px(70), kui:px(70))
end
fr.attr[1].tiptxt:assigntooltip("strength")
fr.attr[2].tiptxt:assigntooltip("wisdom" )
fr.attr[3].tiptxt:assigntooltip("alacrity")
fr.attr[4].tiptxt:assigntooltip("vitality")
-- left/right page arrows:
fr.rightarr = kui.frame:newbtntemplate(fr, "war3mapImported\\icon-arrow_right.blp")
fr.rightarr:setsize(kui:px(32), kui:px(32))
fr.rightarr:setfp(fp.tl, fp.tl, fr, kui:px(395), kui:px(-279))
fr.rightarr.btn:addhoverhl(fr.rightarr)
fr.leftarr = kui.frame:newbtntemplate(fr, "war3mapImported\\icon-arrow_left.blp")
fr.leftarr:setsize(kui:px(32), kui:px(32))
fr.leftarr:setfp(fp.c, fp.c, fr.rightarr)
fr.leftarr.btn:addhoverhl(fr.leftarr)
fr.leftarr:hide()
-- start building page text:
fr.page = {}
for i = 1,2 do
fr.page[i] = kui.frame:newbytype("PARENT", fr)
fr.page[i].enhl = fr.page[i]:createtext(ltext[i]) -- left block
fr.page[i].enhl:setsize(kui:px(162), kui:px(210))
fr.page[i].enhl:setfp(fp.tl, fp.tl, fr, kui:px(60), -kui:px(310))
fr.page[i].enhr = fr.page[i]:createtext(rtext[i]) -- right block
fr.page[i].enhr:setsize(kui:px(162), kui:px(210))
fr.page[i].enhr:setfp(fp.tl, fp.tl, fr, kui:px(245), -kui:px(310))
-- start "enhancements" data value blocks:
fr.page[i].lval = fr.page[i]:createtext(lval[i]) -- left vals
fr.page[i].lval:setsize(kui:px(162), kui:px(210))
fr.page[i].lval:setfp(fp.tr, fp.tr, fr.page[i].enhl, kui:px(6), 0.0)
BlzFrameSetTextAlignment(fr.page[i].lval.fh, TEXT_JUSTIFY_TOP, TEXT_JUSTIFY_RIGHT)
fr.page[i].rval = fr.page[i]:createtext(rval[i]) -- right vals
fr.page[i].rval:setsize(kui:px(162), kui:px(210))
fr.page[i].rval:setfp(fp.tr, fp.tr, fr.page[i].enhr, kui:px(10), 0.0)
BlzFrameSetTextAlignment(fr.page[i].rval.fh, TEXT_JUSTIFY_TOP, TEXT_JUSTIFY_RIGHT)
end
local leftarrclick = function()
if utils.islocaltrigp() then
fr.leftarr:hide()
fr.rightarr:show()
fr.page[1]:show()
fr.page[2]:hide()
end
end
local rightarrclick = function()
if utils.islocaltrigp() then
fr.rightarr:hide()
fr.leftarr:show()
fr.page[1]:hide()
fr.page[2]:show()
end
end
fr.leftarr.btn:addevents(nil, nil, leftarrclick, nil, nil)
fr.rightarr.btn:addevents(nil, nil, rightarrclick, nil, nil)
fr.page[2]:hide()
-- kobold level indicator:
fr.lvl = fr:createheadertext(color:wrap(color.tooltip.alert,"1")) -- left block
fr.lvl:setsize(kui:px(70), kui:px(34))
fr.lvl:setfp(fp.c, fp.c, fr, kui:px(59), -kui:px(259))
fr.lvl:assigntooltip("level")
-- points available label:
fr.pointsbd = kui.frame:newbytype("BACKDROP", fr)
fr.pointsbd:addbgtex("war3mapImported\\ui-scroll_decoration.blp")
fr.pointsbd:setsize(kui:px(240), kui:px(60))
fr.pointsbd:setfp(fp.c, fp.t, fr, 0.0, -kui:px(92))
fr.points = fr.pointsbd:createtext("Points Available: "..color:wrap(color.txt.txtdisable,"0"), nil, true) -- left block
fr.points:setsize(kui:px(160), kui:px(26))
fr.points:setfp(fp.c, fp.c, fr.pointsbd, 0.0, kui:px(3))
fr.pointsbd:hide() -- only show when points > 0
-- auto-assign btn:
fr.autoattr = kui.frame:newbtntemplate(fr, 'war3mapImported\\btn-add_attr_auto.blp')
fr.autoattr:setsize(kui:px(43), kui:px(25))
fr.autoattr:setfp(fp.c, fp.t, fr, kui:px(0), kui:px(-110))
fr.autoattr.btn:assigntooltip("attrauto")
fr.autoattr.btn:addevents(nil, nil, function() kobold.player[utils.trigp()]:autoapplyattr() end)
fr.autoattr:hide()
ltext = nil rtext = nil lval = nil rval = nil
return fr
end
function kui:createequipment(parentfr)
-- this frame has manually placed invisible buttons
-- to help save on total frame count.
local fr = self:createmainframeparent(self.color.black, parentfr, 2)
fr.space = {}
for slotid = 1001,1010 do -- we do a global "item space" table, with equip at the very end.
fr.space[slotid] = self.frame:newbytype("BUTTON", fr)
fr.space[slotid]:setsize(kui:px(kui.meta.equipslotw), kui:px(kui.meta.equipslotw))
fr.space[slotid]:setlvl(2)
fr.space[slotid]:initializeitemslot(slotid)
fr.space[slotid].icon = self.frame:newbytype("BACKDROP", fr)
fr.space[slotid].icon:setsize(kui:px(52), kui:px(52))
fr.space[slotid].icon:setfp(fp.c, fp.c, fr.space[slotid])
fr.space[slotid].icon:addbgtex(kui.tex.invis)
fr.space[slotid].rareind = kui.frame:newbytype("BACKDROP", fr.space[slotid])
fr.space[slotid].rareind:setallfp(fr.space[slotid].icon)
fr.space[slotid].rareind:addbgtex(kui.tex.invis)
end
-- unequip item selection pane:
fr:newitempane(fr)
-- equipment slots:
fr.space[1001]:setfp(fp.c, fp.tl, fr, kui:px(96), -kui:px(215))
fr.space[1002]:setfp(fp.c, fp.tl, fr, kui:px(96), -kui:px(317))
fr.space[1003]:setfp(fp.c, fp.tl, fr, kui:px(96), -kui:px(420))
fr.space[1004]:setfp(fp.c, fp.tl, fr, kui:px(190), -kui:px(503))
fr.space[1005]:setfp(fp.c, fp.tl, fr, kui:px(293), -kui:px(503))
fr.space[1006]:setfp(fp.c, fp.tl, fr, kui:px(380), -kui:px(420))
fr.space[1007]:setfp(fp.c, fp.tl, fr, kui:px(380), -kui:px(317))
fr.space[1008]:setfp(fp.c, fp.tl, fr, kui:px(380), -kui:px(215))
fr.space[1009]:setfp(fp.c, fp.tl, fr, kui:px(292), -kui:px(130))
fr.space[1010]:setfp(fp.c, fp.tl, fr, kui:px(189), -kui:px(130))
fr.hidefunc = function() fr.pane:hide() end
-- save btn:
fr.save = kui.frame:newbtntemplate(fr, 'war3mapImported\\btn-save_char.blp')
fr.save:setfp(fp.c, fp.b, fr, kui:px(-180), kui:px(65))
fr.save:setsize(kui:px(38), kui:px(37))
fr.save.btn:assigntooltip("savechar")
fr.save.btn:addevents(nil, nil, function()
utils.debugfunc(function()
local p = utils.trigp()
if not kobold.player[p].pausesave then
kobold.player[p].pausesave = true
kobold.player[p].char:save_character()
utils.playsound(kui.sound.splashclk, p)
fr.save:setnewalpha(175)
utils.timed(1.5, function() fr.save:setnewalpha(255) kobold.player[p].pausesave = nil end)
else
utils.palert(p, "Recently saved, try again in a moment", 3.0)
end
end, "save btn")
end)
-- unequip all btn:
fr.unequipall = kui.frame:newbtntemplate(fr, 'war3mapImported\\btn-unequip_all.blp')
fr.unequipall:setfp(fp.c, fp.b, fr, kui:px(172), kui:px(65))
fr.unequipall:setsize(kui:px(38), kui:px(37))
fr.unequipall.btn:assigntooltip("unequipall")
fr.unequipall.btn:addevents(nil, nil, function()
utils.debugfunc(function()
loot:unequipall(utils.trigp())
end, "sortall")
end)
fr.pane:hide()
return fr
end
function kui:createcurrencypanel(parentfr)
local fr = kui.frame:newbytype("BACKDROP", parentfr)
fr:addbgtex("war3mapImported\\panel-currency-widget.blp")
fr:setsize(kui:px(353), kui:px(89))
fr.txt = {}
for id = 1,6 do
fr.txt[id] = fr:createtext(color:wrap(color.txt.txtdisable, "0"))
BlzFrameSetTextAlignment(fr.txt[id].fh, TEXT_JUSTIFY_TOP, TEXT_JUSTIFY_CENTER)
fr.txt[id]:setsize(kui:px(40), kui:px(40))
fr.txt[id]:assigntooltip(kui.meta.oretooltip[id])
fr.txt[id].icon = kui.frame:newbytype("BACKDROP", fr.txt[id])
fr.txt[id].icon:setbgtex(kui.meta.oreicon[id])
fr.txt[id].icon:setsize(kui:px(24), kui:px(24))
if id == 1 then
fr.txt[id]:setfp(fp.c, fp.tl, fr, kui:px(52), -kui:px(45))
else
fr.txt[id]:setfp(fp.c, fp.c, fr.txt[id-1], kui:px(50), 0.0)
end
fr.txt[id].icon:setfp(fp.c, fp.c, fr.txt[id], 0.0, -kui:px(12))
end
fr:setlvl(0)
return fr
end
function kui:createinventory(parentfr)
parentfr.curr = kui:createcurrencypanel(parentfr)
local fr = kui:createmainframeparent(kui.tex.panelinv, parentfr, 3)
local rowcount = 1
local slottotal = kui.meta.invmaxx*kui.meta.invmaxy
local paddingt = kui:px(kui.meta.invp)*(kui.meta.invmaxx-1)
local iconsize = ( kui:px(kui.meta.mfw) - paddingt - (kui:px(kui.meta.invg)*2) )/kui.meta.invmaxx
parentfr.curr:setfp(fp.c, fp.b, fr, 0, -kui:px(18)) -- move after mainframe is made.
-- create inventory spaces:
fr.space = {}
for slotid = 1,slottotal do
-- layering overview:
-- 1. item slot background .blp
-- 2. invisible mouse event button
-- 3. item icon .blp (invisible when empty)
fr.space[slotid] = kui.frame:newbytype("BACKDROP", fr)
fr.space[slotid].btn = kui.frame:newbytype("BUTTON", fr.space[slotid])
fr.space[slotid].icon = kui.frame:newbytype("BACKDROP", fr.space[slotid])
fr.space[slotid].rareind = kui.frame:newbytype("BACKDROP", fr.space[slotid])
fr.space[slotid].rareind:setallfp(fr.space[slotid].icon)
fr.space[slotid].rareind:setfp(fp.c, fp.b, fr.space[slotid].icon, 0, kui:px(2))
fr.space[slotid].rareind:addbgtex(kui.tex.invis)
fr.space[slotid]:setsize(iconsize,iconsize)
fr.space[slotid]:addbgtex(kui.tex.invslot)
fr.space[slotid]:setlvl(1)
fr.space[slotid].btn:setallfp(fr.space[slotid])
fr.space[slotid].btn:focusfix()
fr.space[slotid].btn:setlvl(2)
fr.space[slotid].btn:addhoverhl(fr.space[slotid])
fr.space[slotid].btn:initializeitemslot(slotid)
fr.space[slotid].icon:setsize(iconsize*0.73,iconsize*0.73)
fr.space[slotid].icon:setfp(fp.c, fp.c, fr.space[slotid])
fr.space[slotid].icon:addbgtex(kui.tex.invis)
-- -- --
if slotid == 1 then
fr.space[slotid]:setfp(fp.tl, fp.tl, fr.fh, kui:px(kui.meta.invg), -kui:px(kui.meta.invg + kui.meta.invoffy))
elseif math.fmod(slotid,kui.meta.invmaxx*rowcount+1) == 0 then
rowcount = rowcount+1
fr.space[slotid]:setfp(fp.tl, fp.bl, fr.space[slotid-kui.meta.invmaxx], 0.0, -kui:px(kui.meta.invp))
else
fr.space[slotid]:setfp(fp.tl, fp.tr, fr.space[slotid-1], kui:px(kui.meta.invp), 0.0)
end
end
-- create gold icon and txt:
fr.gold = kui.frame:newbtntemplate(fr, "war3mapImported\\tooltip-gold_icon.blp")
fr.gold:setsize(0.00888, 0.00888)
fr.gold:setfp(fp.c, fp.br, fr, kui:px(-70), kui:px(52))
fr.gold.btn:assigntooltip("gold")
fr.goldtxt = fr.gold:createtext("0")
fr.goldtxt:setfp(fp.r, fp.l, fr.gold, -0.004, -0.0006)
fr.goldtxt:setsize(kui:px(60), kui:px(20))
fr.goldtxt:assigntooltip("gold")
BlzFrameSetTextAlignment(fr.goldtxt.fh, TEXT_JUSTIFY_CENTER, TEXT_JUSTIFY_RIGHT)
-- ancient fragments:
fr.frag = kui.frame:newbtntemplate(fr, "war3mapImported\\icon-ancient_fragment.blp")
fr.frag:setsize(kui:px(17), kui:px(17))
fr.frag:setfp(fp.c, fp.bl, fr, kui:px(70), kui:px(52))
fr.frag.btn:assigntooltip("fragments")
fr.fragtxt = fr.frag:createtext("0")
fr.fragtxt:setfp(fp.l, fp.r, fr.frag, 0.004, -0.0006)
fr.fragtxt:setsize(kui:px(60), kui:px(20))
fr.fragtxt:assigntooltip("fragments")
BlzFrameSetTextAlignment(fr.fragtxt.fh, TEXT_JUSTIFY_CENTER, TEXT_JUSTIFY_LEFT)
-- mission rewards cache button:
fr.tcache = kui.frame:newbtntemplate(fr, "war3mapImported\\btn-loot_overflow.blp")
fr.tcache:setsize(kui:px(46), kui:px(46))
fr.tcache:setfp(fp.c, fp.b, fr, 0.0, kui:px(35))
fr.tcache:hide() -- shown when tcache is filled by mission reward manager.
fr.tcache.btn:assigntooltip("tcache")
fr.tcache.btn:addhoverscale(fr.tcache, 1.10)
local cacheclick = function() kobold.player[utils.trigp()]:fetchcachedloot() end
fr.tcache.btn:addevents(nil, nil, cacheclick, nil, false)
-- create inventory select pane:
fr.pane = kui.frame:newbytype("BACKDROP", fr)
fr.pane:addbgtex('war3mapImported\\inventory-selected_box.blp')
fr.pane:setsize(iconsize*0.73, iconsize*0.73)
fr.pane:setlvl(3)
for btnid = 1,2 do
fr.pane[btnid] = kui.frame:newbytype("BACKDROP", fr.pane)
fr.pane[btnid]:addbgtex(kui.tex.invis)
fr.pane[btnid]:setnewalpha(170)
fr.pane[btnid]:setsize(kui:px(32),kui:px(32))
fr.pane[btnid].icon = kui.frame:newbytype("BACKDROP", fr.pane)
fr.pane[btnid].icon:setallfp(fr.pane[btnid])
fr.pane[btnid].icon:addbgtex(kui.meta.inv[btnid].tex)
fr.pane[btnid].btn = kui.frame:newbytype("BUTTON", fr.pane)
fr.pane[btnid].btn:setallfp(fr.pane[btnid])
fr.pane[btnid].btn:assigntooltip(kui.meta.inv[btnid].tipstr)
end
local equipfunc = function()
utils.debugfunc( function() loot.item:equip(utils.trigp()) end, "equipfunc" ) -- selslotid is controlled by inventory pane selection.
end
local sellfunc = function()
utils.debugfunc( function() loot.item:sell(utils.trigp()) end, "sellfunc" )
end
fr.pane[1]:setfp(fp.bl, fp.tr, fr.pane, 0.0, 0.0)
fr.pane[2]:setfp(fp.tl, fp.br, fr.pane, 0.0, 0.0) -- sell btn
fr.pane[1].btn:addevents(nil, nil, equipfunc) -- equip btn
fr.pane[2].btn:addevents(nil, nil, sellfunc) -- sell btn
-- sort and sell all btns:
fr.sortall = kui.frame:newbtntemplate(fr, 'war3mapImported\\btn-sort_all.blp')
fr.sortall:setfp(fp.c, fp.b, fr, kui:px(198), kui:px(-24))
fr.sortall:setsize(kui:px(38), kui:px(37))
fr.sortall.btn:assigntooltip("invsortall")
fr.sortall.btn:addevents(nil, nil, function()
utils.debugfunc(function()
loot:inventorysortall(utils.trigp())
end, "sortall")
end)
fr.sellall = kui.frame:newbtntemplate(fr, 'war3mapImported\\btn-sell_all.blp')
fr.sellall:setfp(fp.c, fp.b, fr, kui:px(-198), kui:px(-24))
fr.sellall:setsize(kui:px(38), kui:px(37))
fr.sellall.btn:assigntooltip("invsellall")
fr.sellall.btn:addevents(nil, nil, function()
utils.debugfunc(function()
loot:inventorysellall(utils.trigp())
end, "sellall")
end)
-- wrap up:
fr.showfunc = function() fr.pane:hide() parentfr.curr:show() end
fr.hidefunc = function() fr.pane:hide() parentfr.curr:hide() end
fr.pane:hide()
return fr
end
function kui:createdigsitepage(parentfr)
local fr = kui.frame:newbytype("BACKDROP", parentfr) -- main parent
local cardct = 5 -- total biome ids
fr.panelid = 4
fr.statesnd = true
fr:addbgtex(kui.tex.paneldig)
fr:setsize(kui:px(kui.meta.digsx), kui:px(kui.meta.digsy))
fr:setabsfp(fp.c, kui.position.centerx, 0.31)
-- vault biome map piece:
fr.mappiece = kui.frame:newbytype("BACKDROP", fr)
fr.mappiece:setbgtex('war3mapImported\\panel-dig_site_last_boss_map_piece.blp')
fr.mappiece:setfp(fp.c, fp.tl, fr, kui:px(97), -kui:px(180))
fr.mappiece:setsize(kui:px(260), kui:px(260))
fr.mappiece:hide()
-- active boss biome backdrop:
fr.boss = kui.frame:newbytype("BACKDROP", fr)
fr.boss:setsize(kui:px(216), kui:px(225))
fr.boss:addbgtex('war3mapImported\\panel-dig_site_boss_fight_marker.blp')
fr.boss:setfp(fp.c, fp.c, fr)
fr.boss:hide()
-- dig start btn:
fr.dig = kui.frame:newbytype("BACKDROP", fr)
fr.dig:setsize(kui:px(kui.meta.digcard.btn.w), kui:px(kui.meta.digcard.btn.h))
fr.dig:addbgtex(kui.meta.digcard.btn.tex)
fr.dig:setfp(fp.c, fp.tl, fr, kui:px(kui.meta.digcard.btn.x), -kui:px(kui.meta.digcard.btn.y))
-- init state textures:
fr.dig.distex = kui.meta.digcard.btn.tex
fr.dig.activetex = kui.meta.digcard.btn.texact
fr.dig.hovertex = kui.meta.digcard.btn.texhvr
-- init invis wrapper btn:
fr.dig.btn = kui.frame:newbytype("BUTTON", fr)
fr.dig.btn:setallfp(fr.dig)
fr.dig.btn:assigntooltip("digstart")
fr.dig.btn:addtexhl(fr.dig)
local clickdigfunc = function()
-- FIXME: DESYNCS
-- only run if no map is live:
if not map.manager.activemap and not fr.dig.btn.disabled then
map.manager:run() -- generate map
fr:hide() -- hide map panel
fr.dig.btn:disable()
end
end
fr.dig.btn.disabled = true -- disable events/tooltip until a dig is picked
fr.dig.btn:addevents(nil, nil, clickdigfunc)
-- biome selection cards:
fr.card = kui.frame:newbytype("PARENT", fr)
for id = 1,cardct do -- cardid lookup (careful when reordering meta).
fr.card[id] = kui.frame:newbytype("PARENT", fr)
fr.card[id].bd = kui.frame:newbytype("BACKDROP", fr.card[id])
fr.card[id].bd.hovertex = kui.meta.digcard[id].texhvr
fr.card[id].bd.activetex = kui.meta.digcard[id].texact
fr.card[id].bd:setsize(kui:px(kui.meta.digcard[id].w), kui:px(kui.meta.digcard[id].h))
fr.card[id].bd:setfp(fp.c, fp.tl, fr, kui:px(kui.meta.digcard[id].x), -kui:px(kui.meta.digcard[id].y))
fr.card[id].bd:addbgtex(kui.meta.digcard[id].tex)
fr.card[id].btn = kui.frame:newbytype("BUTTON", fr.card[id])
fr.card[id].btn:setallfp(fr.card[id].bd)
fr.card[id].btn:assigntooltip("digsitecard")
fr.card[id].btn:addtexhl(fr.card[id].bd)
fr.card[id].btn.statesnd = true
-- initiate card selection function:
fr.card[id].clickfunc = function()
if map.manager.selectedid ~= id then
local p = utils.trigp()
if utils.localp() == p then
if map.manager.selectedid ~= nil then -- if previous card active, reset.
fr.card[map.manager.selectedid].bd:inactive()
fr.card[map.manager.selectedid].btn:inactive()
end
fr.selicon:show()
fr.selicon:clearallfp()
fr.selicon:setfp(fp.c, fp.c, fr.card[id].bd)
fr.card[id].bd:active()
fr.card[id].btn:active()
fr.dig:active()
fr.dig.btn:active()
utils.playsound(kui.sound.digsel)
end
map.manager:select(id) -- keep at end so we reference previous id above.
end
end
fr.card[id].btn:addevents(nil, nil, fr.card[id].clickfunc)
end
fr.card[5]:hide() -- hide vault
-- create dig selected btn that we show/hide later:
fr.selicon = kui.frame:newbytype("BACKDROP", fr)
fr.selicon:setsize(kui:px(kui.meta.digcard.btnsel.w), kui:px(kui.meta.digcard.btnsel.h))
fr.selicon:addbgtex(kui.meta.digcard.btnsel.tex)
fr.selicon:hide() -- show later
-- difficulty panel:
fr.diff = kui.frame:newbytype("BACKDROP", fr)
fr.diff:setfp(fp.c, fp.c, fr, kui:px(kui.meta.digdiff.texx), kui:px(kui.meta.digdiff.texy))
fr.diff:addbgtex(kui.meta.digdiff.tex)
fr.diff:setsize(kui:px(kui.meta.digdiff.texw), kui:px(kui.meta.digdiff.texh))
fr.diffsel = kui.frame:newbytype("BACKDROP", fr.diff) -- (keep above click func)
for id = 1,5 do
fr.diff[id] = kui.frame:newbtntemplate(fr.diff, kui.meta.digdiff[id].tex)
fr.diff[id]:setsize(kui:px(kui.meta.digdiff.btnwh), kui:px(kui.meta.digdiff.btnwh))
fr.diff[id].btn:addhoverhl(fr.diff[id])
fr.diff[id]:setfp(fp.c, fp.c, fr.diff, 0.0, kui:px(kui.meta.digdiff[id].y))
fr.diff[id].name = string.gsub(kui.meta.digdiff[id].name, "Difficulty:|n", "")
fr.diff[id].click = function()
local p = utils.trigp()
if not map.manager.activemap and not map.manager.difftimer and utils.trigp() == Player(0) and map.manager.diffid ~= id then
if kobold.player[p].level >= map.diff[id].lvlreq then
local previd = map.manager.diffid -- flip previous icon back.
for pnum = 1,kk.maxplayers do
map.manager.diffid = id -- set new difficulty.
if Player(pnum-1) == utils.localp() then
fr.diff[previd]:setnewalpha(255*kui.frame.alpharatio)
fr.diffsel:setfp(fp.c, fp.c, fr.diff[id], 0.0, 0.0)
fr.diff[id]:setnewalpha(255)
utils.palert(Player(pnum-1),
color:wrap(color.tooltip.alert, "Changed the difficulty to: ")..fr.diff[id].name, 3.25, true)
utils.playsound(kui.sound.digsel, Player(pnum-1))
end
end
map.manager.difftimer = true
TimerStart(NewTimer(), 3.0, false, function() map.manager.difftimer = false end)
else
utils.palert(p, "You do not meet the level requirement for that difficulty.", 3.25 )
end
elseif map.manager.activemap then
utils.palert(p, "You cannot change difficulty during a dig.", 3.25 )
elseif map.manager.difftimer and map.manager.diffid ~= id then
utils.palert(p, "You can only change difficulty once every few seconds.", 3.0 )
end
end
fr.diff[id].btn.advtip = {}
fr.diff[id].btn.advtip[1] = kui.meta.digdiff[id].tex
fr.diff[id].btn.advtip[2] = kui.meta.digdiff[id].name
fr.diff[id].btn.advtip[3] = kui.meta.digdiff[id].descript
fr.diff[id].btn:initializeadvtip()
fr.diff[id].btn:addevents(nil, nil, fr.diff[id].click, nil, false)
end
-- selected diff icon (map manager default):
fr.diffsel:setlvl(2)
fr.diffsel:addbgtex(kui.meta.digcard.btnsel.tex)
fr.diffsel:setsize(kui:px(40), kui:px(45))
fr.diffsel:setfp(fp.c, fp.c, fr.diff[map.manager.diffid], 0.0, 0.0)
-- unequip dig key selection pane:
fr:newitempane(fr)
-- dig site key slot:
local slotid = 1011 -- *note: hardcoded because kui runs first (slot_digkey)
fr.digkeybd = kui.frame:newbytype("BACKDROP", fr)
fr.digkeybd:addbgtex(kui.tex.digkeybd)
fr.digkeybd.activetex = kui.tex.digkeybdact
fr.digkeyglow = kui.frame:newbytype("BACKDROP", fr)
fr.digkeyglow:setbgtex('war3mapImported\\btn-green_glow_aura.blp')
fr.digkeyglow:setsize(kui:px(109), kui:px(117))
fr.digkeyglow:setfp(fp.c, fp.br, fr, kui:px(56), kui:px(179))
fr.digkeyglow:hide()
fr.space = {} -- dig key spaces (for now, only 1)
fr.space[slotid] = kui.frame:newbytype("BUTTON", fr)
fr.space[slotid]:setsize(kui:px(kui.meta.equipslotw), kui:px(kui.meta.equipslotw))
fr.space[slotid]:setfp(fp.c, fp.br, fr, kui:px(56), kui:px(179))
fr.space[slotid]:setlvl(1)
fr.space[slotid]:initializeitemslot(slotid)
fr.space[slotid]:assigntooltip("digkey")
fr.space[slotid].icon = kui.frame:newbytype("BACKDROP", fr)
fr.space[slotid].icon:setsize(kui:px(64), kui:px(64))
fr.space[slotid].icon:setfp(fp.c, fp.c, fr.space[slotid])
fr.space[slotid].icon:addbgtex(kui.tex.invis)
fr.digkeybd:setallfp(fr.space[slotid].icon)
fr.hidefunc = function()
if not map.manager.activemap then map.manager.selectedid = nil end
fr.selicon:hide()
fr.dig:disable()
fr.dig.btn:disable()
fr.pane:hide()
-- reset any active selected card textures:
for i = 1,cardct do fr.card[i].bd:inactive() fr.card[i].btn:inactive() end
end
-- quest marker above everything else:
fr.questmark = kui.frame:newbytype("BACKDROP", fr)
fr.questmark:setbgtex('war3mapImported\\panel-dig_site_quest_marker.blp')
fr.questmark:setsize(kui:px(41), kui:px(48))
fr.questmark:hide()
-- wrap up:
fr:createmainframeclosebtn()
fr.closebtn:setfp(fp.c, fp.tr, fr, -kui:px(294),-kui:px(26))
fr:hide()
return fr
end
function kui:createsplashscreen(parentfr)
-- master container:
local fr = kui.frame:newbytype("BACKDROP", parentfr)
-- create nav parent frames:
fr.splash = kui.frame:newbysimple("SimpleFrameSplash", fr)
-- main frames control groupings of btns:
fr.options = kui.frame:newbytype("BACKDROP", fr)
fr.newmf = kui.frame:newbytype("BACKDROP", fr)
fr.loadmf = kui.frame:newbytype("BACKDROP", fr)
fr.freemf = kui.frame:newbytype("BACKDROP", fr)
-- create splash screen scroll buttons:
fr.options.loadbtn = self:createsplashbtn(kui.tex.opload, fr.options, nil) -- load character scroll.
fr.options.freeplay = self:createsplashbtn('war3mapImported\\menu-scroll-option_freeplay.blp', fr.options, nil) -- load char scroll.
fr.options.newbtn = self:createsplashbtn(kui.tex.opnew, fr.options, nil) -- new character scroll.
fr.newmf.char01 = self:createsplashbtn(kui.tex.char[1].selectchar, fr.newmf, nil, kui.meta.tipcharselect[1]) -- tunneler.
fr.newmf.char02 = self:createsplashbtn(kui.tex.char[2].selectchar, fr.newmf, nil, kui.meta.tipcharselect[2]) -- geomancer.
fr.newmf.char03 = self:createsplashbtn(kui.tex.char[3].selectchar, fr.newmf, nil, kui.meta.tipcharselect[3]) -- rascal.
fr.newmf.char04 = self:createsplashbtn(kui.tex.char[4].selectchar, fr.newmf, nil, kui.meta.tipcharselect[4]) -- wickfighter.
-- simple frames can only have children targeted via framename:
local sfh = BlzGetFrameByName("SimpleFrameSplash",0)
BlzFrameSetLevel(sfh, 0)
utils.looplocalp(function() kui:setfullscreen(sfh) end)
fr:setfp(fp.c, fp.c, self.gameui)
fr.options.newbtn:setabsfp(fp.c, kui.position.centerx + kui:px(217), kui:px(195))
fr.options.newbtn.btn:assigntooltip("storymode")
fr.options.newbtn.btn:addswapevent(fr.newmf, fr.options) -- close all other options when this main frame is open.
-- load btn:
fr.options.loadbtn:setabsfp(fp.c, kui.position.centerx + kui:px(217 + 222), kui:px(195))
fr.options.loadbtn.btn:assigntooltip("loadchar")
-- character loading:
load_character_func = function(fileslot)
utils.debugfunc(function()
local p = utils.trigp()
if not kobold.player[p].isloading and not fr.loadmf.char[fileslot].btn.disabled then
-- disable all loading buttons from further clicks:
for i = 1,4 do
fr.loadmf.char[i].btn.disabled = true
fr.loadmf.char[i].btn.disablehoverscale = true
end
-- load character:
kobold.player[p].char.fileslot = fileslot
kobold.player[p].char:read_file(fileslot)
if kobold.player[p].char:get_file_data() then
kui:hidesplash(p, true)
else
print("error: file is corrupt or missing.")
return
end
kobold.player[p].char:load_character()
end
end, "load char")
end
-- load character btns:
fr.loadmf.char = kui.frame:newbytype("PARENT", fr.loadmf)
for i = 1,4 do
fr.loadmf.char[i] = self:createsplashbtn(kui.tex.invis, fr.loadmf)
fr.loadmf.char[i].txt = fr.loadmf.char[i]:createtext("Missing File")
fr.loadmf.char[i].txt:setfp(fp.b, fp.b, fr.loadmf.char[i], 0.0, kui:px(20))
BlzFrameSetTextAlignment(fr.loadmf.char[i].txt.fh, TEXT_JUSTIFY_BOTTOM, TEXT_JUSTIFY_CENTER)
fr.loadmf.char[i].txt:setsize(kui:px(90), kui:px(40))
fr.loadmf.char[i].btn:addevents(nil, nil, function() load_character_func(i) end)
-- load eyecandy tag:
fr.loadmf.char[i].loadbd = kui.frame:newbytype("BACKDROP", fr.loadmf.char[i])
fr.loadmf.char[i].loadbd:setbgtex("war3mapImported\\btn_save_icon.blp")
fr.loadmf.char[i].loadbd:setsize(kui:px(40), kui:px(40))
fr.loadmf.char[i].loadbd:setfp(fp.c, fp.br, fr.loadmf.char[i], kui:px(-45), kui:px(40))
-- delete file button:
fr.loadmf.char[i].delete = kui.frame:newbtntemplate(fr.loadmf.char[i], "war3mapImported\\btn_delete_icon.blp")
fr.loadmf.char[i].delete:setsize(kui:px(28), kui:px(28))
fr.loadmf.char[i].delete:setfp(fp.c, fp.tr, fr.loadmf.char[i], kui:px(-24), kui:px(-24))
fr.loadmf.char[i].delete.btn:assigntooltip("deletesave")
fr.loadmf.char[i].delete.btn:addevents(nil, nil, function()
utils.debugfunc(function()
local p = utils.trigp()
if kobold.player[p].clickdelete then
kui:showmodal(function()
utils.playsound(kui.sound.portalmerge, p)
kobold.player[p].char:delete_file(i)
if kobold.player[p]:islocal() then
fr.loadmf.char[i]:hide()
end
end)
utils.timed(0.33, function() kobold.player[p].clickdelete = nil end)
else
kobold.player[p].clickdelete = true
end
end, "click delete")
end)
fr.loadmf.char[i]:hide()
end
fr.loadmf.char[1]:setabsfp(fp.c, kui.position.centerx + kui:px(217), kui:px(195 + 285))
fr.loadmf.char[2]:setabsfp(fp.c, kui.position.centerx + kui:px(217 + 222), kui:px(195 + 285))
fr.loadmf.char[3]:setabsfp(fp.c, kui.position.centerx + kui:px(217), kui:px(195))
fr.loadmf.char[4]:setabsfp(fp.c, kui.position.centerx + kui:px(217 + 222), kui:px(195))
-- create 4 new cards that pull in file contents (char name, level)
fr.options.loadbtn.btn:addswapevent(fr.loadmf, fr.options) -- close all other options when this main frame is open.
fr.options.loadbtn.btn.disabled = true -- enabled by file verification.
-- freeplay btn:
fr.options.freeplay:setabsfp(fp.c, kui.position.centerx - kui:px(217), kui:px(195))
fr.options.freeplay.btn:addswapevent(fr.newmf, fr.options)
fr.options.freeplay.btn:addevents(nil, nil, function() freeplay_mode = true end)
fr.options.freeplay.btn:assigntooltip("freeplaymode")
-- back buttons:
fr.newmf.backbtn = kui.frame:createbackbtn(fr.options, fr.newmf)
fr.newmf.backbtn:setabsfp(fp.c, kui.position.centerx, kui:px(90))
fr.loadmf.backbtn = kui.frame:createbackbtn(fr.options, fr.loadmf)
fr.loadmf.backbtn:setabsfp(fp.c, kui.position.centerx, kui:px(90))
fr.freemf.backbtn = kui.frame:createbackbtn(fr.options, fr.newmf)
fr.freemf.backbtn:setabsfp(fp.c, kui.position.centerx, kui:px(90))
fr.freemf.backbtn.btn:addevents(nil, nil, function() freeplay_mode = false end)
-- reposition char scrolls:
fr.newmf.char01:setabsfp(fp.c, kui.position.centerx + kui:px(217), kui:px(195 + 285))
fr.newmf.char02:setabsfp(fp.c, kui.position.centerx + kui:px(217 + 222), kui:px(195 + 285))
fr.newmf.char03:setabsfp(fp.c, kui.position.centerx + kui:px(217), kui:px(195))
fr.newmf.char04:setabsfp(fp.c, kui.position.centerx + kui:px(217 + 222), kui:px(195))
-- *note: when interacting with splash scroll btns, remember to use .btn to target the hidden btn frame.
fr.newmf.char01.btn:addselectclassevent(kui.meta.char01raw, 1, self.sound.char01new)
fr.newmf.char02.btn:addselectclassevent(kui.meta.char02raw, 2, self.sound.char02new)
fr.newmf.char03.btn:addselectclassevent(kui.meta.char03raw, 3, self.sound.char03new)
fr.newmf.char04.btn:addselectclassevent(kui.meta.char04raw, 4, self.sound.char04new)
fr.options.loadbtn:setalpha(145)
fr.newmf:hide()
fr.loadmf:hide()
return fr
end
function kui:createbosschest(parentfr)
local fr = kui.frame:newbytype("PARENT", parentfr)
fr.chest = kui.frame:newbtntemplate(fr, 'war3mapImported\\boss-reward-chest.blp')
fr.chest.gem = {}
fr.chest.btn:assigntooltip("bosschest")
fr.chest.btn:addhoverscale(fr.chest, 1.10)
fr.chest:setabsfp(fp.c, kui.position.centerx, kui:px(345))
fr.chest:setlvl(0)
fr.chest:setsize(kui:px(180), kui:px(128))
for i = 1,5 do
fr.chest.gem[i] = kui.frame:newbytype("BACKDROP", fr.chest)
fr.chest.gem[i]:setsize(kui:px(56), kui:px(56))
fr.chest.gem[i]:setfp(fp.c, fp.c, fr.chest, kui:px(1), kui:px(-18))
fr.chest.gem[i]:hide()
end
fr.chest.gem[1]:setbgtex("war3mapImported\\icon_bosskey_chomp.blp")
fr.chest.gem[2]:setbgtex("war3mapImported\\icon_bosskey_slag.blp")
fr.chest.gem[3]:setbgtex("war3mapImported\\icon_bosskey_mutant.blp")
fr.chest.gem[4]:setbgtex("war3mapImported\\icon_bosskey_thawed.blp")
fr.chest.gem[5]:setbgtex("war3mapImported\\icon_bosskey_amalgam.blp")
local hoverfunc = function()
if utils.islocaltrigp() then
if not kui.previousframesoundfh or (kui.previousframesoundfh and kui.previousframesoundfh ~= fr.chest.btn.fh) then -- 1.33 fix
-- utils.playsound(kui.sound.hoverchest, utils.trigp())
end
end
end
local clickfunc = function()
utils.debugfunc(function()
local p = utils.trigp()
local x,y = utils.unitxy(kobold.player[p].unit)
local diffid = map.manager.prevdiffid
local count = math.max(2, map.manager.prevdiffid-1) -- +1 item roll on heroic and above, up to 4.
local oddsmod, rarityid = 0, 0
for i = 1,count do
oddsmod = kobold.player[p]:getlootodds() + 100*diffid
if diffid == 5 then
oddsmod = oddsmod + 2500
count = count*2
end
if diffid >= 4 then
rarityid = loot:getrandomrarity(rarity_epic, rarity_ancient, oddsmod)
elseif diffid >= 3 then
rarityid = loot:getrandomrarity(rarity_rare, rarity_ancient, oddsmod)
elseif diffid >= 2 then
rarityid = loot:getrandomrarity(rarity_rare, rarity_epic, oddsmod)
else
rarityid = loot:getrandomrarity(rarity_common, rarity_rare, oddsmod)
end
loot:generatelootmissile(x, y, loot.missile[rarityid], 2)
end
-- always drop 1 ancient item:
loot:generatelootmissile(x, y, loot.missile[4], 1)
-- always drop bonus loot for vault boss:
if map.manager.prevbiomeid == 5 then
loot:generatelootmissile(x, y, loot.missile[4], 1) -- bonus ancient.
loot:generatelootmissile(x, y, loot.missile[loot:getrandomrarity(rarity_common, rarity_ancient, 1500)], 2)
end
if utils.islocaltrigp() then
utils.playsound(kui.sound.openchest, p)
fr:hide()
end
end, 'boss chest')
end
fr.chest.btn:addevents(hoverfunc, nil, clickfunc)
fr:hide()
return fr
end
function kui:createskillbar(parentfr)
local fr = kui.frame:newbytype("PARENT", parentfr)
local ypad = kui:px(148) -- push up from bottom this much
local xpos = kui.position.centerx - kui:px(334)
local w,h = 72, 63 -- ability container w/h
fr.skillbg = kui.frame:newbysimple("KoboldSkillBarBackdrop", fr)
fr.skillbg:setabsfp(fp.b, kui.position.centerx + 0.05722, 0.0)
fr.skillbg:setlvl(0)
fr.skillbg:hide()
fr.skill = {}
for i = 1,8 do
fr.skill[i] = kui.frame:newbytype("FRAME", fr)
fr.skill[i]:setsize(kui:px(w), kui:px(h))
fr.skill[i]:setabsfp(fp.c, xpos, ypad)
-- ability icon:
fr.skill[i].fill = kui.frame:newbytype("BACKDROP", fr.skill[i])
fr.skill[i].fill:setsize(kui:px(52),kui:px(52))
fr.skill[i].fill:setfp(fp.c, fp.c, fr.skill[i], 0, 0)
fr.skill[i].fill:addbgtex(kui.color.black)
-- cooldown overlay:
fr.skill[i].cdfill = kui.frame:newbytype("BACKDROP", fr.skill[i])
fr.skill[i].cdfill:addbgtex('war3mapImported\\potion-cdfill.blp')
fr.skill[i].cdfill:setsize(kui:px(w*0.845),kui:px(h*0.845))
fr.skill[i].cdfill:setfp(fp.c, fp.c, fr.skill[i].fill, 0, 0)
fr.skill[i].cdfill:setnewalpha(200)
fr.skill[i].cdfill:hide()
fr.skill[i].hotkey = fr.skill[i]:createbtntext(kui.meta.skillhotkey[i], nil, true)
fr.skill[i].hotkey:setfp(fp.c, fp.b, fr.skill[i], 0.0, kui:px(12))
fr.skill[i].cdtxt = fr.skill[i]:createbtntext("", nil, true)
fr.skill[i].cdtxt:setallfp(fr.skill[i].fill)
fr.skill[i].cdtxt.advtip = {}
fr.skill[i].cdtxt.advtipanchor = fp.b
fr.skill[i].cdtxt.advattachanchor = fp.t
fr.skill[i].cdtxt:initializeadvtip()
-- ability card graphic:
fr.skill[i].card = kui.frame:newbytype("BACKDROP", fr.skill[i])
fr.skill[i].card:setsize(kui:px(w), kui:px(h))
fr.skill[i].card:setfp(fp.c, fp.c, fr.skill[i])
fr.skill[i].card:addbgtex(kui.tex.skillbd)
xpos = xpos + kui:px(w)
if i == 4 then xpos = xpos + kui:px(158) end
end
-- item: healing slot:
fr.skill[9] = kui.frame:newbtntemplate(fr, 'war3mapImported\\potion-health.blp')
fr.skill[9].txt = fr.skill[9]:createbtntext("F", nil, true)
fr.skill[9].txt:setfp(fp.b, fp.b, fr.skill[9], 0, 0)
fr.skill[9]:setabsfp(fp.c, kui.position.centerx - kui:px(398), kui:px(54))
fr.skill[9]:setsize(kui:px(63), kui:px(75))
fr.skill[9].cdfill = kui.frame:newbytype("BACKDROP", fr.skill[9])
fr.skill[9].cdfill:addbgtex('war3mapImported\\potion-cdfill.blp')
fr.skill[9].cdfill:setsize(kui:px(63),kui:px(75))
fr.skill[9].cdfill:setfp(fp.c, fp.c, fr.skill[9])
fr.skill[9].cdfill:setnewalpha(200)
fr.skill[9].cdfill:hide()
fr.skill[9].cdtxt = fr.skill[9]:createbtntext("", nil, true)
fr.skill[9].cdtxt:setallfp(fr.skill[9])
fr.skill[9].cdtxt:assigntooltip("healthpot")
-- fr.skill[9].btn:mapbtntohotkey("F")
-- item: mana slot:
fr.skill[10] = kui.frame:newbtntemplate(fr, 'war3mapImported\\potion-mana.blp')
fr.skill[10].txt = fr.skill[10]:createbtntext("G", nil, true)
fr.skill[10].txt:setfp(fp.b, fp.b, fr.skill[10], 0, 0)
fr.skill[10]:setabsfp(fp.c, kui.position.centerx + kui:px(398), kui:px(54))
fr.skill[10]:setsize(kui:px(63), kui:px(75))
fr.skill[10].cdfill = kui.frame:newbytype("BACKDROP", fr.skill[10])
fr.skill[10].cdfill:addbgtex('war3mapImported\\potion-cdfill.blp')
fr.skill[10].cdfill:setsize(kui:px(63),kui:px(75))
fr.skill[10].cdfill:setfp(fp.c, fp.c, fr.skill[10])
fr.skill[10].cdfill:setnewalpha(200)
fr.skill[10].cdfill:hide()
fr.skill[10].cdtxt = fr.skill[10]:createbtntext("", nil, true)
fr.skill[10].cdtxt:setallfp(fr.skill[10])
fr.skill[10].cdtxt:assigntooltip("manapot")
-- fr.skill[10].btn:mapbtntohotkey("G")
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- start interface buttons / menu buttons:
local btnct = #kui.meta.panelbtn -- how many total btns? lets us calc placement.
local custct = #kui.meta.panelbtn-4 -- how many custom (non-upper menu btn conversions)?
fr.panelbtn = {}
for i = 1,btnct do
fr.panelbtn[i] = kui.frame:newbytype("BACKDROP", fr)
fr.panelbtn[i]:setlvl(5)
fr.panelbtn[i]:setsize(kui:px(28), kui:px(28))
if i < 6 then
fr.panelbtn[i]:setabsfp(fp.c, kui:px(1176) + ((i-1)*kui:px(36)), kui:px(60))
else
fr.panelbtn[i]:setabsfp(fp.c, kui:px(1176) + ((i-6)*kui:px(36)), kui:px(26))
end
fr.panelbtn[i]:addbgtex(kui.meta.panelbtn[i].tex)
-- set hover tip to our custom frames only (1-5)
if i <= custct or i == 9 then
fr.panelbtn[i].btn = kui.frame:newbytype("BUTTON", fr)
fr.panelbtn[i].btn:setallfp(fr.panelbtn[i])
fr.panelbtn[i].btn:assigntooltip(kui.meta.panelbtn[i].tipstr)
end
end
-- add alert indicators (new stuff) to target buttons:
fr.alerticon = {}
for i = 1,5 do
fr.alerticon[i] = kui.frame:newbytype("BACKDROP", fr)
fr.alerticon[i]:setsize(kui:px(18), kui:px(15))
fr.alerticon[i]:setbgtex('war3mapImported\\menu-button-alert-green-icon.blp')
fr.alerticon[i]:hide()
end
fr.alerticon[1]:setfp(fp.c, fp.br, fr.panelbtn[1].btn, kui:px(-8), kui:px(8)) -- character page
fr.alerticon[2]:setfp(fp.c, fp.br, fr.panelbtn[3].btn, kui:px(-8), kui:px(8)) -- equipment page
fr.alerticon[3]:setfp(fp.c, fp.br, fr.panelbtn[4].btn, kui:px(-8), kui:px(8)) -- mastery page
fr.alerticon[4]:setfp(fp.c, fp.br, fr.panelbtn[5].btn, kui:px(-8), kui:px(8)) -- spell page
fr.alerticon[5]:setfp(fp.c, fp.br, fr.panelbtn[9].btn, kui:px(-8), kui:px(8)) -- badge page
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- map standard upper menu bar to new btns:
BlzFrameSetVisible(BlzGetFrameByName("UpperButtonBarFrame",0), true)
fr.mt = {
-- order matters below.
[1] = kui.frame:convert("UpperButtonBarQuestsButton", 0),
[2] = kui.frame:convert("UpperButtonBarMenuButton", 0),
[3] = kui.frame:convert("UpperButtonBarAlliesButton", 0),
[4] = kui.frame:convert("UpperButtonBarChatButton", 0),
}
for i = 1,4 do
fr.mt[i]:setparent(fr.fh)
fr.mt[i]:setallfp(fr.panelbtn[i+custct])
fr.mt[i]:setalpha(0)
fr.mt[i]:setlvl(3)
fr.mt[i].hoverfix = kui.frame:newbytype("LISTBOX", fr.panelbtn[i+custct])
fr.mt[i].hoverfix:assigntooltip(kui.meta.panelbtn[i+custct].tipstr)
fr.mt[i].hoverfix:setallfp(fr.panelbtn[i+custct])
fr.mt[i].hoverfix:setlvl(9)
-- hackishly hide the wrapped players button, which we converted to the Badges button:
if i == 3 then
BlzFrameSetVisible(fr.mt[i].fh, false)
BlzFrameSetVisible(fr.mt[i].hoverfix.fh, false)
fr.mt[i]:setabsfp(fp.tl, -0.1, -0.1) fr.mt[i]:setabsfp(fp.tr, -0.1, -0.1) fr.mt[i]:setabsfp(fp.bl, -0.1, -0.1) fr.mt[i]:setabsfp(fp.br, -0.1, -0.1)
fr.mt[i].hoverfix:setabsfp(fp.tl, -0.1, -0.1) fr.mt[i].hoverfix:setabsfp(fp.tr, -0.1, -0.1)
fr.mt[i].hoverfix:setabsfp(fp.bl, -0.1, -0.1) fr.mt[i].hoverfix:setabsfp(fp.br, -0.1, -0.1)
end
end
fr.dec = {}
fr.xpbar = kui.frame:newbysimple("KoboldExperienceBar", fr)
fr.xpbar:setabsfp(fp.c, kui.position.centerx, kui:px(19))
fr.xpbar:setlvl(2)
BlzFrameSetValue(fr.xpbar.fh, 0)
fr.xpbarwrap = kui.frame:newbytype("TEXT", fr) -- tooltip wrapper
fr.xpbarwrap:setallfp(fr.xpbar)
fr.xpbarwrap:assigntooltip("experience")
fr.xpbar:hide()
-- gold statue:
fr.statue = kui.frame:newbytype("BACKDROP", fr)
fr.statue:addbgtex("war3mapImported\\skillbar-gold_statue.blp")
fr.statue:setsize(kui:px(123), kui:px(127))
fr.statue:setabsfp(fp.b, kui.position.centerx, 0.0)
fr.statue:setlvl(2)
-- hp bar:
fr.hpbar = kui.frame:newbysimple("SkillBarHealth", fr)
fr.hpbar:setsize(kui:px(352), kui:px(32))
fr.hpbar:setabsfp(fp.r, kui.position.centerx, kui:px(55))
fr.hpbar:setlvl(2)
fr.hptxt = fr:createbtntext("0/0", nil, true)
fr.hptxt:setallfp(fr.hpbar)
fr.hptxt:assigntooltip("health")
-- mana bar:
fr.manabar = kui.frame:newbysimple("SkillBarMana", fr)
fr.manabar:setsize(kui:px(352), kui:px(32))
fr.manabar:setabsfp(fp.l, kui.position.centerx, kui:px(55))
fr.manabar:setlvl(2)
fr.manatxt = fr:createbtntext("0/0", nil, true)
fr.manatxt:setallfp(fr.manabar)
fr.manatxt:assigntooltip("mana")
-- decorate minimap:
fr.minimapbd = kui.frame:newbytype("BACKDROP", fr)
fr.minimapbd:setallfp(kui.minimap)
fr.minimapbd:addbgtex(kui.color.black)
fr.minimapbd:setalpha(225)
fr.minimapbd:setlvl(0) -- make it below the minimap.
fr.minimapcover = kui.frame:newbytype("BACKDROP", fr)
fr.minimapcover:setfp(fp.tl, fp.tl, kui.minimap, -kui:px(7), kui:px(7))
fr.minimapcover:setsize(kui:px(274), kui:px(261))
fr.minimapcover:addbgtex(kui.tex.minimap)
fr.minimapcover:setlvl(2) -- make it above the minimap.
BlzFrameSetParent(kui.minimap, fr.fh)
-- do additional things on show/hide:
fr.hidefunc = function()
fr.xpbar:hide()
fr.hpbar:hide()
fr.manabar:hide()
fr.skillbg:hide()
end
fr.showfunc = function()
fr.xpbar:show()
fr.hpbar:show()
fr.manabar:show()
fr.skillbg:show()
end
return fr
end
function kui:createoverwriteprompt(parentfr)
local fr = kui.frame:newbytype("BACKDROP", parentfr)
fr:setbgtex("war3mapImported\\panel-interface_shadow_blob.blp")
fr:setsize(kui:px(888), kui:px(888))
fr:setfp(fp.c, fp.c, kui.gameui, 0, 0)
fr.delete = {}
overwrite_char_slot_func = function(fileslot)
local p = utils.trigp()
kui:showmodal(function()
kobold.player[p].char:delete_file(fileslot)
fr.delete[fileslot].txt:settext("<Empty File>")
utils.timed(0.11, function()
kobold.player[p].char.fileslot = fileslot
kobold.player[p].char:save_character()
fr:hide()
end)
end)
end
for fileslot = 1,4 do
fr.delete[fileslot] = kui.frame:newbtntemplate(fr, "war3mapImported\\btn-delete.blp")
fr.delete[fileslot]:setsize(kui:px(179), kui:px(44))
fr.delete[fileslot]:setfp(fp.c, fp.c, fr, kui:px(117), kui:px(-87) + kui:px(fileslot*60))
fr.delete[fileslot].txt = fr.delete[fileslot]:createtext("<Empty File>")
fr.delete[fileslot].txt:setsize(kui:px(270), kui:px(44))
fr.delete[fileslot].txt:setfp(fp.r, fp.l, fr.delete[fileslot], kui:px(-15), kui:px(0))
BlzFrameSetTextAlignment(fr.delete[fileslot].txt.fh, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_RIGHT)
fr.delete[fileslot].btn:addevents(nil, nil, function() overwrite_char_slot_func(fileslot) end)
end
-- close btn:
fr.backbtn = kui.frame:newbtntemplate(fr, kui.tex.backbtn)
fr.backbtn.btn:linkcloseevent(fr)
fr.backbtn:setsize(kui:px(kui.meta.backbtnw), kui:px(kui.meta.backbtnh))
fr.backbtn:setabsfp(fp.c, kui.position.centerx, kui:px(435))
fr.backbtn:assigntooltip("cancel")
fr:hide()
return fr
end
function kui:createmodal(parentfr)
local fr = kui.frame:newbytype("BACKDROP", parentfr)
fr:setbgtex("war3mapImported\\modal_bd.blp")
fr:setsize(kui:px(461), kui:px(230))
fr:setfp(fp.c, fp.c, kui.gameui, 0, 0)
fr.title = fr:createheadertext("Are you sure?")
fr.title:setfp(fp.c, fp.t, fr, 0, -kui:px(79))
fr.title:setsize(kui:px(375), kui:px(35))
fr.yesbtn = kui.frame:newbtntemplate(fr, "war3mapImported\\modal-yes_btn.blp")
fr.yesbtn:setfp(fp.l, fp.b, fr, kui:px(4), kui:px(86))
fr.yesbtn:setsize(kui:px(179), kui:px(44))
fr.yesbtn.btn:addevents(nil, nil, function()
utils.debugfunc(function()
if fr.yesbtn.modalfunc then
fr.yesbtn.modalfunc()
fr.yesbtn.modalfunc = nil
end
utils.playsound(kui.sound.clickstr, utils.trigp())
if kobold.player[utils.trigp()]:islocal() then fr:hide() end
end, "modal yes func")
end)
fr.nobtn = kui.frame:newbtntemplate(fr, "war3mapImported\\modal-no_btn.blp")
fr.nobtn:setfp(fp.r, fp.b, fr, kui:px(-4), kui:px(86))
fr.nobtn:setsize(kui:px(179), kui:px(44))
fr.nobtn.btn:addevents(nil, nil, function()
utils.debugfunc(function()
if fr.nobtn.modalfunc then
fr.nobtn.modalfunc()
fr.nobtn.modalfunc = nil
end
utils.playsound(kui.sound.clickstr, utils.trigp())
if kobold.player[utils.trigp()]:islocal() then fr:hide() end
end, "modal no func")
end)
BlzFrameSetLevel(fr.fh, 8)
fr:hide()
return fr
end
function kui:showmodal(yesfunc, _nofunc)
utils.debugfunc(function()
if kobold.player[utils.trigp()]:islocal() then
kui.canvas.game.modal:show()
end
kui.canvas.game.modal.yesbtn.modalfunc = yesfunc
kui.canvas.game.modal.nobtn.modalfunc = _nofunc or nil
end, "modal functions")
end
function kui:createpartypane(parentfr)
local fr = kui.frame:newbytype("PARENT", parentfr)
fr.pill = {}
return fr
end
function kui:createpartypill(parentfr, pnum, charid)
local w,h = 100,119
local fr = kui.frame:newbytype("BACKDROP", parentfr)
fr:addbgtex(kui.meta.partyframe[charid])
fr:setsize(kui:px(w), kui:px(h))
fr.hpbar = kui.frame:newbysimple("KoboldPartyPillHealthBar", fr)
fr.hpbar:setsize(kui:px(118), kui:px(23))
fr.hpbar:setfp(fp.t, fp.b, fr, kui:px(6), kui:px(8))
fr.hpbar:show()
fr.manabar = kui.frame:newbysimple("KoboldPartyPillManaBar", fr)
fr.manabar:setsize(kui:px(118), kui:px(23))
fr.manabar:setfp(fp.t, fp.b, fr.hpbar, 0.0, kui:px(7))
fr.manabar:show()
fr.downskull= kui.frame:newbytype("BACKDROP", fr)
fr.downskull:setbgtex("war3mapImported\\party_pane-down_skull.blp")
fr.downskull:setsize(kui:px(54), kui:px(54))
fr.downskull:setfp(fp.c, fp.c, fr, kui:px(9), kui:px(1))
fr.downskull:hide()
fr.pname = fr:createbtntext(kobold.player[Player(pnum-1)].name)
fr.pname:setfp(fp.c, fp.t, fr, 0, -kui:px(8))
fr:setfp(fp.tl, fp.tl, kui.worldui, 0, -kui:px((pnum-1)*180 + 22))
return fr
end
function kui:linkskillbarbtns()
-- this must be done after the actual panels are made.
kui.canvas.game.skill.panelbtn[1].btn:linkshowhidebtn(kui.canvas.game.char)
kui.canvas.game.skill.panelbtn[1].btn.alerticon = kui.canvas.game.skill.alerticon[1]
kui.canvas.game.char.alerticon = kui.canvas.game.skill.alerticon[1] -- for hotkey functions.
kui.canvas.game.char.alerticon:setlvl(7)
kui.canvas.game.skill.panelbtn[2].btn:linkshowhidebtn(kui.canvas.game.equip)
kui.canvas.game.skill.panelbtn[3].btn:linkshowhidebtn(kui.canvas.game.inv)
kui.canvas.game.skill.panelbtn[3].btn.alerticon = kui.canvas.game.skill.alerticon[2]
kui.canvas.game.inv.alerticon = kui.canvas.game.skill.alerticon[2]
kui.canvas.game.inv.alerticon:setlvl(7)
kui.canvas.game.skill.panelbtn[4].btn:linkshowhidebtn(kui.canvas.game.mast)
kui.canvas.game.skill.panelbtn[4].btn.alerticon = kui.canvas.game.skill.alerticon[3]
kui.canvas.game.mast.alerticon = kui.canvas.game.skill.alerticon[3]
kui.canvas.game.mast.alerticon:setlvl(7)
kui.canvas.game.skill.panelbtn[5].btn:linkshowhidebtn(kui.canvas.game.abil)
kui.canvas.game.skill.panelbtn[5].btn.alerticon = kui.canvas.game.skill.alerticon[4]
kui.canvas.game.abil.alerticon = kui.canvas.game.skill.alerticon[4]
kui.canvas.game.abil.alerticon:setlvl(7)
kui.canvas.game.skill.panelbtn[6].btn:linkshowhidebtn(kui.canvas.game.dig)
kui.canvas.game.skill.panelbtn[9].btn:linkshowhidebtn(kui.canvas.game.badge)
kui.canvas.game.skill.panelbtn[9].btn.alerticon = kui.canvas.game.skill.alerticon[5]
kui.canvas.game.badge.alerticon = kui.canvas.game.skill.alerticon[5]
kui.canvas.game.badge.alerticon:setlvl(7)
-- hacky item selection fix:
kui.canvas.game.skill.panelbtn[2].btn:addevents(nil, nil, function() -- equipment
if kobold.player[utils.trigp()].selslotid and kobold.player[utils.trigp()].selslotid > 1000 then
kobold.player[utils.trigp()].selslotid = nil
end
end)
kui.canvas.game.skill.panelbtn[3].btn:addevents(nil, nil, function() -- inventory
if kobold.player[utils.trigp()].selslotid and kobold.player[utils.trigp()].selslotid < 1000 then
kobold.player[utils.trigp()].selslotid = nil
end
end)
kui.canvas.game.skill.panelbtn[6].btn:addevents(nil, nil, function() -- dig sites
if kobold.player[utils.trigp()].selslotid and kobold.player[utils.trigp()].selslotid == 1011 then
kobold.player[utils.trigp()].selslotid = nil
end
end)
end
function kui:showgameui(p, _skipmusic)
-- when a char is loaded or created,
-- initialize default frame visibility.
ctm_enabled = true
if utils.localp() == p then
kui.canvas.game:show() -- children inherit visibility.
kui.canvas.game.skill:show()
kui.canvas.game.inv:hide()
kui.canvas.game.equip:hide()
kui.canvas.game.char:hide()
kui.canvas.game.dig:hide()
kui.canvas.game.mast:hide()
kui.canvas.game.abil:hide()
kui.canvas.game.skill.xpbar:show()
kui.canvas.game.skill.hpbar:show()
kui.canvas.game.skill.manabar:show()
kui:showhidepartypills(true)
if quest.inprogress then
quest.parentfr:show()
if quest.minimized then quest.parentfr.minfr:show() else quest.parentfr.maxfr:show() end
end
if not map.manager.activemap then
quest.socialfr:show()
if not freeplay_mode then
UnitAddAbility(kobold.player[p].unit, quest.speakcode)
end
else
quest.socialfr:hide()
if not freeplay_mode then
UnitRemoveAbility(kobold.player[p].unit, quest.speakcode)
end
end
BlzFrameSetVisible(kui.minimap, true)
-- load music:
if not _skipmusic then kui:loadmusictrack(p) end
end
kui.previousframesoundfh = nil
end
function kui:hidegameui(p)
-- when a char is loaded or created,
-- initialize default frame visibility.
suppress_all_panel_sounds = true
ctm_enabled = false
if p == utils.localp() then
kui.canvas.game:hide() -- children inherit visibility.
kui.canvas.game.skill:hide()
kui.canvas.game.inv:hide()
kui.canvas.game.equip:hide()
kui.canvas.game.char:hide()
kui.canvas.game.dig:hide()
kui.canvas.game.mast:hide()
kui.canvas.game.abil:hide()
kui.canvas.game.badge:hide()
kui.canvas.game.skill.xpbar:hide()
kui.canvas.game.skill.hpbar:hide()
kui.canvas.game.skill.manabar:hide()
quest.parentfr:hide()
quest.socialfr:hide()
kui:showhidepartypills(false)
BlzFrameSetVisible(kui.minimap, false)
end
suppress_all_panel_sounds = false
end
function kui:loadmusictrack(p)
-- find relevant track:
if map.manager.activemap and map.manager.cache and map.manage.cache.id == m_type_boss_fight then -- boss active.
StopSound(kui.sound.menumusic, false, true)
utils.playsound(kui.sound.bossmusic, p)
elseif map.manager.activemap then -- normal map.
-- TODO
StopSound(kui.sound.menumusic, false, true)
StopSound(kui.sound.bossmusic, false, true)
else -- in town.
StopSound(kui.sound.bossmusic, false, true)
utils.playsound(kui.sound.menumusic, p)
end
end
function kui:showhidepartypills(bool)
if bool then
kui.canvas.game.party:show()
for i = 1,4 do
if kobold.player[Player(i-1)] and kui.canvas.game.party.pill[i] then
kui.canvas.game.party.pill[i].hpbar:show()
kui.canvas.game.party.pill[i].manabar:show()
end
end
else
kui.canvas.game.party:hide()
for i = 1,4 do
if kobold.player[Player(i-1)] and kui.canvas.game.party.pill[i] then
kui.canvas.game.party.pill[i].hpbar:hide()
kui.canvas.game.party.pill[i].manabar:hide()
end
end
end
end
function kui:closeall()
-- when transitioning to map, close all panels.
-- e.g. after a player chooses a dig site.
suppress_all_panel_sounds = true
if kui.canvas.game.inv:isvisible() then kui.canvas.game.inv:hide() end
if kui.canvas.game.equip:isvisible() then kui.canvas.game.equip:hide() end
if kui.canvas.game.char:isvisible() then kui.canvas.game.char:hide() end
if kui.canvas.game.dig:isvisible() then kui.canvas.game.dig:hide() end
if kui.canvas.game.mast:isvisible() then kui.canvas.game.mast:hide() end
if kui.canvas.game.abil:isvisible() then kui.canvas.game.abil:hide() end
if kui.canvas.game.badge:isvisible() then kui.canvas.game.badge:hide() end
suppress_all_panel_sounds = false
end
-- @texture = set this backdrop texture
-- @parentfr = set parent fr object.
-- @callback = run this function on click.
-- @_advtip = pass in an advtip table.
function kui:createsplashbtn(texture, parentfr, callback, _advtip)
local fr = kui.frame:newbtntemplate(parentfr, texture)
fr:setsize(kui:px(kui.meta.splashbtnw), kui:px(kui.meta.splashbtnh))
-- fr.btn:addhoverhl(fr)
if _advtip then
fr.btn.advtip = _advtip
fr.btn.advtipanchor = fp.br
fr.btn.advattachanchor = fp.l
fr.btn:initializeadvtip()
end
fr.btn:addhoverscale(fr,1.06)
fr.statesnd = true
fr.btn.statesnd = true
return fr
end
-- @str = string text.
-- @scale = [optional] scale the text (e.g. 1.25).
-- @iscentered = [optional] aling center of frame? (bool).
function kui.frame:createtext(str, scale, iscentered)
local fr = kui.frame:newbyname("FrameBaseText", self)
BlzFrameSetText(fr.fh, str)
if scale then BlzFrameSetScale(fr.fh, scale) end
if iscentered then BlzFrameSetTextAlignment(fr.fh, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER) end
fr:setlvl(2)
return fr
end
-- @str = string text.
-- @scale = [optional] scale the text (e.g. 1.25).
function kui.frame:createheadertext(str, scale)
-- this is vert/horiz centered by default
local fr = kui.frame:newbyname("FrameHeaderText", self)
BlzFrameSetText(fr.fh, str)
if scale then BlzFrameSetScale(fr.fh, scale) end
fr:setlvl(4)
return fr
end
-- @str = string text.
function kui.frame:createbtntext(str)
-- this is vert/horiz centered by default
local fr = kui.frame:newbyname("FrameBtnText", self)
BlzFrameSetText(fr.fh, str)
fr:setlvl(4)
return fr
end
-- @str = btn text
function kui.frame:creategluebtn(str, parentfr)
local fr = self:newbyname("ScriptDialogButton", parentfr)
fr:setsize(kui:px(200), kui:px(60))
fr:settext(str)
return fr
end
-- @ownerfr = the owner of the close btn (hide this frame on click).
function kui.frame:createmainframeclosebtn()
self.closebtn = kui.frame:newbyname("FrameCloseBtn", self)
self.closebtn.statesnd = true
self.closebtn:setfp(fp.c, fp.c, self, (self:w()/2)-kui:px(38), (self:h()/2)-kui:px(26))
self.closebtn:addbgtex(kui.tex.closebtn)
self.closebtn:setlvl(6)
self.closebtn:linkcloseevent(self)
self.closebtn:addhoverhl()
self.closebtn:resetalpha()
self.closebtn:assigntooltip("closebtn")
end
-- when this frame is pressed, force playre to press key 'keystr' (e.g. "F")
function kui.frame:mapbtntohotkey(keystr)
self:addevents(nil, nil, function()
if utils.islocaltrigp() then
ForceUIKey(keystr)
end
end)
end
function kui:initializefootsteps(pobj)
pobj.steptmr = NewTimer()
pobj.stepx = 0
pobj.stepy = 0
pobj.prevstepx = utils.unitxy(pobj.unit)
pobj.prevstepy = utils.unitxy(pobj.unit)
pobj.stepcheck = false
TimerStart(pobj.steptmr, 0.33, true, function()
utils.debugfunc(function()
if pobj and pobj.unit then
pobj.prevstepx = pobj.stepx
pobj.prevstepy = pobj.stepy
pobj.stepx = utils.unitxy(pobj.unit)
pobj.stepy = utils.unitxy(pobj.unit)
pobj.stepcheck = false
if pobj.prevstepx == pobj.stepx and pobj.prevstepy == pobj.stepy then
-- not moving, do nothing:
loot.ancient:raiseevent(ancient_event_movement, pobj, false)
elseif not pobj.currentlykb then
-- moving, play a footstep:
loot.ancient:raiseevent(ancient_event_movement, pobj, true)
utils.playsound(kui.sound.footsteps[math.random(#kui.sound.footsteps)], pobj.p)
end
else
ReleaseTimer()
end
end, "movement timer")
end)
end
function kui:hidesplash(p, hidebool)
-- hides intro splash screen and menu for triggering player.
if p == utils.localp() then
if hidebool then
BlzFrameSetVisible(BlzGetFrameByName("SimpleFrameSplash",0),false)
self.canvas.splash:hide()
self.canvas.splash.options:hide()
self.canvas.splash.newmf:hide() -- hide char select for possible splash re-open.
else
BlzFrameSetVisible(BlzGetFrameByName("SimpleFrameSplash",0),true)
self.canvas.splash:show()
self.canvas.splash.options:show()
end
end
end
function kui:hidecmdbtns()
local func = function()
for i = 0,#self.cmdmap do
local fh = BlzGetFrameByName(self.cmdmap[i],0) BlzFrameClearAllPoints(fh) BlzFrameSetAbsPoint(fh, fp.tl, 0.0, -0.25)
end
-- hide annoying invisible cmd btn cover:
BlzFrameSetVisible(BlzFrameGetChild(self.consuleui, 5), false)
end
local unit = utils.unitinrect(gg_rct_expeditionVision, p, 'hfoo')
for p,_ in pairs(kobold.playing) do
SelectUnitForPlayerSingle(unit, p)
-- SyncSelections()
if p == utils.localp() then func() end
end
RemoveUnit(unit)
end
function kui:dataload()
--------------------------------------
-- ui vars
--------------------------------------
self.canvas = {}
self.worldui = BlzGetOriginFrame(ORIGIN_FRAME_WORLD_FRAME, 0)
self.ubertip = BlzGetOriginFrame(ORIGIN_FRAME_UBERTOOLTIP, 0)
self.otip = BlzGetOriginFrame(ORIGIN_FRAME_TOOLTIP, 0)
self.unitmsg = BlzGetOriginFrame(ORIGIN_FRAME_UNIT_MSG,0)
self.chatmsg = BlzGetOriginFrame(ORIGIN_FRAME_CHAT_MSG,0)
self.consolebd = BlzGetFrameByName("ConsoleUIBackdrop",0)
self.consuleui = BlzGetFrameByName("ConsoleUI", 0)
self.minimap = BlzGetFrameByName("MiniMapFrame",0)
self.minimapbar = BlzGetFrameByName("MinimapButtonBar",0)
self.effects = {
char = {
[1] = speffect:new('Abilities\\Spells\\Human\\Thunderclap\\ThunderClapCaster.mdl', 1.0),
[2] = speffect:new('Abilities\\Spells\\Undead\\DarkRitual\\DarkRitualTarget.mdl', 1.0),
[3] = speffect:new('Abilities\\Spells\\Orc\\FeralSpirit\\feralspiritdone.mdl', 1.0),
[4] = speffect:new('Abilities\\Spells\\Other\\Incinerate\\FireLordDeathExplode.mdl', 1.0),
}
}
kui.framestack = {}
--------------------------------------
-- ui assets
--------------------------------------
self.tex = {
-- for hiding parent containers:
invis = "war3mapImported\\misc-invisible-texture.blp",
-- general btns:
closebtn = "war3mapImported\\btn-close_x.blp",
backbtn = "war3mapImported\\menu-backbtn.blp",
-- main splash menu:
splash = "war3mapImported\\menu-intro_splash.blp",
opnew = "war3mapImported\\menu-scroll-option_new.blp",
opload = "war3mapImported\\menu-scroll-option_load.blp",
-- main panels:
panelinv = "war3mapImported\\panel-inventory.blp",
panelchar = "war3mapImported\\panel-character-page.blp",
-- character components:
addatr = "war3mapImported\\btn-add_attr-dis.blp",
addatrgrn = "war3mapImported\\btn-add_attr.blp",
-- inventory components:
invslot = "war3mapImported\\inventory-slot_bd.blp",
-- skill bar ui:
skillbd = "war3mapImported\\skillbar-card_bd.blp", -- skill bar backdrop
cdfill = "war3mapImported\\skillbar-cooldown_overlay.blp",
-- dig site panel:
paneldig = "war3mapImported\\panel-dig_site_map.blp",
digkeybd = "war3mapImported\\digsite-key_insert_btn_dis.blp",
digkeybdact = "war3mapImported\\digsite-key_insert_btn_act.blp",
-- minimap bd:
minimap = "war3mapImported\\minimap-frame.blp",
-- character specific graphics by charid:
char = { -- charid lookup
[1] = { -- tunneler
selectchar = "war3mapImported\\menu-scroll-char_01.blp",
panelequip = "war3mapImported\\panel-equipment-char01.blp",
},
[2] = { -- geomancer
selectchar = "war3mapImported\\menu-scroll-char_02.blp",
panelequip = "war3mapImported\\panel-equipment-char02.blp",
},
[3] = { -- rascal
selectchar = "war3mapImported\\menu-scroll-char_03.blp",
panelequip = "war3mapImported\\panel-equipment-char03.blp",
},
[4] = { -- wickfighter
selectchar = "war3mapImported\\menu-scroll-char_04.blp",
panelequip = "war3mapImported\\panel-equipment-char04.blp",
},
}
}
--------------------------------------
-- positioning and group meta.
--------------------------------------
self.meta = {
tipcharselect = {
[1] = {
[1] = "war3mapImported\\icon-class_01.blp",
[2] = color:wrap(color.tooltip.alert, "Tunneler").."|n".."The Kinetic Sapper",
[3] = "Lord over the tunnels with an array of volatile explosives and experimental Kobold technology."
.."|n|n"..color:wrap(color.tooltip.good, "Select this scroll to begin your adventure as the Tunneler.")
},
[2] = {
[1] = "war3mapImported\\icon-class_02.blp",
[2] = color:wrap(color.tooltip.alert, "Geomancer").."|n".."The Menacing Mage",
[3] = "Conjure powerful spells and golems from the physical, nature, and arcane schools of magic."
.."|n|n"..color:wrap(color.tooltip.good, "Select this scroll to begin your adventure as the Geomancer.")
},
[3] = {
[1] = "war3mapImported\\icon-class_03.blp",
[2] = color:wrap(color.tooltip.alert, "Rascal").."|n".."The Speedy Varmint",
[3] = "Eviscerate your foes with nimble comboes and punishing disable effects."
.."|n|n"..color:wrap(color.tooltip.good, "Select this scroll to begin your adventure as the Rascal.")
},
[4] = {
[1] = "war3mapImported\\icon-class_04.blp",
[2] = color:wrap(color.tooltip.alert, "Wickfighter").."|n".."The Tough Rat",
[3] = "Spew scorching flames and molten wax as you get up close and personal with creatures of the darkness."
.."|n|n"..color:wrap(color.tooltip.good, "Select this scroll to begin your adventure as the Wickfighter.")
},
},
partyframe = {
[1] = "war3mapImported\\party_pane-player_pill_01.blp",
[2] = "war3mapImported\\party_pane-player_pill_02.blp",
[3] = "war3mapImported\\party_pane-player_pill_03.blp",
[4] = "war3mapImported\\party_pane-player_pill_04.blp",
},
oretooltip = { -- for kui UI loops.
[1] = "orearcane",
[2] = "orefrost",
[3] = "orenature",
[4] = "orefire",
[5] = "oreshadow",
[6] = "orephysical",
},
oreicon = { -- for kui UI loops.
[1] = "war3mapImported\\ore-type-icons_01.blp",
[2] = "war3mapImported\\ore-type-icons_02.blp",
[3] = "war3mapImported\\ore-type-icons_03.blp",
[4] = "war3mapImported\\ore-type-icons_04.blp",
[5] = "war3mapImported\\ore-type-icons_05.blp",
[6] = "war3mapImported\\ore-type-icons_06.blp",
},
-- pixel dimensions (width, height, border)
mfw = 475, -- was: 447
mfh = 598, -- was: 564
mfp = 10,
headerw = 620,
headerh = 90,
splashbtnw = 365*0.6,
splashbtnh = 445*0.6,
splashbtny = 0.185,
splashbtnx = 0.71,
menubtnw = 179,
menubtnh = 44,
menubtnp = 30,
backbtnw = 126,
backbtnh = 55,
closebtnw = 53,
closebtnh = 49,
invp = 10,
invg = 50,
invoffy = 40, -- pushes inventory buttons down from top.
digsx = 1062, -- 72.7% of origin size.
digsy = 759, -- 72.7% of origin size.
skillw = 82, -- skill card backdrop dimensions.
skillh = 80, -- skill card backdrop dimensions.
skillp = 6,
skillhotkey = {
[1] = "Q", [2] = "W", [3] = "E", [4] = "R", [5] = "Z", [6] = "X", [7] = "T", [8] = "Y",
},
digcard = { -- cardid lookup
["btnsel"] = { -- selected pointer icon
tex = "war3mapImported\\panel-map_card-selected.blp",
w = 60,
h = 68,
},
["btn"] = { -- begin dig site btn
tex = "war3mapImported\\panel-map_go_btn.blp",
texact = "war3mapImported\\panel-map_go_btn_active.blp",
texhvr = "war3mapImported\\panel-map_go_btn_active_hover.blp",
w = 200,
h = 210,
x = 976,
y = 576,
},
[1] = { -- fossile
tex = "war3mapImported\\panel-map_card_fossile_std.blp",
texhvr = "war3mapImported\\panel-map_card_fossile_hover.blp",
texact = "war3mapImported\\panel-map_card_fossile_hover.blp",
w = 97,
h = 144,
x = 340,
y = 212,
},
[2] = { -- slag
tex = "war3mapImported\\panel-map_card_slag_std.blp",
texhvr = "war3mapImported\\panel-map_card_slag_hover.blp",
texact = "war3mapImported\\panel-map_card_slag_hover.blp",
w = 101,
h = 144,
x = 600,
y = 380,
},
[3] = { -- mire
tex = "war3mapImported\\panel-map_card_mire_std.blp",
texhvr = "war3mapImported\\panel-map_card_mire_hover.blp",
texact = "war3mapImported\\panel-map_card_mire_hover.blp",
w = 90,
h = 144,
x = 690,
y = 158,
},
[4] = { -- glacier
tex = "war3mapImported\\panel-map_card_ice_std.blp",
texhvr = "war3mapImported\\panel-map_card_ice_hover.blp",
texact = "war3mapImported\\panel-map_card_ice_hover.blp",
w = 96,
h = 144,
x = 358,
y = 509,
},
[5] = { -- vault
tex = "war3mapImported\\panel-map_card_vault_std.blp",
texhvr = "war3mapImported\\panel-map_card_vault_hover.blp",
texact = "war3mapImported\\panel-map_card_vault_hover.blp",
w = 103,
h = 152,
x = 108,
y = 170,
},
},
digdiff = { -- dig difficulty bar
-- these need to be layer-ordered (first = in back).
tex = "war3mapImported\\panel-dig_site_map_difficulty_backdrop.blp",
texx = 530,
texy = 130,
texw = 186,
texh = 417,
btnwh = 62, -- btn size
[1] = {
tex = "war3mapImported\\panel-dig_site_difficulty_01.blp",
y = 120,
advtip = "",
name = "Difficulty:|n|cffebce5aGreenwhisker|r",
descript = "You grab your pickaxe with enthusiasm and then trip on a pebble. We'll go easy on you.|n|n"
..color:wrap(color.tooltip.good, "-25%%").." Enemy Toughness|n"
..color:wrap(color.tooltip.good, "-25%%").." Enemy Lethality|n"
..color:wrap(color.tooltip.good, "-25%%").." Enemy Density|n"
..color:wrap(color.tooltip.good, "-25%%").." Enemy Movespeed|n"
..color:wrap(color.tooltip.good, "+50%%").." Wax Efficiency|n"
..color:wrap(color.tooltip.alert, "+0%%").." Treasure Find|n"
..color:wrap(color.tooltip.alert, "+0%%").." Dig Site XP|n|n"
..color:wrap(color.tooltip.good, "Bosses drop common to rare loot"),
},
[2] = {
tex = "war3mapImported\\panel-dig_site_difficulty_02.blp",
y = 56,
name = "Difficulty:|n|cffd1d8ebStandard|r",
descript = "Just another day on the job. You curl your whiskers confidently and secure your candle in place.|n|n"
..color:wrap(color.tooltip.alert, "+0%%").." Enemy Toughness|n"
..color:wrap(color.tooltip.alert, "+0%%").." Enemy Lethality|n"
..color:wrap(color.tooltip.alert, "+0%%").." Enemy Density|n"
..color:wrap(color.tooltip.alert, "+0%%").." Wax Efficiency|n"
..color:wrap(color.tooltip.good, "+10%%").." Treasure Find|n"
..color:wrap(color.tooltip.good, "+25%%").." Dig Site XP|n|n"
..color:wrap(color.tooltip.good, "Bosses drop rare to epic loot").."|n"
..color:wrap(color.tooltip.alert, "Recommended difficulty"),
},
[3] = {
tex = "war3mapImported\\panel-dig_site_difficulty_03.blp",
y = -10,
name = "Difficulty:|n|cff2fceebHeroic|r",
descript = "You have the vision of a Greywhisker basked in the light of a fire, sharing your past heroic deeds.|n|n"
..color:wrap(color.tooltip.bad, "+50%%").." Enemy Toughness|n"
..color:wrap(color.tooltip.bad, "+50%%").." Enemy Lethality|n"
..color:wrap(color.tooltip.bad, "+10%%").." Enemy Density|n"
..color:wrap(color.tooltip.bad, "+50%%").." Elite Lethality & Pack Size|n"
..color:wrap(color.tooltip.bad, "-10%%").." Wax Efficiency|n"
..color:wrap(color.tooltip.good, "+50%%").." Treasure Find|n"
..color:wrap(color.tooltip.good, "+50%%").." Dig Site XP|n|n"
..color:wrap(color.tooltip.good, "Bosses drop rare to ancient loot"),
},
[4] = {
tex = "war3mapImported\\panel-dig_site_difficulty_04.blp",
y = -76,
name = "Difficulty:|n|cffb250d8Vicious|r",
descript = "You are an aspiring underlord with a treasure addiction, but the Darkness is fierce and encroaching. Will you prevail?|n|n"
..color:wrap(color.tooltip.bad, "+125%%").." Enemy Toughness|n"
..color:wrap(color.tooltip.bad, "+125%%").." Enemy Lethality|n"
..color:wrap(color.tooltip.bad, "+30%%").." Enemy Density|n"
..color:wrap(color.tooltip.bad, "+50%%").." Elite Lethality & Pack Size|n"
..color:wrap(color.tooltip.bad, "-20%%").." Wax Efficiency|n"
..color:wrap(color.tooltip.good, "+200%%").." Treasure Find|n"
..color:wrap(color.tooltip.good, "+100%%").." Dig Site XP|n|n"
..color:wrap(color.tooltip.good, "Bosses drop epic to ancient loot").."|n"
..color:wrap(color.tooltip.alert, "Required Level: ")..color:wrap(color.tooltip.good, "20+"),
},
[5] = {
tex = "war3mapImported\\panel-dig_site_difficulty_05.blp",
y = -140,
name = "Difficulty:|n|cffed6d00Tyrannical|r",
descript = "You are Rajah Rat: face the apocalypse and bask your whiskers in its unyielding flames.|n|n"
..color:wrap(color.tooltip.bad, "+???%%").." Enemy Toughness|n"
..color:wrap(color.tooltip.bad, "+???%%").." Enemy Lethality|n"
..color:wrap(color.tooltip.bad, "+???%%").." Enemy Density|n"
..color:wrap(color.tooltip.bad, "+???%%").." Elite Lethality & Pack Size|n"
..color:wrap(color.tooltip.bad, "-???%%").." Wax Efficiency|n"
..color:wrap(color.tooltip.good, "+???%%").." Treasure Find|n"
..color:wrap(color.tooltip.good, "+???%%").." Dig Site XP|n|n"
..color:wrap(color.tooltip.good, "Bosses drop epic to ancient loot").."|n"
..color:wrap(color.tooltip.alert, "Required Level: ")..color:wrap(color.tooltip.good, "30+"),
},
},
inv = {
btnh = 32, -- sizing for item selected icons.
btnw = 32, -- ``
[1] = {
tex = "war3mapImported\\inventory-icon_equip.blp",
tipstr = "invbtnequip",
},
[2] = {
tex = "war3mapImported\\inventory-icon_sell.blp",
tipstr = "invbtnsell",
},
},
panelbtn = {
[1] = {
tex = "war3mapImported\\skillbar-tome-btn.blp",
tipstr = "charbtn",
keystr = "C",
},
[2] = {
tex = "war3mapImported\\skillbar-tools-icon.blp",
tipstr = "equipbtn",
keystr = "V",
},
[3] = {
tex = "war3mapImported\\skillbar-backpack-btn.blp",
tipstr = "invbtn",
keystr = "B",
},
[4] = {
tex = "war3mapImported\\skillbar-mastery-btn.blp",
tipstr = "masterybtn",
keystr = "N",
},
[5] = {
tex = "war3mapImported\\skillbar-tome_yellow-btn.blp",
tipstr = "abilbtn",
keystr = "K",
},
[6] = {
tex = "war3mapImported\\skillbar-map-btn.blp",
tipstr = "digbtn",
keystr = "Tab",
},
[7] = {
tex = "war3mapImported\\skillbar-scroll-btn.blp",
tipstr = "questbtn",
keystr = "F9",
},
[8] = {
tex = "war3mapImported\\skillbar-x-btn.blp",
tipstr = "mainbtn",
keystr = "F10",
},
[9] = {
tex = "war3mapImported\\skillbar-kobold-btn.blp",
tipstr = "badgebtn",
keystr = "J",
},
[10] = {
tex = "war3mapImported\\skillbar-env-btn.blp",
tipstr = "logbtn",
keystr = "F12",
},
},
equipslotw = 55, -- equipment slot invis button width.
abiliconw = 65, -- skill icon size.
abilnudgey = 42, -- push down from top of skill bd.
-- massive scroll panel backdrop:
massivescroll = {
tex = "war3mapImported\\panel-background-huge-scroll.blp",
headerx = -3,
headery = 335,
w = 1436,
h = 651,
},
-- ability panel:
abilpanel = {
btnwh = 86, -- abil circle icon size
btnoffy = -10,
[1] = { -- abil card
tex = "war3mapImported\\panel-ability-ability_backdrop.blp",
x = 147, -- start xy offset from top left
y = -112,
offx = 300, -- move next abil over this much
offy = 160, -- move next row down this much
w = 265,
h = 152,
},
[2] = { -- icon backdrop
tex = "war3mapImported\\panel-ability-abil_circle_icon.blp",
texsel = "war3mapImported\\panel-ability-abil_circle_icon_selected.blp",
},
headers = {
[1] = "Q", [2] = "W", [3] = "E", [4] = "R",
},
},
masterypanel = {
nodex = -599, -- start xy offset from center
nodey = 240,
tex = "war3mapImported\\panel-mastery-huge-scroll.blp",
texstart = "war3mapImported\\panel-mastery-starting_backdrop.blp",
[1] = { -- standard node
tex = "war3mapImported\\panel-mastery-node.blp",
texsel = "war3mapImported\\panel-mastery-node_sel.blp",
},
[2] = { -- ability/power node
tex = "war3mapImported\\panel-mastery-node_abil.blp",
texsel = "war3mapImported\\panel-mastery-node_abil_sel.blp",
},
[3] = { -- runeword variant
tex = "war3mapImported\\mastery_panel-runeword_dis_01.blp",
texact = "war3mapImported\\mastery_panel-runeword_act_01.blp",
},
[4] = { -- runeword variant
tex = "war3mapImported\\mastery_panel-runeword_dis_02.blp",
texact = "war3mapImported\\mastery_panel-runeword_act_02.blp",
},
[5] = { -- runeword variant
tex = "war3mapImported\\mastery_panel-runeword_dis_03.blp",
texact = "war3mapImported\\mastery_panel-runeword_act_03.blp",
},
[6] = { -- runeword variant
tex = "war3mapImported\\mastery_panel-runeword_dis_04.blp",
texact = "war3mapImported\\mastery_panel-runeword_act_04.blp",
},
[7] = { -- runeword variant
tex = "war3mapImported\\mastery_panel-runeword_dis_05.blp",
texact = "war3mapImported\\mastery_panel-runeword_act_05.blp",
},
[8] = { -- runeword variant
tex = "war3mapImported\\mastery_panel-runeword_dis_06.blp",
texact = "war3mapImported\\mastery_panel-runeword_act_06.blp",
},
},
classicon = { -- kobold class icons.
[1] = "war3mapImported\\icon-class_01.blp",
[2] = "war3mapImported\\icon-class_02.blp",
[3] = "war3mapImported\\icon-class_03.blp",
[4] = "war3mapImported\\icon-class_04.blp",
},
-- frame metadata
invmaxx = 6, -- max x slots.
invmaxy = 7, -- max y slots.
-- misc.
stickyscale = 1.25, -- sticky nav height growth.
hoverscale = 1.09, -- button hover eye candy scale.
splashscale = 1.03, -- splash button scale amount.
char01raw = 'H000', -- tunneler.
char02raw = 'H00E', -- geomancer.
char03raw = 'H00G', -- rascal.
char04raw = 'H00F', -- wickfighter.
charname = {
[1] = "Tunneler",
[2] = "Geomancer",
[3] = "Rascal",
[4] = "Wickfighter",
},
rectspawn = gg_rct_charspawn,
}
self.font = {
header = "war3mapImported\\fonts-tarzan.ttf",
tooltip = "war3mapImported\\fonts-consolas.ttf"
}
self.position = {
-- global ui refs
centerx = 0.40,
centery = 0.30,
maxx = 0.80,
maxy = 0.60,
-- panel position values
padding = 0.0083,
}
self.color = {
black = "war3mapImported\\colors_black.blp",
bgbrown = "war3mapImported\\colors_bgbrown.blp",
bgtan = "war3mapImported\\colors_bgtan.blp",
accentgrey = "war3mapImported\\colors_accentgrey.blp",
}
--------------------------------------
-- debug
--------------------------------------
self.debugstr = {}
self.debugstr[FRAMEEVENT_MOUSE_ENTER] = "MOUSE_ENTER"
self.debugstr[FRAMEEVENT_MOUSE_LEAVE] = "MOUSE_LEAVE"
self.debugstr[FRAMEEVENT_MOUSE_UP] = "MOUSE_UP"
self.debugstr[FRAMEEVENT_CONTROL_CLICK] = "MOUSE_UP_CONTROL"
--------------------------------------
-- misc. ui
--------------------------------------
self.cmdmap = { [0] = "CommandButton_0", [1] = "CommandButton_1", [2] = "CommandButton_2",
[3] = "CommandButton_3", [4] = "CommandButton_4", [5] = "CommandButton_5",
[6] = "CommandButton_6", [7] = "CommandButton_7", [8] = "CommandButton_8",
[9] = "CommandButton_9", [10] = "CommandButton_10", [11] = "CommandButton_11",
}
end
kobold = {}
kobold.player = {}
function kobold:init()
self.player:init()
self.lvlcap = 60 -- the current level cap.
self.playing = {}
self.trigger = {}
self.trigger.death = {}
self.trigger.leave = CreateTrigger()
TriggerAddAction(self.trigger.leave, function() kobold.player[utils.trigp()]:leftgame() end)
for pnum = 1,kk.maxplayers do
local p = Player(pnum-1)
if GetPlayerController(p) == MAP_CONTROL_USER and GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING then
kobold.playing[p] = kobold.player:new(p)
end
end
utils.setcambounds(gg_rct_mapinitbounds) -- requires kobold.playing object
end
function kobold.player:init()
total_ids = 0
local idf = function() total_ids = total_ids + 1 return total_ids end
-- be converted into fixed values (for save/load).
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- global ids
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
p_stat_hp = idf() -- int, health value
p_stat_mana = idf() -- int, mana value
p_stat_wax = idf() -- %, bonus wax
p_stat_bms = idf() -- %, bonus movement speed
p_stat_bhp = idf() -- %, bonus health
p_stat_bmana = idf() -- %, bonus mana
-- main attributes
p_stat_strength = idf() -- int
p_stat_wisdom = idf() -- int
p_stat_alacrity = idf() -- int
p_stat_vitality = idf() -- int
-- main attribute via items:
p_stat_strength_b = idf() -- int
p_stat_wisdom_b = idf() -- int
p_stat_alacrity_b = idf() -- int
p_stat_vitality_b = idf() -- int
-- incompetence stats:
p_stat_fear = idf() -- int
p_stat_cowardice = idf() -- int
p_stat_paranoia = idf() -- int
-- damage multipliers:
p_stat_arcane = idf() -- %
p_stat_frost = idf() -- %
p_stat_fire = idf() -- %
p_stat_nature = idf() -- %
p_stat_shadow = idf() -- %
p_stat_phys = idf() -- %
-- damage resistances:
p_stat_arcane_res = idf() -- %
p_stat_frost_res = idf() -- %
p_stat_nature_res = idf() -- %
p_stat_fire_res = idf() -- %
p_stat_shadow_res = idf() -- %
p_stat_phys_res = idf() -- %
-- utility properties:
p_stat_healing = idf() -- %, increased healing.
p_stat_absorb = idf() -- %, increased absorbs.
p_stat_minepwr = idf() -- %, mining power increase (attack damage).
p_stat_minespd = idf() -- %, `` speed.
p_stat_eleproc = idf() -- %, elemental proc chance.
p_stat_thorns = idf() -- int, deal x damage to attackers.
p_stat_thorns_b = idf() -- bonus thorns dmg.
p_stat_shielding = idf() -- int, absorb int damage after using ability.
p_stat_dodge = idf() -- %, chance to dodge non-spell attacks.
p_stat_armor = idf() -- %, reduction of non-spell attacks.
-- secondary properties:
p_stat_treasure = idf() -- %, aka magic find.
p_stat_digxp = idf() -- %, bonus mission xp.
p_stat_mislrange = idf() -- %, max range of missile effects, etc.
p_stat_abilarea = idf() -- %, max radius of area-effect abils.
p_stat_castspeed = idf()
p_stat_manartrn = idf() -- %, mana returned on spellcast.
p_stat_elels = idf() -- %, elemental damage life steal.
p_stat_physls = idf() -- %, physical damage life steal.
p_stat_potionpwr = idf() -- %, increase healing potion effectiveness.
p_stat_artifactpwr = idf() -- %, increase mana potion effectiveness.
p_stat_vendor = idf() -- %, increases gold earned from selling items.
p_stat_dmg_reduct = idf() -- %, pure damage reduction (stacks with ele resists).
-- added damage:
p_stat_dmg_arcane = idf()
p_stat_dmg_frost = idf()
p_stat_dmg_nature = idf()
p_stat_dmg_fire = idf()
p_stat_dmg_shadow = idf()
p_stat_dmg_phys = idf()
-- minion keywords:
p_stat_miniondmg = idf()
-- epic keywords:
p_epic_arcane_mis = idf()
p_epic_frost_mis = idf()
p_epic_nature_mis = idf()
p_epic_fire_mis = idf()
p_epic_shadow_mis = idf()
p_epic_phys_mis = idf()
p_epic_heal_aoe = idf()
p_epic_aoe_stun = idf()
p_epic_hit_crit = idf()
p_epic_arcane_conv = idf()
p_epic_frost_conv = idf()
p_epic_nature_conv = idf()
p_epic_fire_conv = idf()
p_epic_shadow_conv = idf()
p_epic_phys_conv = idf()
p_epic_dmg_reduct = idf()
p_epic_demon = idf()
p_epic_arcane_aoe = idf()
p_epic_frost_aoe = idf()
p_epic_nature_aoe = idf()
p_epic_fire_aoe = idf()
p_epic_shadow_aoe = idf()
p_epic_phys_aoe = idf()
-- potion keywords:
p_potion_life = idf()
p_potion_mana = idf()
p_potion_arcane_dmg = idf()
p_potion_frost_dmg = idf()
p_potion_fire_dmg = idf()
p_potion_nature_dmg = idf()
p_potion_shadow_dmg = idf()
p_potion_phys_dmg = idf()
p_potion_dmgr = idf()
p_potion_fire_res = idf()
p_potion_frost_res = idf()
p_potion_nature_res = idf()
p_potion_shadow_res = idf()
p_potion_arcane_res = idf()
p_potion_phys_res = idf()
p_potion_aoe_stun = idf()
p_potion_aoe_heal = idf()
p_potion_aoe_mana = idf()
p_potion_aoe_slow = idf()
p_potion_absorb = idf()
p_potion_armor = idf()
p_potion_dodge = idf()
p_potion_lifesteal = idf()
p_potion_thorns = idf()
--
idf = nil
-- oretype ids:
ore_arcane = 1
ore_frost = 2
ore_nature = 3
ore_fire = 4
ore_shadow = 5
ore_phys = 6
ore_gold = 7
-- More secondary stat ideas:
-- Spell on Hit - X chance to cast <spell> when dealing damage.
-- (idea: spell could be initialized via ability templates and a damage modifier for its power could
-- be added to the calc formula e.g. do 50% less damage).
-- Spell when Damaged - X chance to cast <spell> when taking damage.
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- power attributes map
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
p_attr_map = {
[1] = p_stat_strength,
[2] = p_stat_wisdom,
[3] = p_stat_alacrity,
[4] = p_stat_vitality,
}
p_attr_b_map = {
[1] = p_stat_strength_b,
[2] = p_stat_wisdom_b,
[3] = p_stat_alacrity_b,
[4] = p_stat_vitality_b,
}
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- lookup tables
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
p_dmg_lookup = { -- added bonus damage lookup table (by dmgtypeid 1-6)
[1] = p_stat_dmg_arcane,
[2] = p_stat_dmg_frost,
[3] = p_stat_dmg_nature,
[4] = p_stat_dmg_fire,
[5] = p_stat_dmg_shadow,
[6] = p_stat_dmg_phys,
}
p_resist_lookup = { -- mirrors p_dmg_lookup but for resistances
[1] = p_stat_arcane_res,
[2] = p_stat_frost_res,
[3] = p_stat_nature_res,
[4] = p_stat_fire_res,
[5] = p_stat_shadow_res,
[6] = p_stat_phys_res,
}
p_conv_lookup = { -- mirrors p_dmg_lookup but for resistances
[p_epic_arcane_conv] = 1,
[p_epic_frost_conv] = 2,
[p_epic_nature_conv] = 3,
[p_epic_fire_conv] = 4,
[p_epic_shadow_conv] = 5,
[p_epic_phys_conv] = 6,
}
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- player data
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
self.basecastspeed = 0.51
self.level = 1
self.nextlvlxp = 100
self.experience = 0
self.hasleft = false
self.defaulthp = 256
self.defaultmana = 128
self.defaultatk = 26
self.defaultatkspd = 1.6
self.defaultms = 300 -- default movespeed
self.unit = nil -- player hero
self.name = "" -- player name
self.attrpoints = 0 -- power attributes available.
self.ancientfrag = 0 -- ancient fragments
self.downed = false
self.reseff = speffect:new("Abilities\\Spells\\Human\\Resurrect\\ResurrectTarget.mdl")
self.lvleff = speffect:new("Abilities\\Spells\\Other\\Levelup\\LevelupCaster.mdl")
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- player mission score data
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
self.score = { -- statid mapping
[1] = 0, -- damage
[2] = 0, -- absorbed
[3] = 0, -- healing
[4] = 0, -- spells used
[5] = 0, -- ore mined
[6] = 0, -- revives
xp = 0, -- xp earned
g = 0, -- gold earned
}
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- character panel details
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- character
self[p_stat_hp] = 512 -- starting base health
self[p_stat_mana] = 252 -- `` mana
-- init empty stat indexes:
for id = 3,total_ids do -- we init hp and mana (1 and 2) with fixed values, so skip them.
self[id] = 0
end
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- currency:
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
self.ore = {}
self.gold = 0
self.ore[ore_arcane] = 0
self.ore[ore_frost] = 0
self.ore[ore_nature] = 0
self.ore[ore_fire] = 0
self.ore[ore_shadow] = 0
self.ore[ore_phys] = 0
self.ore[ore_gold] = 0
--
self.__index = self
end
-- @p = player
function kobold.player:new(p)
kobold.player[p] = {}
setmetatable(kobold.player[p], self)
kobold.player[p].tcache = {}
kobold.player[p].items = {} -- where items are stored.
kobold.player[p].p = p -- player handle.
kobold.player[p].pnum = utils.pnum(p) -- player number.
kobold.player[p].name = string.gsub(GetPlayerName(p), "(#.+)", "")
kobold.player[p].level = 1
kobold.player[p].spell = trg:new("spell", kobold.player[p].p)
kobold.player[p].ore = utils.shallow_copy(self.ore) -- need to override tables since using metatable.
kobold.player[p].score = utils.shallow_copy(self.score) -- ``
kobold.player[p]:regabilscore()
kobold.player[p]:regabilabsorb()
TriggerRegisterPlayerEvent(kobold.trigger.leave, p, EVENT_PLAYER_LEAVE)
-- failed lethal check trigger:
if not kobold.trigger.death[p] then
kobold.trigger.death[p] = trg:new("death", p)
kobold.trigger.death[p]:regaction(function()
if kobold.player[p] and kobold.player[p].unit == utils.trigu() then
ReviveHero(kobold.playing[p].unit, utils.unitx(kobold.playing[p].unit), utils.unity(kobold.playing[p].unit), false)
kobold.playing[p]:down(true)
end
end)
end
kobold.player[p].ancients = loot.ancient:newplayertable() -- store equipped ancient ids.
kobold.player[p].ancientsdata = loot.ancient:newplayertable() -- store any additional data for effects.
kobold.player[p].ancientscd = {} -- store which ids' effects are on cooldown,
-- initialize achievement values:
kobold.player[p].badge = {}
kobold.player[p].badgeclass = {}
for id = 1,18 do
kobold.player[p].badge[id] = 0
end
for id = 1,72 do
kobold.player[p].badgeclass[id] = 0
end
return kobold.player[p]
end
function kobold.player:leftgame()
utils.textall(self.name.." has abandoned the dig!")
self.hasleft = true
RemoveUnit(self.unit)
if kui.canvas.game.party.pill[self.pnum] then
kui.canvas.game.party.pill[self.pnum]:hide()
kui.canvas.game.party.pill[self.pnum].hpbar:hide()
kui.canvas.game.party.pill[self.pnum].manabar:hide()
end
self:cleardata()
if map.manager.activemap then
map.manager.totalp = map.manager.totalp - 1
map.manager:rundefeatcheck()
end
end
function kobold.player:cleardata()
for i,v in pairs(self.items) do
self.items[i] = nil
end
for i,v in pairs(self) do
self[i] = nil
end
for slotid = 1001,1010 do
loot.item:unequip(self.p)
end
loot:clearitems(self.p)
end
function kobold.player:repick()
if not map.manager.activemap and not scoreboard_is_active then
self:cleardata()
end
end
function kobold.player:endcooldowns()
for i,v in pairs(self.ability.spells) do
for _,spell in pairs(v) do
BlzEndUnitAbilityCooldown(self.unit, spell.code)
spell.cdactive = false
end
end
end
function kobold.player:updatecastspeed()
BlzSetUnitRealField(self.unit, UNIT_RF_CAST_POINT, math.max(0.1,self.basecastspeed*(1-self[p_stat_castspeed]/100)))
BlzSetUnitRealField(self.unit, UNIT_RF_CAST_BACK_SWING, math.max(0.1,self.basecastspeed*(1-self[p_stat_castspeed]/100)))
end
function kobold.player:updatemovespeed()
SetUnitMoveSpeed(self.unit, math.floor(self.defaultms*(1+self[p_stat_bms]/100)))
end
function kobold.player:updateattack()
BlzSetUnitBaseDamage(self.unit, math.floor(self.defaultatk*(1+self[p_stat_minepwr]/100)), 0)
end
function kobold.player:updateattackspeed()
BlzSetUnitAttackCooldown(self.unit, math.max(0.33, self.defaultatkspd*(1-self[p_stat_minespd]/100)), 0)
end
function kobold.player:updatehpmana()
-- health/mana increase by 4 per assigned attr point:
utils.setnewunithp(self.unit, (self.defaulthp + (self[p_stat_vitality_b]*4) + (self[p_stat_vitality]*4))*(1 + self[p_stat_bhp]/100))
utils.setnewunitmana(self.unit, (self.defaultmana + (self[p_stat_wisdom_b]*4) + (self[p_stat_wisdom]*4))*(1 + self[p_stat_bmana]/100))
end
function kobold.player:updateallstats()
utils.debugfunc(function()
self:updatecastspeed()
self:updatemovespeed()
self:updateattackspeed()
self:updateattack()
self:updatehpmana()
-- check equip-all badge:
if self.badgeclass[badge:get_class_index(2, self.charid)] == 0 then
local count = 0
for slotid = 1001,1010 do
if self.items[slotid] ~= nil then
count = count + 1
end
end
if count == 10 then
badge:earn(self, 2, self.charid)
end
end
end)
end
function kobold.player:getitem(slotid)
return self.items[slotid]
end
function kobold.player:setlvl(lvl)
self.level = lvl
SetHeroLevel(self.unit, lvl)
self:updatelevel()
end
-- @count = [optional] add this many levels, default: 1
function kobold.player:addlevel(count, _skipeffect, _isloading)
local count = count or 1
for i = 1,count do
local remainder = 0
if self.experience > self.nextlvlxp then -- overflow
remainder = self.experience - self.nextlvlxp
end
SetHeroLevel(self.unit, GetHeroLevel(self.unit) + 1)
if not _isloading then -- don't add points if character is loaded from save file.
self:addattrpoint(5)
self.mastery:addpoint()
end
self.level = self.level + 1
self.experience = 0
self:updatelevel()
self:abilityunlockcheck()
if remainder > 0 then -- recursively apply overflow xp.
self:awardxp(remainder)
end
end
-- show alert eyecandy for player so they know they have earned things:
kui.canvas.game.skill.alerticon[1]:show() -- char page alert
kui.canvas.game.skill.alerticon[3]:show() -- mastery page alert
if not _skipeffect then self.lvleff:playu(self.unit) end
end
function kobold.player:abilityunlockcheck()
local unlock = false
if math.fmod(self.level - 1, 3) == 0 then
if self:islocal() then
for row = 1,3 do
for col = 1,4 do
if kui.canvas.game.abil.slot[row][col].btn.disabled then
kui.canvas.game.abil.slot[row][col].btn:show()
kui.canvas.game.abil.slot[row][col].lock:hide()
kui.canvas.game.abil.slot[row][col].btn.disabled = false
kui.canvas.game.abil.slot[row][col]:setnewalpha(255)
unlock = true
-- automatically learn abilities to fill empty bar:
if self.level < 11 then
self.ability:learn(row, col)
end
break
end
end
if unlock then
kui.canvas.game.skill.alerticon[4]:show() -- show new ability alert eyecandy on menu.
break
end
end
end
end
end
function kobold.player:addattrpoint(val)
-- add power attributes when player levels up
self.attrpoints = self.attrpoints + val
self:updateattrui()
end
function kobold.player:autoapplyattr(_attrid)
-- apply attributes automatically in even distribution (optional @_attrid: apply to only 1).
utils.debugfunc(function()
if self.attrpoints > 0 then -- duplicate click prevention.
for i = 1,self.attrpoints do
if _attr then
self:applyattrpoint(_attrid, true)
else
self:applyattrpoint(p_stat_strength, true)
self:applyattrpoint(p_stat_wisdom, true)
self:applyattrpoint(p_stat_alacrity, true)
self:applyattrpoint(p_stat_vitality, true)
end
end
utils.playsound(kui.sound.selnode, self.p)
else
self.attrpoints = 0
if self:islocal() then kui.canvas.game.char.autoattr:hide() end
end
end)
end
function kobold.player:applyattrpoint(statid, _sndsuppress)
utils.debugfunc(function()
if self.attrpoints > 0 then -- duplicate click prevention.
self.attrpoints = self.attrpoints - 1
self:modstat(statid, true, 1)
if not _sndsuppress then utils.playsound(kui.sound.selnode, self.p) end
self:updateattrui()
else
self.attrpoints = 0
end
end)
end
function kobold.player:modstat(statid, addbool, _val)
-- add or remove stats from a player, checking for special stat operations to run (e.g. attributes):
utils.debugfunc(function()
local val = _val or 1
if addbool then
if statid == p_stat_strength_b or statid == p_stat_strength then
self:modstat(p_stat_armor, true, val)
self:modstat(p_stat_absorb, true, val)
elseif statid == p_stat_wisdom_b or statid == p_stat_wisdom then
ModifyHeroStat(bj_HEROSTAT_INT, self.unit, bj_MODIFYMETHOD_ADD, val)
elseif statid == p_stat_alacrity_b or statid == p_stat_alacrity then
self:modstat(p_stat_minepwr, true, val)
self:modstat(p_stat_dodge, true, val)
elseif statid == p_stat_vitality_b or statid == p_stat_vitality then
ModifyHeroStat(bj_HEROSTAT_STR, self.unit, bj_MODIFYMETHOD_ADD, val)
end
self[statid] = self[statid] + val
else
if statid == p_stat_strength_b or statid == p_stat_strength then
self:modstat(p_stat_armor, false, val)
self:modstat(p_stat_absorb, false, val)
elseif statid == p_stat_wisdom_b or statid == p_stat_wisdom then
ModifyHeroStat(bj_HEROSTAT_INT, self.unit, bj_MODIFYMETHOD_SUB, val)
elseif statid == p_stat_alacrity_b or statid == p_stat_alacrity then
self:modstat(p_stat_minepwr, false, val)
self:modstat(p_stat_dodge, false, val)
elseif statid == p_stat_vitality_b or statid == p_stat_vitality then
ModifyHeroStat(bj_HEROSTAT_STR, self.unit, bj_MODIFYMETHOD_SUB, val)
end
self[statid] = self[statid] - val
end
tooltip:updatecharpage(self.p)
self:updateallstats()
end, "modstat")
end
function kobold.player:calcminiondmg(amount)
return math.floor(amount*(1+self[p_stat_miniondmg]/100))
end
function kobold.player:calcarmor()
-- explained: every level = 1% less effective per active point (500 @ 60 = 40% reduced) (100 @ 20 = 16%)
return math.ceil((self[p_stat_armor]-(self[p_stat_armor]*(self.level*0.01)))/5)
end
function kobold.player:calcdodge()
-- explained: every level = 1.25% less effective per active point (500 @ 60 = 25% chance) (100 @ 20 = 15%)
return math.ceil((self[p_stat_dodge]-(self[p_stat_dodge]*(self.level*0.0125)))/5)*10 -- dodge is 1000-based in the damage engine calculation (thus mult by 10).
end
function kobold.player:calceleproc()
-- return math.ceil((self[p_stat_eleproc]-(self[p_stat_eleproc]*(self.level*0.0133)))/5)*10
return self[p_stat_eleproc]
end
function kobold.player:updateattrui()
-- update points available and toggle add (+) buttons.
if self:islocal() then
if self.attrpoints > 0 then
kui.canvas.game.char.points:settext("Points Available: "..color:wrap(color.tooltip.good,self.attrpoints))
kui.canvas.game.char.pointsbd:show()
kui.canvas.game.char.autoattr:show()
for i = 1,4 do kui.canvas.game.char.attr[i].addbd:setbgtex(kui.tex.addatrgrn) end
else
kui.canvas.game.char.points:settext("Points Available: "..color:wrap(color.txt.txtdisable,"0"))
kui.canvas.game.char.pointsbd:hide()
kui.canvas.game.char.autoattr:hide()
for i = 1,4 do kui.canvas.game.char.attr[i].addbd:setbgtex(kui.tex.addatr) end
end
end
end
function kobold.player:updatelevel()
self:calcnextlvlxp()
if self:islocal() then
kui.canvas.game.char.lvl:settext(color:wrap(color.tooltip.alert, self.level))
end
end
function kobold.player:updatexpbar()
if self:islocal() then
BlzFrameSetValue(kui.canvas.game.skill.xpbar.fh, math.floor(100*self.experience/self.nextlvlxp))
end
end
function kobold.player:resetscore()
if self.score then for i,v in pairs(self.score) do v = nil end else self.score = {} end
end
function kobold.player:regabilscore()
self.spell:regaction(function()
if map.manager.activemap then self.score[5] = self.score[5] + 1 end
end)
end
function kobold.player:regabilabsorb()
-- see if player has absorb on spell stat.
self.spell:regaction(function()
if self[p_stat_shielding] > 0 and not self.shieldingpause then
local amount = self[p_stat_shielding]*(1+self[p_stat_absorb]/100)
self.shieldingpause = true
dmg.absorb:new(utils.trigu(), 6.0, {all = amount}, nil, nil)
utils.timed(3.0, function() self.shieldingpause = nil end)
buffy:new_absorb_indicator(self.p, "Absorb on Cast", "ReplaceableTextures\\CommandButtons\\BTNNeutralManaShield.blp", nil, 6.0,
"Absorbs "..tostring(math.floor(amount)).." damage (stacks)")
end
end)
end
function kobold.player:getlootodds()
local findtotal = self[p_stat_treasure]
-- if active mission, add mission treasure find:
if map.mission.setting and map.mission.setting[m_stat_treasure] then
findtotal = findtotal + map.mission.setting[m_stat_treasure]
end
return (findtotal*100)/4 -- convert to 10,000-based rolls; each point of TF makes rarity rolls 2.5 percent more effective.
end
function kobold.player:awardoretype(oreid, val, _showalert)
-- print(val.." ore awarded for "..oreid)
self.ore[oreid] = self.ore[oreid] + val
if self.ore[oreid] < 0 then self.ore[oreid] = 0 end
self:updateorepane()
shop:updateframes()
if _showalert then
alert:new("You received "..color:wrap(color.tooltip.good, val).." "..tooltip.orename[oreid], 4)
end
end
function kobold.player:updateorepane()
if self:islocal() then
for oreid,val in pairs(self.ore) do
if kui.canvas.game.curr.txt[oreid] then
if val < 0
then val = 0
elseif val == 0 then
kui.canvas.game.curr.txt[oreid]:settext(color:wrap(color.txt.txtdisable, "0"))
else
kui.canvas.game.curr.txt[oreid]:settext(color:wrap(color.tooltip.good, val))
end
end
end
end
end
function kobold.player:awardxp(val)
self.experience = math.floor(self.experience + val)
if self.level < kobold.lvlcap then
if self.experience >= self.nextlvlxp then
utils.debugfunc( function() self:addlevel() end, "levelup" )
self:calcnextlvlxp()
end
else
self.experience = 0
end
self:updatexpbar()
end
function kobold.player:awardgold(val)
self.gold = math.floor(self.gold + val)
if self.gold < 0 then self.gold = 0 end
if self:islocal() then kui.canvas.game.inv.goldtxt:settext(math.floor(self.gold)) end
shop:updateframes()
end
function kobold.player:awardfragment(val, _hidealert)
self.ancientfrag = math.floor(self.ancientfrag + val)
if self.ancientfrag < 0 then self.ancientfrag = 0 end
if self:islocal() then kui.canvas.game.inv.fragtxt:settext(math.floor(self.ancientfrag)) end
shop:updateframes()
if not _hidealert and val > 0 then
if val > 1 then
ArcingTextTag("+"..color:wrap(color.rarity.ancient, "Fragments"), self.unit)
else
ArcingTextTag("+"..color:wrap(color.rarity.ancient, "Fragment"), self.unit)
end
end
end
function kobold.player:calcnextlvlxp()
self.nextlvlxp = math.ceil(100 + (self.level^1.02*12))
end
function kobold.player:down(bool)
SetUnitInvulnerable(self.unit, bool)
ResetUnitAnimation(self.unit)
UnitRemoveBuffs(self.unit, true, true)
PauseUnit(self.unit, bool)
self.downed = bool
if bool then
SetUnitPathing(self.unit, false)
kui.canvas.game.party.pill[self.pnum].downskull:show()
map.manager.downp = map.manager.downp + 1
map.manager:rundefeatcheck()
utils.setlifep(self.unit, 1)
utils.setmanap(self.unit, 0)
SetUnitAnimation(self.unit, "death")
if not self.downtmr then self.downtmr = NewTimer() end
TimerStart(self.downtmr, 0.23, true, function()
utils.setlifep(self.unit, 1)
utils.setmanap(self.unit, 0)
end)
utils.palertall(color:wrap(color.tooltip.alert, self.name).." has been downed!", 3.0, true)
else
SetUnitPathing(self.unit, true)
kui.canvas.game.party.pill[self.pnum].downskull:hide()
map.manager.downp = map.manager.downp - 1
self.reseff:playu(self.unit)
ReleaseTimer(self.downtmr)
utils.setlifep(self.unit, 20)
utils.setmanap(self.unit, 0)
end
end
function kobold.player:fetchcachedloot()
-- retrieve loot that couldn't be transfered due to bag space, etc.
if #self.tcache > 0 then -- prevent duplicate frame clicks.
local flag = false -- prevent error msg spam.
for k,lootfunc in pairs(self.tcache) do
if not loot:isbagfull(self.p) then
lootfunc()
self.tcache[k] = nil
-- utils.palert(self.p, "Success! Previous treasure placed in your inventory.", 2.5, true)
utils.playsound(kui.sound.itemsel, self.p)
else
if not flag then
flag = true
utils.palert(self.p, "Your bag is full! Free up space to accept this reward.")
end
end
end
end
utils.tablecollapse(self.tcache)
if utils.tablelength(self.tcache) == 0 then
if self:islocal() then kui.canvas.game.inv.tcache:hide() end
end
end
function kobold.player:islocal()
return self.p == utils.localp()
end
function kobold.player:freeplaymode()
self:addlevel(44, true)
self:awardgold(100000)
self:awardfragment(1000, true)
for slotid = 1001,1010 do
local item = loot:generate(self.p, self.level, slotid, rarity_rare)
self.selslotid = 1
item:equip(self.p)
end -- how to do if we do multiplayer?
for i = 1,6 do
quest:addoreaward(200, i, false)
end
kui.canvas.game.equip.save:hide()
kui.canvas.game.dig.mappiece:show()
kui.canvas.game.dig.card[5]:show()
for i = 1,5 do
loot:generatedigkey(self.p, 1, i)
end
placeproject('shiny1')
placeproject('shiny2')
placeproject('shiny3')
placeproject('ele1')
placeproject('ele2')
placeproject('ele3')
placeproject('boss1')
placeproject('boss2')
placeproject('boss3')
placeproject('boss4')
placeproject('boss5')
quest_shinykeeper_unlocked = true
quest_shinykeeper_upgrade_1 = true
quest_shinykeeper_upgrade_2 = true
quest_elementalist_unlocked = true
quest_elementalist_upgrade_1 = true
quest_elementalist_upgrade_2 = true
quest.socialfr.shinshop:show()
quest.socialfr.eleshop:show()
quest.socialfr.speakto:hide()
utils.restorestate(self.unit)
quest:disablequests()
UnitRemoveAbility(self.unit, quest.speakcode)
end
-- this class controls what happens for the elemental proc chance effects (p_stat_eleproc)
-- if the proc occurs on the damage type (in the damage engine), run the corresponding damage id effect below.
proliferate = {}
-- arcane: deals 25% bonus damage and slows the target for 3 sec.
proliferate[1] = function(e)
dmg.type.stack[atypelookup[e.atktype]]:pdeal(e.sourcep, e.amount*0.20, e.target)
bf_slow:apply(e.target, 3.0)
end
-- frost: deals 15% of its original damage as bonus AoE damage in a 3m radius, freezing the main target for 1.5 sec.
proliferate[2] = function(e)
spell:gdmgxy(e.sourcep, dmg.type.stack[atypelookup[e.atktype]], e.amount*0.15, utils.unitx(e.target), utils.unity(e.target), 300.0)
bf_freeze:apply(e.target, 1.5)
end
-- nature: deals 25% bonus damage and restores 2.5% of your max health.
proliferate[3] = function(e)
dmg.type.stack[atypelookup[e.atktype]]:pdeal(e.sourcep, e.amount*0.25, e.target)
utils.addlifep(e.source, 2.5, true, e.source)
end
-- fire: creates a pool of fire that damages targets for 50% of its original damage over 3 sec in a 3m radius.
proliferate[4] = function(e)
local d,p,x,y,dtype = e.amount/3, e.sourcep, utils.unitx(e.target), utils.unity(e.target), dmg.type.stack[atypelookup[e.atktype]]
speff_liqfire:play(utils.unitx(e.target), utils.unity(e.target), 3.0, 1.1)
utils.timedrepeat(1.0, 3, function() dmg.overtime = true spell:gdmgxy(p, dtype, d, x, y, 300.0) end,
function() dtype = nil end)
end
-- shadow: summons a demon which attacks for 10% of the original damage for 6 sec.
proliferate[5] = function(e)
if kobold.player[e.sourcep] then
local x,y = utils.projectxy(utils.unitx(e.target), utils.unity(e.target), math.random(0,100), math.random(0,360))
ai:new(utils.unitatxy(e.sourcep, x, y, 'n00J', math.random(0,360)))
:initcompanion(900.0, kobold.player[e.sourcep]:calcminiondmg(e.amount*0.10), p_stat_shadow)
:timed(6.0)
speff_deathpact:play(x, y)
end
end
-- physical: deals 20% bonus damage, if the target is under 20% health, they take 100% bonus damage.
proliferate[6] = function(e)
if utils.getlifep(e.target) < 20 then
dmg.type.stack[atypelookup[e.atktype]]:pdeal(e.sourcep, e.amount*1.0, e.target)
else
dmg.type.stack[atypelookup[e.atktype]]:pdeal(e.sourcep, e.amount*0.20, e.target)
end
end
map = {} -- level data class.
map.block = {} -- placed block class.
map.diff = {} -- difficulty class.
map.mission = {} -- mission (dig objective) class.
map.manager = {} -- class for managing UI -> map load.
map.grid = {} -- map grid management.
map.biome = {} -- store biome metadata.
function map:init()
self.__index = self
map.mission:init()
map.diff:init()
map.manager:init()
map.grid:init()
map.block:init()
-- generate biomes:
self.biome.biomet = {}
self.devmode = false -- speeds up timers etc. when on.
biome_id_foss = 1
biome_id_slag = 2
biome_id_mire = 3
biome_id_ice = 4
biome_id_vault = 5
self.biomemap = { -- to retrieve name by id.
[biome_id_foss] = "Fossil Gorge",
[biome_id_slag] = "The Slag Pit",
[biome_id_mire] = "The Mire",
[biome_id_ice] = "Glacier Cavern",
[biome_id_vault] = "The Vault",
}
self.biomeportal = {
[biome_id_foss] = speff_voidport[6],
[biome_id_slag] = speff_voidport[4],
[biome_id_mire] = speff_voidport[3],
[biome_id_ice] = speff_voidport[2],
[biome_id_vault] = speff_voidport[5],
}
-- id 1 - fossile gorge
biome_foss = map.biome:new(biome_id_foss)
biome_foss.dirt = t_udgrd_rock
biome_foss.terraint = t_desert_g
biome_foss.blockt = { [1] = b_petrified, [2] = b_stone }
biome_foss.oret = { [1] = b_ore_gold, [2] = b_ore_phys, [3] = b_ore_arcane }
-- id 2 - slag pit
biome_slag = map.biome:new(biome_id_slag)
biome_slag.dirt = t_udgrd_rock
biome_slag.terraint = t_slag_g
biome_slag.blockt = { [1] = b_slag, [2] = b_volc }
biome_slag.oret = { [1] = b_ore_gold, [2] = b_ore_fire, [3] = b_ore_shadow }
-- id 3 - the mire
biome_mire = map.biome:new(biome_id_mire)
biome_mire.dirt = t_udgrd_rock
biome_mire.terraint = t_marsh_g
biome_mire.blockt = { [1] = b_shroom, [2] = b_stone }
biome_mire.oret = { [1] = b_ore_gold, [2] = b_ore_nature, [3] = b_ore_arcane }
-- id 4 - glacier cavern
biome_ice = map.biome:new(biome_id_ice)
biome_ice.dirt = t_udgrd_rock
biome_ice.terraint = t_ice_g
biome_ice.blockt = { [1] = b_ice, [2] = b_stone }
biome_ice.oret = { [1] = b_ore_gold, [2] = b_ore_frost, [3] = b_ore_arcane }
-- id 5 - the vault
biome_vault = map.biome:new(biome_id_vault)
biome_vault.dirt = t_udgrd_tile
biome_vault.terraint = t_vault_g
biome_vault.blockt = { [1] = b_stone, [2] = b_ore_gold }
biome_vault.oret = { [1] = b_ore_gold, [2] = b_ore_gold, [3] = b_ore_gold }
end
function map.biome:new(id)
local o = {}
self.__index = self
self.dirt = nil -- excavation marker.
self.blockt = nil -- ipairs, blocks that can be randomized in generation.
o.id = id
self.biomet[#self.biomet + 1] = o
setmetatable(o, self)
return o
end
function map.grid:init()
self.__index = self
self.yoff = 512 -- the map isn't dead-center due to bottom base offset
self.blockw = 256
self.rect = Rect(0,0,0,0)
self.leaverect= Rect(0,0,0,0)
self.digrect = Rect(0,0,0,0)
self.start = { x = 0, y = 0 } -- where players are spawned.
self.coord = {}
self.center = {}
self.center.x, self.center.y = utils.rectxy(gg_rct_enterdigsite)
end
-- @xsize,@ysize = int; grid size in blocks.
-- @iscircle = bool; make it a circle instead of a square.
function map.grid:build(xsize, ysize, iscircle)
local wrapsize = 4 -- bedrock wrapper.
self.maxx = math.floor(xsize/2)
self.maxy = math.floor(ysize/2)
self.minx = -self.maxx
self.miny = -self.maxy
for xb = self.minx,self.maxx do
self.coord[xb] = {}
for yb = self.miny,self.maxy do
self.coord[xb][yb] = {}
end
end
if iscircle then
-- ( x - h )^2 + ( y - k )^2 = r^2
self.r = (-self.minx) - 1 -- remove 1 to be safe
local step = 2*math.pi/360
local h,k = 1/(2*math.pi),self.r/(2*math.pi)
for theta = step, 360, step do
local ptx, pty = math.floor(h + self.r*math.cos(theta)), math.floor(self.r*math.sin(theta))
self.coord[ptx][pty].edge = true
end
else
print('no square yet')
end
utils.debugfunc(function() self:runprocedures() end, "runprocedures")
utils.debugfunc(function() self:wrapbedrock(wrapsize) end, "wrapbedrock")
utils.debugfunc(function() self:setrectbounds(wrapsize) end, "setrectbounds")
utils.debugfunc(function() self:setleavebounds() end, "setleavebounds")
utils.debugfunc(function() self:clearunitsall(self.rect) end, "clearunitsall")
utils.debugfunc(function() utils.setcambounds(self.rect) end, "setcambounds")
-- register out of bounds event:
self.boundstrig = CreateTrigger()
TriggerRegisterLeaveRectSimple(self.boundstrig, self.leaverect, nil)
TriggerAddAction(self.boundstrig, function()
local p = utils.unitp()
if utils.pnum(p) < 5 then -- is player
spell.interrupt[utils.pnum(p)] = true -- interrupt certain spells.
local u = GetTriggerUnit()
local ux,uy = utils.unitx(u),utils.unity(u)
local x,y = utils.projectxy(ux,uy,2248,utils.anglexy(ux,uy,GetRectCenterX(self.leaverect),GetRectCenterY(self.leaverect)))
PauseUnit(u, true)
utils.setxy(u, x, y)
utils.palert(p, "You've exited the dig area! You are disabled for 6 seconds.")
TimerStart(NewTimer(), 6.0, false, function() PauseUnit(u, false) spell.interrupt[utils.pnum(p)] = false end)
elseif p == Player(PLAYER_NEUTRAL_AGGRESSIVE) and not utils.isancient(utils.trigu()) then
RemoveUnit(utils.trigu())
end
end)
map.manager:addprogress(10)
end
function map.grid:setrectbounds(wrapwidth)
-- expand rect 256 x 256 to make absolutely sure we delete everything within it.
SetRect(self.rect,
self:getx(self.minx) - wrapwidth*256 - 256,
self:gety(self.miny) + wrapwidth*256 - 256,
self:getx(self.maxx) + wrapwidth*256 + 256,
self:gety(self.maxy) - wrapwidth*256 + 256)
end
function map.grid:setleavebounds(wrapwidth)
-- expand rect by 128 units to allow a small exit range.
SetRect(self.leaverect,
self:getx(self.minx) - 96,
self:gety(self.miny) - 96,
self:getx(self.maxx) + 96,
self:gety(self.maxy) + 96)
end
function map.grid:purge()
utils.debugfunc(function()
if map_cleanup_func then map_cleanup_func() map_cleanup_func = nil end
elite:cleanup()
self:cleardest(self.rect, true)
self:clearunitsall(self.rect)
for xb = self.minx,self.maxx do
for yb = self.miny,self.maxy do
terrain:paint(self:getx(xb), self:gety(yb), t_udgrd_dirt, 3)
self.coord[xb][yb] = nil
end
self.coord[xb] = nil
end
end, "map.grid:purge")
end
function map.grid:pickstart(r)
if map.mission.cache.id == m_type_monster_hunt then
self.a = math.random(1,360)
local r = math.random(math.floor(r*0.7), math.floor(r*0.8))
local ptx, pty = math.floor(r*math.cos(self.a)), math.floor(r*math.sin(self.a))
self.coord[ptx][pty].start = true
self.start.x = self:getx(ptx)
self.start.y = self:gety(pty)
elseif map.mission.cache.id == m_type_boss_fight then
self.start.x = self.center.x
self.start.y = self.center.y - 1200.0
elseif map.mission.cache.id == m_type_gold_rush then
self.start.x = self.center.x
self.start.y = self.center.y
elseif map.mission.cache.id == m_type_candle_heist then
self.start.x = self.center.x
self.start.y = self.center.y
end
end
function map.grid:clearstart()
self:excavate(self.start.x, self.start.y, 1792, map.manager.biome.dirt)
if map.mission.cache.id ~= m_type_boss_fight then
self:clearunits(self.start.x, self.start.y, 2304)
end
end
function map.grid:excavate(x, y, radius, _terrainid, _clearbedrock)
-- move the dig rect then clear blocks at this spot.
SetRect(self.digrect, x - radius/2, y - radius/2, x + radius/2, y + radius/2)
self:cleardest(self.digrect, _clearbedrock)
if _terrainid then
terrain:paint(x, y, _terrainid, math.floor(radius/256))
end
end
function map.grid:tilepaint(xb, yb, radius, terrainid)
-- paint new tiles at a block location in a radius.
terrain:paint(self:getx(xb), self:getx(yb), terrainid, math.floor(radius/256))
end
function map.grid:cleardest(rect, _bedrockbool)
EnumDestructablesInRect(rect, nil, function()
if _bedrockbool then -- clear every block type.
RemoveDestructable(GetEnumDestructable())
elseif GetDestructableTypeId(GetEnumDestructable()) ~= b_bedrock.code then -- ignore bedrock blocks.
RemoveDestructable(GetEnumDestructable())
end
end)
end
function map.grid:clearunits(x, y, radius, _ignoreobjectives)
SetRect(self.digrect, x - radius/2, y - radius/2, x + radius/2, y + radius/2)
self:clearunitsall(self.digrect, _ignoreobjectives)
end
function map.grid:clearunitsall(rect, _ignoreobjectives)
local grp = g:newbyrect(nil, nil, rect)
if _ignoreobjectives then
grp:action(function()
if GetUnitTypeId(grp.unit) == FourCC("h00J") or GetUnitTypeId(grp.unit) == FourCC("o000") or GetUnitTypeId(grp.unit) == FourCC("n01V")
or GetUnitTypeId(grp.unit) == FourCC("n005") or GetUnitTypeId(grp.unit) == FourCC("ngol") then
grp:remove(grp.unit)
end
end)
end
grp:completepurge()
end
function map.grid:runprocedures()
local funcs = {}
-- pick a start location (*note: must be done before funcs that rely on map.mission.cache):
funcs[#funcs+1] = function() self:pickstart(self.r) end
-- pre-clear any lingering destructables and units not caught in cleanup:
funcs[#funcs+1] = function()
self:clearunits(0, 1000, 27*512)
self:excavate(0, 1000, 27*512, nil, true)
end
-- wrap edge in bedrock:
funcs[#funcs+1] = function()
self:gridloop(function(xb, yb)
if self.coord[xb][yb].edge then
b_bedrock:place(xb,yb)
end
end)
end
-- fill rest of grid with bedrock:
funcs[#funcs+1] = function() -- traverse down
for xb = self.minx,self.maxx,1 do
for yb = self.miny,self.maxy do
if self.coord[xb][yb].edge then
break
else
b_bedrock:place(xb,yb)
end
end
end
end
funcs[#funcs+1] = function() -- traverse up
for xb = self.minx,self.maxx do
for yb = self.maxy,self.miny,-1 do
if self.coord[xb][yb].edge then
break
else
b_bedrock:place(xb,yb)
end
end
end
end
-- decorate with biome-specific terrain:
funcs[#funcs+1] = function()
self:gridloop(function(xb, yb)
if not self.coord[xb][yb].code then
self:tilepaint(xb, yb, 768, self:randomterrain())
end
end)
end
--[[
***************************************************
*************mission-specific generators***********
***************************************************
--]]
-- build mission:
if map.mission.debug then print("running block generator for missionid",map.mission.cache.id) end
-- fill out map with blocks if mission is eligible:
if map.mission.cache.id ~= m_type_boss_fight then
-- fill map with excavation blocks:
funcs[#funcs+1] = function()
self:gridloop(function(xb, yb)
if not self.coord[xb][yb].code then
self:randomblock():place(xb,yb)
end
end)
end
-- run random excavation throughout map:
funcs[#funcs+1] = function() self:randomexcavate() end
-- place random ore deposits on map:
funcs[#funcs+1] = function() self:randomdeposits() end
if map.mission.cache.id ~= m_type_gold_rush then
-- generate random creatures:
funcs[#funcs+1] = function() self:randomcreatures(map.manager.biome.id, self.center.x, self.center.y) end
end
end
-- implement mission-specific components:
if map.mission.cache.id == m_type_gold_rush then
funcs[#funcs+1] = function() self:placegoldrush() end
elseif map.mission.cache.id == m_type_candle_heist then
funcs[#funcs+1] = function() self:placecandleheist() end
elseif map.mission.cache.id == m_type_boss_fight then
-- do nothing, empty arena.
end
-- clear start location of blocks and creatures for non-boss missions:
if map.mission.cache.id ~= m_type_boss_fight and map.mission.cache.id ~= m_type_gold_rush then
funcs[#funcs+1] = function() self:clearstart() end
end
-- place objective elements on the map:
funcs[#funcs+1] = function() map.mission.cache:placeobj() end
-- queue and run procedures:
local i = 0
TimerStart(NewTimer(),0.25,true,function()
utils.debugfunc(function()
i = i + 1
map.manager:addprogress(math.floor(90/#funcs))
funcs[i]()
if i == #funcs then
map.manager:loadend()
for func in pairs(funcs) do func = nil end
funcs = nil
ReleaseTimer()
end
end, "grid funcs[i]() timer")
end)
end
function map.grid:placecandleheist()
self.rushwave = 1
if map.mission.cache.level < 10 then
self.grwavemin, self.grwavemax = 2, 3
elseif map.mission.cache.level < 20 then
self.grwavemin, self.grwavemax = 3, 4
else
self.grwavemin, self.grwavemax = 4, 5
end
self.chpylon = {}
-- place pylons and wax canisters:
local starta,x,y,x2,y2,z = math.random(0,360),0,0,0,0,z
for i = 1,3 do
x,y = utils.projectxy(0, 0, 2675, starta + i*120)
-- clear nearby blocks:
map.grid:excavate(x, y, 512, t_udgrd_tile)
map.grid:clearunits(x, y, 300.0)
-- initialize pylon objects:
self.chpylon[i] = {}
self.chpylon[i].unit = {}
self.chpylon[i].lightn = {}
self.chpylon[i].icon = CreateMinimapIconBJ(x, y, 255, 0, 255, "UI\\Minimap\\MiniMap-ControlPoint.mdl", FOG_OF_WAR_MASKED)
self.chpylon[i].e = speff_waxcan:create(x, y, 2.0)
self.chpylon[i].destroyed = 0
utils.seteffectxy(self.chpylon[i].e, x, y, utils.getcliffz(x, y, 425.0))
for b = 1,3 do
-- init center point and clear blocks/units:
self.chpylon[i].x,self.chpylon[i].y = utils.projectxy(x, y, 384, b*120)
map.grid:excavate(self.chpylon[i].x, self.chpylon[i].y, 512, t_udgrd_tile)
map.grid:clearunits(self.chpylon[i].x, self.chpylon[i].y, 300.0)
-- place pylon and lightning effects:
self.chpylon[i].unit[b] = utils.unitatxy(Player(24), self.chpylon[i].x, self.chpylon[i].y, map.mission.darkkobpylonid, 270.0)
utils.setnewunithp(self.chpylon[i].unit[b], (map.mission.chpylonhp + (((map.mission.cache.level-1)^1.2)*1.2))*(1+map.manager.diffid*0.3), true)
z = utils.getcliffz(self.chpylon[i].x, self.chpylon[i].y, 240.0)
self.chpylon[i].lightn[b] = AddLightningEx("CLPB", false, self.chpylon[i].x, self.chpylon[i].y, z, x, y, utils.getcliffz(x, y, 490.0))
SetLightningColorBJ(self.chpylon[i].lightn[b], 1, 0, 1, 1)
end
end
-- destroy a pylon:
if not self.chtrig then
self.chtrig = trg:new('death', Player(24))
self.chtrig:regaction(function()
if utils.triguid() == FourCC(map.mission.darkkobpylonid) and map.manager.activemap and not map.manager.objiscomplete then
local x,y = utils.unitxy(utils.trigu())
map.grid:spawnattackwave(utils.trigu(), nil, cl_vault_dkobold, 400, 1.21, 600.0)
-- check which unit died and destroy lightning linkage:
if self.chpylon then
for i = 1,3 do
if self.chpylon[i] then
for b = 1,3 do
if self.chpylon[i].unit[b] and utils.trigu() == self.chpylon[i].unit[b] then
DestroyLightning(self.chpylon[i].lightn[b])
self.chpylon[i].destroyed = self.chpylon[i].destroyed + 1
if self.chpylon[i].destroyed == 3 then -- all nearby pylons are dead, recover the wax canister.
self.grwavemin = self.grwavemin + 1
self.grwavemax = self.grwavemax + 1
if map.mission.cache.objdone == 2 then -- before marking progress on 3 completed pylons, do a 15 sec survival timer.
alert:new(color:wrap(color.tooltip.good, "Teleport inbound! Survive for 15 seconds!"), 13.0)
utils.timed(15.0, function()
if map.manager.downp ~= map.manager.totalp then -- players are still alive
map.mission.cache:objstep()
end
end)
else
map.mission.cache:objstep()
end
DestroyEffect(self.chpylon[i].e)
DestroyMinimapIcon(self.chpylon[i].icon)
local x,y,t = 0,0,2
if map.manager.diffid == 5 then t = t + 1 end -- add some more padding for tyrannical.
for i2 = 1, t do
x,y = utils.projectxy(self.chpylon[i].x, self.chpylon[i].y, math.random(50,80), math.random(0,360))
candle:spawnwax(x, y)
end
end
end
end
end
end
end
end
end)
end
map_cleanup_func = function()
self:candleheistcleanup()
end
-- place shrines:
for i = 1,3 do
local a = utils.anglexy(self.chpylon[i].x, self.chpylon[i].y, 0, 0) + math.random(23,65)
local sx,sy = utils.projectxy(self.chpylon[i].x, self.chpylon[i].y, math.random(1300,2000), a)
shrine:placerandom(sx, sy)
end
end
function map.grid:candleheistcleanup()
for i = 1,3 do
if self.chpylon[i].e then DestroyEffect(self.chpylon[i].e) end
if self.chpylon[i].icon then DestroyMinimapIcon(self.chpylon[i].icon) end
for b = 1,3 do
if self.chpylon[i].lightn[b] then DestroyLightning(self.chpylon[i].lightn[b]) end
if self.chpylon[i].unit[b] then RemoveUnit(self.chpylon[i].unit[b]) end
end
self.chpylon[i] = nil
end
self.chpylon = nil
end
function map.grid:placegoldrush()
-- config creature wave size based on level:
if map.mission.cache.level < 10 then
self.grwavemin, self.grwavemax = 1, 3
elseif map.mission.cache.level < 20 then
self.grwavemin, self.grwavemax = 2, 4
else
self.grwavemin, self.grwavemax = 3, 4
end
self.grwavemax = self.grwavemax + ((map.manager.totalp-1)*2) -- if multiplayer, add to wave size
-- place collection point at random angle from center:
local x1,y1,a = self.start.x, self.start.y, math.random(0,360)
map.grid:excavate(x1, y1, 1150, t_udgrd_tile)
map.grid:clearunits(x1, y1, 2100.0)
self.grunit = {}
-- place hall:
local x2,y2 = utils.projectxy(x1, y1, 384, math.random(0-360))
map.grid:excavate(x2, y2, 1150, t_udgrd_tile)
self.grunit[1] = utils.unitatxy(Player(11), x2, y2, map.mission.koboldhallid)
utils.playerloop(function(p) UnitShareVisionBJ(true, self.grunit[1], p) end)
self.gricon = CreateMinimapIconBJ(x2, y2, 255, 255, 0, "UI\\Minimap\\Minimap-QuestObjectiveBonus.mdl", FOG_OF_WAR_MASKED)
-- place mine:
x2, y2 = utils.projectxy(x2, y2, 1024, a - 180.0)
map.grid:excavate(x2, y2, 1150, t_udgrd_tile)
self.grunit[2] = utils.unitatxy(Player(11), x2, y2, map.mission.goldmineid)
utils.playerloop(function(p) UnitShareVisionBJ(true, self.grunit[2], p) end)
-- create defensive towers:
local ta, tx, ty = utils.angleunits(self.grunit[1], self.grunit[2]), utils.unitxy(self.grunit[1])
local tx2, ty2 = utils.projectxy(tx, ty, 384, ta - 67)
local tx3, ty3 = utils.projectxy(tx, ty, 384, ta + 67)
self.grunit[3] = utils.unitatxy(Player(11), tx2, ty2, map.mission.koboldtowerid)
self.grunit[4] = utils.unitatxy(Player(11), tx3, ty3, map.mission.koboldtowerid)
for i = 3,4 do
BlzSetUnitBaseDamage(self.grunit[i], 6 + math.floor(map.mission.cache.level^1.15*0.25*(1+map.manager.diffid*0.2)), 0)
utils.setnewunithp(self.grunit[i], (map.mission.grtowerhp + (((map.mission.cache.level-1)^1.4)*1.60))*(1+map.manager.diffid*0.35), true)
end
-- create workers:
for i = 5,9 do
self.grunit[i] = map.grid:placegoldrushworker(x1, y1)
utils.setnewunithp(self.grunit[i], (map.mission.grworkerhp + (((map.mission.cache.level-1)^1.4)*1.33))*(1+map.manager.diffid*0.25), true)
end
-- tyrannical modifiers:
if map.manager.diffid == 5 then
for i = 3,9 do
utils.setnewunithp(self.grunit[i], math.floor(utils.maxlife(self.grunit[i])*2.0), true)
end
end
-- worker death listener:
if not self.grtrig then
self.grtrig = trg:new('death', Player(11))
self.grtrig:regaction(function()
if utils.triguid() == FourCC(map.mission.koboldworkerid) and map.manager.activemap and not map.manager.objiscomplete then
local x,y = utils.unitxy(self.grunit[1])
utils.timed(18.0, function()
if map.manager.activemap and not scoreboard_is_active and map.mission.cache and map.mission.cache.id == m_type_gold_rush then
if not map.manager.objiscomplete and not map.manager.success then
speff_dustcloud:play(x, y) speff_pothp:play(x, y)
utils.setnewunithp(map.grid:placegoldrushworker(x, y), map.mission.grworkerhp + (((map.mission.cache.level-1)^1.5)*1.5), true)
end
end
end)
end
end)
end
-- reset gold state and queue gold listener timer:
SetPlayerStateBJ(Player(11), PLAYER_STATE_RESOURCE_GOLD, 0)
if self.rushtmr then ReleaseTimer(self.rushtmr) end
self.rushtick = 0
self.rushwave = 1
self.rushtmr = TimerStart(NewTimer(), 1.0, true, function()
utils.debugfunc(function()
if not map.manager.success and not map.manager.objiscomplete and not scoreboard_is_active then
self.rushtick = self.rushtick + 1
self.rushgold = GetPlayerState(Player(11), PLAYER_STATE_RESOURCE_GOLD)
if self.rushgold > 0 then
SetPlayerStateBJ(Player(11), PLAYER_STATE_RESOURCE_GOLD, 0) -- "consume" gold and add it to the objective step.
map.mission.cache:objstep(nil, true)
self.rushgold = 0
end
-- spawn features:
if math.fmod(self.rushtick, 45) == 0 then
candle:spawnwax(utils.unitx(self.grunit[1]), utils.unity(self.grunit[1]))
end
if math.fmod(self.rushtick, 23) == 0 then
map.grid:spawnattackwave(self.grunit[1])
end
if math.fmod(self.rushtick, 48) == 0 then
self.rushwave = self.rushwave + 1
end
else
ReleaseTimer()
end
end, "gold rush timer")
end)
map_cleanup_func = function()
if map.grid.grunit then map.grid.grunit = nil end
if map.grid.rushtmr then ReleaseTimer(map.grid.rushtmr) end
if map.grid.gricon then DestroyMinimapIcon(map.grid.gricon) end
end
-- place shrines:
local a = math.random(0,360)
for i = 1,2 do
local sx,sy = utils.unitprojectxy(self.grunit[1], math.random(1500,1900), a)
shrine:placerandom(sx, sy)
a = a + math.random(160,200)
end
end
-- spawn a creature cluster near @unit and attack move to @unit.
function map.grid:spawnattackwave(unit, _wavecount, _cluster, _spawndist, _delay, _attackradius)
local x2,y2,biomeid = 0,0,map.manager.biome.id
local delay = _delay or 4.0
local radius = _attackradius or 2400
local dist = _spawndist or math.random(900,1000)
local cluster = _cluster or creature.cluster.biome[biomeid][math.random(1, #creature.cluster.biome[biomeid])]
local x,y = utils.projectxy(utils.unitx(unit), utils.unity(unit), dist, math.random(0,360))
map.biomeportal[biomeid]:play(x, y, 10.0, 1.5)
local dmy = buffy:create_dummy(Player(PLAYER_NEUTRAL_PASSIVE), x, y, 'h008', 15.0)
utils.timed(delay, function()
utils.timedrepeat(2.0, _wavecount or self.rushwave, function()
if not map.manager.success and not map.manager.objiscomplete and not scoreboard_is_active then
x2,y2 = utils.projectxy(x, y, math.random(50,200), math.random(0,360))
cluster:spawn(x2, y2, self.grwavemin, self.grwavemax, port_yellowtp)
utils.setrectradius(map.grid.digrect, x2, y2, 512)
utils.dmgdestinrect(map.grid.digrect, 3000)
utils.issueatkmoveall(Player(24), radius, x2, y2, unit)
else
ReleaseTimer()
end
end)
end)
end
function map.grid:placegoldrushworker(x, y)
-- for use with gold rush: place worker and order to harvest.
if self.grunit[2] then -- gold mine exists.
local u = utils.unitatxy(Player(11), x, y, map.mission.koboldworkerid)
IssueTargetOrderById(u, 852018, self.grunit[2]) -- command to gather gold.
return u
end
end
function map.grid:wrapbedrock(width)
-- place bedrock around the outside arena.
for xb = self.minx-width, self.maxx+width do
for i = 1,width do
b_bedrock:place(xb,self.miny - i)
b_bedrock:place(xb,self.maxy + i)
end
end
for yb = self.miny, self.maxy do
for i = 1,width do
b_bedrock:place(self.minx - i,yb)
b_bedrock:place(self.maxx + i,yb)
end
end
end
function map.grid:randomterrain()
self.rand = math.random(1,100)
if self.rand > 50 then
return map.manager.biome.terraint[1]
elseif self.rand > 25 then
return map.manager.biome.terraint[2]
else
return map.manager.biome.terraint[3]
end
end
function map.grid:randomexcavate()
map.grid:gridloop(function(xb, yb)
self.rand = math.random(0, 100)
if self.rand < 18 and self.coord[xb][yb].code ~= b_bedrock.code then
self.rand = math.random(256, 768)
self:excavate(self:getx(xb), self:gety(yb), self.rand)
end
end)
end
function map.grid:randomdeposits()
map.grid:gridloop(function(xb, yb)
self.rand = math.random(0, 100)
-- TODO : IDEA : threshold for ore could be a map stat mod.
if self.rand < math.floor(2*map.mission.cache.setting[m_stat_oredensity]) and self.coord[xb][yb].code ~= b_bedrock.code then
self.rand = math.random(1,100)
self:excavate(self:getx(xb), self:gety(yb), 256)
if self.rand < 24 then
map.manager.biome.oret[1]:place(xb, yb)
elseif self.rand < 62 then
map.manager.biome.oret[2]:place(xb, yb)
elseif self.rand < 100 then
map.manager.biome.oret[3]:place(xb, yb)
end
end
end)
end
function map.grid:randomcreatures(biomeid, centerx, centery)
local x,y,a = 0,0,0
local l = math.ceil(self.r*256/creature.cluster.dist.max/3.14)-- # of radials required to fill arena.
local c = math.ceil(l*3.14*2) -- clusters required to complete a radial.
local rpos = self.r/(l+1)/self.r -- position on radius to generate cluster when traversing circle.
local rinc = rpos
for radial = 1,l do
local cmax = c*radial
local ainc = math.floor(360/cmax)
for _ = 1,cmax do
a = a + math.random(math.floor(ainc*0.9), ainc) -- give some variation with randomness.
x,y = utils.projectxy(centerx, centery, math.random(256*self.r*rpos, 256*self.r*rpos*1.1), a)
creature.cluster.biome[biomeid][math.random(1, #creature.cluster.biome[biomeid])]:spawn(x, y)
end
rpos = rpos + rinc
end
end
function map.grid:gridloop(func, _xdir, _ydir)
self.gridbreak = false
-- initialize default loop directions:
local xdir, ydir = _xdir or 1, _ydir or 1
local minx, maxx, miny, maxy = self.minx, self.maxx, self.miny, self.maxy
-- reverse the loop if params are passed:
if xdir == -1 then minx, maxx = self.maxx, self.minx end
if ydir == -1 then miny, maxy = self.maxy, self.miny end
-- run loop code:
for xb = minx, maxx, xdir do
for yb = miny, maxy, ydir do
func(xb, yb)
if self.gridbreak then break end
end
end
end
function map.grid:randomblock()
self.rand = math.random(1,100)
if self.rand > 20 then
return map.manager.biome.blockt[1]
else
return map.manager.biome.blockt[2]
end
end
function map.grid:debugfill()
for xb = self.minx,self.maxx do
for yb = self.miny,self.maxy do
if not self.coord[xb][yb].edge and not self.coord[xb][yb].code then
AddSpecialEffect('Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl', self:getx(xb), self:gety(yb))
end
end
end
end
--@x,y = convert this grid.coord location to in-game units.
function map.grid:getxy(xb,yb)
return map.grid:getx(xb), map.grid:gety(yb)
end
function map.grid:getx(xb)
return self.center.x + xb*self.blockw
end
function map.grid:gety(yb)
return self.center.y - yb*self.blockw
end
function map.block:init()
self.__index = self
self.blockt = {}
self.vars = 1 -- the number of possible variants.
self.code = nil -- FourCC() id.
self.hp = 50 -- mining damage required (auto attack).
self.lvlmulthp= 0.03 -- hp multiplier based on map level.
self.scale = 1.5
self.invul = false
self.size = map.grid.blockw
self.effect = speffect:new('Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl', 1.66)
-- type constants:
btype_dest = 0
btype_unit = 1
self.btype = btype_dest -- default.
-- generate block types:
b_bedrock = map.block:new('B000', 6, true, btype_dest, 1.80)
b_stone = map.block:new('B001', 6)
b_volc = map.block:new('B007', 1, false, btype_dest, 0.6)
b_slag = map.block:new('B008', 6)
b_ice = map.block:new('B002', 6, false, btype_dest, 0.85)
b_shroom = map.block:new('B004', 8, false, btype_dest, 1.0)
b_petrified = map.block:new('B009', 10, false, btype_dest, 0.94)
-- objective blocks:
b_trove = map.block:new('n005', 1, false, btype_unit)
-- mineral blocks:
b_ore_arcane = map.block:new('h007', 1, false, btype_unit)
b_ore_frost = map.block:new('h004', 1, false, btype_unit)
b_ore_nature = map.block:new('h006', 1, false, btype_unit)
b_ore_fire = map.block:new('h003', 1, false, btype_unit)
b_ore_shadow = map.block:new('h002', 1, false, btype_unit)
b_ore_phys = map.block:new('h005', 1, false, btype_unit)
b_ore_gold = map.block:new('h001', 1, false, btype_unit)
b_ore_arcane.hp = 64
b_ore_fire.hp = 64
b_ore_frost.hp = 64
b_ore_nature.hp = 64
b_ore_shadow.hp = 64
b_ore_phys.hp = 64
b_ore_gold.hp = 64
self.oreitemid = {
[1] = FourCC('I003'),
[2] = FourCC('I004'),
[3] = FourCC('I005'),
[4] = FourCC('I006'),
[5] = FourCC('I007'),
[6] = FourCC('I008'),
[7] = FourCC('I009'),
}
self.itemoreid = {
[FourCC('I003')] = 1,
[FourCC('I004')] = 2,
[FourCC('I005')] = 3,
[FourCC('I006')] = 4,
[FourCC('I007')] = 5,
[FourCC('I008')] = 6,
[FourCC('I009')] = 7,
}
-- create player ore lookup table:
self.oretypet = {} -- look up ore by dmgtypeid/elementid.
self.orepstat = {} -- look up ore by ore block's unit raw code.
self.orepstat[b_ore_arcane.code] = ore_arcane
self.orepstat[b_ore_frost.code] = ore_frost
self.orepstat[b_ore_nature.code] = ore_nature
self.orepstat[b_ore_fire.code] = ore_fire
self.orepstat[b_ore_shadow.code] = ore_shadow
self.orepstat[b_ore_phys.code] = ore_phys
self.orepstat[b_ore_gold.code] = ore_gold
self.oretypet[dmg_arcane] = ore_arcane
self.oretypet[dmg_frost] = ore_frost
self.oretypet[dmg_nature] = ore_nature
self.oretypet[dmg_fire] = ore_fire
self.oretypet[dmg_shadow] = ore_shadow
self.oretypet[dmg_phys] = ore_phys
-- ore gather trigger:
self.oretrig = trg:new("death", utils.passivep()) -- listens for destroyed ore blocks.
self.oretrig:regaction(function()
if utils.pnum(utils.powner(utils.killu())) <= kk.maxplayers and self.orepstat[utils.triguid()] then
local x,y = utils.unitxy(utils.trigu())
local oreid = self.orepstat[utils.triguid()]
local count = 3
if map.mission.cache then
count = math.ceil((1*map.mission.cache.setting[m_stat_treasure] + map.manager.diffid)/2)
end
if oreid ~= ore_gold then
mis_bolt_stack[oreid]:play(x, y)
else
speff_goldburst:play(x, y)
end
map.block:spawnore(count, oreid, x, y)
candle:spawnwax(x, y, "tiny")
end
end)
end
function map.block:collectore()
-- NOTE: this event is fired within the candle wax collection trigger, to reduce trigger clutter.
local id = GetItemTypeId(GetManipulatedItem())
if self.itemoreid[id] then
local oreid = self.itemoreid[id]
if oreid ~= 7 then -- picked up ore.
utils.playerloop(function(p) kobold.player[p]:awardoretype(oreid, 1, false) end)
ArcingTextTag(color:wrap(color.tooltip.good, '+1 ')..tooltip.orename[oreid], utils.trigu(), 1.75)
else -- picked up gold.
utils.playerloop(function(p) kobold.player[p]:awardgold(5*map.manager.diffid) end)
ArcingTextTag(color:wrap(color.tooltip.good, '+10 ')..tooltip.orename[oreid], utils.trigu(), 1.75)
end
kobold.player[utils.unitp()].score[6] = kobold.player[utils.unitp()].score[6] + 1
end
end
-- spawn ore chunks to collect around a point.
function map.block:spawnore(count, oreid, x, y)
local size,tangle,incangle,x2,y2 = nil,math.random(0,360),360/count,0,0
local itm = {}
local dmy = buffy:create_dummy(Player(PLAYER_NEUTRAL_PASSIVE), x, y, 'h008', 3.0)
for i = 1,count do
local mis
x2,y2 = utils.projectxy(x, y, math.random(16,96), tangle + math.randomneg(-10,10))
if oreid == 7 then
mis = missile:create_arc(x2, y2, dmy, 'war3mapImported\\mdl-GoldIngot.mdl', 0)
else
mis = missile:create_arc(x2, y2, dmy, 'war3mapImported\\mdl-BagItem.mdx', 0)
end
mis.time = 0.90
mis.height = 20.0
mis:initduration()
mis.explfunc = function()
if map.manager.activemap then
itm[#itm+1] = CreateItem(self.oreitemid[oreid], mis.x, mis.y)
end
end
tangle = tangle + incangle
end
-- clean up items left behind after a delay:
utils.timed(30.0, function()
if itm then
for _,i in ipairs(itm) do
if i then RemoveItem(i) end
end
end
itm = nil
end)
end
function map.block:new(rawcode, _variants, _isinvul, _btype, _scale)
local o = {}
setmetatable(o, self)
o.code = FourCC(rawcode)
o.btype = _btype or self.btype
o.invul = _isinvul or self.invul
o.vars = _variants or self.vars
o.scale = _scale or self.scale
self.blockt[o.code] = o
return o
end
function map.block:get(rawcode)
return self.blockt[rawcode]
end
-- @xb,yb = coord on the grid to place.
function map.block:place(xb, yb)
if self.btype == btype_dest then
local dest = CreateDestructable(
self.code,
map.grid:getx(xb),
map.grid:gety(yb),
math.floor(math.random(0,360)),
math.random(self.scale*100, self.scale*110)/100,
math.random(1,self.vars) )
if self.invul then
SetDestructableInvulnerable(dest, true)
else
utils.setnewdesthp(dest, self.hp*map.mission.cache.setting[m_stat_blockhp]*(1 + map.mission.cache.level*self.lvlmulthp/2) )
end
if map.grid.coord[xb] and map.grid.coord[xb][yb] then map.grid.coord[xb][yb].code = self.code end
return dest
else
local unit = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), self.code, map.grid:getx(xb), map.grid:gety(yb), 270.0)
utils.setnewunithp(unit, self.hp*map.mission.cache.setting[m_stat_blockhp]*(1 + map.mission.cache.level*self.lvlmulthp), true)
return unit
end
end
-- @x,y = in-game coordinate (*note: does not get recorded in grid)
function map.block:placexy(x, y)
if self.btype == btype_dest then
local dest = CreateDestructable(
self.code,
x,
y,
math.floor(math.random(0,360)),
math.random(self.scale*100, self.scale*133)/100,
math.random(1,self.vars) )
if self.invul then
SetDestructableInvulnerable(dest, true)
else
utils.setnewdesthp(dest, self.hp*map.mission.cache.setting[m_stat_blockhp]*(1 + map.mission.cache.level*self.lvlmulthp) )
end
else
local unit = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), self.code, x, y, 270.0)
utils.setnewunithp(unit, self.hp*map.mission.cache.setting[m_stat_blockhp]*(1 + map.mission.cache.level*self.lvlmulthp), true)
return unit
end
end
function map.manager:init()
-- TODO: how do we handle dig sites with multiple players? hide? show? vote?
-- Idea: the host can initiate a dig, while other players can select a map to
-- place a visual "vote" marker on a map for the host.
self.owner = Player(0) -- the host.
self.totalp = 0 -- total players present.
self.downp = 0 -- total players downed.
self.success = false -- flag for mission completed or failed.
self.debug = false -- overrides loading splash
self.activemap = false -- are players currently in a map?
self.selectedid = nil -- the currently selected map type id.
self.diffid = 2 -- 1 = Greenwhisker, 2 = Standard, 3 = Heroic, 4 = Vicious, 5 = Tyrannical
self.prevdiffid = nil -- the difficulty last completed (for post-mission logic). ``
self.difftimer = false -- only allow difficulty changes every few sec (prevent msg spam).
self.loadstate = 0 -- 0 of 100 loading screen progress.
self.candletmr = NewTimer()-- wax countdown
self.__index = self
end
function map.manager:rundefeatcheck()
-- check if all players are down.
if map.mission.cache then
if self.downp >= self.totalp then
map.mission.cache:objfailed()
if map.mission.cache.boss then
utils.setinvul(map.mission.cache.boss.unit, true)
end
end
end
end
function map.manager:initframes()
-- ran in kui so we have proper context.
self.loadsplash = kui.frame:newbytype("FRAME", kui.worldui) -- for disabling player clicks, etc.
self.loadgraphic = kui.frame:newbysimple("SimpleLoadingSplash", kui.worldui)
self.loadbar = kui.frame:newbysimple("DigSiteLoadBar", kui.worldui)
self.loadbar:setabsfp(fp.c, kui.position.centerx, 0.1)
self.loadbar:setlvl(5)
self.candlebar = kui.frame:newbysimple("CandleLightBar", kui.worldui)
self.candlebar:setabsfp(fp.c, kui.position.centerx, 0.60 - kui:px(66))
self.candletxt = BlzGetFrameByName("CandleLightBarText",0)
self.candlewave = {}
for waveid = 1,3 do self.candlewave[waveid] = kui.frame:convert("CandleLightBarWave"..waveid, 0) end
local sfh = BlzGetFrameByName("SimpleLoadingSplash",0)
-- run locally since client w/h are checked:
utils.looplocalp(function() kui:setfullscreen(self.loadgraphic.fh) end)
utils.looplocalp(function() kui:setfullscreen(sfh) end)
-- wrap up:
self.loadsplash.hidefunc = function() self.loadgraphic:hide() self.loadbar:hide() map.manager:resetloadbar() end
self.loadsplash.showfunc = function() self.loadgraphic:show() self.loadbar:show() map.manager:resetloadbar() end
self.loadsplash:hide()
self.candlebar:hide()
-- build score screen:
self.scoreboard = kui.frame:newbytype("PARENT", kui.gameui)
self.scoreboard.card = {}
self.scoreboard.tmrtxt = self.scoreboard:createheadertext("30")
self.scoreboard.tmrtxt:setabsfp(fp.c, kui.position.centerx, 0.60 - kui:px(66))
-- fail/success header:
self.scoreboard.objstate = self.scoreboard:createheadertext("Mission Success")
self.scoreboard.objstate:setabsfp(fp.c, kui.position.centerx, 0.60 - kui:px(196))
-- continue btn:
self.scoreboard.skipbtn = kui.frame:creategluebtn("Continue", self.scoreboard)
self.scoreboard.skipbtn:setfp(fp.c, fp.c, self.scoreboard.tmrtxt, 0, -kui:px(60))
local skipclick = function() map.manager:scoreboardskip() end
self.scoreboard.skipbtn:addevents(nil, nil, skipclick, nil, false)
local labels = {
[1] = "Damage Done:",
[2] = "Damage Absorbed:",
[3] = "Damage Taken:",
[4] = "Healing Done:",
[5] = "Abilities Used:",
[6] = "Ore Gathered:",
[7] = "Loot Found:"
}
for pnum = 1,kk.maxplayers do -- generate score card templates.
self.scoreboard.card[pnum] = kui.frame:newbytype("BACKDROP", self.scoreboard)
self.scoreboard.card[pnum]:addbgtex("war3mapImported\\scoreboard-card.blp")
self.scoreboard.card[pnum]:setsize(kui:px(316), kui:px(445))
if is_single_player then -- center score card.
self.scoreboard.card[pnum]:setfp(fp.c, fp.c, kui.gameui, 0, -kui:px(280))
else
self.scoreboard.card[pnum]:setfp(fp.c, fp.c, kui.gameui, -kui:px(474 - ((pnum-1)*316)), -kui:px(280))
end
--self.scoreboard.card[pnum]:setabsfp(fp.c, 0.4, 0.24)
-- score bd containers:
self.scoreboard.card[pnum].score = {}
for i = 1,6 do
-- score backdrops:
self.scoreboard.card[pnum].score[i] = kui.frame:newbytype("BACKDROP", self.scoreboard.card[pnum])
self.scoreboard.card[pnum].score[i]:addbgtex(kui.color.black)
self.scoreboard.card[pnum].score[i]:setnewalpha(170)
self.scoreboard.card[pnum].score[i]:setsize(kui:px(245), kui:px(27))
if i > 1 then
self.scoreboard.card[pnum].score[i]:setfp(fp.c, fp.c, self.scoreboard.card[pnum].score[i-1], 0, -kui:px(30))
else
self.scoreboard.card[pnum].score[i]:setfp(fp.c, fp.c, self.scoreboard.card[pnum], 0, kui:px(149))
end
-- add score text:
self.scoreboard.card[pnum].score[i].label = self.scoreboard.card[pnum]:createtext(labels[i])
self.scoreboard.card[pnum].score[i].label:setfp(fp.l, fp.l, self.scoreboard.card[pnum].score[i], kui:px(8), 0)
self.scoreboard.card[pnum].score[i].val = self.scoreboard.card[pnum].score[i]:createtext("0")
self.scoreboard.card[pnum].score[i].val:setfp(fp.r, fp.r, self.scoreboard.card[pnum].score[i], -kui:px(8), 0)
BlzFrameSetTextAlignment(self.scoreboard.card[pnum].score[i].val.fh, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_RIGHT)
end
-- loot bd container:
self.scoreboard.card[pnum].loot = kui.frame:newbytype("BACKDROP", self.scoreboard.card[pnum])
self.scoreboard.card[pnum].loot:addbgtex(kui.color.black)
self.scoreboard.card[pnum].loot:setnewalpha(170)
self.scoreboard.card[pnum].loot:setsize(kui:px(245), kui:px(106))
self.scoreboard.card[pnum].loot:setfp(fp.t, fp.c, self.scoreboard.card[pnum].score[6], 0, -kui:px(17))
-- loot icons:
for i = 1,3 do
self.scoreboard.card[pnum].loot[i] = kui.frame:newbytype("BACKDROP", self.scoreboard.card[pnum].loot)
self.scoreboard.card[pnum].loot[i]:addbgtex("ReplaceableTextures\\CommandButtons\\BTNSelectHeroOn.blp")
self.scoreboard.card[pnum].loot[i]:setsize(kui:px(65), kui:px(65))
if i > 1 then
self.scoreboard.card[pnum].loot[i]:setfp(fp.c, fp.c, self.scoreboard.card[pnum].loot[i-1], kui:px(80), 0)
else
self.scoreboard.card[pnum].loot[i]:setfp(fp.c, fp.c, self.scoreboard.card[pnum], -kui:px(80), -kui:px(80))
end
self.scoreboard.card[pnum].loot[i].rarity = kui.frame:newbytype("BACKDROP", self.scoreboard.card[pnum].loot[i])
self.scoreboard.card[pnum].loot[i].rarity:addbgtex("war3mapImported\\rarity-gem-icons_01.blp")
self.scoreboard.card[pnum].loot[i].rarity:setfp(fp.c, fp.b, self.scoreboard.card[pnum].loot[i], 0.0, 0.0011)
self.scoreboard.card[pnum].loot[i].rarity:setsize(0.0125, 0.0125)
end
-- loot txt:
self.scoreboard.card[pnum].lootlab = self.scoreboard.card[pnum]:createtext(labels[7])
self.scoreboard.card[pnum].lootlab:setfp(fp.l, fp.tl, self.scoreboard.card[pnum].loot, kui:px(8), -kui:px(14))
-- XP and gold text:
self.scoreboard.card[pnum].goldtxt = self.scoreboard.card[pnum]:createbtntext("Gold Payment: 0")
self.scoreboard.card[pnum].goldtxt:setfp(fp.c, fp.c, self.scoreboard.card[pnum], 0, -kui:px(151))
self.scoreboard.card[pnum].xptxt = self.scoreboard.card[pnum]:createbtntext("XP Earned: 0")
self.scoreboard.card[pnum].xptxt:setfp(fp.c, fp.c, self.scoreboard.card[pnum], 0, -kui:px(184))
-- class icon:
self.scoreboard.card[pnum].class = kui.frame:newbytype("BACKDROP", self.scoreboard.card[pnum])
self.scoreboard.card[pnum].class:addbgtex("ReplaceableTextures\\CommandButtons\\BTNSelectHeroOn.blp")
self.scoreboard.card[pnum].class:setsize(kui:px(34), kui:px(34))
self.scoreboard.card[pnum].class:setfp(fp.c, fp.c, self.scoreboard.card[pnum], 0, kui:px(223+23))
-- player name:
self.scoreboard.card[pnum].pname = self.scoreboard.card[pnum]:createbtntext("PlayerName")
self.scoreboard.card[pnum].pname:setfp(fp.c, fp.c, self.scoreboard.card[pnum], 0, kui:px(205))
end
labels = nil
self.scoreboard:hide()
end
function map.manager:scoreboardstart()
DestroyTrigger(map.grid.boundstrig)
scoreboard_is_active = true
map.grid.boundstrig = nil
DisableTrigger(kk.boundstrig)
utils.disableuicontrol(true)
utils.setcambounds(gg_rct_scorebounds)
if self.success then
badge:earn(kobold.player[Player(0)], 1)
map.manager.scoreboard.objstate:settext(color:wrap(color.tooltip.good,"Dig Site Complete"))
if map.mission.cache.id ~= m_type_boss_fight then
-- prevent back to back victory sounds.
utils.playsoundall(kui.sound.completebig)
end
else
map.manager.scoreboard.objstate:settext(color:wrap(color.tooltip.bad,"Dig Site Failed"))
utils.playsoundall(kui.sound.failure)
end
self.candlebar:hide()
self.scoreboard.skipbtn:hide() -- show after delay finishes.
self.scoreboard.tmrtxt:hide() -- ``
self.sbskip = false
self.sbactive = true
sb_tmr = {}
sb_dur = 60 -- screen countdown timer.
local x,y = map.mission.score.x,map.mission.score.y
local tpnum = 1 -- move kobolds timer pnum.
-- run the scoreboard screen:
map.manager.scoreboard.tmrtxt:settext(tostring(sb_dur))
utils.playerloop(function(p) kui:hidegameui(p) end)
for pnum = 1,kk.maxplayers do
local p = Player(pnum-1)
sb_tmr[p] = NewTimer()
map.manager.scoreboard.card[pnum]:hide() -- initialize hidden cards
if kobold.player[p] then
utils.debugfunc( function()
utils.face(kobold.player[p].unit, 270.0)
PauseUnit(kobold.player[p].unit, true)
PauseTimer(kobold.player[p].permtimer)
TimerStart(sb_tmr[p], 0.03, true, function() CameraSetupApplyForPlayer(true, gg_cam_scorecam, p, 0.23) end)
end, "1")
end
end
local dur = 2.5 -- timer duration
if self.devmode then dur = 0.03 end
utils.timed(dur, function()
-- move kobolds into frame:
local dur = 1.0 -- timer duration
if self.devmode then dur = 0.03 end
utils.timedrepeat(dur, kk.maxplayers, function()
utils.debugfunc( function()
local p = Player(tpnum-1)
local dur = 0.66 -- timer duration
if self.devmode then dur = 0.03 end
if kobold.player[p] then
self:scoreupdate(kobold.player[p])
map.manager.scoreboard:show()
map.manager.scoreboard.card[tpnum]:show()
if is_single_player then
utils.setxy(kobold.player[p].unit, 10750, -18450)
-- utils.issmovexy(kobold.player[p].unit, 10750, -18600)
else
utils.setxy(kobold.player[p].unit, x + (tpnum-1)*map.mission.score.d, y)
-- utils.issmovexy(kobold.player[p].unit, x + (tpnum-1)*map.mission.score.d, y - map.mission.score.walkd)
end
utils.playsoundall(kui.sound.scorebcard)
kui.effects.char[kobold.player[p].charid]:playu(kobold.player[p].unit)
if self.runlvleff == true then -- show level up eye candy
self.runlvleff = false
TimerStart(NewTimer(), dur, false, function()
kobold.player[p].lvleff:playu(kobold.player[p].unit)
self.scoreboard.card[tpnum].pname:settext(kobold.player[p].name.." ("
..color:wrap(color.tooltip.alert, kobold.player[p].level)..")")
end)
end
end
tpnum = tpnum + 1
end, "2")
end)
if self.devmode then dur = 0.15 end
-- show skip option after cards show:
utils.timed(dur, function()
self.scoreboard.skipbtn:show()
self.scoreboard.tmrtxt:show()
utils.disableuicontrol(false)
-- start global timer:
utils.timedrepeat(1.0, sb_dur + 1, function()
sb_dur = sb_dur - 1
map.manager.scoreboard.tmrtxt:settext(tostring(sb_dur))
if sb_dur == 0 or not self.sbactive then
map.manager:scoreboardend()
sb_dur = nil
ReleaseTimer()
end
end)
end)
end)
map.grid:purge()
end
function map.manager:scoreboardskip()
-- a player clicks "continue" button.
if not self.sbskip then -- prevent dup click events.
self.sbskip = true
if sb_dur > 5 and not self.devmode then
sb_dur = 3
self.scoreboard.tmrtxt:settext(tostring(sb_dur))
self.scoreboard.skipbtn:hide()
else
self:scoreboardend()
end
end
end
function map.manager:scoreboardend()
utils.debugfunc(function()
map.manager.selectedid = nil
scoreboard_is_active = false
self.scoreboard:hide()
self.sbactive = false
map.manager:exit()
utils.playerloop(function(p)
ReleaseTimer(sb_tmr[p])
PauseUnit(kobold.player[p].unit, false)
-- resume sticky camera:
CameraSetupApplyForPlayer(false, gg_cam_gameCameraTemplate, p, 0)
TimerStart(kobold.player[p].permtimer, 0.03, true, GetTimerData(kobold.player[p].permtimer))
kui:showgameui(p)
self:scorepurge(kobold.player[p])
if kobold.player[p].downed then kobold.player[p]:down(false) end
utils.restorestate(kobold.player[p].unit)
SetUnitInvulnerable(kobold.player[p].unit, false)
end)
-- store previous mission details if needed after:
map.manager.prevdiffid = map.manager.diffid
map.manager.prevbiomeid = map.manager.biome.id
-- **************
-- BOSS UPDATES/LOOT:
-- **************
-- general boss defeat:
if map.mission.cache and map.mission.cache.id == m_type_boss_fight and map.manager.success then
boss:awardbosschest(map.manager.diffid)
-- consume dig key after use:
kui.canvas.game.dig.pane:hide()
utils.playerloop(function(p)
if kobold.player[p].items[slot_digkey] then
kobold.player[p].items[slot_digkey] = nil
if utils.islocalp(p) then
kui.canvas.game.dig.space[slot_digkey].icon:setbgtex(kui.tex.invis)
kui.canvas.game.dig.digkeyglow:hide()
kui.canvas.game.dig.boss:hide()
end
end
end)
end
-- **************
-- QUEST UPDATES:
-- **************
if map.manager.success and quest.current and quest.current.triggered then
if map.mission.cache.id == quest.current.qst_m_type and quest.current.qst_m_type ~= m_type_boss_fight and map.manager.biome.id == quest.current.biomeid then
-- complete a normal dig:
quest.current:markprogress(true)
elseif quest.current.qst_m_type == m_type_boss_fight and map.manager.biome.id == quest.current.biomeid then
-- complete boss fight quest:
if quest.current.bossid and map.mission.cache.boss and quest.current.bossid == map.mission.cache.bossid then
quest.current:markprogress(true)
end
end
end
-- reset quest markers so they don't bug out:
if quest.current then quest.current:refreshmapmarker() end
-- **************
-- **************
-- **************
if map.mission.cache then
map.mission.cache:cleanup()
map.mission.cache = nil
end
shrine:cleanup()
end, "3")
end
function map.manager:scoreupdate(pobj)
local pnum = utils.pnum(pobj.p)
-- update visuals/stats for a player:
self.scoreboard.card[pnum].class:setbgtex(kui.meta.classicon[pobj.charid])
self.scoreboard.card[pnum].pname:settext(pobj.name.." ("..color:wrap(color.tooltip.alert, pobj.level)..")")
if self.success then -- only run on mission success.
self:calcearnings(pobj)
else
pobj.score.g = 0
pobj.score.xp = 0
end
self.scoreboard.card[pnum].goldtxt:settext( "Gold: "..color:wrap(color.ui.gold, pobj.score.g) )
if pobj.score.xp == "MAX LEVEL" then -- display alternative text.
self.scoreboard.card[pnum].xptxt:settext(color:wrap(color.ui.xp, pobj.score.xp))
else
self.scoreboard.card[pnum].xptxt:settext( "XP: "..color:wrap(color.ui.xp, pobj.score.xp) )
end
for statid = 1,6 do
self.scoreboard.card[pnum].score[statid].val:settext(math.floor(math.abs(pobj.score[statid])))
end
end
function map.manager:scorepurge(pobj)
-- reset a player's score card details.
for i = 1,6 do pobj.score[i] = 0 end
end
function map.manager:calcearnings(pobj)
-- determine dig site XP and gold earned from mission.
local prevlvl, baseg, bonusg, minedg, basexp, bonusxp = pobj.level, 0, 0, 0, 0, 0
-- award xp:
if pobj.level < 60 then
if freeplay_mode then
basexp = math.floor( 400 + 25*pobj.level )
else
basexp = math.floor( 125 + 8*pobj.level )
end
bonusxp = math.floor( basexp*(map.mission.cache.setting[m_stat_xp] + pobj[p_stat_digxp]/100) - basexp )
if bonusxp > 0 then
pobj.score.xp = basexp..color:wrap(color.tooltip.good," (+"..tostring(bonusxp)..")")
elseif bonusxp == 0 then
pobj.score.xp = basexp + bonusxp
else
bonusxp = 0
end
-- for boss fights, double the XP earned:
if map.mission.cache.id == m_type_boss_fight then
basexp = basexp*2
end
pobj:awardxp(basexp + bonusxp)
else
pobj.score.xp = "MAX LEVEL"
end
-- award gold:
baseg = math.floor( 100 + 8*pobj.level )
bonusg = math.floor( baseg*(map.mission.cache.setting[m_stat_treasure] + pobj[p_stat_treasure]/100) - baseg )
-- convert gold mined to currency:
if pobj.ore[ore_gold] > 0 then
minedg = math.floor( 5*pobj.ore[ore_gold]*(1+map.mission.cache.level*map.mission.goldmult) - baseg )
if minedg < 0 then minedg = 0 end
pobj.ore[ore_gold] = 0
end
if bonusg > 0 then
pobj.score.g = baseg..color:wrap(color.tooltip.good," (+"..tostring(bonusg + minedg)..")")
elseif bonusg == 0 then
pobj.score.g = baseg + bonusg + minedg
else
bonusg = 0
end
pobj:awardgold(baseg + bonusg + minedg)
-- queue level eye candy if level earned:
if prevlvl < pobj.level then self.runlvleff = true end -- queue level up eye candy flag.
end
function map.manager:resetloadbar()
self.loadstate = 0
BlzFrameSetValue( self.loadbar.fh, 0 )
end
-- @val = increment toward 100
function map.manager:addprogress(val)
if self.loading then
self.loadstate = self.loadstate + val
if self.loadstate > 100 then self.loadstate = 100 end
BlzFrameSetValue( self.loadbar.fh, math.ceil(self.loadstate) )
end
end
-- @id = level id (1-x)
function map.manager:select(id)
self.selectedid = id
end
function map.manager:run()
DisableTrigger(kk.boundstrig)
self.biome = map.biome.biomet[map.manager.selectedid]
self.activemap = true
self.success = false
self.objiscomplete = false
if not map.manager.debug then
map.manager:loadstart()
else -- if debug, skip loading sequence
map.manager:loadend()
end
-- see if dig key should generate a boss:
if kobold.player[utils.trigp()].items[slot_digkey] and
kobold.player[utils.trigp()].items[slot_digkey].biomeid and kobold.player[utils.trigp()].items[slot_digkey].biomeid == map.manager.selectedid then
-- **************
-- ****BOSSES****
-- **************
local bossid = kobold.player[utils.trigp()].items[slot_digkey].bossid
map.mission:build(m_type_boss_fight)
map_manager_load_boss = function()
-- init mission and boss unit:
map.mission.cache.boss = boss:new(utils.unitatxy(Player(24), 0, 1000, bossid, 270.0), bossid)
map.mission.cache.bossid = bossid
map.mission.cache.boss:run_intro_scene()
end
alert:new(color:wrap(color.tooltip.alert, "Dig key activated!"), 2.5)
-- **************
-- **************
-- **************
-- see if a quest will override the mission for this biome:
elseif quest.current and quest.current.triggered and map.manager.selectedid == quest.current.biomeid and quest.current.qst_m_type and quest.current.qst_m_type ~= 0
and quest.current.qst_m_type ~= m_type_boss_fight then
map.mission:build(quest.current.qst_m_type)
else
map.mission:build()
end
utils.debugfunc(function()
-- build arena size based on chosen mission:
if map.mission.cache.id == m_type_monster_hunt then
map.grid:build(45, 45, true)
elseif map.mission.cache.id == m_type_gold_rush then
map.grid:build(24, 24, true)
elseif map.mission.cache.id == m_type_candle_heist then
map.grid:build(34, 34, true)
elseif map.mission.cache.id == m_type_boss_fight then
map.grid:build(22, 22, true)
end
end, "map.grid:build")
end
function map.manager:loadstart()
-- show loading splash, disable players.
self.loading = true
self.totalp = 0
self.prevdiffid = nil
self.prevbiomeid = nil
candle:init()
utils.fadeblack(true)
BlzChangeMinimapTerrainTex('war3mapImported\\minimap-dig.blp')
for pnum = 1,kk.maxplayers do
local p = Player(pnum-1)
if kobold.player[p] then
kobold.player[p]:resetscore()
if kobold.player[p].downed then kobold.player[p]:down(false) end
self.totalp = self.totalp + 1
PauseUnit(kobold.player[p].unit, true)
utils.playsound(kui.sound.digstart)
if p == utils.localp() then
kui:closeall()
kui:hidegameui(p)
self.loadsplash:show()
kui.canvas.game.bosschest:hide()
kui.canvas.game.equip.save:hide()
kui.canvas.game.modal:hide()
end
end
end
self.downp = 0 -- keep after down check.
end
function map.manager:loadend()
-- remove loading splash, initialized player entry.
self.loading = false
utils.fadeblack(false, 1.33)
utils.moveallpxy(map.grid.start.x, map.grid.start.y)
candle:load()
for pnum = 1,kk.maxplayers do
local p = Player(pnum-1)
if kobold.player[p] then
PauseUnit(kobold.player[p].unit, false)
utils.setinvul(kobold.player[p].unit, false)
utils.pantounit(p, kobold.player[p].unit, 0.0)
utils.playsound(kui.sound.loadend, p)
utils.restorestate(kobold.player[p].unit)
-- ui elements:
if p == utils.localp() then
self.loadsplash:hide()
BlzFrameSetValue(self.candlebar.fh, 100) -- prevent initial show jankiness.
if map.mission.cache.id ~= m_type_boss_fight then
self.candlebar:show()
self.candlebar:setfp(fp.c, fp.t, kui.worldui, 0, -kui:px(76))
else -- move candle bar down if boss bar is present:
self.candlebar:setfp(fp.c, fp.t, kui.worldui, 0, -kui:px(142))
end
if not map_manager_load_boss then kui:showgameui(p) end
utils.looplocalp(ClearTextMessages)
end
-- if boss should initialize:
if map_manager_load_boss then map_manager_load_boss() map_manager_load_boss = nil end
end
end
-- play any music:
if map.mission.cache.id == m_type_boss_fight then
utils.playsoundall(kui.sound.bossmusic)
end
-- show objective intro text:
alert:new(map.mission.missionintros[map.mission.cache.id], 6.0)
-- begin burning candle:
TimerStart(self.candletmr, candle.cadence*map.mission.cache.setting[m_stat_waxrate], true, function()
utils.debugfunc(function()
if not map.manager.activemap or map.manager.sbactive or candle.spawned[3] then
PauseTimer(self.candletmr)
else
candle:burn(1)
end
end,"burn")
end)
end
function map.manager:exit()
utils.debugfunc(function()
map.manager.activemap = false
map.manager:setbasebounds()
utils.panallp(utils.rectx(gg_rct_betsyenter), utils.recty(gg_rct_betsyenter), 0.0)
utils.playsoundall(kui.sound.digend)
utils.moveallp(gg_rct_betsyenter)
BlzChangeMinimapTerrainTex('war3mapImported\\minimap-hub.blp')
-- save the character:
utils.playerloop(function(p)
port_yellowtp:play(utils.unitxy(kobold.player[p].unit))
if not freeplay_mode then
kobold.player[p].char:save_character()
if p == utils.localp() then
kui.canvas.game.equip.save:show()
end
end
end)
end, "map.manager:exit")
end
function map.manager:setbasebounds()
utils.setcambounds(gg_rct_expeditionVision)
EnableTrigger(kk.boundstrig)
end
function map.mission:init()
self.__index = self
self.debug = false -- troubleshooting prints.
self.mtypet = {}
self.mtable = {}
-- globals:
m_stat_density = 1
m_stat_movespeed = 2
m_stat_attack = 3
m_stat_health = 4
m_stat_enemy_res = 5
m_stat_waxrate = 6
m_stat_player_ms = 7
m_stat_player_hp = 8
m_stat_treasure = 9
m_stat_xp = 10
m_stat_ms = 11
m_stat_blockhp = 12
m_stat_oredensity = 13
m_stat_elite_pack = 14
m_stat_elite_str = 15
-- gold rush:
self.goldmineid = 'ngol'
self.koboldworkerid = 'nkob'
self.koboldhallid = 'h00J'
self.koboldtowerid = 'o000'
self.grworkerhp = 164 -- starting health of kobold workers.
self.grtowerhp = 250 -- `` kobold towers.
-- candle heist:
self.darkkobpylonid = 'n01V'
self.chpylonhp = 124
-- mission ids:
m_type_monster_hunt = 1
m_type_gold_rush = 2
m_type_candle_heist = 3
m_type_boss_fight = 4
-- mission data:
self.missionintros = {
[m_type_monster_hunt] = "|c0047c9ffMonster Hunt|r: Defeat the two treasure guardians!",
[m_type_gold_rush] = "|c00fff000Gold Rush|r: Defend your workers while they mine!",
[m_type_candle_heist] = "|c00fdff6fCandle Heist|r: Recover wax from the Dark Kobolds!",
[m_type_boss_fight] = "|c00ff3e3eBoss Fight|r",
}
m_asset = {}
m_asset.obj = {}
m_asset.map = {}
m_asset.name = {}
-- obj tex:
m_asset.obj[m_type_monster_hunt] = "war3mapImported\\obj_icon-monster.blp"
m_asset.obj[m_type_gold_rush] = "war3mapImported\\obj_icon-rush.blp"
m_asset.obj[m_type_candle_heist] = "war3mapImported\\obj_icon-heist.blp"
m_asset.obj[m_type_boss_fight] = "war3mapImported\\obj_icon-monster.blp"
-- map icon mdl:
m_asset.map[m_type_monster_hunt] = "UI\\Minimap\\MiniMap-Boss.mdl"
m_asset.map[m_type_gold_rush] = "UI\\Minimap\\Minimap-QuestObjectiveBonus.mdl"
m_asset.map[m_type_candle_heist] = "UI\\Minimap\\MiniMap-ControlPoint.mdl"
m_asset.map[m_type_boss_fight] = "UI\\Minimap\\MiniMap-Boss.mdl"
--
m_asset.name[m_type_monster_hunt] = "Monster Hunt"
m_asset.name[m_type_gold_rush] = "Gold Rush"
m_asset.name[m_type_candle_heist] = "Candle Heist"
m_asset.name[m_type_boss_fight] = "Boss Fight"
--
self.starticon = "UI\\Minimap\\Minimap-Waypoint.mdl"
--
self.diff = map.manager.diff
-- screen settings:
self.score = {}
self.score.d = 128
self.score.x = 10560
self.score.y = -18300
self.score.walkd = 256
-- instantiate player ore stat table:
self.goldmult = 0.07 -- ore multiplier per map level.
-- enemy modifiers:
self.setting = {}
self.setting[m_stat_density] = 1.0
self.setting[m_stat_attack] = 1.0
self.setting[m_stat_health] = 1.0
self.setting[m_stat_enemy_res] = 1.0
self.setting[m_stat_waxrate] = 1.0
self.setting[m_stat_player_ms] = 1.0
self.setting[m_stat_player_hp] = 1.0
self.setting[m_stat_treasure] = 1.0
self.setting[m_stat_xp] = 1.0
self.setting[m_stat_ms] = 1.0
self.setting[m_stat_blockhp] = 1.0
self.setting[m_stat_oredensity] = 1.0
self.setting[m_stat_elite_pack] = 1.0
self.setting[m_stat_elite_str] = 1.0
self.level = 1
self.forceditem = nil -- table for fragment modifier.
self.forcedrarity = nil -- ``
-- instantiated mission settings:
self.id = id
self.label = label
self.objicon = "war3mapImported\\obj_icon-monster.blp" -- icon displayed in objective counter.
self.mapicon = "UI\\Minimap\\Minimap-QuestObjectiveBonus.mdl" -- minimap icon path.
-- where minimap icons are stored:
self.icont = {}
-- generate base mission templates:
map.mission:buildtypes()
end
function map.mission:new(missionid, objtotal)
local o = {}
setmetatable(o, self)
o.id = missionid
o.objtotal= objtotal or 2
o.objdone = 0
o.name = m_asset.name[missionid]
o.objicon = m_asset.obj[missionid] -- objective counter
o.mapicon = m_asset.map[missionid] -- minimap image
o.setting = utils.shallow_copy(self.setting) -- clone to do custom mods per mission type if needed.
o.mods = map.diff:getkeymods()
o:modmerge()
return o
end
function map.mission:generate(missionid)
-- create a copy of the missionid template for total manipulation.
local o = utils.deep_copy(self.mtypet[missionid])
if o then
setmetatable(o, self)
else
print("error: map.mission:generate found no missionid in self.mtypet")
return
end
return o
end
function map.mission:modmerge()
if self.mods then -- if dig site fragment present, add any modifiers
for m_stat,mult in pairs(self.mods) do
self.setting[m_stat] = self.setting[m_stat] + mult
end
end
end
function map.mission:diffmerge()
-- update cached mission settings based on selected dig site difficulty.
for m_stat,diffmult in pairs(map.diff[map.manager.diffid]) do
if self.setting[m_stat] then
self.setting[m_stat] = self.setting[m_stat] * diffmult
end
end
end
function map.mission:objstep(_scoredelaydur, _sndsuppress)
-- increment the obj counter.
self.objdone = self.objdone + 1
-- run objective complete:
if self.objdone >= self.objtotal and not map.manager.objiscomplete then
map.manager.objiscomplete = true
kui.canvas.game.skill.obj.txt:settext(color:wrap(color.tooltip.good, math.min(self.objdone, self.objtotal).."/"..self.objtotal))
if map.mission.cache.id ~= m_type_boss_fight then alert:new(color:wrap(color.tooltip.good, "Objective Complete!"), 4.0) end
utils.playerloop(function(p)
SetUnitInvulnerable(kobold.player[p].unit, true)
speff_voidport[1]:attachu(kobold.player[p].unit, _scoredelaydur or 6.0, 'origin', 0.9)
end)
utils.playsoundall(kui.sound.complete)
PauseTimer(map.manager.candletmr)
utils.timed(_scoredelaydur or 6.0, function() utils.debugfunc(function() self:objcomplete() end, "objcomplete") end)
elseif self.objdone < self.objtotal then -- add to progress:
kui.canvas.game.skill.obj.txt:settext(color:wrap(color.txt.txtwhite, self.objdone.."/"..self.objtotal))
if not _sndsuppress then utils.playsoundall(kui.sound.objstep) end -- play step complete sound
end
end
function map.mission:objfailed()
map.manager.success = false
utils.playerloop(function(p)
for i = 1,3 do map.manager.scoreboard.card[kobold.player[p].pnum].loot[i]:hide() end
utils.palert(p, "Your team has been downed.", 6.0)
end)
utils.playsoundall(kui.sound.failure)
utils.timed(6.0, function()
utils.debugfunc(function() map.manager:scoreboardstart() end, "objfailed")
end)
end
function map.mission:objcomplete()
map.manager.success = true
utils.looplocalp(ClearTextMessages)
-- generate loot:
local rarityid, ilvl, slotid
for p,_ in pairs(kobold.playing) do
local pobj = kobold.player[p]
local warningflag = false -- alert spam prevention.
for i = 1,3 do
local newitem = nil
rarityid, ilvl, slotid = self:rolltreasure(pobj)
newitem = loot:generate(pobj.p, ilvl, slotid, rarityid)
utils.looplocalp(function() map.manager.scoreboard.card[pobj.pnum].loot[i]:show() end)
utils.looplocalp(function() map.manager.scoreboard.card[pobj.pnum].loot[i].rarity:setbgtex(loot.raritytable[rarityid].icon) end)
if newitem then utils.looplocalp(function() map.manager.scoreboard.card[pobj.pnum].loot[i]:setbgtex(newitem.itemtype.icon) end) newitem = nil end
end
pobj = nil
end
map.manager:scoreboardstart()
end
function map.mission:rolltreasure(pobj)
local raremin, raremax, ilvlmin, ilvlmax, slotid, ilvl = rarity_common, rarity_common, math.max(1,pobj.level-3), math.min(60,pobj.level+1), loot:getrandomslottype(), 1
local oddsmod = pobj:getlootodds()
-- get item level:
if oddsmod >= 10000 or math.random(0,10000 - oddsmod) < 500 then -- 5 percent of the time, roll a perfect match ilvl. improved by treasure find.
ilvl = ilvlmax
else
ilvl = math.random(ilvlmin, ilvlmax)
end
-- get rarity min:
if pobj.level >= 30 then -- epics can only roll above level 30.
-- tyrannical difficulty = guaranteed rare or higher.
if map.manager.diffid == 5 then
raremin = rarity_rare
raremax = rarity_epic
else
raremin, raremax = rarity_common, rarity_epic
end
elseif pobj.level > 10 then -- rares can only randomly roll above level 10.
raremin, raremax = rarity_common, rarity_rare
else -- commons can only roll while below level 10.
raremin, raremax = rarity_common, rarity_common
end
rarityid = loot:getrandomrarity(raremin, raremax, oddsmod)
return rarityid, ilvl, slotid
end
function map.mission:buildpanel()
-- objective tracker panel.
local fr = kui.frame:newbytype("PARENT", kui.canvas.game.skill)
fr.txt = fr:createbtntext("0/3", nil)
fr.txt:setsize(kui:px(104), kui:px(16))
fr.txt:setfp(fp.c, fp.t, kui.minimap, 0, kui:px(76))
fr.txt:assigntooltip("obj")
fr.icon = kui.frame:newbytype("BACKDROP", fr)
fr.icon:addbgtex(self.objicon)
fr.icon:setsize(kui:px(64), kui:px(64))
fr.icon:setfp(fp.t, fp.b, fr.txt, 0, 0)
BlzFrameSetTextAlignment(fr.txt.fh, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)
fr:hide()
return fr
end
function map.mission:seticon()
kui.canvas.game.obj.icon:setbgtex(self.objicon)
end
function map.mission:placestarticon()
CreateMinimapIconBJ(map.grid.start.x, map.grid.start.y, 0, 255, 255, self.starticon, FOG_OF_WAR_MASKED)
self.icont[#self.icont+1] = GetLastCreatedMinimapIcon()
end
function map.mission:placemapicon(x, y, missionid)
-- for unit in future?: SetMinimapIconOrphanDestroy( GetLastCreatedMinimapIcon(), true )
CreateMinimapIconBJ(x, y, 255, 255, 255, self.mapicon, FOG_OF_WAR_MASKED)
self.icont[#self.icont1] = GetLastCreatedMinimapIcon()
end
function map.mission:attachmapicon(unit)
-- destroy when this unit dies
CreateMinimapIconOnUnitBJ(unit, 255, 255, 255, self.mapicon, FOG_OF_WAR_MASKED)
SetMinimapIconOrphanDestroy(GetLastCreatedMinimapIcon(), true)
self.icont[#self.icont+1] = GetLastCreatedMinimapIcon()
end
function map.mission:clearmapicons()
for _,icon in pairs(self.icont) do
DestroyMinimapIcon(icon)
icon = nil
end
end
function map.mission:cleanup()
map.mission:clearmapicons()
for _,trig in pairs(self.trigstack) do trig:destroy() end
self.trigstack = nil
if self ~= map.mission then
self.setting = nil
else
print("error: ran cleanup() on 'map.mission' class")
end
utils.looplocalp(function() kui.canvas.game.skill.obj:hide() end)
end
function map.mission:buildtypes()
-- monster hunt:
self.mtypet[m_type_monster_hunt] = map.mission:new(m_type_monster_hunt)
-- gold rush:
self.mtypet[m_type_gold_rush] = map.mission:new(m_type_gold_rush, 200)
-- candle heist:
self.mtypet[m_type_candle_heist] = map.mission:new(m_type_candle_heist, 3)
-- boss fight:
self.mtypet[m_type_boss_fight] = map.mission:new(m_type_boss_fight, 1)
end
-- @missiontype = mission type
function map.mission:build(missionid)
-- RANDOMIZE MISSION TYPE:
local missionid = missionid or math.random(m_type_monster_hunt, m_type_candle_heist) --[[m_type_candle_heist m_type_gold_rush--]]
if dev_mission_id then missionid = dev_mission_id end
self.cache = map.mission:generate(missionid)
self.cache.level = kobold.player[Player(0)].level
self.cache:diffmerge()
self.cache:modmerge()
end
function map.mission:placeobj()
-- NOTE: this function's objective component only applies to monster hunt now
self.trigstack = {}
if map.mission.cache.id == m_type_monster_hunt then
-- place bounty activators:
local d = map.grid:getx( math.random(math.floor(map.grid.r*0.5), math.floor(map.grid.r*0.8)) )
local a = utils.anglexy(map.grid.start.x, map.grid.start.y, map.grid.center.x, map.grid.center.y)
self.spawn = { [1] = {}, [2] = {} }
self.spawn[1].x, self.spawn[1].y = utils.projectxy(map.grid.center.x, map.grid.center.y, d, a + 45)
self.spawn[2].x, self.spawn[2].y = utils.projectxy(map.grid.center.x, map.grid.center.y, d, a - 45)
for i = 1,2 do
map.grid:excavate(self.spawn[i].x, self.spawn[i].y, 1024, map.manager.biome.dirt)
map.grid:clearunits(self.spawn[i].x, self.spawn[i].y, 1536)
local bossid = math.random(1,#creature.boss[map.manager.biome.id])
local unit = creature.boss[map.manager.biome.id][bossid]:create(self.spawn[i].x, self.spawn[i].y)
local trove = b_trove:placexy(self.spawn[i].x, self.spawn[i].y)
self.trigstack[#self.trigstack+1] = trg:newdeathtrig(trove, function() ShowUnit(unit, true) end, speff_explode)
self.trigstack[#self.trigstack+1] = trg:newdeathtrig(unit, function() self:objstep() end)
self:attachmapicon(unit)
ShowUnit(unit, false)
end
-- place shrines:
for i = 1,2 do
local a = utils.anglexy(self.spawn[i].x, self.spawn[i].y, 0, 0) + math.randomneg(-17,17)
local sx,sy = utils.projectxy(self.spawn[i].x, self.spawn[i].y, math.random(1512,3584), a)
shrine:placerandom(sx, sy)
end
self.spawn = nil
elseif map.mission.debug then
print("caution: map.mission:placeobj(): mission id",map.mission.cache.id,"not implemented")
end
utils.looplocalp(function()
kui.canvas.game.skill.obj.txt:settext("0/"..self.objtotal)
kui.canvas.game.skill.obj.icon:addbgtex(self.objicon)
kui.canvas.game.skill.obj:show()
end)
end
function map.diff:init()
self.lvlreq = 0
-- id map: 1 = greenwhisker | 2 = standard | 3 = heroic | 4 = vicious | 5 = tyrannical
self[1] = map.diff:new(0.60, 0.60, 1.00, 0.60, 1.50, 1.00, 1.00, 0.75, 1.00, 0.75)
self[2] = map.diff:new(1.00, 1.00, 1.00, 1.00, 1.00, 1.10, 1.25, 1.00, 1.00, 1.00)
self[3] = map.diff:new(1.50, 1.50, 1.10, 1.10, 0.90, 1.50, 1.50, 1.00, 1.25, 1.50)
self[4] = map.diff:new(2.25, 2.25, 1.25, 1.30, 0.80, 3.00, 2.00, 1.00, 1.50, 1.75)
self[4].lvlreq = 20
self[5] = map.diff:new(3.75, 3.25, 1.50, 1.40, 0.70, 10.00, 5.00, 1.50, 1.50, 1.75)
self[5].lvlreq = 30
end
-- retrieve dig site key modifiers:
function map.diff:getkeymods()
local t = {}
local key = kobold.player[Player(0)].items[slot_digkey] or nil
if key and key.kw then
for kw_type,kwt in pairs(key.kw) do
-- TODO
end
else
t = nil
end
return t
end
function map.diff:new(enemyhp, enemystr, enemyres, enemydensity, waxcap, treasure, xp, ms, elitepacksize, elitestr)
local o = {}
o[m_stat_health] = enemyhp
o[m_stat_attack] = enemystr
o[m_stat_enemy_res] = enemyres
o[m_stat_density] = enemydensity
o[m_stat_waxrate] = waxcap
o[m_stat_treasure] = treasure
o[m_stat_xp] = xp
o[m_stat_ms] = ms
o[m_stat_elite_pack] = elitepacksize
o[m_stat_elite_str] = elitestr
o[m_stat_player_ms] = 1.0
o[m_stat_blockhp] = 1.0
o[m_stat_player_hp] = 1.0
o[m_stat_oredensity] = 1.0
o.lvlreq = 0
setmetatable(o, self)
return o
end
mastery = {}
mastery.nodemap = {} -- template for placement/mapping.
mastery.nodemap[#mastery.nodemap+1] = {0,2,0,0,0,2,0,0,0,2,0,0,0,2,0,0,0,2,0,0,0,2,0,0,0,2,0,0,0,2,0}
mastery.nodemap[#mastery.nodemap+1] = {1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1}
mastery.nodemap[#mastery.nodemap+1] = {1,6,1,0,1,7,1,0,1,8,1,0,1,4,1,0,1,5,1,0,1,6,1,0,1,7,1,0,1,8,1}
mastery.nodemap[#mastery.nodemap+1] = {1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1}
mastery.nodemap[#mastery.nodemap+1] = {1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1}
mastery.nodemap[#mastery.nodemap+1] = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
mastery.nodemap[#mastery.nodemap+1] = {1,3,1,1,1,4,1,1,1,5,1,1,1,6,1,-1,1,7,1,1,1,8,1,1,1,3,1,1,1,4,1}
mastery.nodemap[#mastery.nodemap+1] = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
mastery.nodemap[#mastery.nodemap+1] = {1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1}
mastery.nodemap[#mastery.nodemap+1] = {1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1}
mastery.nodemap[#mastery.nodemap+1] = {1,8,1,0,1,7,1,0,1,6,1,0,1,5,1,0,1,4,1,0,1,8,1,0,1,7,1,0,1,6,1}
mastery.nodemap[#mastery.nodemap+1] = {1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1}
mastery.nodemap[#mastery.nodemap+1] = {0,2,0,0,0,2,0,0,0,2,0,0,0,2,0,0,0,2,0,0,0,2,0,0,0,2,0,0,0,2,0}
-- mastery.nodemap[#mastery.nodemap+1] = {1,1,1,1,1,1,1,1,1,1,1,0,2,1,1,0,1,1,2,0,1,1,1,1,1,1,1,1,1,1,1}
-- mastery.nodemap[#mastery.nodemap+1] = {2,1,1,1,1,1,1,2,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,2,1,1,1,1,1,1,2}
-- mastery.nodemap[#mastery.nodemap+1] = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
-- mastery.nodemap[#mastery.nodemap+1] = {0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0}
-- mastery.nodemap[#mastery.nodemap+1] = {0,0,0,0,0,0,0,0,1,1,1,0,1,1,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0}
-- mastery.nodemap[#mastery.nodemap+1] = {1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1}
-- mastery.nodemap[#mastery.nodemap+1] = {2,1,1,1,1,1,1,2,1,1,1,1,1,1,1,-1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,2}
-- mastery.nodemap[#mastery.nodemap+1] = {1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1}
-- mastery.nodemap[#mastery.nodemap+1] = {0,0,0,0,0,0,0,0,1,1,1,0,1,1,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0}
-- mastery.nodemap[#mastery.nodemap+1] = {0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0}
-- mastery.nodemap[#mastery.nodemap+1] = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
-- mastery.nodemap[#mastery.nodemap+1] = {2,1,1,1,1,1,1,2,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,2,1,1,1,1,1,1,2}
-- mastery.nodemap[#mastery.nodemap+1] = {1,1,1,1,1,1,1,1,1,1,1,0,2,1,1,0,1,1,2,0,1,1,1,1,1,1,1,1,1,1,1}
-- abilcode | name |
function mastery:init() -- ran in kui:uigen()
self.__index = self
self.points = 0
self.nodemaxx = 31
self.centerx = math.ceil(self.nodemaxx/2)
self.nodemaxy = 13
self.centery = math.ceil(self.nodemaxy/2)
self.tipinfo = color:wrap(color.txt.txtdisable, "|n|nSelect this node to spend a Mastery Point and learn it, gaining its passive benefit."
.."|n|nYou are awarded 1 Mastery Point each level.")
self.tipinfo2 = color:wrap(color.txt.txtdisable, "|n|nSelect this node to spend a Mastery Point and learn it, acquiring its active ability.")
.."|n|n"..color:wrap(color.tooltip.good, "Click learned Mastery Abilities to add and remove them from your skillbar.")
-- mastery abilities:
mastery.abillist = {
ar1 = ability.template.mastery[1],
ar2 = ability.template.mastery[2],
fr1 = ability.template.mastery[3],
fr2 = ability.template.mastery[4],
na1 = ability.template.mastery[5],
na2 = ability.template.mastery[6],
fi1 = ability.template.mastery[7],
fi2 = ability.template.mastery[8],
sh1 = ability.template.mastery[9],
sh2 = ability.template.mastery[10],
ph1 = ability.template.mastery[11],
ph2 = ability.template.mastery[12],
hl1 = ability.template.mastery[13],
hl2 = ability.template.mastery[14],
ab1 = ability.template.mastery[15],
ab2 = ability.template.mastery[16],
}
-- amount awarded | stat | node name | icon | stat descript | modifier symbol
mastery.statlist = {
str = { 2, p_stat_strength_b, "Warrior", 'ReplaceableTextures\\CommandButtons\\BTNGauntletsOfOgrePower.blp', "Strength", ""},
wis = { 2, p_stat_wisdom_b, "Sage", 'ReplaceableTextures\\CommandButtons\\BTNPendantOfMana.blp', "Wisdom", ""},
ala = { 2, p_stat_alacrity_b, "Rogue", 'ReplaceableTextures\\CommandButtons\\BTNDaggerOfEscape.blp', "Alacrity", ""},
vit = { 2, p_stat_vitality_b, "Bruiser", 'ReplaceableTextures\\CommandButtons\\BTNPeriapt.blp', "Vitality", ""},
ard = { 3, p_stat_arcane, "Arcane Spell", 'ReplaceableTextures\\CommandButtons\\BTNManaBurn.blp', "Arcane Spell Damage", "%%"},
frd = { 3, p_stat_frost, "Frost Spell", 'ReplaceableTextures\\CommandButtons\\BTNDarkRitual.blp', "Frost Spell Damage", "%%"},
nad = { 3, p_stat_nature, "Nature Spell", 'ReplaceableTextures\\CommandButtons\\BTNRegenerate.blp', "Nature Spell Damage", "%%"},
fid = { 3, p_stat_fire, "Fire Spell", 'ReplaceableTextures\\CommandButtons\\BTNImmolationOn.blp', "Fire Spell Damage", "%%"},
shd = { 3, p_stat_shadow, "Shadow Spell", 'ReplaceableTextures\\CommandButtons\\BTNFaerieFire.blp', "Shadow Spell Damage", "%%"},
phd = { 3, p_stat_phys, "Physical Spell", 'ReplaceableTextures\\CommandButtons\\BTNDeathPact.blp', "Physical Spell Damage", "%%"},
abd = { 2, p_stat_dmg_arcane, "Arcane Amplification", 'ReplaceableTextures\\CommandButtons\\BTNReplenishMana.blp', "Added Arcane Damage", "%%"},
rbd = { 2, p_stat_dmg_frost, "Frost Amplification", 'ReplaceableTextures\\CommandButtons\\BTNBreathOfFrost.blp', "Added Frost Damage", "%%"},
nbd = { 2, p_stat_dmg_nature, "Nature Amplification", 'ReplaceableTextures\\CommandButtons\\BTNMonsoon.blp', "Added Nature Damage", "%%"},
fbd = { 2, p_stat_dmg_fire, "Fire Amplification", 'ReplaceableTextures\\CommandButtons\\BTNFire.blp', "Added Fire Damage", "%%"},
sbd = { 2, p_stat_dmg_shadow, "Shadow Amplification", 'ReplaceableTextures\\CommandButtons\\BTNPossession.blp', "Added Shadow Damage", "%%"},
pbd = { 2, p_stat_dmg_phys, "Physical Amplification", 'ReplaceableTextures\\CommandButtons\\BTNImpale.blp', "Added Physical Damage", "%%"},
arr = { 3, p_stat_arcane_res, "Arcane Ward", 'ReplaceableTextures\\CommandButtons\\BTNOrbOfLightning.blp', "Arcane Resistance", "%%"},
frr = { 3, p_stat_frost_res, "Frost Ward", 'ReplaceableTextures\\CommandButtons\\BTNOrbOfFrost.blp', "Frost Resistance", "%%"},
nar = { 3, p_stat_nature_res, "Nature Ward", 'ReplaceableTextures\\CommandButtons\\BTNOrbOfVenom.blp', "Nature Resistance", "%%"},
fir = { 3, p_stat_fire_res, "Fire Ward", 'ReplaceableTextures\\CommandButtons\\BTNOrbOfFire.blp', "Fire Resistance", "%%"},
shr = { 3, p_stat_shadow_res, "Shadow Ward", 'ReplaceableTextures\\CommandButtons\\BTNOrbOfDarkness.blp', "Shadow Resistance", "%%"},
phr = { 3, p_stat_phys_res, "Physical Ward", 'ReplaceableTextures\\CommandButtons\\BTNOrbofSlowness.blp', "Physical Resistance", "%%"},
bhp = { 3, p_stat_bhp, "Toughness", 'ReplaceableTextures\\CommandButtons\\BTNHealthStone.blp', "Maximum Health", "%%"},
bmn = { 3, p_stat_bmana, "Energy", 'ReplaceableTextures\\CommandButtons\\BTNManaStone.blp', "Maximum Mana", "%%"},
hlg = { 3, p_stat_healing, "Mending", 'ReplaceableTextures\\CommandButtons\\BTNHealingWard.blp', "Healing Power", "%%"},
abs = { 3, p_stat_absorb, "Shielding", 'ReplaceableTextures\\CommandButtons\\BTNPurge.blp', "Absorb Power", "%%"},
wax = { 3, p_stat_wax, "Candle", 'ReplaceableTextures\\CommandButtons\\BTNPotionOfRestoration.blp', "Wax Capacity", "%%"},
min = { 4, p_stat_minepwr, "Mining", 'ReplaceableTextures\\CommandButtons\\BTNGatherGold.blp', "Mining Power", "%%"},
thr = { 3, p_stat_thorns, "Vengeance", 'ReplaceableTextures\\CommandButtons\\BTNMetamorphosis.blp', "Thorns", ""},
shl = { 2, p_stat_shielding, "Shielding", 'ReplaceableTextures\\CommandButtons\\BTNAbsorbMagic.blp', "Absorb on Spell", ""},
els = { 1, p_stat_elels, "Spell Leech", 'ReplaceableTextures\\CommandButtons\\BTNDevourMagic.blp', "Elemental Lifesteal", "%%"},
pls = { 1, p_stat_physls, "Blood Leech", 'ReplaceableTextures\\CommandButtons\\BTNVampiricAura.blp', "Physical Lifesteal", "%%"},
drd = { 1, p_stat_dmg_reduct, "Protection", 'ReplaceableTextures\\CommandButtons\\BTNDefend.blp', "Damage Reduction", "%%"},
poh = { 2, p_stat_potionpwr, "Potion", 'ReplaceableTextures\\CommandButtons\\BTNPotionGreenSmall.blp', "Health Potion Power", "%%"},
pom = { 2, p_stat_artifactpwr, "Artifact", 'ReplaceableTextures\\CommandButtons\\BTNPotionBlueSmall.blp', "Mana Potion Power", "%%"},
ele = { 1, p_stat_eleproc, "Elemental", 'ReplaceableTextures\\CommandButtons\\BTNScatterRockets.blp', "Proliferation", "%%"},
sum = { 3, p_stat_miniondmg, "Summoner", 'ReplaceableTextures\\CommandButtons\\BTNDarkSummoning.blp', "Minion Damage", "%%"},
arm = { 3, p_stat_armor, "Endurance", 'ReplaceableTextures\\CommandButtons\\BTNThoriumArmor.blp', "Armor Rating", ""},
dog = { 3, p_stat_dodge, "Avoidance", 'ReplaceableTextures\\CommandButtons\\BTNCloudOfFog.blp', "Dodge Rating", ""},
}
-- template for stat modifier/ability placement:
mastery.statmap = {}
mastery.statmap[#mastery.statmap+1] = {"___","ph1","___","___","___","sh1","___","___","___","na1","___","___","___","ab1","___","___","___","hl1","___","___","___","ar1","___","___","___","fi1","___","___","___","fr1","___"}
mastery.statmap[#mastery.statmap+1] = {"phr","phd","phr","___","shr","shd","shr","___","nar","nad","nar","___","abs","abs","abs","___","hlg","hlg","hlg","___","arr","ard","arr","___","fir","fid","fir","___","frr","frd","frr"}
mastery.statmap[#mastery.statmap+1] = {"phd","***","phd","___","shd","***","shd","___","nad","***","nad","___","abs","***","abs","___","hlg","***","hlg","___","ard","***","ard","___","fid","***","fid","___","frd","***","frd"}
mastery.statmap[#mastery.statmap+1] = {"phr","phd","phr","___","shr","shd","shr","___","nar","nad","nar","___","abs","abs","abs","___","hlg","hlg","hlg","___","arr","ard","arr","___","fir","fid","fir","___","frr","frd","frr"}
mastery.statmap[#mastery.statmap+1] = {"vit","drd","vit","___","vit","str","vit","___","vit","ala","vit","___","vit","str","vit","___","vit","wis","vit","___","vit","ala","vit","___","vit","wis","vit","___","vit","drd","vit"}
mastery.statmap[#mastery.statmap+1] = {"thr","pls","thr","str","pom","bmn","pom","ala","wax","wax","wax","str","bhp","bhp","bhp","___","bmn","bmn","bmn","wis","min","min","min","ala","poh","bhp","poh","wis","shl","els","shl"}
mastery.statmap[#mastery.statmap+1] = {"pls","***","pls","str","bmn","***","bmn","ala","wax","***","wax","str","bhp","***","bhp","cen","bmn","***","bmn","wis","min","***","min","ala","bhp","***","bhp","wis","els","***","els"}
mastery.statmap[#mastery.statmap+1] = {"thr","pls","thr","str","pom","bmn","pom","ala","wax","wax","wax","str","bhp","bhp","bhp","___","bmn","bmn","bmn","wis","min","min","min","ala","poh","bhp","poh","wis","shl","els","shl"}
mastery.statmap[#mastery.statmap+1] = {"vit","drd","vit","___","vit","str","vit","___","vit","ala","vit","___","vit","str","vit","___","vit","wis","vit","___","vit","ala","vit","___","vit","wis","vit","___","vit","drd","vit"}
mastery.statmap[#mastery.statmap+1] = {"nar","nad","nar","___","shr","shd","shr","___","phr","phd","phr","___","hlg","hlg","hlg","___","abs","abs","abs","___","frr","frd","frr","___","fir","fid","fir","___","arr","ard","arr"}
mastery.statmap[#mastery.statmap+1] = {"nad","***","nad","___","shd","***","shd","___","phd","***","phd","___","hlg","***","hlg","___","abs","***","abs","___","frd","***","frd","___","fid","***","fid","___","ard","***","ard"}
mastery.statmap[#mastery.statmap+1] = {"nar","nad","nar","___","shr","shd","shr","___","phr","phd","phr","___","hlg","hlg","hlg","___","abs","abs","abs","___","frr","frd","frr","___","fir","fid","fir","___","arr","ard","arr"}
mastery.statmap[#mastery.statmap+1] = {"___","na2","___","___","___","sh2","___","___","___","ph2","___","___","___","hl2","___","___","___","ab2","___","___","___","fr2","___","___","___","fi2","___","___","___","ar2","___"}
-- mastery.statmap[#mastery.statmap+1] = {"phd","phd","phd","phr","phd","phd","phd","phd","arm","arm","arm","...","hl2","hlg","hlg","...","abs","abs","ab1","...","shl","shl","shl","frd","frd","frd","frd","frr","frd","frd","frd"}
-- mastery.statmap[#mastery.statmap+1] = {"ph2","phd","phd","phr","phd","phd","phd","ph1","arm","arm","arm","min","hlg","hlg","hlg","...","abs","abs","abs","min","shl","shl","shl","fr1","frd","frd","frd","frr","frd","frd","fr2"}
-- mastery.statmap[#mastery.statmap+1] = {"phd","phd","phd","phr","phd","phd","phd","phd","arm","arm","arm","min","hlg","hlg","hlg","...","abs","abs","abs","min","shl","shl","shl","frd","frd","frd","frd","frr","frd","frd","frd"}
-- mastery.statmap[#mastery.statmap+1] = {"___","___","___","___","___","___","___","___","pls","pls","pls","min","bhp","bhp","bhp","...","pom","pom","pom","min","wax","wax","wax","___","___","___","___","___","___","___","___"}
-- mastery.statmap[#mastery.statmap+1] = {"___","___","___","___","___","___","___","___","pls","pls","pls","...","bhp","bhp","bhp","vit","pom","pom","pom","...","wax","wax","wax","___","___","___","___","___","___","___","___"}
-- mastery.statmap[#mastery.statmap+1] = {"shd","shd","shd","shr","shd","shd","shd","shd","dog","dog","dog","...","...","...","...","vit","...","...","...","...","ele","ele","ele","nad","nad","nad","nad","nar","nad","nad","nad"}
-- mastery.statmap[#mastery.statmap+1] = {"sh2","shd","shd","shr","shd","shd","shd","sh1","dog","dog","dog","ala","ala","str","str","cen","wis","wis","vit","vit","ele","ele","ele","na1","nad","nad","nad","nar","nad","nad","na2"}
-- mastery.statmap[#mastery.statmap+1] = {"shd","shd","shd","shr","shd","shd","shd","shd","dog","dog","dog","...","...","...","...","vit","...","...","...","...","ele","ele","ele","nad","nad","nad","nad","nar","nad","nad","nad"}
-- mastery.statmap[#mastery.statmap+1] = {"___","___","___","___","___","___","___","___","wax","wax","wax","...","poh","poh","poh","vit","bhp","bhp","bhp","...","els","els","els","___","___","___","___","___","___","___","___"}
-- mastery.statmap[#mastery.statmap+1] = {"___","___","___","___","___","___","___","___","wax","wax","wax","min","poh","poh","poh","...","bhp","bhp","bhp","min","els","els","els","___","___","___","___","___","___","___","___"}
-- mastery.statmap[#mastery.statmap+1] = {"fid","fid","fid","fir","fid","fid","fid","fid","sum","sum","sum","min","abs","abs","abs","...","hlg","hlg","hlg","min","bmn","bmn","bmn","ard","ard","ard","ard","arr","ard","ard","ard"}
-- mastery.statmap[#mastery.statmap+1] = {"fi2","fid","fid","fir","fid","fid","fid","fi1","sum","sum","sum","min","abs","abs","abs","...","hlg","hlg","hlg","min","bmn","bmn","bmn","ar1","ard","ard","ard","arr","ard","ard","ar2"}
-- mastery.statmap[#mastery.statmap+1] = {"fid","fid","fid","fir","fid","fid","fid","fid","sum","sum","sum","...","ab2","abs","abs","...","hlg","hlg","hl1","...","bmn","bmn","bmn","ard","ard","ard","ard","arr","ard","ard","ard"}
-- build advtip tables:
mastery.statadvtip = {}
mastery.abiladvtip = {}
for statcode,t in pairs(mastery.statlist) do
mastery.statlist[statcode].advtip = {}
mastery.statlist[statcode].advtipanchor = fp.bl
mastery.statlist[statcode].advattachanchor = fp.tr
mastery.statlist[statcode].advtip[1] = t[4]
mastery.statlist[statcode].advtip[2] = color:wrap(color.tooltip.alert, t[3].." Mastery")
mastery.statlist[statcode].advtip[3] = "Gain "..color:wrap(color.tooltip.good, t[1]..t[6]).." "..color:wrap(color.tooltip.good, t[5])
.."."..mastery.tipinfo
end
for abilcode,t in pairs(mastery.abillist) do
mastery.abillist[abilcode].advtip = {}
mastery.abillist[abilcode].advtipanchor = fp.bl
mastery.abillist[abilcode].advattachanchor = fp.tr
mastery.abillist[abilcode].advtip[1] = mastery.abillist[abilcode].icon
mastery.abillist[abilcode].advtip[2] =
color:wrap(color.tooltip.alert, mastery.abillist[abilcode].name)
..color:wrap(color.txt.txtdisable, "|nMana Cost: "..tostring(mastery.abillist[abilcode].cost or 0))
..color:wrap(color.txt.txtdisable, "|nCooldown: "..tostring(mastery.abillist[abilcode].cd or 0).." sec")
mastery.abillist[abilcode].advtip[3] = mastery.abillist[abilcode].descript..mastery.tipinfo2
end
end
function mastery:new()
local o = {}
-- create new selected data table:
for row = 1,self.nodemaxy do
o[row] = {}
for col = 1,self.nodemaxx do
o[row][col] = false
end
end
setmetatable(o, self)
return o
end
function mastery:createplayertables()
for pnum = 1,kk.maxplayers do
local p = Player(pnum-1)
if kobold.player[p] then
kobold.player[p].mastery = mastery:new()
kobold.player[p].mastery.p = p
end
end
end
function mastery:buildpanel()
local fr = kui:createmassivepanel("MASTERY", kui.meta.masterypanel.tex)
local nodex = kui:px(kui.meta.masterypanel.nodex) -- starting x
local nodey = kui:px(kui.meta.masterypanel.nodey) -- `` y
local x,y = nodex,nodey
local w,h = kui:px(40),kui:px(40)
local iw = kui:px(22) -- abil/passive icon width/height.
local off = w*1.0
fr.clickfunc = {}
fr.node = {}
for row = 1,mastery.nodemaxy do
fr.node[row] = {}
fr.clickfunc[row] = {}
for col = 1,mastery.nodemaxx do
if col == mastery.centerx and row == mastery.centery then -- center starting point marker
fr.node.center = kui.frame:newbytype("BACKDROP", fr)
fr.node.center:addbgtex(kui.meta.masterypanel.texstart)
fr.node.center:setsize(kui:px(48), kui:px(48))
fr.node.center:setfp(fp.c, fp.c, fr, x, y)
end
if mastery.nodemap[row][col] ~= 0 then
local nodeid = mastery.nodemap[row][col]
local isabil = false
if nodeid == -1 then nodeid = 1 end -- hacky reverse since center is marked negative.
-- create nodes:
fr.node[row][col] = kui.frame:newbtntemplate(fr, kui.meta.masterypanel[nodeid].tex) -- node bg and event wrapper (runes also set here).
fr.node[row][col].btn.advtip = mastery:getadvtip(row, col, fr, nodeid == 2)
fr.node[row][col].btn:addevents(function() mastery:updateadvtipanchor(row, col, fr, nodeid == 2) end, nil, nil)
fr.node[row][col].btn:initializeadvtip()
fr.node[row][col]:setnewalpha(75)
if nodeid == 1 or nodeid == 2 then
fr.node[row][col].icon = kui.frame:newbytype("BACKDROP", fr.node[row][col])
fr.node[row][col].icon:setsize(iw, iw)
fr.node[row][col].icon:setfp(fp.c, fp.c, fr.node[row][col])
fr.node[row][col].icon:setnewalpha(125)
fr.node[row][col].icon:addbgtex(mastery:getnodeicon(row, col, fr, nodeid == 2))
fr.clickfunc[row][col] = function()
local p = utils.trigp()
local pobj = kobold.player[p]
if (not pobj.mastery:nodeistaken(row,col) and pobj.mastery:verifypath(row,col)) then
if pobj.mastery.points > 0 then
fr.respec:show()
pobj.mastery[row][col] = true
pobj.mastery:spendpoint()
pobj.mastery:verifysurround(pobj.mastery:findruneword(row,col))
mastery:addstat(pobj, row, col, nodeid == 2)
if p == utils.localp() then
fr.node[row][col]:setbgtex(kui.meta.masterypanel[nodeid].texsel)
fr.node[row][col]:setnewalpha(255)
fr.node[row][col].icon:setnewalpha(255)
utils.playsound(kui.sound.selnode)
end
-- if center unlock node:
if mastery.statmap[row][col] == "cen" then
-- 5% bonus health from center node:
pobj:modstat(p_stat_bhp, true, 5)
fr.respec:hide() -- don't allow respecs if only center is unlocked.
-- remove hover tooltip:
-- NOTE: disabled these because it doesnt support multiplayer (tip tables are singletons atm).
-- tooltip:get('masterycenter').txt = "(Mastery Tree Unlocked) "..color:wrap(color.tooltip.good,"+5%% Maximum Health")
--DestroyTrigger(fr.node[row][col].btn.mouseenter.trig) fr.node[row][col].btn.mouseenter = nil
end
else
utils.palert(p, "You don't have any available mastery points!")
end
end
-- if ability node and learned, run learn func:
if pobj.mastery[row][col] and mastery.abillist[mastery.statmap[row][col]] then
pobj.ability:learnmastery(mastery.statmap[row][col]) -- pass in the abilid (e.g. 'na1').
end
pobj = nil
end
fr.node[row][col].btn:addevents(nil, nil, fr.clickfunc[row][col])
fr.node[row][col].btn:addhoverscale(fr.node[row][col], 1.12)
elseif nodeid > 2 then
fr.node[row][col]:setnewalpha(125)
end
if nodeid == 2 then
fr.node[row][col]:setsize(w*1.25, h*1.25)
fr.node[row][col].icon:setsize(iw*1.25, iw*1.25)
else
fr.node[row][col]:setsize(w, h)
end
fr.node[row][col]:setabsfp(fp.c, x + kui.position.centerx, y + 0.32 + kui:px(kui.meta.skillh*0.5) )
fr.node[row][col].col = col
fr.node[row][col].row = row
end
x = x + off
end
x = nodex
y = y - off
end
-- unlearn all button:
fr.unlearn = kui.frame:newbtntemplate(fr, 'war3mapImported\\btn-mastery-unlearn-all-spells.blp')
fr.unlearn:setsize(kui:px(22), kui:px(30))
fr.unlearn:setfp(fp.bl, fp.b, kui.worldui, kui:px(365), kui:px(117))
fr.unlearn.btn:assigntooltip("unlearnmastery")
fr.unlearn.btn:addevents(nil, nil, function()
if not map.manager.activemap then
kobold.player[utils.trigp()].mastery:unlearnabilities(kobold.player[utils.trigp()])
else
utils.palert(p, "Cannot reset abilities during a dig!")
end
end)
fr.unlearn:show()
-- respec masteries button:
fr.respec = kui.frame:newbtntemplate(fr, 'war3mapImported\\btn-mastery_reset.blp')
fr.respec:setsize(kui:px(35), kui:px(35))
fr.respec:setfp(fp.c, fp.c, fr, kui:px(136), kui:px(-296))
fr.respec.btn:assigntooltip("respecmastery")
fr.respec.btn:addevents(nil, nil, function()
local p = utils.trigp()
if not map.manager.activemap then
if kobold.player[p].gold >= 5000 then
kui:showmodal(function()
if kobold.player[p].gold >= 5000 then
utils.playsound(kui.sound.runenode)
kobold.player[p]:awardgold(-5000)
kobold.player[p].mastery:resetall(kobold.player[p])
else
utils.palert(p, "You don't have enough gold!")
end
end)
else
utils.palert(p, "Not enough gold!")
end
else
utils.palert(p, "Cannot reset points during a dig!")
end
end)
fr.respec:hide() -- only shows when points have been spent
-- points display:
fr.pointsbd = kui.frame:newbytype("BACKDROP", fr)
fr.pointsbd:addbgtex("war3mapImported\\ui-scroll_decoration.blp")
fr.pointsbd:setsize(kui:px(240), kui:px(60))
fr.pointsbd:setfp(fp.c, fp.b, fr, 0.0, kui:px(30))
fr.points = fr.pointsbd:createtext("Points Available: "..color:wrap(color.txt.txtdisable,"0"), nil, true) -- left block
fr.points:setsize(kui:px(160), kui:px(26))
fr.points:setfp(fp.c, fp.c, fr.pointsbd, 0.0, kui:px(3))
fr.points:assigntooltip("attrzero")
fr.panelid = 5
fr.statesnd = true
fr:hide()
-- generate player mastery templates:
mastery:createplayertables()
return fr
end
function mastery:getnodeicon(row, col, fr, _isabil)
if _isabil then
return mastery.abillist[mastery.statmap[row][col]].icon
else
if mastery:validnodeid(row, col) then
return mastery.statlist[mastery.statmap[row][col]][4]
elseif mastery.statmap[row][col] == "cen" then
fr.node[row][col].btn:assigntooltip("masterycenter")
end
end
return kui.tex.invis
end
-- assign a prebuilt advtip table to a mastery node.
function mastery:getadvtip(row, col, fr, _isabil)
if mastery:validnodeid(row, col) then
if _isabil then
return mastery.abillist[mastery.statmap[row][col]].advtip
elseif mastery.statlist[mastery.statmap[row][col]] then
return mastery.statlist[mastery.statmap[row][col]].advtip
end
end
end
function mastery:validnodeid(row, col)
return mastery.statmap[row][col] ~= "___" and mastery.statmap[row][col] ~= "cen" and mastery.statmap[row][col] ~= "***"
end
-- when on the right side of the panel, change the tip anchor point to prevent frame squish.
function mastery:updateadvtipanchor(row, col, fr)
if col > 15 then
fr.node[row][col].btn.advattachanchor = fp.bl
if row > 6 then
fr.node[row][col].btn.advtipanchor = fp.r
else
fr.node[row][col].btn.advtipanchor = fp.tr
end
else
fr.node[row][col].btn.advattachanchor = fp.br
if row > 6 then
fr.node[row][col].btn.advtipanchor = fp.l
else
fr.node[row][col].btn.advtipanchor = fp.tl
end
end
end
-- add assigned player stat when a node is learned.
function mastery:addstat(pobj, row, col, _isabil, _removebool)
if not _isabil and mastery.statmap[row][col] ~= "cen" and mastery.statmap[row][col] ~= "***" then
if _removebool then
pobj:modstat(mastery.statlist[mastery.statmap[row][col]][2], false, mastery.statlist[mastery.statmap[row][col]][1])
else
pobj:modstat(mastery.statlist[mastery.statmap[row][col]][2], true, mastery.statlist[mastery.statmap[row][col]][1])
end
end
end
-- @row,col = selected node.
function mastery:verifypath(row,col)
-- mastery template set to true = node is learned.
if mastery.nodemap[row][col] < 3 then -- ignore runeword markers.
if mastery.nodemap[row][col] == -1 then -- center start loc.
return true
-- search nearby learned nodes:
elseif self[row][col+1] and self[row][col+1] == true then -- right
return true
elseif self[row+1] and self[row+1][col] and self[row+1][col] == true then -- down
return true
elseif self[row][col-1] and self[row][col-1] == true then -- left
return true
elseif self[row-1] and self[row-1][col] and self[row-1][col] == true then -- up
return true
end
else
return false
end
return false
end
-- @row,col = selected node loc.
-- returns row,col if runeword found.
function mastery:findruneword(row,col)
-- look around a selected node to see if a
-- runeword check should be done.
local y, x = row-1, col-1
for l = 1,8 do
y,x = mastery:loopcheck(l, y, x)
if mastery.nodemap[y] and mastery.nodemap[y][x] and mastery.nodemap[y][x] > 2 then
return y,x -- return row, col
end
end
return false
end
-- @row,col = runeword loc.
function mastery:verifysurround(row,col)
if row and col then
-- verify a runeword is surrounded if near it.
local y, x, c = row-1, col-1, 0
for l = 1,8 do
y,x = mastery:loopcheck(l, y, x)
if self[y] and self[y][x] == true then
c = c + 1
if c == 8 then
-- self[row][col] = true
if self.p == utils.localp() then
kui.canvas.game.mast.node[row][col]:setbgtex(kui.meta.masterypanel[mastery.nodemap[row][col]].texact)
kui.canvas.game.mast.node[row][col]:setnewalpha(255)
utils.playsound(kui.sound.runenode)
end
end
else
break
end
end
end
end
function mastery:loopcheck(l,y,x)
local newy,newx = y,x
-- do an 8-unit square loop starting at top left corner.
if l ~= 1 then
if l < 4 then newx = newx + 1 -- shift right
elseif l < 6 then newy = newy + 1 -- shift down
elseif l < 8 then newx = newx - 1 -- shift left
elseif l == 8 then newy = newy - 1 end -- shift up
end
return newy,newx
end
function mastery:nodeistaken(row,col)
if self[row][col] == true then
return true
end
return false
end
function mastery:addpoint(val)
local val = val or 1
self.points = self.points + val
self:refreshtooltip()
end
function mastery:spendpoint(val)
local val = val or 1
self.points = self.points - val
self:refreshtooltip()
end
function mastery:refreshtooltip()
if self.p == utils.localp() then
local col = color.txt.txtdisable
if self.points < 1 then col = color.txt.txtdisable else col = color.tooltip.good end
kui.canvas.game.mast.points:settext("Points Available: "..color:wrap(col, self.points))
end
end
function mastery:unlearnabilities(pobj)
local p = utils.trigp()
for abilid,_ in pairs(mastery.abillist) do
for col = 5,8 do
-- TODO: this is a copy and paste from ability.template:learnmastery, need to unify
if pobj.ability.spells[col] and pobj.ability.spells[col][abilid] then -- already learned, flag to remove from bar.
for casttype,rawcode in pairs(hotkey.map.codet[hotkey.map.intmap[col]]) do
if BlzGetUnitAbility(kobold.player[pobj.ability.p].unit, rawcode)
and BlzGetUnitAbilityCooldownRemaining(kobold.player[pobj.ability.p].unit, rawcode) > 0 then
utils.palert(pobj.ability.p, "You cannot remove a mastery ability while it's on cooldown!")
return
end
end
-- update key before clearing:
kobold.player[pobj.ability.p].keymap:clearkey(hotkey.map.intmap[col], pobj.ability.spells[col][abilid].casttype)
pobj.ability.spells[col][abilid] = nil
pobj.ability.spells[col] = nil
pobj.ability[col][1] = nil
kui.canvas.game.skill.skill[col].cdtxt.advtip = nil
if pobj.ability.p == utils.localp() then
kui.canvas.game.skill.skill[col].fill:setbgtex(kui.color.black)
utils.playsound(kui.sound.closebtn, pobj.ability.p)
end
end
end
end
end
function mastery:resetall(pobj)
local nodeid, totalpoints = 1, 0
pobj.mastery:unlearnabilities(pobj) -- unlearn all abils.
pobj.ability.savedmastery = nil -- reset save/load config.
for row = 1,mastery.nodemaxy do
for col = 1,mastery.nodemaxx do
nodeid = mastery.nodemap[row][col]
if nodeid == -1 then nodeid = 1 end -- hacky reverse since center is marked negative.
-- undo learned node settings:
if pobj.mastery[row][col] == true and not (col == mastery.centerx and row == mastery.centery) then
-- non-center nodes.
totalpoints = totalpoints + 1
-- force reset every row/col value:
pobj.mastery:addstat(pobj, row, col, nodeid == 2, true)
pobj.mastery[row][col] = false
-- reset icons:
kui.canvas.game.mast.node[row][col].icon:setbgtex(mastery:getnodeicon(row, col, kui.canvas.game.mast, nodeid == 2))
kui.canvas.game.mast.node[row][col].icon:setnewalpha(125)
-- background activation icons:
kui.canvas.game.mast.node[row][col]:setbgtex(kui.meta.masterypanel[nodeid].tex)
kui.canvas.game.mast.node[row][col]:setnewalpha(75)
end
-- if a rune, reset alpha:
if nodeid > 2 and not (col == mastery.centerx and row == mastery.centery) then
kui.canvas.game.mast.node[row][col]:setbgtex(kui.meta.masterypanel[nodeid].tex)
kui.canvas.game.mast.node[row][col]:setnewalpha(125)
end
end
end
-- add back all removed points:
pobj.mastery:addpoint(totalpoints)
if utils.islocalp(pobj.p) then
kui.canvas.game.mast.respec:hide()
end
end
missile = {}
function missile:init()
self.__index = self
--[[
missile instance configurables:
--]]
self.height = 80 -- default z height.
self.radius = 80 -- how wide to search to collide.
self.vel = 20 -- default velocity.
self.pitch = 0 --
self.offset = 30 -- origin distance to project from x and y.
self.locku = nil -- set this to a unit to enable homing.
self.effect = nil -- special effect for missile.
self.model = nil -- special effect model.
self.zbuff = 64 -- to prevent whacky death anims underground.
self.angle = nil -- missile travel angle.
self.cangle = nil -- `` for custom effect rotation.
self.dist = 1000 -- distance to travel.
self.trav = nil -- distance traveled so far.
self.x = nil -- missile current x coord.
self.y = nil -- `` y coord.
self.z = nil -- `` z coord.
self.func = nil -- function to run on every update.
self.time = nil -- use a travel time instead of travel distance.
self.elapsed = nil -- self.time converted to timer ticks required.
self.collide = true -- destroy on collision.
self.colfunc = nil -- function to run on collision.
self.explfunc= nil -- `` on explosion.
self.expl = false -- deal area-effect damage/healing.
self.explr = 300 -- explosion radius.
self.pierce = false -- should the missile piece targets?
self.arc = false -- should missile arc?
self.arch = 300 -- if enabled, how high?
self.heal = false -- heal allies on collision instead.
self.coleff = nil -- play this effect on struck units.
self.maxdist = 5000 -- default max missile travel range.
self.reflect = false -- the missile will play again to its origin point.
--[[
missile class settings:
--]]
missile.stack = {}
self.tmr = NewTimer()
self.cad = 0.03
self.acc = 1 -- how accurate are updates? (bigger = performant; scales with missile total).
self.total = 0 -- total missile instances.
self.tick = 0 -- the current timer tick loop (for collision accuracy).
self.buffer = 11 -- on missile destroy, run table clean if total > buffer.
self.sacc = self.acc -- cached accuracy; don't edit.
self.sradius = self.radius -- cached radius; don't edit.
--[[
toggles:
--]]
self.debug = false -- print debug counts, etc.
self.printc = false -- simple debug (print instance count).
end
--[[
basic missile creation functions:
--]]
function missile:create_timedxy(tarx, tary, dur, unit, model, damage)
-- determine angle by @tarx,@tary target coord, lasts @dur seconds.
local mis = missile:create_targetxy(tarx, tary, unit, model, damage)
mis.time = dur
mis:initduration()
return mis
end
function missile:create_arc(tarx, tary, unit, model, damage, _explfunc, _time)
-- determine angle by @tarx,@tary target coord.
local mis = missile:create_targetxy(tarx, tary, unit, model, damage)
mis.arc = true
mis.expl = true
mis.collide = false
mis.explfunc = _explfunc or nil
if _time then
mis.time = _time
mis:initduration()
end
return mis
end
function missile:create_arc_in_radius(centerx, centery, unit, model, damage, dmgtype, count, dist, _time, _hitradius, _height)
-- creates arcing artillery missiles in a radius around @centerx/@centery.
local sa,a,x,y = math.random(0,360),360/count,0,0
for i = 1,count do
x,y = utils.projectxy(utils.unitx(unit),utils.unity(unit),dist,sa)
local mis = missile:create_targetxy(x, y, unit, model, damage)
mis.arc = true
mis.expl = true
mis.collide = false
mis.dmgtype = dmgtype
mis.radius = _hitradius or self.radius
mis.height = _height or self.height
sa = sa + a
if _time then
mis.time = _time
mis:initduration()
end
end
end
function missile:create_in_radius(centerx, centery, unit, model, damage, dmgtype, count, dist, _time, _hitradius, _height, _collides, _velocity, _explodes)
-- creates standard line missiles in a radius around @centerx/@centery; pierces by default.
local sa,a,x,y = math.random(0,360),360/count,0,0
for i = 1,count do
x,y = utils.projectxy(centerx,centery,dist,sa)
local mis = missile:create_targetxy(x, y, unit, model, damage)
mis.dmgtype = dmgtype
mis.collide = _collides or false
mis.expl = _explodes or false
mis.radius = _hitradius or self.radius
mis.height = _height or self.height
mis.vel = _velocity or self.vel
sa = sa + a
if _time then
mis.time = _time
mis:initduration()
end
end
end
function missile:create_piercexy(tarx, tary, unit, model, damage)
-- determine angle by @tarx,@tary target coord.
local mis = missile:create_targetxy(tarx, tary, unit, model, damage)
mis:initpiercing()
return mis
end
-- sends a missile to a target point.
function missile:create_targetxy(tarx, tary, unit, model, damage)
-- determine angle by @tarx,@tary target coord.
local ux, uy = utils.unitxy(unit)
local angle = utils.anglexy(ux, uy, tarx, tary)
local mis = missile:create_angle(angle, utils.distxy(ux, uy, tarx, tary), unit, model, damage)
return mis
end
-- sends a missile in target angle.
function missile:create_angle(angle, dist, unit, model, damage)
-- send a missile towards a specified angle.
local mis = missile:new(unit)
mis.x, mis.y = utils.unitxy(unit)
mis.dist = dist
mis.dmg, mis.angle, mis.model = damage, angle, model
mis:launch()
return mis
end
-- sends a missile from a custom x,y starting point.
function missile:create_point(startx, starty, angle, distance, unit, model, damage, _arcing, _time)
-- send a missile towards a specified angle.
local mis = missile:new(unit)
mis.dist = distance or 1000.0
mis.x, mis.y = startx, starty
if _arcing then
mis.arc = true
mis.expl = true
mis.collide = false
end
mis.dmg, mis.angle, mis.model = damage, angle, model
mis:launch()
if _time then
mis.time = time
mis:initduration()
end
return mis
end
--[[
missile class functions:
--]]
function missile:new(owner)
local o = {}
setmetatable(o, self)
missile.stack[o] = o
o.owner = owner
return o
end
function missile:launch()
-- optional properties:
if self.arc then
self.pitch = 0
end
if self.offset > 0 then
self.x, self.y = utils.projectxy(self.x, self.y, self.offset, self.angle)
end
-- initialize missile:
self.sradius = self.radius
self.trav = 0
self.p = utils.powner(self.owner)
self.effect = AddSpecialEffect(self.model, self.x, self.y)
self.z = self:getheight() + self.height
-- check if timer should be rebooted:
if missile.total == 0 then
TimerStart(self.tmr, self.cad, true, function()
utils.debugfunc(function()
if missile.total <= 0 then
missile.total = 0
PauseTimer(self.tmr)
else
for _,mis in pairs(missile.stack) do
mis:update()
end
end
end, "update timer")
end)
end
-- increment totals, run performance check:
missile.total = missile.total + 1
if self.total > 100 then
self.acc = self.acc + math.floor(self.total/100)
-- make radius scale up to help with reduced accuracy:
self.radius = math.floor(self.radius*(1+(self.acc-self.sacc)*0.1))
else
self.acc = self.sacc
end
if missile.printc then self:debugprint() end
self:update() -- (keep this last)
end
function missile:updatescale(scale)
self.scale = scale
BlzSetSpecialEffectScale(self.effect, scale)
end
function missile:initduration()
if self.time and not self.arc then
-- non-arcing missile:
self.elapsed = math.floor(self.time/self.cad)
self.dist = self.maxdist
elseif self.time and self.arc then
-- arcing:
-- ticks required = dist/vel/cadence
self.vel = math.max(2,math.floor(self.dist/self.time*self.cad))
self.elapsed = nil
end
end
function missile:initpiercing()
self.pierce = true
self.collide = false
self.dmgtable = {}
end
function missile:initarcing()
self.arc = true
self.expl = true
self.collide = false
end
function missile:update()
-- if owner was removed:
if not self.owner then
self:destroy()
return
end
-- optional properties:
if self.func then
self.func(self)
end
if self.locku then
self.angle = utils.anglexy(self.x, self.y, utils.unitxy(self.locku))
end
-- missile positioning:
self.trav = self.trav + self.vel
self.x, self.y = utils.projectxy(self.x, self.y, self.vel, self.angle)
if self.arc then
if self.z == 0.0 then -- init height if at 0.
self.z = self:getheight() + self.height
end
self.z = (4*self.arch/self.dist)*(self.dist - self.trav)*(self.trav/self.dist) + self:getheight() + self.height
elseif self.tick == 0 then
self.z = self:getheight()
end
-- reposition missile effect:
if not self.cangle then -- customangle can be controlled in the update function (i.e. for rotation or spin effects).
BlzSetSpecialEffectYaw(self.effect, self.angle*bj_DEGTORAD)
else
BlzSetSpecialEffectYaw(self.effect, self.cangle*bj_DEGTORAD)
end
BlzSetSpecialEffectX(self.effect, self.x)
BlzSetSpecialEffectY(self.effect, self.y)
BlzSetSpecialEffectZ(self.effect, self.z)
-- check for update accuracy:
if self.tick == self.acc then
self.tick = 0
if self.arc and self.z < self:getheight() + self.zbuff then -- zbuff makes missile die slightly earlier to prevent underground death animations.
self:destroy()
return
end
else
self.tick = self.tick + 1
end
-- initiate destroy check:
if self.elapsed then
self.elapsed = self.elapsed - 1
end
if self:foundcollision() and self.collide then
self:destroy()
elseif not self.arc then
if self.trav >= self.dist then
self:destroy()
elseif (self.elapsed and self.elapsed <= 0) or self.trav >= self.dist then
self:destroy()
end
end
end
function missile:getheight()
if self.arc then
return GetTerrainCliffLevel(self.x, self.y)*128 - 256
else
return GetTerrainCliffLevel(self.x, self.y)*128 - 256 + self.height
end
end
function missile:foundcollision()
if not self.arc and self.tick == 0 then
local grp = g:newbyxy(self.p, g_type_dmg, self.x, self.y, self.radius)
self.hitsize = grp:getsize()
if self.hitsize > 0 then
if (self.collide and not self.expl) or (self.pierce and self.expl) or self.pierce then
for dex = 0,self.hitsize-1 do
self.hitu = grp:indexunit(dex)
if self.pierce and self.hitu then
if self.dmgtable and not self.dmgtable[utils.data(self.hitu)] then
if self.hitu then self:dealdmg(self.hitu) end
if self.colfunc then self.colfunc(self) end
end
elseif self.collide and self.hitu then
self:dealdmg(self.hitu)
if self.colfunc then self.colfunc(self) end
break
end
end
end
grp:destroy()
return true
end
grp:destroy()
end
return false
end
function missile:explodemissile()
-- clear the damage limit table first:
if self.dmgtable then self.dmgtable = nil end
local grp = g:newbyxy(self.p, g_type_dmg, self.x, self.y, self.explr)
grp:action(function()
self:dealdmg(grp.unit)
end)
grp:destroy()
if self.explfunc then
self.explfunc(self)
end
end
function missile:dealdmg(target)
if self.pierce and self.dmgtable and self.dmgtable[utils.data(target)] then
-- the unit was already struck, so do nothing.
return
else
if self.heal then
UnitDamageTarget(self.owner, target, -self.dmg, true, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
else
if self.dmgtype and utils.pnum(self.p) < kk.maxplayers then
self.dmgtype:pdeal(self.p, self.dmg, target)
elseif self.dmgtype then
self.dmgtype:dealdirect(self.owner, target, self.dmg)
else
UnitDamageTarget(self.owner, target, self.dmg, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
end
end
if self.coleff then
utils.speffect(self.coleff, utils.unitxy(target))
end
if self.pierce and self.dmgtable then
self.dmgtable[utils.data(target)] = true
end
end
end
function missile:destroy()
if self.expl then
self:explodemissile()
end
if not self.reflect then
if self.total > 500 then
-- if we're in bananas count territory, render death effect off screen.
BlzSetSpecialEffectZ(self.effect, 3500.0)
end
DestroyEffect(self.effect)
for i,v in pairs(self) do
v = nil
i = nil
end
missile.total = missile.total - 1
missile.stack[self] = nil
if missile.total == 0 or missile.total > self.buffer then
missile:recycle()
end
if missile.printc then self:debugprint() end
else
self.reflect = false
self.trav = 0.0
self.angle = self.angle - 180.0
if self.pierce then self.dmgtable = nil self.dmgtable = {} end
if self.time then self:initduration() end
end
end
function missile:recycle()
utils.tablecollapse(missile.stack)
if missile.debug then print("recycle buffer met, recycling") end
end
function missile:debugprint()
ClearTextMessages()
print("Instances: "..missile.total)
print("Table size: "..utils.tablelength(missile.stack))
end
quest = {}
quest.lineage = {}
function quest:init()
self.__index = self
self.log = {} -- store quest objects here.
self.id = 0
self.xpaward = 250
self.lvlaward = 0
self.minimized = false -- flag for knowing if frame is min'd or max'd.
self.collectable= false -- flag for allowing quest gathering.
self.triggered = false -- flag for preventing repeat triggers.
self.complete = false -- flag to allow completion.
self.awardfunc = nil -- if present, run this function on completion for bonus effects.
self.startfunc = nil -- if present, run this function on completion for bonus effects.
-- trigger listening for speech events:
self.trig = CreateTrigger()
self.speakcode = FourCC('A03B')
TriggerRegisterAnyUnitEventBJ(self.trig, EVENT_PLAYER_UNIT_SPELL_EFFECT)
TriggerAddAction(quest.trig, function()
utils.debugfunc( function()
if GetSpellAbilityId() == quest.speakcode then
local target = GetSpellTargetUnit()
if not quest.current or (not (quest.current.endunit == target and quest.current.complete) and not (quest.current.startunit == target and quest.current.collectable)) then
if target == udg_actors[2] and quest_shinykeeper_unlocked then -- shinykeeper
shop_vendor_type_open[utils.trigpnum()] = 0
shop:toggleshopopen()
elseif target == udg_actors[4] and quest_elementalist_unlocked then -- elementalist.
shop_vendor_type_open[utils.trigpnum()] = 1
shop:toggleshopopen()
elseif target == udg_actors[8] and quest_greywhisker_unlocked then -- elementalist.
utils.playsound(kui.sound.panel[2].open, utils.unitp())
shop.gwfr:show()
end
end
end
end, "open shop speak")
end)
-- globals:
quest.inprogress = false -- flag to signal that a quest is activated and in progress.
quests_total_completed = 0
-- quest-specific mission types (for descriptions and completion logic):
m_type_speak = 0
-- from map.mission:
-- m_type_monster_hunt = 1
-- m_type_boss_fight = 2
-- m_type_gold_rush = 3
-- m_type_candle_heist = 4
quest:dataload() -- from questdata.
-- activate quest frame:
self.parentfr = kui.frame:newbytype("PARENT", kui.canvas.gameui)
-- maximized frame:
self.parentfr.maxfr = kui.frame:newbytype("BACKDROP", self.parentfr)
self.parentfr.maxfr:setbgtex('war3mapImported\\panel-quest-backdrop.blp')
self.parentfr.maxfr:setsize(kui:px(234), kui:px(128))
self.parentfr.maxfr:setfp(fp.bl, fp.b, kui.worldui, kui:px(394), kui:px(104))
self.parentfr.maxfr.titlefr = self.parentfr.maxfr:createheadertext("Stop Dilly Dallying!", 0.52) -- text frame
self.parentfr.maxfr.titlefr:setparent(self.parentfr.maxfr.fh)
self.parentfr.maxfr.titlefr:setfp(fp.t, fp.t, self.parentfr.maxfr, 0, -kui:px(27))
self.parentfr.maxfr.textfr = self.parentfr.maxfr:createtext("(No Active Quest)") -- text frame
self.parentfr.maxfr.textfr:setparent(self.parentfr.maxfr.fh)
self.parentfr.maxfr.textfr:setsize(kui:px(200), kui:px(98))
self.parentfr.maxfr.textfr:setfp(fp.c, fp.b, self.parentfr.maxfr, 0, kui:px(49))
BlzFrameSetTextAlignment(self.parentfr.maxfr.textfr.fh, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)
-- minimized frame:
self.parentfr.minfr = kui.frame:newbytype("BACKDROP", self.parentfr)
self.parentfr.minfr:setbgtex('war3mapImported\\panel-quest-standalone-banner.blp')
self.parentfr.minfr:setsize(kui:px(221), kui:px(50))
self.parentfr.minfr:setfp(fp.c, fp.b, kui.worldui, kui:px(516), kui:px(122))
self.parentfr.minfr.titlefr = self.parentfr.minfr:createheadertext("Stop Dilly Dallying!", 0.52) -- text frame
self.parentfr.minfr.titlefr:setparent(self.parentfr.minfr.fh)
self.parentfr.minfr.titlefr:setfp(fp.t, fp.t, self.parentfr.minfr, 0, -kui:px(24))
self.parentfr.maxfr:hide()
self.parentfr.minfr:hide()
-- checkmark completion icon:
self.cmfr = kui.frame:newbytype("BACKDROP", self.parentfr.maxfr) -- parent frame
self.cmfr:setsize(kui:px(21), kui:px(22))
self.cmfr:setbgtex('war3mapImported\\panel-quest-checkbox.blp')
self.cmfr:setfp(fp.c, fp.b, self.parentfr.maxfr, 0, kui:px(3))
self.cmfr:hide()
-- init minimize toggle:
self.parentfr.maxbtnwrap = kui.frame:newbtntemplate(self.parentfr, kui.tex.invis) -- create hidden wrapper buttons
self.parentfr.maxbtnwrap:setallfp(self.parentfr.maxfr)
self.parentfr.maxbtnwrap.btn:assigntooltip("questmax")
self.parentfr.minbtnwrap = kui.frame:newbtntemplate(self.parentfr, kui.tex.invis)
self.parentfr.minbtnwrap:setallfp(self.parentfr.minfr)
self.parentfr.minbtnwrap.btn:assigntooltip("questmin")
local showhidefunc = function()
if self.parentfr.maxfr:isvisible() then quest.minimized = true self.parentfr.maxfr:hide() self.parentfr.minfr:show() self.parentfr.minbtnwrap:show() self.parentfr.maxbtnwrap:hide()
else self.parentfr.maxfr:show() quest.minimized = false self.parentfr.minfr:hide() self.parentfr.minbtnwrap:hide() self.parentfr.maxbtnwrap:show() end
utils.playsound(gg_snd_audio_map_open_close, utils.trigp())
end
self.parentfr.maxbtnwrap.btn:addevents(nil, nil, showhidefunc)
self.parentfr.minbtnwrap.btn:addevents(nil, nil, showhidefunc)
self.parentfr.maxbtnwrap:hide()
self.parentfr.minbtnwrap:hide()
-- dialogue buttons:
self.socialfr = kui.frame:newbytype("PARENT", kui.canvas.gameui)
self.socialfr.shinshop = kui.frame:newbtntemplate(self.socialfr, 'war3mapImported\\menu-button-shop-icon-shinykeeper.blp')
self.socialfr.shinshop:setsize(kui:px(84), kui:px(87))
self.socialfr.shinshop:setfp(fp.c, fp.b, kui.worldui, kui:px(42), kui:px(200))
self.socialfr.shinshop.btn:assigntooltip("shopshiny")
self.socialfr.shinshop.btn:clearallfp()
self.socialfr.shinshop.btn:setfp(fp.c, fp.c, self.socialfr.shinshop)
self.socialfr.shinshop.btn:setsize(kui:px(50), kui:px(50))
self.socialfr.shinshop:hide()
self.socialfr.eleshop = kui.frame:newbtntemplate(self.socialfr, 'war3mapImported\\menu-button-shop-icon-elementalist.blp')
self.socialfr.eleshop:setsize(kui:px(84), kui:px(87))
self.socialfr.eleshop:setfp(fp.c, fp.b, kui.worldui, kui:px(-42), kui:px(200))
self.socialfr.eleshop.btn:assigntooltip("shopele")
self.socialfr.eleshop.btn:clearallfp()
self.socialfr.eleshop.btn:setfp(fp.c, fp.c, self.socialfr.eleshop)
self.socialfr.eleshop.btn:setsize(kui:px(50), kui:px(50))
self.socialfr.eleshop:hide()
self.socialfr.fragshop = kui.frame:newbtntemplate(self.socialfr, 'war3mapImported\\menu-button-shop-icon-greywhisker.blp')
self.socialfr.fragshop:setsize(kui:px(84), kui:px(87))
self.socialfr.fragshop:setfp(fp.c, fp.b, kui.worldui, 0, kui:px(236))
self.socialfr.fragshop.btn:assigntooltip("shopfrag")
self.socialfr.fragshop.btn:clearallfp()
self.socialfr.fragshop.btn:setfp(fp.c, fp.c, self.socialfr.fragshop)
self.socialfr.fragshop.btn:setsize(kui:px(50), kui:px(50))
self.socialfr.fragshop:hide()
self.socialfr.speakto = kui.frame:newbtntemplate(self.socialfr, 'war3mapImported\\menu-button-speak-to-npc-icon.blp')
self.socialfr.speakto:setsize(kui:px(84), kui:px(87))
self.socialfr.speakto:setfp(fp.c, fp.b, kui.worldui, 0, kui:px(164))
self.socialfr.speakto.btn:assigntooltip("speakto")
self.socialfr.speakto.btn:clearallfp()
self.socialfr.speakto.btn:setfp(fp.c, fp.c, self.socialfr.speakto)
self.socialfr.speakto.btn:setsize(kui:px(50), kui:px(50))
-- dialogue functions:
self.socialfr.shinshop.btn:addevents(nil, nil, function() shop_vendor_type_open[utils.trigpnum()] = 0 shop:toggleshopopen() end)
self.socialfr.eleshop.btn:addevents(nil, nil, function() shop_vendor_type_open[utils.trigpnum()] = 1 shop:toggleshopopen() end)
self.socialfr.fragshop.btn:addevents(nil, nil, function()
if utils.islocaltrigp() then
if shop.gwfr:isvisible() then
shop.gwfr:hide()
utils.playsound(kui.sound.panel[2].close, utils.trigp())
else
shop.gwfr:show()
utils.playsound(kui.sound.panel[2].open, utils.trigp())
end
end
end)
self.socialfr:hide()
self.socialfr.showfunc = function()
if quest_shinykeeper_unlocked then self.socialfr.shinshop:show() end
if quest_elementalist_unlocked then self.socialfr.eleshop:show() end
end
end
-- @name = quest title to display to users.
-- @qst_m_type = quest type used to build description and objective logic.
-- @startchain = [optional] play this speak chain before the quest alert.
-- @_biomeid = [optional] complete a mission of this biome type to finish the quest.
-- @_xpaward = [optional] award this xp to players.
-- @_lvlaward = [optional] award raw levels to players (overrides _xpaward).
-- @_treasureclass = [optional] controls which rarity, slotids, and itemtypes are available. (TODO: make this class)
function quest:new(name, qst_m_type, _startchain, _endchain, _biomeid, _xpaward, _lvlaward, _treasureclass)
local o = setmetatable({}, self)
o.name = name
if qst_m_type == m_type_boss_fight then
o.bossname = _bossname
o.bossunitid = _bossunitid
end
o.qst_m_type = qst_m_type
o.startchain = _startchain or nil
o.endchain = _endchain or nil
o.biomeid = _biomeid or nil
o.xpaward = _xpaward or self._xpaward
o.lvlaward = _lvlaward or self._lvlaward
o.treasure = _treasureclass or nil
o.id = #self.lineage + 1
self.lineage[o.id] = o
return o
end
-- a generic quest for directing the user to talk to another NPC.
function quest:createspeakquest(name, startchain, endchain, startunit, endunit, _xpaward, _lvlaward)
local qst = quest:new(name, m_type_speak, startchain, endchain, nil, _xpaward or self.xpaward, _lvlaward or nil)
qst:build(startunit, endunit)
qst:getdescription()
return qst
end
function quest:createdigquest(name, mtype, startchain, endchain, startunit, endunit, _xpaward, _lvlaward, _biomeid)
local mtype = mtype or math.random(1,2)
if mtype == m_type_boss_fight then
print("error: use quest:createbossquest to create boss quests, not quest:createdigquest")
return
else
local qst = quest:new(name, mtype, startchain, endchain, _biomeid or math.random(1,4), _xpaward or self.xpaward, _lvlaward or nil)
qst:build(startunit, endunit)
qst:getdescription()
return qst
end
end
function quest:createbossquest(name, mtype, bossname, bossid, startchain, endchain, startunit, endunit, _xpaward, _lvlaward)
local qst = quest:new(name, mtype, startchain, endchain, boss.bosst[bossid].biomeid, _xpaward or self.xpaward, _lvlaward or nil)
qst.bossname = bossname
qst.bossid = bossid
qst:build(startunit, endunit)
qst:getdescription()
return qst
end
function quest:activate()
-- set active so quest can be collected:
self:updatemapmarker("available")
self.collectable = true
self.eyecandystart = speff_quest1:attachu(self.startunit, nil, 'overhead')
quest.current = self
end
-- @startunit = initializes the quest, enabling completion.
-- @endunit = where player goes to complete the quest.
function quest:build(startunit, endunit)
self.startunit = startunit
self.endunit = endunit
self.action = TriggerAddAction(quest.trig, function()
utils.debugfunc( function()
if GetSpellAbilityId() == quest.speakcode then
if self.collectable then
local target = GetSpellTargetUnit()
utils.stop(utils.trigu())
utils.faceunit(target, utils.trigu())
-- if not collected, add quest for player:
if not self.triggered and target == self.startunit then
self.triggered = true
self:begin()
return true
-- if already collected, see if complete when at turn in point:
elseif self.complete and target == self.endunit then
self:finish()
return true
end
end
end
end, "start quest startunit")
end)
end
-- update quest frame.
-- @isquestdone = quest completed?
function quest:markprogress(isquestdone)
local str1, str2 = '', ''
str1 = self.name
if isquestdone then
-- quest is done and completion can be triggered:
self.complete = true
if not self.eyecandyfinish then
self.eyecandyfinish = speff_quest1:attachu(self.endunit, nil, 'overhead')
BlzSetSpecialEffectColor(self.eyecandyfinish, 0, 255, 0)
end
if self.qst_m_type == m_type_speak then
str2 = 'Speak to '..GetUnitName(self.endunit)
else
self.description = string.gsub(string.gsub(self.description, "|c00"..color.tooltip.alert, ""), "|r", "")
str2 = color:wrap(color.txt.txtgrey, self.description)..color:wrap(color.tooltip.good,'|nReturn to '..GetUnitName(self.endunit)..' for your reward')
end
self.cmfr:show()
self:updatemapmarker("complete")
else
-- quest is in progress:
str2 = self.description
end
BlzFrameSetText(self.parentfr.maxfr.titlefr.fh, str1) -- maximized frame title
BlzFrameSetText(self.parentfr.minfr.titlefr.fh, str1) -- minimized frame title
BlzFrameSetText(self.parentfr.maxfr.textfr.fh, str2) -- description
if self.minimized then
self.parentfr.minbtnwrap:show()
else
self.parentfr.maxbtnwrap:show()
end
end
function quest:updatemapmarker(stage)
if self.markerstart then DestroyMinimapIcon(self.markerstart) self.markerstart = nil end
if self.markerend then DestroyMinimapIcon(self.markerend) self.markerend = nil end
if stage == "available" then
CreateMinimapIconBJ(utils.unitx(self.startunit), utils.unity(self.startunit), 255, 225, 0, "UI\\Minimap\\MiniMap-QuestGiver.mdl", FOG_OF_WAR_MASKED)
self.markerstart = GetLastCreatedMinimapIcon()
elseif stage == "inprogress" then
-- DestroyMinimapIcon(quest.markerstart)
-- quest.markerstart = nil
-- show and move marker on dig panel:
if self.qst_m_type ~= my_type_speak and self.biomeid then
kui.canvas.game.dig.questmark:show()
kui.canvas.game.dig.questmark:setfp(fp.c, fp.tl, kui.canvas.game.dig.card[self.biomeid].bd, kui:px(14), -kui:px(30))
end
elseif stage == "complete" then
kui.canvas.game.dig.questmark:hide() -- (here twice for dev skipping)
CreateMinimapIconBJ(utils.unitx(self.endunit), utils.unity(self.endunit), 55, 255, 55, "UI\\Minimap\\MiniMap-QuestGiver.mdl", FOG_OF_WAR_MASKED)
self.markerend = GetLastCreatedMinimapIcon()
elseif stage == "turnedin" then
kui.canvas.game.dig.questmark:hide()
-- if self.markerstart then DestroyMinimapIcon(self.markerstart) self.markerstart = nil end
-- if self.markerend then DestroyMinimapIcon(self.markerend) self.markerend = nil end
end
end
function quest:refreshmapmarker()
utils.timed(3.0, function()
-- because changing map bounds bugs out map icons, refresh them after scoreboard.
if self.markerstart then
DestroyMinimapIcon(self.markerstart)
CreateMinimapIconBJ(utils.unitx(self.startunit), utils.unity(self.startunit), 255, 225, 0, "UI\\Minimap\\MiniMap-QuestGiver.mdl", FOG_OF_WAR_MASKED)
end
if self.markerend then
DestroyMinimapIcon(self.markerend)
CreateMinimapIconBJ(utils.unitx(self.endunit), utils.unity(self.endunit), 55, 255, 55, "UI\\Minimap\\MiniMap-QuestGiver.mdl", FOG_OF_WAR_MASKED)
end
end)
end
function quest:clearprogress()
BlzFrameSetText(self.parentfr.maxfr.textfr.fh, '')
BlzFrameSetText(self.parentfr.minfr.titlefr.fh, '')
end
function quest:getdescription()
local biomename = ''
if self.biomeid then
biomename = color:wrap(color.tooltip.alert, map.biomemap[self.biomeid])
end
if self.qst_m_type == m_type_speak then
self.description = "Speak with "..GetUnitName(self.endunit)
elseif self.qst_m_type == m_type_monster_hunt then
self.description = "Defeat treasure guardians in|n"..biomename
elseif self.qst_m_type == m_type_candle_heist then
self.description = "Steal from Dark Kobolds in|n"..biomename
elseif self.qst_m_type == m_type_boss_fight then
self.description = "Conjure a dig site key and defeat "..color:wrap(color.tooltip.alert, self.bossname).." in "..biomename
elseif self.qst_m_type == m_type_gold_rush then
self.description = "Protect mining efforts in|n"..biomename
end
end
function quest:begin(_skipspeak)
alert:new("New Quest: "..color:wrap(color.tooltip.alert, self.name), 5)
-- on start func if present:
if self.startfunc then
self:startfunc()
end
utils.playsoundall(kui.sound.queststart)
if self.startchain and not _skipspeak then
screenplay:run(self.startchain)
end
quest.inprogress = true
self:updatemapmarker("inprogress")
self.cmfr:hide()
-- if speech quest, mark objective complete:
if self.qst_m_type == m_type_speak then
self:markprogress(true)
else
self:markprogress(false)
end
-- clear quest markers:
if self.eyecandystart then
BlzSetSpecialEffectAlpha(self.eyecandystart, 0)
DestroyEffect(self.eyecandystart)
end
end
function quest:finish(_skipspeak)
alert:new(color:wrap(color.tooltip.good, "Quest Completed")..': '..color:wrap(color.tooltip.alert, self.name))
TriggerRemoveAction(self.trig, self.action)
-- complete current quest and give awards:
utils.playsoundall(kui.sound.questdone)
utils.playerloop(function(p)
if self.lvlaward > 0 then
kobold.player[p]:addlevel(self.lvlaward)
else
kobold.player[p]:awardxp(self.xpaward)
end
if not freeplay_mode then
kobold.player[p].char:save_character()
end
end)
if self.awardfunc then
self.awardfunc()
end
-- play completion dialogue:
if self.endchain and not _skipspeak then
screenplay:run(self.endchain)
end
self:clearprogress()
-- queue up next quest start:
if self.nextquest then
self.nextquest:activate()
elseif self.lineage[self.id + 1] then
self.nextquest = self.lineage[self.id + 1]
self.nextquest:activate()
else
self.current = nil
end
-- clear quest markers:
if self.eyecandyfinish then
BlzSetSpecialEffectAlpha(self.eyecandyfinish, 0)
DestroyEffect(self.eyecandyfinish)
end
-- reset completion-related frames:
quests_total_completed = quests_total_completed + 1
-- story completion badge:
if quests_total_completed >= 15 and kobold.player[Player(0)].badgeclass[badge:get_class_index(12, kobold.player[Player(0)].charid)] == 0 then
badge:earn(kobold.player[Player(0)], 12, kobold.player[Player(0)].charid)
end
quest.inprogress = false
self:updatemapmarker("turnedin")
self.parentfr.maxfr:hide()
end
function quest:addloot(rarityidmin, rarityidmax, _slottype, _count)
utils.debugfunc(function()
utils.playerloop(function(p)
for i = 1,_count or 1 do
local rarityid = math.random(rarityidmin, rarityidmax)
local slotid = _slottype or loot:getrandomslottype()
loot:generate(p, kobold.player[p].level, slotid, rarityid)
ArcingTextTag("+"..color:wrap(color.tooltip.good, "Loot"), kobold.player[p].unit)
end
end)
utils.playsoundall(kui.sound.tutorialpop)
end, "quest:getloot")
end
-- add @val of a random or specificed @_oreid.
-- pass @_starterflag flag to override and make it physical (for starter quests).
function quest:addoreaward(val, _oreid, _starterflag)
utils.playerloop(function(p)
if kobold.player[p] then
if not _oreid and _starterflag then
kobold.player[p]:awardoretype(ore_phys, val, false)
elseif _oreid then
kobold.player[p]:awardoretype(_oreid, val, false)
else
kobold.player[p]:awardoretype(math.random(1,6), val, false)
end
ArcingTextTag("+"..color:wrap(color.tooltip.good, "Ore"), kobold.player[p].unit)
end
end)
end
function quest:addgoldaward(val)
utils.playerloop(function(p)
if kobold.player[p] then
kobold.player[p]:awardgold(val)
end
ArcingTextTag("+"..color:wrap(color.ui.gold, "Gold"), kobold.player[p].unit)
end)
end
function quest:addfragaward(val)
utils.playerloop(function(p)
if kobold.player[p] then
kobold.player[p]:awardfragment(val)
end
end)
end
-- disables the current quest and stops further quest progression.
function quest:disablequests()
if quest.current then
quest.current.collectable = false
DestroyEffect(quest.current.eyecandystart)
if quest.current.markerstart then DestroyMinimapIcon(quest.current.markerstart) quest.current.markerstart = nil end
if quest.current.markerend then DestroyMinimapIcon(quest.current.markerend) quest.current.markerend = nil end
quest.current = nil
end
end
function quest:load_current()
-- loads the current quest in the quest chain.
if self.lineage[quests_total_completed+1] then
self.lineage[quests_total_completed+1]:activate()
end
end
function quest:dataload()
-- boss order:
--[[
Slag
Mire
Fossil
Ice
Vault
]]
-- quest flags:
quest_shinykeeper_unlocked = false
quest_shinykeeper_upgrade_1 = false -- shinies for dummies, +1 affix roll and sometimes rolls a secondary.
quest_shinykeeper_upgrade_2 = false -- can now roll epics.
quest_elementalist_unlocked = false
quest_elementalist_upgrade_1 = false -- adds guaranteed secondary rolls.
quest_elementalist_upgrade_2 = false -- can now roll epics.
quest_greywhisker_unlocked = false
-- build map quests:
-- actors[1] = Kobold
-- actors[2] = Shinykeeper
-- actors[3] = Dig Master
-- actors[4] = Elementalist
-- actors[5] = Narrator
-- actors[6] = Grog
-- actors[7] = Slog
-- actors[8] = Greywhisker
quest[1] = quest:createspeakquest(
"Greenwhisker",
screenplay.chains.quest[1].start,
screenplay.chains.quest[1].finish,
udg_actors[1],
udg_actors[2],
75)
quest[1].awardfunc = function()
quest:addloot(rarity_common, rarity_common, slot_helmet)
quest:addloot(rarity_common, rarity_common, slot_candle)
quest:addloot(rarity_common, rarity_common, slot_outfit)
end
----------------------------------
quest[2] = quest:createdigquest(
"Miner's Trove",
nil,
screenplay.chains.quest[2].start,
screenplay.chains.quest[2].finish,
udg_actors[3],
udg_actors[3],
225)
quest[2].awardfunc = function()
quest:addloot(rarity_common, rarity_common, slot_tool)
quest:addloot(rarity_common, rarity_common, slot_boots)
quest:addloot(rarity_common, rarity_common, slot_backpack)
end
----------------------------------
quest[3] = quest:createdigquest(
"Hammer Time",
nil,
screenplay.chains.quest[3].start,
screenplay.chains.quest[3].finish,
udg_actors[2],
udg_actors[2],
225)
quest[3].awardfunc = function()
placeproject('shiny1')
quest_shinykeeper_unlocked = true
badge:earn(kobold.player[Player(0)], 3)
self.socialfr.shinshop:show()
self:addgoldaward(100)
end
----------------------------------
quest[4] = quest:createdigquest(
"Rock Your Socks",
nil,
screenplay.chains.quest[4].start,
screenplay.chains.quest[4].finish,
udg_actors[4],
udg_actors[4],
300)
quest[4].awardfunc = function()
placeproject('ele1')
quest_elementalist_unlocked = true
badge:earn(kobold.player[Player(0)], 4)
self.socialfr.eleshop:show()
self:addoreaward(10, nil, true)
self:addgoldaward(100)
quest.socialfr.fragshop:show()
end
----------------------------------
quest[5] = quest:createspeakquest(
"Greywhisker",
screenplay.chains.quest[5].start,
screenplay.chains.quest[5].finish,
udg_actors[3],
udg_actors[8],
100)
quest[5].startfunc = function()
utils.playerloop(function(p)
if utils.localp() == p then
quest.socialfr.fragshop:show()
shop.gwfr.card[2].icon:show()
end
end)
end
quest[5].awardfunc = function()
quest_greywhisker_unlocked = true
badge:earn(kobold.player[Player(0)], 5)
quest:addfragaward(5)
end
----------------------------------
quest[6] = quest:createbossquest(
"Reign of Fire",
m_type_boss_fight,
"The Slag King",
"N01B",
screenplay.chains.quest[6].start,
screenplay.chains.quest[6].finish,
udg_actors[3],
udg_actors[3],
300)
quest[6].startfunc = function()
utils.playerloop(function(p)
if utils.localp() == p then
shop.gwfr.card[2].icon:show()
end
end)
end
quest[6].awardfunc = function()
placeproject('boss1')
self:addgoldaward(100)
quest:addloot(rarity_rare, rarity_rare, loot:getrandomslottype(), 1)
end
----------------------------------
quest[7] = quest:createdigquest(
"Scribble Rat",
nil,
screenplay.chains.quest[7].start,
screenplay.chains.quest[7].finish,
udg_actors[2],
udg_actors[2],
375)
quest[7].awardfunc = function()
placeproject('shiny2')
quest_shinykeeper_upgrade_1 = true
quest:addloot(rarity_common, rarity_common, slot_boots)
end
----------------------------------
quest[8] = quest:createdigquest(
"Crystal Clear",
nil,
screenplay.chains.quest[8].start,
screenplay.chains.quest[8].finish,
udg_actors[4],
udg_actors[4],
375)
quest[8].awardfunc = function()
placeproject('ele2')
quest_elementalist_upgrade_1 = true
self:addgoldaward(100)
for i = 1,6 do self:addoreaward(10, i, true) end -- give 10 of every ore type.
end
----------------------------------
quest[9] = quest:createbossquest(
"Marsh Madness",
m_type_boss_fight,
"Marsh Mutant",
"N01F",
screenplay.chains.quest[9].start,
screenplay.chains.quest[9].finish,
udg_actors[3],
udg_actors[3],
375)
quest[9].startfunc = function()
utils.playerloop(function(p)
if utils.localp() == p then
shop.gwfr.card[3].icon:show()
end
end)
end
quest[9].awardfunc = function()
placeproject('boss2')
quest:addloot(rarity_rare, rarity_rare, loot:getrandomslottype(), 1)
end
----------------------------------
quest[10] = quest:createdigquest(
"Epicness, Pt. 1",
nil,
screenplay.chains.quest[10].start,
screenplay.chains.quest[10].finish,
udg_actors[2],
udg_actors[2],
375)
quest[10].awardfunc = function()
placeproject('shiny3')
quest_shinykeeper_upgrade_2 = true
quest:addloot(rarity_rare, rarity_rare, slot_artifact)
quest:addloot(rarity_rare, rarity_rare, slot_potion)
end
----------------------------------
quest[11] = quest:createdigquest(
"Epicness, Pt. 2",
nil,
screenplay.chains.quest[11].start,
screenplay.chains.quest[11].finish,
udg_actors[4],
udg_actors[4],
375)
quest[11].awardfunc = function()
placeproject('ele3')
quest_elementalist_upgrade_2 = true
quest:addloot(rarity_rare, rarity_rare, slot_backpack)
end
----------------------------------
quest[12] = quest:createbossquest(
"Mawrific",
m_type_boss_fight,
"Megachomp",
"N01H",
screenplay.chains.quest[12].start,
screenplay.chains.quest[12].finish,
udg_actors[3],
udg_actors[3],
375)
quest[12].startfunc = function()
utils.playerloop(function(p)
if utils.localp() == p then
shop.gwfr.card[1].icon:show()
end
end)
self:addgoldaward(150)
end
quest[12].awardfunc = function()
placeproject('boss3')
quest:addloot(rarity_rare, rarity_rare, loot:getrandomslottype(), 1)
end
----------------------------------
quest[13] = quest:createdigquest(
"Let's Go Clubin'",
nil,
screenplay.chains.quest[13].start,
screenplay.chains.quest[13].finish,
udg_actors[7],
udg_actors[6],
375,
nil,
biome_id_ice)
quest[13].awardfunc = function()
self:addgoldaward(150)
end
----------------------------------
quest[14] = quest:createbossquest(
"Not Very Ice",
m_type_boss_fight,
"Thawed Experiment",
"N01J",
screenplay.chains.quest[14].start,
screenplay.chains.quest[14].finish,
udg_actors[3],
udg_actors[3],
375)
quest[14].startfunc = function()
speak.showskip = false
utils.playerloop(function(p)
if utils.localp() == p then
shop.gwfr.card[4].icon:show()
end
end)
end
quest[14].awardfunc = function()
placeproject('boss4')
quest:addloot(rarity_rare, rarity_rare, loot:getrandomslottype(), 1)
end
----------------------------------
quest[15] = quest:createbossquest(
"Greed Is Good",
m_type_boss_fight,
"The Guardian",
"N01R",
screenplay.chains.quest[15].start,
screenplay.chains.quest[15].finish,
udg_actors[8],
udg_actors[3],
750)
quest[15].startfunc = function()
utils.playerloop(function(p)
if utils.localp() == p then
shop.gwfr.card[5].icon:show()
end
end)
kui.canvas.game.dig.mappiece:show()
kui.canvas.game.dig.card[5]:show()
alert:new("New Zone Unlocked: "..color:wrap(color.tooltip.good, "The Vault"), 4.0)
end
quest[15].awardfunc = function()
speak.showskip = false
self:addgoldaward(300)
utils.playerloop(function(p)
for i = 1,6 do self:addoreaward(100, i, false) end
end)
end
end
char = {}
char.__index = char
--
char.newseed = "1252222224111111253412555555451234512341235444444234" -- code won't load properly if tinkered with. our encrypt is very dumb, can't have a value over 5.
--
char.abil = {
[1] = "AHhb",
[2] = "AHds",
[3] = "AHre",
[4] = "AHad",
[5] = "AHtc",
[6] = "AHtb",
[7] = "AHbh",
[8] = "AHav",
[9] = "AHfs",
[10] = "AHbn",
[11] = "AHdr",
[12] = "AHpx",
[13] = "AHbz",
[14] = "AHab",
[15] = "AHwe",
[16] = "AHmt",
[17] = "AOsh",
[18] = "AOae",
[19] = "AOre",
[20] = "AOws",
}
char.charcode = {
[1] = 'H000', -- tunneler.
[2] = 'H00E', -- geomancer.
[3] = 'H00G', -- rascal.
[4] = 'H00F', -- wickfighter.
}
-- creates a new object representing a character (for all saving/loading; initialized on init for all players).
function char:new(p)
local o = {}
setmetatable(o, self)
o.data = {}
o.pobj = kobold.player[p]
return o
end
-- updates a char object to save an existing character.
function char:new_save()
self.data = {}
-- data strings:
self:build_save_data()
self:build_meta_string()
self:build_equip_string()
self:build_inv_string()
self:build_mast_string()
self:build_full_string()
return self
end
--[[
----------------------------------------------------------------
------------------------ SAVE FUNCTIONS ------------------------
----------------------------------------------------------------
--]]
function char:save_character()
-- existing slot already loaded, automate the overwrite:
if self.fileslot then
local fileslot = self.fileslot
self:new_save()
self.fileslot = fileslot
self:build_char_file(self.pobj.p)
-- see if a slot is already free:
else
local emptyslotfound = false
for fileslot = 1,4 do
if not self.pobj.hasfile[fileslot] then
-- found a file, initiate save:
self:new_save()
self.fileslot = fileslot
self:build_char_file(self.pobj.p)
emptyslotfound = true
break
end
end
if not emptyslotfound then
utils.palert(self.pobj.p, "No slots available!|nSelect one to delete", 3.0)
if self.pobj:islocal() then
kui:closeall()
kui.canvas.game.overwrite:show()
end
end
end
end
function char:build_meta_string()
self.data.meta = self.pobj.charid.."|"..self.pobj.level.."|"..self.pobj.experience.."|"
..self.pobj[p_stat_strength].."|"..self.pobj[p_stat_wisdom].."|"..self.pobj[p_stat_alacrity].."|"..self.pobj[p_stat_vitality].."|"
..self.pobj.gold.."|"
for oreid = 1,6 do
self.data.meta = self.data.meta..self.pobj.ore[oreid].."|"
end
self.data.meta = self.data.meta..self.pobj.attrpoints.."|"..self.pobj.mastery.points.."|"..quests_total_completed.."|"..self.pobj.ancientfrag
-- store abilities:
self.data.abil = ""
-- store main skills:
for row = 1,3 do
for col = 1,4 do
if self.pobj.ability.map[row][col] then
self.data.abil = self.data.abil..row.."/"..col..","
end
end
end
self.data.abil = utils.purgetrailing(self.data.abil, ",").."|"
-- store mastery skills:
if self.pobj.ability.savedmastery then
for slot = 1,4 do
if self.pobj.ability.savedmastery[slot] then
self.data.abil = self.data.abil..self.pobj.ability.savedmastery[slot]..","
else
self.data.abil = self.data.abil.."empty,"
end
end
end
self.data.abil = utils.purgetrailing(self.data.abil, ",")
self.data.abil = utils.purgetrailing(self.data.abil, "|") -- if empty mastery, purge empty field.
self.data.meta = self.data.meta.."|"..self.data.abil
return self.data.meta
end
function char:build_mast_string()
self.data.mast = ""
for row = 1,self.pobj.mastery.nodemaxy do
for col = 1,self.pobj.mastery.nodemaxx do
self.data.mast = self.data.mast..utils.booltobit(self.pobj.mastery[row][col])
end
self.data.mast = self.data.mast..","
end
self.data.mast = utils.purgetrailing(self.data.mast, ",")
return self.data.mast
end
function char:build_equip_string()
self.data.equip = ""
for slotid = 1001,1011 do
if self.pobj.items[slotid] then
self.data.equip = self.data.equip..self:build_item_string(self.pobj.items[slotid])
end
end
self.data.equip = utils.purgetrailing(self.data.equip, ",")
return self.data.equip
end
function char:build_inv_string()
self.data.inv = ""
for slotid = 1,42 do
if self.pobj.items[slotid] then
self.data.inv = self.data.inv..self:build_item_string(self.pobj.items[slotid])
end
end
self.data.inv = utils.purgetrailing(self.data.inv, ",")
return self.data.inv
end
function char:build_item_string(item)
-- structure: 'itemtypeid|ilvl|sellsfor|modifierid|kwid=kwroll[/kwid2=kwroll2.../kwid3=kwroll3...]'
local str = ""
str = str..item.itemtype.id.."|"
str = str..item.level.."|"
str = str..item.sellsfor.."|"
str = str..item.modifierid.."|"
str = str..item.rarity.id.."|"
-- gather kw ids, statmod ids, rolled values:
if utils.tablesize(item.kw) > 0 then
for kw_type,kw in pairs(item.kw) do
str = str..kw.id.."="..item.kw_roll[kw_type].."/"
end
else
str = utils.purgetrailing(str, "|") -- purge kw field delimiter since empty.
end
str = utils.purgetrailing(str, "/")
str = str..","
return str
end
function char:build_full_string()
self.data.full = self.data.save.."*"..self.data.meta.."*"..self.data.equip.."*"..self.data.inv.."*"..self.data.mast
return self.data.full
end
function char:build_save_data()
self.data.save = kui.meta.charname[self.pobj.charid].."|"..self.newseed
end
--[[
----------------------------------------------------------------
------------------------ FILE FUNCTIONS ------------------------
----------------------------------------------------------------
--]]
function char:build_char_file(p)
char:reset_abils()
-- because of the stupid 259 character limit in a preload line, we must break each string into groups.
PreloadGenClear()
PreloadGenStart()
local chunk_size = 200
local pos, posend = 0, 0
local chunk_count = math.ceil(string.len(self.data.full)/chunk_size)
self.data.full = char:basic_encrypt(char.newseed, self.data.full, true)
if chunk_count > 1 then
for chunk = 1,chunk_count do
pos = 1 + ((chunk-1)*chunk_size)
posend = chunk*chunk_size
if chunk < chunk_count then -- no chunk validation indicator.
Preload("\")\ncall BlzSetAbilityTooltip('"..char.abil[chunk].."',\""..string.sub(self.data.full, pos, posend).."\",".."0"..")\n//")
elseif chunk == chunk_count then
Preload("\")\ncall BlzSetAbilityTooltip('"..char.abil[chunk].."',\""..string.sub(self.data.full, pos, posend).."\",".."0"..")\nreturn//")
end
end
else
-- we should rarely need this:
Preload("\")\ncall BlzSetAbilityTooltip('"..char.abil[1].."',\""..self.data.full.."\",".."0"..")\nreturn//")
end
PreloadGenEnd("KoboldKingdom\\char"..tostring(self.fileslot)..".txt")
if p then
utils.palert(p, "Character Saved!", 2.0, true)
end
-- save badges:
utils.timed(0.06, function()
badge:save_data(kobold.player[p])
end)
end
function char:read_file(fileslot)
-- reads a file into ability tooltips.
self:reset_abils()
utils.timed(0.06,function()
-- sometimes tooltips don't update in time (unknown cause). so we add a delay:
PreloadStart()
Preload("")
Preloader("KoboldKingdom\\char"..tostring(fileslot)..".txt")
Preload("")
PreloadEnd(0.0)
end)
return true
end
function char:delete_file(fileslot)
PreloadGenClear()
PreloadGenStart()
Preload("")
PreloadGenEnd("KoboldKingdom\\char"..tostring(fileslot)..".txt")
utils.palert(self.pobj.p, "Character Deleted!", 3.0, true)
self.pobj.hasfile[fileslot] = false
end
function char:validate_fileslots()
-- sees which files have data to enable UI selection in main menu.
if not self.pobj.hasfile then self.pobj.hasfile = {} end
for fileslot = 1,4 do
utils.timed(fileslot*0.21, function()
self:read_file(fileslot)
utils.timed(0.16, function()
utils.debugfunc(function()
local tempdata = self:get_file_data()
self.pobj.hasfile[fileslot] = true
for i = 1,5 do
if not tempdata[i] then
self.pobj.hasfile[fileslot] = false
break
end
end
if self.pobj.hasfile[fileslot] then
local savedata = self:load_savedata(tempdata[1])
local metadata = self:load_metadata(tempdata[2], true)
local charname = savedata[1]
local charid = tonumber(metadata[1])
local charlevel = tonumber(metadata[2])
kui.canvas.splash.options.loadbtn.btn.disabled = false
kui.canvas.splash.options.loadbtn:setnewalpha(255)
kui.canvas.splash.loadmf.char[fileslot]:setbgtex(kui.tex.char[charid].selectchar)
kui.canvas.splash.loadmf.char[fileslot]:show()
kui.canvas.splash.loadmf.char[fileslot].txt:settext(
color:wrap(color.tooltip.alert, charname).."|nLevel "..color:wrap(color.tooltip.good, charlevel)
)
kui.canvas.game.overwrite.delete[fileslot].txt:settext(
"Slot "..fileslot.." | "..color:wrap(color.tooltip.alert, charname).."| Level "..color:wrap(color.tooltip.good, charlevel)
)
savedata = nil
end
tempdata = nil
end, "file validation timer")
end)
end)
end
end
function char:get_file_data()
-- gets data from loaded file and returns it as a table (also stores as self.dataload)
local datastr, d = "", ""
for _,abilid in ipairs(char.abil) do
d = BlzGetAbilityTooltip(FourCC(abilid), 0)
if d ~= "_" then
datastr = datastr..d
end
end
local decrypted = char:basic_encrypt(char.newseed, datastr, false)
self.dataload = utils.split(decrypted, "*")
char:reset_abils()
return self.dataload
end
function char:load_character()
-- builds a character from the loaded file data.
self.pobj.isloading = true
utils.timed(0.06, function()
self:build_loaded_string()
end)
end
function char:print_data()
for i,v in ipairs(self.dataload) do
print("data",i,"=",v)
end
end
function char:reset_abils()
for _,abilid in ipairs(char.abil) do
BlzSetAbilityTooltip(FourCC(abilid), "_", 0)
end
end
function char:basic_encrypt(seed, str, isencrypt)
-- does the dumbest encrypt ever to discourage players from doing weird stuff to files on a whim.
local bytes, bytesmin, bytesmax = 0, 38, 126
local spos, opos = 1, 1
local smax, omax = string.len(seed), string.len(str)
local nstr, ochar = "", ""
for i = 1,omax do
ochar = string.sub(str, opos, opos)
bytes = string.byte(ochar)
bytesoff = tonumber(string.sub(seed, spos, spos))
if isencrypt then
bytes = bytes - bytesoff
else
bytes = bytes + bytesoff
end
spos = spos + 1
opos = opos + 1
if spos > smax then
spos = 1
end
nstr = nstr..string.char(bytes)
end
return nstr
end
--[[
----------------------------------------------------------------
------------------------ LOAD FUNCTIONS ------------------------
----------------------------------------------------------------
--]]
function char:build_loaded_string()
-- merge all chunk data together and build categorical table.
local data = self:get_file_data()
-- split each data group into workable rows:
-- validate ordered data:
for i = 1,5 do
if not data[i] or data[i] == "" then
data[i] = nil
end
end
self:load_loaded_string(data)
end
function char:load_loaded_string(data)
-- iterate through load steps.
local sequence = {}
sequence[#sequence + 1] = function() if data[1] then self:load_savedata(data[1]) end end
sequence[#sequence + 1] = function() if data[2] then self:load_metadata(data[2]) end end
sequence[#sequence + 1] = function() if data[3] then self:load_equipment(data[3]) end end
sequence[#sequence + 1] = function() if data[4] then self:load_inventory(data[4]) end end
sequence[#sequence + 1] = function() if data[5] then self:load_mastery(data[5]) utils.restorestate(self.pobj.unit) end end
sequence[#sequence + 1] = function() badge:sync_past_achievements(self.pobj) end
sequence[#sequence + 1] = function() self:load_wrapup() end
for i = 1,#sequence do
utils.timed(0.35*i, function() utils.debugfunc(function() sequence[i]() end, "load_loaded_string, function"..tostring(i)) end)
end
end
function char:load_savedata(str)
-- field map: charname, code version
local t = utils.split(str, "|")
self.seed = t[2]
return t
end
function char:load_equipment(str)
local t = utils.split(str, ",")
for i,itemstr in ipairs(t) do
self:load_item(itemstr, true)
end
utils.timed(10.0, function() t = nil end) -- because we do a delay for sync'd data, do a hacky wait before cleanup.
end
function char:load_inventory(str)
local t = utils.split(str, ",")
for i,itemstr in ipairs(t) do
self:load_item(itemstr, false)
end
t = nil
end
function char:load_item(str, _isequip)
-- field map: itemtype.id, level, sellsfor, rarityid, modifierid
-- @_isequip = build in slot 1 and then equip the item (equipped item was saved).
-- e.g. 58|15|26|0|1=11/3=3
local t = utils.split(str, "|")
local kw_type, kw_id = 0, 0
-- rebuild base item:
local itemtypeid = tonumber(t[1])
-- see if item is a dig key or normal item:
if utils.tablehasindex(loot.digkeyt, itemtypeid) then
loot:generatedigkey(self.pobj.p, 1, loot.digkeyt[itemtypeid])
if _isequip then
loot.item:equip(self.pobj.p, 1)
end
else
local newitem = loot.item:new(loot.itemtype.stack[itemtypeid], self.pobj.p)
newitem.level = tonumber(t[2])
newitem.sellsfor = tonumber(t[3])
newitem.modifierid = tonumber(t[4])
newitem.rarity = loot.raritytable[tonumber(t[5])]
-- fetch kw if present:
if t[6] and t[6] ~= "" then
-- kw field map:
local kwt = utils.split(t[6], "/")
-- fetch all stored keywords:
for _,kwdata in ipairs(kwt) do
-- fetch all kw metadata (id and rolled value):
local kwv = utils.split(kwdata, "=")
-- kwv[1] == kw_id; kwv[2] == kw_roll
kw_id = tonumber(kwv[1])
kw_type = loot.kw.stack[kw_id].kw_type
newitem.kw[kw_type] = loot.kw.stack[kw_id]
newitem.kw_roll[kw_type] = tonumber(kwv[2])
kwv = nil
end
kwt = nil
end
if loot.itemtype.stack[itemtypeid].ancientid then
newitem:buildancientname(loot.itemtype.stack[itemtypeid].ancientid, tonumber(t[4]))
newitem.ancientid = loot.itemtype.stack[itemtypeid].ancientid
else
newitem:buildname()
end
newitem:giveto(self.pobj.p)
if _isequip then
loot.item:equip(self.pobj.p, 1) -- built in slot 1, so we equip from 1.
end
end
t = nil
end
function char:load_metadata(str, _isread)
-- field map: charid, level, xp, p_stat_strength, p_stat_wisdom, p_stat_alacrity, p_stat_vitality, gold, ore1, ore2, ore3, ore4, ore5, ore6, attrpoints, masterypoints
local t = utils.split(str, "|")
if not _isread then
self.pobj.loadchar = true
BlzSendSyncData("classpick", tostring(utils.pnum(self.pobj.p))..t[1]..char.charcode[tonumber(t[1])])
utils.timed(0.33, function()
utils.debugfunc(function()
self.pobj:addlevel(tonumber(t[2])-1, true, true)
self.pobj:awardxp(tonumber(t[3]))
self.pobj:modstat(p_stat_strength, true, tonumber(t[4]))
self.pobj:modstat(p_stat_wisdom, true, tonumber(t[5]))
self.pobj:modstat(p_stat_alacrity, true, tonumber(t[6]))
self.pobj:modstat(p_stat_vitality, true, tonumber(t[7]))
self.pobj:awardgold(tonumber(t[8]))
for i = 9,14 do
self.pobj.ore[i-8] = tonumber(t[i])
end
self.pobj:addattrpoint(tonumber(t[15]))
self.pobj.mastery:addpoint(tonumber(t[16]))
self:load_quest_progress(tonumber(t[17]))
self.pobj:awardfragment(tonumber(t[18]), true)
self.tempabilstr = t[19] -- mastery and ability data must load at the end, so store in temp fields:
self.tempmaststr = t[20]
-----------------------------------------------------------------------------------------
------------ post-launch data must go below and be wrapped in 'if' checks ---------------
-----------------------------------------------------------------------------------------
t = nil
end, "set metadata to player")
end)
end
if _isread then
return t
end
end
function char:load_abilities(str)
if str then
local t = utils.split(str, ",")
for _,abilstr in ipairs(t) do
if abilstr then
local rowcol = utils.split(abilstr, "/")
self.pobj.ability:learn(tonumber(rowcol[1]), tonumber(rowcol[2]))
rowcol = nil
end
end
t = nil
end
end
function char:load_masteries(str)
if str then
local t = utils.split(str, ",")
for _,abilcode in ipairs(t) do
if abilcode and abilcode ~= "empty" then
self.pobj.ability:learnmastery(abilcode)
end
end
t = nil
end
end
function char:load_quest_progress(completedcount)
quests_total_completed = completedcount
quest:load_current()
-- quest vars:
if completedcount >= 3 then quest_shinykeeper_unlocked = true placeproject('shiny1') if self.pobj:islocal() then quest.socialfr.shinshop:show() end end
if completedcount >= 4 then quest_elementalist_unlocked = true placeproject('ele1') if self.pobj:islocal() then quest.socialfr.eleshop:show() end end
if completedcount >= 5 then quest_greywhisker_unlocked = true placeproject('grey1') if self.pobj:islocal() then quest.socialfr.fragshop:show() end end
if completedcount >= 7 then quest_shinykeeper_upgrade_1 = true placeproject('shiny2') end
if completedcount >= 8 then quest_elementalist_upgrade_1 = true placeproject('ele2') end
if completedcount >= 10 then quest_shinykeeper_upgrade_2 = true placeproject('shiny3') end
if completedcount >= 11 then quest_elementalist_upgrade_2 = true placeproject('ele3') end
-- boss projects:
if completedcount >= 6 then placeproject('boss1') if self.pobj:islocal() then shop.gwfr.card[2].icon:show() end end
if completedcount >= 9 then placeproject('boss2') if self.pobj:islocal() then shop.gwfr.card[3].icon:show() end end
if completedcount >= 12 then placeproject('boss3') if self.pobj:islocal() then shop.gwfr.card[1].icon:show() end end
if completedcount >= 14 then placeproject('boss4') placeproject('boss5')
if self.pobj:islocal() then shop.gwfr.card[4].icon:show() kui.canvas.game.dig.mappiece:show() kui.canvas.game.dig.card[5]:show() end end
if completedcount >= 15 then shop.gwfr.card[5].icon:show() end
end
function char:load_mastery(str)
-- field map: each table value = row of bits; array of bits = col
local t = utils.split(str, ",")
local colmax = 31
local nodeid = 0
for row,datastr in ipairs(t) do
for col = 1,colmax do
if tonumber(string.sub(datastr, col, col)) == 1 then
nodeid = mastery.nodemap[row][col]
if nodeid == -1 then nodeid = 1 end -- center id hack reversion.
self.pobj.mastery[row][col] = true
self.pobj.mastery:verifysurround(self.pobj.mastery:findruneword(row,col))
mastery:addstat(self.pobj, row, col, nodeid == 2)
if self.pobj:islocal() then
kui.canvas.game.mast.node[row][col]:setbgtex(kui.meta.masterypanel[nodeid].texsel)
kui.canvas.game.mast.node[row][col]:setnewalpha(255)
kui.canvas.game.mast.node[row][col].icon:setnewalpha(255)
if not (col == mastery.centerx and row == mastery.centery) then
kui.canvas.game.mast.respec:show()
end
end
end
end
end
self:load_abilities(self.tempabilstr)
self:load_masteries(self.tempmaststr)
self.tempabilstr = nil
self.tempmaststr = nil
t = nil
end
function char:load_wrapup()
-- refresh all UI details that might've been left untouched.
self.pobj:updateorepane()
self.pobj:updateallstats()
shop:updateframes()
tooltip:updatecharpage(self.pobj.p)
self.pobj.isloading = nil
if self.pobj:islocal() then
kui.canvas.game.char.autoattr:hide()
for i = 1,4 do
kui.canvas.game.skill.alerticon[i]:hide()
end
end
end
screenplay = {}
screenplay.chains = {} -- store built screenplay objects.
screenplay.chains.quest = {}
screenplay.pause_camera = false -- stop camera lock features when needed.
screenplay.__index = screenplay
function screenplay:new()
local o = {}
setmetatable(o, self)
return o
end
function screenplay:run(chain)
utils.debugfunc(function()
speak:startscene(chain)
end, "screenplay:run")
end
function screenplay:build()
-- quest lineage id builder:
local id = 0
local idf = function() id = id + 1 return id end
-- uses pre-placed units (will break if they are deleted).
actor_worker = speak.actor:new()
actor_worker:assign(udg_actors[1], { none = 'war3mapImported\\portrait_worker.blp' })
actor_shinykeeper = speak.actor:new()
actor_shinykeeper:assign(udg_actors[2], { none = 'war3mapImported\\portrait_blacksmith.blp' })
actor_digmaster = speak.actor:new()
actor_digmaster:assign(udg_actors[3], { none = 'war3mapImported\\portrait_taskmaster.blp' })
actor_elementalist = speak.actor:new()
actor_elementalist:assign(udg_actors[4], { none = 'war3mapImported\\portrait_gemhoarder.blp' })
actor_narrator = speak.actor:new()
actor_narrator:assign(udg_actors[5], { none = 'war3mapImported\\portrait_narrator.blp' })
actor_grog = speak.actor:new()
actor_grog:assign(udg_actors[6], { none = 'war3mapImported\\portrait_grog.blp' })
actor_slog = speak.actor:new()
actor_slog:assign(udg_actors[7], { none = 'war3mapImported\\portrait_slog.blp' })
actor_greywhisker = speak.actor:new()
actor_greywhisker:assign(udg_actors[8], { none = 'war3mapImported\\portrait_greywhisker.blp' })
--[[
***************************************************************************
******************************* screenplays *******************************
***************************************************************************
item format:
{ text, actor [, emotion, anim, sound, func] }
--]]
screenplay.chains.narrator_intro = speak.chain:build({
[1] = {"...", actor_narrator},
[2] = {"Oh, I didn't see you there.", actor_narrator},
[3] = {"Very well, no need for long introductions. Let's get you on your journey, shall we? I sense your hunger for adventure and treasure.", actor_narrator},
[4] = {"This is the cave of the Tunnel Rat Clan.", actor_narrator},
[5] = {"They've seen... better times. Can you help them reclaim their glory in the depths?", actor_narrator},
[6] = {"Well, what are we waiting for?", actor_narrator},
[7] = {"Use the |c0000f066D|r key to speak to characters in the world. Try it on the kobold marked by a yellow |c0000f066'!'|r.", actor_narrator},
[8] = {"If you ever miss a piece of dialogue or want to re-read it, try the Message Log (|c0000f066F12|r).", actor_narrator},
})
screenplay.chains.narrator_intro_freeplay = speak.chain:build({
[1] = {"Welcome to |c0000f066free play|r mode.", actor_narrator},
[2] = {"This is intended for players who have completed the story, or for those who don't care for it in the first place.", actor_narrator},
[3] = {"In this mode, no story quests are available, and |c00ff3e3esaving is disabled|r.", actor_narrator},
[4] = {"Instead, you start at |c0000f066level 45|r with items and a stash of ore and gold.", actor_narrator},
[5] = {"Additionally, the speed of leveling is greatly accelerated.", actor_narrator},
[6] = {"Explore, run missions, and fight bosses at your leisure. Have fun!", actor_narrator},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"The kobold jumps, not noticing you walk up behind him. How rude.", actor_narrator},
[2] = {"Scrat scrat! Welcome to depths, greenwhisker.", actor_worker},
[3] = {"Why you come? You here for treasure? We have no treasure.", actor_worker},
[4] = {"Big boyos in tunnels keep miners away. Come from old chambers sometimes. Take kobolds. Kobolds never return.", actor_worker},
[5] = {"The kobold examines your torn pants, missing shirt, and mostly-melted candle. And your complete lack of treasure.", actor_narrator},
[6] = {"Ah, you loser like me!", actor_worker},
[7] = {"The kobold bursts into laughter, nearly falling to the floor. Before long, his face freezes as his eyes meet your gaze.", actor_narrator},
[8] = {"Ah, but you different. It clear. You determined, why else you come all the way down?", actor_worker},
[9] = {"I sense Koboltagonist! I see it! Prophecy? Koboltagonist return to save us? Could be? Is! Is be!", actor_worker},
[10] = {"You look at the kobold with peculiar eyes as he babbles incoherently, but it does not deter his excitement.", actor_narrator},
[11] = {"Yes, yes, you save us from big boyos! But, but...", actor_worker},
[12] = {"...but first, see Shinykeeper. Shinykeeper have shinies for you. Your dull shinies no good for Koboltagonist.", actor_worker},
[13] = {"Now go! For Kingdom! Rock and stone, to the bo- err, wrong game. Ahem.", actor_worker},
[14] = {"...", actor_worker},
[15] = {"I can't come up with phrase, I give up. Okay, you go now! Scrat!", actor_worker},
[16] = {"Well, that was fast. The Shinykeeper is just south of you. By the piles of junk.", actor_narrator},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"Strange tinkering noises can be heard as the Shinykeeper rummages through a bag of what is obviously junk.", actor_narrator},
[2] = {"...", actor_shinykeeper},
[3] = {"Baah!", actor_shinykeeper},
[4] = {"Me no see you there!", actor_shinykeeper},
[5] = {"What you want?", actor_shinykeeper},
[6] = {"...", actor_shinykeeper},
[7] = {"Oh, I see. The silent Kolbotagonist type. See what I do there? I break fourth tunnel.", actor_shinykeeper},
[8] = {"...", actor_shinykeeper},
[9] = {"Shinies? I see in your eye. Scrat! Okay, you have some of my shinies, but not all!", actor_shinykeeper},
[10] = {"With reluctance to let go, the Shinykeeper gives you a handful of junk.", actor_narrator},
[11] = {"Wha? You no like? You wear on head, see!", actor_shinykeeper},
[12] = {"The Shinykeeper puts a bucket on his head and lights a pile of dirty wax to demonstrate his mastery, then places it on your head.", actor_narrator},
[13] = {"The follow-up grin is too wide for any sane kobold.", actor_narrator},
[14] = {"See! Shiny fit perfect. Okay, now you go! Dig Master waiting.", actor_shinykeeper},
[15] = {"Don't forget to equip your... new junk. The equipment and inventory pages can be opened with the |c0000f066V|r and |c0000f066B|r keys respectively.", actor_narrator},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"The Dig Master notices your presence, but continues to stare at what resembles a paper map of tunnels.", actor_narrator},
[2] = {"You casually point out to him that the map is upside down.", actor_narrator},
[3] = {"...", actor_digmaster},
[4] = {"Bagh!? A greenwhisker know best, aye?", actor_digmaster},
[5] = {"What appears as anger suddenly fades as The Dig Master bursts out in laughter.", actor_narrator},
[6] = {"By dirt, no one talk like that in long time. You okay to me, greenwhisker. You tough to authority. Definitely Koboltagonist.", actor_digmaster},
[7] = {"We save HARDEST missions for Koboltagonist. You prove worth, okay?", actor_digmaster},
[9] = {"The Dig Master points to his now-correctly-oriented map.", actor_narrator},
[10] = {"Here, you go here. Many shinies left by miners long ago.", actor_digmaster},
[11] = {"You careful though, greenie. There two strong guardians!", actor_digmaster},
[12] = {"You defeat them, yes? Gather shinies? No come back with shinies means no come back at all!", actor_digmaster},
[13] = {"Well, what you wait for? Go fetch shinies!", actor_digmaster},
[14] = {"The Dig Master has given you a quest to complete a dig in a random biome.", actor_narrator},
[15] = {"Use the |c0000f066Tab|r key to open the dig map, or select it with its icon in the lower right. All main interface pages have a menu button there.", actor_narrator},
[16] = {"Inside a dig, your candle light will dwindle over time. The powers of The Darkness will be attracted to you as it fades.", actor_narrator},
[17] = {"|c00fdff6fWax Canisters|r can be acquired within digs from certain creatures or events to replenish your wax capacity.", actor_narrator},
[18] = {"Now, get going. And good luck! Oh, almost forgot. If your candle light goes out |c00ff3e3eThe Darkness|r itself will hunt you. No pressure!", actor_narrator},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"The Dig Master looks confused while flipping his map around in random directions.", actor_narrator},
[2] = {"Greenwhisker! You back, by dirt!", actor_digmaster},
[3] = {"Little faith, but here you! Good, good.", actor_digmaster},
[4] = {"You shuffle through your treasure bag and pull out a recovered item.", actor_narrator},
[5] = {"...", actor_digmaster},
[6] = {"What this? This is junk!", actor_digmaster},
[7] = {"The Dig Master twirls the piece in his hand rapidly while examining it.", actor_narrator},
[8] = {"But, you new and green. We do better next time, yea?", actor_digmaster},
[9] = {"He quickly pockets the junk.", actor_narrator},
[10] = {"Now, go see Shinykeeper. They have new task for you.", actor_digmaster},
[11] = {"You've leveled up after finishing a dig and this quest!", actor_narrator},
[12] = {"If you haven't done so already, browse the new items acquired from your successful dig and see if they're worthy of a 'Koboltagonist'.", actor_narrator},
[13] = {"You should also have character attribute and mastery points available to spend.", actor_narrator},
[14] = {"To begin spending, open the character page with the |c0000f066C|r key, and the mastery page with the |c0000f066N|r key.", actor_narrator},
[15] = {"Use character attributes to boost certain stats. Mastery points offer similar benefits, but can ultimately unlock new abilities deeper in.", actor_narrator},
[16] = {"Spend your earned points wisely, as they are non-refundable!", actor_narrator},
[17] = {"Oh, and one last thing, promise.", actor_narrator},
[18] = {"Abilities unlock every 3 levels. You should have a new one unlocked now. See what it is in your skillbook, which opens with the |c0000f066K|r key.", actor_narrator},
[19] = {"If that was too much, here's a tip: whenever you earn new things, a green alert icon will appear over the menu button in the lower right.", actor_narrator},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"The Shinykeeper appears distraught, running around frantically.", actor_narrator},
[2] = {"Scrat! It gone! Scrat, it gone for good!", actor_shinykeeper},
[3] = {"...", actor_shinykeeper},
[4] = {"Ah, Koboltagonist! You help, yes? Like you help Dig Master.", actor_shinykeeper},
[5] = {"My hammer, it is taken. Taken by filthy tunnel vermin on last dig.", actor_shinykeeper},
[6] = {"Without hammer, cannot make shinies! See...", actor_shinykeeper},
[7] = {"The Shinykeeper attempts to bash a nail in with the tip of an old boot. Kobolds seem to have an affection for shoes.", actor_narrator},
[8] = {"...see, you see! No good!", actor_shinykeeper},
[9] = {"What say you? You hunt bests? Slay them! Return hammer?", actor_shinykeeper},
[10] = {"Yes, yes. You do that, we make and trade shinies!", actor_shinykeeper},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"The Shinykeeper stops removing nails from his boot and looks up to greet you.", actor_narrator},
[2] = {"...", actor_shinykeeper},
[3] = {"How? We thought you dead for good! We thought you turn to slag. But... is that...", actor_shinykeeper},
[4] = {"The hammer disappears from your hands before you can extend it outward.", actor_narrator},
[5] = {"...now! Now we in business!", actor_shinykeeper},
[6] = {"He dances around like a maniac.", actor_narrator},
[7] = {"...", actor_shinykeeper},
[8] = {"Ah yes. Yes, yes! You need any shinies, you see me. Okay, Kolbotagonist?", actor_shinykeeper},
[9] = {"They not powerful. Not yet. Still need more tools. But for now, fill missing shoe? Missing bucket? Scrat! Anything!", actor_shinykeeper},
[10] = {"You now have access to the Shinykeeper's shop outside of digs. Open it by clicking his icon near your skill bar.", actor_narrator},
[11] = {"At this new shop, you can turn your earned gold into items with random features.", actor_narrator},
[12] = {"I suspect the hammer was not the only thing missing. We'll find out soon enough.", actor_narrator},
[13] = {"Well, this is great progress thus far, Koboltagonist. And you haven't event died yet! Surely by now-", actor_narrator},
[14] = {"...well, anyways. The Elementalist was making strange poses earlier. Shall you, ahem, get moving?", actor_narrator},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"Oh, so that's how it will be!?", actor_elementalist},
[2] = {"You?! You! Why you...!", actor_elementalist},
[3] = {"...", actor_elementalist},
[4] = {"The kobold wizard points a finger at a rock while holding up a shoe.", actor_narrator},
[5] = {"By the power of STONE, I transmute and convert thee!", actor_elementalist},
[6] = {"...", actor_elementalist},
[7] = {"Nothing happens.", actor_narrator},
[8] = {"Blast, no good stones! Not again!", actor_elementalist},
[9] = {"The Elementalist turns to you, now pointing his finger at your nose.", actor_narrator},
[10] = {"Kolbotagonist, I can sense it. The rocks speak, and do not lie. They tell of the shinies you bring for the Dig Master.", actor_elementalist},
[11] = {"We need different shinies. We need magical shinies. Can you bring?", actor_elementalist},
[12] = {"Bring me shinies and I will show you the true power of geomancy!", actor_elementalist},
[13] = {"The Elementalist resumes his incantation, this time holding a spoon.", actor_narrator},
[14] = {"You'd best be off, before something terrible happens.", actor_narrator},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"You step over a bent spoon.", actor_narrator},
[2] = {"Ah! Koboltagonist! You bring good stone?", actor_elementalist},
[3] = {"...", actor_elementalist},
[4] = {"The Elementalist takes an imbued rock from your outstretched hand.", actor_narrator},
[5] = {"Good.. good! Glow wax will do wonders on this specimen.", actor_elementalist},
[6] = {"The Elementalist waves his arms around like a lunatic while chanting 'bobbity shmobbity' at his shoe.", actor_narrator},
[7] = {"The Elementalist lowers his hands in disappointment before trying once more.", actor_narrator},
[8] = {"By shroom and fungus! Glow wax too viscose? Candle calibration low? Hand waving not fas-", actor_elementalist},
[9] = {"The shoe bursts in a glow of light, sending the Elementalist back over a table.", actor_narrator},
[10] = {"He peers over the table with a coal-black face save for his two beady eyes.", actor_narrator},
[11] = {"Ahah!", actor_elementalist},
[12] = {"He picks up his shoe from across the cave floor, dancing around in celebration.", actor_narrator},
[13] = {"Today, you do good. You now no longer a greenwhisker, greenwhisker. You ever need magic shoe, you come to me.", actor_elementalist},
[14] = {"Oh, almost forget! Here, you have some stones. We don't need all.", actor_elementalist},
[15] = {"The Elementalist hands you 10 pieces of magical ore. You guessed it right: it's a freebie to test out his new fancy shop.", actor_narrator},
[16] = {"You can |c0000f066craft|r new items at the Elementalist with ore mined on digs. Keep track of your ore count your inventory (|c0000f066B|r).", actor_narrator},
[17] = {"Click his head icon near the center of your skillbar to start making new 'shinies', as they say.", actor_narrator},
[18] = {"Crafting an elemental item will yield a guaranteed damage modifier matching the chosen ore's element type.", actor_narrator},
[19] = {"Additionally, the Elementalist's items will roll slightly higher stats than the Shinykeeper. Less... hammering, is involved.", actor_narrator},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"Koboltagonist!", actor_digmaster},
[2] = {"The Dig Master flips his map around in circles as a fellow kobold scout leaves the table.", actor_narrator},
[3] = {"He steps from his mount and slams the paper on the table.", actor_narrator},
[4] = {"Perfect solution. No fooling. Master plan. Look here.", actor_digmaster},
[5] = {"He points to an assortment of illegible scribbles on his map.", actor_narrator},
[6] = {"...", actor_digmaster},
[7] = {"What? No see?", actor_digmaster},
[8] = {"New tunnel! Or, chamber. Big chamber. But, kept shut by old machine. Requires key.", actor_digmaster},
[9] = {"Elementalist say we need ancient stones to open.", actor_digmaster},
[10] = {"Could be room we heard of long ago.", actor_digmaster},
[11] = {"Uncertain. Risky. But BIG shiny worth all risk! But, for getting ancient stones...", actor_digmaster},
[12] = {"He looks at you and grins.", actor_narrator},
[13] = {"In luck. Elementalist say Greywhisker have stones, stashed away for many winters. He once a Geomancer. Knows ancient magic. Sadly, years numbered.", actor_digmaster},
[14] = {"Here, take to Greywhisker and trade for stones. Ask to use old magic to make key. Perfect trade. No better trade.", actor_digmaster},
[15] = {"He places a giant block of cheese in your hands. A sheet of paper featuring a drawing of a strange jewel is nailed to its top.", actor_narrator},
[16] = {"You look at The Dig Master with a raised brow, your lip half-agape.", actor_narrator},
[17] = {"Well? Why wait? Go get stones! Get key!", actor_digmaster},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"The Greywhisker twists his roast on the fire, not looking up as you approach.", actor_narrator},
[2] = {"You step forward, extending your two open hands with the giant chunk of cheese.", actor_narrator},
[3] = {"The Greywhisker's face does not budge. After a pause, his nose fidgets in your direction with a burst of sniffs.", actor_narrator},
[4] = {"With a sudden clang of metal and stone, he lunges over loose stones and empty pots to stand in front of you.", actor_narrator},
[5] = {"He holds an old cane with twirling designs before meeting a broken tip where a jewel might've been, where he rests a shaking hand.", actor_narrator},
[6] = {"He grunts.", actor_narrator},
[7] = {"You grunt.", actor_narrator},
[8] = {"He grunts again, louder.", actor_narrator},
[9] = {"You raise your hands a little higher.", actor_narrator},
[10] = {"The old kobold uses the tip of his staff to unfold the paper stuck to the block.", actor_narrator},
[11] = {"His white eyes study the paper for a long while. They dart to you occasionally, measuring your attire and pickaxe.", actor_narrator},
[12] = {"In another—surprisingly hasty—dash, he goes to his tent and disappears.", actor_narrator},
[13] = {"There's a clattering of boxes and unfurled bags before he emerges again. He approaches you with a small satchel.", actor_narrator},
[14] = {"From the bag, he pulls a series of glowing stones, each a different color. With precision, he slams a prismatic specimen into his staff.", actor_narrator},
[15] = {"To your amazement, the old kobold manages to let a grin slip.", actor_narrator},
[16] = {"His arm bolts to his his beard of whiskers, rummaging about.", actor_narrator},
[17] = {"From it, he pulls a strange looking slab. It glows orange and emits a strange hum.", actor_narrator},
[18] = {"In the blink of an eye, his hand snatches the chunk of cheese from your hand, leaving the glowing fragment in its place.", actor_narrator},
[19] = {"He grunts a final time before returning to the fire, cutting at the cheese with a broken dagger and placing it atop his roast.", actor_narrator},
[20] = {"When you are ready, young one, come see me. I know this blueprint, from long ago.", actor_greywhisker},
[21] = {"His hand moves to a blackened scar on forearm. His fingers rub at its rough edges.", actor_narrator},
[22] = {"I will make the keys you seek. But, be warned: there is only doom on the other side of the lost chambers. Come to me when you are ready.", actor_greywhisker},
[23] = {"You can now craft |c0047c9ffDig Site Keys|r in exchange for |c00ff9c00Ancient Fragments|r at the Greywhisker.", actor_narrator},
[24] = {"A new icon in the middle of your skillbar allows for easy access. |c00ff9c00Ancient Fragments|r are acquired from certain shrines found within digs.", actor_narrator},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"See, Koboltagonist! Easy.", actor_digmaster},
[2] = {"Cheese never fail.", actor_digmaster},
[3] = {"Now, take new key and open vault.", actor_digmaster},
[4] = {"...", actor_digmaster},
[5] = {"Okay fine. Risk mean reward. We pay you, too. Koboltagonist need gold, too, aye?", actor_digmaster},
[6] = {"Gold and treasure! Yeehehehe-hoo!", actor_digmaster},
[7] = {"...", actor_digmaster},
[8] = {"Okay, go. Best only be you, how else you remain Koboltagonist? Kingdom fall for good without Dig Master.", actor_digmaster},
[9] = {"He smiles to reveal staggered golden teeth.", actor_narrator},
[10] = {"No problem, promise. Easy job! What worst that can happen?", actor_digmaster},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"The sheen of the Slag King's Relic glows bright in the wonderous eyes of the Dig Master.", actor_narrator},
[2] = {"...you... you...", actor_digmaster},
[3] = {"...you did it! By dirt! The Koboltagonist did it! Knew Koboltagonist was Koboltagonist, knew it!", actor_digmaster},
[4] = {"He takes the relic and flails about excitedly before placing it on a giant pedastal.", actor_narrator},
[5] = {"There are several empty pedastals nearby. Your eye pans across them before meeting the Dig Master, who looks at you with a keen brow.", actor_narrator},
[6] = {"Koboltagonist, I see your gaze. You are smart one. Observing. Calculating.", actor_digmaster},
[7] = {"True, this not the last relic to seek.", actor_digmaster},
[8] = {"He places his map on the table. With careful hands, he peels the top of it away to reveal a hidden outline underneath.", actor_narrator},
[9] = {"The new map features a series of colored diamonds: orange, green, red and blue. They each connect via odd shapes to its center, "
.."which is marked by a red 'X'", actor_narrator},
[10] = {"We get all four, from lost chambers. We unlock secret deep within tunnel, far below. Ancient vault. Treasure-filled.", actor_digmaster},
[11] = {"He says the words while shifting a silver chain between his fingers. He then grips it and stands straight.", actor_narrator},
[12] = {"You give a concerned shake of your head.", actor_narrator},
[13] = {"Wha'? Always treasure in center of odd maps and strange doors! Puzzle, aye? What else?", actor_digmaster},
[14] = {"We do this, Koboltagonist, for Kingdom. Must. Necessary. Required.", actor_digmaster},
[15] = {"His confident facade quickly fades and he collapses his shoulders.", actor_narrator},
[16] = {"Scrat! No... Fine! You right. I don't know how to find others. But you, Koboltagonist, have makings of Goldwhisker. Tales for next hundred years!", actor_digmaster},
[17] = {"That why you here, no? But first, see Shinykeeper. To fight more big boyos, need more shinies.", actor_digmaster},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"The Shinykeeper flips through a book while scratching his head. He notices you, placing the book upright on his workbench.", actor_narrator},
[2] = {"It falls open, revealing empty pages.", actor_narrator},
[3] = {"Kolbotagonist, we got problem. See book? You see?", actor_shinykeeper},
[4] = {"He holds the book up. The empty pages fall out and scatter about the floor.", actor_narrator},
[5] = {"No good... no good at all! How can we have shinies with book with no scribbles?", actor_shinykeeper},
[6] = {"You look at him oddly.", actor_narrator},
[7] = {"What? I scribble? I can't! Don't know how. Can only swing HAMMER!", actor_shinykeeper},
[8] = {"The Shinykeeper holds his hammer outward in a daring pose.", actor_narrator},
[9] = {"Now, you go! Get shiny book! Then we make all the shinies!", actor_shinykeeper},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"Ah! You got scribbles!", actor_shinykeeper},
[2] = {"The Shinykeeper snatches the book from your hands and jumps to a page with expert precision.", actor_narrator},
[3] = {"His eyes dart around, observing the details of... another blank page.", actor_narrator},
[4] = {"You look at him confusedly. He shuts the book and grabs his hammer.", actor_narrator},
[5] = {"Tweak the gizmo, slam the doohickey, polish the dinger!", actor_shinykeeper},
[6] = {"Despite the chaotic display, the Shinykeeper emerges with a fancy slab that actually resembles boots.", actor_narrator},
[7] = {"Here, for you, Koboltagonist. And much more coming! You bring gold, I make shinies. Many shinies! UNLIMITED shinies!", actor_shinykeeper},
[8] = {"He returns to his workbench and raises his hammer, only to realize the table is now empty of supplies.", actor_narrator},
[9] = {"Hehe... scrat. Koboltagonist. You got... gold, yes?", actor_shinykeeper},
[10] = {"You have unlocked |c0000f066improved crafting|r at the Shinykeeper.", actor_narrator},
[11] = {"Items sold by the Shinykeeper will roll with an additional stat, and have a chance to roll secondary attributes.", actor_narrator},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"The Elementalist places his latest pair of boots on a carefully arranged pile of other enchanted boots.", actor_narrator},
[2] = {"He stands still for a moment, observing his collection triumphantly with both hands at his hips.", actor_narrator},
[3] = {"Koboltagonist! Timing always perfect, always where needed. Like true Koboltagonist. I have task.", actor_elementalist},
[4] = {"Magic stones are very nice. Yes, very nice, indeed. But, something missing. Not quite right.", actor_elementalist},
[5] = {"He rubs his brow with one hand while pacing slowly. You suspect he might hurt himself in intense thought before he jumps upright.", actor_narrator},
[6] = {"By all that is gold! Of course...", actor_elementalist},
[7] = {"...we missing crystals. Can't have shiniest of shinies with no crystals. Clear rock complement solid rock, yea?", actor_elementalist},
[8] = {"You squint at the suggestion, but he continues before you can interject.", actor_narrator},
[9] = {"You find for me? Crystals help make newer shinies to defeat big boyos.", actor_elementalist},
[10] = {"The Elementalist returns to his collection. A pair of boots slips slightly, which he instantly fixes with a quick gesture and nod.", actor_narrator},
[11] = {"He's clearly... busy. Best we be off.", actor_narrator},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"You slam the crystalline gizmo on the ground in front of the Elementalist.", actor_narrator},
[2] = {"He screeches and jumps back in surprise, hiding behind a stack of debris.", actor_narrator},
[3] = {"He hastily pokes his nose out from the pile of stone, sniffing intensely.", actor_narrator},
[4] = {"My nose... it never lies... could it be...", actor_elementalist},
[5] = {"The Elementalist bolts from behind the rubble, clattering his fingers over the crystal object with intense interest.", actor_narrator},
[6] = {"...pure crystal! Strong infusion, high longevity, minimal energy input required. Maximal output guaranteed.", actor_elementalist},
[7] = {"...", actor_elementalist},
[8] = {"But... red?! Why always red?! Needed clear!", actor_elementalist},
[9] = {"No, no matter. Red will do. Reverse arcane energy stream, fortify amplification process, borrow Shinykeeper hammer. He won't mind.", actor_elementalist},
[10] = {"We begin immediately!", actor_elementalist},
[11] = {"The Elementalist hands you a pile of magical ore.", actor_narrator},
[12] = {"You have unlocked |c0000f066improved crafting|r at the Elementalist.", actor_narrator},
[13] = {"Items crafted by the Elementalist will now roll secondary attributes.", actor_narrator},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"Koboltagonist, news of next chamber! Thanks to Shinykeeper's new book.", actor_digmaster},
[2] = {"The glimpse of empty pages enters your mind. You decide not to dwell on the Shinykeeper's methods. It's probably for the best.", actor_narrator},
[3] = {"Tales speak of big creature in Mire, deep within. Lurking.", actor_digmaster},
[4] = {"Big fish boyo ate adventurer carrying ancient relic...", actor_digmaster},
[5] = {"The Dig Master rubs his belly while grinning.", actor_narrator},
[6] = {"...understand? Of course you understand! You Koboltagonist! You expert! Probably done many times before.", actor_digmaster},
[7] = {"...", actor_digmaster},
[8] = {"Ahem. So, you slay big beastie? Second relic means halfway to vault!", actor_digmaster},
[9] = {"Me? I go? No... no, can't go. See?", actor_digmaster},
[10] = {"He raises his leg, which is wrapped in a bandage, and points with a frown.", actor_narrator},
[11] = {"The Dig Master places it down without effort. You notice an apparent delay before he forces a wince out and clutches it dramatically.", actor_narrator},
[12] = {"He grins with assumed innocence.", actor_narrator},
[13] = {"You look on, unconvinced.", actor_narrator},
[14] = {"*Sigh*... Koboltagonist, I be straight. I too old. Hard to admit, but can barely ride rat for much longer. Greywhisker years approaching.", actor_digmaster},
[15] = {"And Elementalist, he too blind. Shinykeeper? Too cowardly, like most scouts.", actor_digmaster},
[16] = {"But you? You so brave it almost stupid! Already have one relic, what's one more?", actor_digmaster},
[17] = {"What you say?", actor_digmaster},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"The Dig Master notices you approaching and quickly resumes a limping position.", actor_narrator},
[2] = {"You pull the ancient relic from your backpack and place it on the ground. It's fresh with slime.", actor_narrator},
[3] = {"Ugh! Smells!", actor_digmaster},
[4] = {"...", actor_digmaster},
[5] = {"But... so... shiny!", actor_digmaster},
[6] = {"The Dig Master grabs the relic, ripping the bandage from his leg and wiping it clean.", actor_narrator},
[7] = {"He stops, grinning nervously with the relic held above.", actor_narrator},
[8] = {"Ah, such shinies. Make leg better. Hehe...", actor_digmaster},
[9] = {"The Dig Master places the relic on an empty pedastal. It seems to glow slightly brighter when paired with the first.", actor_narrator},
[10] = {"So much closer to vault, Koboltagonist!", actor_digmaster},
[11] = {"Unclear where next relic. I keep eye to ground, yea? Doh! I mean ear! Yes, ear. Or, eye? No, both!", actor_digmaster},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"The Shinykeeper studies his hammer intently as you approach.", actor_narrator},
[2] = {"Koboltagonist, proposition for you.", actor_shinykeeper},
[3] = {"You like shinies, ya?", actor_shinykeeper},
[4] = {"You nod.", actor_narrator},
[5] = {"You like shinies that are more shiny than other shinies, ya?", actor_shinykeeper},
[6] = {"You nod again.", actor_narrator},
[7] = {"The Shinykeeper pulls out his 'scribbles' and looks over a page.", actor_narrator},
[8] = {"It's another empty page. As you tilt your head sideways in typical, confused fashion, he slams the book shut.", actor_narrator},
[9] = {"Design, I have. To make best of all shinies. But, big order coming up for Dig Master. Can't venture to acquire.", actor_shinykeeper},
[10] = {"Requires research. Requires plans. Have plenty of research, but missing blueprint.", actor_shinykeeper},
[10] = {"He doesn't say anything else. Instead, he looks at you in expected, measured silence.", actor_narrator},
[11] = {"...", actor_shinykeeper},
[12] = {"You grab your pickaxe and shoulder it. The Shinykeeper gives a familiar, whacky grin, then returns to hammering at a pile of junk.", actor_narrator},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"You place the discovered 'blueprint' on the Shinykeeper's workbench.", actor_narrator},
[2] = {"He unfurls the document, only to be met with... an empty sheet of paper.", actor_narrator},
[3] = {"Sensing disappointment in the silent stare, you turn to leave...", actor_narrator},
[4] = {"...OF COURSE!", actor_shinykeeper},
[5] = {"He rips the blank blueprint from the table and pins it to a drawing board.", actor_narrator},
[6] = {"The Shinykeeper darts around his workshop, sifting through planks, bolts, cans, shoes, rods and pots.", actor_narrator},
[7] = {"Around you turns to dust in the frenzy, with only the sound of clattering metal and a banging hammer.", actor_narrator},
[8] = {"As the dust settles, the shape of a strange device can be made out in the corner of the room.", actor_narrator},
[9] = {"Test, shall we? Yes, test! What you need... what useful. Hmm. Potions? Artifacts? No problem!", actor_shinykeeper},
[10] = {"He throws a concoction of random junk into the machine, twists dials, turns knobs, inserts gold coins, then pulls a lever.", actor_narrator},
[11] = {"The device roars to life, churning internally with crunching vibrations. As it comes to a halt, two thuds can be heard.", actor_narrator},
[12] = {"Well, what you think? SUPER SHINY!", actor_shinykeeper},
[13] = {"He hands you the two pieces of... well, they're not actually junk anymore. You raise your eyebrows, then nod slowly with satisfaction.", actor_narrator},
[14] = {"Can't always get shinier shinies, but what fun if always shiny?", actor_shinykeeper},
[15] = {"You have unlocked |c0000f066improved crafting|r at the Shinykeeper.", actor_narrator},
[16] = {"Items crafted by the Shinykeeper now have a chance to be |c00c445ffEpic|r in quality.", actor_narrator},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"The Elementalist stares blankly into the infusion crystal.", actor_narrator},
[2] = {"You begin to move forward, but a loud crackle sends the Elementalist flying.", actor_narrator},
[3] = {"He picks himself up and dusts off the black coal front his pant legs.", actor_narrator},
[4] = {"New problem, Koboltagonist. Trying to make better shinies, but crystal not giving in. Overworked. Oversaturated.", actor_elementalist},
[5] = {"Solution, I have.", actor_elementalist},
[6] = {"More crystal! Only way. Twice the crystal, twice the work.", actor_elementalist},
[7] = {"Bring new one, and I show you what is possible.", actor_elementalist},
[8] = {"...", actor_elementalist},
[9] = {"Oh, and not red! Anything but red.", actor_elementalist},
[10] = {"He resumes his incantation in the reflection of the crystalline mirror.", actor_narrator},
[11] = {"You notice singed hairs atop his head, but decide to leave him be as he begins to mumble strange sounds.", actor_narrator},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"The Elementalist removes the crystal from your hands.", actor_narrator},
[2] = {"Bah! Red again!", actor_elementalist},
[3] = {"He looks at you with contempt for a moment before noticing your torn pants, half-melted candle, and bruised arms.", actor_narrator},
[4] = {"Ah, Koboltagonist, forgive. Hard to get. Whining too much. Why strange mage always the one to whine?", actor_elementalist},
[5] = {"Red will do. Stand back.", actor_elementalist},
[6] = {"...", actor_elementalist},
[7] = {"He places the crystal on its new podium and begins a new incantation.", actor_narrator},
[8] = {"You listen intently, placing your backpack on a nearby table and taking a step back, twice as far this time).", actor_narrator},
[9] = {"Before you can object, the Elementalist grabs your backpack from the table.", actor_narrator},
[10] = {"A plume of smoke erupts from the crystal, followed by purple lights.", actor_narrator},
[11] = {"I command thee, backpack, to take on the power of... EPICNESS!", actor_elementalist},
[12] = {"The area clears, and the Elementalist emerges, covered head to toe in black soot.", actor_narrator},
[13] = {"You see! Easy! Can't guarantee every time. But, some of the time: EPICNESS.", actor_elementalist},
[14] = {"You have unlocked |c0000f066improved crafting|r at the Elementalist.", actor_narrator},
[15] = {"Items crafted by the Elementalist now have a chance to be |c00c445ffEpic|r in quality.", actor_narrator},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"The Dig Master stands between the two relics. Occasionally, he pokes at one, but nothing happens.", actor_narrator},
[2] = {"So close, Koboltagonist. Progress bar half full. Or half empty?", actor_digmaster},
[3] = {"Bah, no matter. I sense third chamber is near. Look at what scouts bring to me.", actor_digmaster},
[4] = {"He holds up a massive tooth in his hand, its width double that of his palm.", actor_narrator},
[5] = {"Scouts dug deep into dry tunnels today. We thought nothing down there!", actor_digmaster},
[6] = {"Chamber door open already, by previous adventurer.", actor_digmaster},
[7] = {"Scouts were ambushed! HUGE boyo! Well, haven't seen with own eyes. But, scouts say it looked huge! Smelled huge!", actor_digmaster},
[8] = {"They say, too, that shiny object stuck in back. Part of hide. Shimmering, like them...", actor_digmaster},
[9] = {"He points to the glowing orange relic of the Slag King.", actor_narrator},
[10] = {"...you know what that means, Koboltagonist.", actor_digmaster},
[11] = {"Sadly, beast dragged scout back into chamber. Door shut once more. Not opened again.", actor_digmaster},
[12] = {"Vault so close! Progress bar close to full! No, partly empty!", actor_digmaster},
[13] = {"Go now. But, watch out big teeth.", actor_digmaster},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"You heave the heavy relic onto the pedastal, saving the Dig Master the trouble.", actor_narrator},
[2] = {"As it settles into the grip of its iron holster, a faint ringing can be heard, and each piece glows slightly brighter.", actor_narrator},
[3] = {"Ahhhh...", actor_digmaster},
[4] = {"The Dig Master rubs his hands together and grins.", actor_narrator},
[5] = {"Hopefully not too much trouble. How big beastie? Super big?", actor_digmaster},
[6] = {"He looks at you with curiosity. You're covered head to toe in a layer of thick dirt.", actor_narrator},
[7] = {"Ah, not so bad! Well, at least you okay!", actor_digmaster},
[8] = {"He slams you on the back, the dust flying everywhere.", actor_narrator},
[9] = {"Progress bar very close to done, Koboltagonist. Soon, we unlock greatest treasure in Kingdom history!", actor_digmaster},
[10] = {"Heard of ogres from cold tunnels. Made camp nearby. Rumor of last relic knowledge, but probably need favor.", actor_digmaster},
[11] = {"Try them. I get scouts and lead way to vault. We prepare. We plan for grand opening. Grand party!", actor_digmaster},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"No, it was a dragon!", actor_grog},
[2] = {"Lizard!", actor_slog},
[3] = {"Dragggoooonnnnn!", actor_grog},
[4] = {"The two ogres stare each other down with raised fists before noticing your tiny figure standing between them.", actor_narrator},
[5] = {"Baaah! What that!", actor_grog},
[6] = {"Just a kobold, Grog. He won't hurt ye', ya dolt.", actor_slog},
[7] = {"The blue ogre picks you up with a single hand and dangles you, his other hand pressed to his chin in contemplation.", actor_narrator},
[8] = {"Having almost been eaten by a giant, ancient dinosaur five times his size, you put on a display of absolutely no trepidation.", actor_narrator},
[9] = {"I like this one, Grog.", actor_slog},
[10] = {"He places you back down.", actor_narrator},
[11] = {"This be the Koboltagonist the Dig Masta been talkin' 'bout! Perfect for us'n, Grog.", actor_slog},
[12] = {"Dat pipsqueak ain't makin' me go back down 'der! Kobo'tagonist er' not.", actor_grog},
[13] = {"Whot?! You won't go down that wee lil' tunnel, but this lil' digging rat will?", actor_slog},
[14] = {"You nod once when the ogre looks to you.", actor_narrator},
[15] = {"Bah, fine, Grog, you overgrown baby. Why you even carry dat club around wichye? Should hand'it to this 'ere warrior rat instead!", actor_slog},
[16] = {"The tan ogre turns, waving a hand in disregard and looking the other way.", actor_narrator},
[16] = {"I ain't got it no more.", actor_grog},
[17] = {"Wha? You dropped it?! Can't even hold onto ye' club during a mild jog?", actor_slog},
[18] = {"Hmph!", actor_grog},
[19] = {"The blue ogre shakes his head in disappointment and turns to you.", actor_narrator},
[20] = {"Well, lil' rat, I'll have to stay here an' make sure Grog don't wander off down the wrong tunnel. He's bit scared of 'dat big lizard.", actor_slog},
[21] = {"Dig Master says ye' be looking for the last shiny rock. We knows where it is. We tells ye'.", actor_slog},
[22] = {"But first, needs something of ye'. I think Grog will get his grips back with his ol' club. Fetch it from the cold tunnels fors 'im, aye?", actor_slog},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"With a thud, you slam the hilt of the giant ogre club near the campfire.", actor_narrator},
[2] = {"The blue ogre looks at you and cracks a grin before looking to his companion, who takes the club effortlessly with one hand and wields it about.", actor_narrator},
[3] = {"Ahah! I tolds ye'! You lost, pay up!", actor_slog},
[4] = {"Bah, fine!", actor_grog},
[5] = {"Grog reaches into a nearby satchel and retrieves a sack of gold coins and places it in Slog's hand.", actor_narrator},
[6] = {"Hah! Oi, get your spirits up, mate. This lil' one just got your courage back. By 'imself, too!", actor_slog},
[7] = {"...", actor_grog},
[8] = {"You look on with notable indifference.", actor_narrator},
[9] = {"Ah, you ain't one for the silly bits, lil' rat. No problem 'ere.", actor_slog},
[10] = {"Looky 'ere, as promised. What yer lookin' for is down this abandon tunnel.", actor_slog},
[11] = {"He places a thin cloth on your hand featuring a rough map outlined with a shard of campfire coal.", actor_narrator},
[12] = {"Old magic used down there, long ago. Blue Dragonflight or a thing'a that nature. Some big creature, big lizard...", actor_slog},
[13] = {"...dragon...", actor_grog},
[14] = {"No matter, whatever 'tis! It's deadly. We ain't stayed to see what it was. You just be careful down there, y'hear?", actor_slog},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"Words spread like hot wax, Koboltagonist! We hear of good news.", actor_digmaster},
[2] = {"You hand the ogre's cloth map to the Dig Master.", actor_narrator},
[3] = {"Ah, deep within ice tunnels. Good! Faster we open last chamber, get last relic, faster we get treasure.", actor_digmaster},
[4] = {"Ogres good idea, glad I let them stay. How else we know of last piece?", actor_digmaster},
[5] = {"The Dig Master puts on a smug look and stands tall as he looks over his trophies. A little too smug.", actor_narrator},
[6] = {"He notices you watching him intently.", actor_narrator},
[7] = {"Aye, aye. All because of you, too. But I help too, this time. Ay?", actor_digmaster},
[8] = {"No time for idle-scrat, Koboltagonist. Go be Koboltagonist! You well trained for beasties now!", actor_digmaster},
[9] = {"Kobolds always send the best!", actor_digmaster},
[10] = {"Now ours, hehehehaha!", actor_digmaster},
[11] = {"*Cough*... Ahem...", actor_digmaster},
[12] = {"Off to the lizard-dragon, Koboltagonist!", actor_digmaster},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"The relic has returned! Oh, and Koboltagonist, too.", actor_digmaster},
[2] = {"The Dig Master rubs his hands together impatiently.", actor_narrator},
[3] = {"No time to wait. Place it! Place it! Place it!", actor_digmaster},
[4] = {"...", actor_digmaster},
[5] = {"...ahem. At your own pace, Koboltagonist. Right. We only have 'cause of you. Scrat! No manners.", actor_digmaster},
[6] = {"You approach the final pedastal with the glowing blue relic from the slain ice monster. The Dig Master inches forward as you do.", actor_narrator},
[7] = {"As you bend a knee to release it, a mysterious force rips it from your hands and slams it into place, knocking you back.", actor_narrator,
nil, nil, nil, function() utils.looplocalp(function() StopSound(kui.sound.menumusic, false, true) end) end},
[8] = {"The four pieces link together in a series of elemental lights.", actor_narrator, nil, nil, nil, mergeancientrelics},
[9] = {"The artifacts' colors merge on the ground, forming a single slab of light.", actor_narrator},
[10] = {"In a flash, a beam projects upward to reveal a strange portal.", actor_narrator},
[11] = {"...", actor_narrator},
[12] = {"The light settles, and the room is quiet, save for the the monotonous hum of the portal.", actor_narrator},
[13] = {"You unshield your eyes, taking in the new scene.", actor_narrator},
[14] = {"You walk over to the slab of light. You feel the warmth as its energy, but nothing else occurs.", actor_narrator},
[15] = {"Bah, scrat!", actor_digmaster},
[16] = {"The Dig Master shuffles from underneath a table, returning to edge of the portal.", actor_narrator},
[17] = {"By all gold of Azeroth... well, what now?", actor_digmaster},
[18] = {"For the first time ever, the Dig Master remains still and quiet, contemplating the floor of light with one hand held to his chin.", actor_narrator},
[19] = {"He tries to move the relics, but they are firmly locked in place by a mysterious force.", actor_narrator},
[20] = {"After fidgeting with different objects and instruments, the Dig Master surrenders and throws up his arms.", actor_narrator},
[21] = {"Out of ideas, Koboltagonist.", actor_digmaster},
[22] = {"Scrat! Can't be end! Shouldn't be!", actor_digmaster},
[23] = {"So close!", actor_digmaster},
[24] = {"He rubs at his pockets several times.", actor_narrator},
[25] = {"Bah, no more cheese!", actor_digmaster},
[26] = {"You squint at him.", actor_narrator},
[27] = {"Wha'? Worth shot.", actor_digmaster},
[28] = {"The Dig Master sighs loudly and returns to his table to tinker.", actor_narrator},
[29] = {"A thought enters your mind of the old kobold and his scar. He must know more of what is required.", actor_narrator},
})
screenplay.chains.quest[idf()] = {}
screenplay.chains.quest[id].start = speak.chain:build({
[1] = {"The Greywhisker sits on a petrified stone near his fire with both eyes shut.", actor_narrator},
[2] = {"He chants on occasion, to which the stones near the fire shift and its embers flare.", actor_narrator},
[3] = {"You take a seat on the opposite side of the pit and wait patiently.", actor_narrator},
[4] = {"He opens one eye for a moment, then shuts it again.", actor_narrator},
[5] = {"Such a sight, is it not? The relics of old, united once more. I have not seen it in many moons.", actor_greywhisker},
[6] = {"You stop prodding embers in the fire and look to the old mage.", actor_narrator},
[7] = {"Heh. It is not the first time that portal has graced this cavern.", actor_greywhisker},
[8] = {"Nearly three and ten winters ago, the old guard of 'mancers, and myself, set foot in that vault.", actor_greywhisker},
[9] = {"It was a different time. When us kobolds of the deep lacked proper earthly magic. We stood no chance.", actor_greywhisker},
[10] = {"And we were not the first. Many adventurers from the surface have attempted that place.", actor_greywhisker},
[11] = {"Evident by their bones that we scoured for loot.", actor_greywhisker},
[12] = {"We did not scavenge for long. The creaking and churning of gears caught our attention in the edge of the darkness.", actor_greywhisker},
[13] = {"That is when it struck. Not beast or of flesh, but of gold and jewels. Merged together over millennia, it would seem.", actor_greywhisker},
[14] = {"Contorted and twisted into a form that is only meant to punish those who seek it.", actor_greywhisker},
[15] = {"In the silence, embers pop and bursts of sparks float away. The echoes of picks on rock can be heard from deeper within the tunnel as kobolds work.", actor_narrator},
[16] = {"Do you think you will fare different, young one?", actor_greywhisker},
[17] = {"You frown and hold your pickaxe, rubbing at its silver edge.", actor_narrator},
[18] = {"I will not be the one to stand in your way. It was adventure and riches which I desired in my youth, and too many discouraged it.", actor_greywhisker},
[19] = {"I let their weakness stall my moment of glory.", actor_greywhisker},
[20] = {"The Greywhisker stands tall, cane in hand.", actor_narrator},
[21] = {"Is the vault what you desire?", actor_greywhisker},
[22] = {"You hesitate. A glimpse of gleaming gold fills your head.", actor_narrator},
[23] = {"You nod once.", actor_narrator},
[24] = {"So be it.", actor_greywhisker},
[25] = {"The Greywhisker pulls the prismatic gem from his staff and unties the leather of its hilt, the scabbard showing a luminous circle and strange letters.", actor_narrator},
[26] = {"This is the last of the old keys, and the human tongue that describes how to use it. However, it has long been drained of its power.", actor_greywhisker},
[27] = {"He hands you the cloth, which you place carefuly in your dig map.", actor_narrator},
[28] = {"This map will allow you to access |c0000f066The Vault|r through the portal, so long as you carry it, but not its core chamber.", actor_greywhisker},
[29] = {"You will need to gather enough |c00ff9c00Ancient Fragments|r so that I can imbue the prism with energy to reach the vault's depths.", actor_greywhisker},
[30] = {"Only when the vault's guardian is defeated can our kobolds secure its trove.", actor_greywhisker},
[31] = {"When you have acquired enough fragments, return to me, and we will see if you follow the path to glory, or the path to doom.", actor_greywhisker},
[32] = {"One of those roads has already been traveled between us. Let us hope it does not repeat itself...", actor_greywhisker},
})
screenplay.chains.quest[id].finish = speak.chain:build({
[1] = {"You step in front of the Dig Master, your clothing scorched by molten gold and your head burned clean of hair.", actor_narrator, nil, nil, nil,
function() utils.stopsoundall(kui.sound.menumusic) end},
[2] = {"Bah!", actor_digmaster},
[3] = {"...", actor_digmaster},
[4] = {"Oh, Koboltagonist, didn't recognize. Thought you tunnel mole! Naked one, too!", actor_digmaster},
[5] = {"With a heavy grunt, you drop the solid gold head of the defeated amalgam on the table.", actor_narrator},
[6] = {"The Dig Master rushes to it, rubbing his hand over its raised brow and pointed teeth while staring into its two ruby sockets.", actor_narrator},
[7] = {"By skirt—I mean drat—I mean scrat, by dirt!", actor_digmaster},
[8] = {"He pauses and looks to you from the corner of his eye, per usual.", actor_narrator},
[9] = {"More?", actor_digmaster},
[10] = {"You nod, then point to your map with the runic scabbard, then to the portal. You hand the cloth to the Dig Master.", actor_narrator},
[11] = {"Yes, yes! I get Shinykeeper and diggers, we leave first thing!", actor_digmaster},
[12] = {"You done good Koboltagonist.", actor_digmaster},
[13] = {"The Dig Master nearly knocks over his table as he rummages for bags and buckets, stacking and tying them them atop his giant rat steed.", actor_narrator},
[14] = {"As he comes to a halt—his head now also hosting a silver bucket—he glances to you with eager eyes.", actor_narrator},
[15] = {"...", actor_digmaster},
[16] = {"Koboltagonist? You come? Need more buckets!", actor_digmaster},
[17] = {"You nod.", actor_narrator, nil, nil, nil, function() utils.fadeblack(true, 2.0) end},
[18] = {"Before long, the Dig Master is gone, making a head start for the great vault through the portal with the Shinykeeper, Elementalist, and diggers in tow.", actor_narrator},
[19] = {"You look around, frst to the Shinykeeper's forge, then to the infusion crystals lining the back wall.", actor_narrator},
[20] = {"No other kobolds are in sight. The main tunnel looks to have been left temporarily unattended in the mad dash to the treasure room.", actor_narrator},
[21] = {"You go to see the Greywhisker, but he is not near his camp. His tent is packed and gone, and the fire pit doused.", actor_narrator},
[22] = {"You ponder his intentions, but are distracted by the thought of the conquests achieved this day.", actor_narrator},
[23] = {"Something inside you yearns for it again.", actor_narrator},
[24] = {"But, later. Rest is well-earned. You did a lot today, for a tiny rat with a candle and pickaxe.", actor_narrator},
[25] = {"With an unabashed grin, you grab your rucksack, shoulder your trusty tool, and make for the vault portal at a slow and steady pace.", actor_narrator},
[26] = {"You near the Dig Master's table, where the ogre's club has been placed for whatever reason. They appear to have joined in on the treasure hunt.", actor_narrator},
[27] = {"As you pass by, the chisel of your pickaxe swings outward from your stride and knocks over the brutish ogre's club.", actor_narrator},
[28] = {"Its stone head cracks open from the impact. You look around, but there's still no one.", actor_narrator},
[29] = {"As you shrug and take a step to leave, you catch a reflection from the wreckage.", actor_narrator},
[30] = {"You reach down to the cracked mallet, tossing it with one hand. It crumbles into dust, leaving only a strange, purple orb.", actor_narrator},
[31] = {"It pulsates with dark swirls of black and magenta.", actor_narrator},
[32] = {"You bend down and rub its surface with loose fingers. Something inside it calls to you.", actor_narrator},
[33] = {"You hesitate. After a short delay, you pick it up and place it in your pack.", actor_narrator},
[34] = {"But, that's for another day.", actor_narrator},
[35] = {"", actor_narrator,
nil, nil, nil, function()
speak:show(false, true)
speak.showskip = true
utils.playsoundall(kui.sound.completebig)
alert:new(color:wrap(color.tooltip.alert, "The End|n")..color:wrap(color.tooltip.good, " Thanks for playing!"), 7.5)
utils.timed(9.0, function() utils.fadeblack(false, 2.0) speak:endscene() end)
utils.timed(11.0, function() utils.playsoundall(kui.sound.menumusic) end)
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,
defaultsnd = true, -- play default mumbling when not a pause ("...") or narration ("*").
skipsnd = false, -- when set to true, ignores next item sound (one-time use per item).
fade = true, -- should dialogue components have fading eye candy effects?
fadedur = 0.81, -- how fast to fade if fade is enabled.
unitpan = false, -- 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 = "DialogueText", -- from imported .fdf
fdftextarea = "DialogueTextArea", -- ``
hextitle = "|cffffce22", -- character title text color.
hextext = "|cffffffff", -- character speech text color.
debug = false, -- print debug messages for certain functions.
inprogress = false, -- speech window in progress; for controlling external logic.
-- 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.03, -- 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.clickednext = false -- flag for controlling fade animations.
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.02)
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
for pnum = 1,kk.maxplayers do
if Player(pnum-1) == utils.localp() then
kui:showhidepartypills(false)
kui:hidegameui(Player(pnum-1))
if quest.inprogress then
quest.parentfr:hide()
end
BlzFrameSetVisible(self.customgameui, false)
end
end
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.inprogress = true
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
if self.fade then
self:fadeout(false)
else
BlzFrameSetVisible(self.fr.backdrop, true)
end
BlzFrameSetEnable(self.fr.nextbtn, true)
BlzFrameSetEnable(self.fr.skipbtn, true)
self.currentchain:playnext()
if speak.showskip then
BlzFrameSetVisible(speak.fr.skipbtn, true)
else
BlzFrameSetVisible(speak.fr.skipbtn, false)
end
-- run debug if enabled:
if self.debug then self.currentchain:debug("startscene") end
end
-- end the dialogue sequence.
function speak:endscene()
self.inprogress = false
BlzEnableSelections(true, false)
PauseAllUnitsBJ(false)
if self.hideui then
for pnum = 1,kk.maxplayers do
local p = Player(pnum-1)
if kobold.player[p] then
if p == utils.localp() then
kui:showhidepartypills(true)
kui:showgameui(p, true)
if quest.inprogress then
quest.parentfr:show()
end
BlzFrameSetVisible(self.customgameui, true)
SelectUnitForPlayerSingle(kobold.player[p].unit, p)
SyncSelections()
end
end
end
end
BlzFrameSetEnable(self.fr.nextbtn, false)
BlzFrameSetEnable(self.fr.skipbtn, false)
BlzFrameSetVisible(self.fr.nextbtn, false)
BlzFrameSetVisible(self.fr.skipbtn, false)
utils.fixfocus(self.fr.nextbtn)
utils.fixfocus(self.fr.skipbtn)
if self.fade then
self:fadeout(true)
else
BlzFrameSetVisible(self.fr.backdrop, false)
end
self:enablecamera(false)
-- disable flag for any GUI triggers that might need it:
udg_speakactive = false
self.initialized = false
-- print any dialogue that was skipped for user reference in message log:
-- log speak items to the message log so users can re-read them.
utils.looplocalp(function()
for i = 1,100 do
if self.currentchain[i] then
print(color:wrap(color.ui.wax, self.currentchain[i].actor.name)..": "..self.currentchain[i].text)
end
end
ClearTextMessages()
end)
-- clear cache:
if not self.paused and self.currentchain 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()
if self.inprogress then
-- 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
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
BlzFrameSetVisible(fh, true)
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:clear()
self:show(false)
self.resumeindex = self.currentindex + 1
self.pause = true
self.inprogress = false
end
-- resume after initiating a pause event.
function speak:resume()
self:show(true)
self.currentchain:start(self.resumeindex)
self.resumeindex = nil
self.pause = false
self.clickednext = false
self.inprogress = true
end
-- when a new chain is being played, initialize the default display.
function speak:clear()
BlzFrameSetText(self.fr.text, "")
BlzFrameSetText(self.fr.title, "")
if not 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 not speak.skipsnd then
if self.sound then
utils.playsoundall(self.sound)
elseif speak.defaultsnd then
-- play randomized mumbling if not a comment or pause.
if self.actor.name ~= "Narrator" and string.sub(self.text,1,1) ~= "*" and string.sub(self.text,1,3) ~= "..." then
speak.mumble = math.random(1,6)
-- make sure duplicates aren't played:
if speak.prevmumble and speak.prevmumble == speak.mumble then
if speak.mumble == 1 then
speak.mumble = 6
else
speak.mumble = speak.mumble - 1
end
end
utils.playsoundall(kui.sound.mumble[speak.mumble])
speak.prevmumble = speak.mumble
end
end
else
speak.skipsnd = false
end
if self.func then
self.func()
end
-- manage frames:
self.actor.portrait:render(self.emotion)
if speak.showskip then
BlzFrameSetVisible(speak.fr.skipbtn, true)
end
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
shop = {}
function shop:init()
-- globals
shop_selected_card_id = {} -- array for each player
shop_selected_ore_id = {} -- ``
shop_vendor_type_open = {} -- `` 0 = shinykeeper, 1 = elementalist
greywhisker_selected_card = {}
-- map shop card order to actual slot ids:
shop_slotid_map = {slot_outfit,slot_leggings,slot_gloves,slot_potion,slot_backpack,slot_artifact,slot_candle,slot_boots,slot_helmet,slot_tool}
self.__index = self
self.fr = kui.frame:newbytype("BACKDROP", kui.canvas.game)
self.fr:setbgtex('war3mapImported\\panel-shop-backdrop.blp')
self.fr:setsize(kui:px(1200), kui:px(675))
self.fr:setfp(fp.b, fp.b, kui.worldui, 0, kui:px(325))
self.fr:setlvl(1)
self.fr.showfunc = function()
self:updateframes()
self:resetselection()
end
self.fr.hidefunc = function()
self:resetselection()
end
-- shinykeeper currency pane:
self.fr.shinypane = kui.frame:newbytype("BACKDROP", self.fr)
self.fr.shinypane:setbgtex('war3mapImported\\panel-shop-currency-bg-shinykeeper.blp')
self.fr.shinypane:setsize(kui:px(339), kui:px(208))
self.fr.shinypane:setfp(fp.bl, fp.bl, self.fr, kui:px(64), kui:px(124))
self.fr.shinypane.title = self.fr.shinypane:createbtntext('SPEND GOLD|nTO CRAFT ITEMS')
self.fr.shinypane.title:setfp(fp.c, fp.b, self.fr.shinypane, 0, kui:px(130))
self.fr.shinypane.title:setsize(kui:px(350), kui:px(80))
self.fr.shinypane.goldtxt = self.fr.shinypane:createbtntext('0')
self.fr.shinypane.goldtxt:setfp(fp.bl, fp.bl, self.fr.shinypane, kui:px(156), kui:px(46))
self.fr.shinypane.goldtxt:setsize(kui:px(130), kui:px(40))
BlzFrameSetTextAlignment(self.fr.shinypane.goldtxt.fh, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_LEFT)
self.fr.shinypane.shopname = self.fr.shinypane:createheadertext('The Shinykeeper')
self.fr.shinypane.shopname:setsize(kui:px(350), kui:px(40))
self.fr.shinypane.shopname:setfp(fp.c, fp.bl, self.fr, kui:px(246), kui:px(622))
self.fr.shinypane.portrait = kui.frame:newbytype("BACKDROP", self.fr.shinypane)
self.fr.shinypane.portrait:setbgtex('war3mapImported\\panel-shop-vendor-splash-shinykeeper.blp')
self.fr.shinypane.portrait:setsize(kui:px(444), kui:px(348))
self.fr.shinypane.portrait:setfp(fp.c, fp.bl, self.fr, kui:px(242), kui:px(482))
-- elementalist currency pane:
self.fr.elepane = kui.frame:newbytype("BACKDROP", self.fr)
self.fr.elepane.showfunc = function() self:updateframes() self.fr.sellore:hide() end
self.fr.elepane:setlvl(1)
self.fr.elepane:setbgtex('war3mapImported\\panel-shop-currency-bg-elementalist.blp')
self.fr.elepane:setsize(kui:px(339), kui:px(208))
self.fr.elepane:setfp(fp.bl, fp.bl, self.fr, kui:px(64), kui:px(124))
self.fr.elepane.title = self.fr.elepane:createbtntext('SELECT AN ORE TO USE')
self.fr.elepane.title:setfp(fp.c, fp.b, self.fr.elepane, 0, kui:px(166))
self.fr.elepane.title:setsize(kui:px(350), kui:px(28))
self.fr.elepane.select = kui.frame:newbytype("BACKDROP", self.fr.elepane)
self.fr.elepane.select:setbgtex('war3mapImported\\panel-shop-selection-circle.blp')
self.fr.elepane.select:setsize(kui:px(94), kui:px(97))
self.fr.elepane.select:setfp(fp.c, fp.c, self.fr.elepane, 0, 0)
self.fr.elepane.select:setlvl(7)
self.fr.elepane.select:hide()
self.fr.elepane.portrait = kui.frame:newbytype("BACKDROP", self.fr.elepane)
self.fr.elepane.portrait:setbgtex('war3mapImported\\panel-shop-vendor-splash-elementalist.blp')
self.fr.elepane.portrait:setsize(kui:px(444), kui:px(348))
self.fr.elepane.portrait:setfp(fp.c, fp.bl, self.fr, kui:px(242), kui:px(482))
-- hover glow effect:
self.fr.hoverglow = kui.frame:newbytype("BACKDROP", self.fr)
self.fr.hoverglow:setbgtex('war3mapImported\\panel-shop_ore_selection.blp')
self.fr.hoverglow:setsize(kui:px(80), kui:px(70))
self.fr.hoverglow:setlvl(6)
self.fr.hoverglow:hide()
-- hover glow functions:
local hoverenter = function(targetfr) self.fr.hoverglow:setfp(fp.c, fp.c, targetfr, kui:px(2.5), 0) self.fr.hoverglow:show() end
local hoverleave = function(targetfr) self.fr.hoverglow:hide() end
-- crafting functions:
local checkcrafteligible = function()
if shop_vendor_type_open[utils.trigpnum()] == 0 and shop_selected_card_id[utils.trigpnum()] > 0 then -- shinykeeper
self.fr.craftinactive:hide()
self.fr.craftactive:show()
elseif shop_vendor_type_open[utils.trigpnum()] == 1 and shop_selected_ore_id[utils.trigpnum()] > 0 and shop_selected_card_id[utils.trigpnum()] > 0 then -- elementalist
self.fr.craftinactive:hide()
self.fr.craftactive:show()
end
end
local clicksellore = function()
local p = utils.trigp()
local pobj = kobold.player[p]
if pobj.ore[shop_selected_ore_id[utils.trigpnum()]] >= 1 then
utils.playsound(kui.sound.goldcoins, p)
pobj:awardgold(20)
pobj.ore[shop_selected_ore_id[utils.trigpnum()]] = pobj.ore[shop_selected_ore_id[utils.trigpnum()]] - 1
self:updateframes()
else
alert:new(color:wrap(color.tooltip.bad, "You have no ore to sell!"))
end
pobj = nil
end
local clickbuyore = function()
local p = utils.trigp()
local pobj = kobold.player[p]
if pobj.gold >= 40 then
utils.playsound(kui.sound.goldcoins, p)
pobj:awardgold(-40)
pobj:awardoretype(shop_selected_ore_id[utils.trigpnum()], 1)
self:updateframes()
else
alert:new(color:wrap(color.tooltip.bad, "You don't have enough gold!"))
end
pobj = nil
end
local clickcraftbtn = function()
-- this function uses waterfall logic, returning 'true' denotes item crafted,
-- returning 'false' is a failed condition shared between both shops or that specific shop.
local p, pnum = utils.trigp(), utils.trigpnum()
local gcost = self.fr.card[shop_selected_card_id[pnum]].costval
local pobj = kobold.player[p]
-- check default conditions to craft:
if shop_vendor_type_open[pnum] == 0 and pobj.gold < gcost then
utils.palert(p, color:wrap(color.tooltip.bad, "You don't have enough gold!"))
return false
end
if loot:isbagfull(p) then
utils.palert(p, color:wrap(color.tooltip.bad, "Your bags are full!"))
return false
end
-- shop-specific craft actions:
if shop_vendor_type_open[pnum] == 0 then -- shinykeeper
local maxrarity = rarity_rare
if quest_shinykeeper_upgrade_2 then maxrarity = rarity_epic end
loot:generate(p, pobj.level, shop_slotid_map[shop_selected_card_id[pnum]],
loot:getrandomrarity(rarity_common, maxrarity, pobj:getlootodds()), true).sellsfor = math.floor(gcost*0.1)
utils.playsound(kui.sound.eleforge, p)
elseif shop_vendor_type_open[pnum] == 1 then -- elementalist
if pobj.ore[shop_selected_ore_id[pnum]] < self.fr.card[shop_selected_card_id[pnum]].costval then
utils.palert(p, color:wrap(color.tooltip.bad, "You don't have enough ore!"))
return false
end
local maxrarity = rarity_rare
if quest_elementalist_upgrade_2 then maxrarity = rarity_epic end
local goldworth = math.floor(25 + math.random(pobj.level^1.05,pobj.level^1.1) + self.fr.card[shop_selected_card_id[pnum]].costval*5)
loot:generateforcedelement(p, pobj.level, shop_slotid_map[shop_selected_card_id[pnum]],
loot:getrandomrarity(rarity_common, maxrarity, pobj:getlootodds()), shop_selected_ore_id[pnum]).sellsfor = goldworth
pobj:awardoretype(shop_selected_ore_id[pnum], -self.fr.card[shop_selected_card_id[pnum]].costval)
utils.playsound(kui.sound.eleforge, p)
end
-- wrap-up:
pobj:awardgold(-self.fr.card[shop_selected_card_id[pnum]].costval)
alert:new(color:wrap(color.tooltip.good, "Item crafted!"))
pobj = nil
return true
end
-- sell ore button:
self.fr.sellore = kui.frame:newbtntemplate(self.fr, 'war3mapImported\\inventory-icon_sell.blp')
self.fr.sellore:setsize(kui:px(32),kui:px(32))
self.fr.sellore.btn:assigntooltip("sellore")
self.fr.sellore.btn:addevents(nil, nil, clicksellore)
self.fr.sellore:setlvl(6)
self.fr.sellore:hide()
-- buy ore button:
self.fr.buyore = kui.frame:newbtntemplate(self.fr, 'war3mapImported\\btn-purchase.blp')
self.fr.buyore:setsize(kui:px(32),kui:px(32))
self.fr.buyore.btn:assigntooltip("buyore")
self.fr.buyore.btn:addevents(nil, nil, clickbuyore)
self.fr.buyore:setlvl(6)
self.fr.buyore:hide()
self.fr.elepane.ore = {}
local x,y = 68,121
for i = 1,6 do
if i == 4 then
x = 68
y = 52
elseif i > 1 then
x = x + 107
end
self.fr.elepane.ore[i] = kui.frame:newbtntemplate(self.fr.elepane, kui.tex.invis)
self.fr.elepane.ore[i].costval = 10 -- use for pricing logic.
self.fr.elepane.ore[i].btn:assigntooltip(kui.meta.oretooltip[i])
self.fr.elepane.ore[i]:setsize(kui:px(60), kui:px(60))
self.fr.elepane.ore[i]:setfp(fp.c, fp.bl, self.fr.elepane, kui:px(x), kui:px(y))
self.fr.elepane.ore[i].costtxt = self.fr.elepane.ore[i]:createbtntext(self.fr.elepane.ore[i].costval)
self.fr.elepane.ore[i].costtxt:setfp(fp.b, fp.b, self.fr.elepane.ore[i], 0, kui:px(-5))
self.fr.elepane.ore[i].costtxt:setsize(kui:px(40), kui:px(18))
-- select ore:
local selectfunc = function()
-- set picked ore:
if shop_selected_ore_id[utils.trigpnum()] ~= i then
shop_selected_ore_id[utils.trigpnum()] = i
if utils.islocaltrigp() then
self.fr.elepane.select:show()
self.fr.elepane.select:setfp(fp.c, fp.c, self.fr.elepane.ore[i], kui:px(-2), 0)
utils.playsound(kui.sound.itemsel, utils.trigp())
-- move sell btn to this ore:
self.fr.sellore:setfp(fp.tl, fp.br, self.fr.elepane.ore[i], kui:px(-15), kui:px(15))
self.fr.sellore:show()
self.fr.buyore:setfp(fp.tr, fp.bl, self.fr.elepane.ore[i], kui:px(10), kui:px(15))
self.fr.buyore:show()
end
end
end
self.fr.elepane.ore[i].btn:addevents(
function() hoverenter(self.fr.elepane.ore[i]) end,
function() hoverleave(self.fr.elepane.ore[i]) end,
selectfunc)
self.fr.elepane.ore[i].btn:addevents(nil, nil, checkcrafteligible)
end
self.fr.elepane.shopname = self.fr.elepane:createheadertext('The Elementalist')
self.fr.elepane.shopname:setsize(kui:px(350), kui:px(40))
self.fr.elepane.shopname:setfp(fp.c, fp.bl, self.fr, kui:px(246), kui:px(622))
self.fr.elepane:hide()
-- select item center tooltip:
self.fr.tiptxt = self.fr:createbtntext('SELECT AN ITEM TO BUILD')
self.fr.tiptxt:setsize(kui:px(350), kui:px(40))
self.fr.tiptxt:setfp(fp.c, fp.bl, self.fr, kui:px(834), kui:px(384))
-- item selection circle:
self.fr.select = kui.frame:newbytype("BACKDROP", self.fr)
self.fr.select:setbgtex('war3mapImported\\panel-shop-selection-circle.blp')
self.fr.select:setsize(kui:px(134), kui:px(139))
self.fr.select:setfp(fp.c, fp.c, self.fr, 0, 0)
self.fr.select:setlvl(5)
self.fr.select:hide()
-- invisible close button:
self.fr.closebtn = kui.frame:newbtntemplate(self.fr, kui.tex.invis)
self.fr.closebtn.btn:assigntooltip("closebtn")
self.fr.closebtn.btn:linkcloseevent(self.fr)
self.fr.closebtn:setsize(kui:px(182), kui:px(72))
self.fr.closebtn:setfp(fp.c, fp.bl, self.fr, kui:px(158), kui:px(60))
-- create shop cards and attach icon buttons:
self.fr.card = {}
local x,y = 462,386
for i = 1, 10 do
if i == 6 then
x = 462
y = 109
elseif i > 1 then
x = x + 145
end
self.fr.card[i] = kui.frame:newbytype("BACKDROP", self.fr)
self.fr.card[i]:setbgtex('war3mapImported\\panel-shop-card.blp')
self.fr.card[i]:setsize(kui:px(145), kui:px(275))
self.fr.card[i]:setfp(fp.bl, fp.bl, self.fr, kui:px(x), kui:px(y))
if i < 10 then -- hacky fix for path string.
self.fr.card[i].icon = kui.frame:newbtntemplate(self.fr.card[i], 'war3mapImported\\item_loot_icons_crafting_generic_0'..i..'.blp')
else
self.fr.card[i].icon = kui.frame:newbtntemplate(self.fr.card[i], 'war3mapImported\\item_loot_icons_crafting_generic_'..i..'.blp')
end
self.fr.card[i].icon:setsize(kui:px(64), kui:px(64))
self.fr.card[i].icon:setfp(fp.b, fp.b, self.fr.card[i], kui:px(-1), kui:px(108))
self.fr.card[i].icon:setlvl(7)
-- card text:
if i == 4 or i == 5 or i == 6 or i == 7 or i == 10 then
self.fr.card[i].costval = 200
else
self.fr.card[i].costval = 100 -- use this for price logic.
end
self.fr.card[i].costtxt = self.fr.card[i]:createbtntext('Cost: '..color:wrap(color.ui.gold, self.fr.card[i].costval))
self.fr.card[i].costtxt:setfp(fp.c, fp.b, self.fr.card[i], 0, kui:px(67))
self.fr.card[i].cardname = self.fr.card[i]:createbtntext(loot:getslotname(shop_slotid_map[i]))
self.fr.card[i].cardname:setfp(fp.c, fp.b, self.fr.card[i], 0, kui:px(205))
-- select card:
local selectfunc = function()
if shop_selected_card_id[utils.trigpnum()] ~= i then
shop_selected_card_id[utils.trigpnum()] = i
if utils.islocaltrigp() then
self.fr.select:show()
self.fr.select:setfp(fp.c, fp.c, self.fr.card[i].icon, 0, 0)
utils.playsound(kui.sound.itemsel, utils.trigp())
end
end
end
self.fr.card[i].icon.btn:addevents(
function() hoverenter(self.fr.card[i]) end,
function() hoverleave(self.fr.card[i]) end,
selectfunc)
self.fr.card[i].icon.btn:addevents(nil, nil, checkcrafteligible)
end
-- craft button:
self.fr.craftinactive = kui.frame:newbtntemplate(self.fr, 'war3mapImported\\panel-shop-craft-inactive.blp')
self.fr.craftinactive:setsize(kui:px(106), kui:px(109))
self.fr.craftinactive:setfp(fp.c, fp.bl, self.fr, kui:px(1116), kui:px(60))
self.fr.craftactive = kui.frame:newbtntemplate(self.fr, 'war3mapImported\\panel-shop-craft-active.blp')
self.fr.craftactive.btn:assigntooltip("craftitem")
self.fr.craftactive:setallfp(self.fr.craftinactive)
self.fr.craftactive:hide()
self.fr.craftactive.btn:addevents(nil, nil, clickcraftbtn)
self.fr:hide() -- hidden by default
self:buildgreywhisker()
end
function shop:buildgreywhisker()
self.gwfr = kui.frame:newbytype("BACKDROP", kui.canvas.game)
self.gwfr:setbgtex('war3mapImported\\panel-shop-backdrop_small.blp')
self.gwfr:setsize(kui:px(648), kui:px(664))
self.gwfr:setfp(fp.b, fp.b, kui.worldui, 0, kui:px(325))
self.gwfr:setlvl(1)
self.gwfr.showfunc = function()
self:updateframes()
self:resetselection()
self.gwfr.craftinactive:show()
self.gwfr.craftactive:hide()
self.gwfr.select:hide()
self.gwfr.hoverglow:hide()
self.fr:hide() -- hide other shops.
end
self.gwfr.hidefunc = function()
self:resetselection()
end
self.gwfr.hoverglow = kui.frame:newbytype("BACKDROP", self.gwfr)
self.gwfr.hoverglow:setbgtex('war3mapImported\\panel-shop_ore_selection.blp')
self.gwfr.hoverglow:setsize(kui:px(80), kui:px(70))
self.gwfr.hoverglow:setlvl(6)
self.gwfr.hoverglow:hide()
self.gwfr.portrait = kui.frame:newbytype("BACKDROP", self.gwfr)
self.gwfr.portrait:setbgtex('war3mapImported\\panel-greywhisker_portrait.blp')
self.gwfr.portrait:setsize(kui:px(385), kui:px(298))
self.gwfr.portrait:setfp(fp.c, fp.b, self.gwfr, 0, kui:px(464))
self.gwfr.shopname = self.gwfr:createheadertext('The Greywhisker')
self.gwfr.shopname:setsize(kui:px(350), kui:px(40))
self.gwfr.shopname:setfp(fp.c, fp.b, self.gwfr, 0, kui:px(615))
self.gwfr.select = kui.frame:newbytype("BACKDROP", self.gwfr)
self.gwfr.select:setbgtex('war3mapImported\\panel-shop-selection-circle.blp')
self.gwfr.select:setsize(kui:px(94), kui:px(97))
self.gwfr.select:setfp(fp.c, fp.c, self.gwfr, 0, 0)
self.gwfr.select:setlvl(7)
self.gwfr.select:hide()
local hoverenter = function(targetfr) self.gwfr.hoverglow:setfp(fp.c, fp.c, targetfr, kui:px(2.5), 0) self.gwfr.hoverglow:show() end
local hoverleave = function(targetfr) self.gwfr.hoverglow:hide() end
local x,y = 40, 101
self.gwfr.card = {}
for i = 1,5 do
self.gwfr.card[i] = kui.frame:newbytype("BACKDROP", self.gwfr)
if i == 5 then
self.gwfr.card[i].costval = 15
else
self.gwfr.card[i].costval = 5
end
self.gwfr.card[i]:setbgtex('war3mapImported\\panel-shop-card.blp')
self.gwfr.card[i]:setsize(kui:px(113), kui:px(215))
self.gwfr.card[i]:setfp(fp.bl, fp.bl, self.gwfr, kui:px(x), kui:px(y))
self.gwfr.card[i].icon = kui.frame:newbtntemplate(self.gwfr.card[i], loot.itemtable[slot_digkey][i].icon)
self.gwfr.card[i].icon:setsize(kui:px(64), kui:px(64))
self.gwfr.card[i].icon:setfp(fp.b, fp.b, self.gwfr.card[i], kui:px(-1), kui:px(78))
self.gwfr.card[i].icon:setlvl(7)
self.gwfr.card[i].icon.btn:assigntooltip("relic"..tostring(i))
self.gwfr.card[i].costtxt = self.gwfr.card[i].icon:createbtntext('Cost: '..color:wrap(color.rarity.ancient, self.gwfr.card[i].costval))
self.gwfr.card[i].costtxt:setfp(fp.c, fp.b, self.gwfr.card[i], 0, kui:px(53))
self.gwfr.card[i].icon.btn:addevents(
function() hoverenter(self.gwfr.card[i]) end,
function() hoverleave(self.gwfr.card[i]) end,
function()
if greywhisker_selected_card[utils.trigpnum()] ~= i then
greywhisker_selected_card[utils.trigpnum()] = i
if utils.islocaltrigp() then
self.gwfr.select:show()
self.gwfr.select:setfp(fp.c, fp.c, self.gwfr.card[i].icon, 0, 0)
utils.playsound(kui.sound.itemsel, utils.trigp())
end
end
self.gwfr.craftinactive:hide()
self.gwfr.craftactive:show()
end)
x = x + 114
self.gwfr.card[i].icon:hide()
end
-- close:
self.gwfr.closebtn = kui.frame:newbtntemplate(self.gwfr, kui.tex.invis)
self.gwfr.closebtn.btn:assigntooltip("closebtn")
self.gwfr.closebtn.btn:linkcloseevent(self.gwfr)
self.gwfr.closebtn:setsize(kui:px(182), kui:px(72))
self.gwfr.closebtn:setfp(fp.c, fp.bl, self.gwfr, kui:px(154), kui:px(51))
-- currency:
self.gwfr.currency = kui.frame:newbtntemplate(self.gwfr, "war3mapImported\\panel-greywhisker_currency.blp")
self.gwfr.currency:setsize(kui:px(156), kui:px(70))
self.gwfr.currency:setfp(fp.c, fp.bl, self.gwfr, kui:px(395), kui:px(53))
self.gwfr.currency.btn:assigntooltip("fragments")
self.gwfr.currency.txt = self.gwfr.currency:createbtntext("0")
self.gwfr.currency.txt:setfp(fp.r, fp.bl, self.gwfr.currency, kui:px(108), kui:px(33))
self.gwfr.currency.txt:assigntooltip("fragments")
BlzFrameSetTextAlignment(self.gwfr.currency.txt.fh, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_RIGHT)
-- craft:
local clickcraftbtn = function()
-- this function uses waterfall logic, returning 'true' denotes item crafted,
-- returning 'false' is a failed condition shared between both shops or that specific shop.
local p, pnum = utils.trigp(), utils.trigpnum()
local cost = self.gwfr.card[greywhisker_selected_card[pnum]].costval
local pobj = kobold.player[p]
-- check default conditions to craft:
if pobj.ancientfrag < cost then
utils.palert(p, color:wrap(color.tooltip.bad, "You don't have enough fragments!"))
return false
end
if loot:isbagfull(p) then
utils.palert(p, color:wrap(color.tooltip.bad, "Your bags are full!"))
return false
end
loot:generatedigkey(p, 1, greywhisker_selected_card[pnum])
utils.playsound(kui.sound.reliccraft, p)
pobj:awardfragment(-self.gwfr.card[greywhisker_selected_card[pnum]].costval)
-- wrap-up:
alert:new(color:wrap(color.tooltip.good, "Key crafted!"))
pobj = nil
return true
end
-- buy frag:
local clickbuyfrag = function()
local p = utils.trigp()
local pobj = kobold.player[p]
if pobj.gold >= 300 then
utils.playsound(kui.sound.goldcoins, p)
pobj:awardgold(-300)
pobj:awardfragment(1)
self:updateframes()
else
alert:new(color:wrap(color.tooltip.bad, "You don't have enough gold!"))
end
pobj = nil
end
self.gwfr.craftinactive = kui.frame:newbtntemplate(self.gwfr, 'war3mapImported\\panel-shop-craft-inactive.blp')
self.gwfr.craftinactive:setsize(kui:px(106), kui:px(109))
self.gwfr.craftinactive:setfp(fp.c, fp.br, self.gwfr, kui:px(-75), kui:px(50))
self.gwfr.craftactive = kui.frame:newbtntemplate(self.gwfr, 'war3mapImported\\panel-shop-craft-active.blp')
self.gwfr.craftactive.btn:assigntooltip("greywhiskercraft")
self.gwfr.craftactive:setallfp(self.gwfr.craftinactive)
self.gwfr.craftactive:hide()
self.gwfr.craftactive.btn:addevents(nil, nil, clickcraftbtn)
-- buy ore:
self.gwfr.buyfrag = kui.frame:newbtntemplate(self.gwfr, 'war3mapImported\\btn-purchase.blp')
self.gwfr.buyfrag:setsize(kui:px(32),kui:px(32))
self.gwfr.buyfrag:setfp(fp.c, fp.br, self.gwfr, kui:px(-189),kui:px(54))
self.gwfr.buyfrag.btn:assigntooltip("buyfragment")
self.gwfr.buyfrag.btn:addevents(nil, nil, clickbuyfrag)
self.gwfr.buyfrag:setlvl(6)
-- select item center tooltip:
self.gwfr.tiptxt = self.gwfr:createbtntext('SELECT A KEY TO CONJURE')
self.gwfr.tiptxt:setsize(kui:px(350), kui:px(40))
self.gwfr.tiptxt:setfp(fp.c, fp.b, self.gwfr, 0, kui:px(325))
self.gwfr:hide()
end
-- update displayed values for various frames.
function shop:updateframes()
for pnum = 1,kk.maxplayers do
if kobold.player[utils.getp(pnum)] and kobold.player[utils.getp(pnum)]:islocal() then
-- update player ore:
for i = 1,6 do
if kobold.player[utils.getp(pnum)].ore[i] > 0 then
BlzFrameSetText(self.fr.elepane.ore[i].costtxt.fh, color:wrap(color.tooltip.good, kobold.player[utils.getp(pnum)].ore[i]))
else
BlzFrameSetText(self.fr.elepane.ore[i].costtxt.fh, color:wrap(color.txt.txtdisable, 0))
end
end
-- update gold:
BlzFrameSetText(self.fr.shinypane.goldtxt.fh, kobold.player[utils.getp(pnum)].gold or 0)
BlzFrameSetText(self.gwfr.currency.txt.fh, kobold.player[utils.getp(pnum)].ancientfrag or 0)
end
end
end
function shop:toggleshopopen()
-- uses waterfall logic to recycle shop assets; 'true' return = vendor is displayed.
if utils.islocaltrigp() then
-- always update to target shop:
if shop_vendor_type_open[utils.trigpnum()] == 0 then -- shinykeeper
if self.fr.shinypane:isvisible() then
self:close()
return false
else
self.fr.shinypane:show()
self.gwfr:hide()
self.fr.elepane:hide()
for i = 1,10 do
if i == 4 or i == 5 or i == 6 or i == 7 or i == 10 then
self.fr.card[i].costval = 225
else
self.fr.card[i].costval = 125
end
self.fr.card[i].costtxt:settext('Cost: '..color:wrap(color.ui.gold, self.fr.card[i].costval))
end
self:resetselection()
end
elseif shop_vendor_type_open[utils.trigpnum()] == 1 then -- elementalist
if self.fr.elepane:isvisible() then
self:close()
return false
else
self.fr.shinypane:hide()
self.gwfr:hide()
self.fr.elepane:show()
for i = 1,10 do
if i == 4 or i == 5 or i == 6 or i == 7 or i == 10 then
self.fr.card[i].costval = 20
else
self.fr.card[i].costval = 10
end
self.fr.card[i].costtxt:settext('Cost: '..color:wrap(color.tooltip.alert, self.fr.card[i].costval))
end
self:resetselection()
end
end
-- check whether to close/open or do nothing and leave the swap update from above:
if not self.fr:isvisible() then
utils.playsound(kui.sound.panel[2].open, utils.trigp())
self.fr:show()
return true
else
utils.playsound(kui.sound.closebtn, utils.trigp())
return true
end
self:close()
return false
end
end
function shop:resetselection()
shop_selected_card_id[utils.trigpnum()] = 0
shop_selected_ore_id[utils.trigpnum()] = 0
greywhisker_selected_card[utils.trigpnum()] = 0
self.fr.craftinactive:show()
self.fr.craftactive:hide()
self.fr.sellore:hide()
self.fr.buyore:hide()
self.fr.select:hide()
self.fr.hoverglow:hide()
self.fr.elepane.select:hide()
end
function shop:close()
utils.playsound(kui.sound.panel[2].close, utils.trigp())
self.fr:hide()
end
shrine = {} -- controls shrines in the world and what happens when a player right clicks on them.
shrine.biome = {}
function shrine:init()
shrine_buff_dur = 120.0
self.__index = self
self.stack = {} -- stores FourCC() codes as index, shrine object as value
self.devstack = {} -- for dev testing, ipairs with shrine object as value.
self.units = {} -- active shrine units index by unit indexer value, with a table to store more data.
-- h012 -- Ancient Crystal = Drops 1-2 Ancient Fragments and 3-6 random ore type.
shrine_anctcrys = shrine:new("h012", function(u, ux, uy, tx, ty)
local p = utils.powner(u)
loot:generatelootmissile(tx, ty, "I00F", math.random(1,2), nil, speff_fireroar, speff_fragment.effect)
map.block:spawnore(math.random(3*map.manager.diffid, 6*map.manager.diffid), math.random(1,6), tx, ty)
end, true, speff_holyglow)
-- h011 -- Ancient Chest = Drops 2-4 Ancient Fragments.
shrine_anctchest = shrine:new("h011", function(u, ux, uy, tx, ty)
local p = utils.powner(u)
loot:generatelootmissile(tx, ty, "I00F", math.random(2,3) + math.ceil(map.manager.diffid/3), nil, speff_fireroar, speff_fragment.effect)
cl_shrine_arcana:spawn(tx, ty, 6, 8, speff_iceburst2)
end, false, speff_holyglow, "stand portrait alternate", "stand portrait")
-- h00O -- Arcane Prison = Gives 15% Treasure Find and summons a Void Lord minion that follows you for shrine_buff_dur sec.
shrine_arcprison = shrine:new("h00O", function(u, ux, uy, tx, ty)
local p = utils.powner(u)
local u = ai:new(utils.unitatxy(p, tx, ty, 'h00H', math.random(0,360)))
u:initcompanion(900.0, 16.0 + math.floor((kobold.player[p].level/4)^1.2), p_stat_arcane)
buffy:add_indicator(p, "Arcane Shrine", 'ReplaceableTextures\\CommandButtons\\BTNUrnOfKelThuzad.blp', shrine_buff_dur,
"Followed by an Arcane Lord, +15%% Treasure Find", sp_armor_boost[4], 'overhead')
spell:enhancepstat(p_stat_treasure, 15, shrine_buff_dur, p, true)
utils.timed(shrine_buff_dur, function() if u then u:releasecompanion() end end)
end, true, speff_rescast)
-- h00V -- Abandoned Mine = Drops 5*diffid gold ore and summons a cluster of rock golems.
shrine_mine = shrine:new("h00V", function(u, ux, uy, tx, ty)
local p = utils.powner(u)
map.block:spawnore(math.random(2*map.manager.diffid, 3*map.manager.diffid), 7, tx, ty)
cl_foss_golem:spawn(tx, ty, 5, 8, speff_quake)
end, true, speff_rescast)
-- h00U -- Heroic Reliquary = Increases Physical damage done by 15% and reduces damages taken by 15% for shrine_buff_dur sec.
shrine_hero = shrine:new("h00U", function(u, ux, uy, tx, ty)
local p = utils.powner(u)
buffy:add_indicator(p, "Heroic Shrine", 'ReplaceableTextures\\CommandButtons\\BTNAvatar.blp', shrine_buff_dur,
"+15%% Physical Damage, +15%% damage reduction", sp_armor_boost[4], 'overhead')
spell:enhancedmgtype(dmg_phys, 15, shrine_buff_dur, p, true)
spell:enhancepstat(p_stat_dmg_reduct, 15, shrine_buff_dur, p, true)
end, false, speff_rescast, "stand", "stand work")
-- h00R -- Bloodthirsty Reliquary = Adds 15% Physical and Elemental lifesteal for shrine_buff_dur sec.
shrine_blood = shrine:new("h00R", function(u, ux, uy, tx, ty)
local p = utils.powner(u)
buffy:add_indicator(p, "Bloodthirsty Shrine", 'ReplaceableTextures\\CommandButtons\\BTNVampiricAura.blp', shrine_buff_dur,
"+15%% Elemental Lifesteal, +15%% Physical Lifesteal", speff_drainlf, 'overhead')
spell:enhancepstat(p_stat_elels, 15, shrine_buff_dur, p, true)
spell:enhancepstat(p_stat_physls, 15, shrine_buff_dur, p, true)
end, false, speff_rescast, "stand", "stand work")
-- h00T -- Enchanted Reliquary = Increases Elemental damage done by 15% and adds 15% Proliferation for shrine_buff_dur sec.
shrine_enchant = shrine:new("h00T", function(u, ux, uy, tx, ty)
local p = utils.powner(u)
buffy:add_indicator(p, "Enchanted Shrine", 'ReplaceableTextures\\CommandButtons\\BTNSpellSteal.blp', shrine_buff_dur,
"+15%% Elemental Damage, +15%% Proliferation", speff_phxfire, 'overhead')
for dmgtypeid = 1,6 do if dmgtypeid ~= 6 then spell:enhancedmgtype(dmgtypeid, 15, shrine_buff_dur, p, true) end end
spell:enhancepstat(p_stat_eleproc, 15, shrine_buff_dur, p, true)
end, false, speff_rescast, "stand", "stand work")
-- h00W -- Abyssal Reliquary = Summons 3 murloc minions for shrine_buff_dur sec.
shrine_abyss = shrine:new("h00W", function(u, ux, uy, tx, ty)
local p = utils.powner(u)
buffy:add_indicator(p, "Abyssal Shrine", "ReplaceableTextures\\CommandButtons\\BTNMurloc.blp", shrine_buff_dur,
"Followed by 3 Murloc Warriors", mis_acid, 'overhead')
local mt = {}
for i = 1,3 do
mt[i] = {}
mt[i].unit = utils.unitatxy(p, tx, ty, 'n01Q', math.random(0,360))
mt[i].ai = ai:new(mt[i].unit)
mt[i].ai:initcompanion(900.0, 12.0 + math.floor((kobold.player[p].level/6)^1.2), p_stat_nature)
end
utils.timed(shrine_buff_dur, function() for i,t in ipairs(mt) do t.ai:releasecompanion() mt[i] = nil end mt = nil end)
end, false, speff_rescast, "stand", "stand work")
-- h00Q -- Brutish Reliquary = Increases movespeed and mining speed by 25% for shrine_buff_dur sec, and adds a 25% to crit for 50% bonus damage (crit p_stat)
shrine_brut = shrine:new("h00Q", function(u, ux, uy, tx, ty)
local p = utils.powner(u)
buffy:add_indicator(p, "Brutish Shrine", 'ReplaceableTextures\\CommandButtons\\BTNBerserk.blp', shrine_buff_dur,
"+25%% Movespeed, +25%% Mining Speed, +25%% Critical Hit Chance", speff_lust, 'overhead')
spell:enhancepstat(p_epic_hit_crit, 25.0, shrine_buff_dur, p, true)
spell:enhancepstat(p_stat_minespd, 25.0, shrine_buff_dur, p, true)
bf_ms:apply(u, shrine_buff_dur, nil, true)
end, false, speff_rescast, "stand", "stand work")
-- h00S -- Lost Treasure Chest = Spawns 1-3 random items, with a 25% chance to activate a radius of missiles trap after 1.5 sec.
shrine_tchest = shrine:new("h00S", function(u, ux, uy, tx, ty)
loot:generatelootpile(tx, ty, math.random(1,3), utils.powner(u))
end, false, speff_holyglow, "stand portrait alternate", "stand portrait")
-- h00K -- Blessed Fountain = Restores 50% Health and Mana instantly, plus restores 2.5% of the player's Health and Mana per sec for shrine_buff_dur sec.
shrine_blessed = shrine:new("h00K", function(u, ux, uy, tx, ty)
local p, u = utils.powner(u), u
utils.addlifep(u, 50, true, u)
utils.addmanap(u, 2.5, false)
buffy:add_indicator(p, "Blessed Shrine", 'ReplaceableTextures\\CommandButtons\\BTNRegenerate.blp', shrine_buff_dur,
"2.5%% health and mana recovered per sec", speff_rejuv, 'overhead')
utils.timedrepeat(1.33, shrine_buff_dur, function()
if kobold.player[p] and not kobold.player[p].downed then
utils.addlifep(u, 2.5, false, u)
utils.addmanap(u, 2.5, false) else
ReleaseTimer() end
end)
end, false, speff_waterexpl, "stand", "stand alternate")
-- h00N -- Wax-Lit Altar = Adds 15 Wax and drops 1-2 random item, and summons skeletal enemies.
shrine_waxaltar = shrine:new("h00N", function(u, ux, uy, tx, ty)
local p, u = utils.powner(u), u
candle:add(15)
cl_skele:spawn(tx, ty, 5, 8, speff_udexpl)
loot:generatelootpile(tx, ty, math.random(1-2), utils.powner(u))
end, true, speff_udexpl)
--[[
--------------------
ancient boss shrines
--------------------
--]]
-- h00L -- Demonic Tribute
shrine_a_demonic = shrine:new("h00L", function(u, ux, uy, tx, ty)
speff_judge:play(tx, ty)
utils.timed(0.78, function()
local u = utils.unitatxy(Player(PLAYER_NEUTRAL_AGGRESSIVE), tx, ty, 'n024', math.random(0,360))
local ai = ai.miniboss:new(u, 4.72, "spell", true, 2)
ai.spellfunc = function()
shrine:mini_boss_spell_mortar(ai.unit, ai.trackedunit, cr_elefirelesr, speff_conflagoj, dmg_shadow, mis_voidballhge, 16)
end
end)
end, true)
-- h00P -- Arcane Relic
shrine_a_arcane = shrine:new("h00P", function(u, ux, uy, tx, ty)
speff_judge:play(tx, ty)
utils.timed(0.78, function()
local u = utils.unitatxy(Player(PLAYER_NEUTRAL_AGGRESSIVE), tx, ty, 'n020', math.random(0,360))
local ai = ai.miniboss:new(u, 4.72, "spell", true, 2)
ai.spellfunc = function()
shrine:mini_boss_spell_mortar(ai.unit, ai.trackedunit, sh_arcshard, mis_boltblue, dmg_arcane, mis_boltblue, 16)
end
end)
end, true)
-- h00M -- Lost Tomb
shrine_a_losttomb = shrine:new("h00M", function(u, ux, uy, tx, ty)
speff_judge:play(tx, ty)
utils.timed(0.78, function()
local u = utils.unitatxy(Player(PLAYER_NEUTRAL_AGGRESSIVE), tx, ty, 'n025', math.random(0,360))
local ai = ai.miniboss:new(u, 4.72, "spell", true, 2)
ai.spellfunc = function()
shrine:mini_boss_spell_mortar(ai.unit, ai.trackedunit, cr_skele_arch, speff_conflagoj, dmg_phys, mis_boltred, 16)
end
end)
end, true)
-- h00X -- Brood Dragon Roost
shrine_a_brooddrag = shrine:new("h00X", function(u, ux, uy, tx, ty)
speff_judge:play(tx, ty)
utils.timed(0.78, function()
local u = utils.unitatxy(Player(PLAYER_NEUTRAL_AGGRESSIVE), tx, ty, 'n021', math.random(0,360))
local ai = ai.miniboss:new(u, 4.72, "spell", true, 2)
ai.spellfunc = function()
shrine:mini_boss_spell_mortar(ai.unit, ai.trackedunit, sh_dragbrood, speff_conflaggn, dmg_nature, mis_bat, 16)
end
end)
end, true)
-- h00Z -- Frost Dragon Roost
shrine_a_frostdrag = shrine:new("h00Z", function(u, ux, uy, tx, ty)
speff_judge:play(tx, ty)
utils.timed(0.78, function()
local u = utils.unitatxy(Player(PLAYER_NEUTRAL_AGGRESSIVE), tx, ty, 'n022', math.random(0,360))
local ai = ai.miniboss:new(u, 4.72, "spell", true, 2)
ai.spellfunc = function()
shrine:mini_boss_spell_mortar(ai.unit, ai.trackedunit, sh_dragfrost, speff_iceblast, dmg_frost, mis_blizzard, 16)
end
end)
end, true)
-- h00Y -- Fire Dragon Roost
shrine_a_firedrag = shrine:new("h00Y", function(u, ux, uy, tx, ty)
speff_judge:play(tx, ty)
utils.timed(0.78, function()
local u = utils.unitatxy(Player(PLAYER_NEUTRAL_AGGRESSIVE), tx, ty, 'n023', math.random(0,360))
local ai = ai.miniboss:new(u, 4.72, "spell", true, 2)
ai.spellfunc = function()
shrine:mini_boss_spell_mortar(ai.unit, ai.trackedunit, sh_dragfire, speff_conflagoj, dmg_fire, mis_fireballbig, 16)
end
end)
end, true)
-- h010 -- Marrow Hut
shrine_a_marrowhut = shrine:new("h010", function(u, ux, uy, tx, ty)
speff_judge:play(tx, ty)
utils.timed(0.78, function()
local u = utils.unitatxy(Player(PLAYER_NEUTRAL_AGGRESSIVE), tx, ty, 'n02A', math.random(0,360))
local ai = ai.miniboss:new(u, 4.72, "spell", true, 2)
ai.spellfunc = function()
shrine:mini_boss_spell_mortar(ai.unit, ai.trackedunit, sh_fossloot, speff_quake, dmg_phys, mis_rock, 16)
end
end)
end, true)
-- add to rarity tables:
for biomeid = 1,5 do shrine.biome[biomeid] = {} end
shrine.biome:add_all("common", {shrine_tchest, shrine_mine, shrine_hero, shrine_blood, shrine_enchant, shrine_abyss, shrine_brut, shrine_blessed, shrine_waxaltar})
shrine.biome:add_all("rare", {shrine_tchest, shrine_arcprison})
shrine.biome:add_all("ancient", {shrine_a_demonic, shrine_anctchest, shrine_anctcrys})
shrine.biome:add(biome_id_foss, "ancient", {shrine_a_marrowhut})
shrine.biome:add(biome_id_slag, "ancient", {shrine_a_firedrag})
shrine.biome:add(biome_id_mire, "ancient", {shrine_a_brooddrag})
shrine.biome:add(biome_id_ice, "ancient", {shrine_a_frostdrag})
shrine.biome:add(biome_id_vault, "ancient", {shrine_a_arcane})
self.trig = trg:new("attacked", Player(PLAYER_NEUTRAL_AGGRESSIVE))
self.trig:regaction(function()
utils.debugfunc(function()
local t, u = utils.trigu(), GetAttacker()
-- if utils.powner(GetOrderTargetUnit()) == Player(PLAYER_NEUTRAL_AGGRESSIVE) and shrine:checkorder() and shrine:checkrange(t, u) then
if utils.powner(t) == Player(PLAYER_NEUTRAL_AGGRESSIVE) then
if self.units[utils.data(t)] and not self.units[utils.data(t)].exhausted then
local id = GetUnitTypeId(t)
local tx, ty = utils.unitxy(t)
local ux, uy = utils.unitxy(u)
if self.stack[id] then
self.stack[id].clickfunc(u, ux, uy, tx, ty)
utils.palert(utils.powner(u), "Shrine Activated!", nil, true)
if self.stack[id].dies then
KillUnit(t)
else
utils.playsoundall(kui.sound.shrine)
SetUnitVertexColor(t, 100, 100, 100, 255)
self.units[utils.data(t)].exhausted = true
SetUnitAnimation(t, self.stack[id].clickanim)
utils.timed(2.0, function()
SetUnitTimeScale(t, 0)
end)
utils.setinvul(t, true)
BlzSetUnitStringField(t, UNIT_SF_NAME, self.units[utils.data(t)].prevname.."|n|cffaaaaaa<Exhausted>|r")
end
if self.stack[id].effect then self.stack[id].effect:play(tx, ty) end
end
end
end
end, "shrine click")
end)
end
function shrine:mini_boss_spell_mortar(unit, trackedunit, minion, minioneffect, dmgtypeid, miseffect, damage)
-- launches mortar missiles at the tracked player and summons minions.
utils.timedrepeat(0.63, 3, function()
utils.debugfunc(function()
if utils.isalive(unit) then
local x,y = utils.unitprojectxy(trackedunit, math.random(0,100), math.random(0,360))
boss:marker(x, y)
local mis = missile:create_arc(x, y, unit, miseffect.effect, boss:get_dmg(damage), nil, 0.93)
mis.dmgtype = dmg.type.stack[dmgtypeid]
mis.arch = 450.0
mis.explr = 132.0
x,y = utils.unitprojectxy(unit, math.random(200,500), math.random(0,360))
minioneffect:play(x,y)
utils.issatkxy(minion:create(x, y), utils.unitxy(trackedunit))
end
end, "mini boss spell")
end)
end
function shrine:checkrange(tar, clicker)
return utils.isalive(tar) and utils.isalive(clicker) and utils.distunits(tar, clicker) < 235.0
end
function shrine:checkorder()
return (GetIssuedOrderId() == 851986 or GetIssuedOrderId() == 851971)
and utils.pnum(utils.powner(utils.trigu())) <= kk.maxplayers and kobold.player[utils.powner(utils.trigu())]
end
function shrine:create(x, y)
if not shrine_dev_mode then
map.grid:excavate(x, y, 768, t_udgrd_tile)
map.grid:clearunits(x, y, 768, true)
end
local u = utils.unitatxy(Player(PLAYER_NEUTRAL_AGGRESSIVE), x, y, self.id)
if self.anim then SetUnitAnimation(u, self.anim) end
self.units[utils.data(u)] = {}
self.units[utils.data(u)].prevname = BlzGetUnitStringField(u, UNIT_SF_NAME)
BlzSetUnitStringField(u, UNIT_SF_NAME, self.units[utils.data(u)].prevname.."|n|cff00ff00<Attack to Activate>|r")
end
function shrine:placerandom(x, y, _biomeid)
local biomeid = _biomeid or map.manager.biome.id
local roll = 0
local rarity = ""
-- see if ancient is placed:
if math.random(0,100) < 25 then
rarity = "ancient"
-- see if rare is placed:
elseif math.random(0,100) < 50 then
rarity = "rare"
-- see if common is placed:
elseif math.random(0,100) < 90 then
rarity = "common"
end
if rarity ~= "" then
roll = math.random(1,#shrine.biome[biomeid][rarity])
shrine.biome[biomeid][rarity][roll]:create(x, y)
end
end
-- @anim = animation to play after creation.
function shrine:new(id, clickfunc, diesonclick, _effect, _anim, _clickanim)
local o = setmetatable({}, self)
o.id = id
o.dies = diesonclick
o.clickfunc = clickfunc
o.effect = _effect or nil
o.anim = _anim or nil
o.clickanim = _clickanim or nil
self.stack[FourCC(id)] = o
self.devstack[#self.devstack+1] = o
return o
end
function shrine:collectfragment()
if GetItemTypeId(GetManipulatedItem()) == FourCC("I00F") then
utils.playerloop(function(p) kobold.player[p]:awardfragment(1) end)
end
end
-- add shrine to a single biome.
function shrine.biome:add(biomeid, rarity, shrinest)
if not shrine.biome[biomeid][rarity] then shrine.biome[biomeid][rarity] = {} end
for i,v in ipairs(shrinest) do
shrine.biome[biomeid][rarity][#shrine.biome[biomeid][rarity]+1] = v
end
end
-- add shrine to every biome.
function shrine.biome:add_all(rarity, shrinest)
for biomeid,_ in ipairs(shrine.biome) do
shrine.biome:add(biomeid, rarity, shrinest)
end
end
function shrine:cleanup()
for i,v in pairs(self.units) do
self.units[i] = nil
end
end
function shrine:print_all()
for biomeid = 1,5 do
print("biomeid",biomeid)
for rarityname,rarityt in pairs(shrine.biome[biomeid]) do
if not rarityt[1] then
print(rarityname,"is empty")
else
print(rarityname,"contains:")
end
for i,v in ipairs(rarityt) do
print(i,v,v.id)
end
end
print("")
end
end
spell = {}
function spell:init()
-- init templates:
casttype_instant = 1 -- instant cast, no target
casttype_unit = 2 -- target a unit
casttype_point = 3 -- target a location
casttype_missile = 4 -- target location to shoot projectile
casttype_lob = 5 -- target location with targeting image
self.interrupt = {}
self.hppotid = FourCC('A01O')
self.manapotid = FourCC('A01P')
self.__index = self
for pnum = 1,kk.maxplayers do
self.interrupt[pnum] = false
end
end
function spell:new(casttype, p, abilcode, _eventtype)
-- initialize a spell for a player.
local o = {}
local e = _eventtype or EVENT_PLAYER_UNIT_SPELL_EFFECT
setmetatable(o, self)
o.casttype = casttype
o.p = p
o.code = abilcode
o.trig = CreateTrigger()
o.cdtimer = NewTimer()
o.caster = nil
o.target = nil
o.casterx = nil
o.castery = nil
o.targetx = nil
o.targety = nil
o.actions = {}
o:addlistener(e)
if utils.pnum(p) <= kk.maxplayers then
o.pobj = kobold.player[p]
end
return o
end
function spell:addlistener(e)
TriggerRegisterPlayerUnitEvent(self.trig, self.p, e, nil)
self.actions[#self.actions+1] = function()
self.caster = GetTriggerUnit()
self.casterx,self.castery = utils.unitxy(self.caster)
end
if self.casttype == casttype_lob or self.casttype == casttype_point or self.casttype == casttype_missile then
self.actions[#self.actions+1] = function()
self.targetx,self.targety = utils.spellxy(self.target)
end
elseif self.casttype == casttype_unit then
self.actions[#self.actions+1] = function()
self.target = GetSpellTargetUnit()
self.targetx,self.targety = utils.unitxy(self.target)
end
end
TriggerAddAction(self.trig, function()
utils.debugfunc( function()
if GetSpellAbilityId() == self.code then
for _,func in ipairs(self.actions) do
func()
end
if self.cdfunc then
self.cdfunc()
end
if kobold.player[self.p] then
if self.code == self.hppotid or self.code == self.manapotid then
self:pstatpotion(kobold.player[self.p])
else
self:pstatcastepic(kobold.player[self.p])
loot.ancient:raiseevent(ancient_event_ability, kobold.player[self.p])
end
end
end
end, "spell action")
end)
end
function spell:addaction(func)
-- pass a single function or a table of functions.
utils.debugfunc(function()
self.actions[#self.actions+1] = func
end, "spell actions")
end
function spell:pstatcastepic(pobj)
local dmgid = 0
for epicid = p_epic_arcane_mis,p_epic_phys_mis do
dmgid = dmgid+1
if math.random(0,100) < 30 then
if pobj[epicid] > 0 then
local mis = self:pmissileangle(self:range(800.0), mis_ele_stack2[dmgid], pobj[epicid])
mis.angle = mis.angle - math.random(0,18) + math.random(0,18)
mis:initpiercing()
mis.vel = 24
mis.dmgtype = dmg.type.stack[dmgid]
end
end
end
dmgid = 0
for epicid = p_epic_arcane_aoe,p_epic_phys_aoe do
dmgid = dmgid+1
if math.random(0,100) < 25 then
if pobj[epicid] > 0 then
speff_expl_stack[dmgid]:play(self.casterx, self.castery, nil, 0.65)
spell:gdmgxy(self.p, dmg.type.stack[dmgid], pobj[epicid], self.casterx, self.castery, self:radius(300.0))
end
end
end
if pobj[p_epic_heal_aoe] > 0 then
if math.random(1,100) <= pobj[p_epic_heal_aoe] then
local grp = g:newbyxy(self.p, g_type_heal, self.casterx, self.castery, self:radius(10000.0))
grp:action(function()
speff_pothp:playu(grp.unit, nil, nil, 0.6)
utils.addlifep(grp.unit, 3.0, true, pobj.unit)
end)
grp:destroy()
end
end
if pobj[p_epic_dmg_reduct] > 0 and not pobj.dmgreductflag then
pobj.dmgreductflag = true
self:enhanceresistpure(pobj[p_epic_dmg_reduct], 3.0, self.p)
utils.timed(3.1, function() pobj.dmgreductflag = nil end)
end
if pobj[p_epic_demon] > 0 then
if math.random(1,100) <= 15 then
local x,y = utils.projectxy(self.casterx, self.castery, 128, math.random(0,360))
local unit =
ai:new(utils.unitatxy(pobj.p, x, y, 'n00J', utils.getface(self.caster)))
:initcompanion(900.0, pobj[p_epic_demon], p_stat_shadow)
:timed(12.0)
speff_deathpact:play(x, y)
end
end
if pobj[p_epic_aoe_stun] > 0 then
if math.random(1,100) <= pobj[p_epic_aoe_stun] then
speff_warstomp:playu(pobj.unit)
local grp = g:newbyxy(self.p, g_type_dmg, self.casterx, self.castery, self:radius(300.0))
spell:gbuff(grp, bf_stun, 2.0)
grp:destroy()
end
end
end
function spell:pstatpotion(pobj)
-- health potion effects:
if self.code == self.hppotid then
if pobj[p_potion_life] > 0 then
speff_rejuv:attachu(pobj.unit, 10.0, 'origin', 0.7)
utils.timedrepeat(1.0, 10, function()
utils.addlifep(pobj.unit, pobj[p_potion_life]/10, true, pobj.unit)
end)
end
if pobj[p_potion_fire_res] > 0 then
speff_ember:attachu(pobj.unit, 6.0, 'chest', 0.7)
self:enhanceresist(dmg_fire, pobj[p_potion_fire_res], 6.0, self.p, true)
end
if pobj[p_potion_frost_res] > 0 then
speff_emberfro:attachu(pobj.unit, 6.0, 'chest', 0.7)
self:enhanceresist(dmg_frost, pobj[p_potion_frost_res], 6.0, self.p, true)
end
if pobj[p_potion_nature_res] > 0 then
speff_embernat:attachu(pobj.unit, 6.0, 'chest', 0.7)
self:enhanceresist(dmg_nature, pobj[p_potion_nature_res], 6.0, self.p, true)
end
if pobj[p_potion_shadow_res] > 0 then
speff_embersha:attachu(pobj.unit, 6.0, 'chest', 0.7)
self:enhanceresist(dmg_shadow, pobj[p_potion_shadow_res], 6.0, self.p, true)
end
if pobj[p_potion_arcane_res] > 0 then
speff_emberarc:attachu(pobj.unit, 6.0, 'chest', 0.7)
self:enhanceresist(dmg_arcane, pobj[p_potion_arcane_res], 6.0, self.p, true)
end
if pobj[p_potion_phys_res] > 0 then
speff_emberphy:attachu(pobj.unit, 6.0, 'chest', 0.7)
self:enhanceresist(dmg_phys, pobj[p_potion_phys_res], 6.0, self.p, true)
end
if pobj[p_potion_aoe_stun] > 0 then
speff_warstomp:playu(pobj.unit)
local grp = g:newbyxy(self.p, g_type_dmg, self.casterx, self.castery, self:radius(300.0))
spell:gbuff(grp, bf_stun, pobj[p_potion_aoe_stun])
grp:destroy()
end
if pobj[p_potion_aoe_heal] > 0 then
local grp = g:newbyxy(self.p, g_type_heal, self.casterx, self.castery, self:radius(1000.0))
grp:action(function()
if grp.unit ~= pobj.unit and utils.ishero(grp.unit) then
utils.addlifep(grp.unit, pobj[p_potion_aoe_heal], true, pobj.unit)
speff_pothp:playu(grp.unit)
end
end)
grp:destroy()
end
if pobj[p_potion_aoe_slow] > 0 then
speff_quake:playu(pobj.unit)
local grp = g:newbyxy(self.p, g_type_dmg, self.casterx, self.castery, self:radius(300.0))
spell:gbuff(grp, bf_slow, pobj[p_potion_aoe_slow])
grp:destroy()
end
-- mana potion effects:
else
if pobj[p_potion_mana] > 0 then
speff_erejuv:attachu(pobj.unit, 10, 'origin', 0.7)
utils.timedrepeat(1.0, 10, function()
utils.addmanap(pobj.unit, pobj[p_potion_mana]/10, true, pobj.unit)
end)
end
if pobj[p_potion_fire_dmg] > 0 then
speff_ember:attachu(pobj.unit, 6.0, 'chest', 0.7)
self:enhancedmgtype(dmg_fire, pobj[p_potion_fire_dmg], 6.0, self.p)
end
if pobj[p_potion_frost_dmg] > 0 then
speff_emberfro:attachu(pobj.unit, 6.0, 'chest', 0.7)
self:enhancedmgtype(dmg_frost, pobj[p_potion_frost_dmg], 6.0, self.p)
end
if pobj[p_potion_nature_dmg] > 0 then
speff_embernat:attachu(pobj.unit, 6.0, 'chest', 0.7)
self:enhancedmgtype(dmg_nature, pobj[p_potion_nature_dmg], 6.0, self.p)
end
if pobj[p_potion_shadow_dmg] > 0 then
speff_embersha:attachu(pobj.unit, 6.0, 'chest', 0.7)
self:enhancedmgtype(dmg_shadow, pobj[p_potion_shadow_dmg], 6.0, self.p)
end
if pobj[p_potion_arcane_dmg] > 0 then
speff_emberarc:attachu(pobj.unit, 6.0, 'chest', 0.7)
self:enhancedmgtype(dmg_arcane, pobj[p_potion_arcane_dmg], 6.0, self.p)
end
if pobj[p_potion_phys_dmg] > 0 then
speff_emberphy:attachu(pobj.unit, 6.0, 'chest', 0.7)
self:enhancedmgtype(dmg_phys, pobj[p_potion_phys_dmg], 6.0, self.p)
end
if pobj[p_potion_aoe_mana] > 0 then
local grp = g:newbyxy(self.p, g_type_heal, self.casterx, self.castery, self:radius(1000.0))
grp:action(function()
if grp.unit ~= pobj.unit and utils.ishero(grp.unit) then
utils.addmanap(grp.unit, pobj[p_potion_aoe_mana], true, pobj.unit)
speff_potmana:playu(grp.unit)
end
end)
grp:destroy()
end
end
-- either potion effects:
if pobj[p_potion_dmgr] > 0 then
self:enhanceresistpure(pobj[p_potion_dmgr], 6.0, self.p)
end
if pobj[p_potion_absorb] > 0 then
dmg.absorb:new(pobj.unit, 6.0, { all = utils.maxlife(pobj.unit)*pobj[p_potion_absorb]/100 }, speff_eshield, nil)
end
if pobj[p_potion_armor] > 0 then
speff_silrejuv:attachu(pobj.unit, 8.0, 'origin', 0.7)
self:enhancepstat(p_stat_armor, math.floor(pobj[p_stat_armor]*(pobj[p_potion_armor]/100)), 8.0, self.p)
end
if pobj[p_potion_dodge] > 0 then
speff_silrejuv:attachu(pobj.unit, 8.0, 'origin', 0.7)
self:enhancepstat(p_stat_dodge, math.floor(pobj[p_stat_dodge]*(pobj[p_potion_dodge]/100)), 8.0, self.p)
end
if pobj[p_potion_lifesteal] > 0 then
speff_redrejuv:attachu(pobj.unit, 8.0, 'origin', 0.7)
self:enhancepstat(p_stat_elels, pobj[p_potion_lifesteal], 8.0, self.p)
self:enhancepstat(p_stat_physls, pobj[p_potion_lifesteal], 8.0, self.p)
end
if pobj[p_potion_thorns] > 0 then
speff_yelrejuv:attachu(pobj.unit, 8.0, 'origin', 0.7)
self:enhancepstat(p_stat_thorns, pobj[p_potion_thorns], 8.0, self.p)
self:enhancepstat(p_stat_thorns, math.floor(pobj[p_stat_thorns]*(pobj[p_potion_thorns]/100)), 8.0, self.p)
end
end
-- @_caster,_@code = overrides when using 1-4 dummy abilities.
function spell:uiregcooldown(cdval, hotkeyid, _fillw, _fillh, _caster, _code)
if cdval and cdval > 0 then
local fillw, fillh = _fillw or 63, _fillh or 63
self.casterf, self.codef = _caster or nil, _code or nil
self.uislot = kui.canvas.game.skill.skill[hotkeyid].cdfill
self.uitxt = kui.canvas.game.skill.skill[hotkeyid].cdtxt
self.cd = cdval
self.cdactive = false
self.cdfunc = nil -- if reassigned, need to destroy previous function.
fillw, fillh = kui:px(fillw), kui:px(fillh)
self.cdfunc = function()
local caster, code = self.casterf or self.caster, self.codef or self.code
if not self.cdactive and BlzGetUnitAbility(caster, code) then
self.cdactive = true
local total = BlzGetUnitAbilityCooldown(caster, code, 0)
local counter = 0
TimerStart(self.cdtimer, 0.05, true, function()
if self.cdactive and BlzGetUnitAbility(caster, code) then
counter = counter + 0.05
if self:islocalp(caster) then
self.uitxt:settext(math.ceil(total-counter))
self.uislot:setsize((1-counter/total)*fillw, (1-counter/total)*fillh)
end
if counter >= total then
self.cdactive = false
if self:islocalp(caster) then self.uislot:hide() self.uitxt:settext("") end
PauseTimer(self.cdtimer)
end
else
if self:islocalp(caster) then self.uislot:hide() self.uitxt:settext("") end
PauseTimer(self.cdtimer)
end
end)
if self:islocalp(caster) then self.uislot:show() self.uitxt:settext(math.ceil(total)) end
else
self.cdactive = false
if self:islocalp(caster) then self.uislot:hide() self.uitxt:settext("") end
PauseTimer(self.cdtimer)
end
end
end
end
function spell:islocalp(caster)
return utils.powner(caster) == utils.localp()
end
function spell:radius(val)
return math.floor(val*(1+kobold.player[self.p][p_stat_abilarea]/100))
end
function spell:range(val)
return math.floor(val*(1+kobold.player[self.p][p_stat_mislrange]/100))
end
function spell:targetxy()
return self.targetx, self.targety
end
-- @p = damage owner
function spell:tdmg(p, t, dmgtype, amount)
dmgtype:pdeal(p, amount, t)
dmgtype.effect:playu(t)
end
function spell:theal(p, t, amount, _suppresseff)
if utils.powner(t) ~= Player(PLAYER_NEUTRAL_PASSIVE) then
local amount = amount*(1+kobold.player[p][p_stat_healing]/100)
if not _suppresseff then
speff_lightheal:attachu(t, 0.0, 'origin')
end
utils.addlifeval(t, amount, true, kobold.player[p].unit)
kobold.player[p].score[4] = kobold.player[p].score[4] + amount
end
end
-- damage a group.
-- @p = damage owner
function spell:gdmg(p, grp, dmgtype, amount, _func)
grp:action(function()
if utils.isalive(grp.unit) then
dmgtype:pdeal(p, amount, grp.unit)
dmgtype.effect:playu(grp.unit)
if _func then
_func(grp.unit)
end
end
end)
end
-- damage a temp group then recycle it
-- @p = damage owner
function spell:gdmgxy(p, dmgtype, amount, x, y, r, _func)
local grp = g:newbyxy(p, g_type_dmg, x, y, r)
grp:action(function()
if utils.isalive(grp.unit) then
dmgtype:pdeal(p, amount, grp.unit)
dmgtype.effect:playu(grp.unit)
if _func then
_func(grp.unit)
end
end
end)
grp:destroy()
end
-- damage a temp group for neutral aggressive then recycle it.
function spell:creepgdmgxy(owner, dmgtype, amount, x, y, r, _func)
local grp = g:newbyxy(Player(PLAYER_NEUTRAL_AGGRESSIVE), g_type_dmg, x, y, r)
grp:action(function()
if utils.isalive(grp.unit) then
dmgtype:dealdirect(owner, grp.unit, amount)
dmgtype.effect:playu(grp.unit)
if _func then
_func(grp.unit)
end
end
end)
grp:destroy()
end
-- heal a temp group then recycle it
-- @p = heal owner
function spell:ghealxy(p, amount, x, y, r, _func)
local grp = g:newbyxy(p, g_type_heal, x, y, r)
grp:action(function()
if utils.isalive(grp.unit) and utils.powner(grp.unit) ~= Player(PLAYER_NEUTRAL_PASSIVE) then
spell:theal(p, grp.unit, amount)
if _func then
_func(grp.unit)
end
end
end)
grp:destroy()
end
function spell:gbuff(grp, bf_type, dur, _preventstacking)
grp:action(function()
bf_type:apply(grp.unit, dur, _preventstacking or nil)
end)
end
-- @unit = unit to move.
-- @range = intended max distance (to calc slide time).
-- @dur = duration to get to x,y.
-- @x,y = target point to leap to.
-- @functakeoff = function to run when effect starts.
-- @funcland = function to run when effect ends.
-- returns boolean
function spell:slide(unit, range, dur, x, y, functakeoff, funcland)
local unit = unit -- declare so var isn't anonymous during timer.
local x1,y1 = x,y
local x2,y2 = utils.unitxy(unit)
local angle = utils.anglexy(x2,y2,x1,y1)
local dist = utils.distxy(x1,y1,x2,y2)
local dur = dur*dist/range
local vel = dist/(dur/0.03)
local i = dur/0.03
local id = GetSpellAbilityId() -- store for timer
local pnum = utils.pnum(utils.powner(unit))
if not IsTerrainPathable(x1,y1,PATHING_TYPE_WALKABILITY) then -- pathable, allow (pathing function returns opposite bool)
SetUnitFacing(unit, angle) SetUnitPathing(unit, false) ResetUnitAnimation(unit) SetUnitAnimationByIndex(unit, 2)
if functakeoff ~= nil then functakeoff() end
TimerStart(NewTimer(),0.03,true,function() -- leap
utils.debugfunc(function()
if i > 0 and IsUnitAliveBJ(unit) and not spell.interrupt[pnum] and not kobold.player[utils.powner(unit)].downed then
i = i-1
x2,y2 = utils.projectxy(x2, y2, vel, angle)
utils.setxy(unit, x2, y2)
else
spell.interrupt[pnum] = false
if IsUnitAliveBJ(unit) and funcland ~= nil then utils.debugfunc(funcland, "funcland") end
ResetUnitAnimation(unit) SetUnitPathing(unit, true) ReleaseTimer()
end
end, "slide timer")
end)
return true
else -- not pathable, reset abil cd
utils.palert(utils.powner(unit), 'Target area is unreachable!')
TimerStart(NewTimer(), 0.5,false, function() BlzEndUnitAbilityCooldown(unit, id) end)
return false
end
end
-- get the caster's angle to the target unit or point location of a spell.
function spell:angletotarget()
return utils.anglexy(self.casterx, self.castery, self.targetx, self.targety)
end
-- get the caster's angle to the target unit or point location of a spell.
function spell:disttotarget()
return utils.distxy(self.casterx, self.castery, self.targetx, self.targety)
end
-- if a spell is cast at a point, but the effect distance is fixed in that direction, get that distance.
function spell:fixeddistxy(dist)
return utils.projectxy(self.casterx, self.castery, dist, self:angletotarget())
end
-- reduce @unit's armor (primary damage resistance) by @x for @dur sec.
function spell:armorpenalty(unit, x, dur, _speffect)
local x, unit = x, unit
if _speffect then
_speffect:attachu(unit, dur)
end
BlzSetUnitArmor(unit, BlzGetUnitArmor(unit) - x)
utils.timed(dur, function() if unit then BlzSetUnitArmor(unit, BlzGetUnitArmor(unit) + x) end end)
end
-- boost a player stat for a period.
-- @dmgtypeid = 1-6 damage id number.
function spell:enhancepstat(p_stat, val, dur, _p, _ignorebuff)
local p, val = _p or self.p, val
kobold.player[p][p_stat] = kobold.player[p][p_stat] + val
utils.timed(dur, function()
kobold.player[p][p_stat] = kobold.player[p][p_stat] - val
tooltip:updatecharpage(p)
end)
tooltip:updatecharpage(p)
kobold.player[p]:updateallstats()
if not _ignorebuff and val > 0 then
buffy:new_p_stat_indicator(p, p_stat, dur)
end
end
-- boost a player's bonus damage for a period
-- @dmgtypeid = 1-6 damage id number.
function spell:enhancedmgtype(dmgtypeid, val, dur, _p, _ignorebuff)
local p, val = _p or self.p, val
local s = p_dmg_lookup[dmgtypeid]
kobold.player[p][s] = kobold.player[p][s] + val
utils.timed(dur, function()
kobold.player[p][s] = kobold.player[p][s] - val
tooltip:updatecharpage(p)
end)
tooltip:updatecharpage(p)
if not _ignorebuff and val > 0 then
buffy:new_p_stat_indicator(p, p_dmg_lookup[dmgtypeid], dur)
end
end
-- boost a player's elemental resistance for a period.
-- @dmgtypeid = 1-6 damage id number.
function spell:enhanceresist(dmgtypeid, val, dur, _p, _showeffect, _ignorebuff)
local p, val = _p or self.p, val
local s = p_resist_lookup[dmgtypeid]
kobold.player[p][s] = kobold.player[p][s] + val
utils.timed(dur, function()
kobold.player[p][s] = kobold.player[p][s] - val
tooltip:updatecharpage(p)
end)
if _showeffect then sp_armor_boost[dmgtypeid]:attachu(kobold.player[p].unit, dur) end
tooltip:updatecharpage(p)
if not _ignorebuff and val > 0 then
buffy:new_p_stat_indicator(p, p_resist_lookup[dmgtypeid], dur)
end
end
-- boost ALL elemental resistance for a period (performance-minded).
-- @dmgtypeid = 1-6 damage id number.
function spell:enhanceresistall(val, dur, _p, _ignorebuff)
local p, val = _p or self.p, val
for dmgid = 1,6 do
kobold.player[p][p_resist_lookup[dmgid]] = kobold.player[p][p_resist_lookup[dmgid]] + val
end
utils.timed(dur, function()
for dmgid = 1,6 do
kobold.player[p][p_resist_lookup[dmgid]] = kobold.player[p][p_resist_lookup[dmgid]] - val
end
tooltip:updatecharpage(p)
end)
tooltip:updatecharpage(p)
if not _ignorebuff and val > 0 then
buffy:add_indicator(p, "Enhanced Resistances", "ReplaceableTextures\\CommandButtons\\BTN3M3.blp", dur, "Increased elemental resistances")
end
end
-- add an additional damage reduction effect on top of standard resistances
-- *note: this is always applied as the first step in dmg engine.
function spell:enhanceresistpure(val, dur, _p, _ignorebuff)
local p, val = _p or self.p, val
spell:enhancepstat(p_stat_dmg_reduct, val, dur, p, _ignorebuff or nil)
tooltip:updatecharpage(p)
end
--[[
wrapper functions for spells to create missiles:
--]]
function spell:pmissilepierce(dist, speffect, dmg)
local mis = self:pmissiletargetxy(dist, speffect, dmg)
mis:initpiercing()
return mis
end
-- send toward caster's facing angle.
-- @_angle = [optional] override the angle
function spell:pmissileangle(dist, speffect, dmg, _angle)
local a = _angle or GetUnitFacing(self.caster)
local mis = missile:create_angle(a, self:getmissilerange(dist), self.caster, speffect.effect, dmg)
return mis
end
-- send towards the targetx and targety of the spell cast.
function spell:pmissiletargetxy(dist, speffect, dmg)
local a = utils.anglexy(utils.unitx(self.caster), utils.unity(self.caster), self.targetx, self.targety)
local mis = missile:create_angle(a, self:getmissilerange(dist), self.caster, speffect.effect, dmg)
return mis
end
-- spawn a missile outside of a spell but using spell data (e.g. a summoned unit's missile).
function spell:pmissile_custom_unitxy(unit, dist, tarx, tary, speffect, dmg)
local a = utils.anglexy(utils.unitx(unit), utils.unity(unit), tarx, tary)
local mis = missile:create_angle(a, self:getmissilerange(dist), unit, speffect.effect, dmg)
return mis
end
-- a custom origin and target point, without player data injected.
function spell:pmissile_custom_xy(ownerunit, originx, originy, tarx, tary, speffect, dmg)
local a = utils.anglexy(originx, originy, tarx, tary)
local mis = missile:create_angle(a, utils.distxy(originx, originy, tarx, tary), ownerunit, speffect.effect, dmg)
return mis
end
-- @_tarx,@_tary = override spell cast point if needed.
-- send towards the targetx and targety of the spell cast.
function spell:pmissilearc(speffect, dmg, _tarx, _tary)
local tarx = _tarx or self.targetx
local tary = _tary or self.targety
local mis = missile:create_arc(tarx, tary, self.caster, speffect.effect, dmg)
return mis
end
function spell:getmissilerange(dist)
if kobold.player[self.p][p_stat_mislrange] > 0 then
return dist*(1+kobold.player[self.p][p_stat_mislrange]/100)
end
return dist
end
function spell:buildeffects()
speff_waxcan = speffect:new('war3mapImported\\mdl-Item_Canister_Yellow_Tier4.mdl')
-- shrine effects:
speff_holyglow = speffect:new('Abilities\\Spells\\Human\\HolyBolt\\HolyBoltSpecialArt.mdl')
speff_udexpl = speffect:new('Objects\\Spawnmodels\\Undead\\UCancelDeath\\UCancelDeath.mdl')
speff_rejuv = speffect:new('Abilities\\Spells\\NightElf\\Rejuvenation\\RejuvenationTarget.mdl')
speff_reveal = speffect:new('Abilities\\Spells\\Other\\Andt\\Andt.mdl')
speff_lust = speffect:new('Abilities\\Spells\\Orc\\Bloodlust\\BloodlustTarget.mdl')
speff_phxfire = speffect:new('Abilities\\Weapons\\PhoenixMissile\\Phoenix_Missile_mini.mdl')
speff_drainlf = speffect:new('Abilities\\Spells\\Other\\Drain\\DrainCaster.mdl')
speff_rescast = speffect:new('Abilities\\Spells\\Human\\Resurrect\\ResurrectCaster.mdl')
speff_fragment = speffect:new('Objects\\InventoryItems\\Glyph\\Glyph.mdl')
speff_tome = speffect:new('Abilities\\Spells\\Items\\TomeOfRetraining\\TomeOfRetrainingCaster.mdl')
-- ancient effects:
speff_liqfire = speffect:new('Abilities\\Spells\\Orc\\LiquidFire\\Liquidfire.mdl')
speff_reinc = speffect:new('Abilities\\Spells\\Orc\\Reincarnation\\ReincarnationTarget.mdl')
speff_coin = speffect:new('Objects\\InventoryItems\\PotofGold\\PotofGold.mdl')
speff_transmute = speffect:new('Abilities\\Spells\\Other\\Transmute\\GoldBottleMissile.mdl')
speff_animate = speffect:new('Abilities\\Spells\\Undead\\AnimateDead\\AnimateDeadTarget.mdl')
speff_icestep = speffect:new('Doodads\\Icecrown\\Terrain\\ClearIceRock\\ClearIceRock0.mdl')
speff_goldburst = speffect:new('Abilities\\Spells\\Other\\Transmute\\PileofGold.mdl')
-- boss effects:
speff_waterexpl = speffect:new('Objects\\Spawnmodels\\Naga\\NagaDeath\\NagaDeath.mdl')
speff_followmark= speffect:new('Abilities\\Spells\\Other\\Aneu\\AneuCaster.mdl')
speff_channel = speffect:new('war3mapImported\\mdl-ShockWave.mdx')
speff_darkness = speffect:new('war3mapImported\\mdl-Malevolence Aura Purple.mdx')
speff_ragered = speffect:new('war3mapImported\\mdl-Burning Rage Red.mdx')
speff_darkorb = speffect:new('Abilities\\Spells\\NightElf\\shadowstrike\\ShadowStrikeMissile.mdl')
speff_portal = speffect:new('doodads\\cinematic\\ShimmeringPortal\\ShimmeringPortal.mdl')
speff_psiyel = speffect:new('war3mapImported\\mdl-Psionic Shot Yellow.mdx')
speff_heavgate = speffect:new("war3mapImported\\mdl-Heaven's Gate Channel.mdx")
speff_judge = speffect:new("war3mapImported\\mdl-Judgement NoHive.mdx")
speff_bondgold = speffect:new("war3mapImported\\mdl-Bondage Gold SD.mdx")
speff_goldpile = speffect:new("war3mapImported\\mdl-Gold.mdx")
speff_goldchest = speffect:new("war3mapImported\\mdl-HQ Treasure Chest.mdx")
-- boss markers:
speff_bossmark = speffect:new('Doodads\\Cityscape\\Props\\MagicRunes\\MagicRunes0.mdl')
speff_bossmark2 = speffect:new('war3mapImported\\mdl-PulseTC.mdx')
speff_bossmark3 = speffect:new('UI\\Feedback\\Confirmation\\Confirmation.mdx')
speff_bossmark3.vertex = {0, 175, 255}
-- items:
speff_item = speffect:new('war3mapImported\\mdl-Item_Orb_Purity.mdl')
-- quest markers:
speff_quest1 = speffect:new('Abilities\\Spells\\Other\\TalkToMe\\TalkToMe.mdl')
speff_quest2 = speffect:new('Objects\\RandomObject\\RandomObject.mdl', 0.77)
-- damage registered hit effects by element:
speff_fire = speffect:new('Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl')
speff_frost = speffect:new('Abilities\\Weapons\\FrostWyrmMissile\\FrostWyrmMissile.mdl')
speff_nature = speffect:new('Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl')
speff_shadow = speffect:new('Abilities\\Weapons\\AvengerMissile\\AvengerMissile.mdl')
speff_phys = speffect:new('Abilities\\Weapons\\BallistaMissile\\BallistaMissile.mdl')
speff_arcane = speffect:new('Abilities\\Spells\\Human\\Feedback\\ArcaneTowerAttack.mdl')
-- elemental orbs:
speff_orbarcn = speffect:new('war3mapImported\\mdl-OrbDragonX.mdx')
speff_orbfrst = speffect:new('war3mapImported\\mdl-OrbIceX.mdx')
speff_orbnatr = speffect:new('war3mapImported\\mdl-OrbLightningX.mdx')
speff_orbfire = speffect:new('war3mapImported\\mdl-OrbFireX.mdx')
speff_orbshad = speffect:new('war3mapImported\\mdl-OrbDarknessX.mdx')
speff_orbphys = speffect:new('war3mapImported\\mdl-OrbBloodX.mdx')
-- area effects:
speff_sleet = speffect:new("war3mapImported\\mdl-Sleet Storm.mdx")
speff_reaperblu = speffect:new("war3mapImported\\mdl-Reaper's Claws Blue.mdx")
speff_reaperoj = speffect:new("war3mapImported\\mdl-Reaper's Claws Orange.mdx")
speff_reaperpurp= speffect:new("war3mapImported\\mdl-Reaper's Claws Purple.mdx")
speff_reaperred = speffect:new("war3mapImported\\mdl-Reaper's Claws Red.mdx")
speff_water = speffect:new('Abilities\\Spells\\Other\\CrushingWave\\CrushingWaveDamage.mdl')
speff_sacstorm = speffect:new('war3mapImported\\mdl-Sacred Storm.mdx')
speff_manastorm = speffect:new('war3mapImported\\mdl-Mana Storm.mdx')
speff_eldritch = speffect:new('war3mapImported\\mdl-Eldritch Scroll.mdl')
speff_flare = speffect:new('war3mapImported\\mdl-Shining Flare.mdx')
speff_steamtank = speffect:new('Abilities\\Weapons\\SteamTank\\SteamTankImpact.mdl')
speff_doom = speffect:new('Abilities\\Spells\\Other\\Doom\\DoomDeath.mdl')
speff_beamred = speffect:new('war3mapImported\\mdl-Soul Beam Red.mdx')
speff_warstomp = speffect:new('Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl')
speff_fireroar = speffect:new('war3mapImported\\mdl-Damnation Orange.mdx')
speff_roar = speffect:new('Abilities\\Spells\\NightElf\\BattleRoar\\RoarCaster.mdl')
speff_edict = speffect:new('war3mapImported\\mdl-Divine Edict.mdx')
speff_gravblue = speffect:new('war3mapImported\\mdl-Soul Discharge Blue.mdx')
speff_voidaura = speffect:new('war3mapImported\\mdl-Void Rift Purple.mdx')
speff_quake = speffect:new('Abilities\\Spells\\Human\\Thunderclap\\ThunderClapCaster.mdl')
speff_firebomb = speffect:new('Abilities\\Spells\\Other\\Incinerate\\FireLordDeathExplode.mdl')
speff_explfire = speffect:new('Abilities\\Weapons\\LordofFlameMissile\\LordofFlameMissile.mdl', 1.33)
speff_voidpool = speffect:new('war3mapImported\\mdl-Void Disc.mdx')
speff_frostnova = speffect:new('Abilities\\Spells\\Undead\\FrostNova\\FrostNovaTarget.mdl')
speff_iceblock = speffect:new('Abilities\\Spells\\Undead\\FreezingBreath\\FreezingBreathTargetArt.mdl')
speff_dustcloud = speffect:new('Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl')
speff_dirtexpl = speffect:new('war3mapImported\\mdl-NewDirtEXNofire.mdx')
speff_cataexpl = speffect:new('abilities\\weapons\\catapult\\catapultmissile.mdl')
speff_demoexpl = speffect:new('Abilities\\Weapons\\DemolisherFireMissile\\DemolisherFireMissile.mdl')
speff_marker = speffect:new('Abilities\\Spells\\Orc\\WarDrums\\DrumsCasterHeal.mdl', 1.25)
speff_marker.death = false
speff_stormlght = speffect:new('war3mapImported\\mdl-Stormfall.mdx')
speff_stormarc = speffect:new('war3mapImported\\mdl-Stormfall.mdx')
speff_stormarc.vertex = {200, 100, 200}
speff_stormfrost= speffect:new('war3mapImported\\mdl-Stormfall.mdx')
speff_stormnat = speffect:new('war3mapImported\\mdl-Stormfall Fel.mdx')
speff_stormfire = speffect:new('war3mapImported\\mdl-Stormfall Orange.mdx')
speff_stormshad = speffect:new('war3mapImported\\mdl-Stormfall Void.mdx')
speff_stormphys = speffect:new('war3mapImported\\mdl-Stormfall Red.mdx')
area_storm_stack={
[1] = speff_stormarc,
[2] = speff_stormfrost,
[3] = speff_stormnat,
[4] = speff_stormfire,
[5] = speff_stormshad,
[6] = speff_stormphys,
}
speff_exploj = speffect:new('war3mapImported\\mdl-Singularity I Orange.mdx')
speff_explblu = speffect:new('war3mapImported\\mdl-Singularity I Blue.mdx')
speff_explgrn = speffect:new('war3mapImported\\mdl-Singularity I Green.mdx')
speff_explpurp = speffect:new('war3mapImported\\mdl-Singularity I Purple.mdx')
speff_explred = speffect:new('war3mapImported\\mdl-Singularity I Red.mdx')
speff_explwht = speffect:new('war3mapImported\\mdl-Singularity I White.mdx')
speff_expl_stack= { -- table for pulling random elemental damage effects.
[1] = speff_explblu,
[2] = speff_explwht,
[3] = speff_explgrn,
[4] = speff_exploj,
[5] = speff_explpurp,
[6] = speff_explred,
}
-- potion effects:
speff_pothp = speffect:new('Heal.mdx')
speff_potmana = speffect:new('Heal Blue.mdx')
-- attached effects:
speff_dispel = speffect:new('Abilities\\Spells\\Human\\DispelMagic\\DispelMagicTarget.mdl')
speff_rejuv = speffect:new('war3mapImported\\mdl-Athelas Green.mdx')
speff_erejuv = speffect:new('war3mapImported\\mdl-Athelas Teal.mdx')
speff_orjrejuv = speffect:new('war3mapImported\\mdl-Athelas Orange.mdx')
speff_pinrejuv = speffect:new('war3mapImported\\mdl-Athelas Pink.mdx')
speff_redrejuv = speffect:new('war3mapImported\\mdl-Athelas Red.mdx')
speff_silrejuv = speffect:new('war3mapImported\\mdl-Athelas Silver.mdx')
speff_yelrejuv = speffect:new('war3mapImported\\mdl-Athelas Yellow.mdx')
speff_enhance = speffect:new('Abilities\\Spells\\Human\\Invisibility\\InvisibilityTarget.mdl')
speff_radglow = speffect:new('war3mapImported\\mdl-Soul Armor Radiant_opt.mdx')
speff_bluglow = speffect:new('war3mapImported\\mdl-Soul Armor Scribe_opt.mdx')
speff_charging = speffect:new('war3mapImported\\mdl-MagicLightning Aura.mdx')
speff_blazing = speffect:new('war3mapImported\\mdl-RunningFlame Aura.mdx')
speff_weakened = speffect:new('Abilities\\Spells\\Other\\HowlOfTerror\\HowlTarget.mdl')
speff_shield = speffect:new('Abilities\\Spells\\Human\\ManaFlare\\ManaFlareTarget.mdl', nil, 'overhead')
speff_fshield = speffect:new('war3mapImported\\mdl-RighteousGuard.mdx', nil, 'origin')
speff_eshield = speffect:new('war3mapImported\\mdl-MagicShield.mdx', nil, 'chest')
speff_uberblu = speffect:new('war3mapImported\\mdl-Ubershield Azure.mdx', nil, 'chest')
speff_uberorj = speffect:new('war3mapImported\\mdl-Ubershield Ember.mdx', nil, 'chest')
speff_ubergrn = speffect:new('war3mapImported\\mdl-Ubershield Spring.mdx', nil, 'chest')
speff_cleave = speffect:new('Abilities\\Spells\\Other\\Stampede\\StampedeMissileDeath.mdl', nil, 'chest')
speff_unitcirc = speffect:new('UI\\Feedback\\SelectionCircle\\SelectionCircle.mdl', nil, 'origin')
speff_unitcirc.death = false
-- elite unit markers:
speff_libarcane = speffect:new('war3mapImported\\mdl-Liberty Blue.mdx')
speff_libfrost = speffect:new('war3mapImported\\mdl-Liberty Silver.mdx')
speff_libnature = speffect:new('war3mapImported\\mdl-Liberty Green.mdx')
speff_libfire = speffect:new('war3mapImported\\mdl-Liberty Orange.mdx')
speff_libshadow = speffect:new('war3mapImported\\mdl-Liberty Purple.mdx')
speff_libphys = speffect:new('war3mapImported\\mdl-Liberty Red.mdx')
att_lib_glow = {
[1] = speff_libarcane,
[2] = speff_libfrost,
[3] = speff_libnature,
[4] = speff_libfire,
[5] = speff_libshadow,
[6] = speff_libphys,
}
-- healing:
speff_lightheal = speffect:new('Abilities\\Spells\\Items\\AIhe\\AIheTarget.mdl')
speff_resurrect = speffect:new('Abilities\\Spells\\Human\\Resurrect\\ResurrectTarget.mdl')
-- buff markers:
speff_debuffed = speffect:new('war3mapImported\\mdl-DarknessLeechTarget.mdx')
speff_boosted = speffect:new('Abilities\\Spells\\Orc\\AncestralSpirit\\AncestralSpiritCaster.mdl')
speff_burning = speffect:new('Environment\\LargeBuildingFire\\LargeBuildingFire1.mdl')
speff_radblue = speffect:new('war3mapImported\\mdl-Radiance Royal.mdx', nil, 'chest')
speff_radgreen = speffect:new('war3mapImported\\mdl-Radiance Nature.mdx', nil, 'chest')
speff_emberarc = speffect:new('war3mapImported\\mdl-Ember Blue.mdx')
speff_emberfro = speffect:new('war3mapImported\\mdl-Ember Teal.mdx')
speff_embernat = speffect:new('war3mapImported\\mdl-Ember Green.mdx')
speff_ember = speffect:new('war3mapImported\\mdl-Ember.mdx')
speff_embersha = speffect:new('war3mapImported\\mdl-Ember Purple.mdx')
speff_emberphy = speffect:new('war3mapImported\\mdl-Ember Red.mdx')
ember_eff_stack = {
[1] = speff_emberarc,
[2] = speff_emberfro,
[3] = speff_embernat,
[4] = speff_ember,
[5] = speff_embersha,
[6] = speff_emberphy,
}
-- target/misc:
speff_deathpact = speffect:new('Abilities\\Spells\\Undead\\DeathPact\\DeathPactTarget.mdl')
speff_bloodsplat= speffect:new('Objects\\Spawnmodels\\Human\\HumanLargeDeathExplode\\HumanLargeDeathExplode.mdl')
speff_generate = speffect:new('Abilities\\Spells\\Demon\\DarkPortalDarkPortalTarget.mdl')
speff_rainfire = speffect:new('war3mapImported\\mdl-Rain of Fire III.mdx')
speff_chargeoj = speffect:new('war3mapImported\\mdl-Valiant Charge.mdx')
speff_conflagoj = speffect:new('war3mapImported\\mdl-Conflagrate.mdx')
speff_conflagbl = speffect:new('war3mapImported\\mdl-Conflagrate Blue.mdx')
speff_conflaggn = speffect:new('war3mapImported\\mdl-Conflagrate Green.mdx')
speff_iceblast = speffect:new('war3mapImported\\mdl-Winter Blast SD.mdx')
speff_icecrystal= speffect:new('war3mapImported\\mdl-Frostcraft Crystal SD.mdx')
speff_iceshard = speffect:new('war3mapImported\\mdl-Ice Shard.mdx')
speff_ltstrike = speffect:new('war3mapImported\\mdl-LightningWrath.mdx')
speff_lightn = speffect:new('Abilities\\Weapons\\Bolt\\BoltImpact.mdl')
speff_iceburst2 = speffect:new('war3mapImported\\mdl-Pillar of Flame Blue.mdx')
speff_redburst = speffect:new('war3mapImported\\mdl-Pillar of Flame Orange.mdx')
speff_grnburst = speffect:new('war3mapImported\\mdl-Pillar of Flame Green.mdx')
speff_feral = speffect:new('Abilities\\Spells\\Orc\\FeralSpirit\\feralspirittarget.mdl')
speff_arcglaive = speffect:new('war3mapImported\\mdl-ArcaneGlaive_2.mdx')
speff_explode = speffect:new('Objects\\Spawnmodels\\Other\\NeutralBuildingExplosion\\NeutralBuildingExplosion.mdl')
speff_explode2 = speffect:new('war3mapImported\\mdl-NewGroundEX.mdl')
speff_explode2.scale = 0.75
speff_reveal = speffect:new('Abilities\\Spells\\Human\\MagicSentry\\MagicSentryCaster.mdl', nil, 'overhead')
speff_defend = speffect:new('Abilities\\Spells\\Human\\Defend\\DefendCaster.mdl')
speff_mine = speffect:new('units\\creeps\\GoblinLandMine\\GoblinLandMine.mdl')
speff_shroom = speffect:new('war3mapImported\\mdl-GreenFungus.mdx')
speff_goldburst = speffect:new('UI\\Feedback\\GoldCredit\\GoldCredit.mdl')
-- portals:
port_yellow = speffect:new('war3mapImported\\mdl-Void Teleport Yellow To.mdx')
port_yellowtp = speffect:new('war3mapImported\\mdl-Void Teleport Yellow Target.mdx')
-- missiles:
mis_bat = speffect:new('war3mapImported\\mdl-Carrion Bat Jade SD.mdx')
mis_blizzard = speffect:new('war3mapImported\\mdl-Blizzard II Missile.mdx')
mis_acid = speffect:new('Abilities\\Weapons\\ChimaeraAcidMissile\\ChimaeraAcidMissile.mdl')
mis_waterwave = speffect:new('Abilities\\Spells\\Other\\CrushingWave\\CrushingWaveMissile.mdl')
mis_waterele = speffect:new('Abilities\\Weapons\\WaterElementalMissile\\WaterElementalMissile.mdl')
mis_runic = speffect:new('war3mapImported\\mdl-Runic Rocket.mdx')
mis_lightnorb = speffect:new('Abilities\\Weapons\\FarseerMissile\\FarseerMissile.mdl')
mis_rock = speffect:new('Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl')
mis_rocket = speffect:new('Abilities\\Weapons\\Mortar\\MortarMissile.mdl')
mis_hammerred = speffect:new('war3mapImported\\mdl-Storm Bolt Red.mdx')
mis_hammergreen = speffect:new('war3mapImported\\mdl-Storm Bolt Green.mdx')
mis_grenadeblue = speffect:new('war3mapImported\\mdl-Chain Grenade Blue.mdx')
mis_grenadeoj = speffect:new('war3mapImported\\mdl-Chain Grenade Orange.mdx')
mis_grenadepurp = speffect:new('war3mapImported\\mdl-Chain Grenade Purple.mdx')
mis_grenadegrn = speffect:new('war3mapImported\\mdl-Chain Grenade Green.mdx')
mis_iceball = speffect:new('Abilities\\Weapons\\FrostWyrmMissile\\FrostWyrmMissile.mdl')
mis_icicle = speffect:new('Abilities\\Weapons\\LichMissile\\LichMissile.mdl')
mis_voidball = speffect:new('war3mapImported\\mdl-Voidbolt Rough Minor.mdx')
mis_voidballhge = speffect:new('war3mapImported\\mdl-Voidbolt Rough Major.mdx')
mis_fireball = speffect:new('war3mapImported\\mdl-Firebolt Medium.mdx')
mis_fireballbig = speffect:new('war3mapImported\\mdl-Fireball Medium.mdx')
mis_fireballhge = speffect:new('war3mapImported\\mdl-Firebolt Rough Major.mdx')
mis_boltyell = speffect:new('war3mapImported\\mdl-Shock Blast Yellow.mdx')
mis_boltblue = speffect:new('war3mapImported\\mdl-Shock Blast Blue.mdx')
mis_boltgreen = speffect:new('war3mapImported\\mdl-Shock Blast Green.mdx')
mis_boltoj = speffect:new('war3mapImported\\mdl-Shock Blast Orange.mdx')
mis_boltpurp = speffect:new('war3mapImported\\mdl-Shock Blast Purple.mdx')
mis_boltred = speffect:new('war3mapImported\\mdl-Shock Blast Red.mdx')
mis_bolt_stack = {
[1] = mis_boltyell,
[2] = mis_boltblue,
[3] = mis_boltgreen,
[4] = mis_boltoj,
[5] = mis_boltpurp,
[6] = mis_boltred,
}
mis_mortar_arc = speffect:new('Abilities\\Weapons\\DragonHawkMissile\\DragonHawkMissile.mdl')
mis_mortar_arc.scale = 2.0
mis_mortar_fro = speffect:new('Abilities\\Weapons\\FrostWyrmMissile\\FrostWyrmMissile.mdl')
mis_mortar_nat = speffect:new('Abilities\\Weapons\\GreenDragonMissile\\GreenDragonMissile.mdl')
mis_mortar_fir = speffect:new('Abilities\\Weapons\\RedDragonBreath\\RedDragonMissile.mdl')
mis_mortar_sha = speffect:new('Abilities\\Spells\\Undead\\DeathCoil\\DeathCoilMissile.mdl')
mis_mortar_phy = speffect:new('Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl')
mis_mortar_stack={
[1] = mis_mortar_arc,
[2] = mis_mortar_fro,
[3] = mis_mortar_nat,
[4] = mis_mortar_fir,
[5] = mis_mortar_sha,
[6] = mis_mortar_phy,
}
mis_shotarcane = speffect:new('war3mapImported\\mdl-Shot II Yellow.mdx')
mis_shotarcane.vertex = {255, 0, 255}
mis_shotblue = speffect:new('war3mapImported\\mdl-Shot II Blue.mdx')
mis_shotgreen = speffect:new('war3mapImported\\mdl-Shot II Green.mdx')
mis_shotred = speffect:new('war3mapImported\\mdl-Shot II Red.mdx')
mis_shotorange = speffect:new('war3mapImported\\mdl-Shot II Orange.mdx')
mis_shotpurple = speffect:new('war3mapImported\\mdl-Shot II Purple.mdx')
mis_ele_stack = { -- matches with kui data element settings to pull random missiles.
[1] = mis_shotarcane,
[2] = mis_shotblue,
[3] = mis_shotgreen,
[4] = mis_shotorange,
[5] = mis_shotpurple,
[6] = mis_shotred,
}
mis_shot_arcane = speffect:new('war3mapImported\\mdl-Firebrand Shot Blue.mdx')
mis_shot_frost = speffect:new('war3mapImported\\mdl-Firebrand Shot Silver.mdx')
mis_shot_nature = speffect:new('war3mapImported\\mdl-Firebrand Shot Green.mdx')
mis_shot_fire = speffect:new('war3mapImported\\mdl-Firebrand Shot Orange.mdx')
mis_shot_shadow = speffect:new('war3mapImported\\mdl-Firebrand Shot Purple.mdx')
mis_shot_phys = speffect:new('war3mapImported\\mdl-Firebrand Shot Red.mdx')
mis_ele_stack2 = { -- matches with kui data element settings to pull random missiles.
[1] = mis_shot_arcane,
[2] = mis_shot_frost,
[3] = mis_shot_nature,
[4] = mis_shot_fire,
[5] = mis_shot_shadow,
[6] = mis_shot_phys,
}
-- enchantment rawcodes:
sp_enchant = { -- see below for dmgtypeid (dmg_[name])
[1] = FourCC('A00F'), -- arcane dmg_arcane | 1
[2] = FourCC('A00D'), -- frost dmg_frost | 2
[3] = FourCC('A00A'), -- nature dmg_nature | 3
[4] = FourCC('A00B'), -- fire dmg_fire | 4
[5] = FourCC('A00C'), -- shadow dmg_shadow | 5
[6] = FourCC('A00E'), -- physical dmg_phys | 6
}
sp_enchant_elite = { -- see below for dmgtypeid (dmg_[name])
[1] = FourCC('A02G'), -- arcane dmg_arcane | 1
[2] = FourCC('A02F'), -- frost dmg_frost | 2
[3] = FourCC('A02E'), -- nature dmg_nature | 3
[4] = FourCC('A02C'), -- fire dmg_fire | 4
[5] = FourCC('A02D'), -- shadow dmg_shadow | 5
[6] = FourCC('A02H'), -- physical dmg_phys | 6
}
-- heavy enchantment effects:
sp_enchant_eff = {
[1] = speffect:new('war3mapImported\\mdl-Windwalk Blue Soul.mdx'),
[2] = speffect:new('war3mapImported\\mdl-Windwalk.mdx'),
[3] = speffect:new('war3mapImported\\mdl-Windwalk Necro Soul.mdx'),
[4] = speffect:new('war3mapImported\\mdl-Windwalk Fire.mdx'),
[5] = speffect:new('war3mapImported\\mdl-Shot II Purple.mdx'),
[6] = speffect:new('war3mapImported\\mdl-Windwalk Blood.mdx'),
}
-- proliferation effects:
orb_prolif_stack = {
[1] = speffect:new('war3mapImported\\mdl-Climax Blue.mdx'),
[2] = speffect:new('war3mapImported\\mdl-Climax Teal.mdx'),
[3] = speffect:new('war3mapImported\\mdl-Climax Green.mdx'),
[4] = speffect:new('war3mapImported\\mdl-Climax Orange.mdx'),
[5] = speffect:new('war3mapImported\\mdl-Climax Purple.mdx'),
[6] = speffect:new('war3mapImported\\mdl-Climax Red.mdx'),
}
-- damage modifiers:
sp_armor_boost = {
[1] = speffect:new('war3mapImported\\mdl-Armor Stimulus Pink.mdx'),
[2] = speffect:new('war3mapImported\\mdl-Armor Stimulus Blue.mdx'),
[3] = speffect:new('war3mapImported\\mdl-Armor Stimulus Green.mdx'),
[4] = speffect:new('war3mapImported\\mdl-Armor Stimulus Orange.mdx'),
[5] = speffect:new('war3mapImported\\mdl-Armor Stimulus Purple.mdx'),
[6] = speffect:new('war3mapImported\\mdl-Armor Stimulus Red.mdx'),
}
sp_armor_penalty = {
[1] = speffect:new('war3mapImported\\mdl-Armor Penetration Pink.mdx'),
[2] = speffect:new('war3mapImported\\mdl-Armor Penetration Blue.mdx'),
[3] = speffect:new('war3mapImported\\mdl-Armor Penetration Green.mdx'),
[4] = speffect:new('war3mapImported\\mdl-Armor Penetration Orange.mdx'),
[5] = speffect:new('war3mapImported\\mdl-Armor Penetration Purple.mdx'),
[6] = speffect:new('war3mapImported\\mdl-Armor Penetration Red.mdx'),
}
-- elemental orbs:
orb_eff_arcane = speffect:new('war3mapImported\\mdl-OrbDragonX.mdx')
orb_eff_frost = speffect:new('war3mapImported\\mdl-OrbIceX.mdx')
orb_eff_nature = speffect:new('war3mapImported\\mdl-OrbLightningX.mdx')
orb_eff_fire = speffect:new('war3mapImported\\mdl-OrbFireX.mdx')
orb_eff_shadow = speffect:new('war3mapImported\\mdl-OrbDarknessX.mdx')
orb_eff_phys = speffect:new('war3mapImported\\mdl-OrbBloodX.mdx')
orb_eff_stack = {
[1] = orb_eff_arcane,
[2] = orb_eff_frost,
[3] = orb_eff_nature,
[4] = orb_eff_fire,
[5] = orb_eff_shadow,
[6] = orb_eff_phys,
}
-- area effect markers:
area_mark_arcane = speffect:new('war3mapImported\\mdl-Longevity Aura Cosmic.mdx')
area_mark_frost = speffect:new('war3mapImported\\mdl-Longevity Aura Scribe.mdx')
area_mark_nature = speffect:new('war3mapImported\\mdl-Longevity Aura Spring.mdx')
area_mark_fire = speffect:new('war3mapImported\\mdl-Longevity Aura Divine.mdx')
area_mark_shadow = speffect:new('war3mapImported\\mdl-Longevity Aura Void.mdx')
area_mark_phys = speffect:new('war3mapImported\\mdl-Longevity Aura Crimson.mdx')
area_marker_stack = {
[1] = area_mark_arcane,
[2] = area_mark_frost,
[3] = area_mark_nature,
[4] = area_mark_fire,
[5] = area_mark_shadow,
[6] = area_mark_phys,
}
-- void portals:
speff_voidport = {
[1] = speffect:new('war3mapImported\\mdl-Void Teleport Blue To.mdx'),
[2] = speffect:new('war3mapImported\\mdl-Void Teleport Silver To.mdx'),
[3] = speffect:new('war3mapImported\\mdl-Void Teleport Green To.mdx'),
[4] = speffect:new('war3mapImported\\mdl-Void Teleport Orange To.mdx'),
[5] = speffect:new('war3mapImported\\mdl-Void Teleport To.mdx'),
[6] = speffect:new('war3mapImported\\mdl-Void Teleport Red To.mdx'),
}
-- tethers:
speff_gatearcane = speffect:new('war3mapImported\\mdl-Accelerator Gate Blue.mdx')
speff_gatearcane.vertex = {255, 0, 255}
speff_gatefrost = speffect:new('war3mapImported\\mdl-Accelerator Gate Blue.mdx')
speff_gatenature = speffect:new('war3mapImported\\mdl-Accelerator Gate Green.mdx')
speff_gatefire = speffect:new('war3mapImported\\mdl-Accelerator Gate Yellow.mdx')
speff_gateshadow = speffect:new('war3mapImported\\mdl-Accelerator Gate Purple.mdx')
speff_gatephys = speffect:new('war3mapImported\\mdl-Accelerator Gate Red.mdx')
teth_gate_stack = {
[1] = speff_gatearcane,
[2] = speff_gatefrost,
[3] = speff_gatenature,
[4] = speff_gatefire,
[5] = speff_gateshadow,
[6] = speff_gatephys,
}
-- flamestrike:
speff_fspurp = speffect:new('war3mapImported\\mdl-Flamestrike Dark Void I.mdx')
end
function spell:createmarker(x, y, _dur, _radius)
local r = 80.0
if _radius then
r = _radius/r
end
local e = speff_unitcirc:play(x, y, _dur or nil, r)
return e
end
function spell:assigndmgtypes()
-- assign what special effect to run on element damage.
dmg.type[dmg_arcane].effect = speff_arcane
dmg.type[dmg_fire].effect = speff_fire
dmg.type[dmg_frost].effect = speff_frost
dmg.type[dmg_nature].effect = speff_nature
dmg.type[dmg_shadow].effect = speff_shadow
dmg.type[dmg_phys].effect = speff_phys
end
-- @unit = add to this unit.
-- @dmgtypeid = this element eye-candy.
-- @_dur = [optional] make it expire after this many sec.
function spell:addenchant(unit, dmgtypeid, _dur)
-- eye-candy effect for enhancements.
local abil = sp_enchant[dmgtypeid]
UnitAddAbility(unit, abil)
if _dur then
utils.timed(_dur, function()
UnitRemoveAbility(unit, abil)
end)
end
end
sync = {}
function sync:init()
self.trig = CreateTrigger()
self.__index = self
end
function sync:new(str, synctable, index, vartype)
-- vartype should be "string" or "number"
local o = {}
setmetatable(o,self)
o.str = str
o.index = index
o.vartype = "string" or vartype
o:initread(synctable)
return o
end
function sync:send(varinput)
-- register data out-flow (varinput should be a string)
if BlzSendSyncData(self.str, varinput) then
--utils.textall("send data success: "..self.str.." "..varinput)
else
--utils.textall("send data failed: "..self.str.." "..varinput)
end
end
function sync:initread(synctable)
-- register data in-flow
for p,_ in pairs(kobold.playing) do
BlzTriggerRegisterPlayerSyncEvent(self.trig, p, self.str, false)
end
TriggerAddAction(self.trig, function()
if BlzGetTriggerSyncPrefix() == self.str then
--utils.textall("data fetched: "..sync:getdata())
if self.vartype == "string" then
synctable[self.index] = sync:getdata()
--utils.textall("synctable set to: "..synctable[self.index])
elseif self.vartype == "number" then
synctable[self.index] = sync:getdata()
synctable[self.index] = tonumber(synctable[self.index])
--utils.textall("synctable set to: "..synctable[self.index])
if type(synctable[self.index]) == "number" then print("is number true") end
else
print("error: sync event has incorrect 'vartype' - should be 'string' or 'number'")
end
end
end)
end
function sync:getdata()
return BlzGetTriggerSyncData()
end
terrain = {}
-- Ldrt Lordaeron Summer Dirt (cliff)
-- Ldro Lordaeron Summer Rough Dirt
-- Ldrg Lordaeron Summer Grassy Dirt
-- Lrok Lordaeron Summer Rock
-- Lgrs Lordaeron Summer Grass (cliff)
-- Lgrd Lordaeron Summer Dark Grass
-- Fdrt Lordaeron Fall Dirt (cliff)
-- Fdro Lordaeron Fall Rough Dirt
-- Fdrg Lordaeron Fall Grassy Dirt
-- Frok Lordaeron Fall Rock
-- Fgrs Lordaeron Fall Grass (cliff)
-- Fgrd Lordaeron Fall Dark Grass
-- Wdrt Lordaeron Winter Dirt
-- Wdro Lordaeron Winter Rough Dirt
-- Wsng Lordaeron Winter Grassy Snow
-- Wrok Lordaeron Winter Rock
-- Wgrs Lordaeron Winter Grass (cliff)
-- Wsnw Lordaeron Winter Snow (cliff)
-- Bdrt Barrens Dirt
-- Bdrh Barrens Rough Dirt
-- Bdrr Barrens Pebbles
-- Bdrg Barrens Grassy Dirt
-- Bdsr Barrens Desert (cliff)
-- Bdsd Barrens Dark Desert
-- Bflr Barrens Rock
-- Bgrr Barrens Grass (cliff)
-- Adrt Ashenvale Dirt (cliff)
-- Adrd Ashenvale Rough Dirt
-- Agrs Ashenvale Grass (cliff)
-- Arck Ashenvale Rock
-- Agrd Ashenvale Lumpy Grass
-- Avin Ashenvale Vines
-- Adrg Ashenvale Grassy Dirt
-- Alvd Ashenvale Leaves
-- Cdrt Felwood Dirt (cliff)
-- Cdrd Felwood Rough Dirt
-- Cpos Felwood Poison
-- Crck Felwood Rock
-- Cvin Felwood Vines
-- Cgrs Felwood Grass (cliff)
-- Clvg Felwood Leaves
-- Ndrt Northrend Dirt (cliff)
-- Ndrd Northrend Dark Dirt
-- Nrck Northrend Rock
-- Ngrs Northrend Grass
-- Nice Northrend Ice
-- Nsnw Northrend Snow (cliff)
-- Nsnr Northrend Rocky Snow
-- Ydrt Cityscape Dirt (cliff)
-- Ydtr Cityscape Rough Dirt
-- Yblm Cityscape Black Marble
-- Ybtl Cityscape Brick
-- Ysqd Cityscape Square Tiles (cliff)
-- Yrtl Cityscape Round Tiles
-- Ygsb Cityscape Grass
-- Yhdg Cityscape Grass Trim
-- Ywmb Cityscape White Marble
-- Vdrt Village Dirt (cliff)
-- Vdrr Village Rough Dirt
-- Vcrp Village Crops
-- Vcbp Village Cobble Path
-- Vstp Village Stone Path
-- Vgrs Village Short Grass
-- Vrck Village Rocks
-- Vgrt Village Thick Grass (cliff)
-- Qdrt Village Fall Dirt (cliff)
-- Qdrr Village Fall Rough Dirt
-- Qcrp Village Fall Crops
-- Qcbp Village Fall Cobble Path
-- Qstp Village Fall Stone Path
-- Qgrs Village Fall Short Grass
-- Qrck Village Fall Rocks
-- Qgrt Village Fall Thick Grass (cliff)
-- Xdrt Dalaran Dirt (cliff)
-- Xdtr Dalaran Rough Dirt
-- Xblm Dalaran Black Marble
-- Xbtl Dalaran Brick
-- Xsqd Dalaran Square Tiles (cliff)
-- Xrtl Dalaran Round Tiles
-- Xgsb Dalaran Grass
-- Xhdg Dalaran Grass Trim
-- Xwmb Dalaran White Marble
-- Ddrt Dungeon Dirt (cliff)
-- Dbrk Dungeon Brick
-- Drds Dungeon Red Stone
-- Dlvc Dungeon Lava Cracks
-- Dlav Dungeon Lava
-- Ddkr Dungeon Dark Rock
-- Dgrs Dungeon Grey Stones
-- Dsqd Dungeon Square Tiles (cliff)
-- Gdrt Underground Dirt (cliff)
-- Gbrk Underground Brick
-- Grds Underground Red Stone
-- Glvc Underground Lava Cracks
-- Glav Underground Lava
-- Gdkr Underground Dark Rock
-- Ggrs Underground Grey Stones
-- Gsqd Underground Square Tiles (cliff)
-- Zdrt Sunken Ruins Dirt (cliff)
-- Zdtr Sunken Ruins Rough Dirt
-- Zdrg Sunken Ruins Grassy Dirt
-- Zbks Sunken Ruins Small Bricks
-- Zsan Sunken Ruins Sand
-- Zbkl Sunken Ruins Large Bricks (cliff)
-- Ztil Sunken Ruins RoundTiles
-- Zgrs Sunken Ruins Grass
-- Zvin Sunken Ruins Dark Grass
-- Idrt Icecrown Dirt
-- Idtr Icecrown Rough Dirt
-- Idki Icecrown Dark Ice
-- Ibkb Icecrown Black Bricks
-- Irbk Icecrown Runed Bricks (cliff)
-- Itbk Icecrown Tiled Bricks
-- Iice Icecrown Ice
-- Ibsq Icecrown Black Squares
-- Isnw Icecrown Snow (cliff)
-- Odrt Outland Dirt
-- Odtr Outland Light Dirt
-- Osmb Outland Rough Dirt (cliff)
-- Ofst Outland Cracked Dirt
-- Olgb Outland Flat Stones
-- Orok Outland Rock
-- Ofsl Outland Light Flat Stone
-- Oaby Outland Abyss (cliff)
-- Kdrt Black Citadel Dirt (cliff)
-- Kfsl Black Citadel Light Dirt
-- Kdtr Black Citadel Rough Dirt
-- Kfst Black Citadel Flat Stones
-- Ksmb Black Citadel Small Bricks
-- Klgb Black Citadel Large Bricks
-- Ksqt Black Citadel Square Tiles
-- Kdkt Black Citadel Dark Tiles (cliff)
-- Jdrt Dalaran Ruins Dirt (cliff)
-- Jdtr Dalaran Ruins Rough Dirt
-- Jblm Dalaran Ruins Black Marble
-- Jbtl Dalaran Ruins Brick
-- Jsqd Dalaran Ruins Square Tiles (cliff)
-- Jrtl Dalaran Ruins Round Tiles
-- Jgsb Dalaran Ruins Grass
-- Jhdg Dalaran Ruins Grass Trim
-- Jwmb Dalaran Ruins White Marble
-- cAc2 Ashenvale Dirt (non-cliff)
-- cAc1 Ashenvale Grass (non-cliff)
-- cBc2 Barrens Desert (non-cliff)
-- cBc1 Barrens Grass (non-cliff)
-- cKc1 Black Citadel Dirt (non-cliff)
-- cKc2 Black Citadel Dark Tiles (non-cliff)
-- cYc2 Cityscape Dirt (non-cliff)
-- cYc1 Cityscape Square Tiles (non-cliff)
-- cXc2 Dalaran Dirt (non-cliff)
-- cXc1 Dalaran Square Tiles (non-cliff)
-- cJc2 Dalaran Ruins Dirt (non-cliff)
-- cJc1 Dalaran Ruins Square Tiles (non-cliff)
-- cDc2 Dungeon Dirt (non-cliff)
-- cDc1 Dungeon Square Tiles (non-cliff)
-- cCc2 Felwood Dirt (non-cliff)
-- cCc1 Felwood Grass (non-cliff)
-- cIc2 Icecrown Runed Bricks (non-cliff)
-- cIc1 Icecrown Snow (non-cliff)
-- cFc2 Lordaeron Fall Dirt (non-cliff)
-- cFc1 Lordaeron Fall Grass (non-cliff)
-- cLc2 Lordaeron Summer Dirt (non-cliff)
-- cLc1 Lordaeron Summer Grass (non-cliff)
-- cWc2 Lordaeron Winter Grass (non-cliff)
-- cWc1 Lordaeron Winter Snow (non-cliff)
-- cNc2 Northrend Dirt (non-cliff)
-- cNc1 Northrend Snow (non-cliff)
-- cOc1 Outland Abyss (non-cliff)
-- cOc2 Outland Rough Dirt (non-cliff)
-- cZc2 Sunken Ruins Dirt (non-cliff)
-- cZc1 Sunken Ruins Large Bricks (non-cliff)
-- cGc2 Underground Dirt (non-cliff)
-- cGc1 Underground Square Tiles (non-cliff)
-- cVc2 Village Dirt (non-cliff)
-- cVc1 Village Thick Grass (non-cliff)
-- cQc2 Village Fall Dirt (non-cliff)
-- cQc1 Village Fall Thick Grass (non-cliff)
function terrain:init()
self.__index = self
self.biomet = {} -- store biome terrain settings.
t_desert_dirt = FourCC('Bdrh')
t_desert_sand = FourCC('Bdsr')
t_desert_crag = FourCC('Bdsd')
t_desert_rock = FourCC('Bflr')
t_udgrd_dirt = FourCC('Gdrt')
t_udgrd_rock = FourCC('Ggrs')
t_udgrd_tile = FourCC('Gsqd')
t_marsh_dirt = FourCC('Cdrd')
t_marsh_vine = FourCC('Cvin')
t_marsh_pois = FourCC('Cpos')
t_marsh_rock = FourCC('Crck')
t_slag_dirt = FourCC('cDc2')
t_slag_rock = FourCC('Dgrs')
t_slag_lava = FourCC('Dlvc')
t_slag_brck = FourCC('Dbrk')
t_ice_ice = FourCC('Iice')
t_ice_icedark = FourCC('Idki')
-- assign biome tiles:
t_desert_g = {
[1] = t_desert_sand,
[2] = t_desert_crag,
[3] = t_desert_rock,
}
t_marsh_g = {
[1] = t_marsh_rock,
[2] = t_marsh_vine,
[3] = t_marsh_pois,
}
t_slag_g = {
[1] = t_slag_rock,
[2] = t_slag_brck,
[3] = t_slag_lava,
}
t_ice_g = {
[1] = t_udgrd_rock,
[2] = t_ice_ice,
[3] = t_ice_icedark,
}
t_vault_g = {
[1] = t_udgrd_tile,
[2] = t_desert_sand,
[3] = t_desert_sand,
}
end
function terrain:paint(x, y, terrainid, brushsize)
-- 0 = circle, 1 = sqare, brushsize = tile size
for pnum = 1,kk.maxplayers do
if utils.localp() == Player(pnum-1) then
SetTerrainType(x, y, terrainid, -1, brushsize, 0)
end
end
end
tooltip = {}
function tooltip:init()
self.__index = self
tooltip_callout_c = "|cff"..color.tooltip.good
self.temp = nil --
self.setting = {
simpleh = 0.016, -- base height.
simplew = 0.020, -- base width.
simplec = 0.00425, -- char sizing.
flevel = 6, -- frame level (layer).
}
self.string = {
-- generate wrapped text for performance:
-- *note: leave blue as color.tooltip.alert to
-- prevent duplicate |cff concats.
resist = color:wrap(color.tooltip.alert,"Resistances"),
second1 = color:wrap(color.tooltip.alert,"Secondary Utility"),
second2 = color:wrap(color.tooltip.alert,"Secondary Other"),
defensive = color:wrap(color.tooltip.alert,"Defensive"),
offensive = color:wrap(color.tooltip.alert,"Offensive"),
character = color:wrap(color.tooltip.alert,"Character"),
digsite = color:wrap(color.tooltip.alert,"Dig Site"),
utility = color:wrap(color.tooltip.alert,"Utility"),
clstr = color:wrap(color.ui.candlelight, "Candle Light"),
waxstr = color:wrap(color.ui.wax, "Wax"),
}
self.orecolor = {
[1] = color.dmg.arcane,
[2] = color.dmg.frost,
[3] = color.dmg.nature,
[4] = color.dmg.fire,
[5] = color.dmg.shadow,
[6] = color.dmg.physical,
[7] = color.ui.gold,
}
self.orename = {
[1] = color:wrap(self.orecolor[1],"Arcanium"),
[2] = color:wrap(self.orecolor[2],"Polarium"),
[3] = color:wrap(self.orecolor[3],"Fungolium"),
[4] = color:wrap(self.orecolor[4],"Scorium"),
[5] = color:wrap(self.orecolor[5],"Obsidium"),
[6] = color:wrap(self.orecolor[6],"Torporium"),
[7] = color:wrap(self.orecolor[7],"Gold"),
}
self.box = {
["abilitylvl"] = {
simple = true,
tipanchor = fp.t,
attachanchor = fp.b,
txt = "Unlocked at Level ??",
hoverset = function(abilslot) return "Unlocked at "..color:wrap(color.tooltip.alert, "Level "..(abilslot*3 + 1)) end
},
["charpage"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Change statistics page"
},
["tcache"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Recover overflow rewards (requires free bag space)"
},
["gold"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Your current stash of "..color:wrap(color.ui.gold,"Gold")..". It's shiny."
},
["fragments"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Your current stash of "..color:wrap(color.rarity.ancient, "Ancient Fragments").."."
},
["obj"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.tr,
txt = "Complete the "..color:wrap(color.tooltip.alert,"objective").." to finish the dig"
},
["closebtn"] = {
simple = true,
tipanchor = fp.t,
attachanchor = fp.b,
txt = "Close ("..color:wrap(color.tooltip.alert,"Esc").."|r)"
},
["digbtn"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Dig Sites ("..color:wrap(color.tooltip.alert,"Tab").."|r)"
},
["invbtn"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Inventory ("..color:wrap(color.tooltip.alert,"B").."|r)"
},
["equipbtn"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Equipment ("..color:wrap(color.tooltip.alert,"V").."|r)"
},
["charbtn"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Character ("..color:wrap(color.tooltip.alert,"C").."|r)"
},
["masterybtn"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Mastery ("..color:wrap(color.tooltip.alert,"N").."|r)"
},
["abilbtn"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Abilities ("..color:wrap(color.tooltip.alert,"K").."|r)"
},
["questbtn"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Info ("..color:wrap(color.tooltip.alert,"F9").."|r)"
},
["badgebtn"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Badges ("..color:wrap(color.tooltip.alert,"J").."|r)"
},
["logbtn"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Chat Log ("..color:wrap(color.tooltip.alert,"F12").."|r)"
},
["mainbtn"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Main Menu ("..color:wrap(color.tooltip.alert,"F10").."|r)"
},
-- attributes tips:
["level"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.tr,
txt = "Earn experience by completing " .. color:wrap(color.tooltip.alert, "Dig Sites")
},
["attrzero"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.tr,
txt = "Level up to earn attribute points",
hoverset = function() if kobold.player[utils.trigp()].attrpoints > 0 then return "Click to apply an attribute point" else return "Level up to earn attribute points" end end
},
["attrauto"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.tr,
txt = "Automatically apply "..color:wrap(color.tooltip.alert,"all").." attribute points"
},
["strength"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.tr,
txt = "Increases " ..
color:wrap(color.tooltip.alert, "armor") ..
" and " .. color:wrap(color.tooltip.alert, "absorb power")
},
["vitality"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.tr,
txt = "Increases " ..
color:wrap(color.tooltip.alert, "maximum health") ..
" and " .. color:wrap(color.tooltip.alert, "health regeneration")
},
["wisdom"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.tr,
txt = "Increases " ..
color:wrap(color.tooltip.alert, "maximum mana") ..
" and " .. color:wrap(color.tooltip.alert, "mana regeneration")
},
["alacrity"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.tr,
txt = "Increases " ..
color:wrap(color.tooltip.alert, "mining power") ..
" and " .. color:wrap(color.tooltip.alert, "dodge chance")
},
["fear"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.tr,
txt = "Reduces movement speed and cooldown rate as " .. tooltip.string.clstr .. " fades"
},
["cowardice"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.tr,
txt = "Reduces mana efficiency as " .. tooltip.string.clstr .. " fades."
},
["paranoia"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.b,
txt = "Reduces " .. tooltip.string.waxstr .. " capacity and efficiency as".." fades."
},
-- quest pane:
["questmax"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Click to minimize details"
},
["questmin"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Click to view details"
},
["speakto"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Press 'D' to speak to a target character"
},
-- shop:
["shopfrag"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Conjure dig site keys at "..color:wrap(color.tooltip.alert, "The Greywhisker")
},
["shopele"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Imbue new items at "..color:wrap(color.tooltip.alert, "The Elementalist")
},
["shopshiny"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Craft new items at "..color:wrap(color.tooltip.alert, "The Shinykeeper")
},
["craftitem"] = {
simple = true,
tipanchor = fp.t,
attachanchor = fp.b,
txt = "Craft item!"
},
["sellore"] = {
simple = true,
tipanchor = fp.t,
attachanchor = fp.b,
txt = "Sell "..color:wrap(color.tooltip.alert, "1").." ore for "..color:wrap(color.ui.gold, "20").." gold"
},
["buyore"] = {
simple = true,
tipanchor = fp.t,
attachanchor = fp.b,
txt = "Buy "..color:wrap(color.tooltip.alert, "1").." ore for "..color:wrap(color.ui.gold, "40").." gold"
},
["buyfragment"] = {
simple = true,
tipanchor = fp.t,
attachanchor = fp.b,
txt = "Buy "..color:wrap(color.tooltip.alert, "1").." "..color:wrap(color.rarity.ancient, "Ancient Fragment").." for "..color:wrap(color.ui.gold, "300").." gold"
},
-- inventory tips:
["invbtnsell"] = {
simple = true,
tipanchor = fp.tr,
attachanchor = fp.bl,
txt = "Sell this item"
},
["invbtnequip"] = {
simple = true,
tipanchor = fp.tr,
attachanchor = fp.bl,
txt = "Equip this item"
},
["itemtip"] = {
simple = false,
tipanchor = fp.r,
attachanchor = fp.l,
fn = "FrameItemTooltip"
},
-- equipment tips:
["equipempty"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "This slot is empty"
},
["invbtnunequip"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Place this item in your bag"
},
-- dig site map tips:
["digsitecard"] = {
simple = true,
tipanchor = fp.t,
attachanchor = fp.b,
txt = "Select this "..self.string.digsite
},
["digstart"] = {
simple = true,
tipanchor = fp.t,
attachanchor = fp.b,
txt = "Start the dig!"
},
["digkey"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "(Optional) Insert a "..color:wrap(color.tooltip.alert, "Dig Key").." to activate a boss"
},
-- skill bar:
["healthpot"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Restore "..color:wrap(color.tooltip.good, "30%%").." of your "
..color:wrap(color.tooltip.bad, "Health.").." 15 sec cooldown."
},
["manapot"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Restore "..color:wrap(color.tooltip.good, "30%%").." of your "
..color:wrap(color.tooltip.alert, "Mana.").." 15 sec cooldown."
},
["health"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Your "..color:wrap(color.tooltip.bad, "Health")..". You are "..color:wrap(color.tooltip.bad, "downed").." if it reaches 0."
},
["mana"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Your "..color:wrap(color.tooltip.alert, "Mana")..". Used to fuel your "..color:wrap(color.tooltip.alert, "abilities.")
},
["experience"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Experience: 0/100",
hoverset = function() return "Experience: "..math.floor(kobold.player[utils.trigp()].experience)
.."/"..math.floor(kobold.player[utils.trigp()].nextlvlxp) end,
},
-- advanced tooltip:
["advtip"] = {
simple = false,
tipanchor = fp.r,
attachanchor = fp.l,
fn = "FrameLongformTooltip"
},
-- mastery tooltips:
["masterycenter"] = {
simple = true,
tipanchor = fp.t,
attachanchor = fp.b,
txt = "Unlock your "..color:wrap(color.tooltip.alert, "Mastery Tree").." (1 Point) "..color:wrap(color.tooltip.good,"+5%% Maximum Health")
},
-- ability tooltips:
["learnabil"] = {
simple = true,
tipanchor = fp.t,
attachanchor = fp.b,
txt = "Learn this "..color:wrap(color.tooltip.alert, "Ability")
},
["cooldown"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = color:wrap(color.tooltip.alert, "Cooldown").." (Seconds)"
},
["manacost"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = color:wrap(color.tooltip.alert, "Mana").." Cost"
},
-- ore name callout:
["orearcane"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Arcanium"
},
["orefrost"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Polarium"
},
["orenature"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Fungolium"
},
["orefire"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Scorium"
},
["oreshadow"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Obsidium"
},
["orephysical"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Torporium"
},
["bosschest"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = color:wrap(color.tooltip.alert, "Spoils of the deep").." (Click to claim)"
},
["unlearnmastery"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Clear all learned mastery abilities"
},
["respecmastery"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Reset all spent mastery points for "..color:wrap(color.ui.gold, "5,000 gold")
},
["invsortall"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Sort all items to the top by equipment type"
},
["invsellall"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Double click quickly to sell all "..color:wrap(color.rarity.ancient, "Non-Ancient").." items "..color:wrap(color.tooltip.bad, "(Cannot be undone)")..""
},
["savechar"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = color:wrap(color.tooltip.good, "Save").." your character"
},
["unequipall"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Double click quickly to remove all equipment"
},
-- main menu:
["freeplaymode"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.t,
txt = "Accelerated leveling, no story, and "..color:wrap(color.tooltip.bad,"no saving").." (start at "..color:wrap(color.tooltip.good, "level 15")..")"
},
["storymode"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.t,
txt = "Help build back the kingdom through a short story (start at "..color:wrap(color.tooltip.good, "level 1")..")"
},
["loadchar"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.t,
txt = color:wrap(color.tooltip.good, "Load").." an existing save file"
},
["deletesave"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = color:wrap(color.tooltip.alert, "Double click").." quickly to delete this file "..color:wrap(color.tooltip.bad, "(Warning: cannot be undone)")
},
["cancel"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.tl,
txt = "Cancel"
},
["buff"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "<empty>",
hoverset = function(buffid)
if buffy.fr.stack[buffid].data and buffy.fr.stack[buffid].data.name and buffy.fr.stack[buffid].data.descript then
return color:wrap(color.tooltip.alert, buffy.fr.stack[buffid].data.name)..": "..buffy.fr.stack[buffid].data.descript
end
end
},
-- greywhisker vendor:
["greywhiskerfrag"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Your current stash of "..color:wrap(color.rarity.ancient, "Ancient Fragments")
},
["greywhiskercraft"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Spend "..color:wrap(color.rarity.ancient, "Ancient Fragments").." to craft the selected "..color:wrap(color.tooltip.alert, "Dig Site Key")
},
["relic1"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Summons a powerful foe in "..color:wrap(color.dmg.physical,"Fossil Gorge")
},
["relic2"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Summons a powerful foe in "..color:wrap(color.dmg.fire,"The Slag Pit")
},
["relic3"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Summons a powerful foe in "..color:wrap(color.dmg.nature,"The Mire")
},
["relic4"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Summons a powerful foe in "..color:wrap(color.dmg.frost,"Glacier Cavern")
},
["relic5"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "Summons a powerful foe in "..color:wrap(color.ui.gold,"The Vault")
},
["badgetip1"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Greenwhisker").." - Complete your first dig site."
},
["badgetip2"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Checked Up").." - Fill every item slot with a piece of equipment."
},
["badgetip3"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Hammer Time").." - Unlock the Shinykeeper vendor."
},
["badgetip4"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Magic Rocks").." - Unlock the Elementalist vendor."
},
["badgetip5"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Rat Wisdom").." - Unlock the Greywhisker vendor."
},
["badgetip6"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Raw Power").." - Equip an Ancient-quality item."
},
["badgetip7"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Relic of Fire").." - Defeat the Slag King."
},
["badgetip8"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Relic of Nature").." - Defeat the Marsh Mutant."
},
["badgetip9"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Relic of Earth").." - Defeat Megachomp."
},
["badgetip10"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Relic of Ice").." - Defeat the Thawed Experiment."
},
["badgetip11"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Gold Thunder").." - Defeat the Amalgam of Greed."
},
["badgetip12"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Kobold Kingdom").." - Complete the main storyline."
},
["badgetip13"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Burn, Baby, Burn").." - Defeat the Slag King on |cffed6d00Tyrannical|r difficulty at level 60."
},
["badgetip14"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Marsh Madness").." - Defeat the Marsh Mutant on |cffed6d00Tyrannical|r difficulty at level 60."
},
["badgetip15"] = {
simple = true,
tipanchor = fp.bl,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Dino-Sore").." - Defeat Megachomp on |cffed6d00Tyrannical|r difficulty at level 60."
},
["badgetip16"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Ice Loot").." - Defeat the Thawed Experiment on |cffed6d00Tyrannical|r difficulty at level 60."
},
["badgetip17"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Pocket Change").." - Defeat the Amalgam of Greed on |cffed6d00Tyrannical|r difficulty at level 60."
},
["badgetip18"] = {
simple = true,
tipanchor = fp.br,
attachanchor = fp.t,
txt = color:wrap(color.ui.gold,"Rajah Rat").." - Defeat all bosses on |cffed6d00Tyrannical|r difficulty at level 60."
},
["badgetipclass1"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "As Tunneler"
},
["badgetipclass2"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "As Geomancer"
},
["badgetipclass3"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "As Rascal"
},
["badgetipclass4"] = {
simple = true,
tipanchor = fp.b,
attachanchor = fp.t,
txt = "As Wickfighter"
},
}
end
function tooltip:getorename(oreid)
return self.orename[oreid]
end
-- @tipstr = tooltip name.
function tooltip:get(tipstr)
return self.box[tipstr]
end
function tooltip:bake()
-- build tooltips on map load to control tip features (we then attach the singleton frames to the tooltip table in .box).
-- the assign tooltip functions control showing/hiding/updating these singleton frames.
self.simpletipfr = self:basictemplate()
self.simpletipfr.alpha = 240
self.simpletipfr:setsize(w, self.setting.simpleh)
self.simpletipfr:addtext("tip", "", "all" )
self.simpletipfr:hide()
self.simpletipfr:setlvl(self.setting.flevel)
self.simpletipfr:resetalpha()
--
self.advtipfr = kui.frame:newbyname("FrameLongformTooltip", nil)
self.advtipfr:setlvl(self.setting.flevel)
self.advtipfr:hide()
--
self.itemtipfr = kui.frame:newbyname("FrameItemTooltip", nil)
self.itemtipfr:setlvl(self.setting.flevel)
self.itemtipfr:hide()
--
self.itemcompfr = kui.frame:newbyname("FrameItemCompTooltip", nil)
self.itemcompfr:setlvl(self.setting.flevel)
self.itemcompfr:hide()
-- assign the new frame to the tooltip table:
for i,tip in pairs(self.box) do
if type(tip) == "table" then
if tip.simple then -- simple frame, 1-line.
tip.fr = self.simpletipfr
elseif tip.fn then -- .fdf frame, custom defined.
if tip.fn == "FrameItemTooltip" then
tip.fr = self.itemtipfr
elseif tip.fn == "FrameLongformTooltip" then
tip.fr = self.advtipfr
end
end
end
end
self.enhtextleft =
self.string.offensive -- header
.."|n"
.."Arcane Damage:|n"
.."Frost Damage:|n"
.."Nature Damage:|n"
.."Fire Damage:|n"
.."Shadow Damage:|n"
.."Physical Damage:|n"
.."|n"
..self.string.character -- header
.."|n"
.."Bonus Health:|n"
.."Bonus Mana:|n"
.."Movespeed:|n"
.."Wax Efficiency:|n"
self.enhtextright =
self.string.resist -- header
.."|n"
.."Arcane Resist:|n"
.."Frost Resist:|n"
.."Nature Resist:|n"
.."Fire Resist:|n"
.."Shadow Resist:|n"
.."Physical Resist:|n"
.."|n"
..self.string.utility -- header
.."|n"
.."Healing Power:|n"
.."Absorb Power:|n"
.."Proliferation:|n"
.."Mining Power:|n"
self.enhtextleft2 =
self.string.second1 -- header
.."|n"
.."Treasure Find:|n"
.."Dig Site XP:|n"
.."Item Sell Value:|n"
.."Healing Potion:|n"
.."Mana Potion:|n"
.."Minion Damage:|n"
.."|n"
..self.string.defensive -- header
.."|n"
.."Damage Reduction:|n"
.."Dodge Rating:|n"
.."Armor Rating:|n"
.."Absorb on Spell:|n"
self.enhtextright2 =
self.string.second2 -- header
.."|n"
.."Missile Range:|n"
.."Ability Radius:|n"
.."Casting Speed:|n"
.."Ele. Lifesteal:|n"
.."Phys. Lifesteal:|n"
.."Thorns:|n"
.."Add Arcane Dmg.:|n"
.."Add Frost Dmg.:|n"
.."Add Nature Dmg.:|n"
.."Add Fire Dmg.:|n"
.."Add Shadow Dmg.:|n"
.."Add Phys. Dmg.:|n"
self.itemtipfh = {
[1] = BlzGetFrameByName("FrameItemTooltipBackdrop",0),
[2] = BlzGetFrameByName("FrameItemTooltipItemIcon",0),
[3] = BlzGetFrameByName("FrameItemTooltipItemRarityIcon",0),
[4] = BlzGetFrameByName("FrameItemTooltipName",0),
[5] = BlzGetFrameByName("FrameItemTooltipDescription",0),
[6] = BlzGetFrameByName("FrameItemTooltipLevel",0),
[7] = BlzGetFrameByName("FrameItemTooltipGoldValue",0),
[8] = BlzGetFrameByName("FrameItemTooltipGoldIcon",0),
}
self.itemcompfh = {
[1] = BlzGetFrameByName("FrameItemCompTooltipBackdrop",0),
[2] = BlzGetFrameByName("FrameItemCompTooltipItemIcon",0),
[3] = BlzGetFrameByName("FrameItemCompTooltipItemRarityIcon",0),
[4] = BlzGetFrameByName("FrameItemCompTooltipName",0),
[5] = BlzGetFrameByName("FrameItemCompTooltipDescription",0),
[6] = BlzGetFrameByName("FrameItemCompTooltipLevel",0),
[7] = BlzGetFrameByName("FrameItemCompTooltipGoldValue",0),
[8] = BlzGetFrameByName("FrameItemCompTooltipGoldIcon",0),
[9] = BlzGetFrameByName("FrameItemCompTooltipTitleBackdrop",0),
}
self.advtipfh = {
[1] = BlzGetFrameByName("FrameLongformTooltipItemIcon",0),
[2] = BlzGetFrameByName("FrameLongformTooltipName",0),
[3] = BlzGetFrameByName("FrameLongformTooltipDescription",0),
[4] = BlzGetFrameByName("FrameLongformTooltipBackdrop",0),
}
end
function tooltip:basictemplate()
-- single line tip that auto-expands horizontally.
-- we handle the width calculation based on char count.
return kui.frame:newbyname("FrameBasicTooltip", kui.gameui)
end
-- @pobj = the player object to read data from.
function tooltip:charcleanse(pobj)
-- TODO: the performance way to do this would be to
-- have a frame for each piece of stat text.
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- left block 1
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
local lval =
"|n"..tooltip:getstat(pobj[p_stat_arcane]).."|r|n"
..tooltip:getstat(pobj[p_stat_frost]).."|r|n"
..tooltip:getstat(pobj[p_stat_nature]).."|r|n"
..tooltip:getstat(pobj[p_stat_fire]).."|r|n"
..tooltip:getstat(pobj[p_stat_shadow]).."|r|n"
..tooltip:getstat(pobj[p_stat_phys]).."|r|n"
.."|n|n"
..tooltip:getstat(pobj[p_stat_bhp]).."|r|n"
..tooltip:getstat(pobj[p_stat_bmana]).."|r|n"
..tooltip:getstat(pobj[p_stat_bms]).."|r|n"
..tooltip:getstat(pobj[p_stat_wax]).."|r"
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- right block 1
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
local rval =
"|n"..tooltip:getcappedval(pobj[p_stat_arcane_res], 75).."|r|n"
..tooltip:getcappedval(pobj[p_stat_frost_res], 75).."|r|n"
..tooltip:getcappedval(pobj[p_stat_nature_res], 75).."|r|n"
..tooltip:getcappedval(pobj[p_stat_fire_res], 75).."|r|n"
..tooltip:getcappedval(pobj[p_stat_shadow_res], 75).."|r|n"
..tooltip:getcappedval(pobj[p_stat_phys_res], 75).."|r|n"
.."|n|n"
..tooltip:getstat(pobj[p_stat_healing]).."|r|n"
..tooltip:getstat(pobj[p_stat_absorb]).."|r|n"
..tooltip:getcappedval(pobj[p_stat_eleproc], 75).."|r|n"
..tooltip:getstat(pobj[p_stat_minepwr]).."|r|n"
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- left block 2
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
local lval2 =
"|n"..tooltip:getstat(pobj[p_stat_treasure]).."|r|n"
..tooltip:getstat(pobj[p_stat_digxp]).."|r|n"
..tooltip:getstat(pobj[p_stat_vendor]).."|r|n"
..tooltip:getstat(pobj[p_stat_potionpwr]).."|r|n"
..tooltip:getstat(pobj[p_stat_artifactpwr]).."|r|n"
..tooltip:getstat(pobj[p_stat_miniondmg]).."|r|n"
.."|n|n"
..tooltip:getcappedval(pobj[p_stat_dmg_reduct], 90).."|r|n"
..tooltip:getstat(pobj[p_stat_dodge], true).."|r|n"
..tooltip:getstat(pobj[p_stat_armor], true).."|r|n"
..tooltip:getstat(pobj[p_stat_shielding], true).."|r|n"
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- right block 2
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
local rval2 =
"|n"..tooltip:getstat(pobj[p_stat_mislrange]).."|r|n"
..tooltip:getstat(pobj[p_stat_abilarea]).."|r|n"
..tooltip:getstat(pobj[p_stat_castspeed]).."|r|n"
..tooltip:getstat(pobj[p_stat_elels]).."|r|n"
..tooltip:getstat(pobj[p_stat_physls]).."|r|n"
..tooltip:getstat(pobj[p_stat_thorns], true).."|r|n"
..tooltip:getstat(pobj[p_stat_dmg_arcane]).."|r|n"
..tooltip:getstat(pobj[p_stat_dmg_frost]).."|r|n"
..tooltip:getstat(pobj[p_stat_dmg_nature]).."|r|n"
..tooltip:getstat(pobj[p_stat_dmg_fire]).."|r|n"
..tooltip:getstat(pobj[p_stat_dmg_shadow]).."|r|n"
..tooltip:getstat(pobj[p_stat_dmg_phys]).."|r|n"
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
return self.enhtextleft, self.enhtextleft2, self.enhtextright, self.enhtextright2, lval, lval2, rval, rval2
end
function tooltip:getstat(val, _isint)
local append
if _isint then append = "" else append = "%%" end
self.temp = math.floor(val)
if self.temp > 0 then
self.temp = color:wrap(color.tooltip.good, self.temp..append)
else
self.temp = color:wrap(color.txt.txtdisable, self.temp..append)
end
return self.temp
end
function tooltip:convertval(val, pointval, _shouldfloor)
-- if each point should equal a different display value, convert it (i.e. 1 pt = 0.25%)
if _shouldfloor then
return math.floor(val*pointval)
else
return val*pointval
end
end
function tooltip:getcappedval(val, cap)
self.temp = val
if self.temp > cap then
self.temp = color:wrap(color.tooltip.alert, cap.."%%")
else
self.temp = tooltip:getstat(val)
end
return self.temp
end
function tooltip:updatecharpage(p)
if p == utils.localp() then
local _,_,_,_,newlval1,newlval2,newrval1,newrval2 = tooltip:charcleanse(kobold.player[p])
kui.canvas.game.char.page[1].rval:settext(newrval1)
kui.canvas.game.char.page[2].rval:settext(newrval2)
kui.canvas.game.char.page[1].lval:settext(newlval1)
kui.canvas.game.char.page[2].lval:settext(newlval2)
kui.canvas.game.char.attr[1]:settext(math.floor(kobold.player[p][p_stat_strength] + kobold.player[p][p_stat_strength_b]))
kui.canvas.game.char.attr[2]:settext(math.floor(kobold.player[p][p_stat_wisdom] + kobold.player[p][p_stat_wisdom_b]))
kui.canvas.game.char.attr[3]:settext(math.floor(kobold.player[p][p_stat_alacrity] + kobold.player[p][p_stat_alacrity_b]))
kui.canvas.game.char.attr[4]:settext(math.floor(kobold.player[p][p_stat_vitality] + kobold.player[p][p_stat_vitality_b]))
end
end
trg = {}
function trg:init()
self.__index = self
self.debug = false
self.total = 0
self.dis = false
self.trig = nil -- attached game trigger.
self.allp = false -- triggered for any unit?
self.burner = false -- when true, trig is destroyed on first event.
self.trgt = {} -- object storage.
self.trgpmap =
{
attacked = EVENT_PLAYER_UNIT_ATTACKED,
order = EVENT_PLAYER_UNIT_ISSUED_UNIT_ORDER,
death = EVENT_PLAYER_UNIT_DEATH,
spell = EVENT_PLAYER_UNIT_SPELL_EFFECT,
startcast = EVENT_PLAYER_UNIT_SPELL_CAST,
}
self.trgmap =
{
death = EVENT_UNIT_DEATH,
order = EVENT_PLAYER_UNIT_ISSUED_UNIT_ORDER,
startcast = EVENT_PLAYER_UNIT_SPELL_CAST,
attacked = EVENT_PLAYER_UNIT_ATTACKED,
}
end
function trg:new(eventstr, _powner)
local o = {}
setmetatable(o, self)
if _powner then o.powner = _powner else o.allp = true end
o.trig = CreateTrigger()
o.actiont = {} -- stored actions
o.condt = {} -- stored conditions
o.unitt = {} -- stored trigger units (for referencing and loops).
o.pass = true -- flag for running actions.
o:regevent(eventstr)
TriggerAddAction(o.trig, function()
utils.debugfunc(function()
o.pass = true
if o.condt and #o.condt > 0 then
for _,cond in ipairs(o.condt) do
if not cond() then
o.pass = false
break
end
end
end
if o.pass and o.actiont then
for _,action in ipairs(o.actiont) do
action()
end
o:wrapup()
end
end, "trigaction")
end)
self.total = self.total + 1
self.trgt[self] = self
return o
end
function trg:regevent(eventstr)
local found = false
if self.allp then
for str,event in pairs(self.trgmap) do
if eventstr == str then
if self.debug then print("found eventstr "..str.." with event id "..GetHandleId(event)) end
TriggerRegisterAnyUnitEventBJ(self.trig, event)
found = true
break
end
end
else
for str,event in pairs(self.trgpmap) do
if eventstr == str then
if self.debug then print("found eventstr "..str.." with event id "..GetHandleId(event)) end
TriggerRegisterPlayerUnitEventSimple(self.trig, self.powner, event)
found = true
break
end
end
end
if found then
self.event = eventstr
if self.debug then print("register trigger event success") end
else
assert(false, "error: attempted to register an event that is not yet mapped: '"..eventstr.."'")
end
end
function trg:regaction(func)
self.actiont[#self.actiont+1] = func
return #self.actiont -- id.
end
function trg:regcond(func)
self.condt[#self.condt+1] = func
return #self.condt -- id.
end
function trg:removeaction(id)
self.condt[id] = nil
self:sortcollapse()
end
function trg:removecond(id)
self.actiont[id] = nil
self:sortcollapse()
end
function trg:sortcollapse()
-- collapse tables after removing function.
utils.tablecollapse(self.condt)
utils.tablecollapse(self.actiont)
end
function trg:attachtrigu(unit)
-- attach a condition for a triggering unit.
self:regcond(function() return unit == utils.trigu() end)
self.unitt[#self.unitt+1] = unit
end
function trg:attachspellid(id)
-- attach a condition for a matching spell id (FourCC rawcode)
self:regcond(function() return id == GetSpellAbilityId() end)
end
function trg:attachuniteffect(unit, effect)
-- if a trig unit is present and in the unit table, run an effect on them.
local unit = unit
self.effect = effect
self:regaction(function()
if self.unitt then
for _,unit in ipairs(self.unitt) do
if self.trigu == unit then self.effect:playu(unit) end
end
end
end)
end
function trg:disable()
self.dis = true
DisableTrigger(self.trig)
end
function trg:enable()
self.dis = false
EnableTrigger(self.trig)
end
function trg:wrapup()
if self.burner then self:destroy() end
end
function trg:destroy()
TriggerClearActions(self.trig)
TriggerClearConditions(self.trig)
for i,v in pairs(self.actiont) do v = nil i = nil end
for i,v in pairs(self.condt) do v = nil i = nil end
for i,v in pairs(self.unitt) do v = nil i = nil end
self.actiont = nil
self.condt = nil
self.unitt = nil
self.trgt[self] = nil
self.total = self.total - 1
end
function trg:newdeathtrig(unit, func, _effect)
-- one-time burner Trigger for a single unit (destroyed on death).
local unit, func = unit, func
local trig = trg:new("death", utils.powner(unit))
trig:attachtrigu(unit)
trig:regaction(func)
if _effect then trig:attachuniteffect(unit, _effect) end
return trig
end
function trg:newspelltrig(unit, func)
-- trigger listening for ability casts.
local trig = trg:new("spell", utils.powner(unit))
trig:regaction(func)
return trig
end