diff --git a/code/datums/ai/objects/mod.dm b/code/datums/ai/objects/mod.dm
deleted file mode 100644
index ced2ffceb6..0000000000
--- a/code/datums/ai/objects/mod.dm
+++ /dev/null
@@ -1,48 +0,0 @@
-/// An AI controller for the MODsuit pathfinder module. It's activated by implant and attaches itself to the user.
-/datum/ai_controller/mod
- blackboard = list(
- BB_MOD_TARGET,
- BB_MOD_IMPLANT,
- )
- max_target_distance = MOD_AI_RANGE //a little spicy but its one specific item that summons it, and it doesnt run otherwise
- ai_movement = /datum/ai_movement/jps
- ///ID card generated from the suit's required access. Used for pathing.
- var/obj/item/card/id/advanced/id_card
-
-/datum/ai_controller/mod/TryPossessPawn(atom/new_pawn)
- if(!istype(new_pawn, /obj/item/mod/control))
- return AI_CONTROLLER_INCOMPATIBLE
- var/obj/item/mod/control/mod = new_pawn
- id_card = new /obj/item/card/id/advanced/simple_bot()
- if(length(mod.req_access))
- id_card.set_access(mod.req_access)
- return ..() //Run parent at end
-
-/datum/ai_controller/mod/UnpossessPawn(destroy)
- QDEL_NULL(id_card)
- return ..() //Run parent at end
-
-/datum/ai_controller/mod/SelectBehaviors(delta_time)
- current_behaviors = list()
- if(blackboard[BB_MOD_TARGET] && blackboard[BB_MOD_IMPLANT])
- queue_behavior(/datum/ai_behavior/mod_attach)
-
-/datum/ai_controller/mod/get_access()
- return id_card
-
-/datum/ai_behavior/mod_attach
- behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT|AI_BEHAVIOR_MOVE_AND_PERFORM
-
-/datum/ai_behavior/mod_attach/perform(delta_time, datum/ai_controller/controller)
- . = ..()
- if(!controller.pawn.Adjacent(controller.blackboard[BB_MOD_TARGET]))
- return
- var/obj/item/implant/mod/implant = controller.blackboard[BB_MOD_IMPLANT]
- implant.module.attach(controller.blackboard[BB_MOD_TARGET])
- finish_action(controller, TRUE)
-
-/datum/ai_behavior/mod_attach/finish_action(datum/ai_controller/controller, succeeded)
- . = ..()
- controller.blackboard[BB_MOD_TARGET] = null
- var/obj/item/implant/mod/implant = controller.blackboard[BB_MOD_IMPLANT]
- implant.end_recall(succeeded)
diff --git a/code/modules/client/preferences/mod_select.dm b/code/modules/client/preferences/mod_select.dm
new file mode 100644
index 0000000000..c80c8b4c69
--- /dev/null
+++ b/code/modules/client/preferences/mod_select.dm
@@ -0,0 +1,23 @@
+/// Switches between mouse buttons for MODsuit active modules
+/datum/preference/choiced/mod_select
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "mod_select"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/choiced/mod_select/init_possible_values()
+ return list(MIDDLE_CLICK, ALT_CLICK)
+
+/datum/preference/choiced/mod_select/create_default_value()
+ return MIDDLE_CLICK
+
+/datum/preference/choiced/mod_select/apply_to_client_updated(client/client, value)
+ if(!ishuman(client.mob))
+ return
+ var/mob/living/carbon/human/client_owner = client.mob
+ if(!istype(client_owner.back, /obj/item/mod/control))
+ return
+ var/obj/item/mod/control/mod = client_owner.back
+ if(!mod.selected_module)
+ return
+ UnregisterSignal(mod.wearer, mod.selected_module.used_signal)
+ mod.selected_module.update_signal(value)
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index e12a28d01a..daedb7a2b3 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -27,8 +27,8 @@
if(prob(40))
if(prob(25))
audible_message("You hear something rumbling inside [src]'s stomach...", \
- "You hear something rumbling.", 4,\
- "Something is rumbling inside your stomach!")
+ "You hear something rumbling.", 4,\
+ "Something is rumbling inside your stomach!")
var/obj/item/I = user.get_active_held_item()
if(I && I.force)
var/d = rand(round(I.force / 4), I.force)
@@ -624,12 +624,21 @@
if(M.name == XRAY)
sight |= (SEE_TURFS|SEE_MOBS|SEE_OBJS)
see_in_dark = max(see_in_dark, 8)
+
+ if(HAS_TRAIT(src, TRAIT_TRUE_NIGHT_VISION))
+ lighting_alpha = min(lighting_alpha, LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE)
+ see_in_dark = max(see_in_dark, 8)
+
+ if(HAS_TRAIT(src, TRAIT_MESON_VISION))
+ sight |= SEE_TURFS
+ lighting_alpha = min(lighting_alpha, LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE)
+
if(HAS_TRAIT(src, TRAIT_THERMAL_VISION))
- sight |= (SEE_MOBS)
+ sight |= SEE_MOBS
lighting_alpha = min(lighting_alpha, LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE)
if(HAS_TRAIT(src, TRAIT_XRAY_VISION))
- sight |= (SEE_TURFS|SEE_MOBS|SEE_OBJS)
+ sight |= SEE_TURFS|SEE_MOBS|SEE_OBJS
see_in_dark = max(see_in_dark, 8)
if(see_override)
diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm
index 4c30a1d742..2f3c1357da 100644
--- a/code/modules/mob/living/carbon/human/inventory.dm
+++ b/code/modules/mob/living/carbon/human/inventory.dm
@@ -304,7 +304,8 @@
if(equip_to_slot_if_possible(thing, ITEM_SLOT_BACK))
update_inv_hands()
return
- if(!SEND_SIGNAL(equipped_back, COMSIG_CONTAINS_STORAGE)) // not a storage item
+ var/datum/component/storage/storage = equipped_back.GetComponent(/datum/component/storage)
+ if(!storage)
if(!thing)
equipped_back.attack_hand(src)
else
@@ -314,10 +315,11 @@
if(!SEND_SIGNAL(equipped_back, COMSIG_TRY_STORAGE_INSERT, thing, src))
to_chat(src, "You can't fit anything in!")
return
- if(!equipped_back.contents.len) // nothing to take out
- to_chat(src, "There's nothing in your backpack to take out!")
+ var/atom/real_location = storage.real_location()
+ if(!real_location.contents.len) // nothing to take out
+ to_chat(src, "There's nothing in your [equipped_back.name] to take out!")
return
- var/obj/item/stored = equipped_back.contents[equipped_back.contents.len]
+ var/obj/item/stored = real_location.contents[real_location.contents.len]
if(!stored || stored.on_found(src))
return
stored.attack_hand(src) // take out thing from backpack
diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm
index afceabf2a6..68a9f8bd0f 100644
--- a/code/modules/mob/living/carbon/human/species.dm
+++ b/code/modules/mob/living/carbon/human/species.dm
@@ -1303,7 +1303,7 @@ GLOBAL_LIST_EMPTY(roundstart_race_names)
if(ITEM_SLOT_FEET)
if(H.shoes)
return FALSE
- if( !(I.slot_flags & ITEM_SLOT_FEET) )
+ if((DIGITIGRADE in species_traits) && !(I.item_flags & IGNORE_DIGITIGRADE))
return FALSE
if(num_legs < 2)
return FALSE
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index be6508dd96..e6aac9cde9 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -47,6 +47,8 @@
return ..()
/mob/living/proc/ZImpactDamage(turf/T, levels)
+ if(SEND_SIGNAL(src, COMSIG_LIVING_Z_IMPACT, levels, T) & NO_Z_IMPACT_DAMAGE)
+ return
visible_message("[src] crashes into [T] with a sickening noise!", \
"You crash into [T] with a sickening noise!")
adjustBruteLoss((levels * 5) ** 1.5)
@@ -753,7 +755,8 @@
return pick("trails_1", "trails_2")
/mob/living/experience_pressure_difference(pressure_difference, direction, pressure_resistance_prob_delta = 0)
- if(buckled)
+ playsound(src, 'sound/effects/space_wind.ogg', 50, TRUE)
+ if(buckled || mob_negates_gravity())
return
if(client && client.move_delay >= world.time + world.tick_lag*2)
pressure_resistance_prob_delta -= 30
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index deaf50973d..3cda68c015 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -38,8 +38,8 @@
var/can_be_carded = TRUE
var/alarms = list("Motion"=list(), "Fire"=list(), "Atmosphere"=list(), "Power"=list(), "Camera"=list(), "Burglar"=list())
var/viewalerts = 0
- var/icon/holo_icon//Female is assigned when AI is created.
- var/obj/vehicle/sealed/mecha/controlled_mech //For controlled_mech a mech, to determine whether to relaymove or use the AI eye.
+ var/icon/holo_icon //Female is assigned when AI is created.
+ var/obj/controlled_equipment //A piece of equipment, to determine whether to relaymove or use the AI eye.
var/radio_enabled = TRUE //Determins if a carded AI can speak with its built in radio or not.
radiomod = ";" //AIs will, by default, state their laws on the internal radio.
var/obj/item/pda/ai/aiPDA
@@ -173,10 +173,20 @@
GLOB.ai_list -= src
GLOB.shuttle_caller_list -= src
SSshuttle.autoEvac()
- qdel(eyeobj) // No AI, no Eye
+ QDEL_NULL(eyeobj) // No AI, no Eye
+ QDEL_NULL(spark_system)
+ QDEL_NULL(malf_picker)
+ QDEL_NULL(doomsday_device)
+ QDEL_NULL(robot_control)
+ QDEL_NULL(aiMulti)
+ QDEL_NULL(alert_control)
malfhack = null
-
- . = ..()
+ current = null
+ Bot = null
+ controlled_equipment = null
+ linked_core = null
+ apc_override = null
+ return ..()
/mob/living/silicon/ai/IgniteMob()
fire_stacks = 0
@@ -410,7 +420,7 @@
if (href_list["ai_take_control"]) //Mech domination
var/obj/vehicle/sealed/mecha/M = locate(href_list["ai_take_control"])
- if(controlled_mech)
+ if(controlled_equipment)
to_chat(src, "You are already loaded into an onboard computer!")
return
if(!GLOB.cameranet.checkCameraVis(M))
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 507a94b613..baa4d87075 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -722,7 +722,7 @@ GLOBAL_VAR_INIT(exploit_warn_spam_prevention, 0)
return pick(protection_sources)
else
return src
- if((magic && HAS_TRAIT(src, TRAIT_ANTIMAGIC)) || (holy && HAS_TRAIT(src, TRAIT_HOLY)))
+ if((magic && HAS_TRAIT(src, TRAIT_ANTIMAGIC)) || (!self && magic && HAS_TRAIT(src, TRAIT_ANTIMAGIC_NO_SELFBLOCK)) || (holy && HAS_TRAIT(src, TRAIT_HOLY)))
return src
//You can buckle on mobs if you're next to them since most are dense
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index 90a7aae95e..cdad94cf3f 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -585,7 +585,7 @@ It's fairly easy to fix if dealing with single letters but not so much with comp
//Can the mob see reagents inside of containers?
/mob/proc/can_see_reagents()
- return stat == DEAD || silicon_privileges //Dead guys and silicons can always see reagents
+ return stat == DEAD || silicon_privileges || HAS_TRAIT(src, TRAIT_REAGENT_SCANNER) //Dead guys and silicons can always see reagents
/mob/proc/is_blind()
SHOULD_BE_PURE(TRUE)
diff --git a/code/modules/mod/mod_actions.dm b/code/modules/mod/mod_actions.dm
new file mode 100644
index 0000000000..2c27879e79
--- /dev/null
+++ b/code/modules/mod/mod_actions.dm
@@ -0,0 +1,71 @@
+/datum/action/item_action/mod
+ background_icon_state = "bg_tech_blue"
+ icon_icon = 'icons/mob/actions/actions_mod.dmi'
+ check_flags = AB_CHECK_CONSCIOUS
+ var/obj/item/mod/control/mod
+
+/datum/action/item_action/mod/New(Target)
+ ..()
+ mod = Target
+
+/datum/action/item_action/mod/Grant(mob/M)
+ if(owner)
+ Share(M)
+ return
+ ..()
+
+/datum/action/item_action/mod/Remove(mob/M)
+ var/mob_to_grant
+ for(var/datum/weakref/reference as anything in sharers)
+ var/mob/freeloader = reference.resolve()
+ if(!freeloader)
+ continue
+ mob_to_grant = freeloader
+ break
+ ..()
+ if(mob_to_grant)
+ Grant(mob_to_grant)
+
+/datum/action/item_action/mod/deploy
+ name = "Deploy MODsuit"
+ desc = "Deploy/Conceal a part of the MODsuit."
+ button_icon_state = "deploy"
+
+/datum/action/item_action/mod/deploy/Trigger()
+ if(!IsAvailable())
+ return FALSE
+ mod.choose_deploy(usr)
+ return TRUE
+
+/datum/action/item_action/mod/activate
+ name = "Activate MODsuit"
+ desc = "Activate/Deactivate the MODsuit."
+ button_icon_state = "activate"
+
+/datum/action/item_action/mod/activate/Trigger()
+ if(!IsAvailable())
+ return FALSE
+ mod.toggle_activate(usr)
+ return TRUE
+
+/datum/action/item_action/mod/module
+ name = "Toggle Module"
+ desc = "Toggle a MODsuit module."
+ button_icon_state = "module"
+
+/datum/action/item_action/mod/module/Trigger()
+ if(!IsAvailable())
+ return FALSE
+ mod.quick_module(usr)
+ return TRUE
+
+/datum/action/item_action/mod/panel
+ name = "MODsuit Panel"
+ desc = "Open the MODsuit's panel."
+ button_icon_state = "panel"
+
+/datum/action/item_action/mod/panel/Trigger()
+ if(!IsAvailable())
+ return FALSE
+ mod.ui_interact(usr)
+ return TRUE
diff --git a/code/modules/mod/mod_activation.dm b/code/modules/mod/mod_activation.dm
new file mode 100644
index 0000000000..f7869dc227
--- /dev/null
+++ b/code/modules/mod/mod_activation.dm
@@ -0,0 +1,217 @@
+#define MOD_ACTIVATION_STEP_TIME 2 SECONDS
+#define MOD_ACTIVATION_STEP_FLAGS IGNORE_USER_LOC_CHANGE|IGNORE_TARGET_LOC_CHANGE|IGNORE_HELD_ITEM|IGNORE_INCAPACITATED
+
+/// Creates a radial menu from which the user chooses parts of the suit to deploy/retract. Repeats until all parts are extended or retracted.
+/obj/item/mod/control/proc/choose_deploy(mob/user)
+ if(!length(mod_parts))
+ return
+ var/list/display_names = list()
+ var/list/items = list()
+ for(var/obj/item/piece as anything in mod_parts)
+ display_names[piece.name] = REF(piece)
+ var/image/piece_image = image(icon = piece.icon, icon_state = piece.icon_state)
+ items += list(piece.name = piece_image)
+ var/pick = show_radial_menu(user, src, items, custom_check = FALSE, require_near = TRUE, tooltips = TRUE)
+ if(!pick)
+ return
+ var/part_reference = display_names[pick]
+ var/obj/item/part = locate(part_reference) in mod_parts
+ if(!istype(part) || user.incapacitated())
+ return
+ if(active || activating)
+ balloon_alert(user, "deactivate the suit first!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return
+ var/parts_to_check = mod_parts - part
+ if(part.loc == src)
+ deploy(user, part)
+ for(var/obj/item/piece as anything in parts_to_check)
+ if(piece.loc != src)
+ continue
+ choose_deploy(user)
+ break
+ else
+ conceal(user, part)
+ for(var/obj/item/piece as anything in parts_to_check)
+ if(piece.loc == src)
+ continue
+ choose_deploy(user)
+ break
+
+/// Deploys a part of the suit onto the user.
+/obj/item/mod/control/proc/deploy(mob/user, part)
+ var/obj/item/piece = part
+ if(piece == gauntlets && wearer.gloves)
+ gauntlets.overslot = wearer.gloves
+ wearer.transferItemToLoc(gauntlets.overslot, gauntlets, force = TRUE)
+ if(piece == boots && wearer.shoes)
+ boots.overslot = wearer.shoes
+ wearer.transferItemToLoc(boots.overslot, boots, force = TRUE)
+ if(wearer.equip_to_slot_if_possible(piece, piece.slot_flags, qdel_on_fail = FALSE, disable_warning = TRUE))
+ ADD_TRAIT(piece, TRAIT_NODROP, MOD_TRAIT)
+ if(!user)
+ return TRUE
+ wearer.visible_message(span_notice("[wearer]'s [piece] deploy[piece.p_s()] with a mechanical hiss."),
+ span_notice("[piece] deploy[piece.p_s()] with a mechanical hiss."),
+ span_hear("You hear a mechanical hiss."))
+ playsound(src, 'sound/mecha/mechmove03.ogg', 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ return TRUE
+ else if(piece.loc != src)
+ if(!user)
+ return FALSE
+ balloon_alert(user, "[piece] already deployed!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ else
+ if(!user)
+ return FALSE
+ balloon_alert(user, "bodypart clothed!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+
+/// Retract a part of the suit from the user
+/obj/item/mod/control/proc/conceal(mob/user, part)
+ var/obj/item/piece = part
+ REMOVE_TRAIT(piece, TRAIT_NODROP, MOD_TRAIT)
+ wearer.transferItemToLoc(piece, src, force = TRUE)
+ if(piece == gauntlets)
+ gauntlets.show_overslot()
+ if(piece == boots)
+ boots.show_overslot()
+ if(!user)
+ return
+ wearer.visible_message(span_notice("[wearer]'s [piece] retract[piece.p_s()] back into [src] with a mechanical hiss."),
+ span_notice("[piece] retract[piece.p_s()] back into [src] with a mechanical hiss."),
+ span_hear("You hear a mechanical hiss."))
+ playsound(src, 'sound/mecha/mechmove03.ogg', 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+
+/// Starts the activation sequence, where parts of the suit activate one by one until the whole suit is on
+/obj/item/mod/control/proc/toggle_activate(mob/user, force_deactivate = FALSE)
+ if(!wearer)
+ if(!force_deactivate)
+ balloon_alert(user, "put suit on back!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+ if(!force_deactivate && (SEND_SIGNAL(src, COMSIG_MOD_ACTIVATE, user) & MOD_CANCEL_ACTIVATE))
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+ for(var/obj/item/part as anything in mod_parts)
+ if(!force_deactivate && part.loc == src)
+ balloon_alert(user, "deploy all parts first!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+ if(locked && !active && !allowed(user) && !force_deactivate)
+ balloon_alert(user, "access insufficient!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+ if(!cell?.charge && !force_deactivate)
+ balloon_alert(user, "suit not powered!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+ if(open && !force_deactivate)
+ balloon_alert(user, "close the suit panel!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+ if(activating)
+ if(!force_deactivate)
+ balloon_alert(user, "suit already [active ? "shutting down" : "starting up"]!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+ for(var/obj/item/mod/module/module as anything in modules)
+ if(!module.active)
+ continue
+ module.on_deactivation()
+ activating = TRUE
+ to_chat(wearer, span_notice("MODsuit [active ? "shutting down" : "starting up"]."))
+ if(do_after(wearer, MOD_ACTIVATION_STEP_TIME, wearer, MOD_ACTIVATION_STEP_FLAGS))
+ to_chat(wearer, span_notice("[boots] [active ? "relax their grip on your legs" : "seal around your feet"]."))
+ playsound(src, 'sound/mecha/mechmove03.ogg', 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ seal_part(boots, seal = !active)
+ if(do_after(wearer, MOD_ACTIVATION_STEP_TIME, wearer, MOD_ACTIVATION_STEP_FLAGS))
+ to_chat(wearer, span_notice("[gauntlets] [active ? "become loose around your fingers" : "tighten around your fingers and wrists"]."))
+ playsound(src, 'sound/mecha/mechmove03.ogg', 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ seal_part(gauntlets, seal = !active)
+ if(do_after(wearer, MOD_ACTIVATION_STEP_TIME, wearer, MOD_ACTIVATION_STEP_FLAGS))
+ to_chat(wearer, span_notice("[chestplate] [active ? "releases your chest" : "cinches tightly against your chest"]."))
+ playsound(src, 'sound/mecha/mechmove03.ogg', 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ seal_part(chestplate,seal = !active)
+ if(do_after(wearer, MOD_ACTIVATION_STEP_TIME, wearer, MOD_ACTIVATION_STEP_FLAGS))
+ to_chat(wearer, span_notice("[helmet] hisses [active ? "open" : "closed"]."))
+ playsound(src, 'sound/mecha/mechmove03.ogg', 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ seal_part(helmet, seal = !active)
+ if(do_after(wearer, MOD_ACTIVATION_STEP_TIME, wearer, MOD_ACTIVATION_STEP_FLAGS))
+ to_chat(wearer, span_notice("Systems [active ? "shut down. Parts unsealed. Goodbye" : "started up. Parts sealed. Welcome"], [wearer]."))
+ if(ai)
+ to_chat(ai, span_notice("SYSTEMS [active ? "DEACTIVATED. GOODBYE" : "ACTIVATED. WELCOME"]: \"[ai]\""))
+ finish_activation(on = !active)
+ if(active)
+ playsound(src, 'sound/machines/synth_yes.ogg', 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE, frequency = 6000)
+ SEND_SOUND(wearer, sound('sound/mecha/nominal.ogg',volume=50))
+ else
+ playsound(src, 'sound/machines/synth_no.ogg', 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE, frequency = 6000)
+ activating = FALSE
+ return TRUE
+
+///Seals or unseals the given part
+/obj/item/mod/control/proc/seal_part(obj/item/clothing/part, seal)
+ if(seal)
+ part.clothing_flags |= part.visor_flags
+ part.flags_inv |= part.visor_flags_inv
+ part.flags_cover |= part.visor_flags_cover
+ part.heat_protection = initial(part.heat_protection)
+ part.cold_protection = initial(part.cold_protection)
+ else
+ part.flags_cover &= ~part.visor_flags_cover
+ part.flags_inv &= ~part.visor_flags_inv
+ part.clothing_flags &= ~part.visor_flags
+ part.heat_protection = NONE
+ part.cold_protection = NONE
+ if(part == boots)
+ boots.icon_state = "[skin]-boots[seal ? "-sealed" : ""]"
+ wearer.update_inv_shoes()
+ if(part == gauntlets)
+ gauntlets.icon_state = "[skin]-gauntlets[seal ? "-sealed" : ""]"
+ wearer.update_inv_gloves()
+ if(part == chestplate)
+ chestplate.icon_state = "[skin]-chestplate[seal ? "-sealed" : ""]"
+ wearer.update_inv_wear_suit()
+ wearer.update_inv_w_uniform()
+ if(part == helmet)
+ helmet.icon_state = "[skin]-helmet[seal ? "-sealed" : ""]"
+ if(seal)
+ helmet.alternate_worn_layer = null
+ else
+ helmet.alternate_worn_layer = helmet.alternate_layer
+ wearer.update_inv_head()
+ wearer.update_inv_wear_mask()
+ wearer.update_hair()
+
+/// Finishes the suit's activation, starts processing
+/obj/item/mod/control/proc/finish_activation(on)
+ icon_state = "[skin]-control[on ? "-sealed" : ""]"
+ slowdown = on ? slowdown_active : slowdown_inactive
+ if(on)
+ for(var/obj/item/mod/module/module as anything in modules)
+ module.on_suit_activation()
+ START_PROCESSING(SSobj, src)
+ else
+ for(var/obj/item/mod/module/module as anything in modules)
+ module.on_suit_deactivation()
+ STOP_PROCESSING(SSobj, src)
+ wearer.update_equipment_speed_mods()
+ active = on
+ wearer.update_inv_back()
+
+/// Quickly deploys all the suit parts and if successful, seals them and turns on the suit. Intended mostly for outfits.
+/obj/item/mod/control/proc/quick_activation()
+ var/seal = TRUE
+ for(var/obj/item/part in mod_parts)
+ if(!deploy(null, part))
+ seal = FALSE
+ if(!seal)
+ return
+ for(var/obj/item/part in mod_parts)
+ seal_part(part, seal = TRUE)
+ finish_activation(on = TRUE)
+
+#undef MOD_ACTIVATION_STEP_TIME
+#undef MOD_ACTIVATION_STEP_FLAGS
diff --git a/code/modules/mod/mod_ai.dm b/code/modules/mod/mod_ai.dm
new file mode 100644
index 0000000000..f470fb8046
--- /dev/null
+++ b/code/modules/mod/mod_ai.dm
@@ -0,0 +1,88 @@
+/obj/item/mod/control/transfer_ai(interaction, mob/user, mob/living/silicon/ai/intAI, obj/item/aicard/card)
+ . = ..()
+ if(!.)
+ return
+ if(!open) //mod must be open
+ balloon_alert(user, "suit must be open to transfer!")
+ return
+ switch(interaction)
+ if(AI_TRANS_TO_CARD)
+ if(!ai)
+ balloon_alert(user, "no AI in suit!")
+ return
+ balloon_alert(user, "transferring to card...")
+ if(!do_after(user, 5 SECONDS, target = src))
+ balloon_alert(user, "interrupted!")
+ return
+ intAI = ai
+ intAI.ai_restore_power()//So the AI initially has power.
+ intAI.control_disabled = TRUE
+ intAI.radio_enabled = FALSE
+ intAI.disconnect_shell()
+ intAI.forceMove(card)
+ card.AI = intAI
+ for(var/datum/action/action as anything in actions)
+ if(action.owner == intAI)
+ action.Remove(intAI)
+ else
+ action.Unshare(intAI)
+ intAI.controlled_equipment = null
+ intAI.remote_control = null
+ balloon_alert(intAI, "transferred to a card")
+ balloon_alert(user, "AI transferred to card")
+ ai = null
+
+ if(AI_TRANS_FROM_CARD) //Using an AI card to upload to the suit.
+ intAI = card.AI
+ if(!intAI)
+ balloon_alert(user, "no AI in card!")
+ return
+ if(intAI.deployed_shell) //Recall AI if shelled so it can be checked for a client
+ intAI.disconnect_shell()
+ if(intAI.stat || !intAI.client)
+ balloon_alert(user, "AI unresponsive!")
+ return
+ balloon_alert(user, "transferring to suit...")
+ if(!do_after(user, 5 SECONDS, target = src))
+ balloon_alert(user, "interrupted!")
+ return
+ balloon_alert(user, "AI transferred to suit")
+ ai_enter_mod(intAI)
+ card.AI = null
+
+/obj/item/mod/control/proc/ai_enter_mod(mob/living/silicon/ai/new_ai)
+ new_ai.control_disabled = FALSE
+ new_ai.radio_enabled = TRUE
+ new_ai.ai_restore_power()
+ new_ai.cancel_camera()
+ new_ai.controlled_equipment = src
+ new_ai.remote_control = src
+ new_ai.forceMove(src)
+ ai = new_ai
+ balloon_alert(new_ai, "transferred to a suit")
+ for(var/datum/action/action as anything in actions)
+ action.Grant(new_ai)
+
+#define MOVE_DELAY 2
+#define WEARER_DELAY 1
+#define LONE_DELAY 5
+#define CELL_PER_STEP DEFAULT_CELL_DRAIN * 2.5
+
+/obj/item/mod/control/relaymove(mob/user, direction)
+ if((!active && wearer) || !cell || cell.charge < CELL_PER_STEP || user != ai || !COOLDOWN_FINISHED(src, cooldown_mod_move) || (wearer?.pulledby?.grab_state > GRAB_PASSIVE))
+ return FALSE
+ var/timemodifier = MOVE_DELAY * (ISDIAGONALDIR(direction) ? SQRT_2 : 1) * (wearer ? WEARER_DELAY : LONE_DELAY)
+ COOLDOWN_START(src, cooldown_mod_move, movedelay * timemodifier + slowdown)
+ playsound(src, 'sound/mecha/mechmove01.ogg', 25, TRUE)
+ cell.charge = max(0, cell.charge - CELL_PER_STEP)
+ if(ismovable(wearer?.loc))
+ return wearer.loc.relaymove(wearer, direction)
+ if(wearer && !wearer.Process_Spacemove(direction))
+ return FALSE
+ var/atom/movable/mover = wearer || src
+ return step(mover, direction)
+
+#undef MOVE_DELAY
+#undef WEARER_DELAY
+#undef LONE_DELAY
+#undef CELL_PER_STEP
diff --git a/code/modules/mod/mod_clothes.dm b/code/modules/mod/mod_clothes.dm
new file mode 100644
index 0000000000..37045015d6
--- /dev/null
+++ b/code/modules/mod/mod_clothes.dm
@@ -0,0 +1,123 @@
+/obj/item/clothing/head/helmet/space/mod
+ name = "MOD helmet"
+ desc = "A helmet for a MODsuit."
+ icon = 'icons/obj/mod.dmi'
+ icon_state = "helmet"
+ worn_icon = 'icons/mob/mod.dmi'
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 100, FIRE = 25, ACID = 25, WOUND = 10)
+ body_parts_covered = HEAD
+ heat_protection = HEAD
+ cold_protection = HEAD
+ max_heat_protection_temperature = SPACE_SUIT_MAX_TEMP_PROTECT
+ min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
+ clothing_flags = THICKMATERIAL
+ resistance_flags = NONE
+ flash_protect = FLASH_PROTECTION_NONE
+ clothing_flags = SNUG_FIT
+ flags_inv = HIDEFACIALHAIR
+ flags_cover = NONE
+ visor_flags = THICKMATERIAL|STOPSPRESSUREDAMAGE
+ visor_flags_inv = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT
+ visor_flags_cover = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF
+ var/alternate_layer = NECK_LAYER
+ var/obj/item/mod/control/mod
+
+/obj/item/clothing/head/helmet/space/mod/Destroy()
+ if(!QDELETED(mod))
+ mod.helmet = null
+ mod.mod_parts -= src
+ QDEL_NULL(mod)
+ return ..()
+
+/obj/item/clothing/suit/armor/mod
+ name = "MOD chestplate"
+ desc = "A chestplate for a MODsuit."
+ icon = 'icons/obj/mod.dmi'
+ icon_state = "chestplate"
+ worn_icon = 'icons/mob/mod.dmi'
+ blood_overlay_type = "armor"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 100, FIRE = 25, ACID = 25, WOUND = 10)
+ body_parts_covered = CHEST|GROIN
+ heat_protection = CHEST|GROIN
+ cold_protection = CHEST|GROIN
+ max_heat_protection_temperature = SPACE_SUIT_MAX_TEMP_PROTECT
+ min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
+ clothing_flags = THICKMATERIAL
+ visor_flags = STOPSPRESSUREDAMAGE
+ visor_flags_inv = HIDEJUMPSUIT
+ allowed = list(/obj/item/flashlight, /obj/item/tank/internals)
+ resistance_flags = NONE
+ var/obj/item/mod/control/mod
+
+/obj/item/clothing/suit/armor/mod/Destroy()
+ if(!QDELETED(mod))
+ mod.chestplate = null
+ mod.mod_parts -= src
+ QDEL_NULL(mod)
+ return ..()
+
+/obj/item/clothing/gloves/mod
+ name = "MOD gauntlets"
+ desc = "A pair of gauntlets for a MODsuit."
+ icon = 'icons/obj/mod.dmi'
+ icon_state = "gauntlets"
+ worn_icon = 'icons/mob/mod.dmi'
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 100, FIRE = 25, ACID = 25, WOUND = 10)
+ body_parts_covered = HANDS|ARMS
+ heat_protection = HANDS|ARMS
+ cold_protection = HANDS|ARMS
+ max_heat_protection_temperature = SPACE_SUIT_MAX_TEMP_PROTECT
+ min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
+ clothing_flags = THICKMATERIAL
+ resistance_flags = NONE
+ var/obj/item/mod/control/mod
+ var/obj/item/clothing/overslot
+
+/obj/item/clothing/gloves/mod/Destroy()
+ if(!QDELETED(mod))
+ mod.gauntlets = null
+ mod.mod_parts -= src
+ QDEL_NULL(mod)
+ return ..()
+
+/// Replaces these gloves on the wearer with the overslot ones
+
+/obj/item/clothing/gloves/mod/proc/show_overslot()
+ if(!overslot)
+ return
+ if(!mod.wearer.equip_to_slot_if_possible(overslot, overslot.slot_flags, qdel_on_fail = FALSE, disable_warning = TRUE))
+ mod.wearer.dropItemToGround(overslot, force = TRUE, silent = TRUE)
+ overslot = null
+
+/obj/item/clothing/shoes/mod
+ name = "MOD boots"
+ desc = "A pair of boots for a MODsuit."
+ icon = 'icons/obj/mod.dmi'
+ icon_state = "boots"
+ worn_icon = 'icons/mob/mod.dmi'
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 100, FIRE = 25, ACID = 25, WOUND = 10)
+ body_parts_covered = FEET|LEGS
+ heat_protection = FEET|LEGS
+ cold_protection = FEET|LEGS
+ max_heat_protection_temperature = SPACE_SUIT_MAX_TEMP_PROTECT
+ min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
+ clothing_flags = THICKMATERIAL
+ resistance_flags = NONE
+ item_flags = IGNORE_DIGITIGRADE
+ var/obj/item/mod/control/mod
+ var/obj/item/clothing/overslot
+
+/obj/item/clothing/shoes/mod/Destroy()
+ if(!QDELETED(mod))
+ mod.boots = null
+ mod.mod_parts -= src
+ QDEL_NULL(mod)
+ return ..()
+
+/// Replaces these shoes on the wearer with the overslot ones
+/obj/item/clothing/shoes/mod/proc/show_overslot()
+ if(!overslot)
+ return
+ if(!mod.wearer.equip_to_slot_if_possible(overslot, overslot.slot_flags, qdel_on_fail = FALSE, disable_warning = TRUE))
+ mod.wearer.dropItemToGround(overslot, force = TRUE, silent = TRUE)
+ overslot = null
diff --git a/code/modules/mod/mod_construction.dm b/code/modules/mod/mod_construction.dm
new file mode 100644
index 0000000000..4916f8b6bc
--- /dev/null
+++ b/code/modules/mod/mod_construction.dm
@@ -0,0 +1,278 @@
+/obj/item/mod/construction
+ desc = "A part used in MOD construction."
+ inhand_icon_state = "rack_parts"
+
+/obj/item/mod/construction/helmet
+ name = "MOD helmet"
+ icon_state = "helmet"
+
+/obj/item/mod/construction/chestplate
+ name = "MOD chestplate"
+ icon_state = "chestplate"
+
+/obj/item/mod/construction/gauntlets
+ name = "MOD gauntlets"
+ icon_state = "gauntlets"
+
+/obj/item/mod/construction/boots
+ name = "MOD boots"
+ icon_state = "boots"
+
+/obj/item/mod/construction/core
+ name = "MOD core"
+ icon_state = "mod-core"
+ desc = "Growing in the most lush, fertile areas of the planet Sprout, there is a crystal known as the Heartbloom. \
+ These rare, organic piezoelectric crystals are of incredible cultural significance to the artist castes of the Ethereals, \
+ owing to their appearance; which is exactly similar to that of an Ethereal's heart. \n\
+ Which one you have in your suit is unclear, but either way, \
+ it's been repurposed to be an internal power source for a Modular Outerwear Device."
+
+/obj/item/mod/construction/broken_core
+ name = "broken MOD core"
+ icon_state = "mod-core-broken"
+ desc = "An internal power source for a Modular Outerwear Device. You don't seem to be able to source any power from this one, though."
+
+/obj/item/mod/construction/broken_core/examine(mob/user)
+ . = ..()
+ . += span_notice("You could repair it with a screwdriver...")
+
+/obj/item/mod/construction/broken_core/screwdriver_act(mob/living/user, obj/item/tool)
+ . = ..()
+ if(!tool.use_tool(src, user, 5 SECONDS, volume = 30))
+ return
+ new /obj/item/mod/construction/core(drop_location())
+ qdel(src)
+
+/obj/item/mod/construction/armor
+ name = "MOD armor plates"
+ desc = "Armor plates used to finish a MOD."
+ icon_state = "standard-armor"
+ var/datum/mod_theme/theme = /datum/mod_theme
+
+/obj/item/mod/construction/armor/Initialize(mapload)
+ . = ..()
+ var/datum/mod_theme/used_theme = GLOB.mod_themes[theme]
+ name = "MOD [used_theme.name] armor plates"
+ desc = "[desc] [used_theme.desc]"
+ icon_state = "[used_theme.default_skin]-armor"
+
+/obj/item/mod/construction/armor/engineering
+ theme = /datum/mod_theme/engineering
+
+/obj/item/mod/construction/armor/atmospheric
+ theme = /datum/mod_theme/atmospheric
+
+/obj/item/mod/construction/armor/mining
+ theme = /datum/mod_theme/mining
+
+/obj/item/mod/construction/armor/medical
+ theme = /datum/mod_theme/medical
+
+/obj/item/mod/construction/armor/security
+ theme = /datum/mod_theme/security
+
+/obj/item/mod/construction/armor/cosmohonk
+ theme = /datum/mod_theme/cosmohonk
+
+/obj/item/mod/paint
+ name = "MOD paint kit"
+ desc = "This kit will repaint your MODsuit to something unique."
+ icon = 'icons/obj/mod.dmi'
+ icon_state = "paintkit"
+
+#define START_STEP "start"
+#define CORE_STEP "core"
+#define SCREWED_CORE_STEP "screwed_core"
+#define HELMET_STEP "helmet"
+#define CHESTPLATE_STEP "chestplate"
+#define GAUNTLETS_STEP "gauntlets"
+#define BOOTS_STEP "boots"
+#define WRENCHED_ASSEMBLY_STEP "wrenched_assembly"
+#define SCREWED_ASSEMBLY_STEP "screwed_assembly"
+
+/obj/item/mod/construction/shell
+ name = "MOD shell"
+ icon_state = "mod-construction_start"
+ desc = "A MOD shell."
+ var/obj/item/core
+ var/obj/item/helmet
+ var/obj/item/chestplate
+ var/obj/item/gauntlets
+ var/obj/item/boots
+ var/step = START_STEP
+
+/obj/item/mod/construction/shell/examine(mob/user)
+ . = ..()
+ var/display_text
+ switch(step)
+ if(START_STEP)
+ display_text = "It looks like it's missing a MOD core..."
+ if(CORE_STEP)
+ display_text = "The core seems loose..."
+ if(SCREWED_CORE_STEP)
+ display_text = "It looks like it's missing a helmet..."
+ if(HELMET_STEP)
+ display_text = "It looks like it's missing a chestplate..."
+ if(CHESTPLATE_STEP)
+ display_text = "It looks like it's missing gauntlets..."
+ if(GAUNTLETS_STEP)
+ display_text = "It looks like it's missing boots..."
+ if(BOOTS_STEP)
+ display_text = "The assembly seems unsecured..."
+ if(WRENCHED_ASSEMBLY_STEP)
+ display_text = "The assembly seems loose..."
+ if(SCREWED_ASSEMBLY_STEP)
+ display_text = "All it's missing is external armor..."
+ . += span_notice(display_text)
+
+/obj/item/mod/construction/shell/attackby(obj/item/part, mob/user, params)
+ . = ..()
+ switch(step)
+ if(START_STEP)
+ if(!istype(part, /obj/item/mod/construction/core))
+ return
+ if(!user.transferItemToLoc(part, src))
+ balloon_alert(user, "core stuck to your hand!")
+ return
+ playsound(src, 'sound/machines/click.ogg', 30, TRUE)
+ balloon_alert(user, "core inserted")
+ core = part
+ step = CORE_STEP
+ if(CORE_STEP)
+ if(part.tool_behaviour == TOOL_SCREWDRIVER) //Construct
+ if(part.use_tool(src, user, 0, volume=30))
+ balloon_alert(user, "core screwed")
+ step = SCREWED_CORE_STEP
+ else if(part.tool_behaviour == TOOL_CROWBAR) //Deconstruct
+ if(part.use_tool(src, user, 0, volume=30))
+ core.forceMove(drop_location())
+ balloon_alert(user, "core taken out")
+ step = START_STEP
+ if(SCREWED_CORE_STEP)
+ if(istype(part, /obj/item/mod/construction/helmet)) //Construct
+ if(!user.transferItemToLoc(part, src))
+ balloon_alert(user, "helmet stuck to your hand!")
+ return
+ playsound(src, 'sound/machines/click.ogg', 30, TRUE)
+ balloon_alert(user, "helmet added")
+ helmet = part
+ step = HELMET_STEP
+ else if(part.tool_behaviour == TOOL_SCREWDRIVER) //Deconstruct
+ if(part.use_tool(src, user, 0, volume=30))
+ balloon_alert(user, "core unscrewed")
+ step = CORE_STEP
+ if(HELMET_STEP)
+ if(istype(part, /obj/item/mod/construction/chestplate)) //Construct
+ if(!user.transferItemToLoc(part, src))
+ balloon_alert(user, "chestplate stuck to your hand!")
+ return
+ playsound(src, 'sound/machines/click.ogg', 30, TRUE)
+ balloon_alert(user, "chestplate added")
+ chestplate = part
+ step = CHESTPLATE_STEP
+ else if(part.tool_behaviour == TOOL_CROWBAR) //Deconstruct
+ if(part.use_tool(src, user, 0, volume=30))
+ helmet.forceMove(drop_location())
+ balloon_alert(user, "helmet removed")
+ helmet = null
+ step = SCREWED_CORE_STEP
+ if(CHESTPLATE_STEP)
+ if(istype(part, /obj/item/mod/construction/gauntlets)) //Construct
+ if(!user.transferItemToLoc(part, src))
+ balloon_alert(user, "gauntlets stuck to your hand!")
+ return
+ playsound(src, 'sound/machines/click.ogg', 30, TRUE)
+ balloon_alert(user, "gauntlets added")
+ gauntlets = part
+ step = GAUNTLETS_STEP
+ else if(part.tool_behaviour == TOOL_CROWBAR) //Deconstruct
+ if(part.use_tool(src, user, 0, volume=30))
+ chestplate.forceMove(drop_location())
+ balloon_alert(user, "chestplate removed")
+ chestplate = null
+ step = HELMET_STEP
+ if(GAUNTLETS_STEP)
+ if(istype(part, /obj/item/mod/construction/boots)) //Construct
+ if(!user.transferItemToLoc(part, src))
+ balloon_alert(user, "boots added")
+ return
+ playsound(src, 'sound/machines/click.ogg', 30, TRUE)
+ balloon_alert(user, "You fit [part] onto [src].")
+ boots = part
+ step = BOOTS_STEP
+ else if(part.tool_behaviour == TOOL_CROWBAR) //Deconstruct
+ if(part.use_tool(src, user, 0, volume=30))
+ gauntlets.forceMove(drop_location())
+ balloon_alert(user, "gauntlets removed")
+ gauntlets = null
+ step = CHESTPLATE_STEP
+ if(BOOTS_STEP)
+ if(part.tool_behaviour == TOOL_WRENCH) //Construct
+ if(part.use_tool(src, user, 0, volume=30))
+ balloon_alert(user, "assembly secured")
+ step = WRENCHED_ASSEMBLY_STEP
+ else if(part.tool_behaviour == TOOL_CROWBAR) //Deconstruct
+ if(part.use_tool(src, user, 0, volume=30))
+ boots.forceMove(drop_location())
+ balloon_alert(user, "boots removed")
+ boots = null
+ step = GAUNTLETS_STEP
+ if(WRENCHED_ASSEMBLY_STEP)
+ if(part.tool_behaviour == TOOL_SCREWDRIVER) //Construct
+ if(part.use_tool(src, user, 0, volume=30))
+ balloon_alert(user, "assembly screwed")
+ step = SCREWED_ASSEMBLY_STEP
+ else if(part.tool_behaviour == TOOL_WRENCH) //Deconstruct
+ if(part.use_tool(src, user, 0, volume=30))
+ balloon_alert(user, "assembly unsecured")
+ step = BOOTS_STEP
+ if(SCREWED_ASSEMBLY_STEP)
+ if(istype(part, /obj/item/mod/construction/armor)) //Construct
+ var/obj/item/mod/construction/armor/external_armor = part
+ if(!user.transferItemToLoc(part, src))
+ return
+ playsound(src, 'sound/machines/click.ogg', 30, TRUE)
+ balloon_alert(user, "suit finished")
+ var/obj/item/modsuit = new /obj/item/mod/control(drop_location(), external_armor.theme)
+ qdel(src)
+ user.put_in_hands(modsuit)
+ else if(part.tool_behaviour == TOOL_SCREWDRIVER) //Construct
+ if(part.use_tool(src, user, 0, volume=30))
+ balloon_alert(user, "assembly unscrewed")
+ step = SCREWED_ASSEMBLY_STEP
+ update_icon_state()
+
+/obj/item/mod/construction/shell/update_icon_state()
+ . = ..()
+ icon_state = "mod-construction_[step]"
+
+/obj/item/mod/construction/shell/Destroy()
+ QDEL_NULL(core)
+ QDEL_NULL(helmet)
+ QDEL_NULL(chestplate)
+ QDEL_NULL(gauntlets)
+ QDEL_NULL(boots)
+ return ..()
+
+/obj/item/mod/construction/shell/handle_atom_del(atom/deleted_atom)
+ if(deleted_atom == core)
+ core = null
+ if(deleted_atom == helmet)
+ helmet = null
+ if(deleted_atom == chestplate)
+ chestplate = null
+ if(deleted_atom == gauntlets)
+ gauntlets = null
+ if(deleted_atom == boots)
+ boots = null
+ return ..()
+
+#undef START_STEP
+#undef CORE_STEP
+#undef SCREWED_CORE_STEP
+#undef HELMET_STEP
+#undef CHESTPLATE_STEP
+#undef GAUNTLETS_STEP
+#undef BOOTS_STEP
+#undef WRENCHED_ASSEMBLY_STEP
+#undef SCREWED_ASSEMBLY_STEP
diff --git a/code/modules/mod/mod_control.dm b/code/modules/mod/mod_control.dm
new file mode 100644
index 0000000000..8ca7dc8573
--- /dev/null
+++ b/code/modules/mod/mod_control.dm
@@ -0,0 +1,559 @@
+/// MODsuits, trade-off between armor and utility
+/obj/item/mod
+ name = "Base MOD"
+ desc = "You should not see this, yell at a coder!"
+ icon = 'icons/obj/mod.dmi'
+ icon_state = "standard-control"
+ worn_icon = 'icons/mob/mod.dmi'
+
+/obj/item/mod/control
+ name = "MOD control unit"
+ desc = "The control unit of a Modular Outerwear Device, a powered, back-mounted suit that protects against various environments."
+ icon_state = "control"
+ inhand_icon_state = "mod_control"
+ w_class = WEIGHT_CLASS_BULKY
+ slot_flags = ITEM_SLOT_BACK
+ strip_delay = 10 SECONDS
+ slowdown = 2
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 100, FIRE = 25, ACID = 25, WOUND = 10)
+ actions_types = list(/datum/action/item_action/mod/deploy, /datum/action/item_action/mod/activate, /datum/action/item_action/mod/module, /datum/action/item_action/mod/panel)
+ resistance_flags = NONE
+ max_heat_protection_temperature = SPACE_SUIT_MAX_TEMP_PROTECT
+ min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
+ permeability_coefficient = 0.01
+ siemens_coefficient = 0.5
+ alternate_worn_layer = BODY_FRONT_LAYER
+ /// The MOD's theme, decides on some stuff like armor and statistics.
+ var/datum/mod_theme/theme = /datum/mod_theme
+ /// Looks of the MOD.
+ var/skin = "standard"
+ /// Theme of the MOD TGUI
+ var/ui_theme = "ntos"
+ /// If the suit is deployed and turned on.
+ var/active = FALSE
+ /// If the suit wire/module hatch is open.
+ var/open = FALSE
+ /// If the suit is ID locked.
+ var/locked = FALSE
+ /// If the suit is malfunctioning.
+ var/malfunctioning = FALSE
+ /// If the suit is currently activating/deactivating.
+ var/activating = FALSE
+ /// How long the MOD is electrified for.
+ var/seconds_electrified = MACHINE_NOT_ELECTRIFIED
+ /// If the suit interface is broken.
+ var/interface_break = FALSE
+ /// How much module complexity can this MOD carry.
+ var/complexity_max = DEFAULT_MAX_COMPLEXITY
+ /// How much module complexity this MOD is carrying.
+ var/complexity = 0
+ /// Power usage of the MOD.
+ var/cell_drain = DEFAULT_CELL_DRAIN
+ /// Slowdown of the MOD when not active.
+ var/slowdown_inactive = 2
+ /// Slowdown of the MOD when active.
+ var/slowdown_active = 1
+ /// MOD cell.
+ var/obj/item/stock_parts/cell/cell
+ /// MOD helmet.
+ var/obj/item/clothing/head/helmet/space/mod/helmet
+ /// MOD chestplate.
+ var/obj/item/clothing/suit/armor/mod/chestplate
+ /// MOD gauntlets.
+ var/obj/item/clothing/gloves/mod/gauntlets
+ /// MOD boots.
+ var/obj/item/clothing/shoes/mod/boots
+ /// List of parts (helmet, chestplate, gauntlets, boots).
+ var/list/mod_parts = list()
+ /// Modules the MOD should spawn with.
+ var/list/initial_modules = list()
+ /// Modules the MOD currently possesses.
+ var/list/modules = list()
+ /// Currently used module.
+ var/obj/item/mod/module/selected_module
+ /// AI mob inhabiting the MOD.
+ var/mob/living/silicon/ai/ai
+ /// Delay between moves as AI.
+ var/movedelay = 0
+ /// Cooldown for AI moves.
+ COOLDOWN_DECLARE(cooldown_mod_move)
+ /// Person wearing the MODsuit.
+ var/mob/living/carbon/human/wearer
+
+/obj/item/mod/control/Initialize(mapload, new_theme, new_skin)
+ . = ..()
+ if(new_theme)
+ theme = new_theme
+ theme = GLOB.mod_themes[theme]
+ slowdown_inactive = theme.slowdown_inactive
+ slowdown_active = theme.slowdown_active
+ slowdown = slowdown_inactive
+ complexity_max = theme.complexity_max
+ skin = new_skin || theme.default_skin
+ ui_theme = theme.ui_theme
+ cell_drain = theme.cell_drain
+ initial_modules += theme.inbuilt_modules
+ wires = new /datum/wires/mod(src)
+ if(length(req_access))
+ locked = TRUE
+ if(ispath(cell))
+ cell = new cell(src)
+ helmet = new /obj/item/clothing/head/helmet/space/mod(src)
+ helmet.mod = src
+ mod_parts += helmet
+ chestplate = new /obj/item/clothing/suit/armor/mod(src)
+ chestplate.mod = src
+ mod_parts += chestplate
+ gauntlets = new /obj/item/clothing/gloves/mod(src)
+ gauntlets.mod = src
+ mod_parts += gauntlets
+ boots = new /obj/item/clothing/shoes/mod(src)
+ boots.mod = src
+ mod_parts += boots
+ var/list/all_parts = mod_parts.Copy() + src
+ for(var/obj/item/piece as anything in all_parts)
+ piece.name = "[theme.name] [piece.name]"
+ piece.desc = "[piece.desc] [theme.desc]"
+ piece.armor = getArmor(arglist(theme.armor))
+ piece.resistance_flags = theme.resistance_flags
+ piece.heat_protection = NONE
+ piece.cold_protection = NONE
+ piece.max_heat_protection_temperature = theme.max_heat_protection_temperature
+ piece.min_cold_protection_temperature = theme.min_cold_protection_temperature
+ piece.permeability_coefficient = theme.permeability_coefficient
+ piece.siemens_coefficient = theme.siemens_coefficient
+ piece.icon_state = "[skin]-[initial(piece.icon_state)]"
+ update_flags()
+ for(var/obj/item/mod/module/module as anything in initial_modules)
+ module = new module(src)
+ install(module)
+ RegisterSignal(src, COMSIG_ATOM_EXITED, .proc/on_exit)
+ movedelay = CONFIG_GET(number/movedelay/run_delay)
+
+/obj/item/mod/control/Destroy()
+ if(active)
+ STOP_PROCESSING(SSobj, src)
+ var/atom/deleting_atom
+ if(!QDELETED(helmet))
+ deleting_atom = helmet
+ helmet.mod = null
+ helmet = null
+ mod_parts -= deleting_atom
+ qdel(deleting_atom)
+ if(!QDELETED(chestplate))
+ deleting_atom = chestplate
+ chestplate.mod = null
+ chestplate = null
+ mod_parts -= deleting_atom
+ qdel(deleting_atom)
+ if(!QDELETED(gauntlets))
+ deleting_atom = gauntlets
+ gauntlets.mod = null
+ gauntlets = null
+ mod_parts -= deleting_atom
+ qdel(deleting_atom)
+ if(!QDELETED(boots))
+ deleting_atom = boots
+ boots.mod = null
+ boots = null
+ mod_parts -= deleting_atom
+ qdel(deleting_atom)
+ for(var/obj/item/mod/module/module as anything in modules)
+ module.mod = null
+ modules -= module
+ QDEL_NULL(wires)
+ QDEL_NULL(cell)
+ return ..()
+
+/obj/item/mod/control/process(delta_time)
+ if(seconds_electrified > MACHINE_NOT_ELECTRIFIED)
+ seconds_electrified--
+ if((!cell || !cell.charge) && active && !activating)
+ power_off()
+ return PROCESS_KILL
+ var/malfunctioning_charge_drain = 0
+ if(malfunctioning)
+ malfunctioning_charge_drain = rand(1,20)
+ cell.charge = max(0, cell.charge - (cell_drain + malfunctioning_charge_drain)*delta_time)
+ update_cell_alert()
+ for(var/obj/item/mod/module/module as anything in modules)
+ if(malfunctioning && module.active && DT_PROB(5, delta_time))
+ module.on_deactivation()
+ module.on_process(delta_time)
+
+/obj/item/mod/control/equipped(mob/user, slot)
+ ..()
+ if(slot == ITEM_SLOT_BACK)
+ set_wearer(user)
+ else if(wearer)
+ unset_wearer()
+
+/obj/item/mod/control/dropped(mob/user)
+ . = ..()
+ if(wearer)
+ unset_wearer()
+
+/obj/item/mod/control/item_action_slot_check(slot)
+ if(slot == ITEM_SLOT_BACK)
+ return TRUE
+
+/obj/item/mod/control/allow_attack_hand_drop(mob/user)
+ var/mob/living/carbon/carbon_user = user
+ if(!istype(carbon_user) || src != carbon_user.back)
+ return ..()
+ for(var/obj/item/part in mod_parts)
+ if(part.loc != src)
+ balloon_alert(carbon_user, "retract parts first!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, FALSE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+
+/obj/item/mod/control/MouseDrop(atom/over_object)
+ if(src != wearer?.back || !istype(over_object, /atom/movable/screen/inventory/hand))
+ return ..()
+ for(var/obj/item/part in mod_parts)
+ if(part.loc != src)
+ balloon_alert(wearer, "retract parts first!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, FALSE, SILENCED_SOUND_EXTRARANGE)
+ return
+ if(!wearer.incapacitated())
+ var/atom/movable/screen/inventory/hand/ui_hand = over_object
+ if(wearer.putItemFromInventoryInHandIfPossible(src, ui_hand.held_index))
+ add_fingerprint(usr)
+ return ..()
+
+/obj/item/mod/control/attack_hand(mob/user)
+ if(seconds_electrified && cell?.charge)
+ if(shock(user))
+ return
+ if(open && loc == user)
+ if(!cell)
+ balloon_alert(user, "no cell!")
+ return
+ balloon_alert(user, "removing cell...")
+ if(!do_after(user, 1.5 SECONDS, target = src))
+ balloon_alert(user, "interrupted!")
+ return
+ balloon_alert(user, "cell removed")
+ playsound(src, 'sound/machines/click.ogg', 50, TRUE, SILENCED_SOUND_EXTRARANGE)
+ if(!user.put_in_hands(cell))
+ cell.forceMove(drop_location())
+ update_cell_alert()
+ return
+ return ..()
+
+/obj/item/mod/control/screwdriver_act(mob/living/user, obj/item/screwdriver)
+ if(..())
+ return TRUE
+ if(active || activating || ai_controller)
+ balloon_alert(user, "deactivate suit first!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+ balloon_alert(user, "[open ? "closing" : "opening"] panel...")
+ screwdriver.play_tool_sound(src, 100)
+ if(screwdriver.use_tool(src, user, 1 SECONDS))
+ if(active || activating)
+ balloon_alert(user, "deactivate suit first!")
+ screwdriver.play_tool_sound(src, 100)
+ balloon_alert(user, "panel [open ? "closed" : "opened"]")
+ open = !open
+ else
+ balloon_alert(user, "interrupted!")
+ return TRUE
+
+/obj/item/mod/control/crowbar_act(mob/living/user, obj/item/crowbar)
+ . = ..()
+ if(!open)
+ balloon_alert(user, "open the panel first!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+ if(!allowed(user))
+ balloon_alert(user, "insufficient access!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return
+ if(length(modules))
+ var/list/removable_modules = list()
+ for(var/obj/item/mod/module/module as anything in modules)
+ if(!module.removable)
+ continue
+ removable_modules += module
+ var/obj/item/mod/module/module_to_remove = tgui_input_list(user, "Which module to remove?", "Module Removal", removable_modules)
+ if(!module_to_remove?.mod)
+ return FALSE
+ uninstall(module_to_remove)
+ module_to_remove.forceMove(drop_location())
+ crowbar.play_tool_sound(src, 100)
+ return TRUE
+ balloon_alert(user, "no modules!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+
+/obj/item/mod/control/attackby(obj/item/attacking_item, mob/living/user, params)
+ if(istype(attacking_item, /obj/item/mod/module))
+ if(!open)
+ balloon_alert(user, "open the panel first!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+ install(attacking_item, user)
+ return TRUE
+ else if(istype(attacking_item, /obj/item/stock_parts/cell))
+ if(!open)
+ balloon_alert(user, "open the panel first!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+ if(cell)
+ balloon_alert(user, "cell already installed!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return FALSE
+ attacking_item.forceMove(src)
+ cell = attacking_item
+ balloon_alert(user, "cell installed")
+ playsound(src, 'sound/machines/click.ogg', 50, TRUE, SILENCED_SOUND_EXTRARANGE)
+ update_cell_alert()
+ return TRUE
+ else if(is_wire_tool(attacking_item) && open)
+ wires.interact(user)
+ return TRUE
+ else if(istype(attacking_item, /obj/item/mod/paint))
+ if(active || activating)
+ balloon_alert(user, "suit is active!")
+ else if(paint(user, attacking_item))
+ balloon_alert(user, "suit painted")
+ else
+ balloon_alert(user, "not painted!")
+ return TRUE
+ else if(open && attacking_item.GetID())
+ update_access(user, attacking_item)
+ return TRUE
+ return ..()
+
+/obj/item/mod/control/get_cell()
+ if(open)
+ return cell
+
+/obj/item/mod/control/GetAccess()
+ if(ai_controller)
+ return req_access.Copy()
+ else
+ return ..()
+
+/obj/item/mod/control/emag_act(mob/user)
+ locked = !locked
+ balloon_alert(user, "[locked ? "locked" : "unlocked"]")
+
+/obj/item/mod/control/emp_act(severity)
+ . = ..()
+ to_chat(wearer, span_notice("[severity > 1 ? "Light" : "Strong"] electromagnetic pulse detected!"))
+ if(!active || !wearer || . & EMP_PROTECT_CONTENTS)
+ return
+ selected_module = null
+ wearer.apply_damage(10 / severity, BURN, spread_damage=TRUE)
+ to_chat(wearer, span_danger("You feel [src] heat up from the EMP, burning you slightly."))
+ if (wearer.stat < UNCONSCIOUS && prob(10))
+ wearer.emote("scream")
+
+/obj/item/mod/control/on_outfit_equip(mob/living/carbon/human/outfit_wearer, visuals_only, item_slot)
+ if(visuals_only)
+ set_wearer(outfit_wearer) //we need to set wearer manually since it doesnt call equipped
+ quick_activation()
+
+/obj/item/mod/control/doStrip(mob/stripper, mob/owner)
+ if(active && !toggle_activate(stripper, force_deactivate = TRUE))
+ return
+ for(var/obj/item/part in mod_parts)
+ conceal(null, part)
+ return ..()
+
+/obj/item/mod/control/worn_overlays(mutable_appearance/standing, isinhands = FALSE, icon_file)
+ . = ..()
+ if(!active)
+ return
+ for(var/obj/item/mod/module/module as anything in modules)
+ var/list/module_icons = module.generate_worn_overlay(standing)
+ if(!length(module_icons))
+ continue
+ . += module_icons
+
+/obj/item/mod/control/proc/set_wearer(mob/user)
+ wearer = user
+ RegisterSignal(wearer, COMSIG_ATOM_EXITED, .proc/on_exit)
+ RegisterSignal(wearer, COMSIG_PROCESS_BORGCHARGER_OCCUPANT, .proc/on_borg_charge)
+ update_cell_alert()
+ for(var/obj/item/mod/module/module as anything in modules)
+ module.on_equip()
+
+/obj/item/mod/control/proc/unset_wearer()
+ for(var/obj/item/mod/module/module as anything in modules)
+ module.on_unequip()
+ UnregisterSignal(wearer, list(COMSIG_ATOM_EXITED, COMSIG_PROCESS_BORGCHARGER_OCCUPANT))
+ wearer.clear_alert("mod_charge")
+ wearer = null
+
+/obj/item/mod/control/proc/update_flags()
+ var/list/used_skin = theme.skins[skin]
+ for(var/obj/item/clothing/part as anything in mod_parts)
+ var/used_category
+ if(part == helmet)
+ used_category = HELMET_FLAGS
+ helmet.alternate_worn_layer = used_skin[HELMET_LAYER]
+ helmet.alternate_layer = used_skin[HELMET_LAYER]
+ if(part == chestplate)
+ used_category = CHESTPLATE_FLAGS
+ if(part == gauntlets)
+ used_category = GAUNTLETS_FLAGS
+ if(part == boots)
+ used_category = BOOTS_FLAGS
+ var/list/category = used_skin[used_category]
+ part.clothing_flags = category[UNSEALED_CLOTHING] || NONE
+ part.visor_flags = category[SEALED_CLOTHING] || NONE
+ part.flags_inv = category[UNSEALED_INVISIBILITY] || NONE
+ part.visor_flags_inv = category[SEALED_INVISIBILITY] || NONE
+ part.flags_cover = category[UNSEALED_COVER] || NONE
+ part.visor_flags_cover = category[SEALED_COVER] || NONE
+
+/obj/item/mod/control/proc/quick_module(mob/user)
+ if(!length(modules))
+ return
+ var/list/display_names = list()
+ var/list/items = list()
+ for(var/obj/item/mod/module/module as anything in modules)
+ if(module.module_type == MODULE_PASSIVE)
+ continue
+ display_names[module.name] = REF(module)
+ var/image/module_image = image(icon = module.icon, icon_state = module.icon_state)
+ items += list(module.name = module_image)
+ if(!length(items))
+ return
+ var/pick = show_radial_menu(user, src, items, custom_check = FALSE, require_near = TRUE)
+ if(!pick)
+ return
+ var/module_reference = display_names[pick]
+ var/obj/item/mod/module/selected_module = locate(module_reference) in modules
+ if(!istype(selected_module) || user.incapacitated())
+ return
+ selected_module.on_select()
+
+/obj/item/mod/control/proc/paint(mob/user, obj/item/paint)
+ if(length(theme.skins) <= 1)
+ return FALSE
+ var/list/skins = list()
+ for(var/mod_skin in theme.skins)
+ skins[mod_skin] = image(icon = icon, icon_state = "[mod_skin]-control")
+ var/pick = show_radial_menu(user, src, skins, custom_check = FALSE, require_near = TRUE)
+ if(!pick || !user.is_holding(paint))
+ return FALSE
+ skin = pick
+ var/list/skin_updating = mod_parts.Copy() + src
+ for(var/obj/item/piece as anything in skin_updating)
+ piece.icon_state = "[skin]-[initial(piece.icon_state)]"
+ update_flags()
+ wearer?.regenerate_icons()
+ return TRUE
+
+/obj/item/mod/control/proc/shock(mob/living/user)
+ if(!istype(user) || cell?.charge < 1)
+ return FALSE
+ do_sparks(5, TRUE, src)
+ var/check_range = TRUE
+ return electrocute_mob(user, cell, src, 0.7, check_range)
+
+/obj/item/mod/control/proc/install(module, mob/user)
+ var/obj/item/mod/module/new_module = module
+ for(var/obj/item/mod/module/old_module as anything in modules)
+ if(is_type_in_list(new_module, old_module.incompatible_modules) || is_type_in_list(old_module, new_module.incompatible_modules))
+ if(user)
+ balloon_alert(user, "[new_module] incompatible with [old_module]!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return
+ if(is_type_in_list(module, theme.module_blacklist))
+ if(user)
+ balloon_alert(user, "[src] doesn't accept [new_module]!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return
+ var/complexity_with_module = complexity
+ complexity_with_module += new_module.complexity
+ if(complexity_with_module > complexity_max)
+ if(user)
+ balloon_alert(user, "[new_module] would make [src] too complex!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return
+ new_module.forceMove(src)
+ modules += new_module
+ complexity += new_module.complexity
+ new_module.mod = src
+ new_module.on_install()
+ if(wearer)
+ new_module.on_equip()
+ if(user)
+ balloon_alert(user, "[new_module] added")
+ playsound(src, 'sound/machines/click.ogg', 50, TRUE, SILENCED_SOUND_EXTRARANGE)
+
+/obj/item/mod/control/proc/uninstall(module)
+ var/obj/item/mod/module/old_module = module
+ modules -= old_module
+ complexity -= old_module.complexity
+ if(active)
+ old_module.on_suit_deactivation()
+ if(old_module.active)
+ old_module.on_deactivation()
+ if(wearer)
+ old_module.on_unequip()
+ old_module.on_uninstall()
+ old_module.mod = null
+
+/obj/item/mod/control/proc/update_access(mob/user, obj/item/card/id/card)
+ if(!allowed(user))
+ balloon_alert(user, "insufficient access!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return
+ req_access = card.access.Copy()
+ balloon_alert(user, "access updated")
+
+/obj/item/mod/control/proc/update_cell_alert()
+ if(!wearer)
+ return
+ if(!cell)
+ wearer.throw_alert("mod_charge", /atom/movable/screen/alert/nocell)
+ return
+ var/remaining_cell = cell.charge/cell.maxcharge
+ switch(remaining_cell)
+ if(0.75 to INFINITY)
+ wearer.clear_alert("mod_charge")
+ if(0.5 to 0.75)
+ wearer.throw_alert("mod_charge", /atom/movable/screen/alert/lowcell, 1)
+ if(0.25 to 0.5)
+ wearer.throw_alert("mod_charge", /atom/movable/screen/alert/lowcell, 2)
+ if(0.01 to 0.25)
+ wearer.throw_alert("mod_charge", /atom/movable/screen/alert/lowcell, 3)
+ else
+ wearer.throw_alert("mod_charge", /atom/movable/screen/alert/emptycell)
+
+/obj/item/mod/control/proc/power_off()
+ balloon_alert(wearer, "no power!")
+ toggle_activate(wearer, force_deactivate = TRUE)
+
+/obj/item/mod/control/proc/on_exit(datum/source, atom/movable/part, direction)
+ SIGNAL_HANDLER
+
+ if(part.loc == src)
+ return
+ if(part == cell)
+ cell = null
+ update_cell_alert()
+ return
+ if(part.loc == wearer)
+ return
+ if(modules.Find(part))
+ uninstall(part)
+ return
+ if(mod_parts.Find(part))
+ conceal(wearer, part)
+ if(active)
+ INVOKE_ASYNC(src, .proc/toggle_activate, wearer, TRUE)
+ return
+
+/obj/item/mod/control/proc/on_borg_charge(datum/source, amount)
+ SIGNAL_HANDLER
+
+ if(!cell)
+ return
+ cell.give(amount)
diff --git a/code/modules/mod/mod_theme.dm b/code/modules/mod/mod_theme.dm
new file mode 100644
index 0000000000..174cc1bab3
--- /dev/null
+++ b/code/modules/mod/mod_theme.dm
@@ -0,0 +1,854 @@
+/// Global proc that sets up all MOD themes as singletons in a list and returns it.
+/proc/setup_mod_themes()
+ . = list()
+ for(var/path in typesof(/datum/mod_theme))
+ var/datum/mod_theme/new_theme = new path()
+ .[path] = new_theme
+
+/// MODsuit theme, instanced once and then used by MODsuits to grab various statistics.
+/datum/mod_theme
+ /// Theme name for the MOD.
+ var/name = "standard"
+ /// Description added to the MOD.
+ var/desc = "A civilian class suit by Nakamura Engineering, doesn't offer much other than slightly quicker movement."
+ /// Default skin of the MOD.
+ var/default_skin = "standard"
+ /// Armor shared across the MOD pieces.
+ var/armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 100, FIRE = 25, ACID = 25, WOUND = 10)
+ /// Resistance flags shared across the MOD pieces.
+ var/resistance_flags = NONE
+ /// Max heat protection shared across the MOD pieces.
+ var/max_heat_protection_temperature = SPACE_SUIT_MAX_TEMP_PROTECT
+ /// Max cold protection shared across the MOD pieces.
+ var/min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
+ /// Permeability shared across the MOD pieces.
+ var/permeability_coefficient = 0.01
+ /// Siemens shared across the MOD pieces.
+ var/siemens_coefficient = 0.5
+ /// How much modules can the MOD carry without malfunctioning.
+ var/complexity_max = DEFAULT_MAX_COMPLEXITY
+ /// How much battery power the MOD uses by just being on
+ var/cell_drain = DEFAULT_CELL_DRAIN
+ /// Slowdown of the MOD when not active.
+ var/slowdown_inactive = 1.25
+ /// Slowdown of the MOD when active.
+ var/slowdown_active = 0.75
+ /// Theme used by the MOD TGUI.
+ var/ui_theme = "ntos"
+ /// List of inbuilt modules. These are different from the pre-equipped suits, you should mainly use these for unremovable modules with 0 complexity.
+ var/list/inbuilt_modules = list()
+ /// Modules blacklisted from the MOD.
+ var/list/module_blacklist = list()
+ /// List of skins with their appropriate clothing flags.
+ var/list/skins = list(
+ "standard" = list(
+ HELMET_LAYER = NECK_LAYER,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT,
+ SEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ "civilian" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ UNSEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/engineering
+ name = "engineering"
+ desc = "An engineer-fit suit with heat and shock resistance. Nakamura Engineering's classic."
+ default_skin = "engineering"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 10, BIO = 100, FIRE = 100, ACID = 25, WOUND = 10)
+ resistance_flags = FIRE_PROOF
+ max_heat_protection_temperature = FIRE_SUIT_MAX_TEMP_PROTECT
+ siemens_coefficient = 0
+ slowdown_inactive = 1.5
+ slowdown_active = 1
+ skins = list(
+ "engineering" = list(
+ HELMET_LAYER = NECK_LAYER,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT,
+ SEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/atmospheric
+ name = "atmospheric"
+ desc = "An atmospheric-resistant suit by Nakamura Engineering, offering extreme heat resistance compared to the engineer suit."
+ default_skin = "atmospheric"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 10, BIO = 100, FIRE = 100, ACID = 75, WOUND = 10)
+ resistance_flags = FIRE_PROOF
+ max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
+ slowdown_inactive = 1.5
+ slowdown_active = 1
+ skins = list(
+ "atmospheric" = list(
+ HELMET_LAYER = NECK_LAYER,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT,
+ SEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR|HIDESNOUT,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR,
+ UNSEALED_COVER = HEADCOVERSMOUTH,
+ SEALED_COVER = HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/advanced
+ name = "advanced"
+ desc = "An advanced version of Nakamura Engineering's classic suit, shining with a white, acid and fire resistant polish."
+ default_skin = "advanced"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 50, BIO = 100, FIRE = 100, ACID = 90, WOUND = 10)
+ resistance_flags = FIRE_PROOF
+ max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
+ siemens_coefficient = 0
+ slowdown_inactive = 1
+ slowdown_active = 0.5
+ inbuilt_modules = list(/obj/item/mod/module/magboot/advanced)
+ skins = list(
+ "advanced" = list(
+ HELMET_LAYER = NECK_LAYER,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT,
+ SEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/mining
+ name = "mining"
+ desc = "A high-power Nanotrasen mining suit, supporting more complexity at a bigger drain."
+ default_skin = "mining"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 50, BIO = 100, FIRE = 100, ACID = 75, WOUND = 15)
+ resistance_flags = FIRE_PROOF
+ max_heat_protection_temperature = FIRE_SUIT_MAX_TEMP_PROTECT
+ cell_drain = DEFAULT_CELL_DRAIN * 2
+ complexity_max = DEFAULT_MAX_COMPLEXITY + 5
+ skins = list(
+ "mining" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEEARS|HIDEHAIR|HIDESNOUT,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEYES|HIDEFACE,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/medical
+ name = "medical"
+ desc = "A lightweight suit by DeForest Medical Corporation, allows for easier movement."
+ default_skin = "medical"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 10, BIO = 100, FIRE = 60, ACID = 75, WOUND = 10)
+ cell_drain = DEFAULT_CELL_DRAIN * 1.5
+ slowdown_inactive = 1
+ slowdown_active = 0.5
+ skins = list(
+ "medical" = list(
+ HELMET_LAYER = NECK_LAYER,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT,
+ SEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ "corpsman" = list(
+ HELMET_LAYER = NECK_LAYER,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT,
+ SEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/rescue
+ name = "rescue"
+ desc = "An advanced version of DeForest Medical Corporation's medical suit, designed for quick rescue of bodies from the most dangerous environments."
+ default_skin = "rescue"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 10, BIO = 100, FIRE = 100, ACID = 100, WOUND = 10)
+ resistance_flags = FIRE_PROOF|ACID_PROOF
+ max_heat_protection_temperature = FIRE_SUIT_MAX_TEMP_PROTECT
+ cell_drain = DEFAULT_CELL_DRAIN * 1.5
+ slowdown_inactive = 0.75
+ slowdown_active = 0.25
+ inbuilt_modules = list(/obj/item/mod/module/quick_carry/advanced)
+ skins = list(
+ "rescue" = list(
+ HELMET_LAYER = NECK_LAYER,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT,
+ SEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/research
+ name = "research"
+ desc = "A private military EOD suit by Aussec Armory, intended for explosive research. Bulky, but expansive."
+ default_skin = "research"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 100, BIO = 100, FIRE = 100, ACID = 100, WOUND = 15)
+ resistance_flags = FIRE_PROOF|ACID_PROOF
+ max_heat_protection_temperature = FIRE_SUIT_MAX_TEMP_PROTECT
+ complexity_max = DEFAULT_MAX_COMPLEXITY + 5
+ slowdown_inactive = 2
+ slowdown_active = 1.5
+ inbuilt_modules = list(/obj/item/mod/module/reagent_scanner/advanced)
+ skins = list(
+ "research" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ UNSEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/security
+ name = "security"
+ desc = "An Apadyne Technologies security suit, offering shock protection and quicker speed, at the cost of carrying capacity."
+ default_skin = "security"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 10, BIO = 100, FIRE = 75, ACID = 75, WOUND = 20)
+ siemens_coefficient = 0
+ complexity_max = DEFAULT_MAX_COMPLEXITY - 5
+ slowdown_inactive = 1
+ slowdown_active = 0.5
+ skins = list(
+ "security" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEEARS|HIDEHAIR|HIDESNOUT,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEYES|HIDEFACE,
+ UNSEALED_COVER = HEADCOVERSMOUTH,
+ SEALED_COVER = HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/safeguard
+ name = "safeguard"
+ desc = "An Apadyne Technologies advanced security suit, offering greater speed and fire protection than the standard security model."
+ default_skin = "safeguard"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 25, BIO = 100, FIRE = 100, ACID = 95, WOUND = 25)
+ resistance_flags = FIRE_PROOF
+ max_heat_protection_temperature = FIRE_SUIT_MAX_TEMP_PROTECT
+ siemens_coefficient = 0
+ complexity_max = DEFAULT_MAX_COMPLEXITY - 5
+ slowdown_inactive = 0.75
+ slowdown_active = 0.25
+ skins = list(
+ "safeguard" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ UNSEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/magnate
+ name = "magnate"
+ desc = "A fancy, very protective suit for Nanotrasen's captains. Shock, fire and acid-proof while also having a large capacity and high speed."
+ default_skin = "magnate"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 50, BIO = 100, FIRE = 100, ACID = 100, WOUND = 20)
+ resistance_flags = FIRE_PROOF|ACID_PROOF
+ max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
+ siemens_coefficient = 0
+ complexity_max = DEFAULT_MAX_COMPLEXITY + 5
+ slowdown_inactive = 0.75
+ slowdown_active = 0.25
+ skins = list(
+ "magnate" = list(
+ HELMET_LAYER = NECK_LAYER,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT,
+ SEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/cosmohonk
+ name = "cosmohonk"
+ desc = "A suit by Honk Ltd. Protects against low humor environments. Most of the tech went to lower the power cost."
+ default_skin = "cosmohonk"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 10, BIO = 100, FIRE = 60, ACID = 30, WOUND = 5)
+ cell_drain = DEFAULT_CELL_DRAIN * 0.25
+ slowdown_inactive = 1.75
+ slowdown_active = 1.25
+ skins = list(
+ "cosmohonk" = list(
+ HELMET_LAYER = NECK_LAYER,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEEARS|HIDEHAIR,
+ SEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEMASK|HIDEEYES|HIDEFACE|HIDESNOUT,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/syndicate
+ name = "syndicate"
+ desc = "A suit designed by Gorlex Marauders, offering armor ruled illegal in most of Spinward Stellar."
+ default_skin = "syndicate"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 35, BIO = 100, FIRE = 50, ACID = 90, WOUND = 25)
+ max_heat_protection_temperature = FIRE_SUIT_MAX_TEMP_PROTECT
+ siemens_coefficient = 0
+ slowdown_inactive = 1
+ slowdown_active = 0.5
+ ui_theme = "syndicate"
+ inbuilt_modules = list(/obj/item/mod/module/armor_booster)
+ skins = list(
+ "syndicate" = list(
+ HELMET_LAYER = NECK_LAYER,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT,
+ SEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/elite
+ name = "elite"
+ desc = "An elite suit upgraded by Cybersun Industries, offering upgraded armor values."
+ default_skin = "elite"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 55, BIO = 100, FIRE = 100, ACID = 100, WOUND = 25)
+ resistance_flags = FIRE_PROOF|ACID_PROOF
+ max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
+ siemens_coefficient = 0
+ slowdown_inactive = 0.75
+ slowdown_active = 0.25
+ ui_theme = "syndicate"
+ inbuilt_modules = list(/obj/item/mod/module/armor_booster/elite)
+ skins = list(
+ "elite" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT,
+ SEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/enchanted
+ name = "enchanted"
+ desc = "The Wizard Federation's relatively low-tech MODsuit. Is very protective, though."
+ default_skin = "enchanted"
+ armor = list(MELEE = 40, BULLET = 40, LASER = 40, ENERGY = 50, BOMB = 35, BIO = 100, FIRE = 100, ACID = 100, WOUND = 30)
+ resistance_flags = FIRE_PROOF|ACID_PROOF
+ max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
+ siemens_coefficient = 0
+ complexity_max = DEFAULT_MAX_COMPLEXITY - 5
+ slowdown_inactive = 0.75
+ slowdown_active = 0.25
+ ui_theme = "wizard"
+ inbuilt_modules = list(/obj/item/mod/module/anti_magic/wizard)
+ skins = list(
+ "enchanted" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ UNSEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/prototype
+ name = "prototype"
+ desc = "A prototype modular suit powered by locomotives. While it is comfortable and has a big capacity, it remains very bulky and power-inefficient."
+ default_skin = "prototype"
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 50, BIO = 100, FIRE = 100, ACID = 75, WOUND = 5)
+ resistance_flags = FIRE_PROOF
+ complexity_max = DEFAULT_MAX_COMPLEXITY + 10
+ slowdown_inactive = 2.5
+ slowdown_active = 2
+ ui_theme = "hackerman"
+ inbuilt_modules = list(/obj/item/mod/module/kinesis)
+ skins = list(
+ "prototype" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ UNSEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/responsory
+ name = "responsory"
+ desc = "A high-speed rescue suit by Nanotrasen, intended for its' emergency response teams."
+ default_skin = "responsory"
+ armor = list(MELEE = 35, BULLET = 30, LASER = 30, ENERGY = 40, BOMB = 50, BIO = 100, FIRE = 100, ACID = 90, WOUND = 15)
+ resistance_flags = FIRE_PROOF
+ max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
+ siemens_coefficient = 0
+ slowdown_inactive = 0.5
+ slowdown_active = 0
+ skins = list(
+ "responsory" = list(
+ HELMET_LAYER = NECK_LAYER,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT,
+ SEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ "inquisitory" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ UNSEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/apocryphal
+ name = "apocryphal"
+ desc = "A high-tech, only technically legal, armored suit created by a collaboration effort between Nanotrasen and Apadyne Technologies."
+ default_skin = "apocryphal"
+ armor = list(MELEE = 80, BULLET = 80, LASER = 50, ENERGY = 60, BOMB = 100, BIO = 100, FIRE = 100, ACID = 100, WOUND = 25)
+ resistance_flags = FIRE_PROOF|ACID_PROOF
+ max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
+ siemens_coefficient = 0
+ complexity_max = DEFAULT_MAX_COMPLEXITY + 10
+ skins = list(
+ "apocryphal" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEEARS|HIDEHAIR,
+ SEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEMASK|HIDEEYES|HIDEFACE|HIDESNOUT,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/corporate
+ name = "corporate"
+ desc = "A fancy, high-tech suit for Nanotrasen's high ranking officers."
+ default_skin = "corporate"
+ armor = list(MELEE = 35, BULLET = 40, LASER = 40, ENERGY = 50, BOMB = 50, BIO = 100, FIRE = 100, ACID = 100, WOUND = 15)
+ resistance_flags = FIRE_PROOF|ACID_PROOF
+ max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
+ siemens_coefficient = 0
+ slowdown_inactive = 0.5
+ slowdown_active = 0
+ skins = list(
+ "corporate" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEEARS|HIDEHAIR|HIDESNOUT,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEYES|HIDEFACE,
+ SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/debug
+ name = "debug"
+ desc = "Strangely nostalgic."
+ default_skin = "debug"
+ armor = list(MELEE = 50, BULLET = 50, LASER = 50, ENERGY = 50, BOMB = 100, BIO = 100, FIRE = 100, ACID = 100, WOUND = 0)
+ resistance_flags = FIRE_PROOF|ACID_PROOF
+ max_heat_protection_temperature = FIRE_SUIT_MAX_TEMP_PROTECT
+ complexity_max = 50
+ slowdown_inactive = 0.5
+ slowdown_active = 0
+ skins = list(
+ "debug" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEEARS|HIDEHAIR|HIDESNOUT,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE,
+ UNSEALED_COVER = HEADCOVERSMOUTH,
+ SEALED_COVER = HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL,
+ SEALED_CLOTHING = STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
+
+/datum/mod_theme/administrative
+ name = "administrative"
+ desc = "A suit made of adminium. Who comes up with these stupid mineral names?"
+ default_skin = "debug"
+ armor = list(MELEE = 100, BULLET = 100, LASER = 100, ENERGY = 100, BOMB = 100, BIO = 100, FIRE = 100, ACID = 100, WOUND = 100)
+ resistance_flags = INDESTRUCTIBLE|LAVA_PROOF|FIRE_PROOF|UNACIDABLE|ACID_PROOF
+ max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
+ complexity_max = 1000
+ cell_drain = DEFAULT_CELL_DRAIN * 0
+ slowdown_inactive = 0
+ slowdown_active = 0
+ skins = list(
+ "debug" = list(
+ HELMET_LAYER = null,
+ HELMET_FLAGS = list(
+ UNSEALED_CLOTHING = SNUG_FIT|THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ UNSEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEEARS|HIDEHAIR|HIDESNOUT,
+ SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE,
+ UNSEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
+ ),
+ CHESTPLATE_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE|BLOCKS_SHOVE_KNOCKDOWN,
+ SEALED_INVISIBILITY = HIDEJUMPSUIT,
+ ),
+ GAUNTLETS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ ),
+ BOOTS_FLAGS = list(
+ UNSEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
+ ),
+ ),
+ )
diff --git a/code/modules/mod/mod_types.dm b/code/modules/mod/mod_types.dm
new file mode 100644
index 0000000000..af482e50f4
--- /dev/null
+++ b/code/modules/mod/mod_types.dm
@@ -0,0 +1,157 @@
+/obj/item/mod/control/pre_equipped
+ cell = /obj/item/stock_parts/cell/high
+ var/applied_skin
+
+/obj/item/mod/control/pre_equipped/Initialize(mapload, new_theme, new_skin)
+ new_skin = applied_skin
+ return ..()
+
+/obj/item/mod/control/pre_equipped/standard
+ initial_modules = list(/obj/item/mod/module/storage, /obj/item/mod/module/welding, /obj/item/mod/module/flashlight)
+
+/obj/item/mod/control/pre_equipped/engineering
+ theme = /datum/mod_theme/engineering
+ initial_modules = list(/obj/item/mod/module/storage, /obj/item/mod/module/welding, /obj/item/mod/module/rad_protection, /obj/item/mod/module/flashlight, /obj/item/mod/module/magboot)
+
+/obj/item/mod/control/pre_equipped/atmospheric
+ theme = /datum/mod_theme/atmospheric
+ initial_modules = list(/obj/item/mod/module/storage, /obj/item/mod/module/welding, /obj/item/mod/module/rad_protection, /obj/item/mod/module/flashlight, /obj/item/mod/module/t_ray)
+
+/obj/item/mod/control/pre_equipped/advanced
+ theme = /datum/mod_theme/advanced
+ cell = /obj/item/stock_parts/cell/super
+ initial_modules = list(/obj/item/mod/module/storage/large_capacity, /obj/item/mod/module/welding, /obj/item/mod/module/rad_protection, /obj/item/mod/module/jetpack, /obj/item/mod/module/flashlight)
+
+/obj/item/mod/control/pre_equipped/mining
+ theme = /datum/mod_theme/mining
+ cell = /obj/item/stock_parts/cell/high/plus
+ initial_modules = list(/obj/item/mod/module/storage/large_capacity, /obj/item/mod/module/welding, /obj/item/mod/module/orebag, /obj/item/mod/module/flashlight, /obj/item/mod/module/magboot, /obj/item/mod/module/drill)
+
+/obj/item/mod/control/pre_equipped/medical
+ theme = /datum/mod_theme/medical
+ initial_modules = list(/obj/item/mod/module/storage, /obj/item/mod/module/flashlight, /obj/item/mod/module/health_analyzer, /obj/item/mod/module/quick_carry)
+
+/obj/item/mod/control/pre_equipped/rescue
+ theme = /datum/mod_theme/rescue
+ cell = /obj/item/stock_parts/cell/super
+ initial_modules = list(/obj/item/mod/module/storage/large_capacity, /obj/item/mod/module/flashlight, /obj/item/mod/module/health_analyzer, /obj/item/mod/module/injector)
+
+/obj/item/mod/control/pre_equipped/research
+ theme = /datum/mod_theme/research
+ cell = /obj/item/stock_parts/cell/super
+ initial_modules = list(/obj/item/mod/module/storage/large_capacity, /obj/item/mod/module/welding, /obj/item/mod/module/flashlight, /obj/item/mod/module/circuit, /obj/item/mod/module/t_ray)
+
+/obj/item/mod/control/pre_equipped/security
+ theme = /datum/mod_theme/security
+ initial_modules = list(/obj/item/mod/module/storage, /obj/item/mod/module/welding, /obj/item/mod/module/flashlight, /obj/item/mod/module/holster)
+
+/obj/item/mod/control/pre_equipped/safeguard
+ theme = /datum/mod_theme/safeguard
+ cell = /obj/item/stock_parts/cell/super
+ initial_modules = list(/obj/item/mod/module/storage/large_capacity, /obj/item/mod/module/welding, /obj/item/mod/module/flashlight, /obj/item/mod/module/jetpack, /obj/item/mod/module/holster)
+
+/obj/item/mod/control/pre_equipped/magnate
+ theme = /datum/mod_theme/magnate
+ cell = /obj/item/stock_parts/cell/hyper
+ initial_modules = list(/obj/item/mod/module/storage/large_capacity, /obj/item/mod/module/welding, /obj/item/mod/module/holster, /obj/item/mod/module/pathfinder)
+
+/obj/item/mod/control/pre_equipped/traitor
+ theme = /datum/mod_theme/syndicate
+ cell = /obj/item/stock_parts/cell/super
+ initial_modules = list(/obj/item/mod/module/storage/syndicate, /obj/item/mod/module/welding, /obj/item/mod/module/tether, /obj/item/mod/module/pathfinder, /obj/item/mod/module/flashlight, /obj/item/mod/module/dna_lock)
+
+/obj/item/mod/control/pre_equipped/nuclear
+ theme = /datum/mod_theme/syndicate
+ cell = /obj/item/stock_parts/cell/hyper
+ initial_modules = list(/obj/item/mod/module/storage/syndicate, /obj/item/mod/module/welding, /obj/item/mod/module/jetpack, /obj/item/mod/module/visor/thermal, /obj/item/mod/module/flashlight, /obj/item/mod/module/holster)
+
+/obj/item/mod/control/pre_equipped/elite
+ theme = /datum/mod_theme/elite
+ cell = /obj/item/stock_parts/cell/bluespace
+ initial_modules = list(/obj/item/mod/module/storage/syndicate, /obj/item/mod/module/welding, /obj/item/mod/module/emp_shield, /obj/item/mod/module/jetpack, /obj/item/mod/module/visor/thermal, /obj/item/mod/module/flashlight, /obj/item/mod/module/holster)
+
+/obj/item/mod/control/pre_equipped/enchanted
+ theme = /datum/mod_theme/enchanted
+ cell = /obj/item/stock_parts/cell/crystal_cell/wizard
+ initial_modules = list(/obj/item/mod/module/storage/large_capacity, /obj/item/mod/module/energy_shield/wizard, /obj/item/mod/module/emp_shield)
+
+/obj/item/mod/control/pre_equipped/prototype
+ theme = /datum/mod_theme/prototype
+ cell = /obj/item/stock_parts/cell/high/plus
+ initial_modules = list(/obj/item/mod/module/storage, /obj/item/mod/module/welding, /obj/item/mod/module/rad_protection, /obj/item/mod/module/flashlight, /obj/item/mod/module/tether)
+
+/obj/item/mod/control/pre_equipped/responsory
+ theme = /datum/mod_theme/responsory
+ cell = /obj/item/stock_parts/cell/hyper
+ initial_modules = list(/obj/item/mod/module/storage/large_capacity, /obj/item/mod/module/welding, /obj/item/mod/module/emp_shield, /obj/item/mod/module/flashlight, /obj/item/mod/module/holster)
+ var/insignia_type = /obj/item/mod/module/insignia
+
+/obj/item/mod/control/pre_equipped/responsory/Initialize(mapload, new_theme, new_skin)
+ initial_modules.Insert(1, insignia_type)
+ return ..()
+
+/obj/item/mod/control/pre_equipped/responsory/commander
+ insignia_type = /obj/item/mod/module/insignia/commander
+
+/obj/item/mod/control/pre_equipped/responsory/security
+ insignia_type = /obj/item/mod/module/insignia/security
+
+/obj/item/mod/control/pre_equipped/responsory/engineer
+ insignia_type = /obj/item/mod/module/insignia/engineer
+
+/obj/item/mod/control/pre_equipped/responsory/medic
+ insignia_type = /obj/item/mod/module/insignia/medic
+
+/obj/item/mod/control/pre_equipped/responsory/janitor
+ insignia_type = /obj/item/mod/module/insignia/janitor
+
+/obj/item/mod/control/pre_equipped/responsory/clown
+ insignia_type = /obj/item/mod/module/insignia/clown
+
+/obj/item/mod/control/pre_equipped/responsory/chaplain
+ insignia_type = /obj/item/mod/module/insignia/chaplain
+
+/obj/item/mod/control/pre_equipped/responsory/inquisitory
+ initial_modules = list(/obj/item/mod/module/storage/large_capacity, /obj/item/mod/module/anti_magic, /obj/item/mod/module/welding, /obj/item/mod/module/emp_shield, /obj/item/mod/module/flashlight, /obj/item/mod/module/holster)
+ applied_skin = "inquisitory"
+
+/obj/item/mod/control/pre_equipped/responsory/inquisitory/commander
+ insignia_type = /obj/item/mod/module/insignia/commander
+
+/obj/item/mod/control/pre_equipped/responsory/inquisitory/security
+ insignia_type = /obj/item/mod/module/insignia/security
+
+/obj/item/mod/control/pre_equipped/responsory/inquisitory/medic
+ insignia_type = /obj/item/mod/module/insignia/medic
+
+/obj/item/mod/control/pre_equipped/responsory/inquisitory/chaplain
+ insignia_type = /obj/item/mod/module/insignia/chaplain
+
+/obj/item/mod/control/pre_equipped/apocryphal
+ theme = /datum/mod_theme/apocryphal
+ cell = /obj/item/stock_parts/cell/bluespace
+ initial_modules = list(/obj/item/mod/module/storage/bluespace, /obj/item/mod/module/welding, /obj/item/mod/module/emp_shield, /obj/item/mod/module/jetpack, /obj/item/mod/module/holster)
+
+/obj/item/mod/control/pre_equipped/corporate
+ theme = /datum/mod_theme/corporate
+ cell = /obj/item/stock_parts/cell/bluespace
+ initial_modules = list(/obj/item/mod/module/storage/bluespace, /obj/item/mod/module/holster)
+
+/obj/item/mod/control/pre_equipped/debug
+ theme = /datum/mod_theme/debug
+ cell = /obj/item/stock_parts/cell/bluespace
+ initial_modules = list(/obj/item/mod/module/storage/bluespace, /obj/item/mod/module/welding, /obj/item/mod/module/flashlight, /obj/item/mod/module/bikehorn, /obj/item/mod/module/rad_protection, /obj/item/mod/module/tether, /obj/item/mod/module/injector) //one of every type of module, for testing if they all work correctly
+
+/obj/item/mod/control/pre_equipped/administrative
+ theme = /datum/mod_theme/administrative
+ cell = /obj/item/stock_parts/cell/infinite/abductor
+ initial_modules = list(/obj/item/mod/module/storage/bluespace, /obj/item/mod/module/welding, /obj/item/mod/module/stealth/ninja, /obj/item/mod/module/quick_carry/advanced, /obj/item/mod/module/magboot/advanced, /obj/item/mod/module/jetpack)
+
+//these exist for the prefs menu
+/obj/item/mod/control/pre_equipped/syndicate_empty
+ theme = /datum/mod_theme/syndicate
+
+/obj/item/mod/control/pre_equipped/syndicate_empty/elite
+ theme = /datum/mod_theme/elite
+
+INITIALIZE_IMMEDIATE(/obj/item/mod/control/pre_equipped/syndicate_empty)
diff --git a/code/modules/mod/mod_ui.dm b/code/modules/mod/mod_ui.dm
new file mode 100644
index 0000000000..1e719e93eb
--- /dev/null
+++ b/code/modules/mod/mod_ui.dm
@@ -0,0 +1,80 @@
+/obj/item/mod/control/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "MODsuit", name)
+ ui.open()
+
+/obj/item/mod/control/ui_data()
+ var/data = list()
+ data["interface_break"] = interface_break
+ data["malfunctioning"] = malfunctioning
+ data["open"] = open
+ data["active"] = active
+ data["locked"] = locked
+ data["complexity"] = complexity
+ data["selected_module"] = selected_module?.name
+ data["wearer_name"] = wearer ? (wearer.get_authentification_name("Unknown") || "Unknown") : "No Occupant"
+ data["wearer_job"] = wearer ? wearer.get_assignment("Unknown", "Unknown", FALSE) : "No Job"
+ data["AI"] = ai?.name
+ data["cell"] = cell?.name
+ data["charge"] = cell ? round(cell.percent(), 1) : 0
+ data["modules"] = list()
+ for(var/obj/item/mod/module/module as anything in modules)
+ var/list/module_data = list(
+ name = module.name,
+ description = module.desc,
+ module_type = module.module_type,
+ active = module.active,
+ idle_power = module.idle_power_cost,
+ active_power = module.active_power_cost,
+ use_power = module.use_power_cost,
+ complexity = module.complexity,
+ cooldown_time = module.cooldown_time,
+ cooldown = round(COOLDOWN_TIMELEFT(module, cooldown_timer), 1 SECONDS),
+ id = module.tgui_id,
+ ref = REF(module),
+ configuration_data = module.get_configuration()
+ )
+ module_data += module.add_ui_data()
+ data["modules"] += list(module_data)
+ return data
+
+/obj/item/mod/control/ui_static_data(mob/user)
+ var/data = list()
+ data["ui_theme"] = ui_theme
+ data["control"] = name
+ data["complexity_max"] = complexity_max
+ data["helmet"] = helmet?.name
+ data["chestplate"] = chestplate?.name
+ data["gauntlets"] = gauntlets?.name
+ data["boots"] = boots?.name
+ return data
+
+/obj/item/mod/control/ui_act(action, params)
+ . = ..()
+ if(.)
+ return
+ if(!allowed(usr) && locked)
+ balloon_alert(usr, "insufficient access!")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return
+ if(malfunctioning && prob(75))
+ balloon_alert(usr, "button malfunctions!")
+ return
+ switch(action)
+ if("lock")
+ locked = !locked
+ balloon_alert(usr, "[locked ? "locked" : "unlocked"]!")
+ if("activate")
+ toggle_activate(usr)
+ if("select")
+ var/obj/item/mod/module/module = locate(params["ref"]) in modules
+ if(!module)
+ return
+ module.on_select()
+ if("configure")
+ var/obj/item/mod/module/module = locate(params["ref"]) in modules
+ if(!module)
+ return
+ module.configure_edit(params["key"], params["value"])
+ return TRUE
diff --git a/code/modules/mod/modules/_module.dm b/code/modules/mod/modules/_module.dm
new file mode 100644
index 0000000000..a310f95537
--- /dev/null
+++ b/code/modules/mod/modules/_module.dm
@@ -0,0 +1,252 @@
+/obj/item/mod/module
+ name = "MOD module"
+ 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_CELL_DRAIN * 0
+ /// Power use when active
+ var/active_power_cost = DEFAULT_CELL_DRAIN * 0
+ /// Power use when used, we call it manually
+ var/use_power_cost = DEFAULT_CELL_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
+ /// 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
+ /// 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_PARENT_PREQDELETED, .proc/on_device_deletion)
+ RegisterSignal(src, COMSIG_ATOM_EXITED, .proc/on_exit)
+
+/obj/item/mod/module/Destroy()
+ mod?.uninstall(src)
+ if(device)
+ UnregisterSignal(device, COMSIG_PARENT_PREQDELETED)
+ QDEL_NULL(device)
+ return ..()
+
+/obj/item/mod/module/examine(mob/user)
+ . = ..()
+ if(HAS_TRAIT(user, TRAIT_DIAGNOSTIC_HUD))
+ . += span_notice("Complexity level: [complexity]")
+
+/// 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()
+ 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()
+ 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
+
+/// Called when the module is selected from the TGUI
+/obj/item/mod/module/proc/on_select()
+ if(!mod.active || mod.activating || module_type == MODULE_PASSIVE)
+ return
+ if(module_type != MODULE_USABLE)
+ if(active)
+ on_deactivation()
+ else
+ on_activation()
+ else
+ on_use()
+ SEND_SIGNAL(mod, COMSIG_MOD_MODULE_SELECTED)
+
+/// 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 || !mod.cell?.charge)
+ balloon_alert(mod.wearer, "unpowered!")
+ return FALSE
+ if(module_type == MODULE_ACTIVE)
+ if(mod.selected_module && !mod.selected_module.on_deactivation())
+ return
+ 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/on_exit)
+ else
+ balloon_alert(mod.wearer, "can't extend [device]!")
+ return
+ 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
+ COOLDOWN_START(src, cooldown_timer, cooldown_time)
+ mod.wearer.update_inv_back()
+ return TRUE
+
+/// Called when the module is deactivated
+/obj/item/mod/module/proc/on_deactivation()
+ active = FALSE
+ if(module_type == MODULE_ACTIVE)
+ mod.selected_module = null
+ if(device)
+ mod.wearer.transferItemToLoc(device, src, TRUE)
+ balloon_alert(mod.wearer, "[device] retracted")
+ UnregisterSignal(mod.wearer, COMSIG_ATOM_EXITED)
+ else
+ balloon_alert(mod.wearer, "[src] deactivated")
+ UnregisterSignal(mod.wearer, used_signal)
+ used_signal = null
+ mod.wearer.update_inv_back()
+ return TRUE
+
+/// Called when the module is used
+/obj/item/mod/module/proc/on_use()
+ if(!COOLDOWN_FINISHED(src, cooldown_timer))
+ return FALSE
+ if(!check_power(use_power_cost))
+ return FALSE
+ COOLDOWN_START(src, cooldown_timer, cooldown_time)
+ addtimer(CALLBACK(mod.wearer, /mob.proc/update_inv_back), cooldown_time)
+ mod.wearer.update_inv_back()
+ return TRUE
+
+/// Called when an activated module without a device is used
+/obj/item/mod/module/proc/on_select_use(atom/target)
+ 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(delta_time)
+ if(active)
+ if(!drain_power(active_power_cost * delta_time))
+ on_deactivation()
+ return FALSE
+ on_active_process(delta_time)
+ else
+ drain_power(idle_power_cost * delta_time)
+ return TRUE
+
+/// Called on the MODsuit's process if it is an active module
+/obj/item/mod/module/proc/on_active_process(delta_time)
+ return
+
+/// Drains power from the suit cell
+/obj/item/mod/module/proc/drain_power(amount)
+ if(!check_power(amount))
+ return FALSE
+ mod.cell.charge = max(0, mod.cell.charge - amount)
+ return TRUE
+
+/obj/item/mod/module/proc/check_power(amount)
+ if(!mod.cell || (mod.cell.charge < amount))
+ return FALSE
+ return TRUE
+
+/// 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()
+
+/// 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)
+
+/// Generates an icon to be used for the suit's worn overlays
+/obj/item/mod/module/proc/generate_worn_overlay(mutable_appearance/standing)
+ . = list()
+ 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
+ var/mutable_appearance/module_icon = mutable_appearance('icons/mob/mod.dmi', used_overlay, layer = standing.layer + 0.1)
+ . += module_icon
+
+/// 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, /obj/item/mod/module.proc/on_special_click)
diff --git a/code/modules/mod/modules/modules.dm b/code/modules/mod/modules/modules.dm
new file mode 100644
index 0000000000..b5fc9a41e9
--- /dev/null
+++ b/code/modules/mod/modules/modules.dm
@@ -0,0 +1,352 @@
+/obj/item/mod/module
+ name = "MOD module"
+ 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_CELL_DRAIN * 0
+ /// Power use when active
+ var/active_power_cost = DEFAULT_CELL_DRAIN * 0
+ /// Power use when used, we call it manually
+ var/use_power_cost = DEFAULT_CELL_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
+ /// 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
+ /// 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_PARENT_PREQDELETED, .proc/on_device_deletion)
+ RegisterSignal(src, COMSIG_ATOM_EXITED, .proc/on_exit)
+
+/obj/item/mod/module/Destroy()
+ mod?.uninstall(src)
+ if(device)
+ UnregisterSignal(device, COMSIG_PARENT_PREQDELETED)
+ QDEL_NULL(device)
+ return ..()
+
+/obj/item/mod/module/examine(mob/user)
+ . = ..()
+ if(HAS_TRAIT(user, TRAIT_DIAGNOSTIC_HUD))
+ . += span_notice("Complexity level: [complexity]")
+
+/// 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()
+ 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()
+ 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
+
+/// Called when the module is selected from the TGUI
+/obj/item/mod/module/proc/on_select()
+ if(!mod.active || mod.activating || module_type == MODULE_PASSIVE)
+ return
+ if(module_type != MODULE_USABLE)
+ if(active)
+ on_deactivation()
+ else
+ on_activation()
+ else
+ on_use()
+ SEND_SIGNAL(mod, COMSIG_MOD_MODULE_SELECTED)
+
+/// 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 || !mod.cell?.charge)
+ balloon_alert(mod.wearer, "unpowered!")
+ return FALSE
+ if(module_type == MODULE_ACTIVE)
+ if(mod.selected_module && !mod.selected_module.on_deactivation())
+ return
+ 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/on_exit)
+ else
+ balloon_alert(mod.wearer, "can't extend [device]!")
+ return
+ 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
+ COOLDOWN_START(src, cooldown_timer, cooldown_time)
+ mod.wearer.update_inv_back()
+ return TRUE
+
+/// Called when the module is deactivated
+/obj/item/mod/module/proc/on_deactivation()
+ active = FALSE
+ if(module_type == MODULE_ACTIVE)
+ mod.selected_module = null
+ if(device)
+ mod.wearer.transferItemToLoc(device, src, TRUE)
+ balloon_alert(mod.wearer, "[device] retracted")
+ UnregisterSignal(mod.wearer, COMSIG_ATOM_EXITED)
+ else
+ balloon_alert(mod.wearer, "[src] deactivated")
+ UnregisterSignal(mod.wearer, used_signal)
+ used_signal = null
+ mod.wearer.update_inv_back()
+ return TRUE
+
+/// Called when the module is used
+/obj/item/mod/module/proc/on_use()
+ if(!COOLDOWN_FINISHED(src, cooldown_timer))
+ return FALSE
+ if(!check_power(use_power_cost))
+ return FALSE
+ COOLDOWN_START(src, cooldown_timer, cooldown_time)
+ addtimer(CALLBACK(mod.wearer, /mob.proc/update_inv_back), cooldown_time)
+ mod.wearer.update_inv_back()
+ return TRUE
+
+/// Called when an activated module without a device is used
+/obj/item/mod/module/proc/on_select_use(atom/target)
+ 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(delta_time)
+ if(active)
+ if(!drain_power(active_power_cost * delta_time))
+ on_deactivation()
+ return FALSE
+ on_active_process(delta_time)
+ else
+ drain_power(idle_power_cost * delta_time)
+ return TRUE
+
+/// Called on the MODsuit's process if it is an active module
+/obj/item/mod/module/proc/on_active_process(delta_time)
+ return
+
+/// Drains power from the suit cell
+/obj/item/mod/module/proc/drain_power(amount)
+ if(!check_power(amount))
+ return FALSE
+ mod.cell.charge = max(0, mod.cell.charge - amount)
+ return TRUE
+
+/obj/item/mod/module/proc/check_power(amount)
+ if(!mod.cell || (mod.cell.charge < amount))
+ return FALSE
+ return TRUE
+
+/// 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()
+
+/// 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)
+
+/// Generates an icon to be used for the suit's worn overlays
+/obj/item/mod/module/proc/generate_worn_overlay(mutable_appearance/standing)
+ . = list()
+ 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
+ var/mutable_appearance/module_icon = mutable_appearance('icons/mob/mod.dmi', used_overlay, layer = standing.layer + 0.1)
+ . += module_icon
+
+/// 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, /obj/item/mod/module.proc/on_special_click)
+ icon_state = "magic_nullifier"
+ removable = FALSE
+ incompatible_modules = list(/obj/item/mod/module/anti_magic)
+
+/obj/item/mod/module/anti_magic/on_suit_activation()
+ ADD_TRAIT(mod.wearer, TRAIT_ANTIMAGIC, MOD_TRAIT)
+ ADD_TRAIT(mod.wearer, TRAIT_HOLY, MOD_TRAIT)
+
+/obj/item/mod/module/anti_magic/on_suit_deactivation()
+ REMOVE_TRAIT(mod.wearer, TRAIT_ANTIMAGIC, MOD_TRAIT)
+ REMOVE_TRAIT(mod.wearer, TRAIT_HOLY, MOD_TRAIT)
+
+/obj/item/mod/module/anti_magic/wizard
+ name = "MOD magic neutralizer module"
+ desc = "The caster wielding this spell gains an invisible barrier around them, channeling arcane power through \
+ specialized runes engraved onto the surface of the suit to generate anti-magic field. \
+ The field will neutralize all magic that comes into contact with the user. \
+ It will not protect the caster from social ridicule."
+ icon_state = "magic_neutralizer"
+
+/obj/item/mod/module/anti_magic/wizard/on_suit_activation()
+ ADD_TRAIT(mod.wearer, TRAIT_ANTIMAGIC_NO_SELFBLOCK, MOD_TRAIT)
+
+/obj/item/mod/module/anti_magic/wizard/on_suit_deactivation()
+ REMOVE_TRAIT(mod.wearer, TRAIT_ANTIMAGIC_NO_SELFBLOCK, MOD_TRAIT)
+
+/obj/item/mod/module/kinesis //TODO POST-MERGE MAKE NOT SUCK ASS, MAKE BALLER AS FUCK
+ 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
+ module_type = MODULE_TOGGLE
+// complexity = 3
+ complexity = 0
+ active_power_cost = DEFAULT_CELL_DRAIN*0.75
+// use_power_cost = DEFAULT_CELL_DRAIN*3
+ removable = FALSE
+ incompatible_modules = list(/obj/item/mod/module/kinesis)
+ cooldown_time = 0.5 SECONDS
+ var/has_tk = FALSE
+
+/obj/item/mod/module/kinesis/on_activation()
+ . = ..()
+ if(!.)
+ return
+ if(mod.wearer.dna.check_mutation(TK))
+ has_tk = TRUE
+ else
+ mod.wearer.dna.add_mutation(TK)
+
+/obj/item/mod/module/kinesis/on_deactivation()
+ . = ..()
+ if(!.)
+ return
+ if(has_tk)
+ has_tk = FALSE
+ return
+ mod.wearer.dna.remove_mutation(TK)
+
+/obj/item/mod/module/insignia
+ name = "MOD insignia module"
+ desc = "Despite the existence of IFF systems, radio communique, and modern methods of deductive reasoning involving \
+ the wearer's own eyes, colorful paint jobs remain a popular way for different factions in the galaxy to display who \
+ they are. This system utilizes a series of tiny moving paint sprayers to both apply and remove different \
+ color patterns to and from the suit."
+ icon_state = "insignia"
+ removable = FALSE
+ incompatible_modules = list(/obj/item/mod/module/insignia)
+ overlay_state_inactive = "insignia"
+
+/obj/item/mod/module/insignia/generate_worn_overlay(mutable_appearance/standing)
+ overlay_state_inactive = "[initial(overlay_state_inactive)]-[mod.skin]"
+ . = ..()
+ for(var/mutable_appearance/appearance as anything in .)
+ appearance.color = color
+
+/obj/item/mod/module/insignia/commander
+ color = "#4980a5"
+
+/obj/item/mod/module/insignia/security
+ color = "#b30d1e"
+
+/obj/item/mod/module/insignia/engineer
+ color = "#e9c80e"
+
+/obj/item/mod/module/insignia/medic
+ color = "#ebebf5"
+
+/obj/item/mod/module/insignia/janitor
+ color = "#7925c7"
+
+/obj/item/mod/module/insignia/clown
+ color = "#ff1fc7"
+
+/obj/item/mod/module/insignia/chaplain
+ color = "#f0a00c"
diff --git a/tgstation.dme b/tgstation.dme
index 1ced56efb1..5efc223582 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -506,7 +506,6 @@
#include "code\datums\achievements\misc_scores.dm"
#include "code\datums\achievements\skill_achievements.dm"
#include "code\datums\actions\beam_rifle.dm"
-#include "code\datums\ai\objects\mod.dm"
#include "code\datums\announcers\_announcer.dm"
#include "code\datums\announcers\default_announcer.dm"
#include "code\datums\announcers\intern_announcer.dm"
@@ -2023,6 +2022,7 @@
#include "code\modules\client\preferences_savefile.dm"
#include "code\modules\client\preferences_toggles.dm"
#include "code\modules\client\preferences_vr.dm"
+#include "code\modules\client\preferences\mod_select.dm"
#include "code\modules\client\verbs\aooc.dm"
#include "code\modules\client\verbs\autobunker.dm"
#include "code\modules\client\verbs\etips.dm"
@@ -2958,6 +2958,18 @@
#include "code\modules\mob\living\simple_animal\slime\slime.dm"
#include "code\modules\mob\living\simple_animal\slime\slime_mobility.dm"
#include "code\modules\mob\living\simple_animal\slime\subtypes.dm"
+#include "code\modules\mod\mod_actions.dm"
+#include "code\modules\mod\mod_activation.dm"
+#include "code\modules\mod\mod_ai.dm"
+#include "code\modules\mod\mod_clothes.dm"
+#include "code\modules\mod\mod_construction.dm"
+#include "code\modules\mod\mod_construction.dm"
+#include "code\modules\mod\mod_control.dm"
+#include "code\modules\mod\mod_theme.dm"
+#include "code\modules\mod\mod_types.dm"
+#include "code\modules\mod\mod_ui.dm"
+#include "code\modules\mod\modules\_module.dm"
+#include "code\modules\mod\modules\modules.dm"
#include "code\modules\modular_computers\laptop_vendor.dm"
#include "code\modules\modular_computers\computers\_modular_computer_shared.dm"
#include "code\modules\modular_computers\computers\item\computer.dm"