From 9eb6eb62255f46e2d48bd0d067714b824e083876 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] [READY] ~SPELL CARDS~, homing projectiles, and more! --- 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 | 915 ++++++++++++++++++ .../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, 1459 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 6ca1107619..b4a44f6d4b 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 91eee45b4d..4ba5d55979 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 26b5ac65cc..1dfacc0149 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 0000000000..24e40cc355 --- /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 a63ff6bdf0..0a212e36a1 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 0000000000..8b590eb2bd --- /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 7a5ba0a189..bd978a921d 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 c603be85b7..1a26052cc9 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 0000000000..017e7ea8eb --- /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 63b4774550..33facca9a1 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 f9602c0217..3c64d5f60d 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 1b744863d8..30d150e3ee 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 c26cb5e65c..8b2ee4083c 100644 --- a/code/modules/antagonists/wizard/equipment/spellbook.dm +++ b/code/modules/antagonists/wizard/equipment/spellbook.dm @@ -1,3 +1,4 @@ +<<<<<<< HEAD /datum/spellbook_entry var/name = "Entry Name" @@ -906,3 +907,917 @@ spellname = "sacred flame" icon_state ="booksacredflame" desc = "Become one with the flames that burn within... and invite others to do so as well." +======= +/datum/spellbook_entry + var/name = "Entry Name" + + var/spell_type = null + var/desc = "" + var/category = "Offensive" + var/cost = 2 + var/refundable = 1 + var/surplus = -1 // -1 for infinite, not used by anything atm + var/obj/effect/proc_holder/spell/S = null //Since spellbooks can be used by only one person anyway we can track the actual spell + var/buy_word = "Learn" + var/limit //used to prevent a spellbook_entry from being bought more than X times with one wizard spellbook + var/list/no_coexistance_typecache //Used so you can't have specific spells together + +/datum/spellbook_entry/New() + ..() + no_coexistance_typecache = typecacheof(no_coexistance_typecache) + +/datum/spellbook_entry/proc/IsAvailible() // For config prefs / gamemode restrictions - these are round applied + return 1 + +/datum/spellbook_entry/proc/CanBuy(mob/living/carbon/human/user,obj/item/spellbook/book) // Specific circumstances + if(book.uses= aspell.level_max) + to_chat(user, "This spell cannot be improved further.") + return 0 + else + aspell.name = initial(aspell.name) + aspell.spell_level++ + aspell.charge_max = round(initial(aspell.charge_max) - aspell.spell_level * (initial(aspell.charge_max) - aspell.cooldown_min)/ aspell.level_max) + if(aspell.charge_max < aspell.charge_counter) + aspell.charge_counter = aspell.charge_max + switch(aspell.spell_level) + if(1) + to_chat(user, "You have improved [aspell.name] into Efficient [aspell.name].") + aspell.name = "Efficient [aspell.name]" + if(2) + to_chat(user, "You have further improved [aspell.name] into Quickened [aspell.name].") + aspell.name = "Quickened [aspell.name]" + if(3) + to_chat(user, "You have further improved [aspell.name] into Free [aspell.name].") + aspell.name = "Free [aspell.name]" + if(4) + to_chat(user, "You have further improved [aspell.name] into Instant [aspell.name].") + aspell.name = "Instant [aspell.name]" + if(aspell.spell_level >= aspell.level_max) + to_chat(user, "This spell cannot be strengthened any further.") + SSblackbox.record_feedback("nested tally", "wizard_spell_improved", 1, list("[name]", "[aspell.spell_level]")) + return 1 + //No same spell found - just learn it + SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) + user.mind.AddSpell(S) + to_chat(user, "You have learned [S.name].") + return 1 + +/datum/spellbook_entry/proc/CanRefund(mob/living/carbon/human/user,obj/item/spellbook/book) + if(!refundable) + return 0 + if(!S) + S = new spell_type() + for(var/obj/effect/proc_holder/spell/aspell in user.mind.spell_list) + if(initial(S.name) == initial(aspell.name)) + return 1 + return 0 + +/datum/spellbook_entry/proc/Refund(mob/living/carbon/human/user,obj/item/spellbook/book) //return point value or -1 for failure + var/area/wizard_station/A = locate() in GLOB.sortedAreas + if(!(user in A.contents)) + to_chat(user, "You can only refund spells at the wizard lair") + return -1 + if(!S) + S = new spell_type() + var/spell_levels = 0 + for(var/obj/effect/proc_holder/spell/aspell in user.mind.spell_list) + if(initial(S.name) == initial(aspell.name)) + spell_levels = aspell.spell_level + user.mind.spell_list.Remove(aspell) + qdel(S) + return cost * (spell_levels+1) + return -1 +/datum/spellbook_entry/proc/GetInfo() + if(!S) + S = new spell_type() + var/dat ="" + dat += "[initial(S.name)]" + if(S.charge_type == "recharge") + dat += " Cooldown:[S.charge_max/10]" + dat += " Cost:[cost]
" + dat += "[S.desc][desc]
" + dat += "[S.clothes_req?"Needs wizard garb":"Can be cast without wizard garb"]
" + return dat + +/datum/spellbook_entry/fireball + 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 + +/datum/spellbook_entry/magicm + name = "Magic Missile" + spell_type = /obj/effect/proc_holder/spell/targeted/projectile/magic_missile + category = "Defensive" + +/datum/spellbook_entry/disintegrate + name = "Disintegrate" + spell_type = /obj/effect/proc_holder/spell/targeted/touch/disintegrate + +/datum/spellbook_entry/disabletech + name = "Disable Tech" + spell_type = /obj/effect/proc_holder/spell/targeted/emplosion/disable_tech + category = "Defensive" + cost = 1 + +/datum/spellbook_entry/repulse + name = "Repulse" + spell_type = /obj/effect/proc_holder/spell/aoe_turf/repulse + category = "Defensive" + +/datum/spellbook_entry/lightningPacket + name = "Lightning bolt! Lightning bolt!" + spell_type = /obj/effect/proc_holder/spell/targeted/conjure_item/spellpacket + category = "Defensive" + +/datum/spellbook_entry/timestop + name = "Time Stop" + spell_type = /obj/effect/proc_holder/spell/aoe_turf/conjure/timestop + category = "Defensive" + +/datum/spellbook_entry/smoke + name = "Smoke" + spell_type = /obj/effect/proc_holder/spell/targeted/smoke + category = "Defensive" + cost = 1 + +/datum/spellbook_entry/blind + name = "Blind" + spell_type = /obj/effect/proc_holder/spell/targeted/trigger/blind + cost = 1 + +/datum/spellbook_entry/mindswap + name = "Mindswap" + spell_type = /obj/effect/proc_holder/spell/targeted/mind_transfer + category = "Mobility" + +/datum/spellbook_entry/forcewall + name = "Force Wall" + spell_type = /obj/effect/proc_holder/spell/targeted/forcewall + category = "Defensive" + cost = 1 + +/datum/spellbook_entry/blink + name = "Blink" + spell_type = /obj/effect/proc_holder/spell/targeted/turf_teleport/blink + category = "Mobility" + +/datum/spellbook_entry/teleport + name = "Teleport" + spell_type = /obj/effect/proc_holder/spell/targeted/area_teleport/teleport + category = "Mobility" + +/datum/spellbook_entry/mutate + name = "Mutate" + spell_type = /obj/effect/proc_holder/spell/targeted/genetic/mutate + +/datum/spellbook_entry/jaunt + name = "Ethereal Jaunt" + spell_type = /obj/effect/proc_holder/spell/targeted/ethereal_jaunt + category = "Mobility" + +/datum/spellbook_entry/knock + name = "Knock" + spell_type = /obj/effect/proc_holder/spell/aoe_turf/knock + category = "Mobility" + cost = 1 + +/datum/spellbook_entry/fleshtostone + name = "Flesh to Stone" + spell_type = /obj/effect/proc_holder/spell/targeted/touch/flesh_to_stone + +/datum/spellbook_entry/summonitem + name = "Summon Item" + spell_type = /obj/effect/proc_holder/spell/targeted/summonitem + category = "Assistance" + cost = 1 + +/datum/spellbook_entry/lichdom + name = "Bind Soul" + spell_type = /obj/effect/proc_holder/spell/targeted/lichdom + category = "Defensive" + +/datum/spellbook_entry/teslablast + name = "Tesla Blast" + spell_type = /obj/effect/proc_holder/spell/targeted/tesla + +/datum/spellbook_entry/lightningbolt + name = "Lightning Bolt" + spell_type = /obj/effect/proc_holder/spell/aimed/lightningbolt + cost = 3 + +/datum/spellbook_entry/lightningbolt/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) //return 1 on success + . = ..() + user.flags_2 |= TESLA_IGNORE_2 + +/datum/spellbook_entry/infinite_guns + name = "Lesser Summon Guns" + spell_type = /obj/effect/proc_holder/spell/targeted/infinite_guns/gun + cost = 3 + no_coexistance_typecache = /obj/effect/proc_holder/spell/targeted/infinite_guns/arcane_barrage + +/datum/spellbook_entry/arcane_barrage + name = "Arcane Barrage" + spell_type = /obj/effect/proc_holder/spell/targeted/infinite_guns/arcane_barrage + cost = 3 + no_coexistance_typecache = /obj/effect/proc_holder/spell/targeted/infinite_guns/gun + +/datum/spellbook_entry/barnyard + name = "Barnyard Curse" + spell_type = /obj/effect/proc_holder/spell/targeted/barnyardcurse + +/datum/spellbook_entry/charge + name = "Charge" + spell_type = /obj/effect/proc_holder/spell/targeted/charge + category = "Assistance" + cost = 1 + +/datum/spellbook_entry/shapeshift + name = "Wild Shapeshift" + spell_type = /obj/effect/proc_holder/spell/targeted/shapeshift + category = "Assistance" + cost = 1 + +/datum/spellbook_entry/spacetime_dist + name = "Spacetime Distortion" + spell_type = /obj/effect/proc_holder/spell/spacetime_dist + category = "Defensive" + cost = 1 + +/datum/spellbook_entry/the_traps + name = "The Traps!" + spell_type = /obj/effect/proc_holder/spell/aoe_turf/conjure/the_traps + category = "Defensive" + cost = 1 + + +/datum/spellbook_entry/item + name = "Buy Item" + refundable = 0 + buy_word = "Summon" + var/item_path= null + + +/datum/spellbook_entry/item/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) + new item_path(get_turf(user)) + SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) + return 1 + +/datum/spellbook_entry/item/GetInfo() + var/dat ="" + dat += "[name]" + dat += " Cost:[cost]
" + dat += "[desc]
" + if(surplus>=0) + dat += "[surplus] left.
" + return dat + +/datum/spellbook_entry/item/staffchange + name = "Staff of Change" + desc = "An artefact that spits bolts of coruscating energy which cause the target's very form to reshape itself." + item_path = /obj/item/gun/magic/staff/change + +/datum/spellbook_entry/item/staffanimation + name = "Staff of Animation" + desc = "An arcane staff capable of shooting bolts of eldritch energy which cause inanimate objects to come to life. This magic doesn't affect machines." + item_path = /obj/item/gun/magic/staff/animate + category = "Assistance" + +/datum/spellbook_entry/item/staffchaos + name = "Staff of Chaos" + desc = "A caprious tool that can fire all sorts of magic without any rhyme or reason. Using it on people you care about is not recommended." + item_path = /obj/item/gun/magic/staff/chaos + +/datum/spellbook_entry/item/spellblade + name = "Spellblade" + desc = "A sword capable of firing blasts of energy which rip targets limb from limb." + item_path = /obj/item/gun/magic/staff/spellblade + +/datum/spellbook_entry/item/staffdoor + name = "Staff of Door Creation" + desc = "A particular staff that can mold solid metal into ornate doors. Useful for getting around in the absence of other transportation. Does not work on glass." + item_path = /obj/item/gun/magic/staff/door + cost = 1 + category = "Mobility" + +/datum/spellbook_entry/item/staffhealing + name = "Staff of Healing" + desc = "An altruistic staff that can heal the lame and raise the dead." + item_path = /obj/item/gun/magic/staff/healing + cost = 1 + category = "Defensive" + +/datum/spellbook_entry/item/scryingorb + name = "Scrying Orb" + desc = "An incandescent orb of crackling energy, using it will allow you to ghost while alive, allowing you to spy upon the station with ease. In addition, buying it will permanently grant you x-ray vision." + item_path = /obj/item/scrying + category = "Defensive" + +/datum/spellbook_entry/item/soulstones + name = "Six Soul Stone Shards and the spell Artificer" + desc = "Soul Stone Shards are ancient tools capable of capturing and harnessing the spirits of the dead and dying. The spell Artificer allows you to create arcane machines for the captured souls to pilot." + item_path = /obj/item/storage/belt/soulstone/full + category = "Assistance" + +/datum/spellbook_entry/item/soulstones/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) + . =..() + if(.) + user.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/conjure/construct(null)) + return . + +/datum/spellbook_entry/item/necrostone + name = "A Necromantic Stone" + desc = "A Necromantic stone is able to resurrect three dead individuals as skeletal thralls for you to command." + item_path = /obj/item/device/necromantic_stone + category = "Assistance" + +/datum/spellbook_entry/item/wands + name = "Wand Assortment" + desc = "A collection of wands that allow for a wide variety of utility. Wands have a limited number of charges, so be conservative in use. Comes in a handy belt." + item_path = /obj/item/storage/belt/wands/full + category = "Defensive" + +/datum/spellbook_entry/item/armor + name = "Mastercrafted Armor Set" + desc = "An artefact suit of armor that allows you to cast spells while providing more protection against attacks and the void of space." + item_path = /obj/item/clothing/suit/space/hardsuit/wizard + category = "Defensive" + +/datum/spellbook_entry/item/armor/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) + . = ..() + if(.) + new /obj/item/clothing/shoes/sandal/magic(get_turf(user)) //In case they've lost them. + new /obj/item/clothing/gloves/color/purple(get_turf(user))//To complete the outfit + +/datum/spellbook_entry/item/contract + name = "Contract of Apprenticeship" + desc = "A magical contract binding an apprentice wizard to your service, using it will summon them to your side." + item_path = /obj/item/antag_spawner/contract + category = "Assistance" + +/datum/spellbook_entry/item/guardian + name = "Guardian Deck" + desc = "A deck of guardian tarot cards, capable of binding a personal guardian to your body. There are multiple types of guardian available, but all of them will transfer some amount of damage to you. \ + It would be wise to avoid buying these with anything capable of causing you to swap bodies with others." + item_path = /obj/item/guardiancreator/choose/wizard + category = "Assistance" + +/datum/spellbook_entry/item/guardian/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) + . = ..() + if(.) + new /obj/item/paper/guides/antag/guardian/wizard(get_turf(user)) + +/datum/spellbook_entry/item/bloodbottle + name = "Bottle of Blood" + desc = "A bottle of magically infused blood, the smell of which will attract extradimensional beings when broken. Be careful though, the kinds of creatures summoned by blood magic are indiscriminate in their killing, and you yourself may become a victim." + item_path = /obj/item/antag_spawner/slaughter_demon + limit = 3 + category = "Assistance" + +/datum/spellbook_entry/item/hugbottle + name = "Bottle of Tickles" + desc = "A bottle of magically infused fun, the smell of which will \ + attract adorable extradimensional beings when broken. These beings \ + are similar to slaughter demons, but they do not permamently kill \ + their victims, instead putting them in an extradimensional hugspace, \ + to be released on the demon's death. Chaotic, but not ultimately \ + damaging. The crew's reaction to the other hand could be very \ + destructive." + item_path = /obj/item/antag_spawner/slaughter_demon/laughter + cost = 1 //non-destructive; it's just a jape, sibling! + limit = 3 + category = "Assistance" + +/datum/spellbook_entry/item/mjolnir + name = "Mjolnir" + desc = "A mighty hammer on loan from Thor, God of Thunder. It crackles with barely contained power." + item_path = /obj/item/twohanded/mjollnir + +/datum/spellbook_entry/item/singularity_hammer + name = "Singularity Hammer" + desc = "A hammer that creates an intensely powerful field of gravity where it strikes, pulling everything nearby to the point of impact." + item_path = /obj/item/twohanded/singularityhammer + +/datum/spellbook_entry/item/battlemage + name = "Battlemage Armour" + desc = "An ensorcelled suit of armour, protected by a powerful shield. The shield can completly negate sixteen attacks before being permanently depleted." + item_path = /obj/item/clothing/suit/space/hardsuit/shielded/wizard + limit = 1 + category = "Defensive" + +/datum/spellbook_entry/item/battlemage_charge + name = "Battlemage Armour Charges" + desc = "A powerful defensive rune, it will grant eight additional charges to a suit of battlemage armour." + item_path = /obj/item/wizard_armour_charge + category = "Defensive" + cost = 1 + +/datum/spellbook_entry/item/warpwhistle + name = "Warp Whistle" + desc = "A strange whistle that will transport you to a distant safe place on the station. There is a window of vulnerability at the beginning of every use." + item_path = /obj/item/warpwhistle + category = "Mobility" + cost = 1 + +/datum/spellbook_entry/summon + name = "Summon Stuff" + category = "Rituals" + refundable = 0 + buy_word = "Cast" + var/active = 0 + +/datum/spellbook_entry/summon/CanBuy(mob/living/carbon/human/user,obj/item/spellbook/book) + return ..() && !active + +/datum/spellbook_entry/summon/GetInfo() + var/dat ="" + dat += "[name]" + if(cost>0) + dat += " Cost:[cost]
" + else + dat += " No Cost
" + dat += "[desc]
" + if(active) + dat += "Already cast!
" + return dat + +/datum/spellbook_entry/summon/ghosts + name = "Summon Ghosts" + desc = "Spook the crew out by making them see dead people. Be warned, ghosts are capricious and occasionally vindicative, and some will use their incredibly minor abilties to frustrate you." + cost = 0 + +/datum/spellbook_entry/summon/ghosts/IsAvailible() + if(!SSticker.mode) + return FALSE + else + return TRUE + +/datum/spellbook_entry/summon/ghosts/Buy(mob/living/carbon/human/user, obj/item/spellbook/book) + SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) + new /datum/round_event/wizard/ghost() + active = TRUE + to_chat(user, "You have cast summon ghosts!") + playsound(get_turf(user), 'sound/effects/ghost2.ogg', 50, 1) + return TRUE + +/datum/spellbook_entry/summon/guns + name = "Summon Guns" + desc = "Nothing could possibly go wrong with arming a crew of lunatics just itching for an excuse to kill you. Just be careful not to stand still too long!" + +/datum/spellbook_entry/summon/guns/IsAvailible() + if(!SSticker.mode) // In case spellbook is placed on map + return 0 + return !CONFIG_GET(flag/no_summon_guns) + +/datum/spellbook_entry/summon/guns/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) + SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) + rightandwrong(SUMMON_GUNS, user, 25) + active = 1 + playsound(get_turf(user), 'sound/magic/castsummon.ogg', 50, 1) + to_chat(user, "You have cast summon guns!") + return 1 + +/datum/spellbook_entry/summon/magic + name = "Summon Magic" + desc = "Share the wonders of magic with the crew and show them why they aren't to be trusted with it at the same time." + +/datum/spellbook_entry/summon/magic/IsAvailible() + if(!SSticker.mode) // In case spellbook is placed on map + return 0 + return !CONFIG_GET(flag/no_summon_magic) + +/datum/spellbook_entry/summon/magic/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) + SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) + rightandwrong(SUMMON_MAGIC, user, 25) + active = 1 + playsound(get_turf(user), 'sound/magic/castsummon.ogg', 50, 1) + to_chat(user, "You have cast summon magic!") + return 1 + +/datum/spellbook_entry/summon/events + name = "Summon Events" + desc = "Give Murphy's law a little push and replace all events with special wizard ones that will confound and confuse everyone. Multiple castings increase the rate of these events." + var/times = 0 + +/datum/spellbook_entry/summon/events/IsAvailible() + if(!SSticker.mode) // In case spellbook is placed on map + return 0 + return !CONFIG_GET(flag/no_summon_events) + +/datum/spellbook_entry/summon/events/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) + SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) + summonevents() + times++ + playsound(get_turf(user), 'sound/magic/castsummon.ogg', 50, 1) + to_chat(user, "You have cast summon events.") + return 1 + +/datum/spellbook_entry/summon/events/GetInfo() + . = ..() + if(times>0) + . += "You cast it [times] times.
" + return . + +/obj/item/spellbook + name = "spell book" + desc = "An unearthly tome that glows with power." + icon = 'icons/obj/library.dmi' + icon_state ="book" + throw_speed = 2 + throw_range = 5 + w_class = WEIGHT_CLASS_TINY + persistence_replacement = /obj/item/spellbook/oneuse/random + var/uses = 10 + var/temp = null + var/tab = null + var/mob/living/carbon/human/owner + var/list/datum/spellbook_entry/entries = list() + var/list/categories = list() + +/obj/item/spellbook/examine(mob/user) + ..() + if(owner) + to_chat(user, "There is a small signature on the front cover: \"[owner]\".") + else + to_chat(user, "It appears to have no author.") + +/obj/item/spellbook/Initialize() + . = ..() + prepare_spells() + +/obj/item/spellbook/proc/prepare_spells() + var/entry_types = subtypesof(/datum/spellbook_entry) - /datum/spellbook_entry/item - /datum/spellbook_entry/summon + for(var/T in entry_types) + var/datum/spellbook_entry/E = new T + if(E.IsAvailible()) + entries |= E + categories |= E.category + else + qdel(E) + tab = categories[1] + +/obj/item/spellbook/attackby(obj/item/O, mob/user, params) + if(istype(O, /obj/item/antag_spawner/contract)) + var/obj/item/antag_spawner/contract/contract = O + if(contract.used) + to_chat(user, "The contract has been used, you can't get your points back now!") + else + to_chat(user, "You feed the contract back into the spellbook, refunding your points.") + uses++ + for(var/datum/spellbook_entry/item/contract/CT in entries) + if(!isnull(CT.limit)) + CT.limit++ + qdel(O) + else if(istype(O, /obj/item/antag_spawner/slaughter_demon)) + to_chat(user, "On second thought, maybe summoning a demon is a bad idea. You refund your points.") + uses++ + for(var/datum/spellbook_entry/item/bloodbottle/BB in entries) + if(!isnull(BB.limit)) + BB.limit++ + qdel(O) + +/obj/item/spellbook/proc/GetCategoryHeader(category) + var/dat = "" + switch(category) + if("Offensive") + dat += "Spells and items geared towards debilitating and destroying.

