· 9 years ago · Oct 01, 2016, 08:10 PM
1-------------------------------------------------------------------------------------------------------------------
2-- General utility functions that can be used by any job files.
3-- Outside the scope of what the main include file deals with.
4-------------------------------------------------------------------------------------------------------------------
5
6-------------------------------------------------------------------------------------------------------------------
7-- Buff utility functions.
8-------------------------------------------------------------------------------------------------------------------
9
10local cancel_spells_to_check = S{'Sneak', 'Stoneskin', 'Spectral Jig', 'Trance', 'Monomi: Ichi', 'Utsusemi: Ichi'}
11local cancel_types_to_check = S{'Waltz', 'Samba'}
12
13-- Function to cancel buffs if they'd conflict with using the spell you're attempting.
14-- Requirement: Must have Cancel addon installed and loaded for this to work.
15function cancel_conflicting_buffs(spell, action, spellMap, eventArgs)
16 if cancel_spells_to_check:contains(spell.english) or cancel_types_to_check:contains(spell.type) then
17 if spell.action_type == 'Ability' then
18 local abil_recasts = windower.ffxi.get_ability_recasts()
19 if abil_recasts[spell.recast_id] > 0 then
20 add_to_chat(123,'Abort: Ability waiting on recast.')
21 eventArgs.cancel = true
22 return
23 end
24 elseif spell.action_type == 'Magic' then
25 local spell_recasts = windower.ffxi.get_spell_recasts()
26 if spell_recasts[spell.recast_id] > 0 then
27 add_to_chat(123,'Abort: Spell waiting on recast.')
28 eventArgs.cancel = true
29 return
30 end
31 end
32
33 if spell.english == 'Spectral Jig' and buffactive.sneak then
34 cast_delay(0.2)
35 send_command('cancel sneak')
36 elseif spell.english == 'Sneak' and spell.target.type == 'SELF' and buffactive.sneak then
37 send_command('cancel sneak')
38 elseif spell.english == ('Stoneskin') then
39 send_command('@wait 1.0;cancel stoneskin')
40 elseif spell.english:startswith('Monomi') then
41 send_command('@wait 1.7;cancel sneak')
42 elseif spell.english == 'Utsusemi: Ichi' then
43 send_command('@wait 1.7;cancel copy image,copy image (2)')
44 elseif (spell.english == 'Trance' or spell.type=='Waltz') and buffactive['saber dance'] then
45 cast_delay(0.2)
46 send_command('cancel saber dance')
47 elseif spell.type=='Samba' and buffactive['fan dance'] then
48 cast_delay(0.2)
49 send_command('cancel fan dance')
50 end
51 end
52end
53
54
55-- Some mythics have special durations for level 1 and 2 aftermaths
56local special_aftermath_mythics = S{'Tizona', 'Kenkonken', 'Murgleis', 'Yagrush', 'Carnwenhan', 'Nirvana', 'Tupsimati', 'Idris'}
57
58-- Call from job_precast() to setup aftermath information for custom timers.
59function custom_aftermath_timers_precast(spell)
60 if spell.type == 'WeaponSkill' then
61 info.aftermath = {}
62
63 local relic_ws = data.weaponskills.relic[player.equipment.main] or data.weaponskills.relic[player.equipment.range]
64 local mythic_ws = data.weaponskills.mythic[player.equipment.main] or data.weaponskills.mythic[player.equipment.range]
65 local empy_ws = data.weaponskills.empyrean[player.equipment.main] or data.weaponskills.empyrean[player.equipment.range]
66
67 if not relic_ws and not mythic_ws and not empy_ws then
68 return
69 end
70
71 info.aftermath.weaponskill = spell.english
72 info.aftermath.duration = 0
73
74 info.aftermath.level = math.floor(player.tp / 1000)
75 if info.aftermath.level == 0 then
76 info.aftermath.level = 1
77 end
78
79 if spell.english == relic_ws then
80 info.aftermath.duration = math.floor(0.2 * player.tp)
81 if info.aftermath.duration < 20 then
82 info.aftermath.duration = 20
83 end
84 elseif spell.english == empy_ws then
85 -- nothing can overwrite lvl 3
86 if buffactive['Aftermath: Lv.3'] then
87 return
88 end
89 -- only lvl 3 can overwrite lvl 2
90 if info.aftermath.level ~= 3 and buffactive['Aftermath: Lv.2'] then
91 return
92 end
93
94 -- duration is based on aftermath level
95 info.aftermath.duration = 30 * info.aftermath.level
96 elseif spell.english == mythic_ws then
97 -- nothing can overwrite lvl 3
98 if buffactive['Aftermath: Lv.3'] then
99 return
100 end
101 -- only lvl 3 can overwrite lvl 2
102 if info.aftermath.level ~= 3 and buffactive['Aftermath: Lv.2'] then
103 return
104 end
105
106 -- Assume mythic is lvl 80 or higher, for duration
107
108 if info.aftermath.level == 1 then
109 info.aftermath.duration = (special_aftermath_mythics:contains(player.equipment.main) and 270) or 90
110 elseif info.aftermath.level == 2 then
111 info.aftermath.duration = (special_aftermath_mythics:contains(player.equipment.main) and 270) or 120
112 else
113 info.aftermath.duration = 180
114 end
115 end
116 end
117end
118
119
120-- Call from job_aftercast() to create the custom aftermath timer.
121function custom_aftermath_timers_aftercast(spell)
122 if not spell.interrupted and spell.type == 'WeaponSkill' and
123 info.aftermath and info.aftermath.weaponskill == spell.english and info.aftermath.duration > 0 then
124
125 local aftermath_name = 'Aftermath: Lv.'..tostring(info.aftermath.level)
126 send_command('timers d "Aftermath: Lv.1"')
127 send_command('timers d "Aftermath: Lv.2"')
128 send_command('timers d "Aftermath: Lv.3"')
129 send_command('timers c "'..aftermath_name..'" '..tostring(info.aftermath.duration)..' down abilities/00027.png')
130
131 info.aftermath = {}
132 end
133end
134
135
136-------------------------------------------------------------------------------------------------------------------
137-- Utility functions for changing spells and target types in an automatic manner.
138-------------------------------------------------------------------------------------------------------------------
139
140local waltz_tp_cost = {['Curing Waltz'] = 200, ['Curing Waltz II'] = 350, ['Curing Waltz III'] = 500, ['Curing Waltz IV'] = 650, ['Curing Waltz V'] = 800}
141
142-- Utility function for automatically adjusting the waltz spell being used to match HP needs and TP limits.
143-- Handle spell changes before attempting any precast stuff.
144function refine_waltz(spell, action, spellMap, eventArgs)
145 if spell.type ~= 'Waltz' then
146 return
147 end
148
149 -- Don't modify anything for Healing Waltz or Divine Waltzes
150 if spell.english == "Healing Waltz" or spell.english == "Divine Waltz" or spell.english == "Divine Waltz II" then
151 return
152 end
153
154 local newWaltz = spell.english
155 local waltzID
156
157 local missingHP
158
159 -- If curing ourself, get our exact missing HP
160 if spell.target.type == "SELF" then
161 missingHP = player.max_hp - player.hp
162 -- If curing someone in our alliance, we can estimate their missing HP
163 elseif spell.target.isallymember then
164 local target = find_player_in_alliance(spell.target.name)
165 local est_max_hp = target.hp / (target.hpp/100)
166 missingHP = math.floor(est_max_hp - target.hp)
167 end
168
169 -- If we have an estimated missing HP value, we can adjust the preferred tier used.
170
171
172 local tpCost = waltz_tp_cost[newWaltz]
173
174 local downgrade
175
176 -- Downgrade the spell to what we can afford
177 if player.tp < tpCost and not buffactive.trance then
178 --[[ Costs:
179 Curing Waltz: 200 TP
180 Curing Waltz II: 350 TP
181 Curing Waltz III: 500 TP
182 Curing Waltz IV: 650 TP
183 Curing Waltz V: 800 TP
184 Divine Waltz: 400 TP
185 Divine Waltz II: 800 TP
186 --]]
187
188 if player.tp < 200 then
189 add_to_chat(122, 'Insufficient TP ['..tostring(player.tp)..']. Cancelling.')
190 eventArgs.cancel = true
191 return
192 elseif player.tp < 350 then
193 newWaltz = 'Curing Waltz'
194 elseif player.tp < 500 then
195 newWaltz = 'Curing Waltz II'
196 elseif player.tp < 650 then
197 newWaltz = 'Curing Waltz III'
198 elseif player.tp < 800 then
199 newWaltz = 'Curing Waltz IV'
200 end
201
202 downgrade = 'Insufficient TP ['..tostring(player.tp)..']. Downgrading to '..newWaltz..'.'
203 end
204
205
206 if newWaltz ~= spell.english then
207 send_command('@input /ja "'..newWaltz..'" '..tostring(spell.target.raw))
208 if downgrade then
209 add_to_chat(122, downgrade)
210 end
211 eventArgs.cancel = true
212 return
213 end
214
215 if missingHP and missingHP > 0 then
216 add_to_chat(122,'Trying to cure '..tostring(missingHP)..' HP using '..newWaltz..'.')
217 end
218end
219
220
221-- Function to allow for automatic adjustment of the spell target type based on preferences.
222function auto_change_target(spell, spellMap)
223 -- Don't adjust targetting for explicitly named targets
224 if not spell.target.raw:startswith('<') then
225 return
226 end
227
228 -- Do not modify target for spells where we get <lastst> or <me>.
229 if spell.target.raw == ('<lastst>') or spell.target.raw == ('<me>') then
230 return
231 end
232
233 -- init a new eventArgs with current values
234 local eventArgs = {handled = false, PCTargetMode = state.PCTargetMode.value, SelectNPCTargets = state.SelectNPCTargets.value}
235
236 -- Allow the job to do custom handling, or override the default values.
237 -- They can completely handle it, or set one of the secondary eventArgs vars to selectively
238 -- override the default state vars.
239 if job_auto_change_target then
240 job_auto_change_target(spell, action, spellMap, eventArgs)
241 end
242
243 -- If the job handled it, we're done.
244 if eventArgs.handled then
245 return
246 end
247
248 local pcTargetMode = eventArgs.PCTargetMode
249 local selectNPCTargets = eventArgs.SelectNPCTargets
250
251
252 local validPlayers = S{'Self', 'Player', 'Party', 'Ally', 'NPC'}
253
254 local intersection = spell.targets * validPlayers
255 local canUseOnPlayer = not intersection:empty()
256
257 local newTarget
258
259 -- For spells that we can cast on players:
260 if canUseOnPlayer and pcTargetMode ~= 'default' then
261 -- Do not adjust targetting for player-targettable spells where the target was <t>
262 if spell.target.raw ~= ('<t>') then
263 if pcTargetMode == 'stal' then
264 -- Use <stal> if possible, otherwise fall back to <stpt>.
265 if spell.targets.Ally then
266 newTarget = '<stal>'
267 elseif spell.targets.Party then
268 newTarget = '<stpt>'
269 end
270 elseif pcTargetMode == 'stpt' then
271 -- Even ally-possible spells are limited to the current party.
272 if spell.targets.Ally or spell.targets.Party then
273 newTarget = '<stpt>'
274 end
275 elseif pcTargetMode == 'stpc' then
276 -- If it's anything other than a self-only spell, can change to <stpc>.
277 if spell.targets.Player or spell.targets.Party or spell.targets.Ally or spell.targets.NPC then
278 newTarget = '<stpc>'
279 end
280 end
281 end
282 -- For spells that can be used on enemies:
283 elseif spell.targets and spell.targets.Enemy and selectNPCTargets then
284 -- Note: this means macros should be written for <t>, and it will change to <stnpc>
285 -- if the flag is set. It won't change <stnpc> back to <t>.
286 newTarget = '<stnpc>'
287 end
288
289 -- If a new target was selected and is different from the original, call the change function.
290 if newTarget and newTarget ~= spell.target.raw then
291 change_target(newTarget)
292 end
293end
294
295
296-------------------------------------------------------------------------------------------------------------------
297-- Environment utility functions.
298-------------------------------------------------------------------------------------------------------------------
299
300-- Function to get the current weather intensity: 0 for none, 1 for single weather, 2 for double weather.
301function get_weather_intensity()
302 return gearswap.res.weather[world.weather_id].intensity
303end
304
305
306-- Returns true if you're in a party solely comprised of Trust NPCs.
307-- TODO: Do we need a check to see if we're in a party partly comprised of Trust NPCs?
308function is_trust_party()
309 -- Check if we're solo
310 if party.count == 1 then
311 return false
312 end
313
314 -- Can call a max of 3 Trust NPCs, so parties larger than that are out.
315 if party.count > 4 then
316 return false
317 end
318
319 -- If we're in an alliance, can't be a Trust party.
320 if alliance[2].count > 0 or alliance[3].count > 0 then
321 return false
322 end
323
324 -- Check that, for each party position aside from our own, the party
325 -- member has one of the Trust NPC names, and that those party members
326 -- are flagged is_npc.
327 for i = 2,4 do
328 if party[i] then
329 if not npcs.Trust:contains(party[i].name) then
330 return false
331 end
332 if party[i].mob and party[i].mob.is_npc == false then
333 return false
334 end
335 end
336 end
337
338 -- If it didn't fail any of the above checks, return true.
339 return true
340end
341
342
343-- Call these function with a list of equipment slots to check ('head', 'neck', 'body', etc)
344-- Returns true if any of the specified slots are currently encumbered.
345-- Returns false if all specified slots are unencumbered.
346function is_encumbered(...)
347 local check_list = {...}
348 -- Compensate for people passing a table instead of a series of strings.
349 if type(check_list[1]) == 'table' then
350 check_list = check_list[1]
351 end
352 local check_set = S(check_list)
353
354 for slot_id,slot_name in pairs(gearswap.default_slot_map) do
355 if check_set:contains(slot_name) then
356 if gearswap.encumbrance_table[slot_id] then
357 return true
358 end
359 end
360 end
361
362 return false
363end
364
365-------------------------------------------------------------------------------------------------------------------
366-- Elemental gear utility functions.
367-------------------------------------------------------------------------------------------------------------------
368
369-- General handler function to set all the elemental gear for an action.
370function set_elemental_gear(spell)
371 set_elemental_gorget_belt(spell)
372 set_elemental_obi_cape_ring(spell)
373 set_elemental_staff(spell)
374end
375
376
377-- Set the name field of the predefined gear vars for gorgets and belts, for the specified weaponskill.
378function set_elemental_gorget_belt(spell)
379 if spell.type ~= 'WeaponSkill' then
380 return
381 end
382
383 -- Get the union of all the skillchain elements for the weaponskill
384 local weaponskill_elements = S{}:
385 union(skillchain_elements[spell.skillchain_a]):
386 union(skillchain_elements[spell.skillchain_b]):
387 union(skillchain_elements[spell.skillchain_c])
388
389 gear.ElementalGorget.name = get_elemental_item_name("gorget", weaponskill_elements) or gear.default.weaponskill_neck or ""
390 gear.ElementalBelt.name = get_elemental_item_name("belt", weaponskill_elements) or gear.default.weaponskill_waist or ""
391end
392
393
394-- Function to get an appropriate obi/cape/ring for the current action.
395function set_elemental_obi_cape_ring(spell)
396 if spell.element == 'None' then
397 return
398 end
399
400 local world_elements = S{world.day_element}
401 if world.weather_element ~= 'None' then
402 world_elements:add(world.weather_element)
403 end
404
405 local obi_name = get_elemental_item_name("obi", S{spell.element}, world_elements)
406 gear.ElementalObi.name = obi_name or gear.default.obi_waist or ""
407
408 if obi_name then
409 if player.inventory['Twilight Cape'] or player.wardrobe['Twilight Cape'] or player.wardrobe2['Twilight Cape'] then
410 gear.ElementalCape.name = "Twilight Cape"
411 end
412 if (player.inventory['Zodiac Ring'] or player.wardrobe['Zodiac Ring'] or player.wardrobe2['Zodiac Ring']) and spell.english ~= 'Impact' and
413 not S{'Divine Magic','Dark Magic','Healing Magic'}:contains(spell.skill) then
414 gear.ElementalRing.name = "Zodiac Ring"
415 end
416 else
417 gear.ElementalCape.name = gear.default.obi_back
418 gear.ElementalRing.name = gear.default.obi_ring
419 end
420end
421
422
423-- Function to get the appropriate fast cast and/or recast staves for the current spell.
424function set_elemental_staff(spell)
425 if spell.action_type ~= 'Magic' then
426 return
427 end
428
429 gear.FastcastStaff.name = get_elemental_item_name("fastcast_staff", S{spell.element}) or gear.default.fastcast_staff or ""
430 gear.RecastStaff.name = get_elemental_item_name("recast_staff", S{spell.element}) or gear.default.recast_staff or ""
431end
432
433
434-- Gets the name of an elementally-aligned piece of gear within the player's
435-- inventory that matches the conditions set in the parameters.
436--
437-- item_type: Type of item as specified in the elemental_map mappings.
438-- EG: gorget, belt, obi, fastcast_staff, recast_staff
439--
440-- valid_elements: Elements that are valid for the action being taken.
441-- IE: Weaponskill skillchain properties, or spell element.
442--
443-- restricted_to_elements: Secondary elemental restriction that limits
444-- whether the item check can be considered valid.
445-- EG: Day or weather elements that have to match the spell element being queried.
446--
447-- Returns: Nil if no match was found (either due to elemental restrictions,
448-- or the gear isn't in the player inventory), or the name of the piece of
449-- gear that matches the query.
450function get_elemental_item_name(item_type, valid_elements, restricted_to_elements)
451 local potential_elements = restricted_to_elements or elements.list
452 local item_map = elements[item_type:lower()..'_of']
453
454 for element in (potential_elements.it or it)(potential_elements) do
455 if valid_elements:contains(element) and (player.inventory[item_map[element]] or player.wardrobe[item_map[element]] or player.wardrobe2[item_map[element]]) then
456 return item_map[element]
457 end
458 end
459end
460
461
462-------------------------------------------------------------------------------------------------------------------
463-- Function to easily change to a given macro set or book. Book value is optional.
464-------------------------------------------------------------------------------------------------------------------
465
466function set_macro_page(set,book)
467 if not tonumber(set) then
468 add_to_chat(123,'Error setting macro page: Set is not a valid number ('..tostring(set)..').')
469 return
470 end
471 if set < 1 or set > 10 then
472 add_to_chat(123,'Error setting macro page: Macro set ('..tostring(set)..') must be between 1 and 10.')
473 return
474 end
475
476 if book then
477 if not tonumber(book) then
478 add_to_chat(123,'Error setting macro page: book is not a valid number ('..tostring(book)..').')
479 return
480 end
481 if book < 1 or book > 20 then
482 add_to_chat(123,'Error setting macro page: Macro book ('..tostring(book)..') must be between 1 and 20.')
483 return
484 end
485 send_command('@input /macro book '..tostring(book)..';wait .1;input /macro set '..tostring(set))
486 else
487 send_command('@input /macro set '..tostring(set))
488 end
489end
490
491
492-------------------------------------------------------------------------------------------------------------------
493-- Utility functions for including local user files.
494-------------------------------------------------------------------------------------------------------------------
495
496-- Attempt to load user gear files in place of default gear sets.
497-- Return true if one exists and was loaded.
498function load_sidecar(job)
499 if not job then return false end
500
501 -- filename format example for user-local files: whm_gear.lua, or playername_whm_gear.lua
502 local filenames = {player.name..'_'..job..'_gear.lua', job..'_gear.lua',
503 'gear/'..player.name..'_'..job..'_gear.lua', 'gear/'..job..'_gear.lua',
504 'gear/'..player.name..'_'..job..'.lua', 'gear/'..job..'.lua'}
505 return optional_include(filenames)
506end
507
508-- Attempt to include user-globals. Return true if it exists and was loaded.
509function load_user_globals()
510 local filenames = {player.name..'-globals.lua', 'user-globals.lua'}
511 return optional_include(filenames)
512end
513
514-- Optional version of include(). If file does not exist, does not
515-- attempt to load, and does not throw an error.
516-- filenames takes an array of possible file names to include and checks
517-- each one.
518function optional_include(filenames)
519 for _,v in pairs(filenames) do
520 local path = gearswap.pathsearch({v})
521 if path then
522 include(v)
523 return true
524 end
525 end
526end
527
528-------------------------------------------------------------------------------------------------------------------
529-- Utility functions for vars or other data manipulation.
530-------------------------------------------------------------------------------------------------------------------
531
532-- Attempt to locate a specified name within the current alliance.
533function find_player_in_alliance(name)
534 for party_index,ally_party in ipairs(alliance) do
535 for player_index,_player in ipairs(ally_party) do
536 if _player.name == name then
537 return _player
538 end
539 end
540 end
541end
542
543
544-- buff_set is a set of buffs in a library table (any of S{}, T{} or L{}).
545-- This function checks if any of those buffs are present on the player.
546function has_any_buff_of(buff_set)
547 return buff_set:any(
548 -- Returns true if any buff from buff set that is sent to this function returns true:
549 function (b) return buffactive[b] end
550 )
551end
552
553
554-- Invert a table such that the keys are values and the values are keys.
555-- Use this to look up the index value of a given entry.
556function invert_table(t)
557 if t == nil then error('Attempting to invert table, received nil.', 2) end
558
559 local i={}
560 for k,v in pairs(t) do
561 i[v] = k
562 end
563 return i
564end
565
566
567-- Gets sub-tables based on baseSet from the string str that may be in dot form
568-- (eg: baseSet=sets, str='precast.FC', this returns the table sets.precast.FC).
569function get_expanded_set(baseSet, str)
570 local cur = baseSet
571 for i in str:gmatch("[^.]+") do
572 if cur then
573 cur = cur[i]
574 end
575 end
576
577 return cur
578end
579
580
581-------------------------------------------------------------------------------------------------------------------
582-- Utility functions data and event tracking.
583-------------------------------------------------------------------------------------------------------------------
584
585-- This is a function that can be attached to a registered event for 'time change'.
586-- It will send a call to the update() function if the time period changes.
587-- It will also call job_time_change when any of the specific time class values have changed.
588-- To activate this in your job lua, add this line to your user_setup function:
589-- windower.register_event('time change', time_change)
590--
591-- Variables it sets: classes.Daytime, and classes.DuskToDawn. They are set to true
592-- if their respective descriptors are true, or false otherwise.
593function time_change(new_time, old_time)
594 local was_daytime = classes.Daytime
595 local was_dusktime = classes.DuskToDawn
596
597 if new_time >= 6*60 and new_time < 18*60 then
598 classes.Daytime = true
599 else
600 classes.Daytime = false
601 end
602
603 if new_time >= 17*60 or new_time < 7*60 then
604 classes.DuskToDawn = true
605 else
606 classes.DuskToDawn = false
607 end
608
609 if was_daytime ~= classes.Daytime or was_dusktime ~= classes.DuskToDawn then
610 if job_time_change then
611 job_time_change(new_time, old_time)
612 end
613
614 handle_update({'auto'})
615 end
616end