mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-08-19 07:06:35 +01:00
tl;dr level collections that can easily be logically restructured/moved as necessary will eventually be used for radio and overmaps location resolution. this is the last part of https://github.com/Citadel-Station-13/Citadel-Station-13-RP/pull/4841 that has not yet been integrated. this will be a draft for a while. --------- Co-authored-by: LetterN <24603524+LetterN@users.noreply.github.com>
452 lines
11 KiB
Plaintext
452 lines
11 KiB
Plaintext
SUBSYSTEM_DEF(throwing)
|
|
name = "Throwing"
|
|
priority = FIRE_PRIORITY_THROWING
|
|
wait = 0.25 // scale up to 40 fps
|
|
subsystem_flags = SS_NO_INIT
|
|
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
|
|
|
|
var/list/currentrun
|
|
var/list/processing = list()
|
|
|
|
/datum/controller/subsystem/throwing/stat_entry()
|
|
return ..() + " P:[processing.len]"
|
|
|
|
/datum/controller/subsystem/throwing/fire(resumed = 0)
|
|
if (!resumed)
|
|
src.currentrun = processing.Copy()
|
|
|
|
//cache for sanic speed (lists are references anyways)
|
|
var/list/currentrun = src.currentrun
|
|
|
|
while(length(currentrun))
|
|
var/atom/movable/AM = currentrun[currentrun.len]
|
|
var/datum/thrownthing/TT = currentrun[AM]
|
|
currentrun.len--
|
|
if (QDELETED(AM) || QDELETED(TT))
|
|
processing -= AM
|
|
if (MC_TICK_CHECK)
|
|
return
|
|
continue
|
|
|
|
TT.tick()
|
|
|
|
if (MC_TICK_CHECK)
|
|
return
|
|
|
|
currentrun = null
|
|
|
|
// todo: the landing stack kinda sucks ass and needs to be unit tested and rewritten
|
|
|
|
/datum/thrownthing
|
|
//! important stuff
|
|
/// thing we threw
|
|
var/atom/movable/thrownthing
|
|
/// flags
|
|
var/throw_flags = NONE
|
|
/// things we impacted already. associative list for speed.
|
|
var/list/impacted
|
|
/// thing we originally were thrown at
|
|
var/atom/target
|
|
/// turf our original target was on at original time of throw
|
|
var/turf/target_turf
|
|
/// turf we initially started from
|
|
var/turf/initial_turf
|
|
/// initial byond get_dir from thrown thing to target
|
|
var/init_dir
|
|
/// max tiles we can travel
|
|
var/maxrange
|
|
/// tiles to move per decisecond
|
|
var/speed
|
|
/// what threw us
|
|
var/atom/thrower
|
|
/// how hard were we thrown?
|
|
var/force
|
|
/// original throw resist
|
|
var/resist
|
|
/// callback to call when we hit something. called with (hit atom, thrownthing datum)
|
|
var/datum/callback/on_hit
|
|
/// callback to call when we land. will not be called if we do not land on anything. called with (landed atom, thrownthing datum)
|
|
var/datum/callback/on_land
|
|
/// paused?
|
|
var/paused = FALSE
|
|
|
|
//! processing vars
|
|
/// have we started yet
|
|
var/started = FALSE
|
|
/// are we done
|
|
var/finished = FALSE
|
|
/// world.time we started
|
|
var/start_time
|
|
/// tiles we travelled
|
|
var/dist_travelled = 0
|
|
/// how long we've been paused for
|
|
var/delayed_time = 0
|
|
/// last world.time we moved
|
|
var/last_move = 0
|
|
/// dx of original throw target
|
|
var/dist_x
|
|
/// dy of original throw target
|
|
var/dist_y
|
|
/// x dir
|
|
var/dx
|
|
/// y dir
|
|
var/dy
|
|
/// current targeted angle
|
|
var/current_angle
|
|
/// tracks diagonal error so we move in a relatively "raycasted" (shittily) path
|
|
var/diagonal_error
|
|
/// are we purely diagonal?
|
|
var/pure_diagonal
|
|
/// What did we hit that's causing us to land?
|
|
var/atom/landing_from_impact
|
|
|
|
//! fluff shit players use to kill each other
|
|
/// zone selected by user at time of throw
|
|
var/target_zone
|
|
/// damage multiplier
|
|
var/damage_multiplier = 1
|
|
|
|
/datum/thrownthing/New(atom/movable/AM, atom/target, range, speed, flags, atom/thrower, datum/callback/on_hit, datum/callback/on_land, force)
|
|
src.thrownthing = AM
|
|
if(!target_atom(target))
|
|
qdel(src)
|
|
return
|
|
// todo: multiz throws
|
|
src.maxrange = range
|
|
src.speed = speed
|
|
src.throw_flags = flags
|
|
src.thrower = thrower
|
|
src.on_hit = on_hit
|
|
src.on_land = on_land
|
|
src.force = force
|
|
src.resist = AM.throw_resist
|
|
impacted = list()
|
|
|
|
/datum/thrownthing/Destroy()
|
|
. = ..()
|
|
if(!finished)
|
|
terminate(TRUE)
|
|
if(thrownthing)
|
|
SSthrowing.processing -= thrownthing
|
|
thrownthing.throwing = null
|
|
thrownthing = null
|
|
landing_from_impact = null
|
|
target = null
|
|
thrower = null
|
|
on_hit = null
|
|
on_land = null
|
|
initial_turf = null
|
|
target_turf = null
|
|
impacted = null
|
|
return ..()
|
|
|
|
/datum/thrownthing/proc/target_atom(atom/target)
|
|
src.target = target
|
|
var/turf/T = get_turf(target)
|
|
var/atom/movable/AM = thrownthing
|
|
if(!T)
|
|
CRASH("tried to throw something at something that wasn't in the game world.")
|
|
target_turf = T
|
|
T = get_turf(AM)
|
|
if(!T)
|
|
CRASH("tried to throw something that wasn't in the game world at something")
|
|
if(!initial_turf)
|
|
initial_turf = T
|
|
init_dir = get_dir(thrownthing, target)
|
|
|
|
dist_x = abs(target_turf.x - AM.x)
|
|
dist_y = abs(target_turf.y - AM.y)
|
|
current_angle = arctan(target_turf.y - AM.y, target_turf.x - AM.x)
|
|
dx = (target_turf.x > AM.x)? EAST : WEST
|
|
dy = (target_turf.y > AM.y)? NORTH : SOUTH
|
|
|
|
if(dist_x == dist_y)
|
|
pure_diagonal = TRUE
|
|
else if(dist_x <= dist_y)
|
|
// standardization: i honestly don't understand why we do this but we do this, thanks MSO
|
|
var/olddist_x = dist_x
|
|
var/olddx = dx
|
|
dist_x = dist_y
|
|
dist_y = olddist_x
|
|
dx = dy
|
|
dy = olddx
|
|
diagonal_error = dist_x / 2 - dist_y
|
|
return TRUE
|
|
|
|
/datum/thrownthing/proc/tick()
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
var/atom/movable/AM = thrownthing
|
|
// if throwing got cancelled maybe like, don't
|
|
if(!isturf(AM.loc) || !AM.throwing)
|
|
terminate()
|
|
return
|
|
|
|
if(paused)
|
|
delayed_time += world.time - last_move
|
|
return
|
|
|
|
//calculate how many tiles to move, making up for any missed ticks.
|
|
var/tilestomove = CEILING(min(((((world.time+world.tick_lag) - start_time + delayed_time) * speed) - (dist_travelled ? dist_travelled : -1)), speed*MAX_TICKS_TO_MAKE_UP) * (world.tick_lag * SSthrowing.wait), 1)
|
|
|
|
// catch anything in our tile
|
|
//? experimental: removed in favor for cross/bump hooks
|
|
/*
|
|
if(dist_travelled)
|
|
var/atom/to_hit = scan_for_impact(get_turf(AM))
|
|
while(to_hit)
|
|
impact(to_hit)
|
|
if(finished)
|
|
return
|
|
to_hit = scan_for_impact(get_turf(AM))
|
|
*/
|
|
|
|
var/atom/stepping
|
|
last_move = world.time
|
|
while(tilestomove-- > 0)
|
|
// if we have gravity we can end, else keep going
|
|
if(AM.has_gravity())
|
|
if(dist_travelled >= maxrange || AM.loc == target_turf)
|
|
land()
|
|
return
|
|
else if(dist_travelled >= MAX_THROWING_DIST)
|
|
terminate()
|
|
return
|
|
|
|
// if we havne't reached target yet
|
|
if(dist_travelled <= max(dist_x, dist_y))
|
|
// home in
|
|
stepping = get_step(AM, get_dir(AM, target_turf))
|
|
else
|
|
// just go init dir, diagonal error solves the rest
|
|
stepping = get_step(AM, init_dir)
|
|
|
|
// solve diagonal error
|
|
if(!pure_diagonal)
|
|
// checks for tile before so we don't raycast past it
|
|
if(diagonal_error >= 0 && (max(dist_x, dist_y) - dist_travelled != 1))
|
|
stepping = get_step(AM, dx)
|
|
diagonal_error += (diagonal_error < 0)? (dist_x / 2) : -dist_y
|
|
|
|
// if we're out of tiles.. don't run out of the map
|
|
if(!stepping)
|
|
land()
|
|
return
|
|
|
|
// move
|
|
AM.Move(stepping, get_dir(AM, stepping))
|
|
|
|
// atom somehow got deleted
|
|
if(QDELETED(AM))
|
|
terminate()
|
|
return
|
|
|
|
// we hit something during move
|
|
if(finished)
|
|
return
|
|
|
|
// tick up dist moved
|
|
++dist_travelled
|
|
|
|
/**
|
|
* hook for making us impact things on bump (we cross them)
|
|
*/
|
|
/datum/thrownthing/proc/bump_into(atom/A)
|
|
// if you sleep, eat shit
|
|
set waitfor = FALSE
|
|
if(!can_hit(A, TRUE))
|
|
return
|
|
impact(A)
|
|
|
|
/**
|
|
* hook for making us impact thing on cross (they cross us)
|
|
*/
|
|
/datum/thrownthing/proc/crossed_by(atom/movable/AM)
|
|
// if you sleep, eat shit
|
|
set waitfor = FALSE
|
|
if(!can_hit(AM))
|
|
return
|
|
impact(AM)
|
|
|
|
/datum/thrownthing/proc/scan_for_impact(turf/T)
|
|
RETURN_TYPE(/atom)
|
|
var/atom/highest
|
|
for(var/atom/thing as anything in T)
|
|
if(!can_hit(thing))
|
|
continue
|
|
if(!highest || highest.layer < thing.layer)
|
|
highest = thing
|
|
return highest
|
|
|
|
/datum/thrownthing/proc/can_hit(atom/A, bumping)
|
|
if((throw_flags & THROW_AT_OVERHAND) && (A != target) && A.check_pass_flags_self(ATOM_PASS_OVERHEAD_THROW))
|
|
return FALSE
|
|
if(A == thrownthing)
|
|
return FALSE
|
|
if(A == thrower)
|
|
return FALSE
|
|
if(impacted[A])
|
|
return FALSE
|
|
if(!bumping && thrownthing.CanPass(A, get_turf(thrownthing)))
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/**
|
|
* quickstart - immediately tick the first tick
|
|
*/
|
|
/datum/thrownthing/proc/quickstart()
|
|
if(throw_flags & THROW_AT_QUICKSTARTED)
|
|
return
|
|
throw_flags |= THROW_AT_QUICKSTARTED
|
|
tick(1)
|
|
|
|
/**
|
|
* start - register to subsystem
|
|
*/
|
|
/datum/thrownthing/proc/start()
|
|
if(started)
|
|
CRASH("double start")
|
|
start_time = world.time
|
|
started = TRUE
|
|
|
|
SSthrowing.processing[thrownthing] = src
|
|
if(SSthrowing.state == SS_PAUSED && length(SSthrowing.currentrun))
|
|
SSthrowing.currentrun[thrownthing] = src
|
|
|
|
if(!(throw_flags & THROW_AT_NO_AUTO_QUICKSTART))
|
|
quickstart()
|
|
|
|
/**
|
|
* handle impacting an atom
|
|
* return TRUE if we should end the throw, FALSE to pierce
|
|
*/
|
|
/datum/thrownthing/proc/impact(atom/impacting, in_land)
|
|
impacted[impacting] = TRUE
|
|
|
|
var/op_return = thrownthing._throw_do_hit(impacting, src)
|
|
if(op_return & COMPONENT_THROW_HIT_TERMINATE)
|
|
terminate()
|
|
return
|
|
|
|
on_hit?.InvokeAsync(impacting, src)
|
|
|
|
if(!(op_return & COMPONENT_THROW_HIT_PIERCE) && !in_land)
|
|
landing_from_impact = impacting
|
|
land(get_turf(thrownthing))
|
|
landing_from_impact = null
|
|
return
|
|
|
|
// we are piercing. move again.
|
|
tick(1)
|
|
|
|
/**
|
|
* land on something and terminate the throw
|
|
*/
|
|
/datum/thrownthing/proc/land(atom/landing = get_turf(thrownthing))
|
|
// todo: need to rewrite to consider qdel's for object + us maybe?
|
|
// nothing to land on
|
|
if(!landing)
|
|
terminate()
|
|
return
|
|
|
|
// hit our target if we haven't already
|
|
if(!impacted[target] && (target in get_turf(landing)) && target.is_throw_explicit_targetable(src))
|
|
impact(target, TRUE)
|
|
|
|
// we got terminated already
|
|
if(finished)
|
|
return
|
|
|
|
// land
|
|
thrownthing._throw_finalize(landing, src)
|
|
on_land?.InvokeAsync(landing, src)
|
|
|
|
// halt
|
|
terminate()
|
|
|
|
/**
|
|
* terminate the throw.
|
|
* when called, immediately erases the throw from the atom and stops it.
|
|
*/
|
|
/datum/thrownthing/proc/terminate(in_qdel)
|
|
// todo: shitcode, rewrite
|
|
if(QDELETED(src))
|
|
return
|
|
finished = TRUE
|
|
thrownthing.throwing = null
|
|
if(!QDELETED(thrownthing))
|
|
// move
|
|
addtimer(CALLBACK(thrownthing, TYPE_PROC_REF(/atom/movable, newtonian_move), init_dir), 1)
|
|
addtimer(CALLBACK(thrownthing, TYPE_PROC_REF(/atom/movable, fall)), 1)
|
|
if(in_qdel)
|
|
return
|
|
qdel(src)
|
|
|
|
|
|
/**
|
|
* should we skip damage entirely?
|
|
*/
|
|
/datum/thrownthing/proc/is_pacifistic()
|
|
return throw_flags & THROW_AT_IS_GENTLE
|
|
|
|
/**
|
|
* get damage scaling - default handling
|
|
*
|
|
* @params
|
|
* * target - (optional) thing being hit
|
|
*/
|
|
/datum/thrownthing/proc/get_damage_multiplier(atom/target)
|
|
if(!resist)
|
|
return MAX_THROWING_DAMAGE_MULTIPLIER
|
|
. = damage_multiplier
|
|
if(thrownthing.movable_flags & MOVABLE_NO_THROW_DAMAGE_SCALING)
|
|
return
|
|
if(throw_flags & THROW_AT_NO_SCALE_DAMAGE)
|
|
return
|
|
// todo: this should be damage_multipliers and damage_classifier based
|
|
// for now, snowflake.
|
|
if(ismob(thrownthing) && isobj(target))
|
|
. *= 2.5 // :trol:
|
|
// multiplier = force > resist? (force / resist) ** (p * 0.1) : 1 / (force / resist) ** (p * 0.1)
|
|
if(isnull(force))
|
|
. *= speed > resist? (speed / resist) ** (thrownthing.throw_damage_scaling_exponential * 0.1) : 1 / (speed / resist) ** (thrownthing.throw_damage_scaling_exponential * 0.1)
|
|
. *= force > resist? (force / resist) ** (thrownthing.throw_damage_scaling_exponential * 0.1) : 1 / (force / resist) ** (thrownthing.throw_damage_scaling_exponential * 0.1)
|
|
|
|
/**
|
|
* get damage tier - default handling
|
|
*/
|
|
/datum/thrownthing/proc/get_damage_tier(atom/target)
|
|
if(isitem(thrownthing))
|
|
var/obj/item/thing = thrownthing
|
|
return thing.damage_tier
|
|
if(iscarbon(thrownthing))
|
|
return 4 // :trol:
|
|
return 2.5
|
|
|
|
/**
|
|
* Get angle we're flying
|
|
*/
|
|
/datum/thrownthing/proc/get_current_angle()
|
|
return current_angle
|
|
|
|
/**
|
|
* simulated thrownthing datums
|
|
* doesn't register to subsystem
|
|
* immediately hits and deletes on start
|
|
*/
|
|
/datum/thrownthing/emulated
|
|
|
|
/datum/thrownthing/emulated/start()
|
|
return // you must manually quickstart
|
|
|
|
/datum/thrownthing/emulated/quickstart()
|
|
if(throw_flags & THROW_AT_QUICKSTARTED)
|
|
return
|
|
throw_flags |= THROW_AT_QUICKSTARTED
|
|
process_hit()
|
|
|
|
/datum/thrownthing/emulated/proc/process_hit()
|
|
// hit without landing
|
|
impact(target, TRUE)
|
|
// gtfo
|
|
terminate()
|