-
Notifications
You must be signed in to change notification settings - Fork 0
/
custom_entities.lua
1864 lines (1722 loc) · 77.2 KB
/
custom_entities.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
meta = {
name = "Custom-Entities-Library",
version = "1.1a",
author = "Estebanfer",
description = "A library for creating custom entities"
}
local FLAGS_BIT = { --https://github.com/Mr-Auto/spelunky2-lua-libs/blob/main/libraries/flags/flags.lua
0x1,
0x2,
0x4,
0x8,
0x10,
0x20,
0x40,
0x80,
0x100,
0x200,
0x400,
0x800,
0x1000,
0x2000,
0x4000,
0x8000,
0x10000,
0x20000,
0x40000,
0x80000,
0x100000,
0x200000,
0x400000,
0x800000,
0x1000000,
0x2000000,
0x4000000,
0x8000000,
0x10000000,
0x20000000,
0x40000000,
0x80000000,
}
local module = {}
---@class CustomEntityType
---@field set fun(ent: Entity, c_data: table, args: any): table | nil
---@field update_callback fun(ent: Entity, c_data: table): nil
---@field update fun(ent: Entity, c_data: table, c_type: table, c_type_id: integer): nil
---@field carry_type integer
---@field ent_type ENT_TYPE
---@field update_type integer
---@field entities table
---@field after_destroy_callback nil | function
---@field custom_powerup_id nil | integer
---@field custom_pickup_id nil | integer
---@field pickup_callback nil | function
---@field entity_name nil | string
---@field texture_id nil | integer
---@field anim_frame nil | integer
---@field price nil | integer
---@field price_inflation nil | integer
---@type CustomEntityType[]
local custom_types = {}
local cb_update, cb_pre_load, cb_pre_level_gen, cb_post_level_gen, cb_clonegunshot = -1, -1, -1, -1, -1, -1
local didnt_init = true
---@class TransitionInfo
---@field custom_type_id integer
---@field data table
---@class PlayerTransitionInfo : TransitionInfo
---@field slot integer
---@field carry_type integer
---@type PlayerTransitionInfo[]
local custom_entities_t_info = {} --transition info
---@class HHTransitionInfo : TransitionInfo
---@field e_type integer
---@field hh_num integer
---@field leader_player_slot integer
---@type HHTransitionInfo[]
local custom_entities_t_info_hh = {}
---@type TransitionInfo[]
local custom_entities_t_info_storage = {}
---@class COG_DuatTransitionInfo : TransitionInfo
---@field slot integer
---For transition of powerups
---@type COG_DuatTransitionInfo[]
local custom_entities_t_info_cog_ankh = {}
local storage_pos = nil
---@enum ARENA_ITEM_SETTING
local ARENA_ITEM_SETTING = {
DISABLED = 1,
START_WITH = 2,
CRATE = 3,
CRATE_START_WITH = 4,
}
---@enum CARRY_TYPE
local CARRY_TYPE = {
HELD = 1,
MOUNT = 2,
BACK = 3,
POWERUP = 4
}
local function has(arr, item)
for _, v in ipairs(arr) do
if v == item then
return true
end
end
return false
end
local function join(a, b)
local result = {table.unpack(a)}
table.move(b, 1, #b, #result + 1, result)
return result
end
local function clone_chances(tabl)
return {
common = {table.unpack(tabl.common)},
low = {table.unpack(tabl.low)},
lower = {table.unpack(tabl.lower)}
}
end
local all_shop_ents = {ENT_TYPE.ITEM_PICKUP_ROPEPILE, ENT_TYPE.ITEM_PICKUP_BOMBBAG, ENT_TYPE.ITEM_PICKUP_BOMBBOX, ENT_TYPE.ITEM_PICKUP_PARACHUTE, ENT_TYPE.ITEM_PICKUP_SPECTACLES, ENT_TYPE.ITEM_PICKUP_SKELETON_KEY, ENT_TYPE.ITEM_PICKUP_COMPASS, ENT_TYPE.ITEM_PICKUP_SPRINGSHOES, ENT_TYPE.ITEM_PICKUP_SPIKESHOES, ENT_TYPE.ITEM_PICKUP_PASTE, ENT_TYPE.ITEM_PICKUP_PITCHERSMITT, ENT_TYPE.ITEM_PICKUP_CLIMBINGGLOVES, ENT_TYPE.ITEM_WEBGUN, ENT_TYPE.ITEM_MACHETE, ENT_TYPE.ITEM_BOOMERANG, ENT_TYPE.ITEM_CAMERA, ENT_TYPE.ITEM_MATTOCK, ENT_TYPE.ITEM_TELEPORTER, ENT_TYPE.ITEM_FREEZERAY, ENT_TYPE.ITEM_METAL_SHIELD, ENT_TYPE.ITEM_PURCHASABLE_CAPE, ENT_TYPE.ITEM_PURCHASABLE_HOVERPACK, ENT_TYPE.ITEM_PURCHASABLE_TELEPORTER_BACKPACK, ENT_TYPE.ITEM_PURCHASABLE_POWERPACK, ENT_TYPE.ITEM_PURCHASABLE_JETPACK, ENT_TYPE.ITEM_PRESENT, ENT_TYPE.ITEM_PICKUP_HEDJET, ENT_TYPE.ITEM_PICKUP_ROYALJELLY, ENT_TYPE.ITEM_ROCK, ENT_TYPE.ITEM_SKULL, ENT_TYPE.ITEM_POT, ENT_TYPE.ITEM_WOODEN_ARROW, ENT_TYPE.ITEM_PICKUP_COOKEDTURKEY, ENT_TYPE.ITEM_SHOTGUN, ENT_TYPE.ITEM_PLASMACANNON, ENT_TYPE.ITEM_FREEZERAY, ENT_TYPE.ITEM_WEBGUN, ENT_TYPE.ITEM_CROSSBOW}
local normal_shop_rooms = {ROOM_TEMPLATE.SHOP, ROOM_TEMPLATE.SHOP_LEFT, ROOM_TEMPLATE.SHOP_ENTRANCE_UP, ROOM_TEMPLATE.SHOP_ENTRANCE_UP_LEFT, ROOM_TEMPLATE.SHOP_ENTRANCE_DOWN, ROOM_TEMPLATE.SHOP_ENTRANCE_DOWN_LEFT, ROOM_TEMPLATE.CURIOSHOP, ROOM_TEMPLATE.CURIOSHOP_LEFT, ROOM_TEMPLATE.CAVEMANSHOP, ROOM_TEMPLATE.CAVEMANSHOP_LEFT, ROOM_TEMPLATE.GHISTSHOP_BACKLAYER}
local DICESHOP_ITEMS = {ENT_TYPE.ITEM_PICKUP_BOMBBAG, ENT_TYPE.ITEM_PICKUP_BOMBBOX, ENT_TYPE.ITEM_PICKUP_ROPEPILE, ENT_TYPE.ITEM_PICKUP_COMPASS, ENT_TYPE.ITEM_PICKUP_PASTE, ENT_TYPE.ITEM_PICKUP_PARACHUTE, ENT_TYPE.ITEM_PURCHASABLE_CAPE, ENT_TYPE.ITEM_PICKUP_SPECTACLES, ENT_TYPE.ITEM_PICKUP_CLIMBINGGLOVES, ENT_TYPE.ITEM_PICKUP_PITCHERSMITT, ENT_TYPE.ITEM_PICKUP_SPIKESHOES, ENT_TYPE.ITEM_PICKUP_SPRINGSHOES, ENT_TYPE.ITEM_MACHETE, ENT_TYPE.ITEM_BOOMERANG, ENT_TYPE.ITEM_CROSSBOW, ENT_TYPE.ITEM_SHOTGUN, ENT_TYPE.ITEM_FREEZERAY, ENT_TYPE.ITEM_WEBGUN, ENT_TYPE.ITEM_CAMERA, ENT_TYPE.ITEM_MATTOCK, ENT_TYPE.ITEM_PURCHASABLE_JETPACK, ENT_TYPE.ITEM_PURCHASABLE_HOVERPACK, ENT_TYPE.ITEM_TELEPORTER, ENT_TYPE.ITEM_PURCHASABLE_TELEPORTER_BACKPACK, ENT_TYPE.ITEM_PURCHASABLE_POWERPACK}
local function new_chances()
return {
common = {},
low = {},
lower = {}
}
end
local custom_types_shop = {} --SHOP_TYPE
for i = 0, 13 do
custom_types_shop[i] = new_chances()
end
local custom_types_diceshop = new_chances()
local custom_types_tuskdiceshop = new_chances()
local custom_shop_items_set = false --if the set_pre_entity_spawn for custom shop items was already set
local shops_by_room_pos = {}
local custom_types_container = {
[ENT_TYPE.ITEM_CRATE] = new_chances(),
[ENT_TYPE.ITEM_PRESENT] = new_chances(),
[ENT_TYPE.ITEM_GHIST_PRESENT] = new_chances()
}
module.ALL_CONTAINERS = {
ENT_TYPE.ITEM_CRATE,
ENT_TYPE.ITEM_PRESENT,
ENT_TYPE.ITEM_GHIST_PRESENT
}
---@type table<integer, ARENA_ITEM_SETTING>
local custom_dm_item_settings = {}
--- Used to check if the number of vanilla items on dm crates is actually zero
--- but was changed by this lib to allows crates to spawn custom items
local no_dm_vanilla_items = false
local arena_customization_options_set = false
local custom_container_items_set = false
local custom_dm_container_items_set = false
local custom_container_item_spawns_set = false
local nonflammable_backs_callbacks_set = false
local item_draw_callbacks_set = false
local entity_crust_callbacks_set = false
local clonegunshot_custom_id = -1
local just_burnt, last_burn = 0, 0 --for non_flammable backpacks
--chance type
module.CHANCE = {
COMMON = "common",
LOW = "low",
LOWER = "lower"
}
--SHOP_TYPE
local SHOP_ROOM_TYPES = {
GENERAL_STORE = 0,
CLOTHING_SHOP = 1,
WEAPON_SHOP = 2,
SPECIALTY_SHOP = 3,
HIRED_HAND_SHOP = 4,
PET_SHOP = 5,
HEDJET_SHOP = 8,
TUN = 9,
CAVEMAN = 10,
TURKEY_SHOP = 11,
GHIST_SHOP = 12,
DICESHOP = ROOM_TEMPLATE.DICESHOP, --75
TUSKDICESHOP = ROOM_TEMPLATE.TUSKDICESHOP,
}
---All common shops (not shops like ghist or hedjet shop)
module.ALL_SHOPS = {SHOP_ROOM_TYPES.GENERAL_STORE, SHOP_ROOM_TYPES.CLOTHING_SHOP, SHOP_ROOM_TYPES.WEAPON_SHOP, SHOP_ROOM_TYPES.SPECIALTY_SHOP, SHOP_ROOM_TYPES.HIRED_HAND_SHOP, SHOP_ROOM_TYPES.PET_SHOP, SHOP_ROOM_TYPES.DICESHOP, SHOP_ROOM_TYPES.TUSKDICESHOP, SHOP_ROOM_TYPES.TUN, SHOP_ROOM_TYPES.CAVEMAN, SHOP_ROOM_TYPES.TURKEY_SHOP}
local weapon_info = {
[ENT_TYPE.ITEM_SHOTGUN] = {
bullet = ENT_TYPE.ITEM_BULLET,
bullet_off_y = 0.099998474121094,
sound = VANILLA_SOUND.ITEMS_SHOTGUN_FIRE,
shots = 0,
callb_set = false,
sound_callb_set = false
},
[ENT_TYPE.ITEM_FREEZERAY] = {
bullet = ENT_TYPE.ITEM_FREEZERAYSHOT,
bullet_off_y = 0.12000274658203,
sound = VANILLA_SOUND.ITEMS_FREEZE_RAY,
shots = 0,
callb_set = false,
sound_callb_set = false
},
[ENT_TYPE.ITEM_PLASMACANNON] = {
bullet = ENT_TYPE.ITEM_PLASMACANNON_SHOT,
bullet_off_y = 0.0,
sound = VANILLA_SOUND.ITEMS_PLASMA_CANNON,
shots = 0,
callb_set = false,
sound_callb_set = false
},
[ENT_TYPE.ITEM_CLONEGUN] = {
bullet = ENT_TYPE.ITEM_CLONEGUNSHOT,
bullet_off_y = 0.12000274658203,
sound = VANILLA_SOUND.ITEMS_CLONE_GUN,
shots = 0,
callb_set = false,
sound_callb_set = false
},
}
module.UPDATE_TYPE = {
FRAME = 0,
POST_STATEMACHINE = 1,
PRE_STATEMACHINE = 2
}
local function unset_custom_entity(uid, c_data, custom_type)
if c_data._lib_callbacks then
for _, callback in pairs(c_data._lib_callbacks) do
clear_entity_callback(uid, callback)
end
end
custom_type.entities[uid] = nil
end
local function _set_custom_entity(uid, ent, custom_type_id, c_data, optional_args)
local custom_type = custom_types[custom_type_id]
c_data = custom_type.set(ent, c_data, custom_type_id, optional_args)
if not c_data then
c_data = {}
end
if custom_type.update_type ~= module.UPDATE_TYPE.FRAME then
if not c_data._lib_callbacks then
c_data._lib_callbacks = {}
end
if custom_type.update_type == module.UPDATE_TYPE.POST_STATEMACHINE then
c_data._lib_callbacks[#c_data._lib_callbacks+1] = set_post_statemachine(uid, custom_type.update)
else
c_data._lib_callbacks[#c_data._lib_callbacks+1] = set_pre_statemachine(uid, custom_type.update)
end
set_on_kill(uid, function()
if not entity_has_item_type(ent.uid, ENT_TYPE.ITEM_POWERUP_ANKH) then
unset_custom_entity(uid, c_data, custom_type)
if custom_type.after_destroy_callback then
custom_type.after_destroy_callback(c_data, uid)
end
end
end)
end
custom_type.entities[uid] = c_data
end
local function set_transition_info(c_type_id, data, slot, carry_type)
table.insert(custom_entities_t_info,
{
custom_type_id = c_type_id,
data = data,
slot = slot,
carry_type = carry_type
})
end
local function get_hh_number(char_uid, hh_info_cache)
---@type Player
local char = get_entity(char_uid)
if char.inventory.player_slot == -1 then
if hh_info_cache[char] then
local char_num, player_slot = table.unpack(hh_info_cache[char])
return char_num + 1, player_slot
end
local char_num, player_slot = get_hh_number(char.linked_companion_parent, hh_info_cache)
char_num = char_num + 1
hh_info_cache[char_uid] = {char_num, player_slot}
return char_num, player_slot
else --is a player
return 0, char.inventory.player_slot
end
end
local function set_transition_info_hh(c_type_id, data, e_type, hh_uid, hh_info_cache)
local hh_num, leader_player_slot = get_hh_number(hh_uid, hh_info_cache)
table.insert(custom_entities_t_info_hh,
{
custom_type_id = c_type_id,
data = data,
e_type = e_type,
hh_num = hh_num,
leader_player_slot = leader_player_slot
})
end
local function set_transition_info_storage(c_type_id, data, e_type)
if custom_entities_t_info_storage[e_type] then
table.insert(custom_entities_t_info_storage[e_type], {
custom_type_id = c_type_id,
data = data
})
else
custom_entities_t_info_storage[e_type] = {
{
custom_type_id = c_type_id,
data = data
}
}
end
end
local is_portal = false
local function update_customs()
is_portal = get_entities_by(ENT_TYPE.FX_PORTAL, MASK.FX, LAYER.BOTH)[1] ~= nil
for c_type_id, c_type in ipairs(custom_types) do
if c_type.update_type == module.UPDATE_TYPE.FRAME then
for uid, c_data in pairs(c_type.entities) do
local ent = get_entity(uid)
if ent then
c_type.update(ent, c_data, c_type, c_type_id)
else
if c_type.after_destroy_callback then
c_type.after_destroy_callback(c_data)
end
c_type.entities[uid] = nil
end
end
end
end
end
local function set_custom_items_waddler(items_zone, layer)
local stored_items = get_entities_overlapping_hitbox(0, MASK.ITEM, items_zone, layer)
for _, uid in ipairs(stored_items) do
local ent = get_entity(uid)
local custom_t_info = custom_entities_t_info_storage[ent.type.id]
if custom_t_info and custom_t_info[1] then
_set_custom_entity(uid, ent, custom_t_info[1].custom_type_id, custom_t_info[1].data)
table.remove(custom_entities_t_info_storage[ent.type.id], 1)
end
end
end
local function set_custom_ents_from_previous(companions)
for _, info in ipairs(custom_entities_t_info) do
for _, p in ipairs(players) do
if p.inventory.player_slot == info.slot then
local custom_ent
if info.carry_type == CARRY_TYPE.MOUNT then
custom_ent = p:topmost_mount()
elseif info.carry_type == CARRY_TYPE.HELD then
custom_ent = p:get_held_entity()
elseif info.carry_type == CARRY_TYPE.BACK then
custom_ent = get_entity(p:worn_backitem())
elseif info.carry_type == CARRY_TYPE.POWERUP then
custom_ent = p
end
if custom_ent ~= nil then
_set_custom_entity(custom_ent.uid, custom_ent, info.custom_type_id, info.data)
end
break
end
end
end
local hh_info_cache = {}
for _, info in pairs(custom_entities_t_info_hh) do
for _, uid in ipairs(companions) do
local hh_num, player_slot = get_hh_number(uid, hh_info_cache)
local ent = get_entity(uid)
if ent.type.id == info.e_type and hh_num == info.hh_num and player_slot == info.leader_player_slot then
local custom_ent = ent:get_held_entity()
if custom_ent then
_set_custom_entity(custom_ent.uid, custom_ent, info.custom_type_id, info.data)
break
end
end
end
end
if storage_pos then
set_custom_items_waddler(AABB:new(storage_pos.x-0.5, storage_pos.y+1.5, storage_pos.x+1.5, storage_pos.y), storage_pos.l)
end
storage_pos = nil
end
set_post_tile_code_callback(function(x, y, l)
if not storage_pos then
storage_pos = {['x'] = x, ['y'] = y, ['l'] = l}
end
end, 'storage_floor')
local function get_types_cloneable(entity_uids)
local ret = {}
for _, uid in ipairs(entity_uids) do
if not test_flag(get_entity_flags(uid), 32) then -- is always enabled when the entity is held by something
local _type = get_entity_type(uid)
ret[_type] = uid
end
end
return ret
end
local CLONEABLE_MASK = MASK.PLAYER | MASK.MOUNT | MASK.MONSTER | MASK.ITEM
local function set_clonegunshot_custom_ent()
local _clonegunshot_custom_id = module.new_custom_entity(function(entity)
local hitbox = get_hitbox(entity.uid)
return {
last_overlapping = get_entities_overlapping_hitbox(0, CLONEABLE_MASK, hitbox, entity.layer)
}
end, function(entity, c_data)
local hitbox = get_hitbox(entity.uid)
c_data.last_overlapping = get_entities_overlapping_hitbox(0, CLONEABLE_MASK, hitbox, entity.layer)
end, nil, ENT_TYPE.ITEM_CLONEGUNSHOT)
module.add_after_destroy_callback(_clonegunshot_custom_id, function(c_data)
local overlapping_types = get_types_cloneable(c_data.last_overlapping)
for _, uid in ipairs(get_entities_by(ENT_TYPE.FX_TELEPORTSHADOW, MASK.ITEM, LAYER.BOTH)) do
if get_entity_type(uid+1) ~= ENT_TYPE.FX_TELEPORTSHADOW then
local spawned_uid = uid-1
local spawned_ent = get_entity(spawned_uid)
if spawned_ent.overlay and spawned_ent.overlay.uid == spawned_uid - 1 then --for jetpacks that spawn a FX_JETPACKFLAME and maybe other ents
spawned_uid = spawned_uid -1
spawned_ent = get_entity(spawned_uid)
end
local _type = get_entity_type(spawned_uid)
local cloned_uid = overlapping_types[_type]
if cloned_uid then
for id, c_type in ipairs(custom_types) do
if c_type.entities[cloned_uid] and c_type.carry_type ~= CARRY_TYPE.POWERUP then
_set_custom_entity(spawned_uid, spawned_ent, id, module.get_custom_entity(cloned_uid, id))
end
end
end
end
end
end)
return _clonegunshot_custom_id
end
---init the lib callbacks
---@param game_frame boolean @Run on GAMEFRAME if `true`
---@param not_handle_clonegun boolean @disable handling cloning of custom entities
function module.custom_init(game_frame, not_handle_clonegun)
if (game_frame) then
cb_update = set_callback(function()
update_customs()
end, ON.GAMEFRAME)
else
cb_update = set_callback(function()
update_customs()
end, ON.FRAME)
end
if not not_handle_clonegun then
if clonegunshot_custom_id == -1 then
clonegunshot_custom_id = set_clonegunshot_custom_ent()
end
cb_clonegunshot = set_post_entity_spawn(function(entity)
module.set_custom_entity(entity.uid, clonegunshot_custom_id)
end, SPAWN_TYPE.ANY, MASK.ANY, ENT_TYPE.ITEM_CLONEGUNSHOT)
end
cb_pre_load = set_callback(function()
if (state.screen_next == SCREEN.TRANSITION and state.screen ~= SCREEN.SPACESHIP)
or (state.screen_next == SCREEN.SPACESHIP)
or (state.screen == SCREEN.LEVEL and state.screen_next == SCREEN.LEVEL)
or (state.screen_next == SCREEN.WIN or state.screen_next == SCREEN.CONSTELLATION)
then
if state.quest_flags & 1 == 0 then
local is_storage_floor_there = get_entities_by(ENT_TYPE.FLOOR_STORAGE, MASK.FLOOR, LAYER.BOTH)[1] ~= nil
local hh_info_cache = {}
for c_id,c_type in ipairs(custom_types) do
for uid, c_data in pairs(c_type.entities) do
if c_type.carry_type == CARRY_TYPE.HELD or c_type.carry_type == CARRY_TYPE.BACK then
local ent = get_entity(uid) --[[@as Movable]]
---@type Player
local holder
if not ent or ent.state == 24 or ent.last_state == 24 then
holder = c_data.last_holder
else
holder = ent.overlay --[[@as Player]]
end
if holder and holder.type.search_flags & MASK.PLAYER == MASK.PLAYER then
if c_data.is_worn_backitem or holder:worn_backitem() == uid then
set_transition_info(c_id, c_data, holder.inventory.player_slot, CARRY_TYPE.BACK)
elseif holder.inventory.player_slot == -1 then
set_transition_info_hh(c_id, c_data, holder.type.id, holder.uid, hh_info_cache)
else
set_transition_info(c_id, c_data, holder.inventory.player_slot, CARRY_TYPE.HELD)
end
elseif ent and is_storage_floor_there and ent.standing_on_uid ~= -1 and get_entity(ent.standing_on_uid).type.id == ENT_TYPE.FLOOR_STORAGE then
set_transition_info_storage(c_id, c_data, ent.type.id)
end
elseif c_type.carry_type == CARRY_TYPE.MOUNT then
local ent = get_entity(uid) --[[@as Mount]]
---@type Player
local holder, rider_uid
if not ent or ent.state == 24 or ent.last_state == 24 then
holder = c_data.last_holder
rider_uid = c_data.last_rider_uid
else
holder = ent.overlay --[[@as Player]]
rider_uid = ent.rider_uid
end
if holder and holder.type.search_flags & MASK.PLAYER == MASK.PLAYER then
if holder.inventory.player_slot == -1 then
set_transition_info_hh(c_id, c_data, holder.type.id, holder.uid, hh_info_cache)
else
set_transition_info(c_id, c_data, holder.inventory.player_slot, CARRY_TYPE.HELD)
end
elseif rider_uid and rider_uid ~= -1 then
holder = get_entity(rider_uid) --[[@as Player]]
if holder and holder.type.search_flags == MASK.PLAYER then
set_transition_info(c_id, c_data, holder.inventory.player_slot, CARRY_TYPE.MOUNT)
end
end
elseif c_type.carry_type == CARRY_TYPE.POWERUP then
---@type Player
local ent = get_entity(uid)
if ent then
set_transition_info(c_id, c_data, ent.inventory.player_slot, CARRY_TYPE.POWERUP)
end
end
end
end
if custom_entities_t_info_cog_ankh[1] then
for _, info in ipairs(custom_entities_t_info_cog_ankh) do
set_transition_info(info.custom_type_id, info.data, info.slot, CARRY_TYPE.POWERUP)
end
end
custom_entities_t_info_cog_ankh = {}
for _,c_type in ipairs(custom_types) do
c_type.entities = {}
end
end
end
end, ON.PRE_LOAD_SCREEN)
cb_post_level_gen = set_callback(function()
if state.screen == SCREEN.LEVEL then
local px, py, pl = get_position(players[1].uid)
local companions = get_entities_at(0, MASK.PLAYER, px, py, pl, 2)
set_custom_ents_from_previous(companions)
custom_entities_t_info = {}
custom_entities_t_info_hh = {}
elseif state.screen == SCREEN.TRANSITION or state.screen == SCREEN.SCORES or state.screen == SCREEN.WIN then
local companions = get_entities_by(0, MASK.PLAYER, LAYER.FRONT)
set_custom_ents_from_previous(companions)
end
end, ON.POST_LOAD_SCREEN)
cb_pre_level_gen = set_callback(function()
shops_by_room_pos = {}
for _,c_type in ipairs(custom_types) do
c_type.entities = {}
end
end, ON.PRE_LEVEL_GENERATION)
end
---init the lib callbacks (checks if it was already init and uses GAMEFRAME by default)
function module.init()
if didnt_init then
module.custom_init(true)
didnt_init = false
end
end
---Stop the library callbacks (not the extras callbacks)
function module.stop()
clear_callback(cb_update)
clear_callback(cb_pre_load)
clear_callback(cb_post_level_gen)
clear_callback(cb_pre_level_gen)
clear_callback(cb_clonegunshot)
didnt_init = true
end
--update last_holder when there's a portal and the entity isn't entering it
local function update_custom_held_portal(ent, c_data)
if is_portal and ent.state ~= 24 and ent.last_state ~= 24 and ent.overlay then --24 seems to be the state when entering portal
c_data.last_holder = ent.overlay
c_data.is_worn_backitem = ent.overlay.type.search_flags & MASK.PLAYER == MASK.PLAYER and ent.overlay:worn_backitem() == ent.uid
end
end
local function update_custom_mount_portal(ent, c_data)
if is_portal and ent.state ~= 24 and ent.last_state ~= 24 then
c_data.last_holder = ent.overlay
c_data.last_rider_uid = ent.rider_uid
end
end
local function update_custom_held(ent, c_data, c_type)
c_type.update_callback(ent, c_data)
update_custom_held_portal(ent, c_data)
end
local function update_custom_mount(ent, c_data, c_type)
c_type.update_callback(ent, c_data)
update_custom_mount_portal(ent, c_data)
end
local function update_custom_ent(ent, c_data, c_type)
c_type.update_callback(ent, c_data)
end
---@alias EntSet fun(ent: userdata, data: table, custom_id: integer, extra_args: any):table
---@alias EntUpdate fun(ent: userdata, c_data: table):nil
local function _new_custom_entity(set_func, _update_func, update_callback, carry_type, ent_type, update_type)
if update_type == nil then
update_type = module.UPDATE_TYPE.FRAME
end
local custom_id = #custom_types + 1
local update_func
if update_type == module.UPDATE_TYPE.FRAME then
update_func = _update_func
else --is post or pre statemachine
update_func = function(entity)
local custom_type = custom_types[custom_id]
local c_data = custom_type.entities[entity.uid]
_update_func(entity, c_data, custom_type, custom_id)
end
end
custom_types[custom_id] = {
set = set_func,
update_callback = update_callback,
update = update_func,
carry_type = carry_type,
ent_type = ent_type,
update_type = update_type,
entities = {}
}
return custom_id, custom_types[custom_id]
end
---Create a new custom entity type
---@param set_func EntSet @Called when the entity is set manually, on transitions, and when cloned
---@param update_func EntUpdate @Called on `FRAME` or `GAMEFRAME`, depending on the init
---@param carry_type? integer @Use `CARRY_TYPE`
---@param ent_type? integer
---@param update_type? integer
---@return integer
function module.new_custom_entity(set_func, update_func, carry_type, ent_type, update_type)
local update
if carry_type == CARRY_TYPE.HELD or carry_type == CARRY_TYPE.BACK then
update = update_custom_held
elseif carry_type == CARRY_TYPE.MOUNT then
update = update_custom_mount
else
update = update_custom_ent
end
return _new_custom_entity(set_func, update, update_func, carry_type, ent_type, update_type)
end
---Create a new custom entity type that is a gun
---@param set_func EntSet @Called when the entity is set manually, on transitions, and when cloned
---@param update_func EntUpdate @Called on `FRAME` or `GAMEFRAME`, depending on the init
---@param firefunc fun(ent:userdata, c_data:table):nil
---@param cooldown integer @Cooldown, in frames
---@param recoil_x number
---@param recoil_y number
---@param ent_type integer
---@return integer
function module.new_custom_gun(set_func, update_func, firefunc, cooldown, recoil_x, recoil_y, ent_type, update_type)
---@type EntSet
local set = function (ent, c_data, custom_id, extra_args)
local cb_id = ent:set_pre_trigger_action(function(ent, holder)
if ent.cooldown == 0 then
ent.cooldown = cooldown
local recoil_dir = test_flag(holder.flags, ENT_FLAG.FACING_LEFT) and 1 or -1
holder.velocityx = holder.velocityx + recoil_x*recoil_dir
holder.velocityy = holder.velocityy + recoil_y
local c_data = module.get_custom_entity(ent.uid, custom_id)
firefunc(ent, c_data)
end
return true
end)
c_data = set_func(ent, c_data, custom_id, extra_args) or {}
c_data._lib_callbacks = {cb_id}
return c_data
end
local custom_id, custom_type = _new_custom_entity(set, update_custom_held, update_func, CARRY_TYPE.HELD, ent_type, update_type)
custom_type.shoot = firefunc
custom_type.cooldown = cooldown
custom_type.recoil_x = recoil_x
custom_type.recoil_y = recoil_y
return custom_id
end
---@class CustomTypeWeapon : CustomEntityType
---@field bulletfunc function
---@field mute_sound boolean
---@field cooldown integer
---@field recoil_x number
---@field recoil_y number
local function set_custom_bullet_callback(weapon_id)
set_pre_entity_spawn(function(entity_type, x, y, layer, _, _)
--horizontal offset probably isn't very useful to know cause it changes when being next to a wall
--freezeray and clonegun bullet offset: 0.5, ~0.12
--plasmacannon: ~0.3545, 0.0
--shotgun: ~0.35, ~0.1
local weapons_left = get_entities_at(weapon_id, MASK.ITEM, x-0.25, y-0.12, layer, 0.4)
local last_left = #weapons_left
local weapons = join(weapons_left, get_entities_at(weapon_id, MASK.ITEM, x+0.25, y-0.12, layer, 0.4))
---@type CustomTypeWeapon
for _,c_type in ipairs(custom_types) do
for i, weapon_uid in ipairs(weapons) do
local c_data = c_type.entities[weapon_uid]
if c_data and c_type.bulletfunc and weapon_info[c_type.ent_type].bullet == entity_type and (c_data.not_shot and c_data.not_shot ~= 0) then
local weapon = get_entity(weapon_uid)
---@type Movable
local holder = weapon.overlay
if holder and ( (holder:is_button_pressed(BUTTON.WHIP) and holder.state ~= CHAR_STATE.DUCKING) or (holder.type.id == ENT_TYPE.MONS_CAVEMAN and holder.velocityy > 0.05 and holder.velocityy < 0.0501 and holder.state == CHAR_STATE.STANDING) ) and weapon.cooldown == 0 then
local wx, wy = get_position(weapon_uid)
if weapon_info[weapon_id].bullet_off_y+0.001 >= y-wy and weapon_info[weapon_id].bullet_off_y-0.001 <= y-wy
and test_flag(weapon.flags, ENT_FLAG.FACING_LEFT) == (i <= last_left) then
if c_type.mute_sound then
weapon_info[weapon_id].shots = weapon_info[weapon_id].shots + 1
end
if entity_type == ENT_TYPE.ITEM_BULLET then
c_data.not_shot = c_data.not_shot - 1
else
c_data.not_shot = false
end
if c_type.cooldown then
weapon.cooldown = c_type.cooldown+2
end
local recoil_dir = test_flag(holder.flags, ENT_FLAG.FACING_LEFT) and 1 or -1
holder.velocityx = holder.velocityx + c_type.recoil_x*recoil_dir
holder.velocityy = holder.velocityy + c_type.recoil_y
c_type.bulletfunc(weapon, c_data)
return spawn_entity(ENT_TYPE.ITEM_BULLET, 0, 0, layer, 0, 0)
end
end
end
end
end
end, SPAWN_TYPE.SYSTEMIC, MASK.ITEM, weapon_info[weapon_id].bullet)
weapon_info[weapon_id].callb_set = true
end
local function custom_gun2_shotgun_update(ent, c_data, c_type)
c_data.not_shot = 6
c_type.update_callback(ent, c_data)
update_custom_held_portal(ent, c_data)
end
local function custom_gun2_update(ent, c_data, c_type)
c_data.not_shot = true
c_type.update_callback(ent, c_data)
update_custom_held_portal(ent, c_data)
end
---Create a new custom entity type that is a gun, is called for each bullet, so be careful with recoil with shotgun
---@param set_func EntSet @Called when the entity is set manually, on transitions, and when cloned
---@param update_func EntUpdate @Called on `FRAME` or `GAMEFRAME`, depending on the init
---@param bulletfunc fun(gun_ent:userdata, c_data:table):nil @Called for each bullet on pre_entity_spawn
---@param cooldown integer
---@param recoil_x number
---@param recoil_y number
---@param ent_type integer
---@param mute_sound boolean
---@return integer
function module.new_custom_gun2(set_func, update_func, bulletfunc, cooldown, recoil_x, recoil_y, ent_type, mute_sound, update_type)
if not weapon_info[ent_type].callb_set then
set_custom_bullet_callback(ent_type)
end
if mute_sound and not weapon_info[ent_type].sound_callb_set then
--Crashes sometimes on OL, not on PL
set_vanilla_sound_callback(weapon_info[ent_type].sound, VANILLA_SOUND_CALLBACK_TYPE.STARTED, function(sound)
if weapon_info[ent_type].shots > 0 then
sound:set_volume(0)
sound:stop()
weapon_info[ent_type].shots = weapon_info[ent_type].shots - 1
end
end)
weapon_info[ent_type].sound_callb_set = true
end
local update = ent_type == ENT_TYPE.ITEM_SHOTGUN and custom_gun2_shotgun_update or custom_gun2_update
local custom_id, custom_type = _new_custom_entity(set_func, update, update_func, CARRY_TYPE.HELD, ent_type, update_type)
custom_type.bulletfunc = bulletfunc
custom_type.cooldown = cooldown
custom_type.recoil_x = recoil_x
custom_type.recoil_y = recoil_y
custom_type.not_shot = true
custom_type.mute_sound = mute_sound
return custom_id
end
local function spawn_replacement(ent, custom_id)
local is_held_by_player = ent.overlay ~= nil and ent.overlay.type.search_flags == MASK.PLAYER
local x, y, l = get_position(ent.uid)
local vx, vy = 0, 0
if not is_held_by_player then
vx, vy = get_velocity(ent.uid)
end
local replacement_uid = spawn(custom_types[custom_id].ent_type, x, y, l, vx, vy)
local replacement = get_entity(replacement_uid)
module.set_custom_entity(replacement_uid, custom_id)
if is_held_by_player then
ent.overlay:pick_up(replacement)
end
ent:destroy()
return replacement
end
local back_warn_sound = get_sound(VANILLA_SOUND.ITEMS_BACKPACK_WARN)
local custom_purchasable_back_flammable_update = function(ent, c_data, c_type)
if not test_flag(ent.flags, ENT_FLAG.SHOP_ITEM) then
spawn_replacement(ent, c_type.toreplace_custom_id)
c_data = nil
else
local danger_entities = get_entities_overlapping_hitbox({ENT_TYPE.MONS_MAGMAMAN, ENT_TYPE.ITEM_BULLET}, MASK.ANY, get_hitbox(ent.uid), ent.layer)
if danger_entities[1] or ent.onfire_effect_timer > 0 then
back_warn_sound:play()
if ent.last_owner_uid ~= -1 then
if get_entity(ent.last_owner_uid).type.search_flags == MASK.PLAYER then
get_entity(c_data.shop_owner).aggro_trigger = true
else
---@type Movable
local shop_owner = get_entity(c_data.shop_owner)
if shop_owner.holding_uid ~= -1 then
get_entity(shop_owner.holding_uid):trigger_action(shop_owner)
else
local ent_type = get_entity_type(c_data.shop_owner)
if ent_type == ENT_TYPE.MONS_SHOPKEEPER or ent_type == ENT_TYPE.MONS_MERCHANT then
local weapon_type = ent_type == ENT_TYPE.MONS_SHOPKEEPER and ENT_TYPE.ITEM_SHOTGUN or ENT_TYPE.ITEM_CROSSBOW
local weapon_uid = spawn(weapon_type, 0, 0, LAYER.FRONT, 0, 0)
pick_up(shop_owner.uid, weapon_uid)
get_entity(weapon_uid):trigger_action(shop_owner)
shop_owner.is_patrolling = true
end
end
end
end
spawn_replacement(ent, c_type.toreplace_custom_id).explosion_trigger = true
c_data = nil
end
c_type.update_callback(ent, c_data)
end
end
local function custom_purchasable_back_nonflammable_update(ent, c_data, c_type)
if not test_flag(ent.flags, ENT_FLAG.SHOP_ITEM) then
spawn_replacement(ent, c_type.toreplace_custom_id)
c_data = nil
else
c_type.update_callback(ent, c_data)
end
end
---Create a new custom entity type, use this for backpacks that spawn in shops
---@param set_func EntSet @Called when the entity is set manually, on transitions, and when cloned
---@param update_func EntUpdate @Called on `FRAME` or `GAMEFRAME`, depending on the init
---@param toreplace_custom_id integer
---@param flammable boolean
---@return integer
function module.new_custom_purchasable_back(set_func, update_func, toreplace_custom_id, flammable, update_type)
local custom_id, custom_type
local update, set
if flammable then
set = function(ent, c_data, c_type_id, args)
ent.flags = clr_flag(ent.flags, ENT_FLAG.TAKE_NO_DAMAGE)
ent.hitboxx = 0.3
ent.hitboxy = 0.35
ent.offsety = -0.03
set_timeout(function()
custom_type.entities[ent.uid].shop_owner = ent.last_owner_uid
end, 1)
return set_func(ent, c_data, c_type_id, args)
end
update = custom_purchasable_back_flammable_update
else
set = function(ent, c_data, c_type_id, args)
ent.hitboxx = 0.3
ent.hitboxy = 0.35
ent.offsety = -0.03
return set_func(ent, c_data, c_type_id, args)
end
update = custom_purchasable_back_nonflammable_update
end
custom_id, custom_type = _new_custom_entity(set, update, update_func, nil, ENT_TYPE.ITEM_ROCK, update_type)
custom_type.toreplace_custom_id = toreplace_custom_id
return custom_id
end
local function set_nonflammable_backs_callbacks()
set_pre_entity_spawn(function(_, x, y, layer, _, _)
if y == -123 then
return spawn_entity_nonreplaceable(ENT_TYPE.ITEM_ROCK, x, y, layer, 0, 0)
end
end, SPAWN_TYPE.SYSTEMIC, MASK.EXPLOSION, ENT_TYPE.FX_EXPLOSION)
set_vanilla_sound_callback(VANILLA_SOUND.ITEMS_BACKPACK_WARN, VANILLA_SOUND_CALLBACK_TYPE.STARTED, function(sound)
if just_burnt > 0 and last_burn == get_frame()-1 then
sound:set_volume(0)
sound:stop()
just_burnt = just_burnt - 1
end
end)
nonflammable_backs_callbacks_set = true
end
local yellow = Color:yellow()
local function custom_back_flammable_update(ent, c_data, c_type)
local holder = ent.overlay
if holder and holder.type.search_flags == MASK.PLAYER then
local backitem_uid = holder:worn_backitem()
if backitem_uid == ent.uid then
ent.fuel = 0
c_type.update_callback(ent, c_data, holder)
local holding = get_entity(holder.holding_uid)
if holding and holding.type.id == ENT_TYPE.ITEM_JETPACK and not c_type.entities[holding.uid] then
holder:unequip_backitem()
holder:pick_up(holding)
end
elseif not c_type.entities[backitem_uid] then
holder:unequip_backitem()
holder:pick_up(ent)
else
c_type.update_callback(ent, c_data)
end
else
c_type.update_callback(ent, c_data)
end
update_custom_held_portal(ent, c_data)
end
local function custom_back_nonflammable_update(ent, c_data, c_type)
local holder = ent.overlay
if holder and holder.type.search_flags == MASK.PLAYER then
local backitem_uid = holder:worn_backitem()
if backitem_uid == ent.uid then
ent.fuel = 0
c_type.update_callback(ent, c_data, holder)
local holding = get_entity(holder.holding_uid)
if holding and holding.type.id == ENT_TYPE.ITEM_JETPACK and not c_type.entities[holding.uid] then
holder:unequip_backitem()
ent.flags = clr_flag(ent.flags, ENT_FLAG.PAUSE_AI_AND_PHYSICS)
holder:pick_up(holding)
end
elseif not c_type.entities[backitem_uid] then
holder:unequip_backitem()
holder:pick_up(ent)
else
c_type.update_callback(ent, c_data)
end
else
c_type.update_callback(ent, c_data)
if test_flag(ent.flags, ENT_FLAG.PAUSE_AI_AND_PHYSICS) then
ent.flags = clr_flag(ent.flags, ENT_FLAG.PAUSE_AI_AND_PHYSICS)
end
end
if ent.explosion_trigger then
ent.explosion_trigger = false
ent.explosion_timer = 0
just_burnt = just_burnt + 1
last_burn = get_frame()