-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Actor.lua
1352 lines (1106 loc) · 40.4 KB
/
Actor.lua
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
-- A few persistent-state options used
RAIL.Validate.TempFriendRange = {"number",-1,-1}
RAIL.Event["AI CYCLE"]:Register(-44, -- Priority
"Actor specialization", -- Handler name
1, -- Max runs
function() -- Handler function
-- Track HP and SP
do
local update = RAIL.Owner.Update
-- An extended variant of Update() to track HP and SP values
RAIL.Owner.Update = function(self,...)
-- First, call the regular update
update(self,unpack(arg))
-- Update the HP and SP tables
History.Update(self.HP,GetV(V_HP,self.ID))
History.Update(self.SP,GetV(V_SP,self.ID))
return self
end
RAIL.Self.Update = RAIL.Owner.Update
RAIL.Owner.GetMaxHP = function(self) return GetV(V_MAXHP,self.ID) end
RAIL.Owner.GetMaxSP = function(self) return GetV(V_MAXSP,self.ID) end
RAIL.Self.GetMaxHP = RAIL.Owner.GetMaxHP
RAIL.Self.GetMaxSP = RAIL.Owner.GetMaxSP
-- Use the maximum values as default, but don't calculate sub-update values
RAIL.Owner.HP = History.New(GetV(V_MAXHP,RAIL.Owner.ID),false)
RAIL.Owner.SP = History.New(GetV(V_MAXSP,RAIL.Owner.ID),false)
RAIL.Self.HP = History.New(GetV(V_MAXHP,RAIL.Self.ID),false)
RAIL.Self.SP = History.New(GetV(V_MAXSP,RAIL.Self.ID),false)
end
-- Never show up as either enemies or friends
RAIL.Owner.IsEnemy = function() return false end
RAIL.Owner.IsFriend = function() return false end
RAIL.Self.IsEnemy = function() return false end
RAIL.Self.IsFriend = function() return false end
-- Don't allow owner or self to expire
RAIL.Owner.ExpireTimeout[1] = false
RAIL.Owner.Active = true
RAIL.Self.ExpireTimeout[1] = false
RAIL.Self.Active = true
-- Add a function to the AI's self
RAIL.Self.GetUsableSP = function(self,skill)
local state = RAIL.State.SkillOptions[skill.ID]
local reserved_sp = state.ReservedSP
if state.ReservedSPisPercent then
reserved_sp = math.ceil(reserved_sp / 100 * self:GetMaxSP())
end
return self.SP[0] - reserved_sp
end
end)
-- Actor data-collection
do
-- This unique table ID will be used as a key to identify Actor tables
local actor_key = {}
RAIL.IsActor = function(actor)
if type(actor) ~= "table" then return false end
if actor[actor_key] == nil then return false end
return true
end
-- The Actor "class" is private, because they're generated by referencing Actors
local Actor = { }
-- Metatables
local Actor_mt = {
__eq = function(self,other)
if not RAIL.IsActor(other) then return false end
return self.ID == other.ID
end,
__index = Actor,
-- When tostring() is called on Actors, we want sensible output
__tostring = function(self)
local buf = StringBuffer.New()
:Append(self.ActorType):Append(" #"):Append(self.ID)
:Append(" [Loc:(")
:Append(self.X[0]):Append(","):Append(self.Y[0])
:Append(")")
if self.Type ~= -2 then
buf:Append(", Type:"):Append(self.Type)
end
if self.BattleOpts.Name ~= RAIL.State.ActorOptions.Default.Name then
buf:Append(", Name:"):Append(self.BattleOpts.Name)
end
return buf:Append("]"):Get()
end
}
-- Private key for keeping closures
local closures = {}
-- Private key of TargetOf, to keep the time of last table update
local targeted_time = {}
-- Position tracking uses a specialty "diff" function
local pos_diff = function(a_value,a_time,b_value,b_time)
-- If a tile changed, then the position is different
if math.abs(a_value-b_value) >= 1 then return true end
-- If enough time has passed, count the position as different
-- Note: This ensures that subvalues will be accurately calculated
-- Note: This isn't really needed...
--if math.abs(a_time-b_time) > 500 then return true end
-- Otherwise, the position is still the same
return false
end
-- BattleOpts metatable
local battleopts_parent = {}
local seen_types = {
["NPC"] = true,
[45] = true,
}
local battleopts_mt = {
__index = function(self,key)
self = self[battleopts_parent]
-- First, check ByID table
local ret = RAIL.State.ActorOptions.ByID[self.ID][key]
if ret ~= nil then
return ret
end
-- Then, check ByType table
if self.Type ~= -2 then
-- Get the table for this type
local type_state = rawget(RAIL.State.ActorOptions.ByType,self.Type)
-- If there was no table yet, log it and then generate it
if not type_state then
-- Generate the table automatically
type_state = RAIL.State.ActorOptions.ByType[self.Type]
-- Check if this is the first instance
if not seen_types[self.Type] and not seen_types[self.ActorType] then
local type_name = ""
if
type(type_state.Name) == "string" and
type_state.Name ~= RAIL.State.ActorOptions.Default.Name
then
type_name = " (" .. type_state.Name .. ")"
end
-- Log it
RAIL.LogT(55,
"Actor type #{1}{2} not in state-file; only logging first occurrence.",
self.Type,
type_name)
-- Set this type as seen
seen_types[self.Type] = true
end
end
ret = type_state[key]
if ret ~= nil then
return ret
end
end
-- If all else fails, use the defaults
return RAIL.State.ActorOptions.Default[key]
end,
}
-- Initialize a new Actor
Actor.New = function(self,ID)
local ret = { }
setmetatable(ret,Actor_mt)
ret.ActorType = "Actor"
ret.ID = ID
ret.Active = false -- false = expired; true = active
ret.Type = -1 -- "fixed" type (homus don't overlap players)
ret.Hide = false -- hidden?
ret.LastCycle = RAIL.CycleID -- Unique cycle to see if actor was updated this round
ret.LastUpdate = -1 -- GetTick() of last :Update() call
ret.FullUpdate = false -- Track position, motion, target, etc?
ret.TargetOf = Table:New() -- Other Actors that are targeting this one
ret.IgnoreTime = -1 -- Actor isn't currently ignored
ret.BattleOpts = { } -- Battle options
ret.BattleOpts[battleopts_parent] = ret
-- Set defaults for battle options
setmetatable(ret.BattleOpts,battleopts_mt)
-- The following have their histories tracked
ret.Target = History.New(-1,false)
ret.Motion = History.New(MOTION_STAND,false)
-- And they'll also predict sub-history positions
ret.X = History.New(-1,true,pos_diff)
ret.Y = History.New(-1,true,pos_diff)
-- Set initial position
local x,y = GetV(V_POSITION,ret.ID)
if x ~= -1 or y ~= nil then
-- Hiding?
if
(x == 0 and y == 0) or
(x == -1 and y == -1)
then
ret.Hide = true
else
History.Update(ret.X,RoundNumber(x))
History.Update(ret.Y,RoundNumber(y))
end
end
-- Set up the expiration timeout for 2.5 seconds...
-- (it will be updated in Actor.Update)
ret.ExpireTimeout = RAIL.Timeouts:New(2500,false,Actor.Expire,ret,"timeout")
-- Create tables to hold the closures
ret[closures] = {
DistanceTo = {},
BlocksTo = {},
AngleTo = {},
AngleFrom = {},
AnglePlot = {},
}
-- Initialize the type
Actor[actor_key](ret)
-- Log
if ID ~= -1 then
RAIL.LogT(40,"Actor class generated for {1}.",ret)
-- Extra data displayed for mercenary AIs
if false and RAIL.Mercenary then
-- Mercenaries should log extra information for Actors and NPCs
if ret.ActorType == "Actor" or ret.ActorType == "NPC" then
RAIL.LogT(40," --> {1}",StringBuffer.New()
--:Append("V_TYPE="):Append(GetV(V_TYPE,ret.ID)):Append("; ")
--:Append("V_HOMUNTYPE="):Append(GetV(V_HOMUNTYPE,ret.ID)):Append("; ")
--:Append("V_MERTYPE="):Append(GetV(V_MERTYPE,ret.ID)):Append("; ")
--:Append("V_MOTION="):Append(GetV(V_MOTION,ret.ID)):Append("; ")
--:Append("V_TARGET="):Append(GetV(V_TARGET,ret.ID)):Append("; ")
--:Append("IsMonster="):Append(IsMonster(ret.ID)):Append("; ")
:Get()
)
end
end
end
return ret
end
-- A temporary "false" return for IsEnemy, as long as an actor is a specific type
local ret_false = function() return false end
-- A "private" function to initialize new actor types
do
Actor[actor_key] = {
[true] = function(self,t,notnpc)
-- Set the new type
self[actor_key] = t
self.Type = t
-- Check the type for sanity
if (self.ID < 100000 or self.ID > 110000000) and
-- Homunculus types
((1 <= self.Type and self.Type <= 16) or
-- Mercenary types
(17 <= self.Type and self.Type <= 46)) and
-- Not a portal
(self.Type ~= 45 or notnpc)
then
self.Type = self.Type + 6000
end
-- Initialize differently based upon type
if self.Type == -1 then
-- Unknowns are never enemies, but track data
return "Unknown",false,true
-- Portals
elseif self.Type == 45 then
-- Portals are never enemies and shouldn't be tracked
return "Portal",false,false
-- Player Jobs
elseif
(0 <= self.Type and self.Type <= 25) or
(161 <= self.Type and self.Type <= 181) or
(4001 <= self.Type and self.Type <= 4049)
then
-- Players are potential enemies and should be tracked
return "Player",true,true
-- AI-controlled (homunc, mercenary)
-- TODO: Find what the cap for AI-controlled actors should properly be.
elseif 6000 < self.Type and self.Type <= 6046 then
-- AIs are similar to players: potential enemies, should be tracked
return "AI",true,true
-- NPCs (non-player jobs that are below 1000)
elseif self.Type < 1000 then
-- NPCs are never enemies and shouldn't be tracked
return "NPC",false,false
-- All other types
else
-- All other actors are probably monsters or homunculi
return "Actor",true,true
end
end,
[false] = function(self,t,notnpc)
self[actor_key] = t
self.Type = t
-- Find players based on ID
if self.ID >= 100000 and self.ID <= 110000000 then
-- Likely a player
return "Player",true,true
-- NPCs and Portals stand still and are never monsters
elseif
not notnpc and
IsMonster(self.ID) == 0 and
not self.WasMonster and
GetV(V_MOTION,self.ID) == MOTION_STAND and
GetV(V_TARGET,self.ID) == 0
then
-- Likely an NPC
return "NPC",false,false
-- All other types
else
return "Actor",true,true
end
end,
}
setmetatable(Actor[actor_key],{
__call = function(self,actor,...)
-- Get the type from MobID handler
local t = RAIL.MobID[actor.ID]
-- Pass it to the proper function
local possibleEnemy
actor.ActorType,possibleEnemy,actor.FullUpdate = self[t ~= -2](actor,t,unpack(arg))
if possibleEnemy then
if rawget(actor,"IsEnemy") == ret_false then
rawset(actor,"IsEnemy",nil)
end
else
actor.IsEnemy = ret_false
end
end,
})
end
-- Update information about the actor
Actor.Update = function(self)
-- Check if the actor is dead
if GetV(V_MOTION,self.ID) == MOTION_DEAD then
-- If the actor is still active, cause it to expire
if self.ExpireTimeout[1] then
self:Expire("death")
end
return self
end
-- Check for a type change
if RAIL.MobID[self.ID] ~= self[actor_key] then
-- Pre-log
local str = tostring(self)
-- Call the private type changing function
Actor[actor_key](self)
-- Log
RAIL.LogT(40,"{1} changed type to {2}.",str,tostring(self))
elseif self.Type == 45 and GetV(V_MOTION,self.ID) ~= MOTION_STAND then
-- Call the private type changing function
Actor[actor_key](self,true)
-- Log
RAIL.LogT(40,"Incorrectly identified {1} as a Portal; fixed.",self)
elseif self.Type == -2 and self.ActorType == "NPC" and GetV(V_MOTION,self.ID) ~= MOTION_STAND then
-- Call the private type changing function
Actor[actor_key](self,true)
-- Log
RAIL.LogT(40,"Incorrectly identified {1} as an NPC; fixed.",self)
end
-- Update ignore time
if self.IgnoreTime > 0 then
self.IgnoreTime = self.IgnoreTime - (GetTick() - self.LastUpdate)
end
-- Update the expiration timeout
self.ExpireTimeout[2] = GetTick()
if not self.ExpireTimeout[1] and not self.Active then
self.ExpireTimeout[1] = true
RAIL.Timeouts:Insert(self.ExpireTimeout)
-- Add the X and Y position
local x,y = GetV(V_POSITION,self.ID)
if x ~= -1 then
History.Update(self.X,x)
History.Update(self.Y,y)
end
-- Log its reactivation
RAIL.LogT(40,"Reactivating {1}.",self)
end
-- Update the LastUpdate and LastCycle fields
self.LastUpdate = GetTick()
self.LastCycle = RAIL.CycleID
-- The actor is active unless it expires
self.Active = true
-- Some actors don't require everything tracked
if not self.FullUpdate then
return self
end
-- Update the motion
History.Update(self.Motion,GetV(V_MOTION,self.ID))
-- Update the actor location
local x,y = GetV(V_POSITION,self.ID)
if x ~= -1 or y ~= nil then
-- Check for hidden
if
(x == 0 and y == 0) or
(x == -1 and y == -1)
then
if not self.Hide then
-- Log it
RAIL.LogT(60,"{1} became invisible.",self)
self.Hide = true
end
else
-- Make sure the X,Y integers are even
x,y = RoundNumber(x,y)
if self.Hide then
-- Log it
RAIL.LogT(60,"{1} is no longer invisible.",self)
self.Hide = false
end
History.Update(self.X,x)
History.Update(self.Y,y)
end
end
-- Check if the actor is able to have a target
if self.Motion[0] ~= MOTION_SIT then
-- Get the current target
local targ = GetV(V_TARGET,self.ID)
-- Normalize it...
if targ == 0 then
targ = -1
end
-- Keep a history of it
History.Update(self.Target,targ)
-- Tell the other actor that it's being targeted
if targ ~= -1 then
Actors[targ]:TargetedBy(self)
end
else
-- Can't target, so it should be targeting nothing
History.Update(self.Target,-1)
end
-- Clear the targeted-by table if it's old
if math.abs((self.TargetOf[targeted_time] or 0) - GetTick()) > 50 then
self.TargetOf = Table:New()
end
return self
end
-- Track when other actors target this one
Actor.TargetedBy = function(self,actor)
-- If something targets an NPC, it isn't an NPC
if self.Type == -2 and self.ActorType == "NPC" then
-- Call the private type changing function
Actor[actor_key](self,true)
-- Log
RAIL.LogT(40,"Incorrectly identified {1} as an NPC; fixed.",self)
elseif self.Type == 45 then
-- Call the private type changing function
Actor[actor_key](self,true)
-- Log
RAIL.LogT(40,"Incorrectly identified {1} as a Portal; fixed.",self)
end
-- Use a table to make looping through and counting it faster
-- * to determine if an actor is targeting this one, use Actors[id].Target[0] == self.ID
if math.abs((self.TargetOf[targeted_time] or 0) - GetTick()) > 50 then
self.TargetOf = Table:New()
self.TargetOf[targeted_time] = GetTick()
end
self.TargetOf:Insert(actor)
return self
end
-- Clear out memory
Actor.Expire = function(self,reason)
-- Check if the reason is supposed to be timeout, but the actor was
-- updated this cycle
if reason == "timeout" and self.LastCycle == RAIL.CycleID then
-- Don't actually expire; something just caused the script to lag hard
return
end
-- Log
RAIL.LogT(40,"Clearing history for {1} due to {2}.",self,reason)
-- Unset any per-actor battle options
for k,v in pairs(self.BattleOpts) do
self.BattleOpts[k] = nil
end
self.BattleOpts[battleopts_parent] = self
-- Clear the histories
History.Clear(self.Motion)
History.Clear(self.Target)
History.Clear(self.X)
History.Clear(self.Y)
-- Disable the timeout
self.ExpireTimeout[1] = false
-- Disable the active flag
self.Active = false
end
-------------
-- Support --
-------------
-- The following functions support other parts of the script
-- Check if the actor is an enemy (monster/pvp-player)
Actor.IsEnemy = {
["Actor"] = function(self)
-- Check if the actor ever had IsMonster() return 1
if not self.WasMonster then
-- Check if it's a monster
if IsMonster(self.ID) ~= 1 then
return false
end
-- Make sure this is counted as an enemy until AI resets
self.WasMonster = true
end
-- Check if it should be defended against only
if self.BattleOpts.DefendOnly then
-- Check if its target is owner, self, other, or a friend
local target = Actors[self.Target[0]]
if
target ~= RAIL.Owner and
target ~= RAIL.Self and
target ~= RAIL.Other and
not target:IsFriend()
then
-- Not attacking a friendly, so not an enemy
return false
end
end
-- Check if the monster is dead
if self.Motion[0] == MOTION_DEAD then
return false
end
-- Check if the monster is in a sane location
if self.X[0] == -1 or self.Y[0] == -1 then
return false
end
-- Default to true
return true
end,
["Player"] = function(self)
-- Check if the player is set as a friend
if self:IsFriend() then
-- Friends are never enemies
return false
end
-- Otherwise, act like a regular actor
return Actor.IsEnemy["Actor"](self)
end,
}
setmetatable(Actor.IsEnemy,{
__call = function(self,...)
-- Note: when calling actor:IsEnemy, self will be Actor.IsEnemy table and
-- arg[1] will be actor (true "self")
local true_self = arg[1]
-- Get the function based on the actor type
local f
if type(true_self) == "table" then
f = self[true_self.ActorType]
end
if not f then
f = self["Actor"]
end
-- Call the actual function
return f(unpack(arg))
end,
})
-- Check if the actor is a friend
Actor.IsFriend = {
["Actor"] = function(self,no_temp)
-- Non-players and non-AIs should never ocunt as friends
return false
end,
["Player"] = function(self,no_temp)
-- Check for temporary friends (players within <opt> range of owner)
if
not no_temp and
RAIL.Owner:DistanceTo(self) <= RAIL.State.TempFriendRange
then
return true
end
-- Check if the actor is set as a permanent friend
return self.BattleOpts.Friend
end,
["AI"] = function(self,no_temp)
-- If disallowing temporary friends, AIs should never count
if no_temp then
return false
end
-- Only friend if within a certain range
return RAIL.Owner:DistanceTo(self) <= RAIL.State.TempFriendRange
end,
}
-- IsEnemy and IsFriend function essentially the same way
setmetatable(Actor.IsFriend,getmetatable(Actor.IsEnemy))
-- Set actor as a friend
Actor.SetFriend = {
["Actor"] = function(self,bool)
-- Normal actors shouldn't be put on the permanent friend list
return
end,
["Player"] = function(self,bool)
-- Check if there is already a ByID field for this actor
if not RAIL.State.ActorOptions.ByID[self.ID] then
-- No table exists for this actor, create it
RAIL.State.ActorOptions.ByID[self.ID] = {
["Friend"] = bool,
}
else
-- Table exists, update the friend value
RAIL.State.ActorOptions.ByID[self.ID].Friend = bool
end
end,
}
-- SetFriend and IsFriend function the same way
setmetatable(Actor.SetFriend,getmetatable(Actor.IsFriend))
-- Check if the actor is ignored
Actor.IsIgnored = function(self)
return self.IgnoreTime > 0
end
-- Ignore the actor for a specific amount of time
Actor.Ignore = function(self,ticks)
-- Use default ticks if needed
if type(ticks) ~= "number" then
ticks = self.BattleOpts.DefaultIgnoreTicks
end
-- If it's already ignored, do nothing
if self:IsIgnored() then
-- Update the ignore time to whichever is higher
self.IgnoreTime = math.max(ticks,self.IgnoreTime)
return self
end
RAIL.LogT(20,"{1} ignored for {2} milliseconds.",self,ticks)
self.IgnoreTime = ticks
end
-- Estimate Movement Speed (in milliseconds per cell) and Direction
local estimate_key = {}
Actor.EstimateMove = function(self)
-- Ensure we have a place to store estimation data
if not self[estimate_key] then
self[estimate_key] = {
-- Default move-speed to regular walk
-- according to http://forums.roempire.com/archive/index.php/t-137959.html:
-- 0.15 sec per cell at regular speed
-- 0.11 sec per cell w/ agi up
-- 0.06 sec per cell w/ Lif's emergency avoid
--
-- Default move-direction to straight north; the same as the server seems to
speed = 150,
angle = 90,
-- Last time calculated was never
last = 0,
last_move = 0,
last_non_move = 0,
-- And the distance used to calculate speed was 1
-- Note: Greater or equal distances will recalculate,
-- to prevent infinite speeds (0 distance)
dist = 1,
}
end
-- Get the estimation data table
local estimate = self[estimate_key]
-- Don't estimate too often
if GetTick() - estimate.last <= 250 then
return estimate.speed, estimate.angle
end
-- Get the list of motions for the actor
local motion_list = History.GetConstList(self.Motion)
-- Loop from the most recent to the oldest
local move
local non_move
for i=motion_list.last,motion_list.first,-1 do
-- Check for a movement motion
if motion_list[i][1] == MOTION_MOVE then
-- Get the time of the motion start
move = motion_list[i][2]
-- Check if we're at the most recent motion
if i == motion_list.last then
-- Get the current time
non_move = GetTick()
else
-- Get the time of the more-recent motion start
non_move = motion_list[i+1][2]
end
-- Ensure that the motions are sufficiently far apart (in time or distance)
local move_delta = GetTick() - move
local non_move_d = GetTick() - non_move
if
non_move - move >= 100 or
BlockDistance(self.X[non_move_d],self.Y[non_move_d],self.X[move_delta],self.Y[non_move_d]) > 0
then
-- Use these values
break
end
-- Don't use these values
move = nil
non_move = nil
end
end
-- If no new moves were found, return from estimate
if
-- No move found
move == nil or
-- Move is same as last estimation
(move == estimate.last_move and non_move == estimate.last_non_move)
then
return estimate.speed, estimate.angle
end
-- Store the move and non_move times we're using
estimate.last_move = move
estimate.last_non_move = non_move
-- Get the X and Y position lists
local x_list = History.GetConstList(self.X)
local y_list = History.GetConstList(self.Y)
-- Find the position (in list) closest to non_move
local begin_x = x_list:BinarySearch(non_move)
local begin_y = y_list:BinarySearch(non_move)
-- Begin searching backward into history
local i_x,i_y = begin_x,begin_y
while true do
-- Get the X,Y coords
local x = x_list[i_x][1]
local y = y_list[i_y][1]
-- Check to see if we can search further back
local next_i_x = i_x
if i_x-1 >= x_list.first and x_list[i_x-1][2] >= move then
next_i_x = i_x - 1
end
local next_i_y = i_y
if i_y-1 >= y_list.first and y_list[i_y-1][2] >= move then
next_i_y = i_y - 1
end
-- Check if we've reached the back
if next_i_x == i_x and next_i_y == i_y then
break
end
-- Find the next changed point
if x_list[next_i_x][2] > y_list[next_i_y][2] and next_i_x ~= i_x then
next_i_y = i_y
elseif x_list[next_i_x][2] < y_list[next_i_y][2] and next_i_y ~= i_y then
next_i_x = i_x
end
-- Get the angle of the two adjacent points
local angle = GetAngle(
x_list[next_i_x][1],y_list[next_i_y][1],
x,y
)
-- Get the angle of the beginning to our current point
local angle2 = GetAngle(
x,y,
x_list[begin_x][1],y_list[begin_y][1]
)
-- If x,y are still at beginning position, move to next
if angle ~= -1 and angle2 ~= -1 then
-- Check to see if the angle is within 45 degrees either way
if not CompareAngle(angle,angle2+45,90) then
-- Doesn't match
break
end
-- Check if the distance is great enough
local blocks = BlockDistance(x,y,x_list[begin_x][1],y_list[begin_y][1])
if blocks >= 6 then
break
end
end
i_x = next_i_x
i_y = next_i_y
end
-- Get the beginning position and end position
-- Note: begin refers to the most recent, so it becomes x2,y2
local x2,y2 = x_list[begin_x][1],y_list[begin_y][1]
local x1,y1 = x_list[i_x][1],y_list[i_y][1]
-- Get the angle and the block distance
local angle = GetAngle(x1,y1,x2,y2)
local dist = BlockDistance(x1,y1,x2,y2)
-- Ensure we're still sane
-- Note: And don't repeat logs for the same angle
if angle ~= -1 and angle ~= estimate.angle then
-- Store our estimated angle
estimate.angle = angle
-- Log it
RAIL.LogT(80,"Movement angle for {1} estimated at {2} degrees.",
self,RoundNumber(estimate.angle))
end
-- Check if we've calculated a better speed than before
if dist >= estimate.dist then
-- Store our speed and distance
estimate.dist = dist
-- Get the tick delta
local tick_delta_x = x_list[begin_x][2] - x_list[i_x][2]
local tick_delta_y = y_list[begin_y][2] - y_list[i_y][2]
local tick_delta = math.max(tick_delta_x,tick_delta_y)
estimate.speed = tick_delta / dist
-- Store the time of last estimation
estimate.last = GetTick()
-- Log it
RAIL.LogT(80,"Movement speed for {1} estimated at {2}ms/tile (dist={3}).",
self,RoundNumber(estimate.speed),estimate.dist)
end
-- And return
return estimate.speed, estimate.angle
end
--------------------
-- Battle Options --
--------------------
-- RAIL allowed to attack monster?
Actor.IsAttackAllowed = function(self)
-- Determine if we are allowed to attack the monster
if not self:IsEnemy() then
return false
end
if not self.BattleOpts.AttackAllowed then
return false
end
local last_attack = self.BattleOpts.LastAttack or 0
if GetTick() < last_attack + self.BattleOpts.TicksBetweenAttack then
return false
end
return true
end
-- RAIL allowed to cast against monster?
Actor.IsSkillAllowed = function(self,level)
-- Check if skills are allowed
if not self:IsEnemy() or not self.BattleOpts.SkillsAllowed then
return false
end
-- Check that the skill level is high enough
if level < self.BattleOpts.MinSkillLevel then
return false
end
-- Check if we've reached max cast count
if
(self.BattleOpts.CastsAgainst or 0) >= self.BattleOpts.MaxCastsAgainst and
self.BattleOpts.MaxCastsAgainst >= 0
then
return false
end
-- Check if we should wait before casting against this actor
if RAIL.SkillState:CompletedTime() + self.BattleOpts.TicksBetweenSkills > GetTick() then
return false
end
-- Skills are allowed (and hint at the max skill level)
return true,math.min(self.BattleOpts.MaxSkillLevel,level)
end
-- Determine if attacking this actor would be kill-stealing
Actor.WouldKillSteal = function(self)
-- Free-for-all monsters are never kill-stealed
if self.BattleOpts.FreeForAll then
return false
end
-- Check if it's an enemy
if not self:IsEnemy() then
return false
end
-- Check if this actor is targeting anything
local targ = self.Target[0]
if targ ~= -1 then
-- Owner and self don't count
if targ == RAIL.Self.ID or targ == RAIL.Owner.ID then
return false
end
local targ = Actors[targ]
-- Can't kill steal friends
if targ:IsFriend() then
return false
end
-- Determine if it's not targeting another enemy
if not targ:IsEnemy() then
-- Determine if the target has been updated recently
if targ.Active then
-- It would be kill stealing