" + dat += "Items are not bound to you and can be stolen. Additionaly they cannot typically be returned once purchased.
" + dat += "For spells: the number after the spell name is the cooldown time.
" + dat += "You can reduce this number by spending more points on the spell.
" + if("Defensive") + dat += "Spells and items geared towards improving your survivabilty or reducing foes' ability to attack.

" + dat += "Items are not bound to you and can be stolen. Additionaly they cannot typically be returned once purchased.
" + dat += "For spells: the number after the spell name is the cooldown time.
" + dat += "You can reduce this number by spending more points on the spell.
" + if("Mobility") + dat += "Spells and items geared towards improving your ability to move. It is a good idea to take at least one.

" + dat += "Items are not bound to you and can be stolen. Additionaly they cannot typically be returned once purchased.
" + dat += "For spells: the number after the spell name is the cooldown time.
" + dat += "You can reduce this number by spending more points on the spell.
" + if("Assistance") + dat += "Spells and items geared towards bringing in outside forces to aid you or improving upon your other items and abilties.

" + dat += "Items are not bound to you and can be stolen. Additionaly they cannot typically be returned once purchased.
" + dat += "For spells: the number after the spell name is the cooldown time.
" + dat += "You can reduce this number by spending more points on the spell.
" + if("Challenges") + dat += "The Wizard Federation typically has hard limits on the potency and number of spells brought to the station based on risk.
" + dat += "Arming the station against you will increases the risk, but will grant you one more charge for your spellbook.
" + if("Rituals") + dat += "These powerful spells change the very fabric of reality. Not always in your favour.
" + return dat + +/obj/item/spellbook/proc/wrap(content) + var/dat = "" + dat +="Spellbook" + dat += {" + + + + "} + dat += {"[content]"} + return dat + +/obj/item/spellbook/attack_self(mob/user) + if(!owner) + to_chat(user, "You bind the spellbook to yourself.") + owner = user + return + if(user != owner) + to_chat(user, "The [name] does not recognize you as its owner and refuses to open!") + return + user.set_machine(src) + var/dat = "" + + dat += "" + + var/datum/spellbook_entry/E + for(var/i=1,i<=entries.len,i++) + var/spell_info = "" + E = entries[i] + spell_info += E.GetInfo() + if(E.CanBuy(user,src)) + spell_info+= "[E.buy_word]
" + else + spell_info+= "Can't [E.buy_word]
" + if(E.CanRefund(user,src)) + spell_info+= "Refund
" + spell_info += "
" + if(cat_dat[E.category]) + cat_dat[E.category] += spell_info + + for(var/category in categories) + dat += "
" + dat += GetCategoryHeader(category) + dat += cat_dat[category] + dat += "
" + + user << browse(wrap(dat), "window=spellbook;size=700x500") + onclose(user, "spellbook") + return + +/obj/item/spellbook/Topic(href, href_list) + ..() + var/mob/living/carbon/human/H = usr + + if(H.stat || H.restrained()) + return + if(!ishuman(H)) + return 1 + + if(H.mind.special_role == "apprentice") + temp = "If you got caught sneaking a peek from your teacher's spellbook, you'd likely be expelled from the Wizard Academy. Better not." + return + + var/datum/spellbook_entry/E = null + if(loc == H || (in_range(src, H) && isturf(loc))) + H.set_machine(src) + if(href_list["buy"]) + E = entries[text2num(href_list["buy"])] + if(E && E.CanBuy(H,src)) + if(E.Buy(H,src)) + if(E.limit) + E.limit-- + uses -= E.cost + else if(href_list["refund"]) + E = entries[text2num(href_list["refund"])] + if(E && E.refundable) + var/result = E.Refund(H,src) + if(result > 0) + if(!isnull(E.limit)) + E.limit += result + uses += result + else if(href_list["page"]) + tab = sanitize(href_list["page"]) + attack_self(H) + return + +//Single Use Spellbooks// + +/obj/item/spellbook/oneuse + var/spell = /obj/effect/proc_holder/spell/targeted/projectile/magic_missile //just a placeholder to avoid runtimes if someone spawned the generic + var/spellname = "sandbox" + var/used = 0 + name = "spellbook of " + uses = 1 + desc = "This template spellbook was never meant for the eyes of man..." + persistence_replacement = null + +/obj/item/spellbook/oneuse/prepare_spells() + name += spellname + +/obj/item/spellbook/oneuse/attack_self(mob/user) + var/obj/effect/proc_holder/spell/S = new spell + for(var/obj/effect/proc_holder/spell/knownspell in user.mind.spell_list) + if(knownspell.type == S.type) + if(user.mind) + if(iswizard(user)) + to_chat(user,"You're already far more versed in this spell than this flimsy how-to book can provide.") + else + to_chat(user,"You've already read this one.") + return + if(used) + recoil(user) + else + user.mind.AddSpell(S) + to_chat(user,"You rapidly read through the arcane book. Suddenly you realize you understand [spellname]!") + user.log_message("learned the spell [spellname] ([S]).", INDIVIDUAL_ATTACK_LOG) + onlearned(user) + +/obj/item/spellbook/oneuse/proc/recoil(mob/user) + user.visible_message("[src] glows in a black light!") + +/obj/item/spellbook/oneuse/proc/onlearned(mob/user) + used = 1 + user.visible_message("[src] glows dark for a second!") + +/obj/item/spellbook/oneuse/attackby() + return + +/obj/item/spellbook/oneuse/fireball + spell = /obj/effect/proc_holder/spell/aimed/fireball + spellname = "fireball" + icon_state ="bookfireball" + desc = "This book feels warm to the touch." + +/obj/item/spellbook/oneuse/fireball/recoil(mob/user) + ..() + explosion(user.loc, -1, 0, 2, 3, 0, flame_range = 2) + qdel(src) + +/obj/item/spellbook/oneuse/smoke + spell = /obj/effect/proc_holder/spell/targeted/smoke + spellname = "smoke" + icon_state ="booksmoke" + desc = "This book is overflowing with the dank arts." + +/obj/item/spellbook/oneuse/smoke/lesser //Chaplain smoke book + spell = /obj/effect/proc_holder/spell/targeted/smoke/lesser + +/obj/item/spellbook/oneuse/smoke/recoil(mob/user) + ..() + to_chat(user,"Your stomach rumbles...") + if(user.nutrition) + user.nutrition -= 200 + if(user.nutrition <= 0) + user.nutrition = 0 + + +/obj/item/spellbook/oneuse/blind + spell = /obj/effect/proc_holder/spell/targeted/trigger/blind + spellname = "blind" + icon_state ="bookblind" + desc = "This book looks blurry, no matter how you look at it." + +/obj/item/spellbook/oneuse/blind/recoil(mob/user) + ..() + to_chat(user,"You go blind!") + user.blind_eyes(10) + +/obj/item/spellbook/oneuse/mindswap + spell = /obj/effect/proc_holder/spell/targeted/mind_transfer + spellname = "mindswap" + icon_state ="bookmindswap" + desc = "This book's cover is pristine, though its pages look ragged and torn." + var/mob/stored_swap = null //Used in used book recoils to store an identity for mindswaps + +/obj/item/spellbook/oneuse/mindswap/onlearned() + spellname = pick("fireball","smoke","blind","forcewall","knock","barnyard","charge") + icon_state = "book[spellname]" + name = "spellbook of [spellname]" //Note, desc doesn't change by design + ..() + +/obj/item/spellbook/oneuse/mindswap/recoil(mob/user) + ..() + if(stored_swap in GLOB.dead_mob_list) + stored_swap = null + if(!stored_swap) + stored_swap = user + to_chat(user,"For a moment you feel like you don't even know who you are anymore.") + return + if(stored_swap == user) + to_chat(user,"You stare at the book some more, but there doesn't seem to be anything else to learn...") + return + + var/obj/effect/proc_holder/spell/targeted/mind_transfer/swapper = new + swapper.cast(user, stored_swap, 1) + + to_chat(stored_swap,"You're suddenly somewhere else... and someone else?!") + to_chat(user,"Suddenly you're staring at [src] again... where are you, who are you?!") + stored_swap = null + +/obj/item/spellbook/oneuse/forcewall + spell = /obj/effect/proc_holder/spell/targeted/forcewall + spellname = "forcewall" + icon_state ="bookforcewall" + desc = "This book has a dedication to mimes everywhere inside the front cover." + +/obj/item/spellbook/oneuse/forcewall/recoil(mob/living/user) + ..() + to_chat(user,"You suddenly feel very solid!") + user.Stun(40, ignore_canstun = TRUE) + user.petrify(30) + +/obj/item/spellbook/oneuse/knock + spell = /obj/effect/proc_holder/spell/aoe_turf/knock + spellname = "knock" + icon_state ="bookknock" + desc = "This book is hard to hold closed properly." + +/obj/item/spellbook/oneuse/knock/recoil(mob/living/user) + ..() + to_chat(user,"You're knocked down!") + user.Knockdown(40) + +/obj/item/spellbook/oneuse/barnyard + spell = /obj/effect/proc_holder/spell/targeted/barnyardcurse + spellname = "barnyard" + icon_state ="bookhorses" + desc = "This book is more horse than your mind has room for." + +/obj/item/spellbook/oneuse/barnyard/recoil(mob/living/carbon/user) + if(ishuman(user)) + to_chat(user,"HOR-SIE HAS RISEN") + var/obj/item/clothing/mask/horsehead/magichead = new /obj/item/clothing/mask/horsehead + magichead.flags_1 |= NODROP_1 //curses! + magichead.flags_inv &= ~HIDEFACE //so you can still see their face + magichead.voicechange = 1 //NEEEEIIGHH + if(!user.dropItemToGround(user.wear_mask)) + qdel(user.wear_mask) + user.equip_to_slot_if_possible(magichead, slot_wear_mask, 1, 1) + qdel(src) + else + to_chat(user,"I say thee neigh") //It still lives here + +/obj/item/spellbook/oneuse/charge + spell = /obj/effect/proc_holder/spell/targeted/charge + spellname = "charging" + icon_state ="bookcharge" + desc = "This book is made of 100% post-consumer wizard." + +/obj/item/spellbook/oneuse/charge/recoil(mob/user) + ..() + to_chat(user,"[src] suddenly feels very warm!") + empulse(src, 1, 1) + +/obj/item/spellbook/oneuse/summonitem + spell = /obj/effect/proc_holder/spell/targeted/summonitem + spellname = "instant summons" + icon_state ="booksummons" + desc = "This book is bright and garish, very hard to miss." + +/obj/item/spellbook/oneuse/summonitem/recoil(mob/user) + ..() + to_chat(user,"[src] suddenly vanishes!") + qdel(src) + +/obj/item/spellbook/oneuse/random + icon_state = "random_book" + +/obj/item/spellbook/oneuse/random/Initialize() + ..() + var/static/banned_spells = list(/obj/item/spellbook/oneuse/mimery_blockade, /obj/item/spellbook/oneuse/mimery_guns) + var/real_type = pick(subtypesof(/obj/item/spellbook/oneuse) - banned_spells) + new real_type(loc) + return INITIALIZE_HINT_QDEL + +/obj/item/spellbook/oneuse/sacredflame + spell = /obj/effect/proc_holder/spell/targeted/sacred_flame + spellname = "sacred flame" + icon_state ="booksacredflame" + desc = "Become one with the flames that burn within... and invite others to do so as well." +>>>>>>> ca10fa6... Spell Cards (#36147) diff --git a/code/modules/projectiles/guns/misc/beam_rifle.dm b/code/modules/projectiles/guns/misc/beam_rifle.dm index 80cbf11318..4edff70e6c 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 ad1d0f76d0..af04402a93 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,7 +485,7 @@ if(targloc || !params) yo = targloc.y - curloc.y xo = targloc.x - curloc.x - setAngle(Get_Angle(src, targloc)) + setAngle(Get_Angle(src, targloc) + spread) //CIT CHANGES START HERE - makes it so laying down makes you unable to shoot through most objects if(iscarbon(source)) @@ -474,14 +499,11 @@ 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 0000000000..67e3ad374e --- /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 6980cba8e2..6c860fda9b 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 9cdd76edd2..62082a27f2 100755 --- a/tgstation.dme +++ b/tgstation.dme @@ -109,6 +109,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" @@ -325,6 +326,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" @@ -2289,6 +2291,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"