diff --git a/citadel.dme b/citadel.dme index df96f0d9d48..3c900ec07e3 100644 --- a/citadel.dme +++ b/citadel.dme @@ -382,6 +382,7 @@ #include "code\__HELPERS\files\paths.dm" #include "code\__HELPERS\files\walk.dm" #include "code\__HELPERS\game\depth.dm" +#include "code\__HELPERS\game\turfs\line.dm" #include "code\__HELPERS\game\turfs\offsets.dm" #include "code\__HELPERS\graphs\astar.dm" #include "code\__HELPERS\icons\alpha.dm" @@ -412,7 +413,6 @@ #include "code\__HELPERS\math\angle.dm" #include "code\__HELPERS\math\distance.dm" #include "code\__HELPERS\math\fractions.dm" -#include "code\__HELPERS\math\multipliers.dm" #include "code\__HELPERS\matrices\color_matrix.dm" #include "code\__HELPERS\matrices\transform_matrix.dm" #include "code\__HELPERS\misc\sonar.dm" diff --git a/code/__DEFINES/_flags/atom_flags.dm b/code/__DEFINES/_flags/atom_flags.dm index 5beef649624..23dfa78116f 100644 --- a/code/__DEFINES/_flags/atom_flags.dm +++ b/code/__DEFINES/_flags/atom_flags.dm @@ -59,11 +59,16 @@ DEFINE_BITFIELD(atom_flags, list( #define MOVABLE_NO_THROW_DAMAGE_SCALING (1<<1) /// Do not spin when thrown. #define MOVABLE_NO_THROW_SPIN (1<<2) +/// We are currently about to be yanked by a Moved() triggering a Move() +/// +/// * used so things like projectile hitscans know to yield +#define MOVABLE_IN_MOVED_YANK (1<<3) DEFINE_BITFIELD(movable_flags, list( BITFIELD(MOVABLE_NO_THROW_SPEED_SCALING), BITFIELD(MOVABLE_NO_THROW_DAMAGE_SCALING), BITFIELD(MOVABLE_NO_THROW_SPIN), + BITFIELD(MOVABLE_IN_MOVED_YANK), )) // Flags for pass_flags. - Used in /atom/movable/var/pass_flags, and /atom/var/pass_flags_self diff --git a/code/__DEFINES/math.dm b/code/__DEFINES/math.dm index dda5022df07..d23d6c4f7d8 100644 --- a/code/__DEFINES/math.dm +++ b/code/__DEFINES/math.dm @@ -41,7 +41,7 @@ #define WRAP(val, min, max) ( min == max ? min : (val) - (round(((val) - (min))/((max) - (min))) * ((max) - (min))) ) // Real modulus that handles decimals -#define MODULUS(x, y) ( (x) - (y) * round((x) / (y)) ) +#define MODULUS_F(x, y) ( (x) - (y) * round((x) / (y)) ) // Cotangent #define COT(x) (1 / tan(x)) @@ -128,12 +128,13 @@ // Will filter out extra rotations and negative rotations // E.g: 540 becomes 180. -180 becomes 180. -#define SIMPLIFY_DEGREES(degrees) (MODULUS((degrees), 360)) +#define SIMPLIFY_DEGREES(degrees) (MODULUS_F((degrees), 360)) -#define GET_ANGLE_OF_INCIDENCE(face, input) (MODULUS((face) - (input), 360)) +#define GET_ANGLE_OF_INCIDENCE(face, input) (MODULUS_F((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) + // todo: optimize this shit if(!isnum(a) || !isnum(b)) return a = SIMPLIFY_DEGREES(a) diff --git a/code/__HELPERS/game/turfs/line.dm b/code/__HELPERS/game/turfs/line.dm new file mode 100644 index 00000000000..c07645d3eb2 --- /dev/null +++ b/code/__HELPERS/game/turfs/line.dm @@ -0,0 +1,205 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2024 silicons *// + +/** + * line drawing algorithm + * + * basically, takes one pixel, + * and returns all turfs it touches as it passes through a certain angle + * for a certain number of pixels + * + * we can't use DDA or Bresenham because it's, ironically, not as accurate for + * our purposes. + * + * we have to bias full diagonal's (45, 135, 225, 315) to one side + * + * todo: make sure this is consistent with how byond would behave if using step; or not, i don't care lmao + * + * @params + * * starting - starting turf + * * starting_px - pixel x on starting turf. this is not pixel_x on atom, 1 is the bottomleft, 32 is topright. + * * starting_py - pixel y on starting turf. this is not pixel_y on atom. 1 is bottomleft, 32 is topright. + * * angle - angle, clockwise of north + * * distance - pixels to go forwards + * * include_start - include starting turf + * * diagonal_expand_north - get turfs above diagonal if perfect diagonal. if both expand params are null, we use the same priority as SS13's movement handler. + * * diagonal_expand_south - get turfs below diagonal if perfect diagonal. if both expand params are null, we use the same priority as SS13's movement handler. + */ +/proc/pixel_physics_raycast(turf/starting, starting_px, starting_py, angle, distance, include_start, diagonal_expand_north, diagonal_expand_south) + if(starting_px < 0 || starting_py < 0 || starting_px > 33 || starting_py > 33) + CRASH("starting_px or starting_py is not 0 < x < 33") + starting_px = clamp(starting_px, 1, 32) + starting_py = clamp(starting_py, 1, 32) + + . = list() + + if(include_start) + . += starting + + var/remaining_distance = distance + var/safety = world.maxx + world.maxy + + // if angle is completely cardinal or completely diagonal + // we use MODULUS_F because it's floating-compatible + if(!MODULUS_F(angle, 45)) + // normalize; we already know it's basically diagonal + angle = angle % 360 + if(angle < 0) + angle += 360 + // go do cardinal / diagonal specials + if(!(angle % 90)) + // cardinal + var/c_sdx + var/c_sdy + var/c_dist_to_next + var/turf/c_moving_into = starting + switch(angle) + if(0) + c_sdx = 0 + c_sdy = 1 + c_dist_to_next = (WORLD_ICON_SIZE + 0.5) - starting_py + if(90) + c_sdx = 1 + c_sdy = 0 + c_dist_to_next = (WORLD_ICON_SIZE + 0.5) - starting_px + if(180) + c_sdx = 0 + c_sdy = -1 + c_dist_to_next = starting_py - 0.5 + if(270) + c_sdx = -1 + c_sdy = 0 + c_dist_to_next = starting_px - 0.5 + + remaining_distance -= c_dist_to_next + while(remaining_distance >= 0) + c_moving_into = locate(c_moving_into.x + c_sdx, c_moving_into.y + c_sdy, c_moving_into.z) + if(!c_moving_into) + break + . += c_moving_into + remaining_distance -= WORLD_ICON_SIZE + return + else + var/is_diagonal_case + var/turf/d_moving_into = starting + var/d_diagonal_distance = (((WORLD_ICON_SIZE ** 2) * 2) ** 0.5) + var/d_dist_to_next + var/d_sdx + var/d_sdy + /// direction to go if we want to include the northern-most cardinal step + var/d_north_dir + /// direction to go if we want to include the southern-most cardinal step + var/d_south_dir + /// direction to go if using ss13 native movement to solve for the cardinal steps + var/d_native_dir + // we're diagonal + switch(angle) + if(45) + is_diagonal_case = round(starting_px, 1) == round(starting_py, 1) + d_sdx = 1 + d_sdy = 1 + d_dist_to_next = ((((WORLD_ICON_SIZE + 0.5) ** 2) * 2) ** 0.5) - (starting_px / WORLD_ICON_SIZE) * d_diagonal_distance + d_north_dir = WEST + d_south_dir = SOUTH + d_native_dir = WEST + if(135) + is_diagonal_case = round(starting_px, 1) == (WORLD_ICON_SIZE - round(starting_py, 1) + 1) + d_sdx = 1 + d_sdy = -1 + d_dist_to_next = ((((WORLD_ICON_SIZE + 0.5) ** 2) * 2) ** 0.5) - (starting_px / WORLD_ICON_SIZE) * d_diagonal_distance + d_north_dir = NORTH + d_south_dir = WEST + d_native_dir = WEST + if(225) + is_diagonal_case = round(starting_px, 1) == round(starting_py, 1) + d_sdx = -1 + d_sdy = -1 + d_dist_to_next = ((starting_px - 0.5) / WORLD_ICON_SIZE) * d_diagonal_distance + d_north_dir = NORTH + d_south_dir = EAST + d_native_dir = EAST + if(315) + is_diagonal_case = round(starting_px, 1) == (WORLD_ICON_SIZE - round(starting_py, 1) + 1) + d_sdx = -1 + d_sdy = 1 + d_dist_to_next = ((starting_px - 0.5) / WORLD_ICON_SIZE) * d_diagonal_distance + d_north_dir = EAST + d_south_dir = SOUTH + d_native_dir = EAST + // only do special diag stuff if it's a close enough to a perfect diagonal + if(is_diagonal_case) + remaining_distance -= d_dist_to_next + var/use_ss13_default_priority = isnull(diagonal_expand_north) && isnull(diagonal_expand_south) + while(remaining_distance >= 0) + d_moving_into = locate(d_moving_into.x + d_sdx, d_moving_into.y + d_sdy, d_moving_into.z) + if(!d_moving_into) + break + . += d_moving_into + if(use_ss13_default_priority) + . += get_step(d_moving_into, d_native_dir) + else + if(diagonal_expand_north) + // we actually want to get the one behind them; we don't need to null check either because of that + . += get_step(d_moving_into, d_north_dir) + if(diagonal_expand_south) + // we actually want to get the one behind them; we don't need to null check either because of that + . += get_step(d_moving_into, d_south_dir) + remaining_distance -= WORLD_ICON_SIZE + return + + // dx, dy for every distance pixel + var/ddx = sin(angle) + var/ddy = cos(angle) + + // sign of dx, dy as 1 or -1 + var/sdx = ddx > 0? 1 : -1 + var/sdy = ddy > 0? 1 : -1 + + var/cx = starting_px + var/cy = starting_py + + var/turf/moving_into = starting + while(safety-- > 0 && remaining_distance > 0) + var/d_next_horizontal = \ + (sdx? ((sdx > 0? (WORLD_ICON_SIZE + 0.5) - cx : -cx + 0.5) / ddx) : INFINITY) + var/d_next_vertical = \ + (sdy? ((sdy > 0? (WORLD_ICON_SIZE + 0.5) - cy : -cy + 0.5) / ddy) : INFINITY) + var/consumed = 0 + + if(d_next_horizontal == d_next_vertical) + // we're diagonal + if(d_next_horizontal <= remaining_distance) + moving_into = locate(moving_into.x + sdx, moving_into.y + sdy, moving_into.z) + consumed = d_next_horizontal + if(!moving_into) + break + cx = sdx > 0? 0.5 : (WORLD_ICON_SIZE + 0.5) + cy = sdy > 0? 0.5 : (WORLD_ICON_SIZE + 0.5) + else + break + else if(d_next_horizontal < d_next_vertical) + // closer is to move left/right + if(d_next_horizontal <= remaining_distance) + moving_into = locate(moving_into.x + sdx, moving_into.y, moving_into.z) + consumed = d_next_horizontal + if(!moving_into) + break + cx = sdx > 0? 0.5 : (WORLD_ICON_SIZE + 0.5) + cy = cy + d_next_horizontal * ddy + else if(d_next_vertical < d_next_horizontal) + // closer is to move up/down + if(d_next_vertical <= remaining_distance) + moving_into = locate(moving_into.x, moving_into.y + sdy, moving_into.z) + consumed = d_next_vertical + if(!moving_into) + break + cx = cx + d_next_vertical * ddx + cy = sdy > 0? 0.5 : (WORLD_ICON_SIZE + 0.5) + else + break + + remaining_distance -= consumed + + // if we need to move + if(moving_into) + . += moving_into diff --git a/code/__HELPERS/game/turfs/offsets.dm b/code/__HELPERS/game/turfs/offsets.dm index cdfabfefc60..cd206ffe074 100644 --- a/code/__HELPERS/game/turfs/offsets.dm +++ b/code/__HELPERS/game/turfs/offsets.dm @@ -54,7 +54,7 @@ // dir2angle is north-zero clockwise // turn_amount will therefore be how much we need to turn clockwise to get there // - // we know this will be a whole number so we use native % instead of MODULUS + // we know this will be a whole number so we use native % instead of MODULUS_F var/turn_amount = (dir2angle(new_dir) - dir2angle(old_dir)) % 360 // rotated x/y is where the entity will be after being rotated in its **current** diff --git a/code/__HELPERS/math/multipliers.dm b/code/__HELPERS/math/multipliers.dm deleted file mode 100644 index 515e4c848cf..00000000000 --- a/code/__HELPERS/math/multipliers.dm +++ /dev/null @@ -1,11 +0,0 @@ -/** - * multiplied effect of an effect multiplier based on '1' being default - */ -/proc/multiply_effect_multiplier(multiplier, multiply_by) - if(multiply_by < 0) - multiply_by = -multiply_by - multiplier = -multiplier - if(multiplier > 0) - return multiplier ** multiply_by - else - return -((-multiplier) ** multiply_by) diff --git a/code/__HELPERS/time.dm b/code/__HELPERS/time.dm index ddcb8d2809c..822b1d8d3c0 100644 --- a/code/__HELPERS/time.dm +++ b/code/__HELPERS/time.dm @@ -149,21 +149,21 @@ GLOBAL_VAR_INIT(roundstart_hour, pick(2,7,12,17)) if(second < 60) return "[second] second[(second != 1)? "s":""]" var/minute = FLOOR(second / 60, 1) - second = MODULUS(second, 60) + second = MODULUS_F(second, 60) var/secondT if(second) secondT = " and [second] second[(second != 1)? "s":""]" if(minute < 60) return "[minute] minute[(minute != 1)? "s":""][secondT]" var/hour = FLOOR(minute / 60, 1) - minute = MODULUS(minute, 60) + minute = MODULUS_F(minute, 60) var/minuteT if(minute) minuteT = " and [minute] minute[(minute != 1)? "s":""]" if(hour < 24) return "[hour] hour[(hour != 1)? "s":""][minuteT][secondT]" var/day = FLOOR(hour / 24, 1) - hour = MODULUS(hour, 24) + hour = MODULUS_F(hour, 24) var/hourT if(hour) hourT = " and [hour] hour[(hour != 1)? "s":""]" diff --git a/code/___compile_options.dm b/code/___compile_options.dm index a5085fa062e..d4040eb8039 100644 --- a/code/___compile_options.dm +++ b/code/___compile_options.dm @@ -189,12 +189,24 @@ // #define AO_USE_LIGHTING_OPACITY // ## Overlays + /** * A reasonable number of maximum overlays an object needs. * If you think you need more, rethink it. */ #define MAX_ATOM_OVERLAYS 100 +// ## Projectiles + +/** + * Enable raycast visuals + */ +// #define CF_PROJECTILE_RAYCAST_VISUALS + +#ifdef CF_PROJECTILE_RAYCAST_VISUALS + #warn Visualization of projectile raycast algorithm enabled. +#endif + // ## Timers // #define TIMER_LOOP_DEBUGGING diff --git a/code/controllers/subsystem/processing/processing.dm b/code/controllers/subsystem/processing/processing.dm index 1e3a57f5c4f..3e4829857a2 100644 --- a/code/controllers/subsystem/processing/processing.dm +++ b/code/controllers/subsystem/processing/processing.dm @@ -18,7 +18,11 @@ SUBSYSTEM_DEF(processing) currentrun = processing.Copy() //cache for sanic speed (lists are references anyways) var/list/current_run = currentrun - var/dt = (subsystem_flags & SS_TICKER)? (wait * world.tick_lag) : max(world.tick_lag, wait * 0.1) + // tick_lag is in deciseconds + // in ticker, our wait is that many ds + // in non-ticker, our wait is either wait in ds, or a minimum of tick_lag in ds + // we convert it to seconds with * 0.1 + var/dt = (subsystem_flags & SS_TICKER? (wait * world.tick_lag) : max(world.tick_lag, wait)) * 0.1 while(current_run.len) var/datum/thing = current_run[current_run.len] @@ -44,6 +48,9 @@ SUBSYSTEM_DEF(processing) * - Probability of something happening, do `if(DT_PROB(25, delta_time))`, not `if(prob(25))`. This way, if the subsystem wait is e.g. lowered, there won't be a higher chance of this event happening per second * * If you override this do not call parent, as it will return PROCESS_KILL. This is done to prevent objects that dont override process() from staying in the processing list + * + * @params + * * delta_time - time that should have elapsed, in seconds */ /datum/proc/process(delta_time) set waitfor = FALSE diff --git a/code/controllers/subsystem/processing/projectiles.dm b/code/controllers/subsystem/processing/projectiles.dm index 08771ed6a15..d0005da211b 100644 --- a/code/controllers/subsystem/processing/projectiles.dm +++ b/code/controllers/subsystem/processing/projectiles.dm @@ -4,13 +4,3 @@ PROCESSING_SUBSYSTEM_DEF(projectiles) stat_tag = "PP" priority = FIRE_PRIORITY_PROJECTILES subsystem_flags = SS_NO_INIT - var/global_max_tick_moves = 10 - var/global_pixel_speed = 2 - var/global_iterations_per_move = 16 - -/datum/controller/subsystem/processing/projectiles/proc/set_pixel_speed(new_speed) - global_pixel_speed = new_speed - for(var/i in processing) - var/obj/projectile/P = i - if(istype(P)) //there's non projectiles on this too. - P.set_pixel_speed(new_speed) diff --git a/code/datums/components/turfs/transition_border.dm b/code/datums/components/turfs/transition_border.dm index a0a7f598d86..7d01434fa28 100644 --- a/code/datums/components/turfs/transition_border.dm +++ b/code/datums/components/turfs/transition_border.dm @@ -50,6 +50,8 @@ /datum/component/transition_border/proc/transit(datum/source, atom/movable/AM) if(AM.atom_flags & ATOM_ABSTRACT) return // nah. + if(AM.movable_flags & MOVABLE_IN_MOVED_YANK) + return // we're already in a yank var/turf/our_turf = parent var/z_index = SSmapping.level_index_in_dir(our_turf.z, dir) if(isnull(z_index)) @@ -57,7 +59,9 @@ qdel(src) return // todo: this is shit but we have to yield to prevent a Moved() before Moved() + AM.movable_flags |= MOVABLE_IN_MOVED_YANK spawn(0) + AM.movable_flags &= ~MOVABLE_IN_MOVED_YANK if(AM.loc != our_turf) return var/turf/target diff --git a/code/datums/position_point_vector.dm b/code/datums/position_point_vector.dm index ed30566a0e6..6f9121469b9 100644 --- a/code/datums/position_point_vector.dm +++ b/code/datums/position_point_vector.dm @@ -9,7 +9,10 @@ #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) -/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. +/** + * Stores x/y/z and pixel_x/pixel_y + */ +/datum/position var/x = 0 var/y = 0 var/z = 0 @@ -66,7 +69,12 @@ /proc/angle_between_points(datum/point/a, datum/point/b) return arctan((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! +/** + * A precise point on the map. + * + * x/y are absolute pixels from map edge, so 1, 1 = the lower-left most pixel on the zlevel, not the center of turf (1,1)! + */ +/datum/point var/x = 0 var/y = 0 var/z = 0 @@ -99,9 +107,9 @@ /datum/point/proc/initialize_location(tile_x, tile_y, tile_z, p_x = 0, p_y = 0) if(!isnull(tile_x)) - x = ((tile_x - 1) * world.icon_size) + world.icon_size / 2 + p_x + 1 + x = ((tile_x - 1) * WORLD_ICON_SIZE) + WORLD_ICON_SIZE / 2 + p_x + 1 if(!isnull(tile_y)) - y = ((tile_y - 1) * world.icon_size) + world.icon_size / 2 + p_y + 1 + y = ((tile_y - 1) * WORLD_ICON_SIZE) + WORLD_ICON_SIZE / 2 + p_y + 1 if(!isnull(tile_z)) z = tile_z @@ -109,29 +117,84 @@ var/turf/T = return_turf() return "\ref[src] aX [x] aY [y] aZ [z] pX [return_px()] pY [return_py()] mX [T.x] mY [T.y] mZ [T.z]" -/datum/point/proc/move_atom_to_src(atom/movable/AM) - AM.forceMove(return_turf()) - AM.pixel_x = return_px() - AM.pixel_y = return_py() +/** + * angle is clockwise from north + */ +/datum/point/proc/shift_in_projectile_angle(angle, distance) + x += sin(angle) * distance + y += cos(angle) * distance -/datum/point/proc/return_turf() - return locate(CEILING(x / world.icon_size, 1), CEILING(y / world.icon_size, 1), z) - -/datum/point/proc/clamped_return_turf() - return locate(clamp(CEILING(x / world.icon_size, 1), 1, world.maxx), clamp(CEILING(y / world.icon_size, 1), 1, world.maxy), z) - -/datum/point/proc/return_coordinates() //[turf_x, turf_y, z] - return list(CEILING(x / world.icon_size, 1), CEILING(y / world.icon_size, 1), z) - -/datum/point/proc/return_position() - return new /datum/position(src) +/** + * doesn't use set base pixel x/y + * + * if not on a turf, we return null + */ +/datum/point/proc/instantiate_movable_with_unmanaged_offsets(typepath, ...) + ASSERT(ispath(typepath, /atom/movable)) + // todo: inline everything + var/turf/where = return_turf() + if(!where) + return + var/atom/movable/created = new typepath(arglist(list(where) + args.Copy(2))) + created.pixel_x = return_px() + created.pixel_y = return_py() + return created +/** + * return rounded pixel x + */ /datum/point/proc/return_px() - return MODULUS(x, world.icon_size) - 16 - 1 + // 1 = -15, + // 32 = +16 + // we start at 16, 16 + . = x % WORLD_ICON_SIZE + if(!.) + return 16 + . -= 16 +/** + * return rounded pixel y + */ /datum/point/proc/return_py() - return MODULUS(y, world.icon_size) - 16 - 1 + // 1 = -15, + // 32 = +16 + // we start at 16, 16 + . = y % WORLD_ICON_SIZE + if(!.) + return 16 + . -= 16 +/** + * return turf + */ +/datum/point/proc/return_turf() + return locate( + ceil(floor(x) / WORLD_ICON_SIZE), + ceil(floor(y) / WORLD_ICON_SIZE), + z, + ) + +/** + * extract closest in-bounds turf + * + * does not check for map transitions + */ +/datum/point/proc/clamped_return_turf() + return locate( + clamp(ceil(floor(x) / WORLD_ICON_SIZE), 1, world.maxx), + clamp(ceil(floor(y) / WORLD_ICON_SIZE), 1, world.maxy), + z, + ) + +/** + * return list(x, y, z) + */ +/datum/point/proc/return_coordinates() //[turf_x, turf_y, z] + return list( + ceil(floor(x) / WORLD_ICON_SIZE), + ceil(floor(y) / WORLD_ICON_SIZE), + z, + ) /datum/point/vector /// Pixels per iteration diff --git a/code/game/gamemodes/technomancer/spells/energy_siphon.dm b/code/game/gamemodes/technomancer/spells/energy_siphon.dm index e45adfb7969..cc51d809667 100644 --- a/code/game/gamemodes/technomancer/spells/energy_siphon.dm +++ b/code/game/gamemodes/technomancer/spells/energy_siphon.dm @@ -170,7 +170,7 @@ /obj/projectile/beam/lightning/energy_siphon name = "energy stream" icon_state = "lightning" - range = 6 // Backup plan in-case the effect somehow misses the Technomancer. + range = WORLD_ICON_SIZE * 6 power = 5 // This fires really fast, so this may add up if someone keeps standing in the beam. penetrating = 5 diff --git a/code/game/objects/effects/temporary_visuals/projectiles/projectile_effects.dm b/code/game/objects/effects/temporary_visuals/projectiles/projectile_effects.dm index 91b9d7812e2..cc2c6be080a 100644 --- a/code/game/objects/effects/temporary_visuals/projectiles/projectile_effects.dm +++ b/code/game/objects/effects/temporary_visuals/projectiles/projectile_effects.dm @@ -1,4 +1,5 @@ /obj/effect/projectile + SET_APPEARANCE_FLAGS(PIXEL_SCALE) name = "pew" icon = 'icons/obj/projectiles.dmi' icon_state = "nothing" diff --git a/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm b/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm index a47d99f188f..de36387ee2c 100644 --- a/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm +++ b/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm @@ -1,12 +1,6 @@ -/datum/beam_legacy_components_cache - var/list/beam_components = list() - -/datum/beam_legacy_components_cache/Destroy() - for(var/component in beam_components) - qdel(component) - return ..() - -/proc/generate_tracer_between_points(datum/point/starting, datum/point/ending, datum/beam_legacy_components_cache/beam_components, beam_type, color, qdel_in = 5, light_range = 2, light_color_override, light_intensity = 1, instance_key) //Do not pass z-crossing points as that will not be properly (and likely will never be properly until it's absolutely needed) supported! +//Do not pass z-crossing points as that will not be properly (and likely will never be properly until it's absolutely needed) supported! +// todo: when do we rework +/proc/generate_tracer_between_points(datum/point/starting, datum/point/ending, list/beam_components, beam_type, color, qdel_in = 5, light_range = 2, light_color_override, light_intensity = 1, instance_key) if(!istype(starting) || !istype(ending) || !ispath(beam_type)) return var/datum/point/midpoint = point_midpoint_points(starting, ending) @@ -29,9 +23,9 @@ for(var/obj/effect/projectile_lighting/PL in T) if(PL.owner == instance_key) continue tracing_line - beam_components.beam_components += new /obj/effect/projectile_lighting(T, light_color_override, light_range, light_intensity, instance_key) + beam_components += new /obj/effect/projectile_lighting(T, light_color_override, light_range, light_intensity, instance_key) line = null - beam_components.beam_components += PB + beam_components += PB /obj/effect/projectile/tracer name = "beam" diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index 262543cb016..1de47c3f95a 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -911,7 +911,7 @@ return FALSE . = TRUE // todo: mobility flags - var/extra_time = MODULUS(time, interval) + var/extra_time = MODULUS_F(time, interval) var/i for(i in 1 to round(time / interval)) if(!do_after(escapee, interval, mobility_flags = MOBILITY_CAN_RESIST)) diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm index 7c9f05c87ac..70bfc334357 100644 --- a/code/game/objects/structures/girders.dm +++ b/code/game/objects/structures/girders.dm @@ -45,7 +45,11 @@ set_multiplied_integrity(1) else name = "[material_reinforcing.display_name]-reinforced [material_structure.display_name] girder" - set_multiplied_integrity(material_structure.relative_integrity * multiply_effect_multiplier(material_reinforcing.relative_integrity, 0.5)) + // ()'s to coerce them into instances, and not text strings. + set_multiplied_integrity(SSmaterials.dynamic_calculate_relative_integrity(list( + (material_structure) = 1, + (material_reinforcing) = 0.5, + ))) // todo: refactor if(material_color) diff --git a/code/modules/materials/dynamics.dm b/code/modules/materials/dynamics.dm index 8f5c505bc16..1136de3b9d2 100644 --- a/code/modules/materials/dynamics.dm +++ b/code/modules/materials/dynamics.dm @@ -1,7 +1,10 @@ //* This file is explicitly licensed under the MIT license. *// //* Copyright (c) 2023 Citadel Station developers. *// -//* Page has all balancing parameters + algorithms for dynamic attribute computations for things like armor +//? Page has all balancing parameters + algorithms for dynamic attribute computations for things like armor ?// +//? Prefix subsystem procs with 'dynamic_', please! ?// + +//* Armor *// /** * creates an armor datum based off of our stats @@ -188,6 +191,25 @@ wall_armor_cache[cache_key] = resolved return resolved +//* Integrity *// + +/** + * gets overall integrity multiplier from a list of materials associated to significances + */ +/datum/controller/subsystem/materials/proc/dynamic_calculate_relative_integrity(list/datum/material/materials) + var/total = 0 + var/pieces = 0 + + for(var/datum/material/material as anything in materials) + var/significance = materials[material] + + pieces += significance + total += material.relative_integrity * significance + + return total / pieces + +//* Melee *// + /** * get melee stats * autodetect with initial damage modes of item diff --git a/code/modules/mining/kinetic_crusher.dm b/code/modules/mining/kinetic_crusher.dm index d88f2a49e09..18744726f27 100644 --- a/code/modules/mining/kinetic_crusher.dm +++ b/code/modules/mining/kinetic_crusher.dm @@ -310,7 +310,7 @@ damage = 0 //We're just here to mark people. This is still a melee weapon. damage_type = BRUTE damage_flag = ARMOR_BOMB - range = 6 + range = WORLD_ICON_SIZE * 6 accuracy = INFINITY // NO. // log_override = TRUE var/obj/item/kinetic_crusher/hammer_synced diff --git a/code/modules/mob/living/simple_mob/subtypes/humanoid/possessed.dm b/code/modules/mob/living/simple_mob/subtypes/humanoid/possessed.dm index f7a7f74eea1..c3e1564fca6 100644 --- a/code/modules/mob/living/simple_mob/subtypes/humanoid/possessed.dm +++ b/code/modules/mob/living/simple_mob/subtypes/humanoid/possessed.dm @@ -105,7 +105,7 @@ "\The A few last desperate seals give out with a weary series of pops, and the suit contorts with the final pressure differentials resolved: the suit tangles and leaks, and finally compacts back into it's rightful shape.", "\The Tightening, the suit re-attempts to remain it's current form, before it collapses under the stress, supporting mechanisms closing in on themselves like a noose with nothing left to catch on.", "\The The suit makes a noise akin to clockwork binding, and shutters, before something imperceptible gives with an abysmal noise and the suit returns to it's default form."))) - gib() + // gib() if(rand(1,2) == 1) new rig1(droploc) else diff --git a/code/modules/mob/living/simple_mob/subtypes/mechanical/mecha/adv_dark_gygax.dm b/code/modules/mob/living/simple_mob/subtypes/mechanical/mecha/adv_dark_gygax.dm index 78d3e27ba78..f9d04e65ecb 100644 --- a/code/modules/mob/living/simple_mob/subtypes/mechanical/mecha/adv_dark_gygax.dm +++ b/code/modules/mob/living/simple_mob/subtypes/mechanical/mecha/adv_dark_gygax.dm @@ -124,16 +124,13 @@ damage = 20 damage_type = BURN damage_flag = ARMOR_LASER + speed = 32 // 10 tiles a second /obj/projectile/energy/homing_bolt/launch_projectile(atom/target, target_zone, mob/user, params, angle_override, forced_spread = 0) ..() if(target) set_homing_target(target) -/obj/projectile/energy/homing_bolt/fire(angle, atom/direct_target) - ..() - set_pixel_speed(0.5) - #define ELECTRIC_ZAP_POWER 20000 // Charges a tesla shot, while emitting a dangerous electric field. The exosuit is immune to electric damage while this is ongoing. diff --git a/code/modules/mob/living/simple_mob/subtypes/slime/feral/feral.dm b/code/modules/mob/living/simple_mob/subtypes/slime/feral/feral.dm index e3f86910974..479a70b9376 100644 --- a/code/modules/mob/living/simple_mob/subtypes/slime/feral/feral.dm +++ b/code/modules/mob/living/simple_mob/subtypes/slime/feral/feral.dm @@ -68,7 +68,7 @@ damage_type = BRUTE damage_flag = ARMOR_MELEE armor_penetration = 30 - speed = 2 + speed = 32 / 2 // ~5 tiles/second icon_scale_x = 2 // It hits like a truck. icon_scale_y = 2 sharp = TRUE diff --git a/code/modules/overmap/entity/physics.dm b/code/modules/overmap/entity/physics.dm index 6d45c07ea34..208d0160b28 100644 --- a/code/modules/overmap/entity/physics.dm +++ b/code/modules/overmap/entity/physics.dm @@ -27,8 +27,8 @@ var/new_turf_x = CEILING(new_pos_pix_x / WORLD_ICON_SIZE, 1) var/new_turf_y = CEILING(new_pos_pix_y / WORLD_ICON_SIZE, 1) - var/new_pixel_x = MODULUS(new_pos_pix_x, WORLD_ICON_SIZE) - (WORLD_ICON_SIZE / 2) - 1 - var/new_pixel_y = MODULUS(new_pos_pix_y, WORLD_ICON_SIZE) - (WORLD_ICON_SIZE / 2) - 1 + var/new_pixel_x = MODULUS_F(new_pos_pix_x, WORLD_ICON_SIZE) - (WORLD_ICON_SIZE / 2) - 1 + var/new_pixel_y = MODULUS_F(new_pos_pix_y, WORLD_ICON_SIZE) - (WORLD_ICON_SIZE / 2) - 1 var/new_loc = locate(new_turf_x, new_turf_y, z) diff --git a/code/modules/overmap/legacy/ships/ship.dm b/code/modules/overmap/legacy/ships/ship.dm index 4e3e8325b55..78148dc31f4 100644 --- a/code/modules/overmap/legacy/ships/ship.dm +++ b/code/modules/overmap/legacy/ships/ship.dm @@ -201,11 +201,11 @@ /obj/overmap/entity/visitable/ship/proc/ETA() . = INFINITY if(vel_x) - var/offset = MODULUS(OVERMAP_DIST_TO_PIXEL(pos_x), WORLD_ICON_SIZE) + var/offset = MODULUS_F(OVERMAP_DIST_TO_PIXEL(pos_x), WORLD_ICON_SIZE) var/dist_to_go = (vel_x > 0) ? (WORLD_ICON_SIZE - offset) : offset . = min(., (dist_to_go / OVERMAP_DIST_TO_PIXEL(abs(vel_x))) * 10) if(vel_y) - var/offset = MODULUS(OVERMAP_DIST_TO_PIXEL(pos_y), WORLD_ICON_SIZE) + var/offset = MODULUS_F(OVERMAP_DIST_TO_PIXEL(pos_y), WORLD_ICON_SIZE) var/dist_to_go = (vel_y > 0) ? (WORLD_ICON_SIZE - offset) : offset . = min(., (dist_to_go / OVERMAP_DIST_TO_PIXEL(abs(vel_y))) * 10) . = max(., 0) diff --git a/code/modules/projectiles/ammunition/ammo_casing.dm b/code/modules/projectiles/ammunition/ammo_casing.dm index d27d97faf58..2c12ce235bf 100644 --- a/code/modules/projectiles/ammunition/ammo_casing.dm +++ b/code/modules/projectiles/ammunition/ammo_casing.dm @@ -108,7 +108,7 @@ /obj/item/ammo_casing/proc/init_projectile() if(istype(stored)) CRASH("double init?") - stored = new projectile_type + stored = new projectile_type(src) return stored /obj/item/ammo_casing/update_icon_state() diff --git a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm index 9f0a4594449..a5d8cbf30df 100644 --- a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm +++ b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm @@ -207,7 +207,7 @@ damage = 30 damage_type = BRUTE damage_flag = ARMOR_BOMB - range = 4 + range = WORLD_ICON_SIZE * 4 // log_override = TRUE var/pressure_decrease_active = FALSE @@ -241,7 +241,7 @@ pressure_decrease_active = TRUE return ..() -/obj/projectile/kinetic/on_range() +/obj/projectile/kinetic/legacy_on_range() strike_thing() ..() @@ -362,8 +362,7 @@ cost = 25 /obj/item/ka_modkit/range/modify_projectile(obj/projectile/kinetic/K) - K.range += modifier - + K.range += modifier * WORLD_ICON_SIZE //Damage /obj/item/ka_modkit/damage diff --git a/code/modules/projectiles/guns/projectile/dartgun.dm b/code/modules/projectiles/guns/projectile/dartgun.dm index fa3f25d4556..6a96b6ffe29 100644 --- a/code/modules/projectiles/guns/projectile/dartgun.dm +++ b/code/modules/projectiles/guns/projectile/dartgun.dm @@ -3,7 +3,7 @@ icon_state = "dart" damage = 5 var/reagent_amount = 15 - range = 15 //shorter range + range = WORLD_ICON_SIZE * 15 muzzle_type = null diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 37d219c36bd..4469da18408 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -1,7 +1,20 @@ -///Not actually hitscan but close as we get without actual hitscan. -#define MOVES_HITSCAN -1 -///How many pixels to move the muzzle flash up so your character doesn't look like they're shitting out lasers. -#define MUZZLE_EFFECT_PIXEL_INCREMENT 17 +/** + * ## Physics Specifications + * + * We track physics as absolute pixel on a tile, not byond's pixel x/y + * thus the first pixel at bottom left of tile is 1, 1 + * and the last pixel at top right is 32, 32 (for a world icon size of 32 pixels) + * + * We cross over to the next tile at above 32, for up/right, + * and to the last tile at below 1, for bottom/left. + * + * The code might handle it based on how it's implemented, + * but as long as the error is 1 pixel or below, it's not a big deal. + * + * The reason we're so accurate (1 pixel/below is pretty insanely strict) is + * so players have the projectile act like what the screen says it should be like; + * hence why projectiles can realistically path across corners based on their 'hitbox center'. + */ /obj/projectile name = "projectile" icon = 'icons/obj/projectiles.dmi' @@ -13,23 +26,88 @@ mouse_opacity = MOUSE_OPACITY_TRANSPARENT depth_level = INFINITY // nothing should be passing over us from depth - ////TG PROJECTILE SYTSEM - //Projectile stuff - var/range = 50 - var/originalRange + //* Physics - Configuration *// + + /// speed, in pixels per decisecond + var/speed = 32 / 0.55 // ~18 tiles/second + /// are we a hitscan projectile? + var/hitscan = FALSE + /// angle, in degrees **clockwise of north** + var/angle + /// max distance in pixels + /// + /// * please set this to a multiple of [WORLD_ICON_SIZE] so we scale with tile size. + var/range = WORLD_ICON_SIZE * 50 + // todo: lifespan + + //* Physics - Tracers *// + + /// tracer /datum/point's + var/list/tracer_vertices + /// first point is a muzzle effect + var/tracer_muzzle_flash + /// last point is an impact + var/tracer_impact_effect + /// default tracer duration + var/tracer_duration = 5 + + //* Physics - State *// + + /// paused? if so, we completely get passed over during processing + var/paused = FALSE + /// currently hitscanning + var/hitscanning = FALSE + /// a flag to prevent movement hooks from resetting our physics on a forced movement + var/trajectory_ignore_forcemove = FALSE + /// cached value: move this much x for this much distance + /// basically, dx / distance + var/calculated_dx + /// cached value: move this much y for this much distance + /// basically, dy / distance + var/calculated_dy + /// cached sign of dx; 1, -1, or 0 + var/calculated_sdx + /// cached sign of dy; 1, -1, or 0 + var/calculated_sdy + /// our current pixel location on turf + /// byond pixel_x rounds, and we don't want that + /// + /// * at below 0 or at equals to WORLD_ICON_SIZE, we move to the next turf + var/current_px + /// our current pixel location on turf + /// byond pixel_y rounds, and we don't want that + /// + /// * at below 0 or at equals to WORLD_ICON_SIZE, we move to the next turf + var/current_py + /// the pixel location we're moving to, or the [current_px] after this iteration step + /// + /// * used so stuff like hitscan deflections work based on the actual raycasted collision step, and not the prior step. + var/next_px + /// the pixel location we're moving to, or the [current_px] after this iteration step + /// + /// * used so stuff like hitscan deflections work based on the actual raycasted collision step, and not the prior step. + var/next_py + /// used to track if we got kicked forwards after calling Move() + var/trajectory_kick_forwards = 0 + /// to avoid going too fast when kicked forwards by a mirror, if we overshoot the pixels we're + /// supposed to move this gets set to penalize the next move with a weird algorithm + /// that i won't bother explaining + var/trajectory_penalty_applied = 0 + /// currently travelled distance in pixels + var/distance_travelled + /// if we get forcemoved, this gets reset to 0 as a trip + /// this way, we know exactly how far we moved + var/distance_travelled_this_iteration + /// where the physics loop and/or some other thing moving us is trying to move to + /// used to determine where to draw hitscan tracers + // todo: this being here is kinda a symptom that things are handled weirdly but whatever + // optimally physics loop should handle tracking for stuff like animations, not require on hit processing to check turfs + var/turf/trajectory_moving_to //Fired processing vars var/fired = FALSE //Have we been fired yet - var/paused = FALSE //for suspending the projectile midair - var/last_projectile_move = 0 - var/last_process = 0 - var/time_offset = 0 - var/datum/point/vector/trajectory - var/trajectory_ignore_forcemove = FALSE //instructs forceMove to NOT reset our trajectory to the new location! var/ignore_source_check = FALSE - var/speed = 0.55 //Amount of deciseconds it takes for projectile to travel - var/Angle = 0 var/original_angle = 0 //Angle at firing var/nondirectional_sprite = FALSE //Set TRUE to prevent projectiles from having their sprites rotated based on firing angle var/spread = 0 //amount (in degrees) of projectile spread @@ -39,16 +117,11 @@ var/ricochet_chance = 30 //Hitscan - var/hitscan = FALSE //Whether this is hitscan. If it is, speed is basically ignored. - var/list/beam_segments //assoc list of datum/point or datum/point/vector, start = end. Used for hitscan effect generation. - var/datum/point/beam_index - var/turf/hitscan_last //last turf touched during hitscanning. /// do we have a tracer? if not we completely ignore hitscan logic var/has_tracer = TRUE var/tracer_type var/muzzle_type var/impact_type - var/datum/beam_legacy_components_cache/beam_components var/miss_sounds var/ricochet_sounds @@ -68,7 +141,9 @@ //Homing var/homing = FALSE var/atom/homing_target - var/homing_turn_speed = 10 //Angle per tick. + // angle per deciseconds + // this is smoother the less time between SSprojectiles fires + var/homing_turn_speed = 10 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 @@ -151,104 +226,22 @@ var/fire_sound = 'sound/weapons/Gunshot_old.ogg' // Can be overriden in gun.dm's fire_sound var. It can also be null but I don't know why you'd ever want to do that. -Ace + // todo: currently unimplemneted var/vacuum_traversal = TRUE //Determines if the projectile can exist in vacuum, if false, the projectile will be deleted if it enters vacuum. - var/temporary_unstoppable_movement = FALSE var/no_attack_log = FALSE -/obj/projectile/proc/Range() - range-- - if(range <= 0 && loc) - on_range() +/obj/projectile/Destroy() + // stop processing + STOP_PROCESSING(SSprojectiles, src) + // cleanup + cleanup_hitscan_tracers() + return ..() -/obj/projectile/proc/on_range() //if we want there to be effects when they reach the end of their range +/obj/projectile/proc/legacy_on_range() //if we want there to be effects when they reach the end of their range + finalize_hitscan_tracers(impact_effect = FALSE, kick_forwards = 8) qdel(src) -/obj/projectile/proc/return_predicted_turf_after_moves(moves, forced_angle) //I say predicted because there's no telling that the projectile won't change direction/location in flight. - if(!trajectory && isnull(forced_angle) && isnull(Angle)) - return FALSE - var/datum/point/vector/current = trajectory - if(!current) - var/turf/T = get_turf(src) - current = new(T.x, T.y, T.z, pixel_x, pixel_y, isnull(forced_angle)? Angle : forced_angle, SSprojectiles.global_pixel_speed) - var/datum/point/vector/v = current.return_vector_after_increments(moves * SSprojectiles.global_iterations_per_move) - return v.return_turf() - -/obj/projectile/proc/return_pathing_turfs_in_moves(moves, forced_angle) - var/turf/current = get_turf(src) - var/turf/ending = return_predicted_turf_after_moves(moves, forced_angle) - return getline(current, ending) - -/obj/projectile/proc/set_pixel_speed(new_speed) - if(trajectory) - trajectory.set_speed(new_speed) - return TRUE - return FALSE - -/obj/projectile/proc/record_hitscan_start(datum/point/pcache) - if(!has_tracer) - return - if(!pcache) - return - beam_segments = list() - beam_index = pcache - beam_segments[beam_index] = null //record start. - -/obj/projectile/proc/process_hitscan() - var/safety = range * 3 - record_hitscan_start(RETURN_POINT_VECTOR_INCREMENT(src, Angle, MUZZLE_EFFECT_PIXEL_INCREMENT, 1)) - while(loc && !QDELETED(src)) - if(paused) - stoplag(1) - continue - if(safety-- <= 0) - if(loc) - Bump(loc) - if(!QDELETED(src)) - qdel(src) - return //Kill! - pixel_move(1, TRUE) - -/obj/projectile/proc/pixel_move(trajectory_multiplier, hitscanning = FALSE) - if(!loc || !trajectory) - return - last_projectile_move = world.time - if(homing) - process_homing() - var/forcemoved = FALSE - for(var/i in 1 to SSprojectiles.global_iterations_per_move) - if(QDELETED(src)) - return - trajectory.increment(trajectory_multiplier) - var/turf/T = trajectory.return_turf() - if(!istype(T)) - qdel(src) - return - if(T.z != loc.z) - var/old = loc - before_z_change(loc, T) - trajectory_ignore_forcemove = TRUE - forceMove(T) - trajectory_ignore_forcemove = FALSE - after_z_change(old, loc) - if(!hitscanning) - pixel_x = trajectory.return_px() - pixel_y = trajectory.return_py() - forcemoved = TRUE - hitscan_last = loc - else if(T != loc) - before_move() - step_towards(src, T) - hitscan_last = loc - after_move() - if(can_hit_target(original, permutated)) - Bump(original) - if(!hitscanning && !forcemoved) - pixel_x = trajectory.return_px() - trajectory.mpx * trajectory_multiplier * SSprojectiles.global_iterations_per_move - pixel_y = trajectory.return_py() - trajectory.mpy * trajectory_multiplier * SSprojectiles.global_iterations_per_move - animate(src, pixel_x = trajectory.return_px(), pixel_y = trajectory.return_py(), time = 1, flags = ANIMATION_END_NOW) - Range() - /obj/projectile/Crossed(atom/movable/AM) //A mob moving on a tile with a projectile is hit by it. if(AM.is_incorporeal()) return @@ -258,138 +251,64 @@ if(can_hit_target(L, permutated, (AM == original))) Bump(AM) -/obj/projectile/proc/process_homing() //may need speeding up in the future performance wise. - if(!homing_target) - return FALSE - var/datum/point/PT = RETURN_PRECISE_POINT(homing_target) - PT.x += clamp(homing_offset_x, 1, world.maxx) - PT.y += clamp(homing_offset_y, 1, world.maxy) - var/angle = closer_angle_difference(Angle, angle_between_points(RETURN_PRECISE_POINT(src), PT)) - setAngle(Angle + clamp(angle, -homing_turn_speed, homing_turn_speed)) - -/obj/projectile/proc/set_homing_target(atom/A) - if(!A || (!isturf(A) && !isturf(A.loc))) - return FALSE - homing = TRUE - homing_target = A - homing_offset_x = rand(homing_inaccuracy_min, homing_inaccuracy_max) - homing_offset_y = rand(homing_inaccuracy_min, homing_inaccuracy_max) - if(prob(50)) - homing_offset_x = -homing_offset_x - if(prob(50)) - homing_offset_y = -homing_offset_y - -/obj/projectile/process(delta_time) - last_process = world.time - if(!loc || !fired || !trajectory) - fired = FALSE - return PROCESS_KILL - if(paused || !isturf(loc)) - last_projectile_move += world.time - last_process //Compensates for pausing, so it doesn't become a hitscan projectile when unpaused from charged up ticks. - return - var/elapsed_time_deciseconds = (world.time - last_projectile_move) + time_offset - time_offset = 0 - var/required_moves = speed > 0? FLOOR(elapsed_time_deciseconds / speed, 1) : MOVES_HITSCAN //Would be better if a 0 speed made hitscan but everyone hates those so I can't make it a universal system :< - if(required_moves == MOVES_HITSCAN) - required_moves = SSprojectiles.global_max_tick_moves - else - if(required_moves > SSprojectiles.global_max_tick_moves) - var/overrun = required_moves - SSprojectiles.global_max_tick_moves - required_moves = SSprojectiles.global_max_tick_moves - time_offset += overrun * speed - time_offset += MODULUS(elapsed_time_deciseconds, speed) - - for(var/i in 1 to required_moves) - pixel_move(1, FALSE) - -/obj/projectile/proc/setAngle(new_angle) //wrapper for overrides. - Angle = new_angle - if(!nondirectional_sprite) - var/matrix/M = new - M.Turn(Angle) - transform = M - if(trajectory) - trajectory.set_angle(new_angle) - return TRUE - /obj/projectile/forceMove(atom/target) - if(!isloc(target) || !isloc(loc) || !z) - return ..() - var/zc = target.z != z - var/old = loc - if(zc) - before_z_change(old, target) + var/is_a_jump = isturf(target) != isturf(loc) || target.z != z || !trajectory_ignore_forcemove + if(is_a_jump) + record_hitscan_end() + render_hitscan_tracers() . = ..() - if(trajectory && !trajectory_ignore_forcemove && isturf(target)) - if(hitscan) - finalize_hitscan_and_generate_tracers(FALSE) - trajectory.initialize_location(target.x, target.y, target.z, 0, 0) - if(hitscan) - record_hitscan_start(RETURN_PRECISE_POINT(src)) - if(zc) - after_z_change(old, target) + if(!.) + stack_trace("projectile forcemove failed; please do not try to forcemove projectiles to invalid locations!") + distance_travelled_this_iteration = 0 + if(!trajectory_ignore_forcemove) + reset_physics_to_turf() + if(is_a_jump) + record_hitscan_start() -/obj/projectile/proc/fire(angle, atom/direct_target) +/obj/projectile/proc/fire(set_angle_to, atom/direct_target) if(only_submunitions) // refactor projectiles whwen holy shit this is awful lmao + // todo: this should make a muzzle flash qdel(src) return //If no angle needs to resolve it from xo/yo! if(direct_target) direct_target.bullet_act(src, def_zone) + // todo: this should make a muzzle flash qdel(src) return - if(isnum(angle)) - setAngle(angle) + if(isnum(set_angle_to)) + set_angle(set_angle_to) + + // setup physics + setup_physics() + var/turf/starting = get_turf(src) - if(isnull(Angle)) //Try to resolve through offsets if there's no angle set. + if(isnull(angle)) //Try to resolve through offsets if there's no angle set. if(isnull(xo) || isnull(yo)) stack_trace("WARNING: Projectile [type] deleted due to being unable to resolve a target after angle was null!") qdel(src) return var/turf/target = locate(clamp(starting + xo, 1, world.maxx), clamp(starting + yo, 1, world.maxy), starting.z) - setAngle(get_visual_angle(src, target)) + set_angle(get_visual_angle(src, target)) if(dispersion) - setAngle(Angle + rand(-dispersion, dispersion)) - original_angle = Angle - trajectory_ignore_forcemove = TRUE + set_angle(angle + rand(-dispersion, dispersion)) + original_angle = angle forceMove(starting) - trajectory_ignore_forcemove = FALSE - trajectory = new(starting.x, starting.y, starting.z, pixel_x, pixel_y, Angle, SSprojectiles.global_pixel_speed) - last_projectile_move = world.time permutated = list() - originalRange = range fired = TRUE + // kickstart if(hitscan) - . = process_hitscan() - START_PROCESSING(SSprojectiles, src) - pixel_move(1, FALSE) //move it now! + physics_hitscan() + else + START_PROCESSING(SSprojectiles, src) + physics_iteration(WORLD_ICON_SIZE, SSprojectiles.wait) /obj/projectile/Move(atom/newloc, dir = NONE) . = ..() if(.) - if(temporary_unstoppable_movement) - temporary_unstoppable_movement = FALSE - movement_type &= ~MOVEMENT_UNSTOPPABLE if(fired && can_hit_target(original, permutated, TRUE)) Bump(original) -/obj/projectile/proc/after_z_change(atom/olcloc, atom/newloc) - -/obj/projectile/proc/before_z_change(atom/oldloc, atom/newloc) - -/obj/projectile/proc/before_move() - return - -/obj/projectile/proc/after_move() - return - -/obj/projectile/proc/store_hitscan_collision(datum/point/pcache) - if(!has_tracer) - return - beam_segments[beam_index] = pcache - beam_index = pcache - beam_segments[beam_index] = null - //Spread is FORCED! /obj/projectile/proc/preparePixelProjectile(atom/target, atom/source, params, spread = 0) var/turf/curloc = get_turf(source) @@ -410,18 +329,18 @@ if(targloc || !params) yo = targloc.y - curloc.y xo = targloc.x - curloc.x - setAngle(get_visual_angle(src, targloc) + spread) + set_angle(get_visual_angle(src, targloc) + spread) if(isliving(source) && params) var/list/calculated = calculate_projectile_angle_and_pixel_offsets(source, params) p_x = calculated[2] p_y = calculated[3] - setAngle(calculated[1] + spread) + set_angle(calculated[1] + spread) else if(targloc) yo = targloc.y - curloc.y xo = targloc.x - curloc.x - setAngle(get_visual_angle(src, targloc) + spread) + set_angle(get_visual_angle(src, targloc) + spread) else stack_trace("WARNING: Projectile [type] fired without either mouse parameters, or a target atom to aim at!") qdel(src) @@ -464,21 +383,7 @@ source = get_turf(src) starting = get_turf(source) original = target - setAngle(get_visual_angle(source, target)) - -/obj/projectile/Destroy() - if(hitscan) - finalize_hitscan_and_generate_tracers() - STOP_PROCESSING(SSprojectiles, src) - qdel(trajectory) - return ..() - -/obj/projectile/proc/cleanup_beam_segments() - if(!has_tracer) - return - QDEL_LIST_ASSOC(beam_segments) - beam_segments = list() - qdel(beam_index) + set_angle(get_visual_angle(source, target)) /obj/projectile/proc/vol_by_damage() if(damage) @@ -486,44 +391,6 @@ else return 50 //if the projectile doesn't do damage, play its hitsound at 50% volume. -/obj/projectile/proc/finalize_hitscan_and_generate_tracers(impacting = TRUE) - if(!has_tracer) - return - if(trajectory && beam_index) - var/datum/point/pcache = trajectory.copy_to() - beam_segments[beam_index] = pcache - generate_hitscan_tracers(null, null, impacting) - -/obj/projectile/proc/generate_hitscan_tracers(cleanup = TRUE, duration = 5, impacting = TRUE) - if(!length(beam_segments)) - return - beam_components = new - if(tracer_type) - var/tempref = "\ref[src]" - for(var/datum/point/p in beam_segments) - generate_tracer_between_points(p, beam_segments[p], beam_components, tracer_type, color, duration, hitscan_light_range, hitscan_light_color_override, hitscan_light_intensity, tempref) - if(muzzle_type && duration > 0) - var/datum/point/p = beam_segments[1] - var/atom/movable/thing = new muzzle_type - p.move_atom_to_src(thing) - var/matrix/M = new - M.Turn(original_angle) - thing.transform = M - thing.color = color - thing.set_light(muzzle_flash_range, muzzle_flash_intensity, muzzle_flash_color_override? muzzle_flash_color_override : color) - beam_components.beam_components += thing - if(impacting && impact_type && duration > 0) - var/datum/point/p = beam_segments[beam_segments[beam_segments.len]] - var/atom/movable/thing = new impact_type - p.move_atom_to_src(thing) - var/matrix/M = new - M.Turn(Angle) - thing.transform = M - thing.color = color - thing.set_light(impact_light_range, impact_light_intensity, impact_light_color_override? impact_light_color_override : color) - beam_components.beam_components += thing - QDEL_IN(beam_components, duration) - //Returns true if the target atom is on our current turf and above the right layer //If direct target is true it's the originally clicked target. /obj/projectile/proc/can_hit_target(atom/target, list/passthrough, direct_target = FALSE, ignore_loc = FALSE) @@ -613,6 +480,18 @@ if(A) on_impact(A) + + if(hitscanning) + if(trajectory_moving_to) + // create tracers + var/datum/point/visual_impact_point = get_intersection_point(trajectory_moving_to) + // kick it forwards a bit + visual_impact_point.shift_in_projectile_angle(angle, 2) + // draw + finalize_hitscan_tracers(visual_impact_point, impact_effect = TRUE) + else + finalize_hitscan_tracers(impact_effect = TRUE, kick_forwards = 32) + qdel(src) return TRUE @@ -743,7 +622,7 @@ SM.damage = damage_override if(submunition_constant_spread) SM.dispersion = 0 - var/calculated = Angle + round((count / amt - 0.5) * submunition_spread_max, 1) + var/calculated = angle + round((count / amt - 0.5) * submunition_spread_max, 1) SM.launch_projectile(target, target_zone, user, params, calculated) else SM.dispersion = rand(temp_min_spread, submunition_spread_max) / 10 @@ -803,7 +682,618 @@ /obj/projectile/proc/get_final_damage(atom/target) return run_damage_vulnerability(target) -//? Targeting +//* Hitscan Visuals *// + +/** + * returns a /datum/point based on where we currently are + */ +/obj/projectile/proc/get_tracer_point() + RETURN_TYPE(/datum/point) + var/datum/point/point = new + if(trajectory_moving_to) + // we're in move. use next px/py to respect 1. kick forwards 2. deflections + point.x = (trajectory_moving_to.x - 1) * WORLD_ICON_SIZE + next_px + point.y = (trajectory_moving_to.y - 1) * WORLD_ICON_SIZE + next_py + else + point.x = (x - 1) * WORLD_ICON_SIZE + current_px + point.y = (y - 1) * WORLD_ICON_SIZE + current_py + point.z = z + return point + +/** + * * returns a /datum/point based on where we'll be when we loosely intersect a tile + * * returns null if we'll never intersect it + * * returns our current point if we're already loosely intersecting it + * * loosely intersecting means that we are level with the tile in either x or y. + */ +/obj/projectile/proc/get_intersection_point(turf/colliding) + RETURN_TYPE(/datum/point) + ASSERT(!isnull(angle)) + + // calculate hwere we are + var/our_x = (x - 1) * WORLD_ICON_SIZE + current_px + var/our_y = (y - 1) * WORLD_ICON_SIZE + current_py + + // calculate how far we have to go to touch their closest x / y axis + var/d_to_reach_x + var/d_to_reach_y + + if(colliding.x != x) + switch(calculated_sdx) + if(0) + return + if(1) + if(colliding.x < x) + return + d_to_reach_x = (((colliding.x - 1) * WORLD_ICON_SIZE + 0.5) - our_x) / calculated_dx + if(-1) + if(colliding.x > x) + return + d_to_reach_x = (((colliding.x - 0) * WORLD_ICON_SIZE + 0.5) - our_x) / calculated_dx + else + d_to_reach_x = 0 + + if(colliding.y != y) + switch(calculated_sdy) + if(0) + return + if(1) + if(colliding.y < y) + return + d_to_reach_y = (((colliding.y - 1) * WORLD_ICON_SIZE + 0.5) - our_y) / calculated_dy + if(-1) + if(colliding.y > y) + return + d_to_reach_y = (((colliding.y - 0) * WORLD_ICON_SIZE + 0.5) - our_y) / calculated_dy + else + d_to_reach_y = 0 + + var/needed_distance = max(d_to_reach_x, d_to_reach_y) + + // calculate if we'll actually be touching the tile once we go that far + var/future_x = our_x + needed_distance * calculated_dx + var/future_y = our_y + needed_distance * calculated_dy + // let's be slightly lenient and do 1 instead of 0.5 + if(future_x < (colliding.x - 1) * WORLD_ICON_SIZE && future_x > (colliding.x) * WORLD_ICON_SIZE + 1 && \ + future_y < (colliding.y - 1) * WORLD_ICON_SIZE && future_y > (colliding.y) * WORLD_ICON_SIZE + 1) + return // not gonna happen + + // make the point based on how far we need to go + var/datum/point/point = new + point.x = future_x + point.y = future_y + point.z = z + return point + +/** + * records the start of a hitscan + * + * this can edit the point passed in! + */ +/obj/projectile/proc/record_hitscan_start(datum/point/point, muzzle_marker, kick_forwards) + if(!hitscanning) + return + if(isnull(point)) + point = get_tracer_point() + tracer_vertices = list(point) + tracer_muzzle_flash = muzzle_marker + + // kick forwards + point.shift_in_projectile_angle(angle, kick_forwards) + +/** + * ends the hitscan tracer + * + * this can edit the point passed in! + */ +/obj/projectile/proc/record_hitscan_end(datum/point/point, impact_marker, kick_forwards) + if(!hitscanning) + return + if(isnull(point)) + point = get_tracer_point() + tracer_vertices += point + tracer_impact_effect = impact_marker + + // kick forwards + point.shift_in_projectile_angle(angle, kick_forwards) + +/** + * records a deflection (change in angle, aka generate new tracer) + */ +/obj/projectile/proc/record_hitscan_deflection(datum/point/point) + if(!hitscanning) + return + if(isnull(point)) + point = get_tracer_point() + // there's no way you need more than 25 + // if this is hit, fix your shit, don't bump this up; there's absolutely no reason for example, + // to simulate reflectors working !!25!! times. + if(length(tracer_vertices) >= 25) + CRASH("tried to add more than 25 vertices to a hitscan tracer") + tracer_vertices += point + +/obj/projectile/proc/render_hitscan_tracers(duration = tracer_duration) + // don't stay too long + ASSERT(duration >= 0 && duration <= 10 SECONDS) + // check everything + if(!has_tracer || !duration || !length(tracer_vertices)) + return + var/list/atom/movable/beam_components = list() + + // muzzle + if(muzzle_type && tracer_muzzle_flash) + var/datum/point/starting = tracer_vertices[1] + var/atom/movable/muzzle_effect = starting.instantiate_movable_with_unmanaged_offsets(muzzle_type) + if(muzzle_effect) + // turn it + var/matrix/muzzle_transform = matrix() + muzzle_transform.Turn(original_angle) + muzzle_effect.transform = muzzle_transform + muzzle_effect.color = color + muzzle_effect.set_light(muzzle_flash_range, muzzle_flash_intensity, muzzle_flash_color_override? muzzle_flash_color_override : color) + // add to list + beam_components += muzzle_effect + // impact + if(impact_type && tracer_impact_effect) + var/datum/point/starting = tracer_vertices[length(tracer_vertices)] + var/atom/movable/impact_effect = starting.instantiate_movable_with_unmanaged_offsets(impact_type) + if(impact_effect) + // turn it + var/matrix/impact_transform = matrix() + impact_transform.Turn(angle) + impact_effect.transform = impact_transform + impact_effect.color = color + impact_effect.set_light(impact_light_range, impact_light_intensity, impact_light_color_override? impact_light_color_override : color) + // add to list + beam_components += impact_effect + // path tracers + if(tracer_type) + var/tempref = "\ref[src]" + for(var/i in 1 to length(tracer_vertices) - 1) + var/j = i + 1 + var/datum/point/first_point = tracer_vertices[i] + var/datum/point/second_point = tracer_vertices[j] + generate_tracer_between_points(first_point, second_point, beam_components, tracer_type, color, duration, hitscan_light_range, hitscan_light_color_override, hitscan_light_intensity, tempref) + + QDEL_LIST_IN(beam_components, duration) + + +/obj/projectile/proc/cleanup_hitscan_tracers() + QDEL_NULL(tracer_vertices) + +/obj/projectile/proc/finalize_hitscan_tracers(datum/point/end_point, impact_effect, kick_forwards) + // if end wasn't recorded yet and we're still on a turf, record end + if(isnull(tracer_impact_effect) && loc) + record_hitscan_end(end_point, impact_marker = impact_effect, kick_forwards = kick_forwards) + // render & cleanup + render_hitscan_tracers() + cleanup_hitscan_tracers() + +//* Physics - Configuration *// + +/** + * sets our angle + */ +/obj/projectile/proc/set_angle(new_angle) + angle = new_angle + + // update sprite + if(!nondirectional_sprite) + var/matrix/M = new + M.Turn(angle) + transform = M + + // update trajectory + calculated_dx = sin(new_angle) + calculated_dy = cos(new_angle) + calculated_sdx = calculated_dx == 0? 0 : (calculated_dx > 0? 1 : -1) + calculated_sdy = calculated_dy == 0? 0 : (calculated_dy > 0? 1 : -1) + + // record our tracer's change + if(hitscanning) + record_hitscan_deflection() + +/** + * sets our speed in pixels per decisecond + */ +/obj/projectile/proc/set_speed(new_speed) + speed = clamp(new_speed, 1, WORLD_ICON_SIZE * 5) + +/** + * sets our angle and speed + */ +/obj/projectile/proc/set_velocity(new_angle, new_speed) + // this is so this can be micro-optimized later but for once i'm not going to do it early for no reason + set_speed(new_speed) + set_angle(new_angle) + +/** + * todo: this is somewhat mildly terrible + */ +/obj/projectile/proc/set_homing_target(atom/A) + if(!A || (!isturf(A) && !isturf(A.loc))) + return FALSE + homing = TRUE + homing_target = A + homing_offset_x = rand(homing_inaccuracy_min, homing_inaccuracy_max) + homing_offset_y = rand(homing_inaccuracy_min, homing_inaccuracy_max) + if(prob(50)) + homing_offset_x = -homing_offset_x + if(prob(50)) + homing_offset_y = -homing_offset_y + +/** + * initializes physics vars + */ +/obj/projectile/proc/setup_physics() + distance_travelled = 0 + +/** + * called after an unhandled forcemove is detected, or other event + * that should reset our on-turf state + */ +/obj/projectile/proc/reset_physics_to_turf() + // we use this because we can center larger than 32x32 projectiles + // without disrupting physics this way + // + // we add by (WORLD_ICON_SIZE / 2) because + // pixel_x / pixel_y starts at center, + // + current_px = pixel_x - base_pixel_x + (WORLD_ICON_SIZE / 2) + current_py = pixel_y - base_pixel_y + (WORLD_ICON_SIZE / 2) + // interrupt active move logic + trajectory_moving_to = null + +//* Physics - Processing *// + +/obj/projectile/process(delta_time) + if(paused) + return + delta_time *= 10 // sigh im fucking mad but whatever why are we using delta_time as seconds and not deciseconds + physics_iteration(delta_time * speed, delta_time) + +/** + * immediately processes hitscan + */ +/obj/projectile/proc/physics_hitscan(safety = 250, resuming) + // setup + if(!resuming) + hitscanning = TRUE + record_hitscan_start(muzzle_marker = TRUE, kick_forwards = 16) + + // just move as many times as we can + while(!QDELETED(src) && loc) + // check safety + safety-- + if(safety <= 0) + // if you're here, you shouldn't be. do not bump safety up, fix whatever + // you're doing because no one should be making projectiles go more than 250 + // tiles in a single life. + stack_trace("projectile hit iteration limit for hitscan") + break + + // move forwards by 1 tile length + distance_travelled += physics_step(WORLD_ICON_SIZE) + // if we're being yanked, yield + if(movable_flags & MOVABLE_IN_MOVED_YANK) + spawn(0) + physics_hitscan(safety, TRUE) + return + + // see if we're done + if(distance_travelled >= range) + legacy_on_range() + break + + hitscanning = FALSE + +/** + * ticks forwards a number of pixels + * + * todo: potential lazy animate support for performance, as we honestly don't need to animate at full fps if the server's above 20fps + * + * * delta_tiem is in deciseconds, not seconds. + */ +/obj/projectile/proc/physics_iteration(pixels, delta_time, additional_animation_length) + // setup iteration + var/safety = 10 + var/pixels_remaining = pixels + distance_travelled_this_iteration = 0 + + // apply penalty + var/penalizing = clamp(trajectory_penalty_applied, 0, pixels_remaining) + pixels_remaining -= penalizing + trajectory_penalty_applied -= penalizing + + // clamp to max distance + pixels_remaining = min(pixels_remaining, range - distance_travelled) + + // move as many times as we need to + // + // * break if we're loc = null (by deletion or otherwise) + // * break if we get paused + while(pixels_remaining > 0) + // check safety + safety-- + if(safety <= 0) + CRASH("ran out of safety! what happened?") + + // move + var/pixels_moved = physics_step(pixels_remaining) + distance_travelled += pixels_moved + distance_travelled_this_iteration += pixels_moved + pixels_remaining -= pixels_moved + // we're being yanked, yield + if(movable_flags & MOVABLE_IN_MOVED_YANK) + spawn(0) + physics_iteration(pixels_remaining, delta_time, distance_travelled_this_iteration) + return + if(!loc || paused) + break + + // penalize next one if we were kicked forwards forcefully too far + trajectory_penalty_applied = max(0, -pixels_remaining) + + // if we don't have a loc anymore just bail + if(!loc) + return + + // if we're at max range + if(distance_travelled >= range) + // todo: egh + legacy_on_range() + if(QDELETED(src)) + return + + // process homing + physics_tick_homing(delta_time) + + // perform animations + // we assume at this point any deflections that should have happened, has happened, + // so we just do a naive animate based on our current loc and pixel x/y + // + // todo: animation needs to take into account angle changes, + // but that's expensive as shit so uh lol + // + // the reason we use distance_travelled_this_iteration is so if something disappears + // by forceMove or whatnot, + // we won't have it bounce from its previous location to the new one as it's not going + // to be accurate anymore + // + // so instead, as of right now, we backtrack via how much we know we moved. + var/final_px = base_pixel_x + current_px - (WORLD_ICON_SIZE / 2) + var/final_py = base_pixel_y + current_py - (WORLD_ICON_SIZE / 2) + var/anim_dist = distance_travelled_this_iteration + additional_animation_length + pixel_x = final_px - (anim_dist * sin(angle)) + pixel_y = final_py - (anim_dist * cos(angle)) + + animate( + src, + delta_time, + flags = ANIMATION_END_NOW, + pixel_x = final_px, + pixel_y = final_py, + ) + +/** + * based on but exactly http://www.cs.yorku.ca/~amana/research/grid.pdf + * + * move into the next tile, or the specified number of pixels, + * whichever is less pixels moved + * + * this will modify our current_px/current_py as necessary + * + * @return pixels moved + */ +/obj/projectile/proc/physics_step(limit) + // distance to move in our angle to get to next turf for horizontal and vertical + var/d_next_horizontal = \ + (calculated_sdx? ((calculated_sdx > 0? (WORLD_ICON_SIZE + 0.5) - current_px : -current_px + 0.5) / calculated_dx) : INFINITY) + var/d_next_vertical = \ + (calculated_sdy? ((calculated_sdy > 0? (WORLD_ICON_SIZE + 0.5) - current_py : -current_py + 0.5) / calculated_dy) : INFINITY) + var/turf/move_to_target + + /** + * explanation on why current and next are done: + * + * projectiles track their pixel x/y on turf, not absolute pixel x/y from edge of map + * this is done to make it simpler to reason about, but is not necessarily the most simple + * or efficient way to do things. + * + * part of the problems with this approach is that Move() is not infallible. the projectile can be blocked. + * if we immediately set current pixel x/y, if the projectile is intercepted by a Bump, we now dont' know the 'real' + * position of the projectile because it's out of sync with where it should be + * + * now, things that require math operations on it don't know the actual location of the projectile until this proc + * rolls it back + * + * so instead, we never touch current px/py until the move is known to be successful, then we set it + * to the stored next px/py + * + * this way, things accessing can mutate our state freely without worrying about needing to handle rollbacks + * + * this entire system however adds overhead + * if we want to not have overhead, we'll need to rewrite hit processing and have it so moves are fully illegal to fail + * but doing that is literally not possible because anything can reject a move for any reason whatsoever + * and we cannot control that, so, instead, we make projectiles track in absolute pixel x/y coordinates from edge of map + * + * that way, we don't even need to care about where the .loc is, we just know where the projectile is supposed to be by + * knowing where it isn't, and by taking the change in its pixels the projectile controller can tell the projectile + * where to go- + * + * (all shitposting aside, this is for future work; it works right now and we have an API to do set angle, kick forwards, etc) + * (so i'm not going to touch this more because it's 4 AM and honestly this entire raycaster is already far less overhead) + * (than the old system of a 16-loop of brute forced 2 pixel increments) + */ + + if(d_next_horizontal == d_next_vertical) + // we're diagonal + if(d_next_horizontal <= limit) + move_to_target = locate(x + calculated_sdx, y + calculated_sdy, z) + . = d_next_horizontal + if(!move_to_target) + // we hit the world edge and weren't transit; time to get deleted. + if(hitscanning) + finalize_hitscan_tracers(impact_effect = FALSE) + qdel(src) + return + next_px = calculated_sdx > 0? 0.5 : (WORLD_ICON_SIZE + 0.5) + next_py = calculated_sdy > 0? 0.5 : (WORLD_ICON_SIZE + 0.5) + else if(d_next_horizontal < d_next_vertical) + // closer is to move left/right + if(d_next_horizontal <= limit) + move_to_target = locate(x + calculated_sdx, y, z) + . = d_next_horizontal + if(!move_to_target) + // we hit the world edge and weren't transit; time to get deleted. + if(hitscanning) + finalize_hitscan_tracers(impact_effect = FALSE) + qdel(src) + return + next_px = calculated_sdx > 0? 0.5 : (WORLD_ICON_SIZE + 0.5) + next_py = current_py + d_next_horizontal * calculated_dy + else if(d_next_vertical < d_next_horizontal) + // closer is to move up/down + if(d_next_vertical <= limit) + move_to_target = locate(x, y + calculated_sdy, z) + . = d_next_vertical + if(!move_to_target) + // we hit the world edge and weren't transit; time to get deleted. + if(hitscanning) + finalize_hitscan_tracers(impact_effect = FALSE) + qdel(src) + return + next_px = current_px + d_next_vertical * calculated_dx + next_py = calculated_sdy > 0? 0.5 : (WORLD_ICON_SIZE + 0.5) + + // if we need to move + if(move_to_target) + var/atom/old_loc = loc + trajectory_moving_to = move_to_target + if(!Move(move_to_target) && ((loc != move_to_target) || !trajectory_moving_to)) + // if we don't successfully move, don't change anything, we didn't move. + . = 0 + if(loc == old_loc) + stack_trace("projectile failed to move, but is still on turf instead of deleted or relocated.") + qdel(src) // bye + else + // only do these if we successfully move, or somehow end up in that turf anyways + if(trajectory_kick_forwards) + . += trajectory_kick_forwards + trajectory_kick_forwards = 0 + current_px = next_px + current_py = next_py + #ifdef CF_PROJECTILE_RAYCAST_VISUALS + new /atom/movable/render/projectile_raycast(move_to_target, current_px, current_py, "#77ff77") + #endif + trajectory_moving_to = null + else + // not moving to another tile, so, just move on current tile + if(trajectory_kick_forwards) + trajectory_kick_forwards = 0 + stack_trace("how did something kick us forwards when we didn't even move?") + . = limit + current_px += limit * calculated_dx + current_py += limit * calculated_dy + next_px = current_px + next_py = current_py + #ifdef CF_PROJECTILE_RAYCAST_VISUALS + new /atom/movable/render/projectile_raycast(loc, current_px, current_py, "#ff3333") + #endif + +#ifdef CF_PROJECTILE_RAYCAST_VISUALS +GLOBAL_VAR_INIT(projectile_raycast_debug_visual_delay, 2 SECONDS) + +/atom/movable/render/projectile_raycast + plane = OBJ_PLANE + icon = 'icons/system/color_32x32.dmi' + icon_state = "white-pixel" + +/** + * px, py are absolute pixel coordinates on the tile, not pixel_x / pixel_y of this renderer! + */ +/atom/movable/render/projectile_raycast/Initialize(mapload, px, py, color) + src.pixel_x = px - 1 + src.pixel_y = py - 1 + src.color = color + . = ..() + QDEL_IN(src, GLOB.projectile_raycast_debug_visual_delay) +#endif + +/** + * immediately, without processing, kicks us forward a number of pixels + * + * since we immediately cross over into a turf when entering, + * things like mirrors/reflectors will immediately set angle + * + * it looks ugly and is pretty bad to just reflect off the edge of a turf so said things can + * call this proc to kick us forwards by a bit + */ +/obj/projectile/proc/physics_kick_forwards(pixels) + trajectory_kick_forwards += pixels + next_px += pixels * calculated_dx + next_py += pixels * calculated_dy + +/** + * only works during non-hitscan + * + * this is called once per tick + * homing is smoother the higher fps the server / SSprojectiles runs at + * + * todo: this is somewhat mildly terrible + * todo: this has absolutely no arc/animation support; this is bad + */ +/obj/projectile/proc/physics_tick_homing(delta_time) + // checks if they're 1. on a turf, 2. on our z + // todo: should we add support for tracking something even if it leaves a turf? + if(homing_target?.z != z) + // bye bye! + return FALSE + // todo: this assumes single-tile objects. at some point, we should upgrade this to be unnecessarily expensive and always center-mass. + var/dx = (homing_target.x - src.x) * WORLD_ICON_SIZE + (0 - current_px) + var/dy = (homing_target.y - src.y) * WORLD_ICON_SIZE + (0 - current_py) + // say it with me, arctan() + // is CCW of east if (dx, dy) + // and CW of north if (dy, dx) + // where dx and dy is distance in x/y pixels from us to them. + + var/nudge_towards = closer_angle_difference(arctan(dy, dx)) + var/max_turn_speed = homing_turn_speed * delta_time + + set_angle(angle + clamp(nudge_towards, -max_turn_speed, max_turn_speed)) + +//* Physics - Querying *// + +/** + * predict what turf we'll be in after going forwards a certain amount of pixels + * + * doesn't actually sim; so this will go through walls/obstacles! + * + * * if we go out of bounds, we will return null; this doesn't level-wrap + */ +/obj/projectile/proc/physics_predicted_turf_after_iteration(pixels) + // -1 at the end if 0, because: + // + // -32 is go back 1 tile and be at the 1st pixel (as 0 is going back) + // 0 is go back 1 tile and be at the 32nd pixel. + var/incremented_px = (current_px + pixels * calculated_dx) || - 1 + var/incremented_py = (current_py + pixels * calculated_dy) || - 1 + + var/incremented_tx = floor(incremented_px / 32) + var/incremented_ty = floor(incremented_py / 32) + + return locate(x + incremented_tx, y + incremented_ty, z) + +/** + * predict what turfs we'll hit, excluding the current turf, after going forwards + * a certain amount of pixels + * + * doesn't actually sim; so this will go through walls/obstacles! + */ +/obj/projectile/proc/physics_predicted_turfs_during_iteration(pixels) + return pixel_physics_raycast(loc, current_px, current_py, angle, pixels) + +//* Targeting *// /** * Checks if something is a valid target when directly clicked. diff --git a/code/modules/projectiles/projectile/arc.dm b/code/modules/projectiles/projectile/arc.dm index ba2b2be7fad..d4fb271b296 100644 --- a/code/modules/projectiles/projectile/arc.dm +++ b/code/modules/projectiles/projectile/arc.dm @@ -44,7 +44,7 @@ var/datum/point/starting_point = new(starting) return pixel_length_between_points(current_point, starting_point) -/obj/projectile/arc/on_range() +/obj/projectile/arc/legacy_on_range() if(loc) on_impact(loc) return ..() @@ -64,17 +64,17 @@ /obj/projectile/arc/fire(angle, atom/direct_target) ..() // The trajectory must exist for set_pixel_speed() to work. - set_pixel_speed(projectile_speed_modifier) // Slows it down and makes the distance checking more accurate. + set_speed(32 * projectile_speed_modifier) -/obj/projectile/arc/pixel_move(trajectory_multiplier, hitscanning = FALSE) +/obj/projectile/arc/physics_iteration(pixels) // Do the other important stuff first. - ..(trajectory_multiplier, hitscanning) + . = ..() // Test to see if its time to 'hit the ground'. var/pixels_flown = distance_flown() if(pixels_flown >= distance_to_fly) - on_range() // This will also cause the projectile to be deleted. + legacy_on_range() // This will also cause the projectile to be deleted. else // Handle visual projectile turning in flight. diff --git a/code/modules/projectiles/projectile/bullets.dm b/code/modules/projectiles/projectile/bullets.dm index e14c5a10382..759ec43f3a7 100644 --- a/code/modules/projectiles/projectile/bullets.dm +++ b/code/modules/projectiles/projectile/bullets.dm @@ -129,11 +129,13 @@ agony = 10 // brute easily heals, agony not so much armor_penetration = 30 // reduces shield blockchance accuracy = -20 // he do miss actually - speed = 0.4 // if the pathfinder gets a funny burst rifle, they deserve a rival - // that's 2x projectile speed btw + // if the pathfinder gets a funny burst rifle, they deserve a rival + // ~25 tiles/second + speed = 32 / 0.4 /obj/projectile/bullet/pistol/medium/ap/suppressor/turbo // spicy boys - speed = 0.2 // this is 4x projectile speed + // ~50 tiles/second + speed = 32 / 0.2 /obj/projectile/bullet/pistol/strong // .357 and .44 caliber stuff. High power pistols like the Mateba or Desert Eagle. Sacrifice capacity for power. fire_sound = 'sound/weapons/weaponsounds_heavypistolshot.ogg' @@ -300,7 +302,8 @@ SA_bonus_damage = 45 // 70 total on animals. SA_vulnerability = MOB_CLASS_ANIMAL embed_chance = -1 - speed = 0.4 + // ~25 tiles/second + speed = 32 / 0.4 /obj/projectile/bullet/rifle/a762/silver // Hunting Demons with bolt action rifles. damage = 20 @@ -387,7 +390,8 @@ /obj/projectile/bullet/musket // Big Slow and bad against armor. fire_sound = 'sound/weapons/weaponsounds_heavypistolshot.ogg' damage = 60 - speed = 1.2 + // ~8.3 tiles/second + speed = 32 / 1.2 armor_penetration = -50 /obj/projectile/bullet/musket/silver // What its a classic @@ -486,7 +490,7 @@ //incendiary = 2 //The Trail of Fire doesn't work. flammability = 4 agony = 30 - range = 4 + range = WORLD_ICON_SIZE * 4 vacuum_traversal = 0 /obj/projectile/bullet/incendiary/flamethrower/weak @@ -494,7 +498,7 @@ /obj/projectile/bullet/incendiary/flamethrower/large damage = 15 - range = 6 + range = WORLD_ICON_SIZE * 6 /obj/projectile/bullet/incendiary/caseless name = "12.7mm phoron slug" diff --git a/code/modules/projectiles/projectile/bullets_vr.dm b/code/modules/projectiles/projectile/bullets_vr.dm index 257cb4aef5e..a8198510700 100644 --- a/code/modules/projectiles/projectile/bullets_vr.dm +++ b/code/modules/projectiles/projectile/bullets_vr.dm @@ -9,7 +9,7 @@ name = "chemical shell" icon_state = "bullet" damage = 10 - range = 15 //if the shell hasn't hit anything after travelling this far it just explodes. + range = WORLD_ICON_SIZE * 15 //if the shell hasn't hit anything after travelling this far it just explodes. flash_strength = 15 brightness = 15 diff --git a/code/modules/projectiles/projectile/energy.dm b/code/modules/projectiles/projectile/energy.dm index 3479abf411f..0e734ded4a5 100644 --- a/code/modules/projectiles/projectile/energy.dm +++ b/code/modules/projectiles/projectile/energy.dm @@ -12,7 +12,7 @@ icon_state = "bullet" fire_sound = 'sound/weapons/gunshot_pathetic.ogg' damage = 5 - range = 15 //if the shell hasn't hit anything after travelling this far it just explodes. + range = WORLD_ICON_SIZE * 15 //if the shell hasn't hit anything after travelling this far it just explodes. var/flash_range = 0 var/brightness = 7 var/light_colour = "#ffffff" @@ -171,7 +171,7 @@ icon_state = "plasma_stun" fire_sound = 'sound/weapons/blaster.ogg' armor_penetration = 10 - range = 4 + range = WORLD_ICON_SIZE * 4 damage = 5 agony = 55 damage_type = BURN @@ -223,20 +223,20 @@ /obj/projectile/energy/phase name = "phase wave" icon_state = "phase" - range = 25 + range = WORLD_ICON_SIZE * 25 damage = 5 SA_bonus_damage = 45 // 50 total on animals SA_vulnerability = MOB_CLASS_ANIMAL /obj/projectile/energy/phase/light - range = 15 + range = WORLD_ICON_SIZE * 15 SA_bonus_damage = 35 // 40 total on animals /obj/projectile/energy/phase/heavy - range = 20 + range = WORLD_ICON_SIZE * 20 SA_bonus_damage = 55 // 60 total on animals /obj/projectile/energy/phase/heavy/cannon - range = 30 + range = WORLD_ICON_SIZE * 30 damage = 15 SA_bonus_damage = 60 // 75 total on animals diff --git a/code/modules/projectiles/projectile/hook.dm b/code/modules/projectiles/projectile/hook.dm index 987beff181e..f01bdb90578 100644 --- a/code/modules/projectiles/projectile/hook.dm +++ b/code/modules/projectiles/projectile/hook.dm @@ -9,7 +9,8 @@ var/beam_state = "b_beam" damage = 5 - speed = 2 + // ~5 tiles/second + speed = 32 / 2 damage_type = BURN damage_flag = ARMOR_ENERGY armor_penetration = 15 @@ -29,7 +30,7 @@ /obj/projectile/energy/hook/launch_projectile(atom/target, target_zone, mob/user, params, angle_override, forced_spread = 0) var/expected_distance = get_dist(target, loc) - range = expected_distance // So the hook hits the ground if no mob is hit. + range = WORLD_ICON_SIZE * expected_distance // So the hook hits the ground if no mob is hit. target_distance = expected_distance if(firer) // Needed to ensure later checks in impact and on hit function. launcher_intent = firer.a_intent diff --git a/code/modules/projectiles/projectile/magnetic.dm b/code/modules/projectiles/projectile/magnetic.dm index e1728daf168..1705a9c1c65 100644 --- a/code/modules/projectiles/projectile/magnetic.dm +++ b/code/modules/projectiles/projectile/magnetic.dm @@ -56,7 +56,7 @@ penetrating = 2 embed_chance = 0 armor_penetration = 40 - range = 20 + range = WORLD_ICON_SIZE * 20 var/searing = 0 //Does this fuelrod ignore shields? var/detonate_travel = 0 //Will this fuelrod explode when it reaches maximum distance? @@ -125,7 +125,7 @@ armor_penetration = 100 penetrating = 100 //Theoretically, this shouldn't stop flying for a while, unless someone lines it up with a wall or fires it into a mountain. irradiate = 120 - range = 75 + range = WORLD_ICON_SIZE * 75 searing = 1 detonate_travel = 1 detonate_mob = 1 @@ -149,7 +149,7 @@ penetrating = 0 damage_flag = ARMOR_MELEE irradiate = 20 - range = 6 + range = WORLD_ICON_SIZE * 6 /obj/projectile/bullet/magnetic/bore/Bump(atom/A, forced=0) if(istype(A, /turf/simulated/mineral)) @@ -174,4 +174,4 @@ penetrating = 0 damage_flag = ARMOR_MELEE irradiate = 20 - range = 12 + range = WORLD_ICON_SIZE * 12 diff --git a/code/modules/projectiles/projectile/reusable.dm b/code/modules/projectiles/projectile/reusable.dm index 41dbc2937b8..a2f5fabbb96 100644 --- a/code/modules/projectiles/projectile/reusable.dm +++ b/code/modules/projectiles/projectile/reusable.dm @@ -16,7 +16,7 @@ handle_drop() //handle_shatter() -/obj/projectile/bullet/reusable/on_range() +/obj/projectile/bullet/reusable/legacy_on_range() handle_drop() ..() diff --git a/code/modules/projectiles/projectile/scatter.dm b/code/modules/projectiles/projectile/scatter.dm index 075891889c4..11f78d5ee41 100644 --- a/code/modules/projectiles/projectile/scatter.dm +++ b/code/modules/projectiles/projectile/scatter.dm @@ -13,7 +13,7 @@ damage = 8 spread_submunition_damage = TRUE only_submunitions = TRUE - range = 0 // Immediately deletes itself after firing, as its only job is to fire other projectiles. + range = WORLD_ICON_SIZE * 0 // Immediately deletes itself after firing, as its only job is to fire other projectiles. submunition_spread_max = 30 submunition_spread_min = 2 diff --git a/code/modules/projectiles/projectile/special.dm b/code/modules/projectiles/projectile/special.dm index 918337188c1..8cffa1a2ca4 100644 --- a/code/modules/projectiles/projectile/special.dm +++ b/code/modules/projectiles/projectile/special.dm @@ -334,7 +334,7 @@ impact_sounds = 'sound/items/bikehorn.ogg' icon = 'icons/obj/items.dmi' icon_state = "banana" - range = 200 + range = WORLD_ICON_SIZE * 200 /obj/projectile/bullet/honker/Initialize(mapload) . = ..() diff --git a/code/modules/projectiles/projectile/trace.dm b/code/modules/projectiles/projectile/trace.dm index c7433c1e205..321a12a7180 100644 --- a/code/modules/projectiles/projectile/trace.dm +++ b/code/modules/projectiles/projectile/trace.dm @@ -25,10 +25,8 @@ has_tracer = FALSE var/list/hit = list() -/obj/projectile/test/process_hitscan() - . = ..() - if(!QDELING(src)) - qdel(src) +/obj/projectile/test/fire(angle, atom/direct_target) + ..() return hit /obj/projectile/test/Bump(atom/A) diff --git a/code/modules/projectiles/unsorted/magic.dm b/code/modules/projectiles/unsorted/magic.dm index 52ed8b77d14..aa8735a1f02 100644 --- a/code/modules/projectiles/unsorted/magic.dm +++ b/code/modules/projectiles/unsorted/magic.dm @@ -403,13 +403,12 @@ damage = 0 var/proxdet = TRUE -/obj/projectile/magic/aoe/Range() +/obj/projectile/magic/aoe/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() if(proxdet) for(var/mob/living/L in range(1, get_turf(src))) if(L.stat != DEAD && L != firer && !L.anti_magic_check()) - return Bump(L) - ..() - + Bump(L) /obj/projectile/magic/aoe/lightning name = "lightning bolt" diff --git a/code/modules/spells/spell_projectile.dm b/code/modules/spells/spell_projectile.dm index 0247ce78357..60538b4ba7b 100644 --- a/code/modules/spells/spell_projectile.dm +++ b/code/modules/spells/spell_projectile.dm @@ -7,7 +7,7 @@ var/spell/targeted/projectile/carried penetrating = 0 - range = 10 //set by the duration of the spell + range = WORLD_ICON_SIZE * 10 //set by the duration of the spell var/proj_trail = 0 //if it leaves a trail var/proj_trail_lifespan = 0 //deciseconds @@ -24,9 +24,10 @@ /obj/projectile/spell_projectile/legacy_ex_act() return -/obj/projectile/spell_projectile/before_move() - if(proj_trail && src && src.loc) //pretty trails - var/obj/effect/overlay/trail = new /obj/effect/overlay(src.loc) +/obj/projectile/spell_projectile/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + if(proj_trail && old_loc) //pretty trails + var/obj/effect/overlay/trail = new /obj/effect/overlay(old_loc) trails += trail trail.icon = proj_trail_icon trail.icon_state = proj_trail_icon_state diff --git a/code/modules/spells/targeted/projectile/projectile.dm b/code/modules/spells/targeted/projectile/projectile.dm index a7b81eef3db..ee66d07df19 100644 --- a/code/modules/spells/targeted/projectile/projectile.dm +++ b/code/modules/spells/targeted/projectile/projectile.dm @@ -29,7 +29,7 @@ If the spell_projectile is seeking, it will update its target every process and projectile.shot_from = user //fired from the user projectile.hitscan = !proj_step_delay - projectile.speed = proj_step_delay + projectile.speed = 32 / proj_step_delay if(istype(projectile, /obj/projectile/spell_projectile)) var/obj/projectile/spell_projectile/SP = projectile SP.carried = src //casting is magical diff --git a/code/modules/vore/fluffstuff/guns/pummeler.dm b/code/modules/vore/fluffstuff/guns/pummeler.dm index 932e1dccae5..1c4283e0b10 100644 --- a/code/modules/vore/fluffstuff/guns/pummeler.dm +++ b/code/modules/vore/fluffstuff/guns/pummeler.dm @@ -34,7 +34,7 @@ damage_flag = ARMOR_MELEE embed_chance = 0 vacuum_traversal = 0 - range = 6 //Scary name, but just deletes the projectile after this range + range = WORLD_ICON_SIZE * 6 //Scary name, but just deletes the projectile after this range /obj/projectile/pummel/on_hit(var/atom/movable/target, var/blocked = 0) if(isliving(target)) diff --git a/code/modules/vore/fluffstuff/guns/secutor.dm b/code/modules/vore/fluffstuff/guns/secutor.dm index b7646c38dc9..dabf9b8d152 100644 --- a/code/modules/vore/fluffstuff/guns/secutor.dm +++ b/code/modules/vore/fluffstuff/guns/secutor.dm @@ -74,7 +74,7 @@ light_range = 2 light_power = 0.6 light_color = "#cea036" - range = 20 + range = WORLD_ICON_SIZE * 20 damage = 5 //minor penalty for repeated FF SA_bonus_damage = 25 // 30 total on animals - lowest of all phasers SA_vulnerability = MOB_CLASS_ANIMAL diff --git a/code/modules/vore/fluffstuff/guns/sickshot.dm b/code/modules/vore/fluffstuff/guns/sickshot.dm index 1fdbb998dab..efa27a57772 100644 --- a/code/modules/vore/fluffstuff/guns/sickshot.dm +++ b/code/modules/vore/fluffstuff/guns/sickshot.dm @@ -32,7 +32,7 @@ damage_flag = ARMOR_MELEE embed_chance = 0 vacuum_traversal = 0 - range = 5 //Scary name, but just deletes the projectile after this range + range = WORLD_ICON_SIZE * 5 //Scary name, but just deletes the projectile after this range /obj/projectile/sickshot/on_hit(var/atom/movable/target, var/blocked = 0) if(isliving(target)) diff --git a/icons/obj/projectiles_impact.dmi b/icons/obj/projectiles_impact.dmi index 3788e515562..af9947dc64c 100644 Binary files a/icons/obj/projectiles_impact.dmi and b/icons/obj/projectiles_impact.dmi differ diff --git a/icons/obj/projectiles_muzzle.dmi b/icons/obj/projectiles_muzzle.dmi index b361df92763..ab4eefab272 100644 Binary files a/icons/obj/projectiles_muzzle.dmi and b/icons/obj/projectiles_muzzle.dmi differ diff --git a/icons/system/color_32x32.dmi b/icons/system/color_32x32.dmi index 826ba84db3d..b12a3115233 100644 Binary files a/icons/system/color_32x32.dmi and b/icons/system/color_32x32.dmi differ