forked from Benjamin-Dobell/ge_tts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathZone.ttslua
598 lines (484 loc) · 22.9 KB
/
Zone.ttslua
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
local EventManager = require('ge_tts.EventManager')
local Grid = require('ge_tts.Grid')
local Instance = require('ge_tts.Instance')
local Logger = require('ge_tts.Logger')
local Object = require('ge_tts.Object')
local ObjectUtils = require('ge_tts.ObjectUtils')
local TableUtils = require('ge_tts.TableUtils')
local Vector3 = require('ge_tts.Vector3')
---@type table<tts__ScriptingTrigger, ge_tts__Zone>
local scriptingTriggerZoneMap = {}
---@type table<tts__Object, ge_tts__Zone[]>
local objectIntersectingZonesMap = {}
---@type table<tts__Object, ge_tts__Zone>
local objectOccupyingZoneMap = {}
---@param object tts__Object
---@param zone ge_tts__Zone
local function addObjectIntersectingZone(object, zone)
local objectZones = objectIntersectingZonesMap[object]
if objectZones and not TableUtils.find(objectZones, zone) then
table.insert(objectZones, zone)
else
objectIntersectingZonesMap[object] = { zone }
end
end
local function removeObjectIntersectingZone(object, zone)
local objectZones = objectIntersectingZonesMap[object]
if objectZones then
local zoneIndex = TableUtils.find(objectZones, zone)
if zoneIndex then
table.remove(objectZones, --[[---@not nil]] zoneIndex)
if #objectZones == 0 then
objectIntersectingZonesMap[object] = nil
end
end
end
end
---@param zone ge_tts__Zone
---@param object tts__Object
local function associateOccupyingObject(zone, object)
local previousOccupiedZone = objectOccupyingZoneMap[object]
if previousOccupiedZone then
previousOccupiedZone.onLeave(object)
end
objectOccupyingZoneMap[object] = zone
for _, instance in ipairs(Instance.getInstances(object)) do
instance.setZone(zone)
end
end
---@overload fun(zone: ge_tts__Zone, object: tts__Object): void
---@param zone ge_tts__Zone
---@param object tts__Object
---@param disassociateInstances nil | boolean
local function disassociateOccupyingObject(zone, object, disassociateInstances)
if objectOccupyingZoneMap[object] == zone then
objectOccupyingZoneMap[object] = nil
end
-- Note that we're intentionally not disassociating instances by default. Instances track the *most recent* zone.
if disassociateInstances ~= nil and disassociateInstances then
for _, instance in ipairs(Instance.getInstances(object)) do
if instance.getZone() == zone then
instance.setZone(nil)
end
end
end
end
---@param position ge_tts__Vector3
---@param rotation ge_tts__Vector3
---@param scale ge_tts__Vector3
---@param callback fun(object: tts__ScriptingTrigger): void @TTS object spawned callback
---@return tts__ScriptingTrigger
local function spawn(position, rotation, scale, callback)
local jsonTable = {
Name = Object.Name.ScriptingTrigger,
Transform = {
posX = position.x,
posY = position.y,
posZ = position.z,
rotX = rotation.x,
rotY = rotation.y,
rotZ = rotation.z,
scaleX = scale.x,
scaleY = scale.y,
scaleZ = scale.z,
},
Locked = true,
}
return --[[---@type tts__ScriptingTrigger]] ObjectUtils.safeSpawnObject(jsonTable, --[[---@type fun(o: tts__Object): void]] callback)
end
---@class ge_tts__Zone : ge_tts__Instance
---@field getObject fun(): tts__ScriptingTrigger
---@shape ge_tts__Zone_SavedState : ge_tts__Instance_SavedState
---@field occupyingObjectGuids string[]
---@class ge_tts__static_Zone : ge_tts__static_Instance
---@overload fun(position: tts__VectorShape, rotation: tts__VectorShape, scale: tts__VectorShape): ge_tts__Zone
---@overload fun(scriptingTrigger: tts__ScriptingTrigger): ge_tts__Zone
---@overload fun(savedState: ge_tts__Zone_SavedState): ge_tts__Zone
---@overload fun(positionOrTriggerOrSavedState: tts__VectorShape | tts__ScriptingTrigger | ge_tts__Zone_SavedState, nilOrRotation: nil | tts__VectorShape, nilOrScale: nil | tts__VectorShape): ge_tts__Zone
local Zone = {}
Zone.TYPE = 'Zone'
---@alias ge_tts__Zone_FilterResult 0 | 1 | 2 | 3
Zone.FilterResult = {
-- The object being dropped will simply be ignored. Depending on the object's trajectory it may
-- end up settling within the drop zone, but it won't be considered as "occupying" the zone.
--
-- If an object is dropped whilst intersecting multiple zones, the zone with the closest origin
-- to the object's origin is tried first. If the object is ignored, then the next closest zone
-- will be tried.
IGNORE = 0,
-- The object will proceed to be dropped in zone and will be deemed "occupying" the zone.
ACCEPT = 1,
-- If the object has an associated Instance, it'll be returned to the instance's zone. If not
-- and the object has a non-zero pick_up_position, it will be returned there. Otherwise, the
-- object will continue on its current trajectory and may end in the zone, but won't be
-- considered "occupying".
REJECT = 2,
-- Will reject the object immediately without giving other intersecting zones an opportunity to
-- handle the object.
FORCE_REJECT = 3,
}
setmetatable(Zone, TableUtils.merge(getmetatable(Instance), {
---@param positionOrTriggerOrSavedState tts__VectorShape | tts__ScriptingTrigger | ge_tts__Zone_SavedState
---@param nilOrRotation nil | tts__VectorShape
---@param nilOrScale nil | tts__VectorShape
__call = function(_, positionOrTriggerOrSavedState, nilOrRotation, nilOrScale)
local isSavedState = Zone.isSavedState(positionOrTriggerOrSavedState)
---@type ge_tts__Zone
local self
if isSavedState then
self = --[[---@type ge_tts__Zone]] Instance(--[[---@type ge_tts__Zone_SavedState]] positionOrTriggerOrSavedState)
else
---@type tts__ScriptingTrigger
local scriptingTrigger
if type(positionOrTriggerOrSavedState) == 'table' then
local position = Vector3(--[[---@type tts__VectorShape]] positionOrTriggerOrSavedState)
local rotation = Vector3(--[[---@not nil]] nilOrRotation)
local scale = Vector3(--[[---@not nil]] nilOrScale)
scriptingTrigger = spawn(position, rotation, scale, function()
self.onSpawned()
end)
elseif type(positionOrTriggerOrSavedState) == 'userdata' then
scriptingTrigger = --[[---@type tts__ScriptingTrigger]] positionOrTriggerOrSavedState
end
self = --[[---@type ge_tts__Zone]] Instance(scriptingTrigger)
end
---@type tts__Object[]
local occupyingObjects = {}
---An identifier for this type of zone.
---
---This is convenience method. Aside from logging, ge_tts does not use this method. However, it may be useful
---for your game to be able to discern one type of zone from another.
---
---Subclasses may either set TYPE on the class (static) table, or alternatively, override this method.
---@return string
function self.getType()
return Zone.TYPE
end
-- Callback for sub-classes to override.
function self.onSpawned()
end
---@return ge_tts__Vector3
function self.getPosition()
return Vector3(self.getObject().getPosition())
end
---@return ge_tts__Vector3
function self.getRotation()
return Vector3(self.getObject().getRotation())
end
---@return ge_tts__Vector3
function self.getScale()
return Vector3(self.getObject().getScale())
end
---@return tts__Object[] @TTS objects that have been dropped in the zone
function self.getOccupyingObjects()
return TableUtils.copy(occupyingObjects)
end
---@param object tts__Object
---@return boolean
function self.isObjectOccupying(object)
return TableUtils.find(occupyingObjects, object) ~= nil
end
--- Called when a player attempts to drop an object within this zone. The return value
--- indicates whether the zone wishes to accept, reject or ignore the object being dropped.
---@param colorName tts__PlayerColor @Color of the TTS player that dropped the TTS object.
---@param object tts__Object
---@return ge_tts__Zone_FilterResult
function self.filterObject(colorName, object)
return Zone.FilterResult.ACCEPT
end
--- Called when a TTS object enters this Zone.
---@param object tts__Object
function self.onEnter(object)
end
--- Called when a TTS object leaves this Zone.
---@param object tts__Object
function self.onLeave(object)
local index = TableUtils.find(occupyingObjects, object)
if index then
table.remove(occupyingObjects, --[[---@not nil]] index)
end
disassociateOccupyingObject(self, object)
self.invalidateSavedState()
end
--- Called when a TTS object is dropped within this Zone.
---@param colorName nil | tts__PlayerColor @Color of the TTS player that dropped the TTS object.
---@param object tts__Object @The object that was dropped.
function self.onDrop(colorName, object)
if not TableUtils.find(occupyingObjects, object) then
table.insert(occupyingObjects, object)
end
associateOccupyingObject(self, object)
self.invalidateSavedState()
end
--- Called when a TTS object is picked up from this Zone.
---@param colorName string @Color of the TTS player that dropped the TTS object.
---@param object tts__Object @The object that was picked up.
function self.onPickUp(colorName, object)
local index = TableUtils.find(occupyingObjects, object)
if index then
table.remove(occupyingObjects, --[[---@not nil]] index)
end
disassociateOccupyingObject(self, object)
self.invalidateSavedState()
end
--- Used programmatically when `object` should be made a direct occupant, but not dropped by a player.
---@param object tts__Object @The object that was dropped.
function self.insertOccupyingObject(object)
if not TableUtils.find(occupyingObjects, object) then
table.insert(occupyingObjects, object)
end
associateOccupyingObject(self, object)
self.invalidateSavedState()
end
--- Can be called to dynamically drop a TTS object in this Zone.
---@param colorName nil | tts__PlayerColor @Color of the TTS player that should be deemed responsible for having dropped the TTS object.
---@param object tts__Object @The object that will be dropped.
function self.drop(colorName, object)
self.onDrop(colorName, object)
end
local superSave = self.save
---@return ge_tts__Zone_SavedState
function self.save()
return --[[---@type ge_tts__Zone_SavedState]] TableUtils.merge(superSave(), {
occupyingObjectGuids = TableUtils.map(occupyingObjects, function(object) return object.getGUID() end)
})
end
local superDestroy = self.destroy
function self.destroy()
if self.isDestroyed() then
return
end
scriptingTriggerZoneMap[self.getObject()] = nil
for _, object in ipairs(occupyingObjects) do
disassociateOccupyingObject(self, object, true)
end
for object, _ in pairs(objectIntersectingZonesMap) do
removeObjectIntersectingZone(object, self)
end
occupyingObjects = {}
superDestroy()
end
scriptingTriggerZoneMap[self.getObject()] = self
if Zone.isSavedState(positionOrTriggerOrSavedState) then
local data = --[[---@type ge_tts__Zone_SavedState]] positionOrTriggerOrSavedState
occupyingObjects = TableUtils.map(data.occupyingObjectGuids, function(guid)
local object = --[[---@not nil]] ObjectUtils.getSpawnedObject(guid)
associateOccupyingObject(self, object)
return object
end)
end
for _, object in ipairs(self.getObject().getObjects()) do
addObjectIntersectingZone(object, self)
end
return self
end,
__index = Instance,
}))
---Returns a list of Zones that `object` is inside.
---Returned Zones are zones that the `object` is presently inside of, and *not* strictly zones in which `object` has
---been dropped, it may still be held by a player, or simply passing through these zones as a result of smooth movement.
---@param object tts__Object
---@return ge_tts__Zone[]
function Zone.getObjectIntersectingZones(object)
return objectIntersectingZonesMap[object] or {}
end
---Returns the Zone that `object` is occupying, or nil if `object` is not occupying a zone.
---@param object tts__Object
---@return nil | ge_tts__Zone
function Zone.getObjectOccupyingZone(object)
return objectOccupyingZoneMap[object]
end
---Returns the Zone associated with a scripting trigger, or nil if the scripting trigger does not belong to a Zone.
---@param scriptingTrigger tts__ScriptingTrigger
---@return nil | ge_tts__Zone
function Zone.getScriptingTriggerZone(scriptingTrigger)
return scriptingTriggerZoneMap[scriptingTrigger]
end
---@param container tts__Container
---@param object tts__Object
local function onObjectEnterContainer(container, object)
for _, zone in ipairs(Zone.getObjectIntersectingZones(object)) do
if zone.isObjectOccupying(object) and not TableUtils.find(zone.getOccupyingObjects(), container) then
Logger.log(
object.tag .. ' (' .. tostring(object.getGUID()) .. '), previously dropped in ' .. tostring(zone) .. ', entered ' ..
container.tag .. ' (' .. tostring(container.getGUID()) .. ') which will now be marked as dropped in the same zone.',
Logger.DEBUG
)
zone.insertOccupyingObject(container)
break
end
end
end
---@param container tts__Container
---@param object tts__Object
local function onObjectLeaveContainer(container, object)
local objectPosition = object.getPosition()
local containerPosition = container.getPosition()
if container.tag ~= Object.Tag.Infinite and container.getQuantity() == 0
and objectPosition.y <= containerPosition.y
and objectPosition.x == containerPosition.x
and objectPosition.z == containerPosition.z
then
local zone = objectOccupyingZoneMap[container]
if zone then
if not TableUtils.find(zone.getOccupyingObjects(), object) then
Logger.log(
object.tag .. ' (' .. tostring(object.getGUID()) .. ') is now dropped in ' .. tostring(zone) .. ' as it was the bottom ' ..
object.tag .. ' in now empty ' .. container.tag .. ' (' .. tostring(container.getGUID()) .. ').',
Logger.DEBUG
)
zone.insertOccupyingObject(object)
end
end
end
end
EventManager.addHandler('onObjectEnterContainer', onObjectEnterContainer)
EventManager.addHandler('onObjectLeaveContainer', onObjectLeaveContainer)
---@param colorName tts__PlayerColor
---@param object tts__Object
local function onObjectPickUp(colorName, object)
local objectZones = objectIntersectingZonesMap[object]
if objectZones then
for _, zone in ipairs(objectZones) do
if zone.isObjectOccupying(object) then
Logger.log(object.tag .. ' (' .. tostring(object.getGUID()) .. ') picked up from ' .. tostring(zone), Logger.DEBUG)
zone.onPickUp(colorName, object)
break
end
end
end
end
---@param colorName tts__PlayerColor
---@param object tts__Object
local function onObjectDrop(colorName, object)
---@type ge_tts__Zone[]
local objectZones
-- TODO: Once TTS provides an object.GetSmoothMovementTarget() or similar we can do away with this craziness...
-- When an object is dropped, TTS may smooth move it to another location in certain circumstances. Generally
-- speaking there are three cases:
-- 1. Snapping to a grid.
-- 2. Snapping to a snap point.
-- 3. The user pressed Esc, and the object is moving back to its pick_up_position.
-- We're going to assume that ge_tts users are using DropZones and not snap points and outright ignore case 2, thus
-- if grid snapping isn't enabled, we'll assume case 3 is taking place.
local cancelling = object.isSmoothMoving() and (Grid.snapping == Grid.Snapping.None or not object.use_grid) and object.pick_up_position:sqrMagnitude() ~= 0
if cancelling then
objectZones = TableUtils.values(TableUtils.select(scriptingTriggerZoneMap, function(zone, trigger)
local localPosition = trigger.positionToLocal(object.pick_up_position)
local scale = trigger.getScale()
return math.abs(localPosition.x) < 0.5 and math.abs(localPosition.z) < 0.5 and math.abs(localPosition.y) < scale.y and localPosition.y > 0
end))
else
objectZones = objectIntersectingZonesMap[object]
end
if objectZones then
local objectPosition = cancelling and object.pick_up_position or object.getPosition()
local zoneDistances = TableUtils.map(objectZones, function(zone)
local distanceSquared = Vector3.distanceSquared(objectPosition, zone.getObject().getPosition())
return {distanceSquared = distanceSquared, zone = zone}
end)
table.sort(zoneDistances, function(zoneDistance1, zoneDistance2)
return zoneDistance1.distanceSquared < zoneDistance2.distanceSquared
end)
local reject = false
for _, zoneDistance in ipairs(zoneDistances) do
local zone = zoneDistance.zone
Logger.log(object.tag .. ' (' .. tostring(object.getGUID()) .. ') attempting to drop in ' .. tostring(zone), Logger.DEBUG)
local filterResult = zone.filterObject(colorName, object)
if filterResult == Zone.FilterResult.ACCEPT then
Logger.log(object.tag .. ' (' .. tostring(object.getGUID()) .. ') dropped in ' .. tostring(zone), Logger.DEBUG)
zone.onDrop(colorName, object)
reject = false
break
elseif filterResult == Zone.FilterResult.REJECT then
Logger.log(object.tag .. ' (' .. tostring(object.getGUID()) .. ') rejected by ' .. tostring(zone), Logger.DEBUG)
reject = true
elseif filterResult == Zone.FilterResult.FORCE_REJECT then
Logger.log(object.tag .. ' (' .. tostring(object.getGUID()) .. ') force rejected by ' .. tostring(zone), Logger.DEBUG)
reject = true
break
end
end
if reject then
-- We only look at one instance (if there is one). If there's multiple because the
-- object is a container, we make no attempt to split the container.
local instance = Instance.getOneInstance(object)
local instanceZone = instance and (--[[---@not nil]] instance).getZone()
if instanceZone and
(TableUtils.find((--[[---@not nil]] instanceZone).getOccupyingObjects(), object)
or (--[[---@not nil]] instanceZone).filterObject(colorName, object) == Zone.FilterResult.ACCEPT)
then
(--[[---@not nil]] instanceZone).drop(colorName, object)
elseif object.pick_up_position:sqrMagnitude() ~= 0 then
object.setPositionSmooth(object.pick_up_position)
object.setRotationSmooth(object.pick_up_rotation)
end
end
end
end
EventManager.addHandler('onObjectPickUp', onObjectPickUp)
EventManager.addHandler('onObjectDrop', onObjectDrop)
---@param scriptingTrigger tts__ScriptingTrigger
---@param object tts__Object
local function onObjectEnterScriptingZone(scriptingTrigger, object)
local zone = scriptingTriggerZoneMap[scriptingTrigger]
if zone then
Logger.log(object.tag .. ' (' .. tostring(object.getGUID()) .. ') entered ' .. tostring(zone), Logger.DEBUG)
addObjectIntersectingZone(object, zone)
zone.onEnter(object)
-- Here we attempt to detect objects that were pulled out of a container and dropped straight in a scripting zone using the UI.
if object.spawning and not object.isSmoothMoving() and not object.held_by_color and not objectOccupyingZoneMap[object] then
local players = Player.getPlayers()
local objectPosition = object.getPosition()
---@type tts__PlayerColor
local closestPlayerColor
local closestPlayerDistanceSq = math.huge
for _, player in ipairs(players) do
local pointerPosition = player.getPointerPosition()
local distanceSq = Vector3.distanceSquared(objectPosition, pointerPosition)
if distanceSq < closestPlayerDistanceSq then
closestPlayerColor = player.color
closestPlayerDistanceSq = distanceSq
end
end
onObjectDrop(closestPlayerColor, object)
end
end
end
---@param scriptingTrigger tts__ScriptingTrigger
---@param object tts__Object
local function onObjectLeaveScriptingZone(scriptingTrigger, object)
local zone = scriptingTriggerZoneMap[scriptingTrigger]
if zone then
Logger.log(object.tag .. ' (' .. tostring(object.getGUID()) .. ') left ' .. tostring(zone), Logger.DEBUG)
removeObjectIntersectingZone(object, zone)
zone.onLeave(object)
end
end
EventManager.addHandler('onObjectEnterScriptingZone', onObjectEnterScriptingZone)
EventManager.addHandler('onObjectLeaveScriptingZone', onObjectLeaveScriptingZone)
---@param object tts__Object
local function onObjectDestroy(object)
local zones = objectIntersectingZonesMap[object]
if zones then
for _, zone in ipairs(zones) do
Logger.log(
object.tag .. ' (' .. tostring(object.getGUID()) .. ') removed from intersecting ' .. tostring(zone) .. ' as it\'s being destroyed',
Logger.DEBUG
)
zone.onLeave(object)
end
objectIntersectingZonesMap[object] = nil
end
-- When objects are programmatically dropped, they may occupy a zone they're not yet intersecting.
local occupiedZone = objectOccupyingZoneMap[object]
if occupiedZone then
Logger.log(
object.tag .. ' (' .. tostring(object.getGUID()) .. ') removed from occupying ' .. tostring(occupiedZone) .. ' as it\'s being destroyed',
Logger.DEBUG
)
occupiedZone.onLeave(object)
end
end
EventManager.addHandler('onObjectDestroy', onObjectDestroy)
return Zone