mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-01-01 05:02:33 +00:00
* VV refactor: dropdown actions. * helps if it builds * append to list cuz we have multiple separators * fix datumrefresh * tiny cleanup * fix jump to turf * Apply suggestions from code review Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> Signed-off-by: warriorstar-orion <orion@snowfrost.garden> * Apply suggestions from code review Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> Signed-off-by: warriorstar-orion <orion@snowfrost.garden> * fix typo --------- Signed-off-by: warriorstar-orion <orion@snowfrost.garden> Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
1185 lines
42 KiB
Plaintext
1185 lines
42 KiB
Plaintext
/atom/movable
|
|
layer = 3
|
|
appearance_flags = TILE_BOUND
|
|
glide_size = 8 // Default, adjusted when mobs move based on their movement delays
|
|
var/last_move = null
|
|
/// A list containing arguments for Moved().
|
|
VAR_PRIVATE/tmp/list/active_movement
|
|
var/anchored = FALSE
|
|
var/move_resist = MOVE_RESIST_DEFAULT
|
|
var/move_force = MOVE_FORCE_DEFAULT
|
|
var/pull_force = PULL_FORCE_DEFAULT
|
|
// var/elevation = 2 - not used anywhere
|
|
var/move_speed = 10
|
|
var/l_move_time = 1
|
|
var/datum/thrownthing/throwing = null
|
|
var/throw_speed = 2 //How many tiles to move per ds when being thrown. Float values are fully supported
|
|
var/throw_range = 7
|
|
var/no_spin = FALSE
|
|
var/no_spin_thrown = FALSE
|
|
var/mob/pulledby = null
|
|
|
|
var/atom/movable/pulling
|
|
/// Face towards the atom while pulling it
|
|
var/face_while_pulling = FALSE
|
|
/// Whether this atom should have its dir automatically changed when it
|
|
/// moves. Setting this to FALSE allows for things such as directional windows
|
|
/// to retain dir on moving without snowflake code all of the place.
|
|
/// PARA: Doesn't set this currently to maintain current behavior for pulling items around,
|
|
/// because modifying direction on pull is expected.
|
|
var/set_dir_on_move = TRUE
|
|
var/throwforce = 0
|
|
|
|
///Are we moving with inertia? Mostly used as an optimization
|
|
var/inertia_moving = FALSE
|
|
///Delay in deciseconds between inertia based movement
|
|
var/inertia_move_delay = 5
|
|
///The last time we pushed off something
|
|
///This is a hack to get around dumb him him me scenarios
|
|
var/last_pushoff
|
|
|
|
var/moving_diagonally = 0 //0: not doing a diagonal move. 1 and 2: doing the first/second step of the diagonal move
|
|
var/list/client_mobs_in_contents
|
|
|
|
/// Either FALSE, [EMISSIVE_BLOCK_GENERIC], or [EMISSIVE_BLOCK_UNIQUE]
|
|
var/blocks_emissive = FALSE
|
|
///Internal holder for emissive blocker object, do not use directly use blocks_emissive
|
|
var/atom/movable/emissive_blocker/em_block
|
|
/// Icon state for thought bubbles. Normally set by mobs.
|
|
var/thought_bubble_image = "thought_bubble"
|
|
|
|
// Atmos
|
|
var/pressure_resistance = 10
|
|
var/last_high_pressure_movement_time = 0
|
|
|
|
/// UID for the atom which the current atom is orbiting
|
|
var/orbiting_uid = null
|
|
|
|
/*
|
|
Buckling Vars
|
|
*/
|
|
var/can_buckle = FALSE
|
|
/// Bed-like behaviour, forces the mob to lie down if buckle_lying != -1
|
|
var/buckle_lying = -1
|
|
/// Require people to be handcuffed before being able to buckle. eg: pipes
|
|
var/buckle_requires_restraints = 0
|
|
/// Lazylist of the mobs buckled to this object.
|
|
var/list/buckled_mobs = null
|
|
/// The Pixel_y to offset the buckled mob by
|
|
var/buckle_offset = 0
|
|
/// The max amount of mobs that can be buckled to this object. Currently set to 1 on every movable
|
|
var/max_buckled_mobs = 1
|
|
/// Can we pull the mob while they're buckled. Currently set to false on every movable
|
|
var/buckle_prevents_pull = FALSE
|
|
|
|
/// Used for icon smoothing. Won't smooth if it ain't anchored and can be unanchored. Only set to true on windows
|
|
var/can_be_unanchored = FALSE
|
|
|
|
///attempt to resume grab after moving instead of before.
|
|
var/atom/movable/moving_from_pull
|
|
///Holds information about any movement loops currently running/waiting to run on the movable. Lazy, will be null if nothing's going on
|
|
var/datum/movement_packet/move_packet
|
|
/// How far (in pixels) should this atom scatter when created/dropped/etc. Does not apply to mapped-in items.
|
|
var/scatter_distance = 0
|
|
|
|
/atom/movable/attempt_init(loc, ...)
|
|
var/turf/T = get_turf(src)
|
|
if(T && SSatoms.initialized != INITIALIZATION_INSSATOMS && GLOB.space_manager.is_zlevel_dirty(T.z))
|
|
GLOB.space_manager.postpone_init(T.z, src)
|
|
return
|
|
. = ..()
|
|
|
|
/atom/movable/Initialize(mapload)
|
|
. = ..()
|
|
switch(blocks_emissive)
|
|
if(EMISSIVE_BLOCK_GENERIC)
|
|
var/mutable_appearance/gen_emissive_blocker = mutable_appearance(icon, icon_state, plane = EMISSIVE_PLANE, alpha = src.alpha)
|
|
gen_emissive_blocker.color = EM_BLOCK_COLOR
|
|
gen_emissive_blocker.dir = dir
|
|
gen_emissive_blocker.appearance_flags |= appearance_flags
|
|
AddComponent(/datum/component/emissive_blocker, gen_emissive_blocker)
|
|
if(EMISSIVE_BLOCK_UNIQUE)
|
|
render_target = ref(src)
|
|
em_block = new(src, render_target)
|
|
add_overlay(list(em_block))
|
|
|
|
/atom/movable/proc/update_emissive_block()
|
|
if(!em_block && !QDELETED(src))
|
|
render_target = ref(src)
|
|
em_block = new(src, render_target)
|
|
add_overlay(list(em_block))
|
|
|
|
/atom/movable/Destroy()
|
|
var/turf/T = loc
|
|
unbuckle_all_mobs(force = TRUE)
|
|
QDEL_NULL(em_block)
|
|
|
|
. = ..()
|
|
|
|
if(loc)
|
|
loc.handle_atom_del(src)
|
|
|
|
for(var/atom/movable/AM in contents)
|
|
qdel(AM)
|
|
|
|
LAZYCLEARLIST(client_mobs_in_contents)
|
|
loc = null
|
|
if(pulledby)
|
|
pulledby.stop_pulling()
|
|
if(move_packet)
|
|
if(!QDELETED(move_packet))
|
|
qdel(move_packet)
|
|
move_packet = null
|
|
|
|
if(opacity && istype(T))
|
|
var/old_has_opaque_atom = T.has_opaque_atom
|
|
T.recalc_atom_opacity()
|
|
if(old_has_opaque_atom != T.has_opaque_atom)
|
|
T.reconsider_lights()
|
|
|
|
//Returns an atom's power cell, if it has one. Overload for individual items.
|
|
/atom/movable/proc/get_cell()
|
|
return
|
|
|
|
/atom/movable/proc/compressor_grind()
|
|
ex_act(EXPLODE_DEVASTATE)
|
|
|
|
/atom/movable/proc/start_pulling(atom/movable/AM, state, force = pull_force, show_message = FALSE)
|
|
if(QDELETED(AM))
|
|
return FALSE
|
|
if(!(AM.can_be_pulled(src, state, force)))
|
|
return FALSE
|
|
|
|
// if we're pulling something then drop what we're currently pulling and pull this instead.
|
|
if(pulling)
|
|
if(state == 0)
|
|
stop_pulling()
|
|
return FALSE
|
|
// Are we trying to pull something we are already pulling? Then enter grab cycle and end.
|
|
if(AM == pulling)
|
|
if(isliving(AM))
|
|
var/mob/living/AMob = AM
|
|
AMob.grabbedby(src)
|
|
return TRUE
|
|
stop_pulling()
|
|
if(AM.pulledby)
|
|
add_attack_logs(AM, AM.pulledby, "pulled from", ATKLOG_ALMOSTALL)
|
|
AM.pulledby.stop_pulling() //an object can't be pulled by two mobs at once.
|
|
pulling = AM
|
|
AM.pulledby = src
|
|
if(ismob(AM))
|
|
var/mob/M = AM
|
|
add_attack_logs(src, M, "passively grabbed", ATKLOG_ALMOSTALL)
|
|
if(show_message)
|
|
visible_message("<span class='warning'>[src] has grabbed [M] passively!</span>")
|
|
return TRUE
|
|
|
|
/atom/movable/proc/stop_pulling()
|
|
if(pulling)
|
|
pulling.pulledby = null
|
|
pulling = null
|
|
|
|
/atom/movable/proc/check_pulling()
|
|
if(pulling)
|
|
var/atom/movable/pullee = pulling
|
|
if(pullee && get_dist(src, pullee) > 1)
|
|
stop_pulling()
|
|
return
|
|
if(!isturf(loc))
|
|
stop_pulling()
|
|
return
|
|
if(pullee && !isturf(pullee.loc) && pullee.loc != loc) //to be removed once all code that changes an object's loc uses forceMove().
|
|
log_game("DEBUG:[src]'s pull on [pullee] wasn't broken despite [pullee] being in [pullee.loc]. Pull stopped manually.")
|
|
stop_pulling()
|
|
return
|
|
if(pulling.anchored || pulling.move_resist > move_force)
|
|
stop_pulling()
|
|
return
|
|
if(pulledby && moving_diagonally != FIRST_DIAG_STEP && get_dist(src, pulledby) > 1) //separated from our puller and not in the middle of a diagonal move.
|
|
pulledby.stop_pulling()
|
|
|
|
/atom/movable/proc/can_be_pulled(user, grab_state, force, show_message = FALSE)
|
|
if(src == user || !isturf(loc))
|
|
return FALSE
|
|
if(anchored || move_resist == INFINITY)
|
|
if(show_message)
|
|
to_chat(user, "<span class='warning'>[src] appears to be anchored to the ground!</span>")
|
|
return FALSE
|
|
if(throwing)
|
|
return FALSE
|
|
if(force < (move_resist * MOVE_FORCE_PULL_RATIO))
|
|
if(show_message)
|
|
to_chat(user, "<span class='warning'>[src] is too heavy to pull!</span>")
|
|
return FALSE
|
|
if(user in buckled_mobs)
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/// Used in shuttle movement and camera eye stuff.
|
|
/// Primarily used to notify objects being moved by a shuttle/bluespace fuckup.
|
|
/atom/movable/proc/set_loc(T, teleported=0)
|
|
var/old_loc = loc
|
|
loc = T
|
|
Moved(old_loc, get_dir(old_loc, loc), null, null, FALSE)
|
|
|
|
|
|
/**
|
|
* Meant for movement with zero side effects. Only use for objects that are supposed to move "invisibly" (like camera mobs or ghosts).
|
|
* If you want something to move onto a tile with a beartrap or recycler or tripmine or mouse without that object knowing about it at all, use this.
|
|
* Most of the time you want [forceMove()].
|
|
*/
|
|
/atom/movable/proc/abstract_move(atom/new_loc)
|
|
RESOLVE_ACTIVE_MOVEMENT // This should NEVER happen, but, just in case...
|
|
var/atom/old_loc = loc
|
|
var/direction = get_dir(old_loc, new_loc)
|
|
loc = new_loc
|
|
Moved(old_loc, direction, TRUE)
|
|
|
|
/// Here's where we rewrite how byond handles movement except slightly different.
|
|
/// To be removed on step_ conversion.
|
|
/// All this work to prevent a second bump.
|
|
/atom/movable/Move(atom/newloc, direction, glide_size_override = 0, update_dir = TRUE, momentum_change = TRUE)
|
|
. = FALSE
|
|
if(!newloc || newloc == loc)
|
|
return
|
|
|
|
// A mid-movement... movement... occured, resolve that first.
|
|
RESOLVE_ACTIVE_MOVEMENT
|
|
|
|
if(!direction)
|
|
direction = get_dir(src, newloc)
|
|
|
|
if(set_dir_on_move && dir != direction && update_dir)
|
|
setDir(direction)
|
|
|
|
var/is_multi_tile_object = is_multi_tile_object(src)
|
|
|
|
var/list/old_locs
|
|
if(is_multi_tile_object && isturf(loc))
|
|
old_locs = locs // locs is a special list, this is effectively the same as .Copy() but with less steps
|
|
for(var/atom/exiting_loc as anything in old_locs)
|
|
if(!exiting_loc.Exit(src, direction))
|
|
return
|
|
else
|
|
if(!loc.Exit(src, direction))
|
|
return
|
|
|
|
var/list/new_locs
|
|
if(is_multi_tile_object && isturf(newloc))
|
|
new_locs = block(
|
|
newloc,
|
|
locate(
|
|
min(world.maxx, newloc.x + CEILING(bound_width / 32, 1)),
|
|
min(world.maxy, newloc.y + CEILING(bound_height / 32, 1)),
|
|
newloc.z
|
|
)
|
|
) // If this is a multi-tile object then we need to predict the new locs and check if they allow our entrance.
|
|
for(var/atom/entering_loc as anything in new_locs)
|
|
if(!entering_loc.Enter(src))
|
|
return
|
|
if(SEND_SIGNAL(src, COMSIG_MOVABLE_PRE_MOVE, entering_loc) & COMPONENT_MOVABLE_BLOCK_PRE_MOVE)
|
|
return
|
|
else // Else just try to enter the single destination.
|
|
if(!newloc.Enter(src))
|
|
return
|
|
if(SEND_SIGNAL(src, COMSIG_MOVABLE_PRE_MOVE, newloc) & COMPONENT_MOVABLE_BLOCK_PRE_MOVE)
|
|
return
|
|
|
|
// Past this is the point of no return
|
|
var/atom/oldloc = loc
|
|
var/area/oldarea = get_area(oldloc)
|
|
var/area/newarea = get_area(newloc)
|
|
|
|
SET_ACTIVE_MOVEMENT(oldloc, direction, FALSE, old_locs, momentum_change)
|
|
loc = newloc
|
|
|
|
. = TRUE
|
|
|
|
if(old_locs) // This condition will only be true if it is a multi-tile object.
|
|
for(var/atom/exited_loc as anything in (old_locs - new_locs))
|
|
exited_loc.Exited(src, direction)
|
|
else // Else there's just one loc to be exited.
|
|
oldloc.Exited(src, direction)
|
|
if(oldarea != newarea)
|
|
oldarea.Exited(src, direction)
|
|
|
|
if(new_locs) // Same here, only if multi-tile.
|
|
for(var/atom/entered_loc as anything in (new_locs - old_locs))
|
|
entered_loc.Entered(src, oldloc, old_locs)
|
|
else
|
|
newloc.Entered(src, oldloc, old_locs)
|
|
if(oldarea != newarea)
|
|
newarea.Entered(src, oldarea)
|
|
|
|
RESOLVE_ACTIVE_MOVEMENT
|
|
|
|
/atom/movable/Move(atom/newloc, direct = 0, glide_size_override = 0, update_dir = TRUE, momentum_change = TRUE)
|
|
var/atom/movable/pullee = pulling
|
|
var/turf/current_turf = loc
|
|
if(!loc || !newloc)
|
|
return FALSE
|
|
var/atom/oldloc = loc
|
|
|
|
if(SEND_SIGNAL(src, COMSIG_MOVABLE_PRE_MOVE, newloc) & COMPONENT_MOVABLE_BLOCK_PRE_MOVE)
|
|
return FALSE
|
|
|
|
if(glide_size_override && glide_size != glide_size_override)
|
|
set_glide_size(glide_size_override)
|
|
|
|
if(loc != newloc)
|
|
if(!IS_DIR_DIAGONAL(direct)) //Cardinal move
|
|
. = ..(newloc, direct, momentum_change = momentum_change)
|
|
else //Diagonal move, split it into cardinal moves
|
|
moving_diagonally = FIRST_DIAG_STEP
|
|
var/first_step_dir = 0
|
|
// For each diagonal direction, we try moving NORTH/SOUTH first, and if it fails, we try moving EAST/WEST first.
|
|
// As long as either succeeds, we try the other.
|
|
var/direct_NS = direct & (NORTH | SOUTH)
|
|
var/direct_EW = direct & (EAST | WEST)
|
|
var/first_step_target = get_step(src, direct_NS)
|
|
// .() is rarely seen (for good reason), but it calls the same proc we're in.
|
|
// We cant' avoid it here, because oour overloading of Move() makes it call the wrong one.
|
|
. = .(first_step_target, direct_NS, glide_size_override, FALSE, momentum_change)
|
|
if(loc == first_step_target)
|
|
first_step_dir = direct_NS
|
|
moving_diagonally = SECOND_DIAG_STEP
|
|
. = .(newloc, direct_EW, glide_size_override, FALSE, momentum_change)
|
|
else if(loc == oldloc)
|
|
first_step_target = get_step(src, direct_EW)
|
|
. = .(first_step_target, direct_EW, glide_size_override, FALSE, momentum_change)
|
|
if(loc == first_step_target)
|
|
first_step_dir = direct_EW
|
|
moving_diagonally = SECOND_DIAG_STEP
|
|
. = .(newloc, direct_NS, glide_size_override, FALSE, momentum_change)
|
|
if(first_step_dir != 0)
|
|
if(!. && set_dir_on_move && update_dir)
|
|
setDir(first_step_dir)
|
|
Moved(oldloc, first_step_dir, FALSE, null, TRUE)
|
|
else if(!inertia_moving)
|
|
newtonian_move(direct)
|
|
if(client_mobs_in_contents)
|
|
update_parallax_contents()
|
|
moving_diagonally = 0
|
|
return
|
|
|
|
if(!loc || (loc == oldloc && oldloc != newloc))
|
|
last_move = 0
|
|
|
|
return
|
|
|
|
if(. && pulling && pulling == pullee && pulling != moving_from_pull) //we were pulling a thing and didn't lose it during our move.
|
|
if(pulling.anchored)
|
|
stop_pulling()
|
|
else
|
|
//puller and pullee more than one tile away or in diagonal position and whatever the pullee is pulling isn't already moving from a pull as it'll most likely result in an infinite loop a la ouroborus.
|
|
if(!pulling.pulling?.moving_from_pull)
|
|
var/pull_dir = get_dir(pulling, src)
|
|
var/target_turf = current_turf
|
|
|
|
if(target_turf != current_turf || (moving_diagonally != SECOND_DIAG_STEP && IS_DIR_DIAGONAL(pull_dir)) || get_dist(src, pulling) > 1)
|
|
pulling.move_from_pull(src, target_turf, glide_size)
|
|
|
|
// PARA: There was code here for handling multi-z, which isn't relevant to us.
|
|
check_pulling()
|
|
|
|
//glide_size strangely enough can change mid movement animation and update correctly while the animation is playing
|
|
//This means that if you don't override it late like this, it will just be set back by the movement update that's called when you move turfs.
|
|
if(glide_size_override)
|
|
set_glide_size(glide_size_override)
|
|
|
|
last_move = direct
|
|
l_move_time = world.time
|
|
|
|
if(. && has_buckled_mobs() && !handle_buckled_mob_movement(loc, direct, glide_size_override)) //movement failed due to buckled mob
|
|
. = FALSE
|
|
|
|
/**
|
|
* Called after a successful Move(). By this point, we've already moved.
|
|
* Arguments:
|
|
* * old_loc is the location prior to the move. Can be null to indicate nullspace.
|
|
* * movement_dir is the direction the movement took place. Can be NONE if it was some sort of teleport.
|
|
* * The forced flag indicates whether this was a forced move, which skips many checks of regular movement.
|
|
* * The old_locs is an optional argument, in case the moved movable was present in multiple locations before the movement.
|
|
* * momentum_change represents whether this movement is due to a "new" force if TRUE or an already "existing" force if FALSE
|
|
**/
|
|
/atom/movable/proc/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
|
|
SEND_SIGNAL(src, COMSIG_MOVABLE_MOVED, old_loc, movement_dir, forced, old_locs, momentum_change)
|
|
|
|
if(!inertia_moving && momentum_change)
|
|
newtonian_move(movement_dir)
|
|
if(length(client_mobs_in_contents))
|
|
update_parallax_contents()
|
|
|
|
var/turf/old_turf = get_turf(old_loc)
|
|
var/turf/new_turf = get_turf(src)
|
|
|
|
if(old_turf?.z != new_turf?.z)
|
|
on_changed_z_level(old_turf, new_turf)
|
|
|
|
var/datum/light_source/L
|
|
var/thing
|
|
for(thing in light_sources) // Cycle through the light sources on this atom and tell them to update.
|
|
L = thing
|
|
L.source_atom.update_light()
|
|
return TRUE
|
|
|
|
/// Called when src is being moved to a target turf because another movable (puller) is moving around.
|
|
/atom/movable/proc/move_from_pull(atom/movable/puller, turf/target_turf, puller_glide_size)
|
|
moving_from_pull = puller
|
|
var/new_glide_size = puller_glide_size
|
|
var/pull_dir = get_dir(src, target_turf)
|
|
// Adjust diagonal pulls for LONG_GLIDE differences.
|
|
if(IS_DIR_DIAGONAL(pull_dir))
|
|
if((puller.appearance_flags & LONG_GLIDE) && !(appearance_flags & LONG_GLIDE))
|
|
new_glide_size *= sqrt(2)
|
|
if(!(puller.appearance_flags & LONG_GLIDE) && (appearance_flags & LONG_GLIDE))
|
|
new_glide_size /= sqrt(2)
|
|
set_glide_size(new_glide_size)
|
|
if(isliving(src))
|
|
var/mob/living/M = src
|
|
if(IS_HORIZONTAL(M) && !M.buckled && (prob(M.getBruteLoss() * 200 / M.maxHealth))) // So once you reach 50 brute damage you hit 100% chance to leave a blood trail for every tile you're pulled
|
|
M.makeTrail(target_turf)
|
|
Move(target_turf, pull_dir)
|
|
moving_from_pull = null
|
|
|
|
// Make sure you know what you're doing if you call this
|
|
// You probably want CanPass()
|
|
/atom/movable/Cross(atom/movable/crossed_atom)
|
|
if(SEND_SIGNAL(src, COMSIG_MOVABLE_CHECK_CROSS, crossed_atom) & COMPONENT_BLOCK_CROSS)
|
|
return FALSE
|
|
if(SEND_SIGNAL(crossed_atom, COMSIG_MOVABLE_CHECK_CROSS_OVER, src) & COMPONENT_BLOCK_CROSS)
|
|
return FALSE
|
|
return CanPass(crossed_atom, get_dir(src, crossed_atom))
|
|
|
|
///default byond proc that is deprecated for us in lieu of signals. do not call
|
|
/atom/movable/Crossed(atom/movable/crossed_atom, oldloc)
|
|
SHOULD_NOT_OVERRIDE(TRUE)
|
|
CRASH("atom/movable/Crossed() was called!")
|
|
|
|
/**
|
|
* `Uncross()` is a default BYOND proc that is called when something is *going*
|
|
* to exit this atom's turf. It is prefered over `Uncrossed` when you want to
|
|
* deny that movement, such as in the case of border objects, objects that allow
|
|
* you to walk through them in any direction except the one they block
|
|
* (think side windows).
|
|
*
|
|
* While being seemingly harmless, most everything doesn't actually want to
|
|
* use this, meaning that we are wasting proc calls for every single atom
|
|
* on a turf, every single time something exits it, when basically nothing
|
|
* cares.
|
|
*
|
|
* If you want to replicate the old `Uncross()` behavior, the most apt
|
|
* replacement is [`/datum/element/connect_loc`] while hooking onto
|
|
* [`COMSIG_ATOM_EXIT`].
|
|
*/
|
|
/atom/movable/Uncross()
|
|
SHOULD_NOT_OVERRIDE(TRUE)
|
|
CRASH("Unexpected atom/movable/Uncross() call")
|
|
|
|
/**
|
|
* default byond proc that is normally called on everything inside the previous turf
|
|
* a movable was in after moving to its current turf
|
|
* this is wasteful since the vast majority of objects do not use Uncrossed
|
|
* use connect_loc to register to COMSIG_ATOM_EXITED instead
|
|
*/
|
|
/atom/movable/Uncrossed(atom/movable/uncrossed_atom)
|
|
SHOULD_NOT_OVERRIDE(TRUE)
|
|
CRASH("/atom/movable/Uncrossed() was called")
|
|
|
|
// Change glide size for the duration of one movement
|
|
/atom/movable/proc/glide_for(movetime)
|
|
if(movetime)
|
|
glide_size = world.icon_size/max(DS2TICKS(movetime), 1)
|
|
spawn(movetime)
|
|
glide_size = initial(glide_size)
|
|
else
|
|
glide_size = initial(glide_size)
|
|
|
|
/atom/movable/Bump(atom/bumped_atom)
|
|
if(!bumped_atom)
|
|
CRASH("Bump was called with no argument.")
|
|
if(SEND_SIGNAL(src, COMSIG_MOVABLE_BUMP, bumped_atom) & COMPONENT_INTERCEPT_BUMPED)
|
|
return
|
|
. = ..()
|
|
if(!QDELETED(throwing))
|
|
throwing.finalize(hit = TRUE, target = bumped_atom)
|
|
. = TRUE
|
|
if(QDELETED(bumped_atom))
|
|
return
|
|
bumped_atom.Bumped(src)
|
|
|
|
/atom/movable/proc/forceMove(atom/destination)
|
|
. = FALSE
|
|
if(destination)
|
|
. = doMove(destination)
|
|
else
|
|
CRASH("No valid destination passed into forceMove")
|
|
|
|
/*
|
|
* Move ourself to nullspace. Use to indicate clearly that you
|
|
* know that you are doing so, as opposed to calling forceMove(null),
|
|
* accidentally or otherwise.
|
|
*/
|
|
/atom/movable/proc/moveToNullspace()
|
|
return doMove(null)
|
|
|
|
/atom/movable/proc/doMove(atom/destination)
|
|
. = FALSE
|
|
RESOLVE_ACTIVE_MOVEMENT
|
|
|
|
var/atom/oldloc = loc
|
|
var/is_multi_tile = bound_width > world.icon_size || bound_height > world.icon_size
|
|
|
|
SET_ACTIVE_MOVEMENT(oldloc, NONE, TRUE, null, TRUE)
|
|
|
|
if(destination)
|
|
if(pulledby && !HAS_TRAIT(src, TRAIT_CURRENTLY_Z_MOVING))
|
|
pulledby.stop_pulling()
|
|
|
|
var/same_loc = oldloc == destination
|
|
var/area/old_area = get_area(oldloc)
|
|
var/area/destarea = get_area(destination)
|
|
var/movement_dir = get_dir(src, destination)
|
|
|
|
moving_diagonally = 0
|
|
|
|
loc = destination
|
|
|
|
if(!same_loc)
|
|
if(is_multi_tile && isturf(destination))
|
|
var/list/new_locs = block(
|
|
destination,
|
|
locate(
|
|
min(world.maxx, destination.x + ROUND_UP(bound_width / 32)),
|
|
min(world.maxy, destination.y + ROUND_UP(bound_height / 32)),
|
|
destination.z
|
|
)
|
|
)
|
|
if(old_area && old_area != destarea)
|
|
old_area.Exited(src, movement_dir)
|
|
for(var/atom/left_loc as anything in locs - new_locs)
|
|
left_loc.Exited(src, movement_dir)
|
|
|
|
for(var/atom/entering_loc as anything in new_locs - locs)
|
|
entering_loc.Entered(src, movement_dir)
|
|
|
|
if(old_area && old_area != destarea)
|
|
destarea.Entered(src, movement_dir)
|
|
else
|
|
if(oldloc)
|
|
oldloc.Exited(src, movement_dir)
|
|
if(old_area && old_area != destarea)
|
|
old_area.Exited(src, movement_dir)
|
|
destination.Entered(src, oldloc)
|
|
if(destarea && old_area != destarea)
|
|
destarea.Entered(src, old_area)
|
|
|
|
. = TRUE
|
|
|
|
//If no destination, move the atom into nullspace (don't do this unless you know what you're doing)
|
|
else
|
|
. = TRUE
|
|
|
|
if(oldloc)
|
|
loc = null
|
|
var/area/old_area = get_area(oldloc)
|
|
if(is_multi_tile && isturf(oldloc))
|
|
for(var/atom/old_loc as anything in locs)
|
|
old_loc.Exited(src, NONE)
|
|
else
|
|
oldloc.Exited(src, NONE)
|
|
|
|
if(old_area)
|
|
old_area.Exited(src, NONE)
|
|
|
|
RESOLVE_ACTIVE_MOVEMENT
|
|
|
|
/**
|
|
* Called when a movable changes z-levels.
|
|
*
|
|
* Arguments:
|
|
* * old_turf - The previous turf they were on before.
|
|
* * new_turf - The turf they have now entered.
|
|
* * notify_contents - Whether or not to notify the movable's contents that their z-level has changed.
|
|
*/
|
|
/atom/movable/proc/on_changed_z_level(turf/old_turf, turf/new_turf, notify_contents = TRUE)
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
SEND_SIGNAL(src, COMSIG_MOVABLE_Z_CHANGED, old_turf, new_turf)
|
|
|
|
if(!notify_contents)
|
|
return
|
|
|
|
for(var/atom/movable/content as anything in src) // Notify contents of Z-transition.
|
|
content.on_changed_z_level(old_turf, new_turf)
|
|
|
|
/mob/living/forceMove(atom/destination)
|
|
if(buckled)
|
|
addtimer(CALLBACK(src, PROC_REF(check_buckled)), 1, TIMER_UNIQUE)
|
|
for(var/mob/living/buckled_mob as anything in buckled_mobs)
|
|
addtimer(CALLBACK(buckled_mob, PROC_REF(check_buckled)), 1, TIMER_UNIQUE)
|
|
if(pulling)
|
|
addtimer(CALLBACK(src, PROC_REF(check_pull)), 1, TIMER_UNIQUE)
|
|
. = ..()
|
|
if(client)
|
|
reset_perspective(destination)
|
|
if(hud_used && length(client.parallax_layers))
|
|
hud_used.update_parallax()
|
|
update_runechat_msg_location()
|
|
|
|
|
|
/**
|
|
* Called whenever an object moves and by mobs when they attempt to move themselves through space
|
|
* And when an object or action applies a force on src, see [newtonian_move][/atom/movable/proc/newtonian_move]
|
|
*
|
|
* Return FALSE to have src start/keep drifting in a no-grav area and TRUE to stop/not start drifting
|
|
*
|
|
* Mobs should return TRUE if they should be able to move of their own volition, see [/client/proc/Move]
|
|
*
|
|
* Arguments:
|
|
* * movement_dir - 0 when stopping or any dir when trying to move
|
|
* * continuous_move - If this check is coming from something in the context of already drifting
|
|
*/
|
|
/atom/movable/proc/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
|
|
if(has_gravity(src))
|
|
return TRUE
|
|
|
|
if(SEND_SIGNAL(src, COMSIG_MOVABLE_SPACEMOVE, movement_dir, continuous_move) & COMSIG_MOVABLE_STOP_SPACEMOVE)
|
|
return TRUE
|
|
|
|
if(pulledby && pulledby.pulledby != src)
|
|
return TRUE
|
|
|
|
if(throwing)
|
|
return TRUE
|
|
|
|
if(locate(/obj/structure/lattice) in range(1, get_turf(src))) //Not realistic but makes pushing things in space easier
|
|
return TRUE
|
|
|
|
return FALSE
|
|
|
|
/atom/movable/proc/newtonian_move(direction, instant = FALSE, start_delay = 0)
|
|
if(!isturf(loc) || Process_Spacemove(direction, continuous_move = TRUE))
|
|
return FALSE
|
|
|
|
if(SEND_SIGNAL(src, COMSIG_MOVABLE_NEWTONIAN_MOVE, direction, start_delay) & COMPONENT_MOVABLE_NEWTONIAN_BLOCK)
|
|
return TRUE
|
|
|
|
AddComponent(/datum/component/drift, direction, instant, start_delay)
|
|
|
|
return TRUE
|
|
|
|
/mob/newtonian_move(direction, instant = FALSE, start_delay = 0)
|
|
if(buckled)
|
|
return FALSE
|
|
return ..()
|
|
|
|
//called when src is thrown into hit_atom
|
|
/atom/movable/proc/throw_impact(atom/hit_atom, throwingdatum)
|
|
set waitfor = FALSE
|
|
SEND_SIGNAL(src, COMSIG_MOVABLE_IMPACT, hit_atom, throwingdatum)
|
|
if(!QDELETED(hit_atom))
|
|
return hit_atom.hitby(src, throwingdatum=throwingdatum)
|
|
|
|
/// called after an items throw is ended.
|
|
/atom/movable/proc/end_throw()
|
|
return
|
|
|
|
/atom/movable/hitby(atom/movable/AM, skipcatch, hitpush = TRUE, blocked, datum/thrownthing/throwingdatum)
|
|
if(!anchored && hitpush && (!throwingdatum || (throwingdatum.force >= (move_resist * MOVE_FORCE_PUSH_RATIO))))
|
|
step(src, AM.dir)
|
|
..()
|
|
|
|
/atom/movable/proc/throw_at(atom/target, range, speed, mob/thrower, spin = TRUE, diagonals_first = FALSE, datum/callback/callback, force = INFINITY, dodgeable = TRUE, block_movement = TRUE)
|
|
if(QDELETED(src))
|
|
CRASH("Qdeleted thing being thrown around.")
|
|
|
|
if(!target || (flags & NODROP) || speed <= 0)
|
|
return FALSE
|
|
|
|
if(pulledby)
|
|
pulledby.stop_pulling()
|
|
|
|
// They are moving! Wouldn't it be cool if we calculated their momentum and added it to the throw?
|
|
if(istype(thrower) && thrower.last_move && thrower.client && thrower.client.move_delay >= world.time + world.tick_lag * 2)
|
|
var/user_momentum = thrower.movement_delay()
|
|
if(!user_momentum) // no movement_delay, this means they move once per byond tick, let's calculate from that instead
|
|
user_momentum = world.tick_lag
|
|
|
|
user_momentum = 1 / user_momentum // convert from ds to the tiles per ds that throw_at uses
|
|
|
|
if(get_dir(thrower, target) & last_move)
|
|
user_momentum = user_momentum // basically a noop, but needed
|
|
else if(get_dir(target, thrower) & last_move)
|
|
user_momentum = -user_momentum // we are moving away from the target, lets slowdown the throw accordingly
|
|
else
|
|
user_momentum = 0
|
|
|
|
if(user_momentum)
|
|
// first lets add that momentum to range
|
|
range *= (user_momentum / speed) + 1
|
|
//then lets add it to speed
|
|
speed += user_momentum
|
|
if(speed <= 0)
|
|
return //no throw speed, the user was moving too fast.
|
|
|
|
var/target_zone
|
|
if(QDELETED(thrower))
|
|
thrower = null //Let's not pass a qdeleting reference if any.
|
|
else
|
|
target_zone = thrower.zone_selected
|
|
|
|
var/datum/thrownthing/thrown_thing = new(src, target, get_dir(src, target), range, speed, thrower, diagonals_first, force, callback, dodgeable, block_movement, target_zone)
|
|
|
|
var/dist_x = abs(target.x - src.x)
|
|
var/dist_y = abs(target.y - src.y)
|
|
var/dx = (target.x > src.x) ? EAST : WEST
|
|
var/dy = (target.y > src.y) ? NORTH : SOUTH
|
|
|
|
if(dist_x == dist_y)
|
|
thrown_thing.pure_diagonal = 1
|
|
|
|
else if(dist_x <= dist_y)
|
|
var/olddist_x = dist_x
|
|
var/olddx = dx
|
|
dist_x = dist_y
|
|
dist_y = olddist_x
|
|
dx = dy
|
|
dy = olddx
|
|
thrown_thing.dist_x = dist_x
|
|
thrown_thing.dist_y = dist_y
|
|
thrown_thing.dx = dx
|
|
thrown_thing.dy = dy
|
|
thrown_thing.diagonal_error = dist_x / 2 - dist_y
|
|
thrown_thing.start_time = world.time
|
|
|
|
if(pulledby)
|
|
pulledby.stop_pulling()
|
|
|
|
throwing = thrown_thing
|
|
if(spin && !no_spin && !no_spin_thrown)
|
|
SpinAnimation(5, 1)
|
|
|
|
SEND_SIGNAL(src, COMSIG_MOVABLE_POST_THROW, thrown_thing, spin)
|
|
SSthrowing.processing[src] = thrown_thing
|
|
thrown_thing.tick()
|
|
|
|
return TRUE
|
|
|
|
/// This proc is recursive, and calls itself to constantly set the glide size of an atom/movable
|
|
/atom/movable/proc/set_glide_size(target = 8)
|
|
if(glide_size == target)
|
|
return
|
|
|
|
var/old_value = glide_size
|
|
glide_size = target
|
|
SEND_SIGNAL(src, COMSIG_MOVABLE_UPDATED_GLIDE_SIZE, old_value)
|
|
|
|
for(var/mob/buckled_mob as anything in buckled_mobs)
|
|
buckled_mob.set_glide_size(target)
|
|
|
|
//Overlays
|
|
/atom/movable/overlay
|
|
var/atom/master = null
|
|
anchored = TRUE
|
|
simulated = FALSE
|
|
|
|
/atom/movable/overlay/New()
|
|
. = ..()
|
|
verbs.Cut()
|
|
return
|
|
|
|
/atom/movable/overlay/attackby__legacy__attackchain(a, b, c)
|
|
if(master)
|
|
return master.attackby__legacy__attackchain(a, b, c)
|
|
|
|
/atom/movable/overlay/attack_hand(a, b, c)
|
|
if(master)
|
|
return master.attack_hand(a, b, c)
|
|
|
|
/atom/movable/proc/handle_buckled_mob_movement(newloc, direct, glide_size_override)
|
|
for(var/m in buckled_mobs)
|
|
var/mob/living/buckled_mob = m
|
|
if(!buckled_mob.Move(newloc, direct, glide_size_override))
|
|
forceMove(buckled_mob.loc)
|
|
last_move = buckled_mob.last_move
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/atom/movable/proc/force_pushed(atom/movable/pusher, force = MOVE_FORCE_DEFAULT, direction)
|
|
return FALSE
|
|
|
|
/atom/movable/proc/force_push(atom/movable/AM, force = move_force, direction, silent = FALSE)
|
|
. = AM.force_pushed(src, force, direction)
|
|
if(!silent && .)
|
|
visible_message("<span class='warning'>[src] forcefully pushes against [AM]!</span>", "<span class='warning'>You forcefully push against [AM]!</span>")
|
|
|
|
/atom/movable/proc/move_crush(atom/movable/AM, force = move_force, direction, silent = FALSE)
|
|
. = AM.move_crushed(src, force, direction)
|
|
if(!silent && .)
|
|
visible_message("<span class='danger'>[src] crushes past [AM]!</span>", "<span class='danger'>You crush [AM]!</span>")
|
|
|
|
/atom/movable/proc/move_crushed(atom/movable/pusher, force = MOVE_FORCE_DEFAULT, direction)
|
|
return FALSE
|
|
|
|
/atom/movable/CanPass(atom/movable/mover, border_dir)
|
|
// This condition is copied from atom to avoid an extra parent call, because this is a very hot proc.
|
|
if(!density)
|
|
return TRUE
|
|
return LAZYIN(buckled_mobs, mover)
|
|
|
|
/atom/movable/proc/get_spacemove_backup()
|
|
var/atom/movable/dense_object_backup
|
|
for(var/A in orange(1, get_turf(src)))
|
|
if(isarea(A))
|
|
continue
|
|
else if(isturf(A))
|
|
var/turf/turf = A
|
|
if(!turf.density)
|
|
continue
|
|
return turf
|
|
else
|
|
var/atom/movable/AM = A
|
|
if(!AM.CanPass(src) || AM.density)
|
|
if(AM.anchored)
|
|
return AM
|
|
dense_object_backup = AM
|
|
break
|
|
. = dense_object_backup
|
|
|
|
/atom/movable/proc/transfer_prints_to(atom/movable/target = null, overwrite = FALSE)
|
|
if(!target)
|
|
return
|
|
if(overwrite)
|
|
target.fingerprints = fingerprints
|
|
target.fingerprintshidden = fingerprintshidden
|
|
else
|
|
target.fingerprints += fingerprints
|
|
target.fingerprintshidden += fingerprintshidden
|
|
target.fingerprintslast = fingerprintslast
|
|
|
|
/atom/movable/proc/do_attack_animation(atom/A, visual_effect_icon, obj/item/used_item, no_effect)
|
|
if(!no_effect && (visual_effect_icon || used_item))
|
|
do_item_attack_animation(A, visual_effect_icon, used_item)
|
|
|
|
if(A == src)
|
|
return //don't do an animation if attacking self
|
|
var/pixel_x_diff = 0
|
|
var/pixel_y_diff = 0
|
|
var/turn_dir = 1
|
|
|
|
var/direction = get_dir(src, A)
|
|
if(direction & NORTH)
|
|
pixel_y_diff = 8
|
|
turn_dir = prob(50) ? -1 : 1
|
|
else if(direction & SOUTH)
|
|
pixel_y_diff = -8
|
|
turn_dir = prob(50) ? -1 : 1
|
|
|
|
if(direction & EAST)
|
|
pixel_x_diff = 8
|
|
else if(direction & WEST)
|
|
pixel_x_diff = -8
|
|
turn_dir = -1
|
|
|
|
var/matrix/initial_transform = matrix(transform)
|
|
var/matrix/rotated_transform = transform.Turn(5 * turn_dir)
|
|
animate(src, pixel_x = pixel_x + pixel_x_diff, pixel_y = pixel_y + pixel_y_diff, transform = rotated_transform, time = 0.1 SECONDS, easing = CUBIC_EASING)
|
|
animate(pixel_x = pixel_x - pixel_x_diff, pixel_y = pixel_y - pixel_y_diff, transform = initial_transform, time = 0.2 SECONDS, easing = SINE_EASING)
|
|
|
|
/atom/movable/proc/do_item_attack_animation(atom/A, visual_effect_icon, obj/item/used_item)
|
|
var/image/I
|
|
if(visual_effect_icon)
|
|
I = image('icons/effects/effects.dmi', A, visual_effect_icon, A.layer + 0.1)
|
|
else if(used_item)
|
|
I = image(icon = used_item, loc = A, layer = A.layer + 0.1)
|
|
I.plane = GAME_PLANE
|
|
|
|
// Scale the icon.
|
|
I.transform *= 0.75
|
|
// The icon should not rotate.
|
|
I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
|
|
|
|
// Set the direction of the icon animation.
|
|
var/direction = get_dir(src, A)
|
|
if(direction & NORTH)
|
|
I.pixel_y = -16
|
|
else if(direction & SOUTH)
|
|
I.pixel_y = 16
|
|
|
|
if(direction & EAST)
|
|
I.pixel_x = -16
|
|
else if(direction & WEST)
|
|
I.pixel_x = 16
|
|
|
|
if(!direction) // Attacked self?!
|
|
I.pixel_z = 16
|
|
|
|
if(!I)
|
|
return
|
|
|
|
// Who can see the attack?
|
|
var/list/viewing = list()
|
|
for(var/mob/M in viewers(A))
|
|
if(M.client && M.client.prefs.toggles2 & PREFTOGGLE_2_ITEMATTACK)
|
|
viewing |= M.client
|
|
|
|
I.appearance_flags |= RESET_TRANSFORM | KEEP_APART
|
|
flick_overlay(I, viewing, 7) // 7 ticks/half a second
|
|
|
|
// And animate the attack!
|
|
var/t_color = "#ffffff"
|
|
if(ismob(src) && ismob(A) && !used_item)
|
|
var/mob/M = src
|
|
t_color = M.a_intent == INTENT_HARM ? "#ff0000" : "#ffffff"
|
|
animate(I, alpha = 175, pixel_x = 0, pixel_y = 0, pixel_z = 0, time = 0.3 SECONDS, color = t_color)
|
|
animate(time = 0.1 SECONDS)
|
|
animate(alpha = 0, time = 0.3 SECONDS, easing = CIRCULAR_EASING | EASE_OUT)
|
|
|
|
/atom/movable/proc/portal_destroyed(obj/effect/portal/P)
|
|
return
|
|
|
|
/atom/movable/proc/decompile_act(obj/item/matter_decompiler/C, mob/user) // For drones to decompile mobs and objs. See drone for an example.
|
|
return FALSE
|
|
|
|
/// called when a mob gets shoved into an items turf. false means the mob will be shoved backwards normally, true means the mob will not be moved by the disarm proc.
|
|
/atom/movable/proc/shove_impact(mob/living/target, mob/living/attacker)
|
|
SEND_SIGNAL(src, COMSIG_MOVABLE_SHOVE_IMPACT, target, attacker)
|
|
return FALSE
|
|
|
|
/**
|
|
* Adds the deadchat_plays component to this atom with simple movement commands.
|
|
*
|
|
* Returns the component added.
|
|
* Arguments:
|
|
* * mode - Either DEADCHAT_ANARCHY_MODE or DEADCHAT_DEMOCRACY_MODE passed to the deadchat_control component. See [/datum/component/deadchat_control] for more info.
|
|
* * cooldown - The cooldown between command inputs passed to the deadchat_control component. See [/datum/component/deadchat_control] for more info.
|
|
*/
|
|
/atom/movable/proc/deadchat_plays(mode = DEADCHAT_ANARCHY_MODE, cooldown = 12 SECONDS)
|
|
return AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list(), cooldown)
|
|
|
|
/// Easy way to remove the component when the fun has been played out
|
|
/atom/movable/proc/stop_deadchat_plays()
|
|
DeleteComponent(/datum/component/deadchat_control)
|
|
|
|
//Update the screentip to reflect what we're hovering over
|
|
/atom/movable/MouseEntered(location, control, params)
|
|
if(invisibility > usr.see_invisible)
|
|
return
|
|
var/datum/hud/active_hud = usr.hud_used // Don't nullcheck this stuff, if it breaks we wanna know it breaks
|
|
var/screentip_mode = usr.client.prefs.screentip_mode
|
|
if(screentip_mode == 0 || (flags & NO_SCREENTIPS))
|
|
active_hud.screentip_text.maptext = ""
|
|
return
|
|
//We inline a MAPTEXT() here, because there's no good way to statically add to a string like this
|
|
active_hud.screentip_text.maptext = "<span class='maptext' style='font-family: sans-serif; text-align: center; font-size: [screentip_mode]px; color: [usr.client.prefs.screentip_color]'>[name]</span>"
|
|
usr.client.moused_over = UID()
|
|
|
|
/atom/movable/MouseExited(location, control, params)
|
|
usr.hud_used.screentip_text.maptext = ""
|
|
usr.client.moused_over = null
|
|
|
|
/atom/movable/proc/choose_crush_crit(mob/living/carbon/victim)
|
|
if(!length(GLOB.tilt_crits))
|
|
return
|
|
for(var/crit_path in shuffle(GLOB.tilt_crits))
|
|
var/datum/tilt_crit/C = GLOB.tilt_crits[crit_path]
|
|
if(C.is_valid(src, victim))
|
|
return C
|
|
|
|
/atom/movable/proc/handle_squish_carbon(mob/living/carbon/victim, damage_to_deal, datum/tilt_crit/crit)
|
|
|
|
// Damage points to "refund", if a crit already beats the shit out of you we can shelve some of the extra damage.
|
|
var/crit_rebate = 0
|
|
|
|
if(HAS_TRAIT(victim, TRAIT_DWARF))
|
|
// also double damage if you're short
|
|
damage_to_deal *= 2
|
|
|
|
if(crit)
|
|
crit_rebate = crit.tip_crit_effect(src, victim)
|
|
if(crit.harmless)
|
|
return
|
|
|
|
add_attack_logs(null, victim, "critically crushed by [src] causing [crit]")
|
|
else
|
|
add_attack_logs(null, victim, "crushed by [src]")
|
|
|
|
// 30% chance to spread damage across the entire body, 70% chance to target two limbs in particular
|
|
damage_to_deal = max(damage_to_deal - crit_rebate, 0)
|
|
if(prob(30))
|
|
victim.apply_damage(damage_to_deal, BRUTE, BODY_ZONE_CHEST, spread_damage = TRUE)
|
|
else
|
|
var/picked_zone
|
|
var/num_parts_to_pick = 2
|
|
for(var/i in 1 to num_parts_to_pick)
|
|
picked_zone = pick(BODY_ZONE_CHEST, BODY_ZONE_HEAD, BODY_ZONE_L_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_ARM, BODY_ZONE_R_LEG)
|
|
victim.apply_damage((damage_to_deal) * (1 / num_parts_to_pick), BRUTE, picked_zone)
|
|
|
|
victim.AddElement(/datum/element/squish, 80 SECONDS)
|
|
|
|
#define NO_CRUSH_DIR "no_dir"
|
|
|
|
/**
|
|
* Tip over this atom onto a turf, crushing things in its path.
|
|
*
|
|
* Arguments:
|
|
* * target_turf - The turf to fall onto.
|
|
* * should_crit - If true, we'll try to crit things that we crush.
|
|
* * crit_damage_factor - If a crit is rolled, crush_damage will be multiplied by this amount.
|
|
* * forced_crit - If passed, this crit will be applied to everything it crushes.
|
|
* * weaken_time - The amount of time that weaken will be applied to crushed mobs.
|
|
* * knockdown_time - The amount of time that knockdown will be applied to crushed mobs.
|
|
* * ignore_gravity - If false, we won't fall over in zero G.
|
|
* * should_rotate - If false, we won't rotate when we fall.
|
|
* * angle - The angle by which we'll rotate. If this is null/0, we'll randomly rotate 90 degrees clockwise or counterclockwise.
|
|
* * rightable - If true, the tilted component will be applied, allowing people to alt-click to right it.
|
|
* * block_interactions_until_righted - If true, interactions with the object will be blocked until it's righted.
|
|
* * crush_dir - An override on the cardinal direction we're crushing.
|
|
*/
|
|
/atom/movable/proc/fall_and_crush(turf/target_turf, crush_damage, should_crit = FALSE, crit_damage_factor = 2, datum/tilt_crit/forced_crit, weaken_time = 4 SECONDS, knockdown_time = 10 SECONDS, ignore_gravity = FALSE, should_rotate = TRUE, angle, rightable = FALSE, block_interactions_until_righted = FALSE, crush_dir = NO_CRUSH_DIR)
|
|
if(QDELETED(src) || isnull(target_turf))
|
|
return
|
|
|
|
if(crush_dir == NO_CRUSH_DIR)
|
|
crush_dir = get_dir(get_turf(src), target_turf)
|
|
|
|
var/has_tried_to_move = FALSE
|
|
|
|
if(target_turf.is_blocked_turf(exclude_mobs = TRUE, ignore_atoms = list(src)))
|
|
has_tried_to_move = TRUE
|
|
if(!Move(target_turf, crush_dir))
|
|
// we'll try to move, and if we didn't end up going anywhere, then we do nothing.
|
|
visible_message("<span class='warning'>[src] seems to rock, but doesn't fall over!</span>")
|
|
return
|
|
|
|
for(var/atom/target in (target_turf.contents) + target_turf)
|
|
if(isarea(target) || target == src) // don't crush ourselves
|
|
continue
|
|
|
|
if(isobserver(target))
|
|
continue
|
|
|
|
// ignore things that are under the ground
|
|
if(isobj(target) && (target.invisibility > SEE_INVISIBLE_LIVING) || iseffect(target) || isitem(target) || target.level == 1)
|
|
continue
|
|
|
|
var/datum/tilt_crit/crit_case = forced_crit
|
|
if(isnull(forced_crit) && should_crit)
|
|
crit_case = choose_crush_crit(target)
|
|
// note that it could still be null after this point, in which case it won't crit
|
|
var/damage_to_deal = crush_damage
|
|
|
|
if(isliving(target))
|
|
var/mob/living/L = target
|
|
|
|
if(L.incorporeal_move)
|
|
continue
|
|
|
|
if(crit_case)
|
|
damage_to_deal *= crit_damage_factor
|
|
if(iscarbon(L))
|
|
handle_squish_carbon(L, damage_to_deal, crit_case)
|
|
else
|
|
L.apply_damage(damage_to_deal, BRUTE)
|
|
L.Weaken(weaken_time)
|
|
L.emote("scream")
|
|
L.KnockDown(knockdown_time)
|
|
playsound(L, 'sound/effects/blobattack.ogg', 40, TRUE)
|
|
playsound(L, 'sound/effects/splat.ogg', 50, TRUE)
|
|
add_attack_logs(src, L, "crushed by [src]")
|
|
|
|
|
|
else if(isobj(target)) // don't crush things on the floor, that'd probably be annoying
|
|
var/obj/O = target
|
|
O.take_damage(damage_to_deal, BRUTE, "", FALSE)
|
|
else
|
|
continue
|
|
|
|
target.visible_message(
|
|
"<span class='danger'>[target] is crushed by [src]!</span>",
|
|
"<span class='userdanger'>[src] crushes you!</span>",
|
|
"<span class='warning'>You hear a loud crunch!</span>"
|
|
)
|
|
|
|
tilt_over(target_turf, angle, should_rotate, rightable, block_interactions_until_righted)
|
|
// for things that trigger on Crossed()
|
|
if(!has_tried_to_move)
|
|
Move(target_turf, crush_dir)
|
|
|
|
return TRUE
|
|
|
|
#undef NO_CRUSH_DIR
|
|
|
|
/**
|
|
* Tip over an atom without too much fuss. This won't cause damage to anything, and just rotates the thing and (optionally) adds the component.
|
|
*
|
|
* Arguments:
|
|
* * target - The turf to tilt over onto
|
|
* * rotation_angle - The angle to rotate by. If not given, defaults to random rotating by 90 degrees clockwise or counterclockwise
|
|
* * should_rotate - Whether or not we should rotate at all
|
|
* * rightable - Whether or not this object should be rightable, attaching the tilted component to it
|
|
* * block_interactions_until_righted - If true, this object will need to be righted before it can be interacted with
|
|
*/
|
|
/atom/movable/proc/tilt_over(turf/target, rotation_angle, should_rotate, rightable, block_interactions_until_righted)
|
|
visible_message("<span class='danger'>[src] tips over!</span>", "<span class='danger'>You hear a loud crash!</span>")
|
|
playsound(src, "sound/effects/bang.ogg", 100, TRUE)
|
|
var/rot_angle = rotation_angle ? rotation_angle : pick(90, -90)
|
|
if(should_rotate)
|
|
var/matrix/to_turn = turn(transform, rot_angle)
|
|
animate(src, transform = to_turn, 0.2 SECONDS)
|
|
if(target && target != get_turf(src))
|
|
throw_at(target, 1, 1, spin = FALSE)
|
|
if(rightable)
|
|
layer = ABOVE_MOB_LAYER
|
|
AddComponent(/datum/component/tilted, 4 SECONDS, block_interactions_until_righted, rot_angle)
|
|
|
|
/// Untilt a tilted object.
|
|
/atom/movable/proc/untilt(mob/living/user, duration = 10 SECONDS)
|
|
SEND_SIGNAL(src, COMSIG_MOVABLE_TRY_UNTILT, user)
|
|
|
|
|
|
/// useful callback for things that want special behavior on crush
|
|
/atom/movable/proc/on_crush_thing(atom/thing)
|
|
return
|
|
|
|
/// Used to scatter atoms so that multiple copies aren't all at the exact same spot.
|
|
/atom/movable/proc/scatter_atom(x_offset = 0, y_offset = 0)
|
|
pixel_x = x_offset + rand(-scatter_distance, scatter_distance)
|
|
pixel_y = y_offset + rand(-scatter_distance, scatter_distance)
|
|
|
|
/**
|
|
* A backwards depth-limited breadth-first-search to see if the target is
|
|
* logically "in" anything adjacent to us.
|
|
*
|
|
* Arguments:
|
|
* * ultimate_target - the specific item we're attempting to reach.
|
|
* * tool - if present, checked to see if the tool can reach the target via [/obj/item/var/reach].
|
|
* * view_only - if TRUE, only considers locations in atoms visible to us, as opposed to nested inventories.
|
|
*/
|
|
/atom/movable/proc/can_reach_nested_adjacent(atom/ultimate_target, obj/item/tool, view_only = FALSE)
|
|
var/list/direct_access = direct_access()
|
|
var/depth = 1 + (view_only ? STORAGE_VIEW_DEPTH : INVENTORY_DEPTH)
|
|
|
|
var/list/closed = list()
|
|
var/list/checking = list(ultimate_target)
|
|
|
|
while(length(checking) && depth > 0)
|
|
var/list/next = list()
|
|
--depth
|
|
|
|
for(var/atom/target in checking) // will filter out nulls
|
|
if(closed[target] || isarea(target)) // avoid infinity situations
|
|
continue
|
|
|
|
if(isturf(target) || isturf(target.loc) || (target in direct_access)) //Directly accessible atoms
|
|
if(Adjacent(target) || (tool && check_tool_reach(src, target, tool.reach))) //Adjacent or reaching attacks
|
|
return TRUE
|
|
|
|
closed[target] = TRUE
|
|
|
|
if(!target.loc)
|
|
continue
|
|
|
|
checking = next
|
|
return FALSE
|