Files
Bubberstation/code/modules/mod/modules/module_kinesis.dm
Stonetear 4756a44141 MODSuit procs now pass who clicked the UI button + misc code cleanup (#92424)
## About The Pull Request

Most of these changes are centered around removing `mod.wearer`
references in module function bubble alerts. However, I cleaned up a few
other things that I thought were easy fixes. ~~This PR should be
testmerged~~ nah send it. I think I did a pretty good job testing, but
there might be a bug or two I missed. (debug modsuit should allow
conflicting modules and have unlimited complexity btw)

### Track who clicks the activate button
* Adds `mob/activator` to `on_select()`, `activate()`, `deactivate()`,
`used()`, `on_activation()`, `on_deactivation()` and `on_use()`
* `mod_ui` now passes `ui.user`, which is who actually clicked the
button in the UI.1
* module action proc now passes the person clicking.
* **Alert bubbles:** Modifies many module code bubbles to pass the
activation bubble text to `mob/activator` instead of `mob.wearer` so
that pAIs get feedback on why clicking the button isn't working.

### Cargo clamp
* **Clamp code cleanup:** The cargo clamp now has a variable for the max
creature weight it can support, and the logic is changed around a bit to
support this.
* The cargo clamp uses an `accepted_items` typecache.

### Code cleanup
* **Button malfunction chance** is controlled by a
`MOD_MALFUNCTION_PROB` define.
* **Pathfinder runtime:** `mod_control`'s `GetAccess()` now checks if
there is an access before returning it. (This previously caused runtimes
when using the pathfinder module if you didn't swipe your ID)
* **Pathfinder code tweaks:** Reworks the code for the pathfinder module
a bit. Activation logic is now stored in the module instead of the
implant. The suit is prevented from being recalled by pAIs, which is
controlled by a variable.
* Adds `MODULE_ALLOW_UNWORN`, which lets you activate modules in suits
that aren't currently being worn. Module activation code now smoothly
supports modules being activated while not worn.
* Chameleon module now works when unworn.

This will probably be a Part 1, with a Part 2 to follow. Actions are
kinda funky and could probably be cleaned up a little better. Plus, I
want to make selectable modules theoretically usable by the AI, even if
I leave it disabled.

## Why It's Good For The Game

This PR doesn't contain any balance changes, and I manually disabled any
new serious functionality that pAIs might gain. (Such as being able to
activate the pathfinder implant) They *can* use the chameleon module
with no wearer- but I'm going to consider this a bug that they couldn't
before.

Paves the way for more pAI modsuit nonsense I'm doing downsteam.

## Changelog
🆑 Stonetear
refactor: MODsuit module code now knows who clicked the activation
button.
/🆑

---------

Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
2025-09-07 10:27:38 +00:00

336 lines
12 KiB
Plaintext

///Kinesis - Gives you the ability to move and launch objects.
/obj/item/mod/module/anomaly_locked/kinesis
name = "MOD kinesis module"
desc = "A modular plug-in to the forearm, this module was presumed lost for many years, \
despite the suits it used to be mounted on still seeing some circulation. \
This piece of technology allows the user to generate precise anti-gravity fields, \
letting them move objects as small as a titanium rod to as large as industrial machinery. \
Oddly enough, it doesn't seem to work on living creatures."
icon_state = "kinesis"
module_type = MODULE_ACTIVE
complexity = 3
use_energy_cost = DEFAULT_CHARGE_DRAIN * 3
incompatible_modules = list(/obj/item/mod/module/anomaly_locked/kinesis)
cooldown_time = 0.5 SECONDS
overlay_state_inactive = "module_kinesis"
overlay_state_active = "module_kinesis_on"
accepted_anomalies = list(/obj/item/assembly/signaler/anomaly/grav)
required_slots = list(ITEM_SLOT_GLOVES)
/// Range of the knesis grab.
var/grab_range = 8
/// Time between us hitting objects with kinesis.
var/hit_cooldown_time = 1 SECONDS
/// Stat required for us to grab a mob.
var/stat_required = DEAD
/// Atom we grabbed with kinesis.
var/atom/movable/grabbed_atom
/// Ref of the beam following the grabbed atom.
var/datum/beam/kinesis_beam
/// Overlay we add to each grabbed atom.
var/mutable_appearance/kinesis_icon
/// Our mouse movement catcher.
var/atom/movable/screen/fullscreen/cursor_catcher/kinesis/kinesis_catcher
/// The sounds playing while we grabbed an object.
var/datum/looping_sound/gravgen/kinesis/soundloop
/// The cooldown between us hitting objects with kinesis.
COOLDOWN_DECLARE(hit_cooldown)
/obj/item/mod/module/anomaly_locked/kinesis/Initialize(mapload)
. = ..()
soundloop = new(src)
/obj/item/mod/module/anomaly_locked/kinesis/Destroy()
QDEL_NULL(soundloop)
return ..()
/obj/item/mod/module/anomaly_locked/kinesis/on_select_use(atom/target)
. = ..()
if(!.)
return
if(!mod.wearer.client)
return
if(grabbed_atom)
var/launched_object = grabbed_atom
clear_grab(playsound = FALSE)
launch(launched_object)
return
if(!range_check(target))
balloon_alert(mod.wearer, "too far!")
return
if(!can_grab(target))
balloon_alert(mod.wearer, "can't grab!")
return
drain_power(use_energy_cost)
grab_atom(target)
/obj/item/mod/module/anomaly_locked/kinesis/on_deactivation(mob/activator, display_message = TRUE, deleting = FALSE)
clear_grab(playsound = !deleting)
/obj/item/mod/module/anomaly_locked/kinesis/process(seconds_per_tick)
if(!mod.wearer.client || INCAPACITATED_IGNORING(mod.wearer, INCAPABLE_GRAB))
clear_grab()
return
if(!range_check(grabbed_atom))
balloon_alert(mod.wearer, "out of range!")
clear_grab()
return
drain_power(use_energy_cost/10)
if(kinesis_catcher.mouse_params)
kinesis_catcher.calculate_params()
if(!kinesis_catcher.given_turf)
return
mod.wearer.setDir(get_dir(mod.wearer, grabbed_atom))
if(grabbed_atom.loc == kinesis_catcher.given_turf)
if(grabbed_atom.pixel_x == kinesis_catcher.given_x - ICON_SIZE_X/2 && grabbed_atom.pixel_y == kinesis_catcher.given_y - ICON_SIZE_Y/2)
return //spare us redrawing if we are standing still
animate(grabbed_atom, 0.2 SECONDS, pixel_x = grabbed_atom.base_pixel_x + kinesis_catcher.given_x - ICON_SIZE_X/2, pixel_y = grabbed_atom.base_pixel_y + kinesis_catcher.given_y - ICON_SIZE_Y/2)
kinesis_beam.redrawing()
return
animate(grabbed_atom, 0.2 SECONDS, pixel_x = grabbed_atom.base_pixel_x + kinesis_catcher.given_x - ICON_SIZE_X/2, pixel_y = grabbed_atom.base_pixel_y + kinesis_catcher.given_y - ICON_SIZE_Y/2)
kinesis_beam.redrawing()
var/turf/next_turf = get_step_towards(grabbed_atom, kinesis_catcher.given_turf)
if(grabbed_atom.Move(next_turf, get_dir(grabbed_atom, next_turf), 8))
if(isitem(grabbed_atom) && (mod.wearer in next_turf))
var/obj/item/grabbed_item = grabbed_atom
clear_grab()
grabbed_item.pickup(mod.wearer)
mod.wearer.put_in_hands(grabbed_item)
return
var/pixel_x_change = 0
var/pixel_y_change = 0
var/direction = get_dir(grabbed_atom, next_turf)
if(direction & NORTH)
pixel_y_change = ICON_SIZE_Y/2
else if(direction & SOUTH)
pixel_y_change = -ICON_SIZE_Y/2
if(direction & EAST)
pixel_x_change = ICON_SIZE_X/2
else if(direction & WEST)
pixel_x_change = -ICON_SIZE_X/2
animate(grabbed_atom, 0.2 SECONDS, pixel_x = grabbed_atom.base_pixel_x + pixel_x_change, pixel_y = grabbed_atom.base_pixel_y + pixel_y_change)
kinesis_beam.redrawing()
if(!isitem(grabbed_atom) || !COOLDOWN_FINISHED(src, hit_cooldown))
return
var/atom/hitting_atom
if(next_turf.density)
hitting_atom = next_turf
for(var/atom/movable/movable_content as anything in next_turf.contents)
if(ismob(movable_content))
continue
if(movable_content.density)
hitting_atom = movable_content
break
var/obj/item/grabbed_item = grabbed_atom
grabbed_item.melee_attack_chain(mod.wearer, hitting_atom)
COOLDOWN_START(src, hit_cooldown, hit_cooldown_time)
/obj/item/mod/module/anomaly_locked/kinesis/proc/can_grab(atom/target)
if(mod.wearer == target)
return FALSE
if(!ismovable(target))
return FALSE
if(iseffect(target))
return FALSE
var/atom/movable/movable_target = target
if(movable_target.anchored)
return FALSE
if(movable_target.throwing)
return FALSE
if(movable_target.move_resist >= MOVE_FORCE_OVERPOWERING)
return FALSE
if(ismob(movable_target))
if(!isliving(movable_target))
return FALSE
var/mob/living/living_target = movable_target
if(living_target.buckled)
return FALSE
if(living_target.stat < stat_required)
return FALSE
else if(isitem(movable_target))
var/obj/item/item_target = movable_target
if(item_target.w_class >= WEIGHT_CLASS_GIGANTIC)
return FALSE
if(item_target.item_flags & ABSTRACT)
return FALSE
return TRUE
/obj/item/mod/module/anomaly_locked/kinesis/proc/grab_atom(atom/movable/target)
grabbed_atom = target
if(isliving(grabbed_atom))
grabbed_atom.add_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), REF(src))
RegisterSignal(grabbed_atom, COMSIG_MOB_STATCHANGE, PROC_REF(on_statchange))
ADD_TRAIT(grabbed_atom, TRAIT_NO_FLOATING_ANIM, REF(src))
RegisterSignal(grabbed_atom, COMSIG_MOVABLE_SET_ANCHORED, PROC_REF(on_setanchored))
playsound(grabbed_atom, 'sound/items/weapons/contractor_baton/contractorbatonhit.ogg', 75, TRUE)
kinesis_icon = mutable_appearance(icon = 'icons/effects/effects.dmi', icon_state = "kinesis", layer = grabbed_atom.layer - 0.1, appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART)
kinesis_icon.overlays += emissive_appearance(icon = 'icons/effects/effects.dmi', icon_state = "kinesis", offset_spokesman = grabbed_atom)
grabbed_atom.add_overlay(kinesis_icon)
kinesis_beam = mod.wearer.Beam(grabbed_atom, "kinesis")
kinesis_catcher = mod.wearer.overlay_fullscreen("kinesis", /atom/movable/screen/fullscreen/cursor_catcher/kinesis, 0)
kinesis_catcher.assign_to_mob(mod.wearer)
RegisterSignal(kinesis_catcher, COMSIG_SCREEN_ELEMENT_CLICK, PROC_REF(on_catcher_click))
soundloop.start()
START_PROCESSING(SSfastprocess, src)
/obj/item/mod/module/anomaly_locked/kinesis/proc/clear_grab(playsound = TRUE)
if(!grabbed_atom)
return
. = grabbed_atom
if(playsound)
playsound(grabbed_atom, 'sound/effects/empulse.ogg', 75, TRUE)
STOP_PROCESSING(SSfastprocess, src)
UnregisterSignal(grabbed_atom, list(COMSIG_MOB_STATCHANGE, COMSIG_MOVABLE_SET_ANCHORED))
kinesis_catcher = null
mod.wearer.clear_fullscreen("kinesis")
grabbed_atom.cut_overlay(kinesis_icon)
QDEL_NULL(kinesis_beam)
if(isliving(grabbed_atom))
grabbed_atom.remove_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), REF(src))
REMOVE_TRAIT(grabbed_atom, TRAIT_NO_FLOATING_ANIM, REF(src))
if(!isitem(grabbed_atom))
animate(grabbed_atom, 0.2 SECONDS, pixel_x = grabbed_atom.base_pixel_x, pixel_y = grabbed_atom.base_pixel_y)
grabbed_atom = null
soundloop.stop()
/obj/item/mod/module/anomaly_locked/kinesis/proc/range_check(atom/target)
if(!isturf(mod.wearer.loc))
return FALSE
if(ismovable(target) && !isturf(target.loc))
return FALSE
if(!can_see(mod.wearer, target, grab_range))
return FALSE
return TRUE
/obj/item/mod/module/anomaly_locked/kinesis/proc/on_catcher_click(atom/source, location, control, params, user)
SIGNAL_HANDLER
var/list/modifiers = params2list(params)
if(LAZYACCESS(modifiers, RIGHT_CLICK))
clear_grab()
/obj/item/mod/module/anomaly_locked/kinesis/proc/on_statchange(mob/grabbed_mob, new_stat)
SIGNAL_HANDLER
if(new_stat < stat_required)
clear_grab()
/obj/item/mod/module/anomaly_locked/kinesis/proc/on_setanchored(atom/movable/grabbed_atom, anchorvalue)
SIGNAL_HANDLER
if(grabbed_atom.anchored)
clear_grab()
/obj/item/mod/module/anomaly_locked/kinesis/proc/launch(atom/movable/launched_object)
playsound(launched_object, 'sound/effects/magic/repulse.ogg', 100, TRUE)
RegisterSignal(launched_object, COMSIG_MOVABLE_IMPACT, PROC_REF(launch_impact))
var/turf/target_turf = get_turf_in_angle(get_angle(mod.wearer, launched_object), get_turf(src), 10)
launched_object.throw_at(target_turf, range = grab_range, speed = launched_object.density ? 3 : 4, thrower = mod.wearer, spin = isitem(launched_object))
/obj/item/mod/module/anomaly_locked/kinesis/proc/launch_impact(atom/movable/source, atom/hit_atom, datum/thrownthing/thrownthing)
UnregisterSignal(source, COMSIG_MOVABLE_IMPACT)
if(!(isstructure(source) || ismachinery(source) || isvehicle(source)))
return
var/damage_self = TRUE
var/damage = 8
if(source.density)
damage_self = FALSE
damage = 15
if(isliving(hit_atom))
var/mob/living/living_atom = hit_atom
living_atom.apply_damage(damage, BRUTE)
else if(hit_atom.uses_integrity)
hit_atom.take_damage(damage, BRUTE, MELEE)
if(damage_self && source.uses_integrity)
source.take_damage(source.max_integrity/5, BRUTE, MELEE)
/atom/movable/screen/fullscreen/cursor_catcher/kinesis
icon_state = "kinesis"
/obj/item/mod/module/anomaly_locked/kinesis/prebuilt
prebuilt = TRUE
/obj/item/mod/module/anomaly_locked/kinesis/prebuilt/locked
core_removable = FALSE
/obj/item/mod/module/anomaly_locked/kinesis/prototype
name = "MOD prototype kinesis module"
prebuilt = TRUE
complexity = 0
use_energy_cost = DEFAULT_CHARGE_DRAIN * 5
removable = FALSE
core_removable = FALSE
/obj/item/mod/module/anomaly_locked/kinesis/plus
name = "MOD kinesis+ module"
desc = "A modular plug-in to the forearm, this module was recently redeveloped in secret. \
The bane of all ne'er-do-wells, the kinesis+ module is a powerful tool that allows the user \
to manipulate the world around them. Like its older counterpart, it's capable of manipulating \
structures, machinery, vehicles, and, thanks to the fruitful efforts of its creators - living beings."
complexity = 0
prebuilt = TRUE
stat_required = CONSCIOUS
/// Admin suit version of kinesis. Can grab anything at any range, may enable phasing through walls.
/obj/item/mod/module/anomaly_locked/kinesis/admin
name = "MOD kinesis++ module"
desc = "A modular plug-in to the forearm, this module was recently reredeveloped in super secret. \
This one can force some of the grasped objects to phase through walls. Oh no."
complexity = 0
grab_range = INFINITY
use_energy_cost = DEFAULT_CHARGE_DRAIN * 0
prebuilt = TRUE
stat_required = CONSCIOUS
/// Does our object phase through stuff?
var/phasing = FALSE
/obj/item/mod/module/anomaly_locked/kinesis/admin/grab_atom(atom/movable/target)
. = ..()
if(phasing)
ADD_TRAIT(grabbed_atom, TRAIT_MOVE_PHASING, REF(src))
/obj/item/mod/module/anomaly_locked/kinesis/admin/clear_grab(playsound)
. = ..()
if(!.)
return
var/atom/movable/previous_grab = .
if(phasing)
REMOVE_TRAIT(previous_grab, TRAIT_MOVE_PHASING, REF(src))
/obj/item/mod/module/anomaly_locked/kinesis/admin/can_grab(atom/target)
if(mod.wearer == target)
return FALSE
if(!ismovable(target))
return FALSE
var/atom/movable/movable_target = target
if(movable_target.throwing)
return FALSE
return TRUE
/obj/item/mod/module/anomaly_locked/kinesis/admin/range_check(atom/target)
if(!isturf(mod.wearer.loc))
return FALSE
if(ismovable(target) && !isturf(target.loc))
return FALSE
if(target.z != mod.wearer.z)
return FALSE
return TRUE
/obj/item/mod/module/anomaly_locked/kinesis/admin/on_setanchored(atom/movable/grabbed_atom, anchorvalue)
return //thog dont care
/obj/item/mod/module/anomaly_locked/kinesis/admin/get_configuration()
. = ..()
.["phasing"] = add_ui_configuration("Phasing", "bool", phasing)
/obj/item/mod/module/anomaly_locked/kinesis/admin/configure_edit(key, value)
switch(key)
if("phasing")
phasing = value
if(!grabbed_atom)
return
if(phasing)
ADD_TRAIT(grabbed_atom, TRAIT_MOVE_PHASING, REF(src))
else
REMOVE_TRAIT(grabbed_atom, TRAIT_MOVE_PHASING, REF(src))