Files
Bubberstation/code/datums/components/tether.dm
T
MrMelbert 442ad835bc Reverts inertia based space movement (#95536)
## About The Pull Request

Reverts space movement being affected by inertia 

What is kept:

- Items have a varying force on your drift speed, ie heavier items will
move you faster through space, and smaller items, slower.
- Jetpacks can have varying force of impulse - the effect is applied
directly to the mob's `inertia_move_multiplier`
- Tethers are unreverted - they still stop you from drifting too far
from the tether point, however you can no longer 'swing' with them.

What is removed:

- Multiple impulses in the same angle/direction no longer speeds you up.
Only the fastest impulse in 1 direction is accounted for.
- An impulse in a different angle/direction will completely override any
existing impulses, even if they are faster.
- Jetpack stabilizers are once again perfectly capable of immediately
stopping any active impulses.

TL;DR

If you point yourself in a direction you will now go that direction

## Why It's Good For The Game

The concept was fun and had potential but the fight between impulses vs
tiles was very, very clunky and janky.
Multiple fixes were attempted to reduce the jank but it ultimately
nograv still acts very cumbersome and jetpacks are still very
unappealing to use.
Smartkar gave the go-ahead to revert this a while back so, o7. 

## Changelog

🆑 Melbert
del: Zero-gravity drifting is no longer affected by inertia, ie it has
been reverted to what it once was.
/🆑
2026-04-03 15:06:38 +01:00

342 lines
14 KiB
Plaintext

/// Creates a tether between two objects that limits movement range. Tether requires LOS and can be adjusted by left/right clicking its
/datum/component/tether
dupe_mode = COMPONENT_DUPE_ALLOWED
/// Other side of the tether
var/atom/tether_target
/// Maximum (and initial) distance that this tether can be adjusted to
var/max_dist
/// What the tether is going to be called
var/tether_name
/// Current extension distance
var/cur_dist
/// Embedded item that the tether "should" originate from
var/atom/embed_target
/// Beam effect
var/datum/beam/tether_beam
/// Tether module if we were created by one
var/obj/item/mod/module/tether/parent_module
/// Ref of source, if any, for TRAIT_TETHER_ATTACHED we add
var/tether_trait_source
/// If TRUE, only add TRAIT_TETHER_ATTACHED to our parent
var/no_target_trait
/// Are we currently attempting to forcefully shorten the tether?
var/force_moving_target = FALSE
/datum/component/tether/Initialize(atom/tether_target, max_dist = 7, tether_name, atom/embed_target = null, start_distance = null, \
parent_module = null, tether_trait_source = null, no_target_trait = FALSE)
if(!ismovable(parent) || !istype(tether_target) || !tether_target.loc)
return COMPONENT_INCOMPATIBLE
if(isatom(tether_trait_source))
stack_trace("Tried to add a [src.type] with a tether_trait_source that is a hard ref! Use REF() first before passing!")
return COMPONENT_INCOMPATIBLE
src.tether_target = tether_target
src.embed_target = embed_target
src.max_dist = max_dist
src.parent_module = parent_module
src.tether_trait_source = tether_trait_source
src.no_target_trait = no_target_trait
cur_dist = max_dist
if (start_distance != null)
cur_dist = start_distance
var/datum/beam/beam = tether_target.Beam(parent, "line", 'icons/obj/clothing/modsuit/mod_modules.dmi', emissive = FALSE, beam_type = /obj/effect/ebeam/tether)
tether_beam = beam
if (ispath(tether_name, /atom))
var/atom/tmp = tether_name
src.tether_name = initial(tmp.name)
else
src.tether_name = tether_name
if (!isnull(tether_trait_source))
ADD_TRAIT(parent, TRAIT_TETHER_ATTACHED, tether_trait_source)
if (!no_target_trait)
ADD_TRAIT(tether_target, TRAIT_TETHER_ATTACHED, tether_trait_source)
/datum/component/tether/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(check_tether))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(check_snap))
RegisterSignal(tether_target, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(check_tether))
RegisterSignal(tether_target, COMSIG_MOVABLE_MOVED, PROC_REF(check_snap))
RegisterSignal(tether_target, COMSIG_QDELETING, PROC_REF(on_delete))
RegisterSignal(tether_beam.visuals, COMSIG_CLICK, PROC_REF(beam_click))
// Also snap if the beam gets deleted, more of a backup check than anything
RegisterSignal(tether_beam.visuals, COMSIG_QDELETING, PROC_REF(on_delete))
if (!isnull(embed_target))
RegisterSignal(embed_target, COMSIG_ITEM_UNEMBEDDED, PROC_REF(on_embedded_removed))
RegisterSignal(embed_target, COMSIG_QDELETING, PROC_REF(on_delete))
if (!isnull(parent_module))
RegisterSignals(parent_module, list(COMSIG_QDELETING, COMSIG_MOVABLE_MOVED, COMSIG_MOD_TETHER_SNAP), PROC_REF(snap))
RegisterSignal(parent_module, COMSIG_MODULE_TRIGGERED, PROC_REF(on_parent_use))
/datum/component/tether/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE, COMSIG_MOVABLE_MOVED))
if (!isnull(tether_trait_source))
REMOVE_TRAIT(parent, TRAIT_TETHER_ATTACHED, tether_trait_source)
if (!QDELETED(tether_beam))
UnregisterSignal(tether_beam.visuals, list(COMSIG_CLICK, COMSIG_QDELETING))
qdel(tether_beam)
if (!QDELETED(embed_target))
UnregisterSignal(embed_target, list(COMSIG_ITEM_UNEMBEDDED, COMSIG_QDELETING))
if (!QDELETED(tether_target))
UnregisterSignal(tether_target, list(COMSIG_MOVABLE_PRE_MOVE, COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
if (!isnull(tether_trait_source) && !no_target_trait)
REMOVE_TRAIT(tether_target, TRAIT_TETHER_ATTACHED, tether_trait_source)
SEND_SIGNAL(tether_target, COMSIG_ATOM_TETHER_SNAPPED, tether_trait_source)
SEND_SIGNAL(parent, COMSIG_ATOM_TETHER_SNAPPED, tether_trait_source)
/datum/component/tether/proc/check_tether(atom/source, new_loc)
SIGNAL_HANDLER
if (check_snap(is_moving = TRUE))
return
if (!isturf(new_loc))
to_chat(source, span_warning("[tether_name] prevents you from entering [new_loc]!"))
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
// If this was called, we know its a movable
var/atom/movable/movable_source = source
var/atom/movable/anchor = (source == tether_target ? parent : tether_target)
// Ignore distance limitations if we're attempting to move the other part of the tether
if (get_dist(anchor, new_loc) > cur_dist && !force_moving_target)
if (!istype(anchor) || anchor.anchored || anchor.move_resist > movable_source.move_force)
to_chat(source, span_warning("[tether_name] runs out of slack and prevents you from moving!"))
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
force_moving_target = TRUE
if (!try_adjust_position(anchor, new_loc, source))
force_moving_target = FALSE
to_chat(source, span_warning("[tether_name] runs out of slack and prevents you from moving!"))
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
force_moving_target = FALSE
var/atom/blocker = check_line(anchor, new_loc, list(source))
if (blocker)
if (!istype(anchor) || anchor.anchored || anchor.move_resist > movable_source.move_force)
to_chat(source, span_warning("[tether_name] runs out of slack and prevents you from moving!"))
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
// If the tether would snag on something when we move, see if we could move to the side to get LOS back
if (!try_adjust_position(anchor, new_loc, source))
to_chat(source, span_warning("[tether_name] catches on [blocker] and prevents you from moving!"))
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
/// Try adjust the anchor's position to move closer to the target or regain LOS
/// true_source is an optional argument in case we're looking for a LOS/closer turf to a new location rather than the actual owner, and need to ignore them
/datum/component/tether/proc/try_adjust_position(atom/movable/anchor, atom/target, atom/true_source)
if (!istype(anchor) || anchor.anchored)
return FALSE
if (anchor.x == target.x && anchor.y == target.y)
return TRUE
var/datum/can_pass_info/pass_info = new(no_id = TRUE)
pass_info.pass_flags = anchor.pass_flags
pass_info.movement_type = anchor.movement_type
if (isliving(anchor))
var/mob/living/living_anchor = anchor
pass_info.is_living = TRUE
pass_info.mob_size = living_anchor.mob_size
pass_info.incorporeal_move = living_anchor.incorporeal_move
pass_info.is_bot = isbot(living_anchor)
var/list/pass_turfs = list()
var/turf/anchor_turf = get_turf(anchor)
var/primary_cardinal = null
if (abs(anchor.x - target.x) > abs(anchor.y - target.y))
primary_cardinal = anchor.x > target.x ? WEST : EAST
else
primary_cardinal = anchor.y > target.y ? SOUTH : NORTH
var/anchor_dir = get_dir(anchor, target)
if (primary_cardinal == anchor_dir)
pass_turfs += get_step(anchor, primary_cardinal)
else if (get_dist(anchor, get_step(target, REVERSE_DIR(primary_cardinal))) >= get_dist(anchor, get_step(target, REVERSE_DIR(anchor_dir))))
pass_turfs += get_step(anchor, anchor_dir)
pass_turfs += get_step(anchor, primary_cardinal)
else
pass_turfs += get_step(anchor, primary_cardinal)
pass_turfs += get_step(anchor, anchor_dir)
// Make a list of secondary dirs to try and sidestep into if we cannot go in our main direction
var/list/match_dirs = null
if (primary_cardinal == NORTH || primary_cardinal == SOUTH)
match_dirs = list(EAST, WEST)
else
match_dirs = list(NORTH, SOUTH)
for (var/match_dir in match_dirs)
if ((match_dir & primary_cardinal) != anchor_dir)
pass_turfs += get_step(anchor, match_dir | primary_cardinal)
for (var/match_dir in match_dirs)
pass_turfs += get_step(anchor, match_dir)
// The final list is something like (direct path, main cardinal, diagonals to main cardinal, 90* dirs to the main cardinal)
// Whichever one we manage to move onto first is our pick
var/list/turf_cache = list()
for (var/turf/pass_turf in pass_turfs) // keep the typecheck in case we accidentally go out of map bounds
if (pass_turf.density || get_dist(pass_turf, target) > cur_dist)
continue
if (anchor_turf.LinkBlockedWithAccess(pass_turf, pass_info))
continue
if (check_line(pass_turf, target, list(anchor, true_source), turf_cache))
continue
if (anchor.Move(pass_turf))
return TRUE
return FALSE
/// Check LOS availibility of a tile, returns a blocking atom, if any
/// turf_cache could be used to reduce the amount of calculations if multiple lines are cast and expected to have multiple shared turfs
/// by sharing located results
/datum/component/tether/proc/check_line(atom/start, atom/end, list/to_ignore, list/turf_cache = list())
var/turf/start_loc = get_turf(start)
var/turf/end_loc = get_turf(end)
var/start_dir = get_dir(start_loc, end_loc)
var/end_dir = REVERSE_DIR(start_dir)
var/list/turf/turf_line = get_line(start_loc, end_loc)
for (var/turf/line_turf in turf_line)
if (turf_cache[line_turf])
return turf_cache[line_turf]
if (line_turf.density && line_turf != start_loc && line_turf != end_loc)
turf_cache[line_turf] = line_turf
return line_turf
if (line_turf == start_loc)
for (var/atom/in_turf in line_turf)
if (in_turf.density && (in_turf.flags_1 & ON_BORDER_1) && (in_turf.dir & start_dir) && in_turf != start && !(in_turf in to_ignore))
turf_cache[line_turf] = in_turf
return in_turf
continue
if (line_turf == end_loc)
for (var/atom/in_turf in line_turf)
if (in_turf.density && (in_turf.flags_1 & ON_BORDER_1) && (in_turf.dir & end_dir) && in_turf != end && !(in_turf in to_ignore))
turf_cache[line_turf] = in_turf
return in_turf
continue
for (var/atom/in_turf in line_turf)
if (!in_turf.density || (in_turf in to_ignore))
continue
if ((in_turf.flags_1 & ON_BORDER_1))
// If the tether is in a straight line, we can ignore border objects parallel to us
if (!(in_turf.dir & start_dir) && !(in_turf.dir & end_dir))
continue
// Also ignore objects that we don't intersect with
if (!(get_step(in_turf, in_turf.dir) in turf_line))
continue
turf_cache[line_turf] = in_turf
return in_turf
turf_cache[line_turf] = null
/datum/component/tether/proc/check_snap(atom/movable/source, atom/old_loc, dir, forced, list/old_locs, is_moving = FALSE)
SIGNAL_HANDLER
var/atom/atom_target = parent
// Something broke us out, snap the tether
if (get_dist(atom_target, tether_target) > cur_dist + 1 || !isturf(atom_target.loc) || !isturf(tether_target.loc) || atom_target.z != tether_target.z)
snap()
else if (!is_moving && check_line(atom_target, tether_target) && !(try_adjust_position(atom_target, tether_target) || try_adjust_position(tether_target, atom_target)))
snap()
/datum/component/tether/proc/snap()
SIGNAL_HANDLER
var/atom/atom_target = parent
atom_target.visible_message(span_warning("[atom_target]'s [tether_name] snaps!"), span_userdanger("Your [tether_name] snaps!"), span_hear("You hear a cable snapping."))
playsound(atom_target, 'sound/effects/snap.ogg', 50, TRUE)
qdel(src)
/datum/component/tether/proc/on_parent_use(obj/item/mod/module/module, atom/target)
SIGNAL_HANDLER
if (get_turf(target) == get_turf(tether_target))
return MOD_ABORT_USE
/datum/component/tether/proc/on_delete()
SIGNAL_HANDLER
qdel(src)
/datum/component/tether/proc/on_embedded_removed(atom/source, mob/living/victim)
SIGNAL_HANDLER
parent.AddComponent(/datum/component/tether, source, max_dist, tether_name, cur_dist)
qdel(src)
/datum/component/tether/proc/beam_click(atom/source, atom/location, control, params, mob/user)
SIGNAL_HANDLER
INVOKE_ASYNC(src, PROC_REF(process_beam_click), source, location, params, user)
/datum/component/tether/proc/process_beam_click(atom/source, atom/location, params, mob/user)
var/turf/nearest_turf
for (var/turf/line_turf in get_line(get_turf(parent), get_turf(tether_target)))
if (line_turf.IsReachableBy(user))
nearest_turf = line_turf
break
if (isnull(nearest_turf))
return
if (!user.can_perform_action(nearest_turf))
nearest_turf.balloon_alert(user, "cannot reach!")
return
var/list/modifiers = params2list(params)
if(LAZYACCESS(modifiers, CTRL_CLICK))
location.balloon_alert(user, "cutting the tether...")
if (!do_after(user, 2 SECONDS, user, (user == parent || user == tether_target) ? IGNORE_USER_LOC_CHANGE|IGNORE_TARGET_LOC_CHANGE : NONE))
return
qdel(src)
location.balloon_alert(user, "tether cut!")
to_chat(parent, span_danger("Your [tether_name] has been cut!"))
return
if (LAZYACCESS(modifiers, RIGHT_CLICK))
if (cur_dist >= max_dist)
location.balloon_alert(user, "no coil remaining!")
return
cur_dist += 1
location.balloon_alert(user, "tether extended")
return
if (cur_dist <= 0)
location.balloon_alert(user, "too short!")
return
if (cur_dist > CEILING(get_dist(parent, tether_target), 1))
cur_dist -= 1
location.balloon_alert(user, "tether shortened")
return
if (!ismovable(parent) && !ismovable(tether_target))
location.balloon_alert(user, "too short!")
return
var/atom/movable/movable_parent = parent
var/atom/movable/movable_target = tether_target
if (istype(movable_parent) && !movable_parent.anchored && movable_parent.move_resist <= movable_target.move_force && movable_parent.Move(get_step(movable_parent.loc, get_dir(movable_parent, movable_target))))
cur_dist -= 1
location.balloon_alert(user, "tether shortened")
return
if (istype(movable_target) && !movable_target.anchored && movable_target.move_resist <= movable_parent.move_force && movable_target.Move(get_step(movable_target.loc, get_dir(movable_target, movable_parent))))
cur_dist -= 1
location.balloon_alert(user, "tether shortened")
return
location.balloon_alert(user, "too short!")
/obj/effect/ebeam/tether
mouse_opacity = MOUSE_OPACITY_ICON