Files
Bubberstation/code/modules/mod/modules/_module.dm
SkyratBot 55135555a0 [MIRROR] Nuclear Operative Jump Jets [MDB IGNORE] (#23659)
* Nuclear Operative Jump Jets (#78088)

## About The Pull Request

This PR gives operative MODsuits access to "jump jets".
This is an activated module (starts pinned) with a 30 second cooldown
which removes your personal gravity for 5 seconds and (if possible)
pushes you upwards by one z level. In combination with your regular
jetpack this allows you to fly over gaps, and (most importantly) out of
pits such as you may inadvertently find yourself wandering into on
Icebox.
I have a few other changes I want to make specifically targetted at the
experience of Icebox station destruction causing people to fall several
z levels and get trapped, but this is the first one.

You have to stand still for 1 second to activate the jump jet. This is
because jetpack movement without gravity is actually usually faster than
an operative will walk, and I don't want them to just toggle it as a
sprint button while running around. If people find other tactical uses
for this though I think that's cool.

This module currently isn't available to crew on the tech web, although
maybe someone could add it later if they wanted to. It's not quite so
useful if you don't _also_ have a jetpack though.
I bumped the available complexity of the suits I attached it to up by
the complexity cost of this module so it's not taking up previously
available space.

## Why It's Good For The Game

It's funny when the whole ops team falls in a hole after an explosion
they caused and gets stuck in there fighting Snow Legions but they
should probably have some method for dealing with that.
It also lets them pop back up from the tram hole, a risky proposition
because any flying mob hit by the tram dies almost instantly.

## Changelog

🆑
add: Operative MODsuits now have an attached "jump jet" which sends you
upwards and allows you to use your jetpack under gravity for a few
seconds, perfect for navigating the pits and valleys of Icebox Station.
/🆑

* Nuclear Operative Jump Jets

---------

Co-authored-by: Jacquerel <hnevard@gmail.com>
2023-09-12 13:14:22 -07:00

431 lines
14 KiB
Plaintext

///MOD Module - A special device installed in a MODsuit allowing the suit to do new stuff.
/obj/item/mod/module
name = "MOD module"
icon = 'icons/obj/clothing/modsuit/mod_modules.dmi'
icon_state = "module"
/// If it can be removed
var/removable = TRUE
/// If it's passive, togglable, usable or active
var/module_type = MODULE_PASSIVE
/// Is the module active
var/active = FALSE
/// How much space it takes up in the MOD
var/complexity = 0
/// Power use when idle
var/idle_power_cost = DEFAULT_CHARGE_DRAIN * 0
/// Power use when active
var/active_power_cost = DEFAULT_CHARGE_DRAIN * 0
/// Power use when used, we call it manually
var/use_power_cost = DEFAULT_CHARGE_DRAIN * 0
/// ID used by their TGUI
var/tgui_id
/// Linked MODsuit
var/obj/item/mod/control/mod
/// If we're an active module, what item are we?
var/obj/item/device
/// Overlay given to the user when the module is inactive
var/overlay_state_inactive
/// Overlay given to the user when the module is active
var/overlay_state_active
/// Overlay given to the user when the module is used, lasts until cooldown finishes
var/overlay_state_use
/// Icon file for the overlay.
var/overlay_icon_file = 'icons/mob/clothing/modsuit/mod_modules.dmi'
/// Does the overlay use the control unit's colors?
var/use_mod_colors = FALSE
/// What modules are we incompatible with?
var/list/incompatible_modules = list()
/// Cooldown after use
var/cooldown_time = 0
/// The mouse button needed to use this module
var/used_signal
/// List of REF()s mobs we are pinned to, linked with their action buttons
var/list/pinned_to = list()
/// flags that let the module ability be used in odd circumstances
var/allow_flags = NONE
/// Timer for the cooldown
COOLDOWN_DECLARE(cooldown_timer)
/obj/item/mod/module/Initialize(mapload)
. = ..()
if(module_type != MODULE_ACTIVE)
return
if(ispath(device))
device = new device(src)
ADD_TRAIT(device, TRAIT_NODROP, MOD_TRAIT)
RegisterSignal(device, COMSIG_QDELETING, PROC_REF(on_device_deletion))
RegisterSignal(src, COMSIG_ATOM_EXITED, PROC_REF(on_exit))
/obj/item/mod/module/Destroy()
mod?.uninstall(src)
if(device)
UnregisterSignal(device, COMSIG_QDELETING)
QDEL_NULL(device)
return ..()
/obj/item/mod/module/examine(mob/user)
. = ..()
if(HAS_TRAIT(user, TRAIT_DIAGNOSTIC_HUD))
. += span_notice("Complexity level: [complexity]")
/// Called when the module is selected from the TGUI, radial or the action button
/obj/item/mod/module/proc/on_select()
if(((!mod.active || mod.activating) && !(allow_flags & MODULE_ALLOW_INACTIVE)) || module_type == MODULE_PASSIVE)
if(mod.wearer)
balloon_alert(mod.wearer, "not active!")
return
// SKYRAT EDIT START - DEPLOYABLE EVERYTHING OVER EVERYTHING
if((mod.wearer.wear_suit != mod.chestplate) && !(allow_flags & MODULE_ALLOW_INACTIVE))
balloon_alert(mod.wearer, "chestplate retracted!")
return
// SKYRAT EDIT END
if(module_type != MODULE_USABLE)
if(active)
on_deactivation()
else
on_activation()
else
on_use()
SEND_SIGNAL(mod, COMSIG_MOD_MODULE_SELECTED, src)
/// Apply a cooldown until this item can be used again
/obj/item/mod/module/proc/start_cooldown(applied_cooldown)
if (isnull(applied_cooldown))
applied_cooldown = cooldown_time
COOLDOWN_START(src, cooldown_timer, applied_cooldown)
SEND_SIGNAL(src, COMSIG_MODULE_COOLDOWN_STARTED, applied_cooldown)
/// Called when the module is activated
/obj/item/mod/module/proc/on_activation()
if(!COOLDOWN_FINISHED(src, cooldown_timer))
balloon_alert(mod.wearer, "on cooldown!")
return FALSE
if(((!mod.active || mod.activating) && !(allow_flags & MODULE_ALLOW_INACTIVE)) || !mod.get_charge()) //SKYRAT ADDITION: INACTIVE USE
balloon_alert(mod.wearer, "unpowered!")
return FALSE
// SKYRAT EDIT START - No using modules when not all parts are deployed.
if(!(allow_flags & MODULE_ALLOW_INACTIVE))
for(var/obj/item/part as anything in mod.mod_parts)
if(part.loc == mod)
balloon_alert(mod.wearer, "deploy all parts first!")
return FALSE
// SKYRAT EDIT END
if(!(allow_flags & MODULE_ALLOW_PHASEOUT) && istype(mod.wearer.loc, /obj/effect/dummy/phased_mob))
//specifically a to_chat because the user is phased out.
to_chat(mod.wearer, span_warning("You cannot activate this right now."))
return FALSE
if(SEND_SIGNAL(src, COMSIG_MODULE_TRIGGERED, mod.wearer) & MOD_ABORT_USE)
return FALSE
if(module_type == MODULE_ACTIVE)
if(mod.selected_module && !mod.selected_module.on_deactivation(display_message = FALSE))
return FALSE
mod.selected_module = src
if(device)
if(mod.wearer.put_in_hands(device))
balloon_alert(mod.wearer, "[device] extended")
RegisterSignal(mod.wearer, COMSIG_ATOM_EXITED, PROC_REF(on_exit))
RegisterSignal(mod.wearer, COMSIG_KB_MOB_DROPITEM_DOWN, PROC_REF(dropkey))
else
balloon_alert(mod.wearer, "can't extend [device]!")
mod.wearer.transferItemToLoc(device, src, force = TRUE)
return FALSE
else
var/used_button = mod.wearer.client?.prefs.read_preference(/datum/preference/choiced/mod_select) || MIDDLE_CLICK
update_signal(used_button)
balloon_alert(mod.wearer, "[src] activated, [used_button]-click to use")
active = TRUE
mod.wearer.update_clothing(mod.slot_flags)
start_cooldown()
SEND_SIGNAL(src, COMSIG_MODULE_ACTIVATED)
return TRUE
/// Called when the module is deactivated
/obj/item/mod/module/proc/on_deactivation(display_message = TRUE, deleting = FALSE)
active = FALSE
if(module_type == MODULE_ACTIVE)
mod.selected_module = null
if(display_message)
balloon_alert(mod.wearer, device ? "[device] retracted" : "[src] deactivated")
if(device)
mod.wearer.transferItemToLoc(device, src, force = TRUE)
UnregisterSignal(mod.wearer, COMSIG_ATOM_EXITED)
UnregisterSignal(mod.wearer, COMSIG_KB_MOB_DROPITEM_DOWN)
else
UnregisterSignal(mod.wearer, used_signal)
used_signal = null
mod.wearer.update_clothing(mod.slot_flags)
SEND_SIGNAL(src, COMSIG_MODULE_DEACTIVATED, mod.wearer)
return TRUE
/// Called when the module is used
/obj/item/mod/module/proc/on_use()
if(!COOLDOWN_FINISHED(src, cooldown_timer))
balloon_alert(mod.wearer, "on cooldown!")
return FALSE
if(!check_power(use_power_cost))
balloon_alert(mod.wearer, "not enough charge!")
return FALSE
if(!(allow_flags & MODULE_ALLOW_PHASEOUT) && istype(mod.wearer.loc, /obj/effect/dummy/phased_mob))
//specifically a to_chat because the user is phased out.
to_chat(mod.wearer, span_warning("You cannot activate this right now."))
return FALSE
if(SEND_SIGNAL(src, COMSIG_MODULE_TRIGGERED, mod.wearer) & MOD_ABORT_USE)
return FALSE
start_cooldown()
addtimer(CALLBACK(mod.wearer, TYPE_PROC_REF(/mob, update_clothing), mod.slot_flags), cooldown_time+1) //need to run it a bit after the cooldown starts to avoid conflicts
mod.wearer.update_clothing(mod.slot_flags)
SEND_SIGNAL(src, COMSIG_MODULE_USED)
return TRUE
/// Called when an activated module without a device is used
/obj/item/mod/module/proc/on_select_use(atom/target)
if(!(allow_flags & MODULE_ALLOW_INCAPACITATED) && mod.wearer.incapacitated(IGNORE_GRAB))
return FALSE
mod.wearer.face_atom(target)
if(!on_use())
return FALSE
return TRUE
/// Called when an activated module without a device is active and the user alt/middle-clicks
/obj/item/mod/module/proc/on_special_click(mob/source, atom/target)
SIGNAL_HANDLER
on_select_use(target)
return COMSIG_MOB_CANCEL_CLICKON
/// Called on the MODsuit's process
/obj/item/mod/module/proc/on_process(seconds_per_tick)
if(active)
if(!drain_power(active_power_cost * seconds_per_tick))
on_deactivation()
return FALSE
on_active_process(seconds_per_tick)
else
drain_power(idle_power_cost * seconds_per_tick)
return TRUE
/// Called on the MODsuit's process if it is an active module
/obj/item/mod/module/proc/on_active_process(seconds_per_tick)
return
/// Called from MODsuit's install() proc, so when the module is installed.
/obj/item/mod/module/proc/on_install()
return
/// Called from MODsuit's uninstall() proc, so when the module is uninstalled.
/obj/item/mod/module/proc/on_uninstall(deleting = FALSE)
return
/// Called when the MODsuit is activated
/obj/item/mod/module/proc/on_suit_activation()
return
/// Called when the MODsuit is deactivated
/obj/item/mod/module/proc/on_suit_deactivation(deleting = FALSE)
return
/// Called when the MODsuit is equipped
/obj/item/mod/module/proc/on_equip()
return
/// Called when the MODsuit is unequipped
/obj/item/mod/module/proc/on_unequip()
return
/// Drains power from the suit charge
/obj/item/mod/module/proc/drain_power(amount)
if(!check_power(amount))
return FALSE
mod.subtract_charge(amount)
mod.update_charge_alert()
return TRUE
/// Checks if there is enough power in the suit
/obj/item/mod/module/proc/check_power(amount)
return mod.check_charge(amount)
/// Adds additional things to the MODsuit ui_data()
/obj/item/mod/module/proc/add_ui_data()
return list()
/// Creates a list of configuring options for this module
/obj/item/mod/module/proc/get_configuration()
return list()
/// Generates an element of the get_configuration list with a display name, type and value
/obj/item/mod/module/proc/add_ui_configuration(display_name, type, value, list/values)
return list("display_name" = display_name, "type" = type, "value" = value, "values" = values)
/// Receives configure edits from the TGUI and edits the vars
/obj/item/mod/module/proc/configure_edit(key, value)
return
/// Called when the device moves to a different place on active modules
/obj/item/mod/module/proc/on_exit(datum/source, atom/movable/part, direction)
SIGNAL_HANDLER
if(!active)
return
if(part.loc == src)
return
if(part.loc == mod.wearer)
return
if(part == device)
on_deactivation(display_message = FALSE)
/// Called when the device gets deleted on active modules
/obj/item/mod/module/proc/on_device_deletion(datum/source)
SIGNAL_HANDLER
if(source == device)
device = null
qdel(src)
/// Adds the worn overlays to the suit.
/obj/item/mod/module/proc/add_module_overlay(obj/item/source, list/overlays, mutable_appearance/standing, isinhands, icon_file)
SIGNAL_HANDLER
overlays += generate_worn_overlay(standing)
/// Generates an icon to be used for the suit's worn overlays
/obj/item/mod/module/proc/generate_worn_overlay(mutable_appearance/standing)
. = list()
if(!mod.active)
return
var/used_overlay
if(overlay_state_use && !COOLDOWN_FINISHED(src, cooldown_timer))
used_overlay = overlay_state_use
else if(overlay_state_active && active)
used_overlay = overlay_state_active
else if(overlay_state_inactive)
used_overlay = overlay_state_inactive
else
return
/* SKYRAT EDIT START - Making MODsuits mutant-compatible - ORIGINAL:
var/mutable_appearance/module_icon = mutable_appearance(overlay_icon_file, used_overlay, layer = standing.layer + 0.1)
if(!use_mod_colors)
module_icon.appearance_flags |= RESET_COLOR
. += module_icon
*/
return handle_module_icon(standing, used_overlay)
// SKYRAT EDIT END
/// Updates the signal used by active modules to be activated
/obj/item/mod/module/proc/update_signal(value)
switch(value)
if(MIDDLE_CLICK)
mod.selected_module.used_signal = COMSIG_MOB_MIDDLECLICKON
if(ALT_CLICK)
mod.selected_module.used_signal = COMSIG_MOB_ALTCLICKON
RegisterSignal(mod.wearer, mod.selected_module.used_signal, TYPE_PROC_REF(/obj/item/mod/module, on_special_click))
/// Pins the module to the user's action buttons
/obj/item/mod/module/proc/pin(mob/user)
if(module_type == MODULE_PASSIVE)
return
var/datum/action/item_action/mod/pinned_module/existing_action = pinned_to[REF(user)]
if(existing_action)
mod.remove_item_action(existing_action)
return
var/datum/action/item_action/mod/pinned_module/new_action = new(mod, src, user)
mod.add_item_action(new_action)
/// On drop key, concels a device item.
/obj/item/mod/module/proc/dropkey(mob/living/user)
SIGNAL_HANDLER
if(user.get_active_held_item() != device)
return
on_deactivation()
return COMSIG_KB_ACTIVATED
///Anomaly Locked - Causes the module to not function without an anomaly.
/obj/item/mod/module/anomaly_locked
name = "MOD anomaly locked module"
desc = "A form of a module, locked behind an anomalous core to function."
incompatible_modules = list(/obj/item/mod/module/anomaly_locked)
/// The core item the module runs off.
var/obj/item/assembly/signaler/anomaly/core
/// Accepted types of anomaly cores.
var/list/accepted_anomalies = list(/obj/item/assembly/signaler/anomaly)
/// If this one starts with a core in.
var/prebuilt = FALSE
/obj/item/mod/module/anomaly_locked/Initialize(mapload)
. = ..()
if(!prebuilt || !length(accepted_anomalies))
return
var/core_path = pick(accepted_anomalies)
core = new core_path(src)
update_icon_state()
/obj/item/mod/module/anomaly_locked/Destroy()
QDEL_NULL(core)
return ..()
/obj/item/mod/module/anomaly_locked/examine(mob/user)
. = ..()
if(!length(accepted_anomalies))
return
if(core)
. += span_notice("There is a [core.name] installed in it. You could remove it with a <b>screwdriver</b>...")
else
var/list/core_list = list()
for(var/path in accepted_anomalies)
var/atom/core_path = path
core_list += initial(core_path.name)
. += span_notice("You need to insert \a [english_list(core_list, and_text = " or ")] for this module to function.")
/obj/item/mod/module/anomaly_locked/on_select()
if(!core)
balloon_alert(mod.wearer, "no core!")
return
return ..()
/obj/item/mod/module/anomaly_locked/on_process(seconds_per_tick)
. = ..()
if(!core)
return FALSE
/obj/item/mod/module/anomaly_locked/on_active_process(seconds_per_tick)
if(!core)
return FALSE
return TRUE
/obj/item/mod/module/anomaly_locked/attackby(obj/item/item, mob/living/user, params)
if(item.type in accepted_anomalies)
if(core)
balloon_alert(user, "core already in!")
return
if(!user.transferItemToLoc(item, src))
return
core = item
balloon_alert(user, "core installed")
playsound(src, 'sound/machines/click.ogg', 30, TRUE)
update_icon_state()
else
return ..()
/obj/item/mod/module/anomaly_locked/screwdriver_act(mob/living/user, obj/item/tool)
. = ..()
if(!core)
balloon_alert(user, "no core!")
return
balloon_alert(user, "removing core...")
if(!do_after(user, 3 SECONDS, target = src))
balloon_alert(user, "interrupted!")
return
balloon_alert(user, "core removed")
core.forceMove(drop_location())
if(Adjacent(user) && !issilicon(user))
user.put_in_hands(core)
core = null
update_icon_state()
/obj/item/mod/module/anomaly_locked/update_icon_state()
icon_state = initial(icon_state) + (core ? "-core" : "")
return ..()