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"