From ca10fa6d7a70f98e6010c25b5d5451ffa0015308 Mon Sep 17 00:00:00 2001
From: kevinz000 <2003111+kevinz000@users.noreply.github.com>
Date: Thu, 22 Mar 2018 13:39:26 -0700
Subject: [PATCH] Spell Cards (#36147)
Adds spell cards. They're a wizard spell that shoots a burst of 5 semi-accurate homing cards.
Projectiles now have a homing framework, complete with some variant of simulated inaccuracy.
The said wizard spell will make use of a new mob component, that allows that mob to select targets by moving their cursor near them. It will give a visual and lock onto the nearest mob to the cursor, allowing the homing projectiles to target on the locked on mob/object.
Removes colliding variable from projectiles - We never used it after Bump was refactored to Collide.
Images soon when I get the lockon datum-components to work.
---
code/__DEFINES/maths.dm | 30 +--
code/__HELPERS/_lists.dm | 2 +
code/__HELPERS/game.dm | 38 ---
code/__HELPERS/mouse_control.dm | 50 ++++
code/__HELPERS/unsorted.dm | 51 ++++
code/__HELPERS/unused.dm | 39 +++
code/_onclick/autoclick.dm | 6 +-
code/datums/callback.dm | 2 +-
code/datums/components/lockon_aiming.dm | 243 ++++++++++++++++++
code/datums/position_point_vector.dm | 26 +-
code/datums/weakrefs.dm | 3 +
code/modules/antagonists/cult/cult_items.dm | 2 -
.../antagonists/wizard/equipment/spellbook.dm | 4 +
.../projectiles/guns/misc/beam_rifle.dm | 6 +-
code/modules/projectiles/projectile.dm | 52 ++--
.../projectiles/projectile/magic/spellcard.dm | 6 +
code/modules/spells/spell_types/aimed.dm | 84 +++++-
icons/obj/projectiles.dmi | Bin 116179 -> 116484 bytes
tgstation.dme | 3 +
19 files changed, 548 insertions(+), 99 deletions(-)
create mode 100644 code/__HELPERS/mouse_control.dm
create mode 100644 code/__HELPERS/unused.dm
create mode 100644 code/datums/components/lockon_aiming.dm
create mode 100644 code/modules/projectiles/projectile/magic/spellcard.dm
diff --git a/code/__DEFINES/maths.dm b/code/__DEFINES/maths.dm
index 6ca1107619b..b4a44f6d4b0 100644
--- a/code/__DEFINES/maths.dm
+++ b/code/__DEFINES/maths.dm
@@ -116,6 +116,20 @@
#define GET_ANGLE_OF_INCIDENCE(face, input) (MODULUS((face) - (input), 360))
+//Finds the shortest angle that angle A has to change to get to angle B. Aka, whether to move clock or counterclockwise.
+/proc/closer_angle_difference(a, b)
+ if(!isnum(a) || !isnum(b))
+ return
+ a = SIMPLIFY_DEGREES(a)
+ b = SIMPLIFY_DEGREES(b)
+ var/inc = b - a
+ if(inc < 0)
+ inc += 360
+ var/dec = a - b
+ if(dec < 0)
+ dec += 360
+ . = inc > dec? -dec : inc
+
//A logarithm that converts an integer to a number scaled between 0 and 1.
//Currently, this is used for hydroponics-produce sprite transforming, but could be useful for other transform functions.
#define TRANSFORM_USING_VARIABLE(input, max) ( sin((90*(input))/(max))**2 )
@@ -146,22 +160,6 @@
return (mean + stddev * R1)
#undef ACCURACY
-/proc/mouse_angle_from_client(client/client)
- var/list/mouse_control = params2list(client.mouseParams)
- if(mouse_control["screen-loc"] && client)
- var/list/screen_loc_params = splittext(mouse_control["screen-loc"], ",")
- var/list/screen_loc_X = splittext(screen_loc_params[1],":")
- 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)
- var/list/screenview = getviewsize(client.view)
- var/screenviewX = screenview[1] * world.icon_size
- var/screenviewY = screenview[2] * world.icon_size
- var/ox = round(screenviewX/2) - client.pixel_x //"origin" x
- var/oy = round(screenviewY/2) - client.pixel_y //"origin" y
- var/angle = SIMPLIFY_DEGREES(ATAN2(y - oy, x - ox))
- return angle
-
/proc/get_turf_in_angle(angle, turf/starting, increments)
var/pixel_x = 0
var/pixel_y = 0
diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm
index 91eee45b4d2..4ba5d559798 100644
--- a/code/__HELPERS/_lists.dm
+++ b/code/__HELPERS/_lists.dm
@@ -13,6 +13,8 @@
#define UNSETEMPTY(L) if (L && !L.len) L = null
#define LAZYREMOVE(L, I) if(L) { L -= I; if(!L.len) { L = null; } }
#define LAZYADD(L, I) if(!L) { L = list(); } L += I;
+#define LAZYOR(L, I) if(!L) { L = list(); } L |= I;
+#define LAZYFIND(L, V) L ? L.Find(V) : 0
#define LAZYACCESS(L, I) (L ? (isnum(I) ? (I > 0 && I <= L.len ? L[I] : null) : L[I]) : null)
#define LAZYSET(L, K, V) if(!L) { L = list(); } L[K] = V;
#define LAZYLEN(L) length(L)
diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index 26b5ac65cc9..1dfacc0149c 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -387,44 +387,6 @@
active_players++
return active_players
-/datum/projectile_data
- var/src_x
- var/src_y
- var/time
- var/distance
- var/power_x
- var/power_y
- var/dest_x
- var/dest_y
-
-/datum/projectile_data/New(var/src_x, var/src_y, var/time, var/distance, \
- var/power_x, var/power_y, var/dest_x, var/dest_y)
- src.src_x = src_x
- src.src_y = src_y
- src.time = time
- src.distance = distance
- src.power_x = power_x
- src.power_y = power_y
- src.dest_x = dest_x
- src.dest_y = dest_y
-
-/proc/projectile_trajectory(src_x, src_y, rotation, angle, power)
-
- // returns the destination (Vx,y) that a projectile shot at [src_x], [src_y], with an angle of [angle],
- // rotated at [rotation] and with the power of [power]
- // Thanks to VistaPOWA for this function
-
- var/power_x = power * cos(angle)
- var/power_y = power * sin(angle)
- var/time = 2* power_y / 10 //10 = g
-
- var/distance = time * power_x
-
- var/dest_x = src_x + distance*sin(rotation);
- var/dest_y = src_y + distance*cos(rotation);
-
- return new /datum/projectile_data(src_x, src_y, time, distance, power_x, power_y, dest_x, dest_y)
-
/proc/showCandidatePollWindow(mob/M, poll_time, Question, list/candidates, ignore_category, time_passed, flashwindow = TRUE)
set waitfor = 0
diff --git a/code/__HELPERS/mouse_control.dm b/code/__HELPERS/mouse_control.dm
new file mode 100644
index 00000000000..24e40cc355b
--- /dev/null
+++ b/code/__HELPERS/mouse_control.dm
@@ -0,0 +1,50 @@
+/proc/mouse_angle_from_client(client/client)
+ var/list/mouse_control = params2list(client.mouseParams)
+ if(mouse_control["screen-loc"] && client)
+ var/list/screen_loc_params = splittext(mouse_control["screen-loc"], ",")
+ var/list/screen_loc_X = splittext(screen_loc_params[1],":")
+ 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)
+ var/list/screenview = getviewsize(client.view)
+ var/screenviewX = screenview[1] * world.icon_size
+ var/screenviewY = screenview[2] * world.icon_size
+ var/ox = round(screenviewX/2) - client.pixel_x //"origin" x
+ var/oy = round(screenviewY/2) - client.pixel_y //"origin" y
+ var/angle = SIMPLIFY_DEGREES(ATAN2(y - oy, x - ox))
+ return angle
+
+//Wow, specific name!
+/proc/mouse_absolute_datum_map_position_from_client(client/client)
+ if(!isloc(client.mob.loc))
+ return
+ var/list/mouse_control = params2list(client.mouseParams)
+ var/cx = client.mob.x
+ var/cy = client.mob.y
+ var/cz = client.mob.z
+ if(mouse_control["screen-loc"])
+ var/x = 0
+ var/y = 0
+ var/z = 0
+ var/p_x = 0
+ var/p_y = 0
+ //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/sx = text2num(screen_loc_X[1])
+ var/sy = text2num(screen_loc_Y[1])
+ //Get the resolution of the client's current screen size.
+ var/list/screenview = getviewsize(client.view)
+ var/svx = screenview[1]
+ var/svy = screenview[2]
+ var/cox = round((svx - 1) / 2)
+ var/coy = round((svy - 1) / 2)
+ x = cx + (sx - 1 - cox)
+ y = cy + (sy - 1 - coy)
+ z = cz
+ p_x = text2num(screen_loc_X[2])
+ p_y = text2num(screen_loc_Y[2])
+ return new /datum/position(x, y, z, p_x, p_y)
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index a63ff6bdf01..0a212e36a1f 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -1572,3 +1572,54 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new)
/proc/get_random_drink()
return pick(subtypesof(/obj/item/reagent_containers/food/drinks))
+
+//For these two procs refs MUST be ref = TRUE format like typecaches!
+/proc/weakref_filter_list(list/things, list/refs)
+ if(!islist(things) || !islist(refs))
+ return
+ if(!refs.len)
+ return things
+ if(things.len > refs.len)
+ var/list/f = list()
+ for(var/i in refs)
+ var/datum/weakref/r = i
+ var/datum/d = r.resolve()
+ if(d)
+ f |= d
+ return things & f
+
+ else
+ . = list()
+ for(var/i in things)
+ if(!refs[WEAKREF(i)])
+ continue
+ . |= i
+
+/proc/weakref_filter_list_reverse(list/things, list/refs)
+ if(!islist(things) || !islist(refs))
+ return
+ if(!refs.len)
+ return things
+ if(things.len > refs.len)
+ var/list/f = list()
+ for(var/i in refs)
+ var/datum/weakref/r = i
+ var/datum/d = r.resolve()
+ if(d)
+ f |= d
+
+ return things - f
+ else
+ . = list()
+ for(var/i in things)
+ if(refs[WEAKREF(i)])
+ continue
+ . |= i
+
+/proc/special_list_filter(list/L, datum/callback/condition)
+ if(!islist(L) || !length(L) || !istype(condition))
+ return list()
+ . = list()
+ for(var/i in L)
+ if(condition.Invoke(i))
+ . |= i
diff --git a/code/__HELPERS/unused.dm b/code/__HELPERS/unused.dm
new file mode 100644
index 00000000000..8b590eb2bd2
--- /dev/null
+++ b/code/__HELPERS/unused.dm
@@ -0,0 +1,39 @@
+
+
+/datum/projectile_data
+ var/src_x
+ var/src_y
+ var/time
+ var/distance
+ var/power_x
+ var/power_y
+ var/dest_x
+ var/dest_y
+
+/datum/projectile_data/New(var/src_x, var/src_y, var/time, var/distance, \
+ var/power_x, var/power_y, var/dest_x, var/dest_y)
+ src.src_x = src_x
+ src.src_y = src_y
+ src.time = time
+ src.distance = distance
+ src.power_x = power_x
+ src.power_y = power_y
+ src.dest_x = dest_x
+ src.dest_y = dest_y
+
+/proc/projectile_trajectory(src_x, src_y, rotation, angle, power)
+
+ // returns the destination (Vx,y) that a projectile shot at [src_x], [src_y], with an angle of [angle],
+ // rotated at [rotation] and with the power of [power]
+ // Thanks to VistaPOWA for this function
+
+ var/power_x = power * cos(angle)
+ var/power_y = power * sin(angle)
+ var/time = 2* power_y / 10 //10 = g
+
+ var/distance = time * power_x
+
+ var/dest_x = src_x + distance*sin(rotation);
+ var/dest_y = src_y + distance*cos(rotation);
+
+ return new /datum/projectile_data(src_x, src_y, time, distance, power_x, power_y, dest_x, dest_y)
diff --git a/code/_onclick/autoclick.dm b/code/_onclick/autoclick.dm
index 7a5ba0a1899..bd978a921d6 100644
--- a/code/_onclick/autoclick.dm
+++ b/code/_onclick/autoclick.dm
@@ -77,10 +77,10 @@
mouseObject = object
mouseControlObject = control
if(mob && LAZYLEN(mob.mousemove_intercept_objects))
- for(var/obj/item/I in mob.mousemove_intercept_objects)
- I.onMouseMove(object, location, control, params)
+ for(var/datum/D in mob.mousemove_intercept_objects)
+ D.onMouseMove(object, location, control, params)
-/obj/item/proc/onMouseMove(object, location, control, params)
+/datum/proc/onMouseMove(object, location, control, params)
return
/client/MouseDrag(src_object,atom/over_object,src_location,over_location,src_control,over_control,params)
diff --git a/code/datums/callback.dm b/code/datums/callback.dm
index c603be85b70..1a26052cc99 100644
--- a/code/datums/callback.dm
+++ b/code/datums/callback.dm
@@ -173,7 +173,7 @@
var/datum/callback_select/CS = new(count, savereturns)
for (var/i in 1 to count)
CS.invoke_callback(i, callbacks[i], callback_args[i], savereturns)
-
+
while(CS.pendingcount)
sleep(resolution*world.tick_lag)
return CS.finished
diff --git a/code/datums/components/lockon_aiming.dm b/code/datums/components/lockon_aiming.dm
new file mode 100644
index 00000000000..017e7ea8ebc
--- /dev/null
+++ b/code/datums/components/lockon_aiming.dm
@@ -0,0 +1,243 @@
+#define LOCKON_AIMING_MAX_CURSOR_RADIUS 7
+#define LOCKON_IGNORE_RESULT "ignore_my_result"
+#define LOCKON_RANGING_BREAK_CHECK if(current_ranging_id != this_id){return LOCKON_IGNORE_RESULT}
+
+/datum/component/lockon_aiming
+ dupe_mode = COMPONENT_DUPE_ALLOWED
+ var/lock_icon = 'icons/mob/blob.dmi'
+ var/lock_icon_state = "marker"
+ var/mutable_appearance/lock_appearance
+ var/list/image/lock_images
+ var/list/target_typecache
+ var/list/immune_weakrefs //list(weakref = TRUE)
+ var/mob_stat_check = TRUE //if a potential target is a mob make sure it's conscious!
+ var/lock_amount = 1
+ var/lock_cursor_range = 5
+ var/list/locked_weakrefs
+ var/update_disabled = FALSE
+ var/current_ranging_id = 0
+ var/list/last_location
+ var/datum/callback/on_lock
+ var/datum/callback/can_target_callback
+
+/datum/component/lockon_aiming/Initialize(range, list/typecache, amount, list/immune, datum/callback/when_locked, icon, icon_state, datum/callback/target_callback)
+ if(!ismob(parent))
+ . = COMPONENT_INCOMPATIBLE
+ CRASH("Lockon aiming component attempted to be added to a non mob!")
+ if(target_callback)
+ can_target_callback = target_callback
+ else
+ can_target_callback = CALLBACK(src, .proc/can_target)
+ if(range)
+ lock_cursor_range = range
+ if(typecache)
+ target_typecache = typecache
+ if(amount)
+ lock_amount = amount
+ immune_weakrefs = list(WEAKREF(parent) = TRUE) //Manually take this out if you want..
+ if(immune)
+ for(var/i in immune)
+ if(isweakref(i))
+ immune_weakrefs[i] = TRUE
+ else if(isatom(i))
+ immune_weakrefs[WEAKREF(i)] = TRUE
+ if(when_locked)
+ on_lock = when_locked
+ if(icon)
+ lock_icon = icon
+ if(icon_state)
+ lock_icon_state = icon_state
+ generate_lock_visuals()
+ var/mob/M = parent
+ LAZYOR(M.mousemove_intercept_objects, src)
+ START_PROCESSING(SSfastprocess, src)
+
+/datum/component/lockon_aiming/Destroy()
+ var/mob/M = parent
+ clear_visuals()
+ LAZYREMOVE(M.mousemove_intercept_objects, src)
+ STOP_PROCESSING(SSfastprocess, src)
+ return ..()
+
+/datum/component/lockon_aiming/proc/show_visuals()
+ LAZYINITLIST(lock_images)
+ var/mob/M = parent
+ if(!M.client)
+ return
+ for(var/i in locked_weakrefs)
+ var/datum/weakref/R = i
+ var/atom/A = R.resolve()
+ if(!A)
+ continue //It'll be cleared by processing.
+ var/image/I = new
+ I.appearance = lock_appearance
+ I.loc = A
+ M.client.images |= I
+ lock_images |= I
+
+/datum/component/lockon_aiming/proc/clear_visuals()
+ var/mob/M = parent
+ if(!M.client)
+ return
+ if(!lock_images)
+ return
+ for(var/i in lock_images)
+ M.client.images -= i
+ qdel(i)
+ lock_images.Cut()
+
+/datum/component/lockon_aiming/proc/refresh_visuals()
+ clear_visuals()
+ show_visuals()
+
+/datum/component/lockon_aiming/proc/generate_lock_visuals()
+ lock_appearance = mutable_appearance(icon = lock_icon, icon_state = lock_icon_state, layer = FLOAT_LAYER)
+
+/datum/component/lockon_aiming/proc/unlock_all(refresh_vis = TRUE)
+ LAZYCLEARLIST(locked_weakrefs)
+ if(refresh_vis)
+ refresh_visuals()
+
+/datum/component/lockon_aiming/proc/unlock(atom/A, refresh_vis = TRUE)
+ if(!A.weak_reference)
+ return
+ LAZYREMOVE(locked_weakrefs, A.weak_reference)
+ if(refresh_vis)
+ refresh_visuals()
+
+/datum/component/lockon_aiming/proc/lock(atom/A, refresh_vis = TRUE)
+ LAZYOR(locked_weakrefs, WEAKREF(A))
+ if(refresh_vis)
+ refresh_visuals()
+
+/datum/component/lockon_aiming/proc/add_immune_atom(atom/A)
+ var/datum/weakref/R = WEAKREF(A)
+ if(immune_weakrefs && (immune_weakrefs[R]))
+ return
+ LAZYSET(immune_weakrefs, R, TRUE)
+
+/datum/component/lockon_aiming/proc/remove_immune_atom(atom/A)
+ if(!A.weak_reference || !immune_weakrefs) //if A doesn't have a weakref how did it get on the immunity list?
+ return
+ LAZYREMOVE(immune_weakrefs, A.weak_reference)
+
+/datum/component/lockon_aiming/onMouseMove(object,location,control,params)
+ var/mob/M = parent
+ if(!istype(M) || !M.client)
+ return
+ var/datum/position/P = mouse_absolute_datum_map_position_from_client(M.client)
+ if(!P)
+ return
+ var/turf/T = P.return_turf()
+ LAZYINITLIST(last_location)
+ if(length(last_location) == 3 && last_location[1] == T.x && last_location[2] == T.y && last_location[3] == T.z)
+ return //Same turf, don't bother.
+ if(last_location)
+ last_location.Cut()
+ else
+ last_location = list()
+ last_location.len = 3
+ last_location[1] = T.x
+ last_location[2] = T.y
+ last_location[3] = T.z
+ autolock()
+
+/datum/component/lockon_aiming/process()
+ if(update_disabled)
+ return
+ if(!last_location)
+ return
+ var/changed = FALSE
+ for(var/i in locked_weakrefs)
+ var/datum/weakref/R = i
+ if(istype(R))
+ var/atom/thing = R.resolve()
+ if(!istype(thing) || (get_dist(thing, locate(last_location[1], last_location[2], last_location[3])) > lock_cursor_range))
+ unlock(R)
+ changed = TRUE
+ else
+ unlock(R)
+ changed = TRUE
+ if(changed)
+ autolock()
+
+/datum/component/lockon_aiming/proc/autolock()
+ var/mob/M = parent
+ if(!M.client)
+ return FALSE
+ var/datum/position/current = mouse_absolute_datum_map_position_from_client(M.client)
+ var/turf/target = current.return_turf()
+ var/list/atom/targets = get_nearest(target, target_typecache, lock_amount, lock_cursor_range)
+ if(targets == LOCKON_IGNORE_RESULT)
+ return
+ unlock_all(FALSE)
+ for(var/i in targets)
+ if(immune_weakrefs[WEAKREF(i)])
+ continue
+ lock(i, FALSE)
+ refresh_visuals()
+ on_lock.Invoke(locked_weakrefs)
+
+/datum/component/lockon_aiming/proc/can_target(atom/A)
+ var/mob/M = A
+ return is_type_in_typecache(A, target_typecache) && !(ismob(A) && mob_stat_check && M.stat != CONSCIOUS) && !immune_weakrefs[WEAKREF(A)]
+
+/datum/component/lockon_aiming/proc/get_nearest(turf/T, list/typecache, amount, range)
+ current_ranging_id++
+ var/this_id = current_ranging_id
+ var/list/L = list()
+ var/turf/center = get_turf(T)
+ if(amount < 1 || range < 0 || !istype(center) || !islist(typecache))
+ return
+ if(range == 0)
+ return typecache_filter_list(T.contents + T, typecache)
+ var/x = 0
+ var/y = 0
+ var/cd = 0
+ while(cd <= range)
+ x = center.x - cd + 1
+ y = center.y + cd
+ LOCKON_RANGING_BREAK_CHECK
+ for(x in x to center.x + cd)
+ T = locate(x, y, center.z)
+ if(T)
+ L |= special_list_filter(T.contents, can_target_callback)
+ if(L.len >= amount)
+ L.Cut(amount+1)
+ return L
+ LOCKON_RANGING_BREAK_CHECK
+ y = center.y + cd - 1
+ x = center.x + cd
+ for(y in center.y - cd to y)
+ T = locate(x, y, center.z)
+ if(T)
+ L |= special_list_filter(T.contents, can_target_callback)
+ if(L.len >= amount)
+ L.Cut(amount+1)
+ return L
+ LOCKON_RANGING_BREAK_CHECK
+ y = center.y - cd
+ x = center.x + cd - 1
+ for(x in center.x - cd to x)
+ T = locate(x, y, center.z)
+ if(T)
+ L |= special_list_filter(T.contents, can_target_callback)
+ if(L.len >= amount)
+ L.Cut(amount+1)
+ return L
+ LOCKON_RANGING_BREAK_CHECK
+ y = center.y - cd + 1
+ x = center.x - cd
+ for(y in y to center.y + cd)
+ T = locate(x, y, center.z)
+ if(T)
+ L |= special_list_filter(T.contents, can_target_callback)
+ if(L.len >= amount)
+ L.Cut(amount+1)
+ return L
+ LOCKON_RANGING_BREAK_CHECK
+ cd++
+ CHECK_TICK
+
+/datum/component/lockon_aiming/OnTransfer(datum/new_parent)
+ CRASH("Warning: Lockon aiming component transfer attempted, but transfer behavior is not implemented!")
diff --git a/code/datums/position_point_vector.dm b/code/datums/position_point_vector.dm
index 63b47745508..33facca9a10 100644
--- a/code/datums/position_point_vector.dm
+++ b/code/datums/position_point_vector.dm
@@ -9,6 +9,19 @@
#define RETURN_POINT_VECTOR(ATOM, ANGLE, SPEED) {new /datum/point/vector(ATOM, null, null, null, null, ANGLE, SPEED)}
#define RETURN_POINT_VECTOR_INCREMENT(ATOM, ANGLE, SPEED, AMT) {new /datum/point/vector(ATOM, null, null, null, null, ANGLE, SPEED, AMT)}
+/proc/point_midpoint_points(datum/point/a, datum/point/b) //Obviously will not support multiZ calculations! Same for the two below.
+ var/datum/point/P = new
+ P.x = a.x + (b.x - a.x) / 2
+ P.y = a.y + (b.y - a.y) / 2
+ P.z = a.z
+ return P
+
+/proc/pixel_length_between_points(datum/point/a, datum/point/b)
+ return sqrt(((b.x - a.x) ** 2) + ((b.y - a.y) ** 2))
+
+/proc/angle_between_points(datum/point/a, datum/point/b)
+ return ATAN2((b.y - a.y), (b.x - a.x))
+
/datum/position //For positions with map x/y/z and pixel x/y so you don't have to return lists. Could use addition/subtraction in the future I guess.
var/x = 0
var/y = 0
@@ -53,19 +66,6 @@
/datum/position/proc/return_point()
return new /datum/point(src)
-/proc/point_midpoint_points(datum/point/a, datum/point/b) //Obviously will not support multiZ calculations! Same for the two below.
- var/datum/point/P = new
- P.x = a.x + (b.x - a.x) / 2
- P.y = a.y + (b.y - a.y) / 2
- P.z = a.z
- return P
-
-/proc/pixel_length_between_points(datum/point/a, datum/point/b)
- return sqrt(((b.x - a.x) ** 2) + ((b.y - a.y) ** 2))
-
-/proc/angle_between_points(datum/point/a, datum/point/b)
- return ATAN2((b.y - a.y), (b.x - a.x))
-
/datum/point //A precise point on the map in absolute pixel locations based on world.icon_size. Pixels are FROM THE EDGE OF THE MAP!
var/x = 0
var/y = 0
diff --git a/code/datums/weakrefs.dm b/code/datums/weakrefs.dm
index f9602c02174..3c64d5f60d2 100644
--- a/code/datums/weakrefs.dm
+++ b/code/datums/weakrefs.dm
@@ -6,6 +6,9 @@
input.weak_reference = new /datum/weakref(input)
return input.weak_reference
+/datum/proc/create_weakref() //Forced creation for admin proccalls
+ return WEAKREF(src)
+
/datum/weakref
var/reference
diff --git a/code/modules/antagonists/cult/cult_items.dm b/code/modules/antagonists/cult/cult_items.dm
index 1b744863d83..30d150e3eeb 100644
--- a/code/modules/antagonists/cult/cult_items.dm
+++ b/code/modules/antagonists/cult/cult_items.dm
@@ -776,7 +776,6 @@
impact_effect_type = /obj/effect/temp_visual/dir_setting/bloodsplatter
/obj/item/projectile/magic/arcane_barrage/blood/Collide(atom/target)
- colliding = TRUE
var/turf/T = get_turf(target)
playsound(T, 'sound/effects/splat.ogg', 50, TRUE)
if(iscultist(target))
@@ -790,7 +789,6 @@
M.adjustHealth(-5)
new /obj/effect/temp_visual/cult/sparks(T)
qdel(src)
- colliding = FALSE
else
..()
diff --git a/code/modules/antagonists/wizard/equipment/spellbook.dm b/code/modules/antagonists/wizard/equipment/spellbook.dm
index 0ba2aac8b4e..55795afd82e 100644
--- a/code/modules/antagonists/wizard/equipment/spellbook.dm
+++ b/code/modules/antagonists/wizard/equipment/spellbook.dm
@@ -106,6 +106,10 @@
name = "Fireball"
spell_type = /obj/effect/proc_holder/spell/aimed/fireball
+/datum/spellbook_entry/spell_cards
+ name = "Spell Cards"
+ spell_type = /obj/effect/proc_holder/spell/aimed/spell_cards
+
/datum/spellbook_entry/rod_form
name = "Rod Form"
spell_type = /obj/effect/proc_holder/spell/targeted/rod_form
diff --git a/code/modules/projectiles/guns/misc/beam_rifle.dm b/code/modules/projectiles/guns/misc/beam_rifle.dm
index 80cbf113182..4edff70e6ca 100644
--- a/code/modules/projectiles/guns/misc/beam_rifle.dm
+++ b/code/modules/projectiles/guns/misc/beam_rifle.dm
@@ -254,9 +254,7 @@
current_user.setDir(SOUTH)
if(226 to 315)
current_user.setDir(WEST)
- var/difference = abs(lastangle - angle)
- if(difference > 350) //Too lazy to properly math, detects 360 --> 0 changes.
- difference = (lastangle > 350? ((360 - lastangle) + angle) : ((360 - angle) + lastangle))
+ var/difference = abs(closer_angle_difference(lastangle, angle))
delay_penalty(difference * aiming_time_increase_angle_multiplier)
lastangle = angle
@@ -292,7 +290,7 @@
current_user = null
if(istype(user))
current_user = user
- LAZYADD(current_user.mousemove_intercept_objects, src)
+ LAZYOR(current_user.mousemove_intercept_objects, src)
mobhook = user.AddComponent(/datum/component/redirect, list(COMSIG_MOVABLE_MOVED), CALLBACK(src, .proc/on_mob_move))
/obj/item/gun/energy/beam_rifle/onMouseDrag(src_object, over_object, src_location, over_location, params, mob)
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index 47d8cce7c4e..b71eb463367 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -46,8 +46,6 @@
var/ricochets_max = 2
var/ricochet_chance = 30
- var/colliding = FALSE //pause processing..
-
//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.
@@ -57,6 +55,15 @@
var/muzzle_type
var/impact_type
+ //Homing
+ var/homing = FALSE
+ 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
@@ -192,7 +199,6 @@
beam_segments[beam_index] = null
/obj/item/projectile/Collide(atom/A)
- colliding = TRUE
var/datum/point/pcache = trajectory.copy_to()
if(check_ricochet(A) && check_ricochet_flag(A) && ricochets < ricochets_max)
ricochets++
@@ -208,7 +214,6 @@
trajectory_ignore_forcemove = TRUE
forceMove(get_turf(A))
trajectory_ignore_forcemove = FALSE
- colliding = FALSE
return FALSE
var/distance = get_dist(get_turf(A), starting) // Get the distance between the turf shot from and the mob we hit and use that for the calculations.
@@ -227,7 +232,6 @@
trajectory_ignore_forcemove = TRUE
forceMove(target_turf)
trajectory_ignore_forcemove = FALSE
- colliding = FALSE
return FALSE
var/permutation = A.bullet_act(src, def_zone) // searches for return value, could be deleted after run so check A isn't null
@@ -237,17 +241,14 @@
trajectory_ignore_forcemove = FALSE
if(A)
permutated.Add(A)
- colliding = FALSE
return FALSE
else
var/atom/alt = select_target(A)
if(alt)
if(!prehit(alt))
- colliding = FALSE
return FALSE
alt.bullet_act(src, def_zone)
qdel(src)
- colliding = FALSE
return TRUE
/obj/item/projectile/proc/select_target(atom/A) //Selects another target from a wall if we hit a wall.
@@ -350,7 +351,7 @@
trajectory_ignore_forcemove = TRUE
forceMove(starting)
trajectory_ignore_forcemove = FALSE
- trajectory = new(starting.x, starting.y, starting.z, 0, 0, Angle, pixel_speed)
+ trajectory = new(starting.x, starting.y, starting.z, pixel_x, pixel_y, Angle, pixel_speed)
last_projectile_move = world.time
fired = TRUE
if(hitscan)
@@ -417,6 +418,8 @@
var/matrix/M = new
M.Turn(Angle)
transform = M
+ if(homing)
+ process_homing()
trajectory.increment(trajectory_multiplier)
var/turf/T = trajectory.return_turf()
if(!istype(T))
@@ -445,10 +448,32 @@
Collide(original)
Range()
+/obj/item/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(angle, -homing_turn_speed, homing_turn_speed))
+
+/obj/item/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
/obj/item/projectile/proc/can_hit_target(atom/target, var/list/passthrough)
return (target && ((target.layer >= PROJECTILE_HIT_THRESHHOLD_LAYER) || ismob(target)) && (loc == get_turf(target)) && (!(target in passthrough)))
+//Spread is FORCED!
/obj/item/projectile/proc/preparePixelProjectile(atom/target, atom/source, params, spread = 0)
var/turf/curloc = get_turf(source)
var/turf/targloc = get_turf(target)
@@ -460,21 +485,18 @@
if(targloc || !params)
yo = targloc.y - curloc.y
xo = targloc.x - curloc.x
- setAngle(Get_Angle(src, targloc))
+ 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]
- if(spread)
- setAngle(calculated[1] + spread)
- else
- setAngle(calculated[1])
+ setAngle(calculated[1] + spread)
else if(targloc)
yo = targloc.y - curloc.y
xo = targloc.x - curloc.x
- setAngle(Get_Angle(src, targloc))
+ 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)
diff --git a/code/modules/projectiles/projectile/magic/spellcard.dm b/code/modules/projectiles/projectile/magic/spellcard.dm
new file mode 100644
index 00000000000..67e3ad374e5
--- /dev/null
+++ b/code/modules/projectiles/projectile/magic/spellcard.dm
@@ -0,0 +1,6 @@
+/obj/item/projectile/spellcard
+ name = "enchanted card"
+ desc = "A piece of paper enchanted to give it extreme durability and stiffness, along with a very hot burn to anyone unfortunate enough to get hit by a charged one."
+ icon_state = "spellcard"
+ damage_type = BURN
+ damage = 2
diff --git a/code/modules/spells/spell_types/aimed.dm b/code/modules/spells/spell_types/aimed.dm
index 6980cba8e22..6c860fda9b1 100644
--- a/code/modules/spells/spell_types/aimed.dm
+++ b/code/modules/spells/spell_types/aimed.dm
@@ -9,6 +9,7 @@
var/list/projectile_var_overrides = list()
var/projectile_amount = 1 //Projectiles per cast.
var/current_amount = 0 //How many projectiles left.
+ var/projectiles_per_fire = 1 //Projectiles per fire. Probably not a good thing to use unless you override ready_projectile().
/obj/effect/proc_holder/spell/aimed/Click()
var/mob/living/user = usr
@@ -26,10 +27,18 @@
charge_counter = charge_max * refund_percent
start_recharge()
remove_ranged_ability(msg)
+ on_deactivation(user)
else
msg = "[active_msg]Left-click to shoot it at a target!"
current_amount = projectile_amount
add_ranged_ability(user, msg, TRUE)
+ on_activation(user)
+
+/obj/effect/proc_holder/spell/aimed/proc/on_activation(mob/user)
+ return
+
+/obj/effect/proc_holder/spell/aimed/proc/on_deactivation(mob/user)
+ return
/obj/effect/proc_holder/spell/aimed/update_icon()
if(!action)
@@ -60,19 +69,25 @@
remove_ranged_ability() //Auto-disable the ability once you run out of bullets.
charge_counter = 0
start_recharge()
+ on_deactivation(user)
return TRUE
/obj/effect/proc_holder/spell/aimed/proc/fire_projectile(mob/living/user, atom/target)
current_amount--
- var/obj/item/projectile/P = new projectile_type(user.loc)
- P.firer = user
- P.preparePixelProjectile(target, user)
- for(var/V in projectile_var_overrides)
- if(P.vars[V])
- P.vv_edit_var(V, projectile_var_overrides[V])
- P.fire()
+ for(var/i in 1 to projectiles_per_fire)
+ var/obj/item/projectile/P = new projectile_type(user.loc)
+ P.firer = user
+ P.preparePixelProjectile(target, user)
+ for(var/V in projectile_var_overrides)
+ if(P.vars[V])
+ P.vv_edit_var(V, projectile_var_overrides[V])
+ ready_projectile(P, target, user, i)
+ P.fire()
return TRUE
+/obj/effect/proc_holder/spell/aimed/proc/ready_projectile(obj/item/projectile/P, atom/target, mob/user, iteration)
+ return
+
/obj/effect/proc_holder/spell/aimed/lightningbolt
name = "Lightning Bolt"
desc = "Fire a high powered lightning bolt at your foes!"
@@ -108,3 +123,58 @@
active_msg = "You prepare to cast your fireball spell!"
deactive_msg = "You extinguish your fireball... for now."
active = FALSE
+
+/obj/effect/proc_holder/spell/aimed/spell_cards
+ name = "Spell Cards"
+ desc = "Blazing hot rapid-fire homing cards. Banish your foes with its mystical power!"
+ school = "evocation"
+ charge_max = 50
+ clothes_req = 0
+ invocation = "Sigi'lu M'Fan 'Tasia"
+ invocation_type = "shout"
+ range = 40
+ cooldown_min = 10
+ projectile_amount = 5
+ projectiles_per_fire = 7
+ projectile_type = /obj/item/projectile/spellcard
+ var/datum/weakref/current_target_weakref
+ var/projectile_turnrate = 10
+ var/projectile_pixel_homing_spread = 32
+ var/projectile_initial_spread_amount = 30
+ var/projectile_location_spread_amount = 12
+ var/datum/component/lockon_aiming/lockon_component
+ ranged_clickcd_override = 1
+
+/obj/effect/proc_holder/spell/aimed/spell_cards/on_activation(mob/M)
+ QDEL_NULL(lockon_component)
+ lockon_component = M.AddComponent(/datum/component/lockon_aiming, 5, typecacheof(list(/mob/living)), 1, null, CALLBACK(src, .proc/on_lockon_component))
+
+/obj/effect/proc_holder/spell/aimed/spell_cards/proc/on_lockon_component(list/locked_weakrefs)
+ if(!length(locked_weakrefs))
+ current_target_weakref = null
+ return
+ current_target_weakref = locked_weakrefs[1]
+ var/atom/A = current_target_weakref.resolve()
+ if(A)
+ var/mob/M = lockon_component.parent
+ M.face_atom(A)
+
+/obj/effect/proc_holder/spell/aimed/spell_cards/on_deactivation(mob/M)
+ QDEL_NULL(lockon_component)
+
+/obj/effect/proc_holder/spell/aimed/spell_cards/ready_projectile(obj/item/projectile/P, atom/target, mob/user, iteration)
+ if(current_target_weakref)
+ var/atom/A = current_target_weakref.resolve()
+ if(A && get_dist(A, user) < 7)
+ P.homing_turn_speed = projectile_turnrate
+ P.homing_inaccuracy_min = projectile_pixel_homing_spread
+ P.homing_inaccuracy_max = projectile_pixel_homing_spread
+ P.set_homing_target(current_target_weakref.resolve())
+ var/rand_spr = rand()
+ var/total_angle = projectile_initial_spread_amount * 2
+ var/adjusted_angle = total_angle - ((projectile_initial_spread_amount / projectiles_per_fire) * 0.5)
+ var/one_fire_angle = adjusted_angle / projectiles_per_fire
+ var/current_angle = iteration * one_fire_angle * rand_spr - (projectile_initial_spread_amount / 2)
+ P.pixel_x = rand(-projectile_location_spread_amount, projectile_location_spread_amount)
+ P.pixel_y = rand(-projectile_location_spread_amount, projectile_location_spread_amount)
+ P.preparePixelProjectile(target, user, null, current_angle)
diff --git a/icons/obj/projectiles.dmi b/icons/obj/projectiles.dmi
index 34e6a8134bf18f9d841a99fd68c03644437f0862..2d0d35f8a51aadcafeccc8da322202aa43408983 100644
GIT binary patch
delta 1971
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
JPDHLkV1hz88qELz
diff --git a/tgstation.dme b/tgstation.dme
index 795ba95fdf4..c2bc3c255fa 100755
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -106,6 +106,7 @@
#include "code\__HELPERS\level_traits.dm"
#include "code\__HELPERS\matrices.dm"
#include "code\__HELPERS\mobs.dm"
+#include "code\__HELPERS\mouse_control.dm"
#include "code\__HELPERS\names.dm"
#include "code\__HELPERS\priority_announce.dm"
#include "code\__HELPERS\pronouns.dm"
@@ -316,6 +317,7 @@
#include "code\datums\components\infective.dm"
#include "code\datums\components\jousting.dm"
#include "code\datums\components\knockoff.dm"
+#include "code\datums\components\lockon_aiming.dm"
#include "code\datums\components\material_container.dm"
#include "code\datums\components\mood.dm"
#include "code\datums\components\ntnet_interface.dm"
@@ -2257,6 +2259,7 @@
#include "code\modules\projectiles\projectile\energy\net_snare.dm"
#include "code\modules\projectiles\projectile\energy\stun.dm"
#include "code\modules\projectiles\projectile\energy\tesla.dm"
+#include "code\modules\projectiles\projectile\magic\spellcard.dm"
#include "code\modules\projectiles\projectile\reusable\_reusable.dm"
#include "code\modules\projectiles\projectile\reusable\foam_dart.dm"
#include "code\modules\projectiles\projectile\reusable\magspear.dm"