mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-08-22 20:27:07 +01:00
436 lines
15 KiB
Plaintext
436 lines
15 KiB
Plaintext
//* This file is explicitly licensed under the MIT license. *//
|
|
//* Copyright (c) 2024 Citadel Station Developers *//
|
|
|
|
//* 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)
|
|
|
|
var/normalized_to_first_quadrant = MODULUS_F(new_angle, 90)
|
|
angle_chebyshev_divisor = cos(normalized_to_first_quadrant >= 45? (90 - normalized_to_first_quadrant) : normalized_to_first_quadrant)
|
|
|
|
// 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 * 100)
|
|
|
|
/**
|
|
* 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
|
|
physics_iteration(min(10 * WORLD_ICON_SIZE, delta_time * speed * SSprojectiles.global_projectile_speed_multiplier), delta_time)
|
|
|
|
/**
|
|
* immediately processes hitscan
|
|
*/
|
|
/obj/projectile/proc/physics_hitscan(safety = 250, resuming)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
// 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
|
|
var/pixels_moved = physics_step(WORLD_ICON_SIZE)
|
|
distance_travelled += pixels_moved
|
|
// 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 = 15
|
|
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
|
|
// this is also a catch-all for deletion
|
|
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
|
|
// mark next distance so impact processing can work
|
|
next_distance = distance_travelled + .
|
|
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)
|
|
if(!homing)
|
|
return FALSE
|
|
// 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) + homing_offset_x
|
|
var/dy = (homing_target.y - src.y) * WORLD_ICON_SIZE + (0 - current_py) + homing_offset_y
|
|
// 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)
|