forked from JohnnyCheese/TTS_X-Wing2.0
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGlobal.-1.ttslua
More file actions
7344 lines (6623 loc) · 301 KB
/
Global.-1.ttslua
File metadata and controls
7344 lines (6623 loc) · 301 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
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- ~~~~~~
-- Script by dzikakulka
-- Issues, history at: http://github.com/tjakubo2/TTS_xwing
--
-- Based on a work of: Flolania, Hera Vertigo
-- ~~~~~~
-- ~~~~~~
-- Code contributions
-- - Characted width data: Indimeco
-- - http://github.com/Indimeco/Tabletop-Simulator-Misc
-- ~~~~~~
-- Should the code execute print functions or skip them?
-- This should be set to false on every release
print_debug = false
TTS_print = print
function print(...)
if print_debug == true then
TTS_print(table.unpack({...}))
end
end
-- Vector manipulation
#include TTS_lib/Vector/Vector
-- Standard libraries extentions
#include TTS_lib/Util/Util
-- Standard event handling
#include TTS_lib/EventSub/EventSub
-- Object type abstraction
#include TTS_lib/ObjType/ObjType
-- Save/load management
#include TTS_lib/SaveManager/SaveManager
-- Component sizes, unit conversion methods
#include TTS_xwing/src/Dimensions
-- AI behaviour sets
#include TTS_xwing/src/BehaviourDB
-- Dice control and dice_statistics
#include TTS_xwing/src/DiceControl
-- Ship verification
#include TTS_xwing/src/ShipVerification
-- Modules API, must be loaded last
#include TTS_xwing/src/API
--------
-- MISC FUNCTIONS
-- Dumbest TTS issue ever workaround
function TTS_Serialize(pos)
return {pos[1], pos[2], pos[3]}
end
ObjType.AddType('ship', function(obj)
return ((obj.tag == 'Figurine') and (obj.getVar('__XW_Ship') == true))
end)
ObjType.AddType('token', function(obj)
return (obj.tag == 'Chip' or obj.tag == 'Coin' or (obj.getVar('__XW_Token') and obj.getVar('__XW_TokenIdle')))
end)
ObjType.AddType('dial', function(obj)
return (obj.tag == 'Card' and XW_cmd.CheckCommand(obj.getDescription()) == 'move')
end)
-- Dud function for info buttons and not yet written sections where Lua complains about no code
function dummy() return end
-- END MISC FUNCTIONS
--------
--------
-- COMMAND HANDLING MODULE
-- Sanitizes input (more like ignores anything not explicitly allowed)
-- Allows other modules to add available commands and passes their execution where they belong
XW_cmd = {}
XW_cmd.commandLUT = {}
-- Table of valid commands: their patterns and general types
XW_cmd.ValidCommands = {}
-- Add given regen expression as a valid command for processing
XW_cmd.AddCommand = function(cmdRegex, type)
-- When adding available commands, assert beggining and end of string automatically
if cmdRegex:sub(1,1) ~= '^' then cmdRegex = '^' .. cmdRegex end
if cmdRegex:sub(-1,-1) ~= '$' then cmdRegex = cmdRegex .. '$' end
table.insert(XW_cmd.ValidCommands, {string.lower(cmdRegex), type})
end
-- Check if command is registered as valid
-- If it is return its type identifier, if not return nil
XW_cmd.CheckCommand = function(cmd)
-- Trim whitespaces
cmd = string.lower(cmd:match( "^%s*(.-)%s*$" ))
local type = nil
if XW_cmd.commandLUT[cmd] then
return XW_cmd.commandLUT[cmd]
end
-- Resolve command type
for k,pat in pairs(XW_cmd.ValidCommands) do
if cmd:match(pat[1]) ~= nil then
type = pat[2]
break
end
end
XW_cmd.commandLUT[cmd] = type
return type
end
-- (special function)
-- Purge all save data (everything that goes to onSave)
XW_cmd.AddCommand('purgeSave', 'special')
XW_cmd.PurgeSave = function()
MoveModule.moveHistory = {}
end
-- (special function)
-- Print ship hitory
XW_cmd.AddCommand('hist', 'special')
XW_cmd.ShowHist = function(ship)
MoveModule.PrintHistory(ship)
end
-- (special function)
-- Check for typical issues with a ship
XW_cmd.AddCommand('diag', 'special')
XW_cmd.Diagnose = function(ship)
-- Check and unlock XW_cmd lock if it's on
local issueFound = false
if ObjType.IsOfType(ship, 'ship') ~= true then return end
if XW_cmd.isReady(ship) ~= true then
XW_cmd.SetReady(ship)
printToAll(ship.getName() .. '\'s deadlock resolved', {0.1, 0.1, 1})
issueFound = true
end
-- Delete lingering buttons
if ship.getButtons() ~= nil then
ship.clearButtons()
printToAll(ship.getName() .. '\'s lingering buttons deleted', {0.1, 0.1, 1})
issueFound = true
end
-- If ship has unrecognized model and said that before, remind
if ship.getVar('missingModelWarned') == true then
printToAll('I hope you do remember that I told you about ' .. ship.getName() .. '\'s model being unrecognized when it was first moved/used', {0.1, 0.1, 1})
issueFound = true
end
-- No issues found
if issueFound ~= true then
printToAll(ship.getName() .. ' looks OK', {0.1, 1, 0.1})
end
end
-- Process provided command on a provided object
-- Return true if command has been executed/started
-- Return false if object cannot process commands right now or command was invalid
XW_cmd.Process = function(obj, cmd)
-- Trim whitespaces
cmd = cmd:match( "^%s*(.-)%s*$" )
-- Resolve command type
local type = XW_cmd.CheckCommand(cmd)
-- Process special commands without taking lock into consideration
if type == nil then
return false
elseif type == 'special' then
if cmd == 'diag' then
XW_cmd.Diagnose(obj)
elseif cmd == 'purgeSave' then
XW_cmd.PurgeSave()
elseif cmd == 'hist' then
XW_cmd.ShowHist(obj)
end
end
-- Return if not ready, else process
if XW_cmd.isReady(obj) ~= true then
return false
end
if type == 'demoMove' then
MoveModule.DemoMove(cmd:sub(3, -1), obj)
elseif type == 'move' or type == 'actionMove' then
local info = MoveData.DecodeInfo(cmd, obj)
MoveModule.PerformMove(cmd, obj)
elseif type == 'historyHandle' then
if cmd == 'q' or cmd == 'undo' then
MoveModule.UndoMove(obj)
elseif cmd == 'z' or cmd == 'redo' then
MoveModule.RedoMove(obj)
elseif cmd == 'keep' then
MoveModule.SaveStateToHistory(obj, false)
elseif cmd:sub(1,8) == 'restore#' then
local keyNum = tonumber(cmd:sub(9, -1))
MoveModule.Restore(obj, keyNum)
end
elseif type == 'dialHandle' then
if cmd == 'sd' then
DialModule.SaveNearby(obj)
elseif cmd == 'rd' then
DialModule.RemoveSet(obj)
end
elseif type == 'rulerHandle' then
RulerModule.ToggleRuler(obj, string.upper(cmd))
elseif type == 'action' then
DialModule.PerformAction(obj, cmd)
elseif type == 'bombDrop' then
BombModule.ToggleDrop(obj, cmd)
elseif type == 'AI' then
AIModule.EnableAI(obj, cmd)
end
obj.setDescription('')
return true
end
-- Is object not processing some commands right now?
XW_cmd.isReady = function(obj)
return (obj.getVar('cmdBusy') ~= true)
end
-- Flag the object as processing commands to ignore any in the meantime
XW_cmd.SetBusy = function(obj)
if XW_cmd.isReady(obj) ~= true then
print('Nested process on ' .. obj.getName())
end
obj.setVar('cmdBusy', true)
end
-- Flag the object as ready to process next command
XW_cmd.SetReady = function(obj)
if XW_cmd.isReady(obj) == true then
print('Double ready on ' .. obj.getName())
end
obj.setVar('cmdBusy', false)
end
--------
-- MOVEMENT DATA MODULE
-- Stores and processes data about moves
-- NOT aware of any ship position, operation solely on relative movements
-- Used for feeding data about a move to a higher level movement module
-- Exclusively uses milimeters and degrees for values, needs external conversion
-- Possible commands supported by this module
XW_cmd.AddCommand('[sk][012345][r]?', 'move') -- Straights/Koiograns + stationary moves
XW_cmd.AddCommand('b[rle][0123][strz]?', 'move') -- Banks + segnor and reverse versions
XW_cmd.AddCommand('t[rle][01234][srfbtz]?[t]?', 'move')
XW_cmd.AddCommand('r[rle][123]?', 'actionMove') --New Roll Done
XW_cmd.AddCommand('v[rle][fb][123]?', 'actionMove') --New ViperRoll
XW_cmd.AddCommand('c[srle][123]?', 'actionMove') --New Cloak side
XW_cmd.AddCommand('e[srle][fbrle][123]?', 'actionMove') --New Echo Cloack
XW_cmd.AddCommand('x[rle][fb]?', 'actionMove') -- Barrel rolls
XW_cmd.AddCommand('s[12345]b', 'actionMove') -- Boost straights
XW_cmd.AddCommand('b[rle][123]b', 'actionMove') -- Boost banks
XW_cmd.AddCommand('t[rle][123]b', 'actionMove') -- Boost turns
XW_cmd.AddCommand('a[12]', 'actionMove') -- Adjusts
--XW_cmd.AddCommand('c[srle]', 'actionMove') -- Decloaks side middle + straight
--XW_cmd.AddCommand('c[rle][fb]', 'actionMove') -- Decloaks side forward + backward
--XW_cmd.AddCommand('ch[rle][fb]', 'actionMove') -- Echo's bullshit
--XW_cmd.AddCommand('chs[rle]', 'actionMove') -- Echo's bullshit, part 2
--XW_cmd.AddCommand('vr[rle][fb]', 'actionMove') -- StarViper Mk.II rolls
-- AI Module:
test_AI = false
AIModule = {}
-- 2000mm is the length between opposite corners of an epic table.
AIModule.max_distance = 2000
-- Information about the most recently executed maneuver by an AI ship. This is
-- stored just before we move the ship, and it used when the ship comes to rest
-- and the callback is fired. Yes, this is a bit of a hack, and yes, I'd love to
-- do it a better way.
AIModule.current_move = {}
AIModule.current_move.in_progress = false
AIModule.current_move.Reset = function()
AIModule.current_move.take_action = false
AIModule.current_move.move_code = nil
AIModule.current_move.collision = false
AIModule.current_move.difficulty = nil
AIModule.current_move.obstacle = false
AIModule.current_move.stress_count = 0
AIModule.current_move.is_ionised = false
AIModule.current_move.target = nil
AIModule.current_move.probes = {}
AIModule.current_move.action_stack = {}
end
AIModule.current_move.Reset()
-- Possible commands supported by the AI module
XW_cmd.AddCommand('ai', 'AI') -- Enables the AI on the selected ship
-- Description function to add the AI functions to a ship
AIModule.EnableAI = function(ship, command)
ship.AddContextMenuItem("AI move and action", function(argument) Global.call("PerformAIManeuver", {['ship']=ship, ['take_action']=true}) end, false)
ship.AddContextMenuItem("AI move only", function(argument) Global.call("PerformAIManeuver", {['ship']=ship, ['take_action']=false}) end, false)
end
-- Sanity check, make sure that all the moves for this ship are actually
-- possible. It won't fix every issue but it'll find most typos. This is not
-- called except when debugging.
AIModule.ValidateMoveTables = function(ship)
local ship_id = ship.getTable('Data')['shipId']
local rule_set = BehaviourDB.GetRuleSet()
local behaviour = rule_set.ships[ship_id]
for arc, range_tables in pairs(behaviour.move_table) do
for range, move_table in pairs(range_tables) do
for roll, move_code in pairs(move_table) do
if AIModule.GetMoveDifficulty(ship, move_code) == nil then
print("Couldn't find maneuver " .. move_code .. " from ship " .. tostring(ship_id) .. "'s move table. Arc: " .. arc .. ", range: " .. range .. ", roll: " .. tostring(roll))
end
end
end
end
end
--[[ AI target selection
Target selection is the simplest of the AI submodules. It consists of a set of
functions for different selection types, each of which takes the querying ship
and returns a target ship. There are also some helper methods for filtering
targets by various metrics.
]]
-- Target selection functions. These take a ship, and return a target ship
AIModule.TargetSelectionFunctions = {}
AIModule.TargetSelectionFunctions['ClosestInArc'] = function(ship)
local targets = AIModule.GetSortedTargetsInArc(ship)
-- Get the first ship as it's already been sorted by distance
for i, target in pairs(targets) do
return target['ship']
end
return nil
end
AIModule.TargetSelectionFunctions['Closest'] = function(ship)
local potential_targets = ArcCheck.GetPotentialTargets(ship, AIModule.max_distance)
local closest_distance = nil
local closest_target = nil
for i, target in pairs(potential_targets) do
local distance = Vect.Length(ship.getPosition() - target.GetPosition())
if closest_distance == nil or distance < closest_distance then
closest_distance = distance
closest_target = target
end
end
return closest_target
end
AIModule.TargetSelectionFunctions['LockedInRange'] = function(ship)
local potential_targets = AIModule.GetSortedTargetsInArc(ship, 'all')
potential_targets = table.sieve(potential_targets, AIModule.FilterInRange)
local closest_distance = nil
local closest_target = nil
for i, target in pairs(potential_targets) do
-- Is this ship locked by us?
if AIModule.HasTargetLockOnShip(ship, target['ship']) then
local distance = Vect.Length(ship.getPosition() - target['ship'].GetPosition())
if closest_distance == nil or distance < closest_distance then
closest_distance = distance
closest_target = target['ship']
end
end
end
return closest_target
end
AIModule.TargetSelectionFunctions['ClosestInArcLowerInitiative'] = function(ship)
local initiative = ship.getTable('Data')['initiative']
local targets = AIModule.GetSortedTargetsInArc(ship)
-- Get the first ship with a lower initiative as it's already been sorted by
-- distance.
for i, target in pairs(targets) do
if (target['ship'].getTable('Data')['initiative'] < initiative) then
return target['ship']
end
end
return nil
end
-- Only choose ships that are in arc and between ranges 1-3
AIModule.FilterInArc = function(el)
if el['in_arc'] == false then
return false
end
return true;
end
-- Only choose ships that are between range one and three
AIModule.FilterInRange = function(el)
if el['closest'].range < 1 or el['closest'].range > 3 then
return false
end
return true;
end
-- Assumed to have been filtered first
AIModule.SortTargetsByAIDesirability = function(e1, e2)
if e1['closest'] ~= nil and e2['closest'] ~= nil then
if e1['closest'].length < e2['closest'].length then
return true
elseif e1['closest'].length > e2['closest'].length then
return false
end
end
return e1['ship'].getGUID() > e2['ship'].getGUID()
end
AIModule.GetSortedTargetsInArc = function(ship, arc)
local targets = ArcCheck.GetTargetsInRelationToArc(ship, arc or 'front')
targets = table.sieve(targets, AIModule.FilterInArc)
targets = table.sieve(targets, AIModule.FilterInRange)
table.sort(targets, AIModule.SortTargetsByAIDesirability)
return targets
end
AIModule.HasTargetLockOnShip = function(locking_ship, locked_ship)
local lock_tokens = locked_ship.call("GetTokens", {type="targetLock"})
for j, token in pairs(lock_tokens) do
if token.GetName() == locking_ship.GetName() then -- TODO: this really should be guid
return true
end
end
return false
end
--[[ AI maneuver selection
The maneuver selection is the main submodule of the AI, as well as the one that
ties most of the others together. It is home to one of the entry points,
"PerformAIManever". The main function, AIModule.PerformManeuver, uses the
target selection functions to pick a target, then plots a maneuver based on
the move tables in BehaviourDB.ttslua. It also cover stress, ion, and obstacle
avoidance.
Worth noting is that due to having to wait for ships to finish moving, part of
the functionality of AIModule.PerformManeuver had to be split out to a callback
function, AIModule.ManeuverPostShipRest. This handles everything after the
maneuver is executed, including calling the action submodule.
It also includes various helper functions for searching through and transforming
manveuvers.
]]
function PerformAIManeuver(args)
local ship = args.ship
local take_action = args.take_action
AIModule.PerformManeuver(ship, take_action)
end
AIModule.PerformManeuver = function(ship, take_action)
if AIModule.current_move.in_progress then
printToAll("Can't perform AI maneuver while another ship is moving.", color(1.0, 1.0, 0.2, 0.9))
return
end
--AIModule.ValidateMoveTables(ship)
AIModule.current_move.Reset()
AIModule.current_move.stress_count = TokenModule.GetShipTokenCount(ship, "Stress")
AIModule.current_move.take_action = take_action
local stress = AIModule.current_move.stress_count > 0
local rule_set = BehaviourDB.GetRuleSet()
local ship_id = ship.getTable('Data')['shipId']
local ship_behaviour = rule_set.ships[ship_id]
if ship_behaviour ~= nil then
printToAll('Performing AI routine for ' .. ship.GetName(), color(1.0, 1.0, 0.2, 0.9))
local move_code = nil
-- Find this ship's target.
local target_ship = nil
for _, target_selection_function in ipairs(ship_behaviour.target_selection) do
target_ship = AIModule.TargetSelectionFunctions[target_selection_function](ship)
if target_ship ~= nil then
break
end
end
AIModule.current_move.target = target_ship
-- Check if we're ionised.
local is_ionised = AIModule.IsIonised(ship)
AIModule.current_move.is_ionised = is_ionised
if target_ship == nil then
-- TODO: This is undefined behaviour, and highly unlikely to happen.
-- Should it default to a 2 straight?
printToAll('Failed to find target')
elseif is_ionised == true then
move_code = 's1'
printToAll('Ionised, skipping movement selection')
else
-- Find out the arc that the target is in.
local target_arc = nil
local arc_parts = nil
local ship_size = ship.getTable("Data").Size or 'small'
if rule_set.useBullseyeArc then
-- First do a check to see if the ship is in our bullseye.
-- Move the bullseye position ahead because it's in the middle of
-- range ruler rather than one end. Could this be moved into the
-- GetBullseyeTargets function?
local bullseye_position = vector(ArcCheck.bullseye_data.pos[ship_size][1], ArcCheck.bullseye_data.pos[ship_size][2], ArcCheck.bullseye_data.pos[ship_size][3] + Convert_mm_igu(AIModule.max_distance / 2) - (ArcCheck.bullseye_data.size[3] / 2))
local bullseye_targets = ArcCheck.GetBullseyeTargets(ship, bullseye_position, AIModule.max_distance)
for i, bullseye_target in pairs(bullseye_targets) do
if bullseye_target == target_ship then
target_arc = 'bullseye'
arc_parts = {'front'}
break
end
end
end
if target_arc == nil then
-- We have our target ship. Now we do some vector maths to get
-- the angle pointing from our ship to the target ship.
local ship_facing = Vect.RotateDeg({0, 0, -1}, ship.GetRotation().y)
local ship_to_target = target_ship.GetPosition() - ship.getPosition()
local angle_to_target = Vect.AngleDeg(ship_facing, ship_to_target)
-- angleToTarget is normalised between 0 and 180, to work out if
-- if it's pointing to our left or our right we need to get the
-- dot product of the shipToTarget vector and the tangent to our
-- facing.
local ship_facing_tangent = {ship_facing[3], 0, -ship_facing[1]}
local tangent_dot_product = Vect.DotProd(ship_to_target, ship_facing_tangent)
if tangent_dot_product < 0 then
angle_to_target = angle_to_target * -1
end
arc_result = rule_set.degreesToArc(angle_to_target)
target_arc = arc_result.target_arc
arc_parts = arc_result.arc_parts
end
-- Find the range bracket - one of closing, fleeing, distant, or
-- stress.
local range_bracket = nil
if stress then
range_bracket = 'stress'
printToAll(' Selected target: ' .. target_ship.getName() .. ', in the ' .. target_arc .. ' arc, while stressed.')
else
local target_range = nil
-- Find the range, if we haven't got it already.
-- TODO: If a ship is at range 0, then treat it as range 1 and
-- behind our ship. (pg 19, "Touching")
if target_range == nil then
-- TODO: Wrap up this code in a re-useable function
local own_line_segments = {}
for i, arc in ipairs(arc_parts) do
local segments = ArcCheck.GetOwnArcLineSegments(ship, ArcCheck.arc_line_segments[ship_size][arc]["segments"])
for k, segment in ipairs(segments) do
table.insert(own_line_segments, segment)
end
end
local target_line_segments = {}
for i, arc in ipairs(arc_parts) do
local segments = ArcCheck.GetTargetLineSegmentsInArc(ship, target_ship, ArcCheck.arc_line_segments[ship_size][arc]["degrees"])
for k, segment in ipairs(segments) do
table.insert(target_line_segments, segment)
end
end
if #target_line_segments ~= 0 then
local closest = nil
closest, _ = ArcCheck.GetDistanceBetweenLineSegments(own_line_segments, target_line_segments)
target_range = closest.range
end
end
-- If we're at range 2, then we need to check if the target is
-- moving towards us (closing) or moving away from us (fleeing)
target_closing = nil
closing_text = '.'
if target_range == 2 then
local target_ship_facing = Vect.RotateDeg({0, 0, -1}, target_ship.GetRotation().y)
local target_to_ship = ship.getPosition() - target_ship.GetPosition()
local angle_to_ship = Vect.AngleDeg(target_ship_facing, target_to_ship)
if (angle_to_ship < 90) then
target_closing = true
closing_text = ' and closing.'
else
target_closing = false
closing_text = ' and fleeing.'
end
end
printToAll(' Selected target: ' .. target_ship.getName() .. ', in the ' .. target_arc .. ' arc at range ' .. tostring(target_range) .. closing_text)
-- Get the move for our situation.
-- Convert our target's range and facing into a range bracket.
range_bracket = 'distant'
if (target_range <= 1 or (target_range == 2 and target_closing == true)) then
range_bracket = 'near'
elseif (target_range == 3 or (target_range == 2 and target_closing == false)) then
range_bracket = 'far'
end
end
-- Check if we need to flip the facing and the final move.
local flip_move = ship_behaviour.move_table[target_arc] == nil
if (flip_move) then
target_arc = rule_set.flipArc(target_arc)
end
-- Roll the d6, and keep substracting 1 until we find a move.
local d6_roll = math.random(6)
while move_code == nil and d6_roll > 0 do
move_code = ship_behaviour.move_table[target_arc][range_bracket][d6_roll]
d6_roll = d6_roll - 1
end
-- If we had to flip the arc, then also flip the resulting move.
if (flip_move) then
if string.find(move_code, 'l') then
move_code = string.gsub(move_code, 'l', 'r')
elseif string.find(move_code, 'r') then
move_code = string.gsub(move_code, 'r', 'l')
end
end
end
AIModule.current_move.move_code = move_code
if move_code ~= nil then
-- Check for collisions.
local move_info = MoveData.DecodeInfo(move_code, ship)
local probe_data = MoveModule.MoveProbe.TryFullMove(move_info, ship, MoveModule.GetFullMove)
AIModule.current_move.collision = probe_data.collObj ~= nil
if probe_data.collObs ~= nil then
printToAll(' Tried ' .. move_code .. ', hit an obstacle')
AIModule.current_move.obstacle = true
-- We've hit an obstacle! If we aren't ionised then We'll try to
-- swerve.
if is_ionised == false then
-- We pick up to two maneuvers that are the closest to our
-- current maneuver, and possible for our ship and stress
-- level. Whichever of these will get us closer to our
-- target is the one that we try. If that move avoids an
-- obstacle, then we choose it instead, otherwise we stick
-- with our original maneuver.
-- TODO: deal with reverse moves
local potential_swerve_moves = {}
local swerve_speed = tostring(math.max(1, math.min(3, move_info.speed)))
if move_info.type == 'straight' then
potential_swerve_moves = {'bl' .. swerve_speed, 'br' .. swerve_speed}
elseif move_info.type == 'bank' then
potential_swerve_moves = {'t' .. string.sub(move_info.dir, 1, 1) .. swerve_speed, 's' .. swerve_speed}
elseif move_info.type == 'turn' then
potential_swerve_moves = {'b' .. string.sub(move_info.dir, 1, 1) .. swerve_speed}
end
local swerve_moves = {}
for _, potential_swerve_move_code in ipairs(potential_swerve_moves) do
-- If we can't do this move, then get the closest move that
-- we can do and try that instead. This may be multiple
-- moves, for example if a two-bank is impossible then we
-- may be able to try a one-bank and a three-bank. They're
-- both the same distance from the original so we'll try
-- both.
local nearest_move_codes = AIModule.GetNearestMoves(ship, potential_swerve_move_code, stress == false)
for _, nearest_move_code in ipairs(nearest_move_codes) do
table.insert(swerve_moves, nearest_move_code)
end
end
-- Loop through all of the possible swerve moves, and find out
-- which one will get us the closest to our target. This is the
-- maneuver that we'll try again with.
local closest_swerve_move_code = nil
local closest_swerve_distance = nil
for _, swerve_move_code in ipairs(swerve_moves) do
local post_swerve = MoveModule.GetFullMove(swerve_move_code, ship)
local post_swerve_position = Vector(post_swerve.pos[1], post_swerve.pos[2], post_swerve.pos[3])
local distance = Vect.Length(post_swerve_position - target_ship.GetPosition())
if closest_swerve_distance == nil or distance < closest_swerve_distance then
closest_swerve_distance = distance
closest_swerve_move_code = swerve_move_code
end
end
if closest_swerve_move_code ~= nil then
local swerve_move_info = MoveData.DecodeInfo(closest_swerve_move_code, ship)
local swerve_probe_data = MoveModule.MoveProbe.TryFullMove(swerve_move_info, ship, MoveModule.GetFullMove)
if swerve_probe_data.collObs == nil then
move_code = closest_swerve_move_code
print(' Tried ' .. closest_swerve_move_code .. ', avoids the obstacle.')
AIModule.current_move.obstacle = false
AIModule.current_move.collision = swerve_probe_data.collObj ~= nil
else
print(' Tried ' .. closest_swerve_move_code .. ', still hits an obstacle.')
end
end
end
end
-- TODO: Check for edge of table collision
AIModule.current_move.difficulty = 'b'
-- If we are ionised, then our move is always blue and we remove
-- all of our ion tokens. Otherwise, we check for our difficulty.
if is_ionised then
while TokenModule.GetShipTokenCount(ship, 'Ion') > 0 do
-- BUG: This fails to identify the ion token stack if the
-- ship has exactly two stress tokens. If this occurs, the
-- ship also leaves behind the single remaining stress
-- token. I've done a bit of debugging and it appears that
-- when a token stack has "takeObject" called on it, it
-- changes from a 'chip' type to a 'generic' type. This
-- means that it isn't picked up by ObjType.GetNearOfType.
-- This does correct itself if the tokens are moved around
-- manually, so I assume that this is just a one-frame
-- occurence that wouldn't matter if the AI wasn't doing
-- everything at the same time. Potential fix: set
-- '__XW_Token' on the token afterwards?
DialModule.PerformAction(ship, 'Ion', ship.getVar('owningPlayer'), {['remove'] = true})
end
else
AIModule.current_move.difficulty = AIModule.GetMoveDifficulty(ship, move_code)
end
-- Remove stress _before_ the move - if we wait till afterwards then
-- the tokens haven't caught up with the ship and the token module
-- doesn't count them.
if stress and AIModule.current_move.difficulty == 'b' then
DialModule.PerformAction(ship, 'Stress', ship.getVar('owningPlayer'), {['remove'] = true})
AIModule.current_move.stress_count = AIModule.current_move.stress_count - 1
end
AIModule.current_move.in_progress = true
if MoveModule.PerformMove(move_code, ship, false, AIModule.ManeuverPostShipRest) == false then
-- If the PerformMove function retured false, then the ship
-- could not move at all. In this case the post-rest callback
-- won't be fired, so we'll call it ourselves. Also, just in
-- case, set the collision flag.
AIModule.current_move.collision = true
AIModule.ManeuverPostShipRest(ship)
end
end
else
printToAll('No AI routine found for ship ID ' .. tostring(ship_id), color(1.0, 1.0, 0.2, 0.9))
end
end
AIModule.ManeuverPostShipRest = function(ship)
local action_selected = false;
if AIModule.current_move.difficulty == 'r' then
DialModule.PerformAction(ship, 'Stress', ship.getVar("owningPlayer"))
elseif AIModule.current_move.take_action == true and AIModule.current_move.stress_count == 0 and AIModule.current_move.collision == false and AIModule.current_move.obstacle == false then
if AIModule.current_move.is_ionised then
-- TODO: Only perform a focus action (if possible)
else
-- Check for Full Throttle and apply it now if appropriate.
AIModule.ApplySpecialAbility(ship, 'fullThrottle')
-- Run through the ship's actions and try them in order.
action_selected = AIModule.SelectAction(ship)
end
end
if action_selected then
AIModule.ProcessActionStack(ship)
else
AIModule.current_move.in_progress = false
end
end
--[[ AIModule.GetMoveDifficulty
This function takes a ship and a move code and returns the difficulty of the
move.
ship: TTS Object, the ship performing the move
move_code: String, the move code being queried.
returns a string corresponding to the difficulty of the move for the ship:
'b': blue maneuver, reduces stress
'w': white manuever, stress neutral
'r': red maneuver, causes stress
nil: maneuver is impossible for the given ship
]]
AIModule.GetMoveDifficulty = function(ship, move_code)
move_set = ship.getTable('Data')['moveSet']
for _, raw_move_code in ipairs(move_set) do
local ship_move_difficulty = string.sub(raw_move_code, 1, 1)
local ship_move_code = string.sub(raw_move_code, 2)
if ship_move_code == move_code then
return ship_move_difficulty
end
end
return nil
end
--[[ AIModule.GetNearestMoves
This function takes a ship and a move code, and returns the nearest maneuvers
that the ship is capable of. This may be the specified move. "Nearest" is
defined as closest in speed. We won't change the angle of the move. So for
example, if tr1 is passed in we will try tr2 and tr3, but not br1.
ship: TTS Object, the ship whose move set the function will be testing
move_code: String, the move code we want to get the closest maneuver to
can_perform_red_maneuvers: Boolean, if true then we will try red maneuvers,
if false then we won't.
returns a table of move codes, as strings.
]]--
AIModule.GetNearestMoves = function(ship, move_code, can_perform_red_maneuvers)
move_set = ship.getTable('Data')['moveSet']
local move_info = MoveData.DecodeInfo(move_code, ship)
local original_speed = move_info.speed
-- We're going to loop through all of the ships moves and test for whether
-- they're possible or not, and each loop we'll increase the speed offset
-- by one. So 0 first, then -1 and +1, then -2 and +2, ... First we need
-- to calculate the maximum number of loops we can make.
local min_speed = 1
local max_speed = 3
if move_info.type == 'straight' then
min_speed = 0
max_speed = 5
end
max_offset = math.max(move_info.speed - min_speed, max_speed - move_info.speed)
-- Break the move code into sections so we can change the speed easily.
local speed_position = string.find(move_code, tostring(original_speed))
local move_pre_speed = string.sub(move_code, 1, speed_position - 1)
local move_post_speed = string.sub(move_code, speed_position + 1)
-- Loop through all the moves at the current offset. If we find at least
-- one possible move, then return it.
local speed_offset = 0
while speed_offset <= max_offset do
-- Prepare a table of 1-2 speeds to try this loop
local test_speeds = {}
if speed_offset == 0 then
table.insert(test_speeds, original_speed)
else
for _, direction in ipairs({1, -1}) do
local test_speed = original_speed + speed_offset * direction
if test_speed <= max_speed and test_speed >= min_speed then
table.insert(test_speeds, test_speed)
end
end
end
-- Turn the move speeds into move codes
test_move_codes = {}
for _, test_speed in ipairs(test_speeds) do
table.insert(test_move_codes, move_pre_speed .. tostring(test_speed) .. move_post_speed)
end
-- Loop through and check if these maneuvers are in our list
move_replacements = {}
for _, raw_move_code in ipairs(move_set) do
local move_difficulty = string.sub(raw_move_code, 1, 1)
if move_difficulty ~= 'r' or can_perform_red_maneuvers then
local move_code = string.sub(raw_move_code, 2)
for _, test_move_code in ipairs(test_move_codes) do
if test_move_code == move_code then
table.insert(move_replacements, move_code)
end
end
end
end
if #move_replacements ~= 0 then
return move_replacements
end
speed_offset = speed_offset + 1
end
end
AIModule.ApplySpecialAbility = function(ship, ability)
if AIModule.HasSpecialAbility(ship, ability) then
if ability == 'fullThrottle' then
-- Take an evade action if the ship fully executed (ie, no
-- ship-to-ship collision) a 3-5 maneuver. We're assumed to not be
-- stressed as this is only called right before actions are
-- selected.
if AIModule.current_move.collision == false then
local move_code = AIModule.current_move.move_code
if string.find(move_code, '3') or string.find(move_code, '4') or string.find(move_code, '5') then
printToAll(string.format('%s used full throttle to evade.', ship.getName()))
DialModule.PerformAction(ship, 'Evade', ship.getVar('owningPlayer'))
end
end
end
end
end
AIModule.HasSpecialAbility = function(ship, ability)
local rule_set = BehaviourDB.GetRuleSet()
local ship_id = ship.getTable('Data')['shipId']
local ship_behaviour = rule_set.ships[ship_id]
if ship_behaviour.special_rules == nil then
return false
end
for _, ship_ability in pairs(ship_behaviour.special_rules) do
if ability == ship_ability then
return true
end
end
return false
end
--[[ AIModule.IsIonised
This function returns whether or not a ship has enough ion tokens to ionise it.
ship: TTS Object, the ship performing the move
returns true if the ship is ionised, false otherwise.
]]
AIModule.IsIonised = function(ship)
local ion_count = TokenModule.GetShipTokenCount(ship, "Ion")
local ship_size = ship.getTable('Data').Size or 'small'
if ship_size == 'small' then
return ion_count >= 1
elseif ship_size == 'medium' then
return ion_count >= 2
elseif ship_size == 'large' then
return ion_count >= 3
else
return ion_count >= 6
end
return false
end
--[[ AI action selection
This submodule is smaller than the maneuver system, but probably more complex.
The main function is AIModule.AttemptAction, which takes an action definition
and checks if it is possible to execute. This involves checking the action's
pre-conditions (if the ship is in the right situation to attempt it), then if
the ship can actually execute the action, and finally checking the action's
post-conditions (if the action met with the desired result).
Where this gets complicated is the use of action probes, which are hypothetical
future positions of the ship. So if a ship makes a boost, then we create a probe
for each type of boost (bl1, s1, br1) and evaluate the post-conditions for each
probe. Where this gets _really_ complicated is when handling ships that can
string together more than one move action, such as the Interceptor's boost
action linking to a barrel roll action. For this we have to "expand" the first
set of probes by the second action - so the probe "bl1" expands into six more
probes: "bl1 > rl1", "bl1 > rl2", "bl1 > rl3", "bl1 > rr1", "bl1 > rr2", and
"bl1 > rr3". These permutations can get quite large (45 in the case of the
Interceptor) so these are cached where possible, but even so they can lead to
long execution times.
Finally, the actions are packed into a stack and processed one-by-one. Just like
the maneuver submodule these have to be dealt with in a callback to wait for the
TTS components to come to a rest.
]]
-- List which actions are move actions, and therefore have to be expanded into
-- a list of potential move codes. Any action not listed here is assumed to be
-- possible to made at any time (such as 'focus', 'evade', etc)
AIModule.move_actions = {