Files
Bubberstation/code/modules/mob/mob_movement.dm
san7890 f39d539b32 Refactors Object Possession into a Component (moar modular, less /mob vars) (#80160)
## About The Pull Request

We have two verbs that allow any given mob to take control of an object
and move it ephemerally, `/proc/possess()` and `/proc/release()`. These
ones leveraged two vars present on every `/mob`: `name_archive` and
`control_object`. I don't like having vars clog up my VV and this just
injected snowflake behavior in a lot of spots - let's just make it a
component that'll clean everything else up.

This also opens up the ability to have more objects be under mob control
without giving someone verbs that spit out to the blackbox as an admin
verb + logs + message admins but that's a later thing. This just subs in
the behavior in a nice way.

Also, since it's a component, I added a small QoL that we can support
now: A screen alert that allows you to get out of the possession early
without navigating the stat panel for the specific verb. I think it's
neat. You can also trigger the aghost keybind if that's something you
want as well.

Also also, nothing actually ever cleaned up `control_object` by setting
it to null. This means that in the old framework, if a mob got qdelled
during a possession, that would have triggered a hung ref harddel. That
won't happen anymore.
## Why It's Good For The Game

Two less variables taking up crud space in the VSC debugger + view
variables panel. Better behavior injection that is far more reusable.
Component handling this behavior allows for better extensibility of this
function in the future.


![image](https://github.com/tgstation/tgstation/assets/34697715/a84238af-e014-4cff-9b4b-6cbaa36c44fd)
## Changelog
🆑
admin: Object Possession has been reworked, please report any potential
bugs.
qol: Object Possession should now throw a screen alert for you to
unpossess the object instead of you having to search the stat-panel for
the "release obj" verb. You can still use the verb but it's a lot nicer
now. Aghosting will also work now.
/🆑
2023-12-09 17:28:19 -05:00

551 lines
17 KiB
Plaintext

/**
* If your mob is concious, drop the item in the active hand
*
* This is a hidden verb, likely for binding with winset for hotkeys
*/
/client/verb/drop_item()
set hidden = TRUE
if(!iscyborg(mob) && mob.stat == CONSCIOUS)
mob.dropItemToGround(mob.get_active_held_item())
return
/**
* Move a client in a direction
*
* Huge proc, has a lot of functionality
*
* Mostly it will despatch to the mob that you are the owner of to actually move
* in the physical realm
*
* Things that stop you moving as a mob:
* * world time being less than your next move_delay
* * not being in a mob, or that mob not having a loc
* * missing the n and direction parameters
* * being in remote control of an object (calls Moveobject instead)
* * being dead (it ghosts you instead)
*
* Things that stop you moving as a mob living (why even have OO if you're just shoving it all
* in the parent proc with istype checks right?):
* * having incorporeal_move set (calls Process_Incorpmove() instead)
* * being grabbed
* * being buckled (relaymove() is called to the buckled atom instead)
* * having your loc be some other mob (relaymove() is called on that mob instead)
* * Not having MOBILITY_MOVE
* * Failing Process_Spacemove() call
*
* At this point, if the mob is is confused, then a random direction and target turf will be calculated for you to travel to instead
*
* Now the parent call is made (to the byond builtin move), which moves you
*
* Some final move delay calculations (doubling if you moved diagonally successfully)
*
* if mob throwing is set I believe it's unset at this point via a call to finalize
*
* Finally if you're pulling an object and it's dense, you are turned 180 after the move
* (if you ask me, this should be at the top of the move so you don't dance around)
*
*/
/client/Move(new_loc, direct)
if(world.time < move_delay) //do not move anything ahead of this check please
return FALSE
next_move_dir_add = 0
next_move_dir_sub = 0
var/old_move_delay = move_delay
move_delay = world.time + world.tick_lag //this is here because Move() can now be called mutiple times per tick
if(!direct || !new_loc)
return FALSE
if(!mob?.loc)
return FALSE
if(HAS_TRAIT(mob, TRAIT_NO_TRANSFORM))
return FALSE //This is sorta the goto stop mobs from moving trait
if(!isliving(mob))
if(SEND_SIGNAL(mob, COMSIG_MOB_CLIENT_PRE_NON_LIVING_MOVE, new_loc, direct) & COMSIG_MOB_CLIENT_BLOCK_PRE_NON_LIVING_MOVE)
return FALSE
return mob.Move(new_loc, direct)
if(mob.stat == DEAD)
mob.ghostize()
return FALSE
if(SEND_SIGNAL(mob, COMSIG_MOB_CLIENT_PRE_LIVING_MOVE, new_loc, direct) & COMSIG_MOB_CLIENT_BLOCK_PRE_LIVING_MOVE)
return FALSE
var/mob/living/L = mob //Already checked for isliving earlier
if(L.incorporeal_move && !is_secret_level(mob.z)) //Move though walls
Process_Incorpmove(direct)
return FALSE
if(mob.remote_control) //we're controlling something, our movement is relayed to it
return mob.remote_control.relaymove(mob, direct)
if(isAI(mob))
return AIMove(new_loc,direct,mob)
if(Process_Grab()) //are we restrained by someone's grip?
return
if(mob.buckled) //if we're buckled to something, tell it we moved.
return mob.buckled.relaymove(mob, direct)
if(!(L.mobility_flags & MOBILITY_MOVE))
return FALSE
if(ismovable(mob.loc)) //Inside an object, tell it we moved
var/atom/loc_atom = mob.loc
return loc_atom.relaymove(mob, direct)
if(!mob.Process_Spacemove(direct))
return FALSE
if(SEND_SIGNAL(mob, COMSIG_MOB_CLIENT_PRE_MOVE, args) & COMSIG_MOB_CLIENT_BLOCK_PRE_MOVE)
return FALSE
//We are now going to move
var/add_delay = mob.cached_multiplicative_slowdown
var/new_glide_size = DELAY_TO_GLIDE_SIZE(add_delay * ( (NSCOMPONENT(direct) && EWCOMPONENT(direct)) ? sqrt(2) : 1 ) )
mob.set_glide_size(new_glide_size) // set it now in case of pulled objects
//If the move was recent, count using old_move_delay
//We want fractional behavior and all
if(old_move_delay + world.tick_lag > world.time)
//Yes this makes smooth movement stutter if add_delay is too fractional
//Yes this is better then the alternative
move_delay = old_move_delay
else
move_delay = world.time
//Basically an optional override for our glide size
//Sometimes you want to look like you're moving with a delay you don't actually have yet
visual_delay = 0
var/old_dir = mob.dir
. = ..()
if((direct & (direct - 1)) && mob.loc == new_loc) //moved diagonally successfully
add_delay *= sqrt(2)
var/after_glide = 0
if(visual_delay)
after_glide = visual_delay
else
after_glide = DELAY_TO_GLIDE_SIZE(add_delay)
mob.set_glide_size(after_glide)
move_delay += add_delay
if(.) // If mob is null here, we deserve the runtime
if(mob.throwing)
mob.throwing.finalize(FALSE)
// At this point we've moved the client's attached mob. This is one of the only ways to guess that a move was done
// as a result of player input and not because they were pulled or any other magic.
SEND_SIGNAL(mob, COMSIG_MOB_CLIENT_MOVED, direct, old_dir)
var/atom/movable/P = mob.pulling
if(P && !ismob(P) && P.density)
mob.setDir(REVERSE_DIR(mob.dir))
/**
* Checks to see if you're being grabbed and if so attempts to break it
*
* Called by client/Move()
*/
/client/proc/Process_Grab()
if(!mob.pulledby)
return FALSE
if(mob.pulledby == mob.pulling && mob.pulledby.grab_state == GRAB_PASSIVE) //Don't autoresist passive grabs if we're grabbing them too.
return FALSE
if(HAS_TRAIT(mob, TRAIT_INCAPACITATED))
COOLDOWN_START(src, move_delay, 1 SECONDS)
return TRUE
else if(HAS_TRAIT(mob, TRAIT_RESTRAINED))
COOLDOWN_START(src, move_delay, 1 SECONDS)
to_chat(src, span_warning("You're restrained! You can't move!"))
return TRUE
return mob.resist_grab(TRUE)
/**
* Allows mobs to ignore density and phase through objects
*
* Called by client/Move()
*
* The behaviour depends on the incorporeal_move value of the mob
*
* * INCORPOREAL_MOVE_BASIC - forceMoved to the next tile with no stop
* * INCORPOREAL_MOVE_SHADOW - the same but leaves a cool effect path
* * INCORPOREAL_MOVE_JAUNT - the same but blocked by holy tiles
*
* You'll note this is another mob living level proc living at the client level
*/
/client/proc/Process_Incorpmove(direct)
var/turf/mobloc = get_turf(mob)
if(!isliving(mob))
return
var/mob/living/L = mob
switch(L.incorporeal_move)
if(INCORPOREAL_MOVE_BASIC)
var/T = get_step(L,direct)
if(T)
L.forceMove(T)
L.setDir(direct)
if(INCORPOREAL_MOVE_SHADOW)
if(prob(50))
var/locx
var/locy
switch(direct)
if(NORTH)
locx = mobloc.x
locy = (mobloc.y+2)
if(locy>world.maxy)
return
if(SOUTH)
locx = mobloc.x
locy = (mobloc.y-2)
if(locy<1)
return
if(EAST)
locy = mobloc.y
locx = (mobloc.x+2)
if(locx>world.maxx)
return
if(WEST)
locy = mobloc.y
locx = (mobloc.x-2)
if(locx<1)
return
else
return
var/target = locate(locx,locy,mobloc.z)
if(target)
L.forceMove(target)
var/limit = 2//For only two trailing shadows.
for(var/turf/T in get_line(mobloc, L.loc))
new /obj/effect/temp_visual/dir_setting/ninja/shadow(T, L.dir)
limit--
if(limit <= 0)
break
else
new /obj/effect/temp_visual/dir_setting/ninja/shadow(mobloc, L.dir)
var/T = get_step(L,direct)
if(T)
L.forceMove(T)
L.setDir(direct)
if(INCORPOREAL_MOVE_JAUNT) //Incorporeal move, but blocked by holy-watered tiles and salt piles.
var/turf/open/floor/stepTurf = get_step(L, direct)
if(stepTurf)
var/obj/effect/decal/cleanable/food/salt/salt = locate() in stepTurf
if(salt)
to_chat(L, span_warning("[salt] bars your passage!"))
if(isrevenant(L))
var/mob/living/basic/revenant/ghostie = L
ghostie.apply_status_effect(/datum/status_effect/revenant/revealed, 2 SECONDS)
ghostie.apply_status_effect(/datum/status_effect/incapacitating/paralyzed/revenant, 2 SECONDS)
return
if(stepTurf.turf_flags & NOJAUNT)
to_chat(L, span_warning("Some strange aura is blocking the way."))
return
if(locate(/obj/effect/blessing) in stepTurf)
to_chat(L, span_warning("Holy energies block your path!"))
return
L.forceMove(stepTurf)
L.setDir(direct)
return TRUE
/**
* Handles mob/living movement in space (or no gravity)
*
* Called by /client/Move()
*
* return TRUE for movement or FALSE for none
*
* You can move in space if you have a spacewalk ability
*/
/mob/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
. = ..()
if(. || HAS_TRAIT(src, TRAIT_SPACEWALK))
return TRUE
// FUCK OFF
if(buckled)
return TRUE
var/atom/movable/backup = get_spacemove_backup(movement_dir, continuous_move)
if(!backup)
return FALSE
if(continuous_move || !istype(backup) || !movement_dir || backup.anchored)
return TRUE
// last pushoff exists for one reason
// to ensure pushing a mob doesn't just lead to it considering us as backup, and failing
last_pushoff = world.time
if(backup.newtonian_move(REVERSE_DIR(movement_dir), instant = TRUE)) //You're pushing off something movable, so it moves
// We set it down here so future calls to Process_Spacemove by the same pair in the same tick don't lead to fucky
backup.last_pushoff = world.time
to_chat(src, span_info("You push off of [backup] to propel yourself."))
return TRUE
/**
* Finds a target near a mob that is viable for pushing off when moving.
* Takes the intended movement direction as input, alongside if the context is checking if we're allowed to continue drifting
*/
/mob/get_spacemove_backup(moving_direction, continuous_move)
for(var/atom/pushover as anything in range(1, get_turf(src)))
if(pushover == src)
continue
if(isarea(pushover))
continue
if(isturf(pushover))
var/turf/turf = pushover
if(isspaceturf(turf))
continue
if(!turf.density && !mob_negates_gravity())
continue
return pushover
var/atom/movable/rebound = pushover
if(rebound == buckled)
continue
if(ismob(rebound))
var/mob/lover = rebound
if(lover.buckled)
continue
var/pass_allowed = rebound.CanPass(src, get_dir(rebound, src))
if(!rebound.density && pass_allowed)
continue
//Sometime this tick, this pushed off something. Doesn't count as a valid pushoff target
if(rebound.last_pushoff == world.time)
continue
if(continuous_move && !pass_allowed)
var/datum/move_loop/move/rebound_engine = SSmove_manager.processing_on(rebound, SSspacedrift)
// If you're moving toward it and you're both going the same direction, stop
if(moving_direction == get_dir(src, pushover) && rebound_engine && moving_direction == rebound_engine.direction)
continue
else if(!pass_allowed)
if(moving_direction == get_dir(src, pushover)) // Can't push "off" of something that you're walking into
continue
if(rebound.anchored)
return rebound
if(pulling == rebound)
continue
return rebound
/mob/has_gravity(turf/gravity_turf)
return mob_negates_gravity() || ..()
/**
* Does this mob ignore gravity
*/
/mob/proc/mob_negates_gravity()
return FALSE
/**
* Called when this mob slips over, override as needed
*
* knockdown_amount - time (in deciseconds) the slip leaves them on the ground
* slipped_on - optional, what'd we slip on? if not set, we assume they just fell over
* lube - bitflag of "lube flags", see [mobs.dm] for more information
* paralyze - time (in deciseconds) the slip leaves them paralyzed / unable to move
* force_drop = the slip forces them to drop held items
*/
/mob/proc/slip(knockdown_amount, obj/slipped_on, lube_flags, paralyze, force_drop = FALSE)
add_mob_memory(/datum/memory/was_slipped, antagonist = slipped_on)
//bodypart selection verbs - Cyberboss
//8: repeated presses toggles through head - eyes - mouth
//9: eyes 8: head 7: mouth
//4: r-arm 5: chest 6: l-arm
//1: r-leg 2: groin 3: l-leg
///Validate the client's mob has a valid zone selected
/client/proc/check_has_body_select()
return mob && mob.hud_used && mob.hud_used.zone_select && istype(mob.hud_used.zone_select, /atom/movable/screen/zone_sel)
/**
* Hidden verb to set the target zone of a mob to the head
*
* (bound to 8) - repeated presses toggles through head - eyes - mouth
*/
///Hidden verb to target the head, bound to 8
/client/verb/body_toggle_head()
set name = "body-toggle-head"
set hidden = TRUE
if(!check_has_body_select())
return
var/next_in_line
switch(mob.zone_selected)
if(BODY_ZONE_HEAD)
next_in_line = BODY_ZONE_PRECISE_EYES
if(BODY_ZONE_PRECISE_EYES)
next_in_line = BODY_ZONE_PRECISE_MOUTH
else
next_in_line = BODY_ZONE_HEAD
var/atom/movable/screen/zone_sel/selector = mob.hud_used.zone_select
selector.set_selected_zone(next_in_line, mob)
///Hidden verb to target the eyes, bound to 7
/client/verb/body_eyes()
set name = "body-eyes"
set hidden = TRUE
if(!check_has_body_select())
return
var/atom/movable/screen/zone_sel/selector = mob.hud_used.zone_select
selector.set_selected_zone(BODY_ZONE_PRECISE_EYES, mob)
///Hidden verb to target the mouth, bound to 9
/client/verb/body_mouth()
set name = "body-mouth"
set hidden = TRUE
if(!check_has_body_select())
return
var/atom/movable/screen/zone_sel/selector = mob.hud_used.zone_select
selector.set_selected_zone(BODY_ZONE_PRECISE_MOUTH, mob)
///Hidden verb to target the right arm, bound to 4
/client/verb/body_r_arm()
set name = "body-r-arm"
set hidden = TRUE
if(!check_has_body_select())
return
var/atom/movable/screen/zone_sel/selector = mob.hud_used.zone_select
selector.set_selected_zone(BODY_ZONE_R_ARM, mob)
///Hidden verb to target the chest, bound to 5
/client/verb/body_chest()
set name = "body-chest"
set hidden = TRUE
if(!check_has_body_select())
return
var/atom/movable/screen/zone_sel/selector = mob.hud_used.zone_select
selector.set_selected_zone(BODY_ZONE_CHEST, mob)
///Hidden verb to target the left arm, bound to 6
/client/verb/body_l_arm()
set name = "body-l-arm"
set hidden = TRUE
if(!check_has_body_select())
return
var/atom/movable/screen/zone_sel/selector = mob.hud_used.zone_select
selector.set_selected_zone(BODY_ZONE_L_ARM, mob)
///Hidden verb to target the right leg, bound to 1
/client/verb/body_r_leg()
set name = "body-r-leg"
set hidden = TRUE
if(!check_has_body_select())
return
var/atom/movable/screen/zone_sel/selector = mob.hud_used.zone_select
selector.set_selected_zone(BODY_ZONE_R_LEG, mob)
///Hidden verb to target the groin, bound to 2
/client/verb/body_groin()
set name = "body-groin"
set hidden = TRUE
if(!check_has_body_select())
return
var/atom/movable/screen/zone_sel/selector = mob.hud_used.zone_select
selector.set_selected_zone(BODY_ZONE_PRECISE_GROIN, mob)
///Hidden verb to target the left leg, bound to 3
/client/verb/body_l_leg()
set name = "body-l-leg"
set hidden = TRUE
if(!check_has_body_select())
return
var/atom/movable/screen/zone_sel/selector = mob.hud_used.zone_select
selector.set_selected_zone(BODY_ZONE_L_LEG, mob)
///Verb to toggle the walk or run status
/client/verb/toggle_walk_run()
set name = "toggle-walk-run"
set hidden = TRUE
set instant = TRUE
if(isliving(mob))
var/mob/living/user_mob = mob
user_mob.toggle_move_intent(usr)
/**
* Toggle the move intent of the mob
*
* triggers an update the move intent hud as well
*/
/mob/living/proc/toggle_move_intent(mob/user)
if(move_intent == MOVE_INTENT_RUN)
move_intent = MOVE_INTENT_WALK
else
move_intent = MOVE_INTENT_RUN
if(hud_used?.static_inventory)
for(var/atom/movable/screen/mov_intent/selector in hud_used.static_inventory)
selector.update_appearance()
update_move_intent_slowdown()
///Moves a mob upwards in z level
/mob/verb/up()
set name = "Move Upwards"
set category = "IC"
var/turf/current_turf = get_turf(src)
var/turf/above_turf = GET_TURF_ABOVE(current_turf)
var/ventcrawling_flag = HAS_TRAIT(src, TRAIT_MOVE_VENTCRAWLING) ? ZMOVE_VENTCRAWLING : 0
if(!above_turf)
to_chat(src, span_warning("There's nowhere to go in that direction!"))
return
if(ismovable(loc)) //Inside an object, tell it we moved
var/atom/loc_atom = loc
return loc_atom.relaymove(src, UP)
if(can_z_move(DOWN, above_turf, current_turf, ZMOVE_FALL_FLAGS|ventcrawling_flag)) //Will we fall down if we go up?
if(buckled)
to_chat(src, span_warning("[buckled] is is not capable of flight."))
else
to_chat(src, span_warning("You are not Superman."))
return
if(zMove(UP, z_move_flags = ZMOVE_FLIGHT_FLAGS|ZMOVE_FEEDBACK|ventcrawling_flag))
to_chat(src, span_notice("You move upwards."))
///Moves a mob down a z level
/mob/verb/down()
set name = "Move Down"
set category = "IC"
var/turf/current_turf = get_turf(src)
var/turf/below_turf = GET_TURF_BELOW(current_turf)
if(!below_turf)
to_chat(src, span_warning("There's nowhere to go in that direction!"))
return
if(ismovable(loc)) //Inside an object, tell it we moved
var/atom/loc_atom = loc
return loc_atom.relaymove(src, DOWN)
var/ventcrawling_flag = HAS_TRAIT(src, TRAIT_MOVE_VENTCRAWLING) ? ZMOVE_VENTCRAWLING : 0
if(zMove(DOWN, z_move_flags = ZMOVE_FLIGHT_FLAGS|ZMOVE_FEEDBACK|ventcrawling_flag))
to_chat(src, span_notice("You move down."))
return FALSE
/mob/abstract_move(atom/destination)
var/turf/new_turf = get_turf(destination)
if(new_turf && (istype(new_turf, /turf/cordon/secret) || is_secret_level(new_turf.z)) && !client?.holder)
return
return ..()