-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgambit.lua
More file actions
776 lines (684 loc) · 26.3 KB
/
Copy pathgambit.lua
File metadata and controls
776 lines (684 loc) · 26.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
_addon.name = 'gambit'
_addon.author = 'Geno'
_addon.version = '0.1.0'
_addon.commands = {'gb', 'gambit'}
require('luau')
packets = require('packets')
res = require('resources')
local gd = require('gambits/gambit_defines')
require('queues')
require('sets')
require('lists')
textboxes = require('texts')
Defaults = {}
Defaults.display = {}
Defaults.display.text = {}
Defaults.display.pos = {}
Defaults.display.pos.x = 17
Defaults.display.pos.y = windower.get_windower_settings().y_res-250
Defaults.display.text.font = 'Courier New'
Defaults.display.text.red = 80
Defaults.display.text.green = 180
Defaults.display.text.blue = 200
Defaults.display.text.alpha = 255
Defaults.display.text.size = 12
Defaults.visible = true
Defaults.display.visible = true
settings = Defaults
action_box = textboxes.new('${current_string}',Defaults.display,settings)
action_box.current_string = ''
action_box:show()
local loaded_gambits = {}
local var_cache = {}
--cool down between inputting an action
local gcd_delay = 3.1
--delay to wait before a character is considered no longer moving
--this is important for casting right after moving
local is_moving_delay = 0.3
local player_casting = false
local paused = true
local last_tick = os.clock()
local gcd_timer = 0
local first_loop = true
local chat_q = {}
local commands_q = Q{}
local player = {}
local logged_in = false
local g_bundle = {}
g_bundle.conditions = {}
player.ja_recasts = {}
player.ma_recasts = {}
player.instance = {}
player.buffs = {}
player.spells = {}
player.self = {}
player.is_moving = false
player.last_move_time = 0
player.party_buffs = {}
Queue = {}
function Queue.new ()
return {first = 0, last = -1}
end
function Queue.push_to_top (list, value)
local first = list.first - 1
list.first = first
list[first] = value
end
function Queue.push (list, value)
local last = list.last + 1
list.last = last
list[last] = value
end
function Queue.pop(list)
local first = list.first
if first > list.last then return nil end
local value = list[first]
list[first] = nil -- to allow garbage collection
list.first = first + 1
return value
end
-- Save copied tables in `copies`, indexed by original table.
function deepcopy(orig, copies)
copies = copies or {}
local orig_type = type(orig)
local copy
if orig_type == 'table' then
if copies[orig] then
copy = copies[orig]
else
copy = {}
copies[orig] = copy
for orig_key, orig_value in next, orig, nil do
copy[deepcopy(orig_key, copies)] = deepcopy(orig_value, copies)
end
setmetatable(copy, deepcopy(getmetatable(orig), copies))
end
else -- number, string, boolean, etc
copy = orig
end
return copy
end
--Taken from Gearswap, credit to Byrthnoth
-----------------------------------------------------------------------------------
--Name: assemble_action_packet(target_id,target_index,category,spell_id)
--Desc: Puts together an "action" packet (0x1A)
--Args:
---- target_id - The target's ID
---- target_index - The target's index
---- category - The action's category. (3 = MA, 7 = WS, 9 = JA, 16 = RA, 25 = MS)
---- spell_ID - The current spell's ID
-----------------------------------------------------------------------------------
--Returns:
---- string - An action packet. First four bytes are dummy bytes.
-----------------------------------------------------------------------------------
function assemble_action_packet(target_id,target_index,category,spell_id,arrow_offset)
local outstr = string.char(0x1A,0x08,0,0)
outstr = outstr..string.char( (target_id%256), math.floor(target_id/256)%256, math.floor( (target_id/65536)%256) , math.floor( (target_id/16777216)%256) )
outstr = outstr..string.char( (target_index%256), math.floor(target_index/256)%256)
outstr = outstr..string.char( (category%256), math.floor(category/256)%256)
if category == 16 then
spell_id = 0
end
outstr = outstr..string.char( (spell_id%256), math.floor(spell_id/256)%256)..string.char(0,0) .. 'fff':pack(0,0,0)
return outstr
end
local use_command = (function(command, target)
if command == "--bundle" then
return (
function(player, params)
windower.console.write('DOING input //'..params.bundle.spell.name..' '..params.bundle.spell.target)
local unify_prefix = {['/ma'] = '/ma', ['/magic']='/ma',['/jobability'] = '/ja',['/ja']='/ja',['/item']='/item',['/song']='/ma',
['/so']='/ma',['/ninjutsu']='/ma',['/weaponskill']='/ws',['/ws']='/ws',['/ra']='/ra',['/rangedattack']='/ra',['/nin']='/ma',
['/throw']='/ra',['/range']='/ra',['/shoot']='/ra',['/monsterskill']='/ms',['/ms']='/ms',['/pet']='/ja',['Monster']='Monster',['/bstpet']='/ja'}
if params.bundle.spell.target_by_id then
local outgoing_action_category_table = {['/ma']=3,['/ws']=7,['/ja']=9,['/ra']=16,['/ms']=25}
if params.bundle.spell.target_id and params.bundle.spell.target_index and params.bundle.spell.prefix and params.bundle.spell.id then
local action_packet = assemble_action_packet(params.bundle.spell.target_id, params.bundle.spell.target_index, outgoing_action_category_table[unify_prefix[params.bundle.spell.prefix]], params.bundle.spell.id, nil)
--windower.packets.inject_outgoing(0x1A, action_packet)
windower.send_command('input '..tostring(unify_prefix[params.bundle.spell.prefix]).." "..tostring(params.bundle.spell.name).." "..tostring(params.bundle.spell.target_id))
if unify_prefix[params.bundle.spell.prefix] == '/ja' then
return 1.2
end
--windower.send_command('input /assist <me>')
end
else
windower.send_command('input //'..params.bundle.spell.name..' '..params.bundle.spell.target)
if unify_prefix[params.bundle.spell.prefix] == '/ja' then
return 1.2
end
end
end)
elseif target == nil then
return (function() windower.send_command('input //'..command) return 1.2 end)
elseif target == "me" or target == "self" then
return (function() windower.send_command('input //'..command..' <me>') return 1.2 end)
elseif target == "t" or target == "target" then
return (function() windower.send_command('input //'..command..' <t>') return 1.2 end)
elseif target ~= nil then
return (function() windower.send_command('input //'..command..' '..target) return 1.2 end)
end
end)
local set_global_condition = (function(variable_name, value)
return (function(player, params)
local v = value
local vn = variable_name
if v == "--bundle" then
v = params.bundle.g_cond_value
end
if vn == "--bundle" then
vn = params.bundle.g_cond_key
end
if v == nil or vn == nil then
windower.add_to_chat(128, "Global Variables cannot be nil Key=Value: "..tostring(vn).."="..tostring(v))
return 0.1
end
windower.add_to_chat(128, "Setting "..tostring(vn).."="..tostring(v))
g_bundle.conditions[vn] = v
return 0.1
end)
end)
local get_global_condition = (function(variable_name)
return (function()
return g_bundle.conditions[variable_name]
end)
end)
local queue_use_commands = (function(...)
local commands = L{...}
return (function(player, params)
windower.add_to_chat(128, "Queued "..tostring(commands:length()).." commands.")
local count = 1
local cmd_len = commands:length()
while count <= cmd_len do
local packed = {}
packed.command = commands[count]
packed.params = params
commands_q:push(packed)
count = count + 1
end
return 0
end)
end)
--taken from Byrth, GearSwap
function refresh_buff_active(bufflist)
buffarr = {}
for i,v in pairs(bufflist) do
if res.buffs[v] then -- For some reason we always have buff 255 active, which doesn't have an entry.
local buff = res.buffs[v]['en']:lower()
if buffarr[buff] then
buffarr[buff] = buffarr[buff] +1
else
buffarr[buff] = 1
end
if buffarr[v] then
buffarr[v] = buffarr[v] +1
else
buffarr[v] = 1
end
end
end
return buffarr
end
--This is taken from gearswap and modified for Gambit
-----------------------------------------------------------------------------------
--Name: pathsearch()
--Args:
---- files_list - table of strings of the file name to search.
-----------------------------------------------------------------------------------
--Returns:
---- path of a valid file, if it exists. False if it doesn't.
-----------------------------------------------------------------------------------
function pathsearch(files_list)
-- base directory search order:
-- windower
-- sub directory search order:
-- stacks
local gearswap_data = windower.addon_path .. 'stacks/'
local gearswap_appdata = (os.getenv('APPDATA') or '') .. '/Windower/Gambit/stacks/'
local search_path = {
[1] = gearswap_data,
[2] = gearswap_appdata,
[3] = gearswap_data .. player.self.name .. '/',
[4] = gearswap_appdata .. player.self.name .. '/',
[5] = windower.addon_path
}
local user_path
local normal_path
for _,basepath in ipairs(search_path) do
if windower.dir_exists(basepath) then
for i,v in ipairs(files_list) do
if v ~= '' then
if include_user_path then
user_path = basepath .. include_user_path .. '/' .. v
end
normal_path = basepath .. v
if user_path and windower.file_exists(user_path) then
return user_path,basepath,v
elseif normal_path and windower.file_exists(normal_path) then
return normal_path,basepath,v
end
end
end
end
end
return false
end
function include_user(str, load_include_in_this_table)
if not (type(str) == 'string') then
error('\nGambit: include() was passed an invalid value ('..tostring(str)..'). (must be a string)', 2)
end
str = str:lower()
if type(package.loaded[str]) == 'table' then
return package.loaded[str]
elseif T{'pack'}:contains(str) then
return
end
if str:sub(-4)~='.lua' then str = str..'.lua' end
local path, loaded_values = pathsearch({str})
if not path then
error('\nGambit: Cannot find the include file ('..tostring(str)..').', 2)
end
local f, err = loadfile(path)
if err ~= nil or f == nil then
windower.add_to_chat(158, 'Include Error: '..tostring(e))
else
--print('Gambit: Included '..tostring(f)..' file.')
end
if f and not err then
if load_include_in_this_table and type(load_include_in_this_table) == 'table' then
setmetatable(load_include_in_this_table, {__index=user_env._G})
setfenv(f, load_include_in_this_table)
pcall(f, load_include_in_this_table)
return load_include_in_this_table
else
setfenv(f,file_env)
return f()
end
else
error('\nGambit: Error loading file ('..tostring(str)..'): '..err, 2)
end
end
function load_stacks()
loaded_gambits = {}
local job_id = windower.ffxi.get_player().main_job_id
local path,base_dir,filename
if not path then
local long_job = res.jobs[job_id].english
local short_job = res.jobs[job_id].english_short
local tab = {player.self.name..'_'..short_job..'.lua',player.self.name..'-'..short_job..'.lua',
player.self.name..'_'..long_job..'.lua',player.self.name..'-'..long_job..'.lua',
player.self.name..'.lua',short_job..'.lua',long_job..'.lua','default.lua'}
path,base_dir,filename = pathsearch(tab)
end
local loaded_file, e = loadfile(path)
if e ~= nil or loaded_file == nil then
windower.add_to_chat(158, 'Error: '..tostring(e))
else
print('Gambit: Loaded '..filename..' file.')
end
file_env = {
include = include_user,
require = require,
ipairs = ipairs,
pairs = pairs,
next = next,
string = string,
table = table,
tostring=tostring,
windower = windower,
use_command = use_command,
set_global_condition = set_global_condition,
get_global_condition = get_global_condition,
queue_use_commands = queue_use_commands,
get_party_member_key_by_name = get_party_member_key_by_name,
distance = distance,
range_to_distance = range_to_distance,
g_bundle = g_bundle,
Q = Q,
S = S,
L = L,
textboxes = textboxes,
math = math,
deepcopy = deepcopy,
os = os,
res = res,
var_cache = var_cache,
type=type,
rawget=rawget,
}
setfenv(loaded_file, file_env)
local status, plugin = pcall(loaded_file)
if status ~= nil and not status then
windower.add_to_chat(158, 'Failed to load file '..plugin)
end
registered_gambits = file_env.registered_gambits
for i, gbt in ipairs(registered_gambits) do
local gam = gbt
gam.initalize_gambit()
table.insert(loaded_gambits, gam)
end
end
windower.register_event('login', function()
player.self = windower.ffxi.get_mob_by_target("me")
load_stacks()
logged_in = true
end)
windower.register_event('load', function()
player.self = windower.ffxi.get_mob_by_target("me")
load_stacks()
logged_in = true
end)
windower.register_event('logout', function()
logged_in = false
first_loop = true
end)
windower.register_event('unload', function()
logged_in = false
first_loop = true
end)
windower.register_event('addon command', function(command, ...)
command = command and command:lower() or nil
args = T{...}
table.insert(args, 1, command)
local next = next
if next(args) ~= nil then
if args[1] == 'stop' then
paused = true
end
if args[1] == 'start' then
paused = false
end
--gb txt Genoxd set trash jas
if args[1] == 'txt' then
table.remove(args, 1)
local speaker = table.remove(args, 1)
local text = table.concat(args, ' ')
local message = '('..speaker:gsub("^%l", string.upper)..') '..text
handleChatString(message)
return
end
end
end)
function pre_load()
chat_q = Queue.new()
commands_q:pop()
player.spells = windower.ffxi.get_spells()
player.self = windower.ffxi.get_mob_by_target("me")
player.last_move_time = 0
end
windower.register_event('prerender', function()
if paused then return end
if not logged_in then return end
if(first_loop) then
first_loop = false
pre_load()
end
local current_time = os.clock()
if(current_time - last_tick > 10) then
--wonder if this is worth it, could make them reload instead
player.spells = windower.ffxi.get_spells()
end
if player_casting and (current_time > gcd_timer) then
player_casting = false
end
if (current_time - last_tick) > 0.1 and (current_time > gcd_timer) and not player_casting then
last_tick = os.clock()
player.instance = windower.ffxi.get_player()
player.ja_recasts = windower.ffxi.get_ability_recasts()
player.ma_recasts = windower.ffxi.get_spell_recasts()
player.buffs = refresh_buff_active(player.instance['buffs'])
player.info = windower.ffxi.get_info()
player.party = windower.ffxi.get_party()
local oldSelf = player.self
player.self = windower.ffxi.get_mob_by_target("me")
if player.self == nil then return end
if player.self ~= nil and player.self.pet_index ~= nil then
player.pet = windower.ffxi.get_mob_by_index(player.self.pet_index)
else
player.pet = nil
end
if oldSelf == nil then
return ---------------------------------------------------- ADDED THIS
end
local posChange = (oldSelf.x ~= player.self.x or oldSelf.z ~= player.self.z)
if posChange then
player.last_move_time = last_tick
player.is_moving = true
else
local dt_last_move = last_tick - player.last_move_time
player.is_moving = dt_last_move < is_moving_delay
end
local dequeued_command = commands_q:pop()
if dequeued_command ~= nil and dequeued_command.command ~= nil then
local delay = dequeued_command.command(player, dequeued_command.params)
if delay ~=nil then
gcd_timer = current_time + delay
else
gcd_timer = current_time + gcd_delay
end
return
end
-- run timed gambits unconditionally
for i, gamb in ipairs(loaded_gambits) do
if(gamb.trigger_type == gd.trigger_types.timed) then
if gamb.should_proc() then
gamb.proc()
end
end
end
-- process chat queue message-by-message, checking all trigger gambits per message
local tmp_chat_q = deepcopy(chat_q)
local msgsChecked = 0
repeat
local msg = Queue.pop(tmp_chat_q)
msgsChecked = msgsChecked + 1
for i, gamb in ipairs(loaded_gambits) do
if gamb.trigger_type == gd.trigger_types.trigger then
local params = {}
params.g_bundle = g_bundle
params.chatStr = msg
params.bundle = {}
local pr, pm = gamb.should_proc(player, params)
params = pm
if pr then
if gamb.after_proc then
local para = gamb.after_proc(player, params)
g_bundle = para.g_bundle
end
if gamb.proc(player, params) then
gcd_timer = current_time + gcd_delay
local first_cmd = commands_q:pop()
if first_cmd ~= nil and first_cmd.command ~= nil then
local delay = first_cmd.command(player, first_cmd.params)
if delay ~= nil then
gcd_timer = current_time + delay
end
end
-- pop only this message and the unmatched ones before it
for _ = 1, msgsChecked do
Queue.pop(chat_q)
end
return
end
end
end
end
-- no gambit matched this message; it will be popped in cleanup below
until(msg == nil or msgsChecked >= 10)
-- discard messages that were checked but matched nothing
for _ = 1, msgsChecked do
Queue.pop(chat_q)
end
end
end)
function handleChatString(message)
Queue.push(chat_q, message)
--processing for just gd.trigger_types.chat
for i, gamb in ipairs(loaded_gambits) do
if(gamb.trigger_type == gd.trigger_types.chat) then
if(gamb.should_proc(player, message)) then
gamb.proc(player, message)
end
end
end
end
windower.register_event('incoming text', function(original,modified,original_mode,modified_mode, blocked)
if paused then return end
if not logged_in then return end
--handles party chat
if type(original) == 'string' and modified_mode == 5 then
handleChatString(original)
end
end)
windower.register_event('chat message', function(message,sender,mode,gm)
--handles tells
-- Broken atm because of null termination in LuaCore not working properly
-- Use handleChatPacket for now
--if mode == 3 or mode == 4 then
--if type(message) == 'string' then
--handleChatString('('..sender..') '..message)
--end
--end
end)
windower.register_event('outgoing chunk', function(id, data, modified, injected, blocked)
local packet = packets.parse('outgoing', data)
if (id == 0x01A) then
--spell cast
if packet.Category == 3 then
gcd_timer = os.clock() + gcd_delay
else
--job ability
gcd_timer = os.clock() + 1.1
end
end
end)
function handleChatPackat(data)
if data == nil then return end
local chat_message = packets.parse('incoming', data)
if chat_message and chat_message.Message then
if chat_message.Mode == 3 or chat_message.Mode == 4 then
handleChatString('('..tostring(chat_message['Sender Name'])..') '..chat_message.Message)
end
end
end
windower.register_event('incoming chunk', function(id, data)
if paused then return end
if not logged_in then return end
if id == 0x017 then
handleChatPackat(data)
return
end
if id == 0x076 then
handleBuffChangePacket(data)
return
end
if id == 0x028 then
local action_message = packets.parse('incoming', data)
if(action_message["Actor"] == player.self.id) then
if action_message["Category"] == 4 then
player_casting = false
gcd_timer = os.clock() + gcd_delay
elseif action_message["Category"] == 8 then
gcd_timer = os.clock() + gcd_delay
player_casting = true
if action_message["Target 1 Action 1 Message"] == 0 then
player_casting = false
gcd_timer = os.clock() + gcd_delay
end
end
end
end
end)
windower.register_event('zone change', function(new_id, old_id)
paused = true
chat_q = Queue.new()
end)
-----------------------------------------------------------------------------------
--Name: convert_buff_list(bufflist)
--Args:
---- bufflist (table): List of buffs from windower.ffxi.get_player()['buffs']
-----------------------------------------------------------------------------------
--Returns:
---- buffarr (table)
---- buffarr is indexed by the string buff name and has a value equal to the number
---- of that string present in the buff array. So two marches would give
---- buffarr.march==2.
-----------------------------------------------------------------------------------
function convert_buff_list(bufflist)
local buffarr = {}
for i,id in pairs(bufflist) do
if res.buffs[id] then -- For some reason we always have buff 255 active, which doesn't have an entry.
local buff = res.buffs[id]['en']:lower()
if buffarr[buff] then
buffarr[buff] = buffarr[buff] +1
else
buffarr[buff] = 1
end
if buffarr[id] then
buffarr[id] = buffarr[id] +1
else
buffarr[id] = 1
end
end
end
return buffarr
end
function handleBuffChangePacket(data)
local partybuffs = {}
for i = 0,4 do
if data:unpack('I',i*48+5) == 0 then
break
else
local index = data:unpack('H',i*48+5+4)
partybuffs[index] = {
id = data:unpack('I',i*48+5+0),
index = data:unpack('H',i*48+5+4),
buffs = {}
}
for n=1,32 do
partybuffs[index].buffs[n] = data:byte(i*48+5+16+n-1) + 256*( math.floor( data:byte(i*48+5+8+ math.floor((n-1)/4)) / 4^((n-1)%4) )%4)
end
-- index of the player can use with windower.ffxi.get_mob_by_index(index)
player.party_buffs[index] = convert_buff_list(partybuffs[index].buffs)
end
end
end
function get_party_member_key_by_name(name)
local party = windower.ffxi.get_party()
for k, v in pairs(party) do
if type(v) == 'table' then
if v.name == name then
return k
end
end
end
return nil
end
function distance(x1, y1, x2, y2)
return math.abs(math.sqrt((x1 - x2)*(x1 -x2) + (y1 - y2)*(y1 - y2)))
end
-- 2: 1.70 0.58824 3.4 1.07273
-- 3?: 1.490909 0.67073 4.47273 1.28727
-- 4: 1.44 0.69444 5.76 1.12889
-- 5?: 1.377778 0.72581 6.88889 0.91111
-- 6?: 1.30 0.76923 7.8 0.6
-- 7: 1.20 0.83333 8.4 2.0
-- 8: 1.30 0.76923 10.4 2.0
-- 9: 1.377778 0.72581 12.4 2.1
-- 10: 1.45 0.69444 14.5 1.9
-- 11: 1.490909 0.67073 16.4 4.0
-- 12: 1.70 0.58824 20.4 -
local ability_range_map = {}
ability_range_map[2] = 3.4
ability_range_map[3] = 4.4
ability_range_map[4] = 5.7
ability_range_map[5] = 6.8
ability_range_map[6] = 7.8
ability_range_map[7] = 8.4
ability_range_map[8] = 10.4
ability_range_map[9] = 12.4
ability_range_map[10] = 14.5
ability_range_map[11] = 16.4
ability_range_map[12] = 20.4
function range_to_distance(range)
return ability_range_map[range]
end