SSprojectiles - smoother and saner projectiles (#17127)

* SSprojectiles - smoother and saner projectiles

* trailing newline

* toki wo tomare

* Lower projectile speed back to 2x, reflector fix

* Tables stop projectiles properly
This commit is contained in:
dearmochi
2022-02-25 18:30:05 +00:00
committed by GitHub
parent bb2eceedaf
commit 5693eb827e
9 changed files with 395 additions and 106 deletions
@@ -0,0 +1,12 @@
PROCESSING_SUBSYSTEM_DEF(projectiles)
name = "Projectiles"
wait = 1
flags = SS_NO_INIT|SS_TICKER
offline_implications = "Projectiles will no longer move. Shuttle call recommended."
/// Maximum moves a projectile can make per tick.
var/global_max_tick_moves = 10
/// How many pixels one iteration can move a projectile.
var/global_pixel_speed = 2
/// Maximum iterations a move can perform.
var/global_iterations_per_move = 16
+242
View File
@@ -0,0 +1,242 @@
//Designed for things that need precision trajectories like projectiles.
//Don't use this for anything that you don't absolutely have to use this with (like projectiles!) because it isn't worth using a datum unless you need accuracy down to decimal places in pixels.
//You might see places where it does - 16 - 1. This is intentionally 17 instead of 16, because of how byond's tiles work and how not doing it will result in rounding errors like things getting put on the wrong turf.
#define RETURN_PRECISE_POSITION(A) new /datum/position(A)
#define RETURN_PRECISE_POINT(A) new /datum/point_precise(A)
#define RETURN_POINT_VECTOR(ATOM, ANGLE, SPEED) (new /datum/point_precise/vector(ATOM, null, null, null, null, ANGLE, SPEED))
#define RETURN_POINT_VECTOR_INCREMENT(ATOM, ANGLE, SPEED, AMT) (new /datum/point_precise/vector(ATOM, null, null, null, null, ANGLE, SPEED, AMT))
/proc/point_midpoint_points(datum/point_precise/a, datum/point_precise/b) //Obviously will not support multiZ calculations! Same for the two below.
var/datum/point_precise/P = new
P.x = a.x + (b.x - a.x) * 0.5
P.y = a.y + (b.y - a.y) * 0.5
P.z = a.z
return P
/proc/pixel_length_between_points(datum/point_precise/a, datum/point_precise/b)
return sqrt(((b.x - a.x) ** 2) + ((b.y - a.y) ** 2))
/proc/angle_between_points(datum/point_precise/a, datum/point_precise/b)
return ATAN2((b.y - a.y), (b.x - a.x))
/// 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.
/datum/position
var/x = 0
var/y = 0
var/z = 0
var/pixel_x = 0
var/pixel_y = 0
/datum/position/proc/valid()
return x && y && z && !isnull(pixel_x) && !isnull(pixel_y)
/datum/position/New(_x = 0, _y = 0, _z = 0, _pixel_x = 0, _pixel_y = 0) //first argument can also be a /datum/point_precise.
if(istype(_x, /datum/point_precise))
var/datum/point_precise/P = _x
var/turf/T = P.return_turf()
_x = T.x
_y = T.y
_z = T.z
_pixel_x = P.return_px()
_pixel_y = P.return_py()
else if(isatom(_x))
var/atom/A = _x
_x = A.x
_y = A.y
_z = A.z
_pixel_x = A.pixel_x
_pixel_y = A.pixel_y
x = _x
y = _y
z = _z
pixel_x = _pixel_x
pixel_y = _pixel_y
/datum/position/proc/return_turf()
return locate(x, y, z)
/datum/position/proc/return_px()
return pixel_x
/datum/position/proc/return_py()
return pixel_y
/datum/position/proc/return_point()
return new /datum/point_precise(src)
/// A precise point on the map in absolute pixel locations based on world.icon_size. Pixels are FROM THE EDGE OF THE MAP!
/datum/point_precise
var/x = 0
var/y = 0
var/z = 0
/datum/point_precise/proc/valid()
return x && y && z
/datum/point_precise/proc/copy_to(datum/point_precise/p = new)
p.x = x
p.y = y
p.z = z
return p
/// First argument can also be a /datum/position or /atom.
/datum/point_precise/New(_x, _y, _z, _pixel_x = 0, _pixel_y = 0)
if(istype(_x, /datum/position))
var/datum/position/P = _x
_x = P.x
_y = P.y
_z = P.z
_pixel_x = P.pixel_x
_pixel_y = P.pixel_y
else if(istype(_x, /atom))
var/atom/A = _x
_x = A.x
_y = A.y
_z = A.z
_pixel_x = A.pixel_x
_pixel_y = A.pixel_y
initialize_location(_x, _y, _z, _pixel_x, _pixel_y)
/datum/point_precise/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 * 0.5 + p_x + 1
if(!isnull(tile_y))
y = ((tile_y - 1) * world.icon_size) + world.icon_size * 0.5 + p_y + 1
if(!isnull(tile_z))
z = tile_z
/datum/point_precise/proc/debug_out()
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_precise/proc/move_atom_to_src(atom/movable/AM)
AM.forceMove(return_turf())
AM.pixel_x = return_px()
AM.pixel_y = return_py()
/datum/point_precise/proc/return_turf()
return locate(CEILING(x / world.icon_size, 1), CEILING(y / world.icon_size, 1), z)
/datum/point_precise/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_precise/proc/return_position()
return new /datum/position(src)
/datum/point_precise/proc/return_px()
return MODULUS(x, world.icon_size) - 16 - 1
/datum/point_precise/proc/return_py()
return MODULUS(y, world.icon_size) - 16 - 1
/datum/point_precise/vector
/// Pixels per iteration
var/speed = 32
var/iteration = 0
var/angle = 0
/// Calculated x movement amounts to prevent having to do trig every step.
var/mpx = 0
/// Calculated y movement amounts to prevent having to do trig every step.
var/mpy = 0
var/starting_x = 0 //just like before, pixels from EDGE of map! This is set in initialize_location().
var/starting_y = 0
var/starting_z = 0
/datum/point_precise/vector/New(_x, _y, _z, _pixel_x = 0, _pixel_y = 0, _angle, _speed, initial_increment = 0)
..()
initialize_trajectory(_speed, _angle)
if(initial_increment)
increment(initial_increment)
/datum/point_precise/vector/initialize_location(tile_x, tile_y, tile_z, p_x = 0, p_y = 0)
. = ..()
starting_x = x
starting_y = y
starting_z = z
/// Same effect as initiliaze_location, but without setting the starting_x/y/z
/datum/point_precise/vector/proc/set_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 * 0.5 + p_x + 1
if(!isnull(tile_y))
y = ((tile_y - 1) * world.icon_size) + world.icon_size * 0.5 + p_y + 1
if(!isnull(tile_z))
z = tile_z
/datum/point_precise/vector/copy_to(datum/point_precise/vector/v = new)
..(v)
v.speed = speed
v.iteration = iteration
v.angle = angle
v.mpx = mpx
v.mpy = mpy
v.starting_x = starting_x
v.starting_y = starting_y
v.starting_z = starting_z
return v
/datum/point_precise/vector/proc/initialize_trajectory(pixel_speed, new_angle)
if(!isnull(pixel_speed))
speed = pixel_speed
set_angle(new_angle)
/// Calculations use "byond angle" where north is 0 instead of 90, and south is 180 instead of 270.
/datum/point_precise/vector/proc/set_angle(new_angle)
if(isnull(angle))
return
angle = new_angle
update_offsets()
/datum/point_precise/vector/proc/update_offsets()
mpx = sin(angle) * speed
mpy = cos(angle) * speed
/datum/point_precise/vector/proc/set_speed(new_speed)
if(isnull(new_speed) || speed == new_speed)
return
speed = new_speed
update_offsets()
/datum/point_precise/vector/proc/increment(multiplier = 1)
iteration++
x += mpx * (multiplier)
y += mpy * (multiplier)
/datum/point_precise/vector/proc/return_vector_after_increments(amount = 7, multiplier = 1, force_simulate = FALSE)
var/datum/point_precise/vector/v = copy_to()
if(force_simulate)
for(var/i in 1 to amount)
v.increment(multiplier)
else
v.increment(multiplier * amount)
return v
/datum/point_precise/vector/proc/on_z_change()
return
/datum/point_precise/vector/processed //pixel_speed is per decisecond.
var/last_process = 0
var/last_move = 0
var/paused = FALSE
/datum/point_precise/vector/processed/Destroy()
STOP_PROCESSING(SSprojectiles, src)
return ..()
/datum/point_precise/vector/processed/proc/start()
last_process = world.time
last_move = world.time
START_PROCESSING(SSprojectiles, src)
/datum/point_precise/vector/processed/process()
if(paused)
last_move += world.time - last_process
last_process = world.time
return
var/needed_time = world.time - last_move
last_process = world.time
last_move = world.time
increment(needed_time / SSprojectiles.wait)
+1 -1
View File
@@ -48,6 +48,6 @@
if(abs(incidence_s) > 90 && abs(incidence_s) < 270)
return FALSE
var/new_angle_s = SIMPLIFY_DEGREES(face_angle + incidence_s)
P.setAngle(new_angle_s)
P.set_angle(new_angle_s)
visible_message("<span class='warning'>[P] reflects off [src]!</span>")
return TRUE
+3 -5
View File
@@ -22,16 +22,14 @@
new_dir = 0
return ..() //Hits as normal, explodes or emps or whatever
reflect_turf = get_step(loc,new_dir)
reflect_turf = get_step(loc, new_dir)
P.original = reflect_turf
P.starting = reflector_turf
P.current = reflector_turf
P.yo = reflect_turf.y - reflector_turf.y
P.xo = reflect_turf.x - reflector_turf.x
P.ignore_source_check = TRUE //If shot by a laser, will now hit the mob that fired it
var/reflect_angle = dir2angle(new_dir)
P.setAngle(reflect_angle)
P.set_angle_centered(reflect_angle)
P.trajectory.set_location(reflect_turf.x, reflect_turf.y, reflect_turf.z)
new_dir = 0
return -1
+19 -25
View File
@@ -138,32 +138,26 @@
var/atom/movable/mover = caller
. = . || mover.checkpass(PASSTABLE)
//checks if projectile 'P' from turf 'from' can hit whatever is behind the table. Returns 1 if it can, 0 if bullet stops.
/**
* Determines whether a projectile crossing our turf should be stopped.
* Return FALSE to stop the projectile.
*
* Arguments:
* * P - The projectile trying to cross.
* * from - Where the projectile is located.
*/
/obj/structure/table/proc/check_cover(obj/item/projectile/P, turf/from)
var/turf/cover = flipped ? get_turf(src) : get_step(loc, get_dir(from, loc))
if(get_dist(P.starting, loc) <= 1) //Tables won't help you if people are THIS close
return 1
if(get_turf(P.original) == cover)
var/chance = 20
if(ismob(P.original))
var/mob/M = P.original
if(M.lying)
chance += 20 //Lying down lets you catch less bullets
if(flipped)
if(get_dir(loc, from) == dir) //Flipped tables catch mroe bullets
chance += 20
else
return 1 //But only from one side
if(prob(chance))
obj_integrity -= P.damage/2
if(obj_integrity > 0)
visible_message("<span class='warning'>[P] hits \the [src]!</span>")
return 0
else
visible_message("<span class='warning'>[src] breaks down!</span>")
deconstruct(FALSE)
return 1
return 1
. = TRUE
if(!flipped)
return
if(get_dist(P.starting, loc) <= 1) // Tables won't help you if people are THIS close
return
var/proj_dir = get_dir(from, loc)
var/block_dir = get_dir(get_step(loc, dir), loc)
if(proj_dir != block_dir) // Back/side shots may pass
return
if(prob(40))
return FALSE // Blocked
/obj/structure/table/CheckExit(atom/movable/O, turf/target)
if(istype(O) && O.checkpass(PASSTABLE))
+1 -1
View File
@@ -139,7 +139,7 @@
if(abs(incidence_s) > 90 && abs(incidence_s) < 270)
return FALSE
var/new_angle_s = SIMPLIFY_DEGREES(face_angle + incidence_s)
P.setAngle(new_angle_s)
P.set_angle(new_angle_s)
return TRUE
/turf/simulated/wall/dismantle_wall(devastated = FALSE, explode = FALSE)
@@ -52,7 +52,7 @@ emp_act
visible_message("<span class='danger'>[src] deflects the projectile!</span>", "<span class='userdanger'>You deflect the projectile!</span>")
if(mind.martial_art.reroute_deflection)
P.firer = src
P.setAngle(rand(0, 360))
P.set_angle(rand(0, 360))
return -1
else
return FALSE
+114 -73
View File
@@ -63,8 +63,16 @@
/// For when you want your projectile to have a chain coming out of the gun
var/chain = null
/// Last world.time the projectile proper moved
var/last_projectile_move = 0
/// Left over ticks in movement calculation
var/time_offset = 0
/// The projectile's trajectory
var/datum/point_precise/vector/trajectory
/// Instructs forceMove to NOT reset our trajectory to the new location!
var/trajectory_ignore_forcemove = FALSE
/obj/item/projectile/New()
permutated = list()
return ..()
/obj/item/projectile/proc/Range()
@@ -232,83 +240,102 @@
/obj/item/projectile/Process_Spacemove(movement_dir = 0)
return 1 //Bullets don't drift in space
/obj/item/projectile/process()
if(!loc || !trajectory)
return PROCESS_KILL
if(paused || !isturf(loc))
last_projectile_move = world.time
return
var/elapsed_time_deciseconds = (world.time - last_projectile_move) + time_offset
time_offset = 0
var/required_moves = FLOOR(elapsed_time_deciseconds / speed, 1)
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)
/obj/item/projectile/proc/pixel_move(trajectory_multiplier)
if(!loc || !trajectory)
return
last_projectile_move = world.time
// Keep on course
var/matrix/M = new
M.Turn(Angle)
transform = M
// Iterate
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)
trajectory_ignore_forcemove = TRUE
forceMove(T)
trajectory_ignore_forcemove = FALSE
pixel_x = trajectory.return_px()
pixel_y = trajectory.return_py()
forcemoved = TRUE
else if(T != loc)
step_towards(src, T)
if(original && (original.layer >= PROJECTILE_HIT_THRESHHOLD_LAYER || ismob(original)))
if(loc == get_turf(original) && !(original in permutated))
Bump(original, TRUE)
if(QDELETED(src)) //deleted on last move
return
if(!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/item/projectile/proc/fire(setAngle)
set waitfor = FALSE
if(setAngle)
Angle = setAngle
while(!QDELETED(src))
if(!paused)
if((!current || loc == current))
current = locate(clamp(x + xo, 1, world.maxx), clamp(y + yo, 1, world.maxy), z)
if(isnull(Angle))
Angle = round(get_angle(src, current))
if(spread)
Angle += (rand() - 0.5) * spread
var/matrix/M = new
M.Turn(Angle)
transform = M
var/Pixel_x = round(sin(Angle) + 16 * sin(Angle) * 2, 1)
var/Pixel_y = round(cos(Angle) + 16 * cos(Angle) * 2, 1)
var/pixel_x_offset = pixel_x + Pixel_x
var/pixel_y_offset = pixel_y + Pixel_y
var/new_x = x
var/new_y = y
while(pixel_x_offset > 16)
pixel_x_offset -= 32
pixel_x -= 32
new_x++ // x++
while(pixel_x_offset < -16)
pixel_x_offset += 32
pixel_x += 32
new_x--
while(pixel_y_offset > 16)
pixel_y_offset -= 32
pixel_y -= 32
new_y++
while(pixel_y_offset < -16)
pixel_y_offset += 32
pixel_y += 32
new_y--
speed = round(speed)
step_towards(src, locate(new_x, new_y, z))
if(QDELETED(src)) // It hit something during the move
return
if(speed <= 1)
pixel_x = pixel_x_offset
pixel_y = pixel_y_offset
else
animate(src, pixel_x = pixel_x_offset, pixel_y = pixel_y_offset, time = max(1, (speed <= 3 ? speed - 1 : speed)))
if(original && (original.layer >= 2.75 || ismob(original)))
if(loc == get_turf(original))
if(!(original in permutated))
Bump(original, TRUE)
Range()
sleep(max(1, speed))
if(!current || loc == current)
current = locate(clamp(x + xo, 1, world.maxx), clamp(y + yo, 1, world.maxy), z)
if(isnull(Angle))
Angle = round(get_angle(src, current))
if(spread)
Angle += (rand() - 0.5) * spread
// Turn right away
var/matrix/M = new
M.Turn(Angle)
transform = M
// Start flying
trajectory = new(x, y, z, pixel_x, pixel_y, Angle, SSprojectiles.global_pixel_speed)
last_projectile_move = world.time
START_PROCESSING(SSprojectiles, src)
pixel_move(1, FALSE)
/obj/item/projectile/proc/reflect_back(atom/source, list/position_modifiers = list(0, 0, 0, 0, 0, -1, 1, -2, 2))
if(starting)
var/new_x = starting.x + pick(position_modifiers)
var/new_y = starting.y + pick(position_modifiers)
var/turf/curloc = get_turf(source)
if(!starting)
return
var/new_x = starting.x + pick(position_modifiers)
var/new_y = starting.y + pick(position_modifiers)
var/turf/curloc = get_turf(source)
if(!curloc)
return
if(ismob(source))
firer = source // The reflecting mob will be the new firer
else
firer = null // Reflected by something other than a mob so firer will be null
if(ismob(source))
firer = source // The reflecting mob will be the new firer
else
firer = null // Reflected by something other than a mob so firer will be null
// redirect the projectile
original = locate(new_x, new_y, z)
starting = curloc
current = curloc
yo = new_y - curloc.y
xo = new_x - curloc.x
Angle = null // Will be calculated in fire()
// redirect the projectile
original = locate(new_x, new_y, z)
starting = curloc
current = curloc
yo = new_y - curloc.y
xo = new_x - curloc.x
set_angle(get_angle(curloc, original))
/obj/item/projectile/Crossed(atom/movable/AM, oldloc) //A mob moving on a tile with a projectile is hit by it.
..()
@@ -316,6 +343,7 @@
Bump(AM, 1)
/obj/item/projectile/Destroy()
STOP_PROCESSING(SSprojectiles, src)
ammo_casing = null
return ..()
@@ -337,9 +365,22 @@
return TRUE
return FALSE
/obj/item/projectile/proc/setAngle(new_angle) //wrapper for overrides.
/obj/item/projectile/set_angle(new_angle)
..()
Angle = new_angle
return TRUE
trajectory.set_angle(new_angle)
/obj/item/projectile/proc/set_angle_centered(new_angle)
set_angle(new_angle)
var/list/coordinates = trajectory.return_coordinates()
trajectory.set_location(coordinates[1], coordinates[2], coordinates[3]) // Sets the trajectory to the center of the tile it bounced at
/obj/item/projectile/experience_pressure_difference()
return
/obj/item/projectile/forceMove(atom/target)
. = ..()
if(QDELETED(src)) // we coulda bumped something
return
if(trajectory && !trajectory_ignore_forcemove && isturf(target))
trajectory.initialize_location(target.x, target.y, target.z, 0, 0)
+2
View File
@@ -279,6 +279,7 @@
#include "code\controllers\subsystem\processing\instruments.dm"
#include "code\controllers\subsystem\processing\obj.dm"
#include "code\controllers\subsystem\processing\processing.dm"
#include "code\controllers\subsystem\processing\projectiles.dm"
#include "code\controllers\subsystem\tickets\mentor_tickets.dm"
#include "code\controllers\subsystem\tickets\tickets.dm"
#include "code\datums\action.dm"
@@ -307,6 +308,7 @@
#include "code\datums\mutable_appearance.dm"
#include "code\datums\periodic_news.dm"
#include "code\datums\pipe_datums.dm"
#include "code\datums\position_point_vector.dm"
#include "code\datums\progressbar.dm"
#include "code\datums\radiation_wave.dm"
#include "code\datums\radio.dm"