From 047104c89dee67e757124b2c42b972fcf7572c74 Mon Sep 17 00:00:00 2001 From: Ephemeralis Date: Tue, 27 Feb 2024 10:29:03 +1100 Subject: [PATCH] [MODULAR] Oh God The Zipper's Stuck: Entombed quirk granting unremovable roundstart MODsuit (#656) * Initial pass of Entombed quirk * Fix incorrect preference path * Fix improper preferences path * Add entombed damage processing & toggle prefs for forced no-deploy * Sort tgui imports and use the not broken text input * Simplify toxin damage from entombed and remove fused MODsuit indestructible flag * Handle custom stripping behavior on entombed MODsuits with storage modules * Improve messaging on strip override * Replace CMO modsuit w/ corpsman reskin & add infiltrator entombed skin * Apply suggestions from code review Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com> * Stack trace and qdel on null modsuit in add_unique(), code formatting improvements * Extend equipping quirks to record what items they force-drop, and make Entombed transfer old bag contents into its MODsuit storage * Apply suggestions from code review Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com> * Add foley jitter to the life support failure process (makes it clearer what's killing the user) * Handle dropped force-equipped items qdels gracefully Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com> * Ensure customizations are applied to the entombed MODsuit * Force deployed MODsuits give the no dismember trait, increase max chars on name customization to 48 & remove broken colonist skin * Remove entombed MODsuit activation to post_add() to avoid loadout shenanigans --------- Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com> --- .../datums/quirks/neutral_quirks/equipping.dm | 10 + .../modules/entombed_quirk/code/entombed.dm | 273 ++++++++++++++++++ .../entombed_quirk/code/entombed_mod.dm | 95 ++++++ tgstation.dme | 2 + .../character_preferences/nova/entombed.tsx | 38 +++ 5 files changed, 418 insertions(+) create mode 100644 modular_nova/master_files/code/modules/entombed_quirk/code/entombed.dm create mode 100644 modular_nova/master_files/code/modules/entombed_quirk/code/entombed_mod.dm create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/nova/entombed.tsx diff --git a/modular_nova/master_files/code/datums/quirks/neutral_quirks/equipping.dm b/modular_nova/master_files/code/datums/quirks/neutral_quirks/equipping.dm index 4f48ddd4610..cb68d141684 100644 --- a/modular_nova/master_files/code/datums/quirks/neutral_quirks/equipping.dm +++ b/modular_nova/master_files/code/datums/quirks/neutral_quirks/equipping.dm @@ -6,6 +6,8 @@ var/list/items = list() /// the items that will be forcefully equipped, formatted in the way of [item_path = list of slots it can be equipped to], will equip over nodrop items var/list/forced_items = list() + /// did we force drop any items? if so, they're in this list. useful for transferring any applicable contents into new items on roundstart + var/list/force_dropped_items = list() /datum/quirk/equipping/add_unique(client/client_source) var/mob/living/carbon/carbon_holder = quirk_holder @@ -45,7 +47,15 @@ if (check_nodrop && HAS_TRAIT(item_in_slot, TRAIT_NODROP)) return FALSE target.dropItemToGround(item_in_slot, force = TRUE) + force_dropped_items += item_in_slot + RegisterSignal(item_in_slot, COMSIG_QDELETING, PROC_REF(dropped_items_cleanup)) + return target.equip_to_slot_if_possible(item, slot, disable_warning = TRUE) // this should never not work tbh +/datum/quirk/equipping/proc/dropped_items_cleanup(obj/item/source) + SIGNAL_HANDLER + + force_dropped_items -= source + /datum/quirk/equipping/proc/on_equip_item(obj/item/equipped, success) return diff --git a/modular_nova/master_files/code/modules/entombed_quirk/code/entombed.dm b/modular_nova/master_files/code/modules/entombed_quirk/code/entombed.dm new file mode 100644 index 00000000000..cec2af9d52c --- /dev/null +++ b/modular_nova/master_files/code/modules/entombed_quirk/code/entombed.dm @@ -0,0 +1,273 @@ +/// How much damage should we be taking when the suit's been disabled a while? +#define ENTOMBED_TICK_DAMAGE 1.5 + +/datum/quirk/equipping/entombed + name = "Entombed" + desc = "You are permanently fused to (or otherwise reliant on) a single MOD unit that can never be removed from your person. If it runs out of charge or is turned off, you'll start to die!" + gain_text = span_warning("Your exosuit is both prison and home.") + lose_text = span_notice("At last, you're finally free from that horrible exosuit.") + medical_record_text = "Patient is physiologically reliant on a MOD unit for homeostasis. Do not attempt removal." + value = 0 + icon = FA_ICON_ARROW_CIRCLE_DOWN + forced_items = list(/obj/item/mod/control/pre_equipped/entombed = list(ITEM_SLOT_BACK)) + quirk_flags = QUIRK_HUMAN_ONLY | QUIRK_PROCESSES + /// The modsuit we're stuck in + var/obj/item/mod/control/pre_equipped/entombed/modsuit + /// Has the player chosen to deploy-lock? + var/deploy_locked = FALSE + /// How long before they start taking damage when the suit's not active? + var/life_support_failure_threshold = 1.5 MINUTES + /// TimerID for our timeframe tracker + var/life_support_timer + /// Are we taking damage? + var/life_support_failed = FALSE + +/datum/quirk/equipping/entombed/process(seconds_per_tick) + var/mob/living/carbon/human/human_holder = quirk_holder + if (!modsuit || life_support_failed) + // we've got no modsuit or life support. take damage ow + human_holder.adjustToxLoss(ENTOMBED_TICK_DAMAGE * seconds_per_tick, updating_health = TRUE, forced = TRUE) + human_holder.set_jitter_if_lower(10 SECONDS) + + if (!modsuit.active) + if (!life_support_timer) + //start the timer and let the player know + life_support_timer = addtimer(CALLBACK(src, PROC_REF(life_support_failure), human_holder), life_support_failure_threshold, TIMER_STOPPABLE | TIMER_DELETE_ME) + + to_chat(human_holder, span_danger("Your physiology begins to erratically seize and twitch, bereft of your MODsuit's vital support. Turn it back on as soon as you can!")) + human_holder.balloon_alert(human_holder, "suit life support warning!") + human_holder.set_jitter_if_lower(life_support_failure_threshold) //give us some foley jitter + return + else + if (life_support_timer) + // clear our timer and let the player know everything's back to normal + deltimer(life_support_timer) + life_support_timer = null + life_support_failed = FALSE + + to_chat(human_holder, span_notice("Relief floods your frame as your suit begins sustaining your life once more.")) + human_holder.balloon_alert(human_holder, "suit life support restored!") + human_holder.adjust_jitter(-(life_support_failure_threshold / 2)) // clear half of it, wow, that was unpleasant + +/datum/quirk/equipping/entombed/proc/life_support_failure() + // Warn the player and begin the gradual dying process. + var/mob/living/carbon/human/human_holder = quirk_holder + + human_holder.visible_message(span_danger("[human_holder] suddenly staggers, a dire pallor overtaking [human_holder.p_their()] features as a feeble 'breep' emanates from their suit..."), span_userdanger("Terror descends as your suit's life support system breeps feebly, and then goes horrifyingly silent.")) + human_holder.balloon_alert(human_holder, "SUIT LIFE SUPPORT FAILING!") + playsound(human_holder, 'sound/effects/alert.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE) // OH GOD THE STRESS NOISE + life_support_failed = TRUE + +/datum/quirk/equipping/entombed/add_unique(client/client_source) + . = ..() + var/mob/living/carbon/human/human_holder = quirk_holder + if (istype(human_holder.back, /obj/item/mod/control/pre_equipped/entombed)) + modsuit = human_holder.back // link this up to the quirk for easy access + + if (isnull(modsuit)) + stack_trace("Entombed quirk couldn't create a fused MODsuit on [quirk_holder] and was force-removed.") + qdel(src) + return + + var/lock_deploy = client_source?.prefs.read_preference(/datum/preference/toggle/entombed_deploy_lock) + if (!isnull(lock_deploy)) + deploy_locked = lock_deploy + + // set no dismember trait for deploy-locked dudes, i'm sorry, there's basically no better way to do this. + // it's a pretty ample buff but i dunno what else to do... + if (deploy_locked) + ADD_TRAIT(human_holder, TRAIT_NODISMEMBER, QUIRK_TRAIT) + + // set all of our customization stuff from prefs, if we have it + var/modsuit_skin = client_source?.prefs.read_preference(/datum/preference/choiced/entombed_skin) + + if (modsuit_skin == NONE) + modsuit_skin = "civilian" + + modsuit.skin = lowertext(modsuit_skin) + + var/modsuit_name = client_source?.prefs.read_preference(/datum/preference/text/entombed_mod_name) + if (modsuit_name) + modsuit.name = modsuit_name + + var/modsuit_desc = client_source?.prefs.read_preference(/datum/preference/text/entombed_mod_desc) + if (modsuit_desc) + modsuit.desc = modsuit_desc + + var/modsuit_skin_prefix = client_source?.prefs.read_preference(/datum/preference/text/entombed_mod_prefix) + if (modsuit_skin_prefix) + modsuit.theme.name = lowertext(modsuit_skin_prefix) + + // ensure we're applying our config theme changes, just in case + for(var/obj/item/part as anything in modsuit.mod_parts) + part.name = "[modsuit.theme.name] [initial(part.name)]" + part.desc = "[initial(part.desc)] [modsuit.theme.desc]" + + install_racial_features() + + //transfer as many items across from our dropped backslot as we can. do this last incase something breaks + if (force_dropped_items) + var/obj/item/old_bag = locate() in force_dropped_items + if (old_bag.atom_storage) + old_bag.atom_storage.dump_content_at(modsuit, human_holder) + +/datum/quirk/equipping/entombed/post_add() + . = ..() + // quickly deploy it on roundstart. we can't do this in add_unique because that gets called in the preview screen, which overwrites people's loadout stuff in suit/shoes/gloves slot. very unfun for them + modsuit.quick_activation() + +/datum/quirk/equipping/entombed/remove() + var/mob/living/carbon/human/human_holder = quirk_holder + if (deploy_locked && HAS_TRAIT_FROM(human_holder, TRAIT_NODISMEMBER, QUIRK_TRAIT)) + REMOVE_TRAIT(human_holder, TRAIT_NODISMEMBER, QUIRK_TRAIT) + QDEL_NULL(modsuit) + +/datum/quirk/equipping/entombed/proc/install_racial_features() + // deploy specific racial features - ethereals get ethereal cores, plasmamen get free plasma stabilizer module + if (!modsuit) // really don't know how this could ever happen but it's better than runtimes + return + var/mob/living/carbon/human/human_holder = quirk_holder + if (isethereal(human_holder)) + var/obj/item/mod/core/ethereal/eth_core = new + eth_core.install(modsuit) + else if (isplasmaman(human_holder)) + var/obj/item/mod/module/plasma_stabilizer/entombed/plasma_stab = new + modsuit.install(plasma_stab, human_holder) + +/datum/quirk_constant_data/entombed + associated_typepath = /datum/quirk/equipping/entombed + customization_options = list( + /datum/preference/choiced/entombed_skin, + /datum/preference/text/entombed_mod_desc, + /datum/preference/text/entombed_mod_name, + /datum/preference/text/entombed_mod_prefix, + /datum/preference/toggle/entombed_deploy_lock, + ) + +/datum/preference/choiced/entombed_skin + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "entombed_skin" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + +/datum/preference/choiced/entombed_skin/init_possible_values() + return list( + "Standard", + "Civilian", + "Advanced", + "Atmospheric", + "Corpsman", + "Cosmohonk", + "Engineering", + "Infiltrator", + "Interdyne", + "Loader", + "Medical", + "Mining", + "Prototype", + "Security", + ) + +/datum/preference/choiced/entombed_skin/create_default_value() + return "Civilian" + +/datum/preference/choiced/entombed_skin/is_accessible(datum/preferences/preferences) + if (!..()) + return FALSE + + return "Entombed" in preferences.all_quirks + +/datum/preference/choiced/entombed_skin/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/preference/text/entombed_mod_name + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "entombed_mod_name" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + maximum_value_length = 48 + +/datum/preference/text/entombed_mod_name/is_accessible(datum/preferences/preferences) + if (!..()) + return FALSE + + return "Entombed" in preferences.all_quirks + +/datum/preference/text/entombed_mod_name/serialize(input) + return htmlrendertext(input) + +/datum/preference/text/entombed_mod_name/deserialize(input, datum/preferences/preferences) + var/sanitized_input = htmlrendertext(input) + if(!isnull(sanitized_input)) + return sanitized_input + else + return "" + +/datum/preference/text/entombed_mod_name/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/preference/text/entombed_mod_desc + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "entombed_mod_desc" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + +/datum/preference/text/entombed_mod_desc/is_accessible(datum/preferences/preferences) + if (!..()) + return FALSE + + return "Entombed" in preferences.all_quirks + +/datum/preference/text/entombed_mod_desc/serialize(input) + return htmlrendertext(input) + +/datum/preference/text/entombed_mod_desc/deserialize(input, datum/preferences/preferences) + var/sanitized_input = htmlrendertext(input) + if(!isnull(sanitized_input)) + return sanitized_input + else + return "" + +/datum/preference/text/entombed_mod_desc/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/preference/text/entombed_mod_prefix + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "entombed_mod_prefix" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + maximum_value_length = 16 + +/datum/preference/text/entombed_mod_prefix/is_accessible(datum/preferences/preferences) + if (!..()) + return FALSE + + return "Entombed" in preferences.all_quirks + +/datum/preference/text/entombed_mod_prefix/serialize(input) + return htmlrendertext(input) + +/datum/preference/text/entombed_mod_prefix/deserialize(input, datum/preferences/preferences) + return htmlrendertext(input) + +/datum/preference/text/entombed_mod_prefix/create_default_value() + return "Fused" + +/datum/preference/text/entombed_mod_prefix/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/preference/toggle/entombed_deploy_lock + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "entombed_deploy_lock" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/toggle/entombed_deploy_lock/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return "Entombed" in preferences.all_quirks + +/datum/preference/toggle/entombed_deploy_lock/apply_to_human(mob/living/carbon/human/target, value) + return + +#undef ENTOMBED_TICK_DAMAGE diff --git a/modular_nova/master_files/code/modules/entombed_quirk/code/entombed_mod.dm b/modular_nova/master_files/code/modules/entombed_quirk/code/entombed_mod.dm new file mode 100644 index 00000000000..68f489e34d0 --- /dev/null +++ b/modular_nova/master_files/code/modules/entombed_quirk/code/entombed_mod.dm @@ -0,0 +1,95 @@ +/datum/mod_theme/entombed + name = "fused" + desc = "Circumstances have rendered this protective suit into someone's second skin. Literally." + extended_desc = "Some great aspect of someone's past has permanently bound them to this device, for better or worse." + + default_skin = "civilian" + armor_type = /datum/armor/mod_entombed + resistance_flags = FIRE_PROOF | ACID_PROOF // It is better to die for the Emperor than live for yourself. + max_heat_protection_temperature = FIRE_SUIT_MAX_TEMP_PROTECT + siemens_coefficient = 0 + complexity_max = DEFAULT_MAX_COMPLEXITY - 5 + charge_drain = DEFAULT_CHARGE_DRAIN + slowdown_inactive = 2.5 // very slow because the quirk infers you rely on this to move/exist + slowdown_active = 0.95 + inbuilt_modules = list( + /obj/item/mod/module/joint_torsion/entombed, + /obj/item/mod/module/storage, + ) + allowed_suit_storage = list( + /obj/item/tank/internals, + /obj/item/flashlight, + ) + +/datum/armor/mod_entombed + melee = ARMOR_LEVEL_WEAK + bullet = ARMOR_LEVEL_WEAK + laser = ARMOR_LEVEL_WEAK + energy = ARMOR_LEVEL_WEAK + bomb = ARMOR_LEVEL_WEAK + bio = ARMOR_LEVEL_WEAK + fire = ARMOR_LEVEL_WEAK + acid = ARMOR_LEVEL_WEAK + wound = WOUND_ARMOR_WEAK + +/obj/item/mod/module/joint_torsion/entombed + name = "internal joint torsion adaptation" + desc = "Your adaptation to life in this MODsuit shell allows you to ambulate in such a way that your movements recharge the suit's internal batteries slightly, but only while under the effect of gravity." + removable = FALSE + complexity = 0 + power_per_step = DEFAULT_CHARGE_DRAIN * 0.4 + +/obj/item/mod/module/plasma_stabilizer/entombed + name = "colony-stabilized interior seal" + desc = "Your colony has fully integrated the internal segments of your suit's plate into your skeleton, forming a hermetic seal between you and the outside world from which none of your atmosphere can escape. This is enough to allow your head to view the world with your helmet retracted." + complexity = 0 + idle_power_cost = 0 + removable = FALSE + +/obj/item/mod/control/pre_equipped/entombed + theme = /datum/mod_theme/entombed + applied_cell = /obj/item/stock_parts/cell/high + +/obj/item/mod/control/pre_equipped/entombed/canStrip(mob/who) + return TRUE //you can always try, and it'll hit doStrip below + +/obj/item/mod/control/pre_equipped/entombed/doStrip(mob/who) + // attempt to handle custom stripping behavior - if we have a storage module of some kind + var/obj/item/mod/module/storage/inventory = locate() in src.modules + if (!isnull(inventory)) + src.atom_storage.remove_all() + to_chat(who, span_notice("You empty out all the items from the MODsuit's storage module!")) + who.balloon_alert(who, "emptied out MOD storage items!") + return TRUE + + to_chat(who, span_warning("The suit seems permanently fused to their frame - you can't remove it!")) + who.balloon_alert(who, "can't strip a fused MODsuit!") + return ..() + +/obj/item/mod/control/pre_equipped/entombed/retract(mob/user, obj/item/part) + if (ishuman(user)) + var/mob/living/carbon/human/human_user = user + var/datum/quirk/equipping/entombed/tomb_quirk = human_user.get_quirk(/datum/quirk/equipping/entombed) + //check to make sure we're not retracting something we shouldn't be able to + if (tomb_quirk && tomb_quirk.deploy_locked) + if (istype(part, /obj/item/clothing)) // make sure it's a modsuit piece and not a module, we retract those too + if (!istype(part, /obj/item/clothing/head/mod)) // they can only retract the helmet, them's the sticks + human_user.balloon_alert(human_user, "part is fused to you - can't retract!") + playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE) + return + return ..() + +/obj/item/mod/control/pre_equipped/entombed/quick_deploy(mob/user) + if (ishuman(user)) + var/mob/living/carbon/human/human_user = user + var/datum/quirk/equipping/entombed/tomb_quirk = human_user.get_quirk(/datum/quirk/equipping/entombed) + //if we're deploy_locked, just disable this functionality entirely + if (tomb_quirk && tomb_quirk.deploy_locked) + human_user.balloon_alert(human_user, "you can only retract your helmet, and only manually!") + playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE) + return + return ..() + +/obj/item/mod/control/pre_equipped/entombed/Initialize(mapload, new_theme, new_skin, new_core) + . = ..() + ADD_TRAIT(src, TRAIT_NODROP, QUIRK_TRAIT) diff --git a/tgstation.dme b/tgstation.dme index dfaa1e8d5cd..ef6fec4b549 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -6423,6 +6423,8 @@ #include "modular_nova\master_files\code\modules\clothing\under\jobs\security.dm" #include "modular_nova\master_files\code\modules\clothing\under\jobs\civilian\civilian.dm" #include "modular_nova\master_files\code\modules\clothing\under\jobs\civilian\suits.dm" +#include "modular_nova\master_files\code\modules\entombed_quirk\code\entombed.dm" +#include "modular_nova\master_files\code\modules\entombed_quirk\code\entombed_mod.dm" #include "modular_nova\master_files\code\modules\events\_event.dm" #include "modular_nova\master_files\code\modules\experisci\experiment.dm" #include "modular_nova\master_files\code\modules\food_and_drinks\recipes\food_mixtures.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/nova/entombed.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/nova/entombed.tsx new file mode 100644 index 00000000000..48c6c63fab2 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/nova/entombed.tsx @@ -0,0 +1,38 @@ +// THIS IS A NOVA SECTOR UI FILE +import { + CheckboxInput, + Feature, + FeatureChoiced, + FeatureDropdownInput, + FeatureShortTextInput, + FeatureToggle, +} from '../../base'; + +export const entombed_skin: FeatureChoiced = { + name: 'MODsuit Skin', + component: FeatureDropdownInput, +}; + +export const entombed_mod_name: Feature = { + name: 'MODsuit Control Unit Name', + component: FeatureShortTextInput, +}; + +export const entombed_mod_desc: Feature = { + name: 'MODsuit Control Unit Description', + component: FeatureShortTextInput, +}; + +export const entombed_mod_prefix: Feature = { + name: 'MODsuit Deployed Prefix', + description: + "This is appended to any deployed pieces of MODsuit gear, like the chest, helmet, etc. The default is 'fused' - try to use an adjective, if you can.", + component: FeatureShortTextInput, +}; + +export const entombed_deploy_lock: FeatureToggle = { + name: 'MODsuit Stays Deployed (Soft DNR)', + description: + 'Prevents anyone from retracting any of your MODsuit, except your helmet. Even you. WARNING: this may make you extremely difficult to revive, and can be considered a soft DNR. Choose wisely.', + component: CheckboxInput, +};