-
-
Notifications
You must be signed in to change notification settings - Fork 444
/
Copy pathprojectile.dm
797 lines (707 loc) · 30 KB
/
projectile.dm
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
/obj/projectile
name = "projectile"
icon = 'icons/obj/projectiles.dmi'
icon_state = "bullet"
density = FALSE
anchored = TRUE
var/item_flags = ABSTRACT
pass_flags = PASSTABLE
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
movement_type = FLYING
wound_bonus = CANT_WOUND // can't wound by default
blocks_emissive = EMISSIVE_BLOCK_GENERIC
layer = MOB_LAYER
//The sound this plays on impact.
var/hitsound = 'sound/weapons/pierce.ogg'
var/hitsound_wall = ""
resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
var/def_zone = "" //Aiming at
var/atom/movable/firer = null//Who shot it
var/atom/fired_from = null // the atom that the projectile was fired from (gun, turret)
var/suppressed = FALSE //Attack message
var/yo = null
var/xo = null
var/atom/original = null // the original target clicked
var/turf/starting = null // the projectile's starting turf
var/list/impacted = list() // we've passed through these atoms, don't try to hit them again
var/p_x = 16
var/p_y = 16 // the pixel location of the tile that the player clicked. Default is the center
//Fired processing vars
var/fired = FALSE //Have we been fired yet
var/paused = FALSE //for suspending the projectile midair
var/last_projectile_move = 0
var/last_process = 0
var/time_offset = 0
var/datum/point/vector/trajectory
var/trajectory_ignore_forcemove = FALSE //instructs forceMove to NOT reset our trajectory to the new location!
/// If objects are below this layer, we pass through them
var/hit_threshhold = PROJECTILE_HIT_THRESHHOLD_LAYER
/// Hit mobs regardless of stun or critical condition
var/ignore_crit = FALSE
/// During each fire of SSprojectiles, the number of deciseconds since the last fire of SSprojectiles
/// is divided by this var, and the result truncated to the next lowest integer is
/// the number of times the projectile's `pixel_move` proc will be called.
var/speed = 0.8
var/Angle = 0
var/original_angle = 0 //Angle at firing
var/nondirectional_sprite = FALSE //Set TRUE to prevent projectiles from having their sprites rotated based on firing angle
var/spread = 0 //amount (in degrees) of projectile spread
animate_movement = 0 //Use SLIDE_STEPS in conjunction with legacy
var/ricochets = 0
var/ricochets_max = 2
var/ricochet_chance = 30
var/force_hit = FALSE //If the object being hit can pass ths damage on to something else, it should not do it for this bullet.
///Whether this projectile can ricochet off of coins
var/can_ricoshot = FALSE
///How many things can this penetrate?
var/penetrations = 0
///Flags used to specify what this projectile can penetrate. Default is mobs only.
var/penetration_flags = PENETRATE_MOBS
//Hitscan
var/hitscan = FALSE //Whether this is hitscan. If it is, speed is basically ignored.
var/list/beam_segments //assoc list of datum/point or datum/point/vector, start = end. Used for hitscan effect generation.
var/datum/point/beam_index
/// The ending/last touched turf during hitscanning.
var/turf/hitscan_last
var/tracer_type
var/muzzle_type
var/impact_type
//Fancy hitscan lighting effects!
var/hitscan_light_intensity = 1.5
var/hitscan_light_range = 0.75
var/hitscan_light_color_override
var/muzzle_flash_intensity = 3
var/muzzle_flash_range = 1.5
var/muzzle_flash_color_override
var/impact_light_intensity = 3
var/impact_light_range = 2
var/impact_light_color_override
//Homing
var/homing = FALSE
var/homing_away = FALSE // In case you want it to instead turn away from the target, useful for when the projectile is going haywire!
var/atom/homing_target
var/homing_turn_speed = 10 //Angle per tick.
var/homing_inaccuracy_min = 0 //in pixels for these. offsets are set once when setting target.
var/homing_inaccuracy_max = 0
var/homing_offset_x = 0
var/homing_offset_y = 0
var/ignore_source_check = FALSE
var/damage = 10
var/damage_type = BRUTE //BRUTE, BURN, TOX, OXY, CLONE are the only things that should be in here
///Determines if the projectile will skip any damage inflictions
var/nodamage = FALSE
///Defines what armor to use when it hits things. Must be set to bullet, laser, energy, or bomb
var/armor_flag = BULLET
///How much armor this projectile pierces.
var/armour_penetration = 0
///How much armor this projectile pierces.
var/projectile_type = /obj/projectile
///This will de-increment every step. When 0, it will deletze the projectile.
var/range = 50
var/decayedRange //stores original range
var/reflect_range_decrease = 5 //amount of original range that falls off when reflecting, so it doesn't go forever
var/reflectable = NONE // Can it be reflected or not?
// Status effects applied on hit
var/stun = 0
var/knockdown = 0
var/paralyze = 0
var/immobilize = 0
var/unconscious = 0
var/eyeblur = 0
/// Drowsiness applied on projectile hit
var/drowsy = 0 SECONDS
/// Jittering applied on projectile hit
var/jitter = 0 SECONDS
/// Extra stamina damage applied on projectile hit (in addition to the main damage)
var/stamina = 0
/// Stuttering applied on projectile hit
var/stutter = 0 SECONDS
/// Slurring applied on projectile hit
var/slur = 0 SECONDS
var/irradiate = 0 //yog radiation
var/dismemberment = 0 //The higher the number, the greater the bonus to dismembering. 0 will not dismember at all.
var/catastropic_dismemberment = FALSE //If TRUE, this projectile deals its damage to the chest if it dismembers a limb.
var/impact_effect_type //what type of impact effect to show when hitting something
var/log_override = FALSE //is this type spammed enough to not log? (KAs)
/// We ignore mobs with these factions.
var/list/ignored_factions
///If defined, on hit we create an item of this type then call hitby() on the hit target with this, mainly used for embedding items (bullets) in targets
var/shrapnel_type
///If we have a shrapnel_type defined, these embedding stats will be passed to the spawned shrapnel type, which will roll for embedding on the target
var/list/embedding
///If TRUE, hit mobs even if they're on the floor and not our target
var/hit_prone_targets = FALSE
///For what kind of brute wounds we're rolling for, if we're doing such a thing. Lasers obviously don't care since they do burn instead.
var/temporary_unstoppable_movement = FALSE
///For what kind of brute wounds we're rolling for, if we're doing such a thing. Lasers obviously don't care since they do burn instead.
var/sharpness = NONE
///How much we want to drop both wound_bonus and bare_wound_bonus (to a minimum of 0 for the latter) per tile, for falloff purposes
var/wound_falloff_tile
///How much we want to drop the embed_chance value, if we can embed, per tile, for falloff purposes
var/embed_falloff_tile
/// If true directly targeted turfs can be hit
var/can_hit_turfs = FALSE
/// If this projectile has been parried before
var/parried = FALSE
var/splatter = FALSE // Make a cool splatter effect even if it doesn't do brute damage
/// If FALSE, allow us to hit something directly targeted/clicked/whatnot even if we're able to phase through it
var/phasing_ignore_direct_target = FALSE
/// Bitflag for things the projectile should just phase through entirely - No hitting unless direct target and [phasing_ignore_direct_target] is FALSE. Uses pass_flags flags.
var/projectile_phasing = NONE
/obj/projectile/Initialize(mapload)
. = ..()
impacted = list()
decayedRange = range
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
)
AddElement(/datum/element/connect_loc, loc_connections)
/obj/projectile/proc/Range()
range--
if(wound_bonus != CANT_WOUND)
wound_bonus += wound_falloff_tile
bare_wound_bonus = max(0, bare_wound_bonus + wound_falloff_tile)
if(range <= 0 && loc)
on_range()
/obj/projectile/proc/on_range() //if we want there to be effects when they reach the end of their range
qdel(src)
//to get the correct limb (if any) for the projectile hit message
/mob/living/proc/check_limb_hit(hit_zone)
if(has_limbs)
return hit_zone
/mob/living/carbon/check_limb_hit(hit_zone)
if(get_bodypart(hit_zone))
return hit_zone
else //when a limb is missing the damage is actually passed to the chest
return BODY_ZONE_CHEST
/obj/projectile/proc/prehit(atom/target)
return TRUE
/obj/projectile/proc/on_hit(atom/target, blocked = FALSE)
if(fired_from)
SEND_SIGNAL(fired_from, COMSIG_PROJECTILE_ON_HIT, firer, target, Angle)
var/turf/target_loca = get_turf(target)
var/hitx
var/hity
if(target == original)
hitx = target.pixel_x + p_x - 16
hity = target.pixel_y + p_y - 16
else
hitx = target.pixel_x + rand(-8, 8)
hity = target.pixel_y + rand(-8, 8)
if(!nodamage && (damage_type == BRUTE || damage_type == BURN) && iswallturf(target_loca) && prob(75))
var/turf/closed/wall/W = target_loca
if(impact_effect_type && !hitscan)
new impact_effect_type(target_loca, hitx, hity)
W.add_dent(WALL_DENT_SHOT, hitx, hity)
if((penetration_flags & PENETRATE_WALLS) && penetrations > 0)
penetrations -= 1
return BULLET_ACT_FORCE_PIERCE
return BULLET_ACT_HIT
if(!isliving(target))
if(impact_effect_type && !hitscan)
new impact_effect_type(target_loca, hitx, hity)
if((penetration_flags & PENETRATE_OBJECTS) && penetrations > 0)
penetrations -= 1
return BULLET_ACT_FORCE_PIERCE
return BULLET_ACT_HIT
var/mob/living/L = target
if(blocked != 100) // not completely blocked
if(damage && L.blood_volume && (damage_type == BRUTE || splatter))
var/mob/living/carbon/C = L
var/splatter_dir = dir
if(starting)
splatter_dir = get_dir(starting, target_loca)
if(isalien(L) || ispolysmorph(L))
new /obj/effect/temp_visual/dir_setting/bloodsplatter/xenosplatter(target_loca, splatter_dir)
else if(iscarbon(L) && !(NOBLOOD in C.dna.species.species_traits))
var/splatter_color
var/mob/living/carbon/carbon_bleeder = L
splatter_color = carbon_bleeder.dna.blood_type.color
new /obj/effect/temp_visual/dir_setting/bloodsplatter(target_loca, splatter_dir, splatter_color)
else
new /obj/effect/temp_visual/dir_setting/bloodsplatter/genericsplatter(target_loca, splatter_dir)
var/obj/item/bodypart/B = L.get_bodypart(def_zone)
if(B?.status == BODYPART_ROBOTIC) // So if you hit a robotic, it sparks instead of bloodspatters
do_sparks(2, FALSE, target.loc)
var/splatter_color = null
if(iscarbon(L))
var/mob/living/carbon/carbon_target = L
splatter_color = carbon_target.dna.blood_type.color
new /obj/effect/temp_visual/dir_setting/bloodsplatter(target_loca, splatter_dir, splatter_color)
if(prob(33))
L.add_splatter_floor(target_loca)
else if(impact_effect_type && !hitscan)
new impact_effect_type(target_loca, hitx, hity)
var/organ_hit_text = ""
var/limb_hit = L.check_limb_hit(def_zone)//to get the correct message info.
if(limb_hit)
organ_hit_text = " in \the [parse_zone(limb_hit)]"
if(suppressed)
playsound(loc, hitsound, 5, 1, -1)
to_chat(L, span_userdanger("You're shot by \a [src][organ_hit_text]!"))
else
if(hitsound)
var/volume = vol_by_damage()
playsound(loc, hitsound, volume, 1, -1)
L.visible_message(span_danger("[L] is hit by \a [src][organ_hit_text]!"), \
span_userdanger("[L] is hit by \a [src][organ_hit_text]!"), null, COMBAT_MESSAGE_RANGE)
L.on_hit(src)
var/viruslist = "" // yogs - adds viruslist variable
var/reagent_note
if(reagents && reagents.reagent_list)
reagent_note = " REAGENTS:"
for(var/datum/reagent/R in reagents.reagent_list)
reagent_note += "[R.name] ([num2text(R.volume)])"
// yogs start - Checks blood for disease
if(istype(R, /datum/reagent/blood))
var/datum/reagent/blood/RR = R
for(var/datum/disease/D in RR.data["viruses"])
viruslist += " [D.name]"
if(istype(D, /datum/disease/advance))
var/datum/disease/advance/DD = D
viruslist += " \[ symptoms: "
for(var/datum/symptom/S in DD.symptoms)
viruslist += "[S.name] "
viruslist += "\]"
if(viruslist)
investigate_log("[firer] injected [src] using a projectile with [viruslist] [blocked == 100 ? "BLOCKED" : ""]", INVESTIGATE_VIROLOGY)
log_game("[firer] injected [src] using a projectile with [viruslist] [blocked == 100 ? "BLOCKED" : ""]")
// yogs end
if(ismob(firer))
log_combat(firer, L, "shot", src, reagent_note)
else
L.log_message("has been shot by [firer] with [src]", LOG_ATTACK, color="orange")
return L.apply_effects(stun, knockdown, unconscious, irradiate, slur, stutter, eyeblur, drowsy, blocked, stamina, jitter, paralyze, immobilize)
/obj/projectile/proc/vol_by_damage()
if(src.damage)
return clamp((src.damage) * 0.67, 30, 100)// Multiply projectile damage by 0.67, then CLAMP the value between 30 and 100
else
return 50 //if the projectile doesn't do damage, play its hitsound at 50% volume
/obj/projectile/proc/on_ricochet(atom/A)
return
/obj/projectile/proc/store_hitscan_collision(datum/point/pcache)
beam_segments[beam_index] = pcache
beam_index = pcache
beam_segments[beam_index] = null
/obj/projectile/Bump(atom/A)
if(!trajectory)
qdel(src)
return
var/datum/point/pcache = trajectory.copy_to()
var/turf/T = get_turf(A)
if(check_ricochet(A) && check_ricochet_flag(A) && ricochets < ricochets_max)
ricochets++
if(A.handle_ricochet(src))
on_ricochet(A)
ignore_source_check = TRUE
decayedRange = max(0, decayedRange - reflect_range_decrease)
range = decayedRange
if(hitscan)
store_hitscan_collision(pcache)
return TRUE
var/distance = get_dist(T, starting) // Get the distance between the turf shot from and the mob we hit and use that for the calculations.
def_zone = ran_zone(def_zone, max(100-(7*distance), 5)) //Lower accurancy/longer range tradeoff. 7 is a balanced number to use.
if(isturf(A) && hitsound_wall)
var/volume = clamp(vol_by_damage() + 20, 0, 100)
if(suppressed)
volume = 5
playsound(loc, hitsound_wall, volume, 1, -1)
return process_hit(T, select_target(T, A))
#define QDEL_SELF 1 //Delete if we're not PHASING flagged non-temporarily
#define DO_NOT_QDEL 2 //Pass through.
#define FORCE_QDEL 3 //Force deletion.
/obj/projectile/proc/process_hit(turf/T, atom/target, qdel_self, hit_something = FALSE) //probably needs to be reworked entirely when pixel movement is done.
if(QDELETED(src) || !T || !target) //We're done, nothing's left.
if((qdel_self == FORCE_QDEL) || ((qdel_self == QDEL_SELF) && !temporary_unstoppable_movement && !CHECK_BITFIELD(movement_type, PHASING)))
qdel(src)
return hit_something
impacted |= target //Make sure we're never hitting it again. If we ever run into weirdness with piercing projectiles needing to hit something multiple times.. well.. that's a to-do.
if(!prehit(target))
return process_hit(T, select_target(T), qdel_self, hit_something) //Hit whatever else we can since that didn't work.
var/result = target.bullet_act(src, def_zone)
if(result == BULLET_ACT_FORCE_PIERCE)
if(!CHECK_BITFIELD(movement_type, PHASING))
temporary_unstoppable_movement = TRUE
ENABLE_BITFIELD(movement_type, PHASING)
return process_hit(T, select_target(T), qdel_self, TRUE) //Hit whatever else we can since we're piercing through but we're still on the same tile.
else if(result == BULLET_ACT_PENETRATE) // This is slightly different from ACT_TURF in that it goes through the first thing
return process_hit(T, select_target(T), qdel_self, TRUE)
else if(result == BULLET_ACT_TURF) //We hit the turf but instead we're going to also hit something else on it.
return process_hit(T, select_target(T), QDEL_SELF, TRUE)
else //Whether it hit or blocked, we're done!
qdel_self = QDEL_SELF
hit_something = TRUE
if((qdel_self == FORCE_QDEL) || ((qdel_self == QDEL_SELF) && !temporary_unstoppable_movement && !CHECK_BITFIELD(movement_type, PHASING)))
qdel(src)
return hit_something
#undef QDEL_SELF
#undef DO_NOT_QDEL
#undef FORCE_QDEL
/obj/projectile/proc/select_target(turf/T, atom/target) //Select a target from a turf.
if((original in T) && can_hit_target(original, impacted, TRUE, TRUE))
return original
if(target && can_hit_target(target, impacted, target == original, TRUE))
return target
var/list/mob/living/possible_mobs = typecache_filter_list(T, GLOB.typecache_mob)
var/list/mob/mobs = list()
for(var/mob/living/M in possible_mobs)
if(!can_hit_target(M, impacted, M == original, TRUE))
continue
mobs += M
if(LAZYLEN(mobs))
var/mob/M = pick(mobs)
return M?.lowest_buckled_mob()
var/list/obj/possible_objs = typecache_filter_list(T, GLOB.typecache_machine_or_structure)
var/list/obj/objs = list()
for(var/obj/O in possible_objs)
if(!can_hit_target(O, impacted, O == original, TRUE))
continue
objs += O
if(LAZYLEN(objs))
var/obj/object_chosen = pick(objs)
return object_chosen
//Nothing else is here that we can hit, hit the turf if we haven't.
if(!(T in impacted) && can_hit_target(T, impacted, T == original, TRUE))
return T
//Returns null if nothing at all was found.
/obj/projectile/proc/check_ricochet()
if(prob(ricochet_chance))
return TRUE
return FALSE
/obj/projectile/proc/check_ricochet_flag(atom/A)
if(A.flags_1 & CHECK_RICOCHET_1)
return TRUE
return FALSE
/obj/projectile/proc/return_predicted_turf_after_moves(moves, forced_angle) //I say predicted because there's no telling that the projectile won't change direction/location in flight.
if(!trajectory && isnull(forced_angle) && isnull(Angle))
return FALSE
var/datum/point/vector/current = trajectory
if(!current)
var/turf/T = get_turf(src)
current = new(T.x, T.y, T.z, pixel_x, pixel_y, isnull(forced_angle)? Angle : forced_angle, SSprojectiles.global_pixel_speed)
var/datum/point/vector/v = current.return_vector_after_increments(moves * SSprojectiles.global_iterations_per_move)
return v.return_turf()
/obj/projectile/proc/return_pathing_turfs_in_moves(moves, forced_angle)
var/turf/current = get_turf(src)
var/turf/ending = return_predicted_turf_after_moves(moves, forced_angle)
return getline(current, ending)
/obj/projectile/Process_Spacemove(movement_dir = 0)
return TRUE //Bullets don't drift in space
/obj/projectile/process()
last_process = world.time
if(!loc || !fired || !trajectory)
fired = FALSE
return PROCESS_KILL
if(paused || !isturf(loc))
last_projectile_move += world.time - last_process //Compensates for pausing, so it doesn't become a hitscan projectile when unpaused from charged up ticks.
return
var/elapsed_time_deciseconds = (world.time - last_projectile_move) + time_offset
time_offset = 0
var/required_moves = speed > 0? FLOOR(elapsed_time_deciseconds / speed, 1) : MOVES_HITSCAN //Would be better if a 0 speed made hitscan but everyone hates those so I can't make it a universal system :<
if(required_moves == MOVES_HITSCAN)
required_moves = SSprojectiles.global_max_tick_moves
else
if(required_moves > SSprojectiles.global_max_tick_moves)
var/overrun = required_moves - SSprojectiles.global_max_tick_moves
required_moves = SSprojectiles.global_max_tick_moves
time_offset += overrun * speed
time_offset += MODULUS(elapsed_time_deciseconds, speed)
for(var/i in 1 to required_moves)
pixel_move(1, FALSE)
/obj/projectile/proc/fire(angle, atom/direct_target)
if(fired_from)
SEND_SIGNAL(fired_from, COMSIG_PROJECTILE_BEFORE_FIRE, src, original)
//If no angle needs to resolve it from xo/yo!
if(!log_override && firer && original)
log_combat(firer, original, "fired at", src, "from [get_area_name(src, TRUE)]")
if(direct_target)
if(prehit(direct_target))
direct_target.bullet_act(src, def_zone)
qdel(src)
return
if(isnum(angle))
setAngle(angle)
if(spread)
setAngle(Angle + ((rand() - 0.5) * spread))
var/turf/starting = get_turf(src)
if(isnull(Angle)) //Try to resolve through offsets if there's no angle set.
if(isnull(xo) || isnull(yo))
stack_trace("WARNING: Projectile [type] deleted due to being unable to resolve a target after angle was null!")
qdel(src)
return
var/turf/target = locate(clamp(starting + xo, 1, world.maxx), clamp(starting + yo, 1, world.maxy), starting.z)
setAngle(Get_Angle(src, target))
original_angle = Angle
if(!nondirectional_sprite)
var/matrix/M = new
M.Turn(Angle)
transform = M
trajectory_ignore_forcemove = TRUE
forceMove(starting)
trajectory_ignore_forcemove = FALSE
trajectory = new(starting.x, starting.y, starting.z, pixel_x, pixel_y, Angle, SSprojectiles.global_pixel_speed)
last_projectile_move = world.time
fired = TRUE
if(hitscan)
process_hitscan()
if(!(datum_flags & DF_ISPROCESSING))
START_PROCESSING(SSprojectiles, src)
pixel_move(1, FALSE) //move it now!
/obj/projectile/proc/setAngle(new_angle) //wrapper for overrides.
Angle = new_angle
if(!nondirectional_sprite)
var/matrix/M = new
M.Turn(Angle)
transform = M
if(trajectory)
trajectory.set_angle(new_angle)
return TRUE
/obj/projectile/forceMove(atom/target)
if(!isloc(target) || !isloc(loc) || !z)
return ..()
var/zc = target.z != z
var/old = loc
if(zc)
before_z_change(old, target)
. = ..()
if(trajectory && !trajectory_ignore_forcemove && isturf(target))
if(hitscan)
finalize_hitscan_and_generate_tracers(FALSE)
trajectory.initialize_location(target.x, target.y, target.z, 0, 0)
if(hitscan)
record_hitscan_start(RETURN_PRECISE_POINT(src))
if(zc)
after_z_change(old, target)
/obj/projectile/proc/after_z_change(atom/olcloc, atom/newloc)
/obj/projectile/proc/before_z_change(atom/oldloc, atom/newloc)
/obj/projectile/vv_edit_var(var_name, var_value)
switch(var_name)
if(NAMEOF(src, Angle))
setAngle(var_value)
return TRUE
else
return ..()
/obj/projectile/proc/set_pixel_speed(new_speed)
if(trajectory)
trajectory.set_speed(new_speed)
return TRUE
return FALSE
/obj/projectile/proc/record_hitscan_start(datum/point/pcache)
if(pcache)
beam_segments = list()
beam_index = pcache
beam_segments[beam_index] = null //record start.
/obj/projectile/proc/process_hitscan()
var/safety = range * 3
record_hitscan_start(RETURN_POINT_VECTOR_INCREMENT(src, Angle, MUZZLE_EFFECT_PIXEL_INCREMENT, 1))
while(loc && !QDELETED(src))
if(paused)
stoplag(1)
continue
if(safety-- <= 0)
if(loc)
Bump(loc)
if(!QDELETED(src))
qdel(src)
return //Kill!
pixel_move(1, TRUE)
/obj/projectile/proc/pixel_move(trajectory_multiplier, hitscanning = FALSE)
if(!loc || !trajectory)
return
last_projectile_move = world.time
if(!nondirectional_sprite && !hitscanning)
var/matrix/M = new
M.Turn(Angle)
transform = M
if(homing)
process_homing()
var/forcemoved = FALSE
for(var/i in 1 to SSprojectiles.global_iterations_per_move)
if(QDELETED(src))
return
trajectory.increment(trajectory_multiplier)
var/turf/T = trajectory.return_turf()
if(!istype(T))
qdel(src)
return
if(T.z != loc.z)
var/old = loc
before_z_change(loc, T)
trajectory_ignore_forcemove = TRUE
forceMove(T)
trajectory_ignore_forcemove = FALSE
after_z_change(old, loc)
if(!hitscanning)
pixel_x = trajectory.return_px()
pixel_y = trajectory.return_py()
forcemoved = TRUE
else if(T != loc)
step_towards(src, T)
hitscan_last = T
if(!hitscanning && !forcemoved)
pixel_x = trajectory.return_px() - trajectory.mpx * trajectory_multiplier * SSprojectiles.global_iterations_per_move
pixel_y = trajectory.return_py() - trajectory.mpy * trajectory_multiplier * SSprojectiles.global_iterations_per_move
animate(src, pixel_x = trajectory.return_px(), pixel_y = trajectory.return_py(), time = 0.1 SECONDS, flags = ANIMATION_END_NOW)
Range()
/obj/projectile/proc/process_homing() //may need speeding up in the future performance wise.
if(!homing_target)
return FALSE
var/datum/point/PT = RETURN_PRECISE_POINT(homing_target)
PT.x += clamp(homing_offset_x, 1, world.maxx)
PT.y += clamp(homing_offset_y, 1, world.maxy)
var/angle = closer_angle_difference(Angle, angle_between_points(RETURN_PRECISE_POINT(src), PT))
setAngle(Angle + clamp(homing_away ? -angle : angle, -homing_turn_speed, homing_turn_speed))
/obj/projectile/proc/set_homing_target(atom/A)
if(!A || (!isturf(A) && !isturf(A.loc)))
return FALSE
homing = TRUE
homing_target = A
homing_offset_x = rand(homing_inaccuracy_min, homing_inaccuracy_max)
homing_offset_y = rand(homing_inaccuracy_min, homing_inaccuracy_max)
if(prob(50))
homing_offset_x = -homing_offset_x
if(prob(50))
homing_offset_y = -homing_offset_y
//Returns true if the target atom is on our current turf and above the right layer
//If direct target is true it's the originally clicked target.
/obj/projectile/proc/can_hit_target(atom/target, list/passthrough, direct_target = FALSE, ignore_loc = FALSE)
if(QDELETED(target))
return FALSE
if(!ignore_source_check && firer)
var/mob/M = firer
if(isliving(M))
var/mob/living/L = M
if((target in L.hasparasites()) && target.loc == L.loc)
return FALSE
if((target == firer) || ((target == firer.loc) && (ismecha(firer.loc) || isspacepod(firer.loc))) || !ismovable(M) || (target in firer.buckled_mobs) || (istype(M) && (M.buckled == target))) //cannot shoot yourself or your mech // yogs - or your spacepod)
return FALSE
if(ignored_factions?.len && ismob(target) && !direct_target)
var/mob/target_mob = target
if(faction_check(target_mob.faction, ignored_factions))
return FALSE
if(!ignore_loc && (loc != target.loc))
return FALSE
if(target in passthrough)
return FALSE
if(target.density) //This thing blocks projectiles, hit it regardless of layer/mob stuns/etc.
return TRUE
if(!isliving(target))
if(target.layer < hit_threshhold)
return FALSE
else
var/mob/living/L = target
if(!direct_target && !ignore_crit)
if(!CHECK_BITFIELD(L.mobility_flags, MOBILITY_STAND) && (L in range(1, starting))) //if we're shooting over someone who's prone and nearby bc formations are cool and not going to be unbalanced
return FALSE
if(!CHECK_BITFIELD(L.mobility_flags, MOBILITY_USE | MOBILITY_STAND | MOBILITY_MOVE) || !(L.stat == CONSCIOUS)) //If they're able to 1. stand or 2. use items or 3. move, AND they are not softcrit, they are not stunned enough to dodge projectiles passing over.
return FALSE
return TRUE
//Spread is FORCED!
/obj/projectile/proc/preparePixelProjectile(atom/target, atom/source, params, spread = 0)
var/turf/curloc = get_turf(source)
var/turf/targloc = get_turf(target)
trajectory_ignore_forcemove = TRUE
forceMove(get_turf(source))
trajectory_ignore_forcemove = FALSE
starting = get_turf(source)
original = target
if(targloc || !params)
yo = targloc.y - curloc.y
xo = targloc.x - curloc.x
setAngle(Get_Angle(src, targloc) + spread)
if(isliving(source) && params)
var/list/calculated = calculate_projectile_angle_and_pixel_offsets(source, params)
p_x = calculated[2]
p_y = calculated[3]
setAngle(calculated[1] + spread)
else if(targloc)
yo = targloc.y - curloc.y
xo = targloc.x - curloc.x
setAngle(Get_Angle(src, targloc) + spread)
else
stack_trace("WARNING: Projectile [type] fired without either mouse parameters, or a target atom to aim at!")
qdel(src)
/proc/calculate_projectile_angle_and_pixel_offsets(mob/user, params)
var/list/mouse_control = params2list(params)
var/p_x = 0
var/p_y = 0
var/angle = 0
if(mouse_control["icon-x"])
p_x = text2num(mouse_control["icon-x"])
if(mouse_control["icon-y"])
p_y = text2num(mouse_control["icon-y"])
if(mouse_control["screen-loc"])
//Split screen-loc up into X+Pixel_X and Y+Pixel_Y
var/list/screen_loc_params = splittext(mouse_control["screen-loc"], ",")
//Split X+Pixel_X up into list(X, Pixel_X)
var/list/screen_loc_X = splittext(screen_loc_params[1],":")
//Split Y+Pixel_Y up into list(Y, Pixel_Y)
var/list/screen_loc_Y = splittext(screen_loc_params[2],":")
var/x = text2num(screen_loc_X[1]) * 32 + text2num(screen_loc_X[2]) - 32
var/y = text2num(screen_loc_Y[1]) * 32 + text2num(screen_loc_Y[2]) - 32
//Calculate the "resolution" of screen based on client's view and world's icon size. This will work if the user can view more tiles than average.
var/list/screenview = view_to_pixels(user.client.view)
var/ox = round(screenview[1] / 2) - user.client.pixel_x //"origin" x
var/oy = round(screenview[2] / 2) - user.client.pixel_y //"origin" y
angle = ATAN2(y - oy, x - ox)
return list(angle, p_x, p_y)
/obj/projectile/proc/on_entered(datum/source, atom/movable/AM, ...) //A mob moving on a tile with a projectile is hit by it.
if(isliving(AM) && !(pass_flags & PASSMOB))
var/mob/living/L = AM
if(can_hit_target(L, impacted, (AM == original)))
Bump(AM)
/obj/projectile/Move(atom/newloc, dir = NONE)
. = ..()
if(.)
if(temporary_unstoppable_movement)
temporary_unstoppable_movement = FALSE
DISABLE_BITFIELD(movement_type, PHASING)
if(fired && can_hit_target(original, impacted, TRUE))
Bump(original)
/obj/projectile/Destroy()
if(hitscan)
finalize_hitscan_and_generate_tracers()
STOP_PROCESSING(SSprojectiles, src)
cleanup_beam_segments()
qdel(trajectory)
return ..()
/obj/projectile/proc/cleanup_beam_segments()
QDEL_LIST_ASSOC(beam_segments)
beam_segments = list()
qdel(beam_index)
/obj/projectile/proc/finalize_hitscan_and_generate_tracers(impacting = TRUE)
if(trajectory && beam_index)
var/datum/point/pcache = trajectory.copy_to()
beam_segments[beam_index] = pcache
generate_hitscan_tracers(null, null, impacting)
/obj/projectile/proc/generate_hitscan_tracers(cleanup = TRUE, duration = 3, impacting = TRUE)
if(!length(beam_segments))
return
if(tracer_type)
var/tempref = REF(src)
for(var/datum/point/p in beam_segments)
generate_tracer_between_points(p, beam_segments[p], tracer_type, color, duration, hitscan_light_range, hitscan_light_color_override, hitscan_light_intensity, tempref)
if(muzzle_type && duration > 0)
var/datum/point/p = beam_segments[1]
var/atom/movable/thing = new muzzle_type
p.move_atom_to_src(thing)
var/matrix/M = new
M.Turn(original_angle)
thing.transform = M
thing.color = color
thing.set_light(muzzle_flash_range, muzzle_flash_intensity, muzzle_flash_color_override? muzzle_flash_color_override : color)
QDEL_IN(thing, duration)
if(impacting && impact_type && duration > 0)
var/datum/point/p = beam_segments[beam_segments[beam_segments.len]]
var/atom/movable/thing = new impact_type
p.move_atom_to_src(thing)
var/matrix/M = new
M.Turn(Angle)
thing.transform = M
thing.color = color
thing.set_light(impact_light_range, impact_light_intensity, impact_light_color_override? impact_light_color_override : color)
QDEL_IN(thing, duration)
if(cleanup)
cleanup_beam_segments()
/obj/projectile/experience_pressure_difference()
return