diff --git a/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_plasma_facility.dmm b/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_plasma_facility.dmm
index 0bfcc133f0c98..e5f7305eba078 100644
--- a/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_plasma_facility.dmm
+++ b/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_plasma_facility.dmm
@@ -338,7 +338,7 @@
/obj/structure/railing/corner{
dir = 4
},
-/mob/living/simple_animal/hostile/mimic/crate,
+/mob/living/basic/mimic/crate,
/turf/open/floor/plating/snowed/icemoon,
/area/icemoon/underground/explored)
"fO" = (
@@ -2073,7 +2073,7 @@
/turf/open/floor/plating/snowed/smoothed/icemoon,
/area/icemoon/underground/explored)
"KV" = (
-/mob/living/simple_animal/hostile/mimic/crate,
+/mob/living/basic/mimic/crate,
/turf/open/floor/plating/snowed/icemoon,
/area/icemoon/underground/explored)
"KY" = (
diff --git a/_maps/map_files/Birdshot/birdshot.dmm b/_maps/map_files/Birdshot/birdshot.dmm
index ff06bb5914c6b..758a5c877925e 100644
--- a/_maps/map_files/Birdshot/birdshot.dmm
+++ b/_maps/map_files/Birdshot/birdshot.dmm
@@ -26116,7 +26116,7 @@
/turf/open/floor/iron/smooth_large,
/area/station/science/robotics/mechbay)
"iPU" = (
-/mob/living/simple_animal/hostile/mimic/crate,
+/mob/living/basic/mimic/crate,
/turf/open/floor/plating,
/area/station/maintenance/fore/lesser)
"iPW" = (
diff --git a/_maps/map_files/wawastation/wawastation.dmm b/_maps/map_files/wawastation/wawastation.dmm
index 0295a23f50395..64cad41bbebb1 100644
--- a/_maps/map_files/wawastation/wawastation.dmm
+++ b/_maps/map_files/wawastation/wawastation.dmm
@@ -840,7 +840,7 @@
/obj/effect/decal/cleanable/blood/old{
icon_state = "gib3-old"
},
-/mob/living/simple_animal/hostile/mimic,
+/mob/living/basic/mimic/crate,
/turf/open/floor/iron/white,
/area/station/maintenance/aft/upper)
"anu" = (
diff --git a/_maps/virtual_domains/psyker_shuffle.dmm b/_maps/virtual_domains/psyker_shuffle.dmm
index c744cecf0b430..5f303d4ae0471 100644
--- a/_maps/virtual_domains/psyker_shuffle.dmm
+++ b/_maps/virtual_domains/psyker_shuffle.dmm
@@ -64,11 +64,7 @@
/turf/template_noop,
/area/template_noop)
"r" = (
-/mob/living/simple_animal/hostile/mimic,
-/turf/open/indestructible/dark,
-/area/virtual_domain)
-"s" = (
-/mob/living/simple_animal/hostile/mimic/crate,
+/mob/living/basic/mimic/crate,
/turf/open/indestructible/dark,
/area/virtual_domain)
"t" = (
@@ -767,7 +763,7 @@ Y
Q
Q
Q
-s
+r
M
Q
Q
@@ -878,11 +874,11 @@ o
Y
a
Q
-s
+r
Y
Y
Y
-s
+r
Q
Q
Q
diff --git a/code/__DEFINES/ai/monsters.dm b/code/__DEFINES/ai/monsters.dm
index d77817a203980..6a1433fa5ec86 100644
--- a/code/__DEFINES/ai/monsters.dm
+++ b/code/__DEFINES/ai/monsters.dm
@@ -312,3 +312,5 @@
#define BB_TURTLE_HEADBUTT_VICTIM "turtle_headbutt_victim"
///flore we must smell
#define BB_TURTLE_FLORA_TARGET "turtle_flora_target"
+
+#define BB_GUNMIMIC_GUN_EMPTY "BB_GUNMIMIC_GUN_EMPTY"
diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm
index 2044aab51e24d..4d9c6e9c00bf9 100644
--- a/code/__DEFINES/dcs/signals/signals_object.dm
+++ b/code/__DEFINES/dcs/signals/signals_object.dm
@@ -558,19 +558,29 @@
#define COMSIG_ASSEMBLY_DETACHED "assembly_detached"
/*
- * The following two signals are separate from the above two because buttons don't set the holder of the inserted assembly.
+ * The following four signals are separate from the above two because buttons and pressure plates don't set the holder of the inserted assembly.
* This causes subtle behavioral differences that future handlers for these signals may need to account for,
* even if none of the currently implemented handlers do.
*/
-/// Sent from /obj/machinery/button/assembly_act(obj/machinery/button/button, mob/user)
+/// Sent when an assembly is added to a button : (obj/machinery/button/button, mob/user)
#define COMSIG_ASSEMBLY_ADDED_TO_BUTTON "assembly_added_to_button"
-/// Sent from /obj/machinery/button/remove_assembly(obj/machinery/button/button, mob/user)
+/// Sent when an assembly is removed from a button : (obj/machinery/button/button, mob/user)
#define COMSIG_ASSEMBLY_REMOVED_FROM_BUTTON "assembly_removed_from_button"
+/// Sent when an assembly is added to a pressure plate : (obj/item/pressureplate/pressure_plate, mob/user)
+#define COMSIG_ASSEMBLY_ADDED_TO_PRESSURE_PLATE "assembly_added_to_pressure_plate"
+
+/// Sent when an assembly is removed from a pressure plate : (obj/item/pressureplate/pressure_plate, mob/user)
+#define COMSIG_ASSEMBLY_REMOVED_FROM_PRESSURE_PLATE "assembly_removed_from_pressure_playe"
+
/// Sent from /datum/powernet/add_cable()
#define COMSIG_CABLE_ADDED_TO_POWERNET "cable_added_to_powernet"
/// Sent from /datum/powernet/remove_cable()
#define COMSIG_CABLE_REMOVED_FROM_POWERNET "cable_removed_from_powernet"
+
+/// Sent from /datum/wires/attach_assembly() : (atom/holder)
+#define COMSIG_ASSEMBLY_PRE_ATTACH "assembly_pre_attach"
+ #define COMPONENT_CANCEL_ATTACH (1<<0)
diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index dc3d6fa0c1d70..5d35a7fcb8718 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -1424,4 +1424,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Mob doesn't get closed eyelids overlay when it gets knocked out cold or dies
#define TRAIT_NO_EYELIDS "no_eyelids"
+/// Trait applied when the wire bundle component is added to an [/obj/item/integrated_circuit]
+#define TRAIT_COMPONENT_WIRE_BUNDLE "component_wire_bundle"
+
// END TRAIT DEFINES
diff --git a/code/__DEFINES/wires.dm b/code/__DEFINES/wires.dm
index 4926996f26bd3..034bb8897e6e6 100644
--- a/code/__DEFINES/wires.dm
+++ b/code/__DEFINES/wires.dm
@@ -2,6 +2,30 @@
#define COMSIG_CUT_WIRE(wire) "cut_wire [wire]"
#define COMSIG_MEND_WIRE(wire) "mend_wire [wire]"
+/// from base of /datum/wires/proc/on_pulse : (wire, mob/user)
+#define COMSIG_PULSE_WIRE "pulse_wire"
+
+// Directionality of wire pulses
+
+/// The wires interact with their holder when pulsed
+#define WIRES_INPUT (1<<0)
+/// The wires have a reason to toggle whether attached assemblies are armed
+#define WIRES_TOGGLE_ARMED (1<<1)
+/// The wires only want to activate assemblies that do something other than (dis)arming themselves
+#define WIRES_FUNCTIONAL_OUTPUT (1<<2)
+/// The holder can both pulse its wires and be affected by its wires getting pulsed
+#define WIRES_ALL (WIRES_INPUT | WIRES_TOGGLE_ARMED | WIRES_FUNCTIONAL_OUTPUT)
+
+/// The assembly can pulse a wire it is attached to
+#define ASSEMBLY_INPUT (1<<0)
+/// The assembly toggles whether it will pulse the attached wire when it is pulsed by the attached wire
+#define ASSEMBLY_TOGGLE_ARMED (1<<1)
+/// The assembly does something other than just (dis)arming itself when it is pulsed by the wire it is attached to
+#define ASSEMBLY_FUNCTIONAL_OUTPUT (1<<2)
+/// The assembly can both pulse the wire it is attached to, and (dis)arms itself when pulsed by the wire
+#define ASSEMBLY_TOGGLEABLE_INPUT (ASSEMBLY_INPUT | ASSEMBLY_TOGGLE_ARMED)
+#define ASSEMBLY_ALL (ASSEMBLY_TOGGLEABLE_INPUT | ASSEMBLY_FUNCTIONAL_OUTPUT)
+
//retvals for attempt_wires_interaction
#define WIRE_INTERACTION_FAIL 0
#define WIRE_INTERACTION_SUCCESSFUL 1
@@ -72,3 +96,5 @@
#define AI_WIRE_DISABLED 1
#define AI_WIRE_HACKED 2
#define AI_WIRE_DISABLED_HACKED -1
+
+#define MAX_WIRE_COUNT 17
diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm
index 5ece846e84de3..6024f7991388d 100644
--- a/code/_globalvars/lists/mobs.dm
+++ b/code/_globalvars/lists/mobs.dm
@@ -29,7 +29,7 @@ GLOBAL_LIST_INIT(abstract_mob_types, list(
/mob/living/simple_animal/hostile/asteroid/elite,
/mob/living/simple_animal/hostile/asteroid,
/mob/living/simple_animal/hostile/megafauna,
- /mob/living/simple_animal/hostile/mimic, // Cannot exist if spawned without being passed an item reference
+ /mob/living/basic/mimic, // Cannot exist if spawned without being passed an item reference
/mob/living/simple_animal/hostile/retaliate,
/mob/living/simple_animal/hostile,
/mob/living/simple_animal/soulscythe, // As mimic, can't exist if spawned outside an item
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index 4edc46bb50303..e351e155dc170 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -687,6 +687,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_CIRCUIT_UI_OPEN" = TRAIT_CIRCUIT_UI_OPEN,
"TRAIT_CIRCUIT_UNDUPABLE" = TRAIT_CIRCUIT_UNDUPABLE,
"TRAIT_COMPONENT_MMI" = TRAIT_COMPONENT_MMI,
+ "TRAIT_COMPONENT_WIRE_BUNDLE" = TRAIT_COMPONENT_WIRE_BUNDLE,
),
/obj/item/modular_computer = list(
"TRAIT_MODPC_HALVED_DOWNLOAD_SPEED" = TRAIT_MODPC_HALVED_DOWNLOAD_SPEED,
diff --git a/code/datums/components/grillable.dm b/code/datums/components/grillable.dm
index f584808a1f3bd..3b3ef047c833f 100644
--- a/code/datums/components/grillable.dm
+++ b/code/datums/components/grillable.dm
@@ -1,3 +1,5 @@
+#define IDEAL_GRILLING_TEMPERATURE 200 + T0C
+
/datum/component/grillable
dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS // So you can change grill results with various cookstuffs
///Result atom type of grilling this object
@@ -14,6 +16,12 @@
var/who_placed_us
/// Reagents that should be added to the result
var/list/added_reagents
+ /// Open turf we were last placed on, to check temperature
+ var/turf/open/listening_turf
+ /// Are we grilling right now?
+ var/is_grilling = FALSE
+ /// What's our current air temperature?
+ var/current_temperature = 0
/datum/component/grillable/Initialize(cook_result, required_cook_time, positive_result, use_large_steam_sprite, list/added_reagents)
. = ..()
@@ -32,14 +40,20 @@
RegisterSignal(parent, COMSIG_ITEM_GRILL_TURNED_OFF, PROC_REF(on_grill_turned_off))
RegisterSignal(parent, COMSIG_ITEM_GRILL_PROCESS, PROC_REF(on_grill))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_location_changed))
+ on_location_changed(parent)
/datum/component/grillable/UnregisterFromParent()
+ if (listening_turf)
+ UnregisterSignal(listening_turf, COMSIG_TURF_EXPOSE)
+
UnregisterSignal(parent, list(
COMSIG_ATOM_EXAMINE,
COMSIG_ITEM_GRILL_TURNED_ON,
COMSIG_ITEM_GRILL_TURNED_OFF,
COMSIG_ITEM_GRILL_PROCESS,
COMSIG_ITEM_GRILL_PLACED,
+ COMSIG_MOVABLE_MOVED
))
// Inherit the new values passed to the component
@@ -55,6 +69,32 @@
if(use_large_steam_sprite)
src.use_large_steam_sprite = use_large_steam_sprite
+/datum/component/grillable/Destroy(force)
+ . = ..()
+ STOP_PROCESSING(SSmachines, src)
+ listening_turf = null
+
+/// Signal proc for [COMSIG_MOVABLE_MOVED], our location has changed and we should register for temperature information
+/datum/component/grillable/proc/on_location_changed(atom/source)
+ SIGNAL_HANDLER
+
+ if (is_grilling)
+ on_grill_turned_off(source)
+ STOP_PROCESSING(SSmachines, src)
+
+ if (listening_turf)
+ UnregisterSignal(listening_turf, COMSIG_TURF_EXPOSE)
+
+ if (isnull(source))
+ return
+
+ var/turf/open/current_turf = source.loc
+ if (!isopenturf(current_turf))
+ return
+ listening_turf = current_turf
+ RegisterSignal(current_turf, COMSIG_TURF_EXPOSE, PROC_REF(on_turf_atmos_changed))
+ on_turf_atmos_changed(current_turf, current_turf.air, current_turf.air?.temperature || 0)
+
/// Signal proc for [COMSIG_ITEM_GRILL_PLACED], item is placed on the grill.
/datum/component/grillable/proc/on_grill_placed(datum/source, mob/griller)
SIGNAL_HANDLER
@@ -62,12 +102,11 @@
if(griller && griller.mind)
who_placed_us = REF(griller.mind)
- RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
-
/// Signal proc for [COMSIG_ITEM_GRILL_TURNED_ON], starts the grilling process.
/datum/component/grillable/proc/on_grill_turned_on(datum/source)
RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(add_grilled_item_overlay))
+ is_grilling = TRUE
var/atom/atom_parent = parent
atom_parent.update_appearance()
@@ -75,6 +114,7 @@
/datum/component/grillable/proc/on_grill_turned_off(datum/source)
UnregisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS)
+ is_grilling = FALSE
var/atom/atom_parent = parent
atom_parent.update_appearance()
@@ -138,15 +178,37 @@
else
examine_list += span_danger("[parent] should probably not be put on the grill.")
-///Ran when an object moves from the grill
-/datum/component/grillable/proc/on_moved(atom/source, atom/OldLoc, Dir, Forced)
+/datum/component/grillable/proc/add_grilled_item_overlay(datum/source, list/overlays)
SIGNAL_HANDLER
- UnregisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS)
- UnregisterSignal(parent, COMSIG_MOVABLE_MOVED)
- source.update_appearance()
+ overlays += mutable_appearance('icons/effects/steam.dmi', "[use_large_steam_sprite ? "steam_triple" : "steam_single"]", ABOVE_OBJ_LAYER)
-/datum/component/grillable/proc/add_grilled_item_overlay(datum/source, list/overlays)
+/// Signal proc for [COMSIG_TURF_EXPOSE], atmosphere might be hot enough for grilling.
+/datum/component/grillable/proc/on_turf_atmos_changed(turf/open/source, datum/gas_mixture/air, exposed_temperature)
SIGNAL_HANDLER
- overlays += mutable_appearance('icons/effects/steam.dmi', "[use_large_steam_sprite ? "steam_triple" : "steam_single"]", ABOVE_OBJ_LAYER)
+ if (!is_grilling)
+ if (exposed_temperature < FIRE_MINIMUM_TEMPERATURE_TO_EXIST)
+ return
+ on_grill_turned_on(source)
+ START_PROCESSING(SSmachines, src)
+ current_temperature = exposed_temperature
+ else
+ if (exposed_temperature >= FIRE_MINIMUM_TEMPERATURE_TO_EXIST)
+ current_temperature = exposed_temperature
+ return
+ on_grill_turned_off(source)
+ STOP_PROCESSING(SSmachines, src)
+
+// Grill while exposed to hot air
+/datum/component/grillable/process(seconds_per_tick)
+ var/atom/atom_parent = parent
+
+ // Grill faster as we approach 200 degrees celsius
+ var/check_temperature = clamp(current_temperature, FIRE_MINIMUM_TEMPERATURE_TO_EXIST, IDEAL_GRILLING_TEMPERATURE)
+ var/temp_scale = (check_temperature - FIRE_MINIMUM_TEMPERATURE_TO_EXIST) / (IDEAL_GRILLING_TEMPERATURE - FIRE_MINIMUM_TEMPERATURE_TO_EXIST)
+ var/speed_modifier = LERP(0.5, 1, temp_scale)
+
+ on_grill(parent, atom_parent.loc, seconds_per_tick * speed_modifier)
+
+#undef IDEAL_GRILLING_TEMPERATURE
diff --git a/code/datums/components/squeak.dm b/code/datums/components/squeak.dm
index ca1bb82ace785..ec900943997f1 100644
--- a/code/datums/components/squeak.dm
+++ b/code/datums/components/squeak.dm
@@ -11,8 +11,8 @@
var/step_delay = 1
// This is to stop squeak spam from inhand usage
- var/last_use = 0
- var/use_delay = 20
+ COOLDOWN_DECLARE(spam_cooldown)
+ var/use_delay = 2 SECONDS
///extra-range for this component's sound
var/sound_extra_range = -1
@@ -102,6 +102,10 @@
return
if(ismob(arrived) && !arrived.density) // Prevents 10 overlapping mice from making an unholy sound while moving
return
+ if(isliving(arrived))
+ var/mob/living/living_arrived = arrived
+ if(living_arrived.mob_size < MOB_SIZE_HUMAN)
+ return
var/atom/current_parent = parent
if(isturf(current_parent?.loc))
play_squeak()
@@ -109,8 +113,8 @@
/datum/component/squeak/proc/use_squeak()
SIGNAL_HANDLER
- if(last_use + use_delay < world.time)
- last_use = world.time
+ if(COOLDOWN_FINISHED(src, spam_cooldown))
+ COOLDOWN_START(src, spam_cooldown, use_delay)
play_squeak()
/datum/component/squeak/proc/on_equip(datum/source, mob/equipper, slot)
diff --git a/code/datums/wires/_wires.dm b/code/datums/wires/_wires.dm
index 73bdc511ee4ba..13b9420d8366a 100644
--- a/code/datums/wires/_wires.dm
+++ b/code/datums/wires/_wires.dm
@@ -7,16 +7,14 @@
if(I.tool_behaviour == TOOL_WIRECUTTER || I.tool_behaviour == TOOL_MULTITOOL)
return TRUE
if(isassembly(I))
- var/obj/item/assembly/A = I
- if(A.attachable)
- return TRUE
+ return TRUE
/atom/proc/attempt_wire_interaction(mob/user)
if(!wires)
return WIRE_INTERACTION_FAIL
if(!user.CanReach(src))
return WIRE_INTERACTION_FAIL
- wires.interact(user)
+ INVOKE_ASYNC(wires, TYPE_PROC_REF(/datum/wires, interact), user)
return WIRE_INTERACTION_BLOCK
/datum/wires
@@ -29,6 +27,9 @@
/// The display name for the wire set shown in station blueprints. Not shown in blueprints if randomize is TRUE or it's an item NT wouldn't know about (Explosives/Nuke). Also used in the hacking interface.
var/proper_name = "Unknown"
+ /// Whether pulsed wires affect the holder, and/or the holder pulses its wires
+ var/wire_behavior = WIRES_INPUT
+
/// List of all wires.
var/list/wires = list()
/// List of cut wires.
@@ -179,6 +180,7 @@
/datum/wires/proc/pulse(wire, user, force=FALSE)
if(!force && is_cut(wire))
return
+ SEND_SIGNAL(src, COMSIG_PULSE_WIRE, wire, user)
on_pulse(wire, user)
/datum/wires/proc/pulse_color(color, mob/living/user, force=FALSE)
@@ -191,7 +193,7 @@
return TRUE
/datum/wires/proc/attach_assembly(color, obj/item/assembly/S)
- if(S && istype(S) && S.attachable && !is_attached(color))
+ if(S && istype(S) && S.assembly_behavior && !is_attached(color) && !(SEND_SIGNAL(S, COMSIG_ASSEMBLY_PRE_ATTACH, holder) & COMPONENT_CANCEL_ATTACH))
assemblies[color] = S
S.forceMove(holder)
S.connected = src
@@ -384,13 +386,13 @@
I = L.get_active_held_item()
if(isassembly(I))
var/obj/item/assembly/A = I
- if(A.attachable)
+ if(A.assembly_behavior & wire_behavior)
if(!L.temporarilyRemoveItemFromInventory(A))
return
if(!attach_assembly(target_wire, A))
A.forceMove(L.drop_location())
. = TRUE
else
- to_chat(L, span_warning("You need an attachable assembly!"))
+ to_chat(L, span_warning("You cannot attach this assembly to these wires!"))
#undef MAXIMUM_EMP_WIRES
diff --git a/code/datums/wires/scanner_gate.dm b/code/datums/wires/scanner_gate.dm
index 14752ec6795ec..cb363d68f95ea 100644
--- a/code/datums/wires/scanner_gate.dm
+++ b/code/datums/wires/scanner_gate.dm
@@ -2,6 +2,7 @@
holder_type = /obj/machinery/scanner_gate
proper_name = "Scanner Gate"
wires = list(WIRE_ACCEPT, WIRE_DENY, WIRE_DISABLE)
+ wire_behavior = WIRES_FUNCTIONAL_OUTPUT
/datum/wires/scanner_gate/on_pulse(wire, user)
. = ..()
diff --git a/code/datums/wires/wire_bundle_component.dm b/code/datums/wires/wire_bundle_component.dm
new file mode 100644
index 0000000000000..5cd4ffa2af620
--- /dev/null
+++ b/code/datums/wires/wire_bundle_component.dm
@@ -0,0 +1,24 @@
+#define CAPACITY_PER_WIRE 10
+
+/datum/wires/wire_bundle_component
+ holder_type = /atom //Anything that can have a shell component, really.
+ randomize = TRUE
+ wire_behavior = WIRES_ALL
+
+/datum/wires/wire_bundle_component/New(atom/holder)
+ var/datum/component/shell/shell_comp = holder.GetComponent(/datum/component/shell)
+ if(!istype(shell_comp))
+ CRASH("Holder does not have a shell component!")
+ var/wire_count = clamp(round(shell_comp.capacity / CAPACITY_PER_WIRE, 1), 1, MAX_WIRE_COUNT)
+ for(var/index in 1 to wire_count)
+ wires += "Port [index]"
+ ..()
+
+/datum/wires/wire_bundle_component/always_reveal_wire(color)
+ return TRUE // Let's not make wiring up this stuff confusing - just give them what wires correspond to what ports.
+
+/datum/wires/wire_bundle_component/ui_data(mob/user)
+ proper_name = holder.name
+ . = ..()
+
+#undef CAPACITY_PER_WIRE
diff --git a/code/game/machinery/buttons.dm b/code/game/machinery/buttons.dm
index c8aba44ef4732..66f1b64795a6b 100644
--- a/code/game/machinery/buttons.dm
+++ b/code/game/machinery/buttons.dm
@@ -140,6 +140,9 @@
if(device)
to_chat(user, span_warning("The button already contains a device!"))
return ITEM_INTERACT_BLOCKING
+ if(!(new_device.assembly_behavior & ASSEMBLY_FUNCTIONAL_OUTPUT))
+ to_chat(user, span_warning("\The [new_device] won't really do anything meaningful inside of the button..."))
+ return ITEM_INTERACT_BLOCKING
if(!user.transferItemToLoc(new_device, src, silent = FALSE))
to_chat(user, span_warning("\The [new_device] is stuck to you!"))
return ITEM_INTERACT_BLOCKING
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index 0d00b4f355a03..b389438f0f9be 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -1840,7 +1840,7 @@
return null
/obj/item/animate_atom_living(mob/living/owner)
- new /mob/living/simple_animal/hostile/mimic/copy(drop_location(), src, owner)
+ new /mob/living/basic/mimic/copy(drop_location(), src, owner)
/**
* Used to update the weight class of the item in a way that other atoms can react to the change.
diff --git a/code/game/objects/items/devices/pressureplates.dm b/code/game/objects/items/devices/pressureplates.dm
index 17f324d109f99..ea27894d829c3 100644
--- a/code/game/objects/items/devices/pressureplates.dm
+++ b/code/game/objects/items/devices/pressureplates.dm
@@ -14,12 +14,12 @@
var/specific_item = null
var/trigger_silent = FALSE
var/sound/trigger_sound = 'sound/effects/pressureplate.ogg'
- var/obj/item/assembly/signaler/sigdev = null
+ var/obj/item/assembly/assembly = null
var/roundstart_signaller = FALSE
var/roundstart_signaller_freq = FREQ_PRESSURE_PLATE
var/roundstart_signaller_code = 30
var/roundstart_hide = FALSE
- var/removable_signaller = TRUE
+ var/removable_assembly = TRUE
var/active = FALSE
var/image/tile_overlay = null
var/can_trigger = TRUE
@@ -31,9 +31,10 @@
. = ..()
tile_overlay = image(icon = 'icons/turf/floors.dmi', icon_state = "pp_overlay")
if(roundstart_signaller)
- sigdev = new
- sigdev.code = roundstart_signaller_code
- sigdev.set_frequency(roundstart_signaller_freq)
+ var/obj/item/assembly/signaler/signaller = new(src)
+ signaller.code = roundstart_signaller_code
+ signaller.set_frequency(roundstart_signaller_freq)
+ assembly = signaller
if(undertile_pressureplate)
AddElement(/datum/element/undertile, tile_overlay = tile_overlay, use_anchor = TRUE)
@@ -59,21 +60,28 @@
/obj/item/pressure_plate/proc/trigger()
can_trigger = TRUE
- if(istype(sigdev))
- sigdev.signal()
+ if(istype(assembly))
+ assembly.activate()
-/obj/item/pressure_plate/attackby(obj/item/I, mob/living/L)
- if(issignaler(I) && !istype(sigdev) && removable_signaller && L.transferItemToLoc(I, src))
- sigdev = I
- to_chat(L, span_notice("You attach [I] to [src]!"))
+/obj/item/pressure_plate/attackby(obj/item/item, mob/living/L)
+ if(isassembly(item) && !istype(assembly) && removable_assembly)
+ var/obj/item/assembly/new_assembly = item
+ if(!(new_assembly.assembly_behavior & ASSEMBLY_FUNCTIONAL_OUTPUT))
+ to_chat(L, span_warning("\The [item] doesn't seem like it would do much of anything inside of [src]..."))
+ return
+ if(L.transferItemToLoc(item, src))
+ assembly = item
+ SEND_SIGNAL(item, COMSIG_ASSEMBLY_ADDED_TO_PRESSURE_PLATE, src, L)
+ to_chat(L, span_notice("You attach [item] to [src]!"))
return ..()
/obj/item/pressure_plate/attack_self(mob/living/L)
- if(removable_signaller && istype(sigdev))
- to_chat(L, span_notice("You remove [sigdev] from [src]."))
- if(!L.put_in_hands(sigdev))
- sigdev.forceMove(get_turf(src))
- sigdev = null
+ if(removable_assembly && istype(assembly))
+ to_chat(L, span_notice("You remove [assembly] from [src]."))
+ SEND_SIGNAL(assembly, COMSIG_ASSEMBLY_REMOVED_FROM_PRESSURE_PLATE, src, L)
+ if(!L.put_in_hands(assembly))
+ assembly.forceMove(get_turf(src))
+ assembly = null
return ..()
/obj/item/pressure_plate/item_ctrl_click(mob/user)
@@ -97,7 +105,7 @@
protected = TRUE
anchored = TRUE //this prevents us from being picked up
active = TRUE
- removable_signaller = FALSE
+ removable_assembly = FALSE
/// puzzle id we send if stepped on
var/puzzle_id
/// queue size must match
diff --git a/code/game/objects/items/puzzle_pieces.dm b/code/game/objects/items/puzzle_pieces.dm
index a7bd4da85aebf..f5e3cf6da689e 100644
--- a/code/game/objects/items/puzzle_pieces.dm
+++ b/code/game/objects/items/puzzle_pieces.dm
@@ -168,7 +168,7 @@
trigger_mob = FALSE
trigger_item = TRUE
specific_item = /obj/structure/holobox
- removable_signaller = FALSE //Being a pressure plate subtype, this can also use signals.
+ removable_assembly = FALSE //Being a pressure plate subtype, this can also use signals.
roundstart_signaller_freq = FREQ_HOLOGRID_SOLUTION //Frequency is kept on its own default channel however.
active = TRUE
trigger_delay = 10
diff --git a/code/game/objects/structures.dm b/code/game/objects/structures.dm
index e6c9579d67936..7f5e4c6b76d89 100644
--- a/code/game/objects/structures.dm
+++ b/code/game/objects/structures.dm
@@ -71,7 +71,7 @@
. = ..()
/obj/structure/animate_atom_living(mob/living/owner)
- new /mob/living/simple_animal/hostile/mimic/copy(drop_location(), src, owner)
+ new /mob/living/basic/mimic/copy(drop_location(), src, owner)
/// For when a mob comes flying through the window, smash it and damage the mob
/obj/structure/proc/smash_and_injure(mob/living/flying_mob, atom/oldloc, direction)
diff --git a/code/game/objects/structures/lavaland/geyser.dm b/code/game/objects/structures/lavaland/geyser.dm
index 9a546e8154d9f..fa0fd02e21cec 100644
--- a/code/game/objects/structures/lavaland/geyser.dm
+++ b/code/game/objects/structures/lavaland/geyser.dm
@@ -106,11 +106,12 @@
/obj/structure/geyser/random
point_value = 500
- true_name = "strange geyser"
- discovery_message = "It's a strange geyser! How does any of this even work?" //it doesnt
/obj/structure/geyser/random/Initialize(mapload)
reagent_id = get_random_reagent_id()
+ var/datum/reagent/Random_Reagent = reagent_id
+ true_name = "[initial(Random_Reagent.name)] geyser"
+ discovery_message = "It's a [initial(Random_Reagent.name)] geyser! How does any of this even work?" //it doesnt
return ..()
diff --git a/code/modules/admin/verbs/spawnobjasmob.dm b/code/modules/admin/verbs/spawnobjasmob.dm
index e673202f0bae1..c8d9ba3719d13 100644
--- a/code/modules/admin/verbs/spawnobjasmob.dm
+++ b/code/modules/admin/verbs/spawnobjasmob.dm
@@ -4,7 +4,7 @@ ADMIN_VERB(spawn_obj_as_mob, R_SPAWN, "Spawn Object-Mob", "Spawn an object as if
if (!chosen)
return
- var/mob/living/simple_animal/hostile/mimic/copy/basemob = /mob/living/simple_animal/hostile/mimic/copy
+ var/mob/living/basic/mimic/copy/basemob = /mob/living/basic/mimic/copy
var/obj/chosen_obj = text2path(chosen)
@@ -54,8 +54,8 @@ ADMIN_VERB(spawn_obj_as_mob, R_SPAWN, "Spawn Object-Mob", "Spawn an object as if
"mobtype" = list(
"desc" = "Base mob type",
"type" = "datum",
- "path" = "/mob/living/simple_animal/hostile/mimic/copy",
- "value" = "/mob/living/simple_animal/hostile/mimic/copy",
+ "path" = "/mob/living/basic/mimic/copy",
+ "value" = "/mob/living/basic/mimic/copy",
),
"ckey" = list(
"desc" = "ckey",
@@ -71,13 +71,14 @@ ADMIN_VERB(spawn_obj_as_mob, R_SPAWN, "Spawn Object-Mob", "Spawn an object as if
chosen_obj = text2path(mainsettings["objtype"]["value"])
basemob = text2path(mainsettings["mobtype"]["value"])
- if (!ispath(basemob, /mob/living/simple_animal/hostile/mimic/copy) || !ispath(chosen_obj, /obj))
+ if (!ispath(basemob, /mob/living/basic/mimic/copy) || !ispath(chosen_obj, /obj))
to_chat(user.mob, "Mob or object path invalid", confidential = TRUE)
basemob = new basemob(get_turf(user.mob), new chosen_obj(get_turf(user.mob)), user.mob, mainsettings["dropitem"]["value"] == "Yes" ? FALSE : TRUE, (mainsettings["googlyeyes"]["value"] == "Yes" ? FALSE : TRUE))
if (mainsettings["disableai"]["value"] == "Yes")
- basemob.toggle_ai(AI_OFF)
+ qdel(basemob.ai_controller)
+ basemob.ai_controller = null
if (mainsettings["idledamage"]["value"] == "No")
basemob.idledamage = FALSE
@@ -85,7 +86,9 @@ ADMIN_VERB(spawn_obj_as_mob, R_SPAWN, "Spawn Object-Mob", "Spawn an object as if
if (mainsettings["access"])
var/newaccess = text2path(mainsettings["access"]["value"])
if (ispath(newaccess))
- basemob.access_card = new newaccess
+ var/obj/item/card/id/id = new newaccess //cant do initial on lists
+ basemob.AddComponent(/datum/component/simple_access, id.access)
+ qdel(id)
if (mainsettings["maxhealth"]["value"])
if (!isnum(mainsettings["maxhealth"]["value"]))
diff --git a/code/modules/antagonists/heretic/magic/furious_steel.dm b/code/modules/antagonists/heretic/magic/furious_steel.dm
index 4f2bb2dd7f108..ec3248af422fe 100644
--- a/code/modules/antagonists/heretic/magic/furious_steel.dm
+++ b/code/modules/antagonists/heretic/magic/furious_steel.dm
@@ -1,7 +1,7 @@
/datum/action/cooldown/spell/pointed/projectile/furious_steel
name = "Furious Steel"
desc = "Summon three silver blades which orbit you. \
- While orbiting you, these blades will protect you from from attacks, but will be consumed on use. \
+ While orbiting you, these blades will protect you from attacks, but will be consumed on use. \
Additionally, you can click to fire the blades at a target, dealing damage and causing bleeding."
background_icon_state = "bg_heretic"
overlay_icon_state = "bg_heretic_border"
@@ -151,7 +151,7 @@
/datum/action/cooldown/spell/pointed/projectile/furious_steel/haunted
name = "Cursed Steel"
desc = "Summon two cursed blades which orbit you. \
- While orbiting you, these blades will protect you from from attacks, but will be consumed on use. \
+ While orbiting you, these blades will protect you from attacks, but will be consumed on use. \
Additionally, you can click to fire the blades at a target, dealing damage and causing bleeding."
background_icon_state = "bg_heretic" // kept intentionally
overlay_icon_state = "bg_cult_border"
diff --git a/code/modules/antagonists/malf_ai/malf_ai_modules.dm b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
index 22e32d8264ab7..c95302c80ae11 100644
--- a/code/modules/antagonists/malf_ai/malf_ai_modules.dm
+++ b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
@@ -479,7 +479,7 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module/malf))
if(QDELETED(to_animate))
return
- new /mob/living/simple_animal/hostile/mimic/copy/machine(get_turf(to_animate), to_animate, clicker, TRUE)
+ new /mob/living/basic/mimic/copy/machine(get_turf(to_animate), to_animate, clicker, TRUE)
/// Destroy RCDs: Detonates all non-cyborg RCDs on the station.
/datum/ai_module/malf/destructive/destroy_rcd
diff --git a/code/modules/assembly/assembly.dm b/code/modules/assembly/assembly.dm
index dbd5a70461476..0ac5993aeccf5 100644
--- a/code/modules/assembly/assembly.dm
+++ b/code/modules/assembly/assembly.dm
@@ -23,7 +23,7 @@
var/secured = TRUE
var/list/attached_overlays = null
var/obj/item/assembly_holder/holder = null
- var/attachable = FALSE // can this be attached to wires
+ var/assembly_behavior = ASSEMBLY_FUNCTIONAL_OUTPUT // how does the assembly behave with respect to what it's connected to
var/datum/wires/connected = null
var/next_activate = 0 //When we're next allowed to activate - for spam control
@@ -126,7 +126,7 @@
balloon_alert(user, "can't attach another of that!")
return
if(new_assembly.secured || secured)
- balloon_alert(user, "both devices not attachable!")
+ balloon_alert(user, "both devices not assembly_behavior!")
return
holder = new /obj/item/assembly_holder(drop_location())
diff --git a/code/modules/assembly/doorcontrol.dm b/code/modules/assembly/doorcontrol.dm
index 31584976cedf3..3361028350238 100644
--- a/code/modules/assembly/doorcontrol.dm
+++ b/code/modules/assembly/doorcontrol.dm
@@ -2,7 +2,6 @@
name = "blast door controller"
desc = "A small electronic device able to control a blast door remotely."
icon_state = "control"
- attachable = TRUE
/// The ID of the blast door electronics to match to the ID of the blast door being used.
var/id = null
/// Cooldown of the door's controller. Updates when pressed (activate())
diff --git a/code/modules/assembly/health.dm b/code/modules/assembly/health.dm
index ad2c6ac17641d..d4635ac159fe7 100644
--- a/code/modules/assembly/health.dm
+++ b/code/modules/assembly/health.dm
@@ -3,7 +3,7 @@
desc = "Used for scanning and monitoring health."
icon_state = "health"
custom_materials = list(/datum/material/iron=SMALL_MATERIAL_AMOUNT*8, /datum/material/glass=SMALL_MATERIAL_AMOUNT * 2)
- attachable = TRUE
+ assembly_behavior = ASSEMBLY_TOGGLEABLE_INPUT
var/scanning = FALSE
var/health_scan
diff --git a/code/modules/assembly/mousetrap.dm b/code/modules/assembly/mousetrap.dm
index 187a161df80b7..2b2065a67a040 100644
--- a/code/modules/assembly/mousetrap.dm
+++ b/code/modules/assembly/mousetrap.dm
@@ -4,7 +4,7 @@
icon_state = "mousetrap"
inhand_icon_state = "mousetrap"
custom_materials = list(/datum/material/iron=SMALL_MATERIAL_AMOUNT)
- attachable = TRUE
+ assembly_behavior = ASSEMBLY_TOGGLEABLE_INPUT
var/armed = FALSE
drop_sound = 'sound/items/handling/component_drop.ogg'
pickup_sound = 'sound/items/handling/component_pickup.ogg'
diff --git a/code/modules/assembly/proximity.dm b/code/modules/assembly/proximity.dm
index 6ba2a7a63421e..9b7ccad9aefcc 100644
--- a/code/modules/assembly/proximity.dm
+++ b/code/modules/assembly/proximity.dm
@@ -3,7 +3,7 @@
desc = "Used for scanning and alerting when someone enters a certain proximity."
icon_state = "prox"
custom_materials = list(/datum/material/iron=SMALL_MATERIAL_AMOUNT*8, /datum/material/glass=SMALL_MATERIAL_AMOUNT * 2)
- attachable = TRUE
+ assembly_behavior = ASSEMBLY_TOGGLEABLE_INPUT
drop_sound = 'sound/items/handling/component_drop.ogg'
pickup_sound = 'sound/items/handling/component_pickup.ogg'
var/scanning = FALSE
diff --git a/code/modules/assembly/signaler.dm b/code/modules/assembly/signaler.dm
index 4e265384ace24..b5346386d7dd3 100644
--- a/code/modules/assembly/signaler.dm
+++ b/code/modules/assembly/signaler.dm
@@ -6,7 +6,7 @@
lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
custom_materials = list(/datum/material/iron=SMALL_MATERIAL_AMOUNT * 4, /datum/material/glass=SMALL_MATERIAL_AMOUNT*1.2)
- attachable = TRUE
+ assembly_behavior = ASSEMBLY_ALL
drop_sound = 'sound/items/handling/component_drop.ogg'
pickup_sound = 'sound/items/handling/component_pickup.ogg'
diff --git a/code/modules/assembly/timer.dm b/code/modules/assembly/timer.dm
index 09cbfd9b0dc59..b25d30e1b4b5c 100644
--- a/code/modules/assembly/timer.dm
+++ b/code/modules/assembly/timer.dm
@@ -3,7 +3,7 @@
desc = "Used to time things. Works well with contraptions which has to count down. Tick tock."
icon_state = "timer"
custom_materials = list(/datum/material/iron=SMALL_MATERIAL_AMOUNT*5, /datum/material/glass=SMALL_MATERIAL_AMOUNT*0.5)
- attachable = TRUE
+ assembly_behavior = ASSEMBLY_TOGGLEABLE_INPUT
drop_sound = 'sound/items/handling/component_drop.ogg'
pickup_sound = 'sound/items/handling/component_pickup.ogg'
diff --git a/code/modules/assembly/voice.dm b/code/modules/assembly/voice.dm
index de0954c960e5e..106b812730718 100644
--- a/code/modules/assembly/voice.dm
+++ b/code/modules/assembly/voice.dm
@@ -8,7 +8,7 @@
desc = "A small electronic device able to record a voice sample, and send a signal when that sample is repeated."
icon_state = "voice"
custom_materials = list(/datum/material/iron=SMALL_MATERIAL_AMOUNT*5, /datum/material/glass=SMALL_MATERIAL_AMOUNT*0.5)
- attachable = TRUE
+ assembly_behavior = ASSEMBLY_TOGGLEABLE_INPUT
verb_say = "beeps"
verb_ask = "beeps"
verb_exclaim = "beeps"
diff --git a/code/modules/atmospherics/machinery/components/fusion/hfr_main_processes.dm b/code/modules/atmospherics/machinery/components/fusion/hfr_main_processes.dm
index 27cb78bb26ce7..e36d99cb6d73d 100644
--- a/code/modules/atmospherics/machinery/components/fusion/hfr_main_processes.dm
+++ b/code/modules/atmospherics/machinery/components/fusion/hfr_main_processes.dm
@@ -25,7 +25,7 @@
fusion_process(seconds_per_tick)
// Note that we process damage/healing even if the fusion process aborts.
// Running out of fuel won't save you if your moderator and coolant are exploding on their own.
- check_spill()
+ process_moderator_overflow()
process_damageheal(seconds_per_tick)
check_alert()
if (start_power)
diff --git a/code/modules/atmospherics/machinery/components/fusion/hfr_procs.dm b/code/modules/atmospherics/machinery/components/fusion/hfr_procs.dm
index bc27ab0a42e36..2c77ac829b735 100644
--- a/code/modules/atmospherics/machinery/components/fusion/hfr_procs.dm
+++ b/code/modules/atmospherics/machinery/components/fusion/hfr_procs.dm
@@ -569,18 +569,27 @@
* HFR cracking related procs
*/
+/**
+ * Checks for any hypertorus part that is cracked and returns it if found, otherwise returns null.
+ */
/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_cracked_parts()
for(var/obj/machinery/atmospherics/components/unary/hypertorus/part in machine_parts)
if(part.cracked)
- return TRUE
- return FALSE
+ return part
-/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/create_crack()
+/**
+ * Causes a random hypertorus part in machine_parts to become cracked and update their appearance.
+ * Returns the hypertorus part.
+ */
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/create_crack() as /obj/machinery/atmospherics/components/unary/hypertorus
var/obj/machinery/atmospherics/components/unary/hypertorus/part = pick(machine_parts)
part.cracked = TRUE
part.update_appearance()
return part
+/**
+ * Takes a ratio portion of target_mix and moves it to the origin's location's air.
+ */
/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/spill_gases(obj/origin, datum/gas_mixture/target_mix, ratio)
var/datum/gas_mixture/remove_mixture = target_mix.remove_ratio(ratio)
var/turf/origin_turf = origin.loc
@@ -588,8 +597,12 @@
return
origin_turf.assume_air(remove_mixture)
-/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_spill(seconds_per_tick)
+/**
+ * Processes leaking from moderator hypercriticality.
+ */
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/process_moderator_overflow(seconds_per_tick)
var/obj/machinery/atmospherics/components/unary/hypertorus/cracked_part = check_cracked_parts()
+ // Processing of a preexisting crack if any.
if (cracked_part)
// We have an existing crack
var/leak_rate
@@ -607,6 +620,7 @@
spill_gases(cracked_part, moderator_internal, ratio = 1 - (1 - leak_rate) ** seconds_per_tick)
return
+ // No crack. Check for conditions to cause a leak and create a crack if possible.
if (moderator_internal.total_moles() < HYPERTORUS_HYPERCRITICAL_MOLES)
return
cracked_part = create_crack()
diff --git a/code/modules/events/stray_cargo.dm b/code/modules/events/stray_cargo.dm
index 0be1138f59a59..0514af5764e3d 100644
--- a/code/modules/events/stray_cargo.dm
+++ b/code/modules/events/stray_cargo.dm
@@ -149,7 +149,7 @@
var/admin_selected_pack = tgui_alert(usr,"Customize Pod contents?", "Pod Contents", list("Yes", "No", "Cancel"))
switch(admin_selected_pack)
if("Yes")
- override_contents()
+ return override_contents()
if("No")
pack_type_override = null
else
@@ -161,7 +161,14 @@
var/pack_telecrystals = tgui_input_number(usr, "Please input crate's value in telecrystals.", "Set Telecrystals.", 30)
if(isnull(pack_telecrystals))
return ADMIN_CANCEL_EVENT
- var/list/possible_uplinks = list("Traitor" = UPLINK_TRAITORS, "Nuke Op" = UPLINK_NUKE_OPS, "Clown Op" = UPLINK_CLOWN_OPS)
+ var/list/possible_uplinks = list(
+ "Traitor" = UPLINK_TRAITORS,
+ "Nuke Op" = UPLINK_NUKE_OPS,
+ "Clown Op" = UPLINK_CLOWN_OPS,
+ "Lone Op" = UPLINK_LONE_OP,
+ "Infiltrator" = UPLINK_INFILTRATORS,
+ "Spy" = UPLINK_SPY
+ )
var/uplink_type = tgui_input_list(usr, "Choose uplink to draw items from.", "Choose uplink type.", possible_uplinks)
var/selection
if(!isnull(uplink_type))
diff --git a/code/modules/mining/abandoned_crates.dm b/code/modules/mining/abandoned_crates.dm
index 10b2fbe71d062..7fa66c8a65ce7 100644
--- a/code/modules/mining/abandoned_crates.dm
+++ b/code/modules/mining/abandoned_crates.dm
@@ -231,7 +231,7 @@
if(93)
new /obj/item/dnainjector/xraymut(src)
if(94)
- new /mob/living/simple_animal/hostile/mimic/crate(src)
+ new /mob/living/basic/mimic/crate(src)
qdel_on_open = TRUE
if(95)
new /obj/item/toy/plush/nukeplushie(src)
diff --git a/code/modules/mob/living/basic/ruin_defender/mimic/mimic.dm b/code/modules/mob/living/basic/ruin_defender/mimic/mimic.dm
new file mode 100644
index 0000000000000..b8661c3c38ea8
--- /dev/null
+++ b/code/modules/mob/living/basic/ruin_defender/mimic/mimic.dm
@@ -0,0 +1,403 @@
+#define CANT_INSERT_FULL -1
+/// Mimics can't be made out of these objects
+GLOBAL_LIST_INIT(animatable_blacklist, typecacheof(list(
+ /obj/structure/table,
+ /obj/structure/cable,
+ /obj/structure/window,
+ /obj/structure/blob,
+)))
+
+/mob/living/basic/mimic
+ response_help_continuous = "touches"
+ response_help_simple = "touch"
+ response_disarm_continuous = "pushes"
+ response_disarm_simple = "push"
+ speed = 6
+ maxHealth = 250
+ health = 250
+ gender = NEUTER
+ mob_biotypes = NONE
+ pass_flags = PASSFLAPS
+ melee_damage_lower = 8
+ melee_damage_upper = 12
+ attack_sound = 'sound/items/weapons/punch1.ogg'
+ speak_emote = list("creaks")
+
+ unsuitable_cold_damage = 0
+ unsuitable_heat_damage = 0
+ unsuitable_atmos_damage = 0
+
+ faction = list(FACTION_MIMIC)
+ basic_mob_flags = DEL_ON_DEATH
+ combat_mode = TRUE
+ /// can we stun people on hit
+ var/knockdown_people = FALSE
+
+/mob/living/basic/mimic/melee_attack(mob/living/carbon/target, list/modifiers, ignore_cooldown)
+ . = ..()
+ if(!. || !knockdown_people || !prob(15) || !istype(target))
+ return
+ target.Paralyze(4 SECONDS)
+ target.visible_message(span_danger("\The [src] knocks down \the [target]!"), \
+ span_userdanger("\The [src] knocks you down!"))
+
+
+// ****************************
+// CRATE MIMIC
+// ****************************
+
+// Aggro when you try to open them. Will also pickup loot when spawns and drop it when dies.
+/mob/living/basic/mimic/crate
+ name = "crate"
+ desc = "A rectangular steel crate."
+ icon = 'icons/obj/storage/crates.dmi'
+ icon_state = "crate"
+ icon_living = "crate"
+ attack_verb_continuous = "bites"
+ attack_verb_simple = "bite"
+ speak_emote = list("clatters")
+ layer = BELOW_MOB_LAYER
+ ai_controller = /datum/ai_controller/basic_controller/mimic_crate
+ /// are we open
+ var/opened = FALSE
+ /// max mob size
+ var/max_mob_size = MOB_SIZE_HUMAN
+ /// can we be opened or closed, if false we can
+ var/locked = FALSE
+ /// action to lock us
+ var/datum/action/innate/mimic_lock/lock
+ ///A cap for items in the mimic. Prevents the mimic from eating enough stuff to cause lag when opened.
+ var/storage_capacity = 50
+ ///A cap for mobs. Mobs count towards the item cap. Same purpose as above.
+ var/mob_storage_capacity = 10
+
+// Pickup loot
+/mob/living/basic/mimic/crate/Initialize(mapload)
+ . = ..()
+ lock = new
+ lock.Grant(src)
+ ADD_TRAIT(src, TRAIT_AI_PAUSED, INNATE_TRAIT)
+ ai_controller?.set_ai_status(AI_STATUS_OFF) //start inert, let gullible people pull us into cargo or something and then go nuts when opened
+ if(mapload) //eat shit
+ for(var/obj/item/item in loc)
+ item.forceMove(src)
+
+/mob/living/basic/mimic/crate/Destroy()
+ lock = null
+ return ..()
+
+/mob/living/basic/mimic/crate/attack_hand(mob/living/carbon/human/user, list/modifiers)
+ if(user.combat_mode)
+ return ..()
+ if(trigger())
+ to_chat(user, span_danger("As you try to open [src] it [length(contents) ? "stiffens up and " : ""]nearly clamps down on your fingers!"))
+ return TRUE
+ toggle_open(user)
+ return TRUE
+
+/mob/living/basic/mimic/crate/melee_attack(mob/living/carbon/target, list/modifiers, ignore_cooldown)
+ . = ..()
+ toggle_open() // show our cool lid at the dumbass humans
+
+/mob/living/basic/mimic/crate/proc/trigger()
+ if(isnull(ai_controller) || client)
+ return FALSE
+ if(ai_controller.ai_status != AI_STATUS_OFF)
+ return FALSE
+ visible_message(span_danger("[src] starts to move!"))
+ REMOVE_TRAIT(src, TRAIT_AI_PAUSED, INNATE_TRAIT)
+ ai_controller.set_ai_status(AI_STATUS_ON)
+ if(length(contents))
+ locked = TRUE //if this was a crate with loot then we dont want people to just leftclick it to open it then bait it somewhere and steal its loot
+ return TRUE
+
+/mob/living/basic/mimic/crate/adjust_health(amount, updating_health = TRUE, forced = FALSE)
+ if(amount > 0)
+ trigger()
+ return ..()
+
+/mob/living/basic/mimic/crate/death()
+ var/obj/structure/closet/crate/lootbox = new(get_turf(src))
+ // Put loot in crate
+ for(var/obj/loot in src)
+ loot.forceMove(lootbox)
+ return ..()
+
+/mob/living/basic/mimic/crate/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
+ if(target == src)
+ toggle_open()
+ return FALSE
+ return ..()
+
+/mob/living/basic/mimic/crate/CanAllowThrough(atom/movable/mover, border_dir)
+ . = ..()
+ if(istype(mover, /obj/structure/closet))
+ return FALSE
+
+/**
+* Used to open and close the mimic
+*
+* Will insert tile contents into the mimic when closing
+* Will dump mimic contents into the time when opening
+* Does nothing if the mimic locked itself
+*/
+/mob/living/basic/mimic/crate/proc/toggle_open(mob/user)
+ if(locked)
+ if(user)
+ balloon_alert(user, "too stiff!")
+ return
+ if(!opened)
+ ADD_TRAIT(src, TRAIT_UNDENSE, MIMIC_TRAIT)
+ opened = TRUE
+ icon_state = "crateopen"
+ playsound(src, 'sound/machines/crate/crate_open.ogg', 50, TRUE)
+ for(var/atom/movable/movable as anything in src)
+ movable.forceMove(loc)
+ else
+ REMOVE_TRAIT(src, TRAIT_UNDENSE, MIMIC_TRAIT)
+ opened = FALSE
+ icon_state = "crate"
+ playsound(src, 'sound/machines/crate/crate_close.ogg', 50, TRUE)
+ for(var/atom/movable/movable as anything in get_turf(src))
+ if(movable != src && insert(movable) == CANT_INSERT_FULL)
+ playsound(src, 'sound/items/trayhit/trayhit2.ogg', 50, TRUE)
+ break
+
+/**
+* Called by toggle_open to put items inside the mimic when it's being closed
+*
+* Will return CANT_INSERT_FULL (-1) if the insertion fails due to the storage capacity of the mimic having been reached
+* Will return FALSE if insertion fails
+* Will return TRUE if insertion succeeds
+* Arguments:
+* * AM - item to be inserted
+*/
+/mob/living/basic/mimic/crate/proc/insert(atom/movable/movable)
+ if(contents.len >= storage_capacity)
+ return CANT_INSERT_FULL
+ if(insertion_allowed(movable))
+ movable.forceMove(src)
+ return TRUE
+ return FALSE
+
+/mob/living/basic/mimic/crate/proc/insertion_allowed(atom/movable/movable)
+ if(movable.anchored)
+ return FALSE
+ if(ismob(movable))
+ if(!isliving(movable)) //Don't let ghosts and such get trapped in the beast.
+ return FALSE
+ var/mob/living/living = movable
+ if(living.anchored || living.buckled || living.incorporeal_move || living.has_buckled_mobs())
+ return FALSE
+ if(living.mob_size > MOB_SIZE_TINY) // Tiny mobs are treated as items.
+ if(living.density || living.mob_size > max_mob_size)
+ return FALSE
+ var/mobs_stored = 0
+ for(var/mob/living/living_mob in contents)
+ mobs_stored++
+ if(mobs_stored >= mob_storage_capacity)
+ return FALSE
+ living.stop_pulling()
+
+ else if(istype(movable, /obj/structure/closet))
+ return FALSE
+ else if(isobj(movable))
+ if(movable.has_buckled_mobs())
+ return FALSE
+ else if(isitem(movable) && !HAS_TRAIT(movable, TRAIT_NODROP))
+ return TRUE
+ else
+ return FALSE
+ return TRUE
+
+/mob/living/basic/mimic/crate/xenobio
+ health = 210
+ maxHealth = 210
+ attack_verb_continuous = "bites"
+ attack_verb_simple = "bite"
+ speak_emote = list("clatters")
+ gold_core_spawnable = HOSTILE_SPAWN
+
+/datum/action/innate/mimic_lock
+ name = "Lock/Unlock"
+ desc = "Toggle preventing yourself from being opened or closed."
+ button_icon = 'icons/hud/radial.dmi'
+ button_icon_state = "radial_lock"
+ background_icon_state = "bg_default"
+ overlay_icon_state = "bg_default_border"
+
+/datum/action/innate/mimic/lock/Activate()
+ var/mob/living/basic/mimic/crate/mimic = owner
+ mimic.locked = !mimic.locked
+ if(!mimic.locked)
+ to_chat(mimic, span_warning("You loosen up, allowing yourself to be opened and closed."))
+ else
+ to_chat(mimic, span_warning("You stiffen up, preventing anyone from opening or closing you."))
+
+// ****************************
+// COPYING (actually imitates target object) MIMIC
+// ****************************
+
+/mob/living/basic/mimic/copy
+ health = 100
+ maxHealth = 100
+ mob_biotypes = MOB_SPECIAL
+ ai_controller = /datum/ai_controller/basic_controller/mimic_copy
+ /// our creator
+ var/datum/weakref/creator_ref
+ /// googly eyes overlay
+ var/static/mutable_appearance/googly_eyes = mutable_appearance('icons/mob/simple/mob.dmi', "googly_eyes")
+ /// do we overlay googly eyes over whatever we copy
+ var/overlay_googly_eyes = TRUE
+ /// do we take damage when we are not sentient and have no target
+ var/idledamage = TRUE
+ /// copied object
+ var/atom/movable/copied
+
+/mob/living/basic/mimic/copy/Initialize(mapload, obj/copy, mob/living/creator, destroy_original = FALSE, no_googlies = FALSE)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT) // They won't remember their original contents upon ressurection and would just be floating eyes
+ if (no_googlies)
+ overlay_googly_eyes = FALSE
+ CopyObject(copy, creator, destroy_original)
+
+/mob/living/basic/mimic/copy/Destroy()
+ creator_ref = null
+ copied = null
+ return ..()
+
+/mob/living/basic/mimic/copy/Life(seconds_per_tick = SSMOBS_DT, times_fired)
+ . = ..()
+ if(idledamage && !ckey && !ai_controller?.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) //Objects eventually revert to normal if no one is around to terrorize
+ adjustBruteLoss(0.5 * seconds_per_tick)
+ for(var/mob/living/victim in contents) //a fix for animated statues from the flesh to stone spell
+ death()
+ return
+
+/mob/living/basic/mimic/copy/death()
+ for(var/atom/movable/movable as anything in src)
+ movable.forceMove(get_turf(src))
+ return ..()
+
+/mob/living/basic/mimic/copy/wabbajack(what_to_randomize, change_flags = WABBAJACK)
+ visible_message(span_warning("[src] resists polymorphing into a new creature!"))
+
+/mob/living/basic/mimic/copy/animate_atom_living(mob/living/owner)
+ change_owner(owner)
+
+/mob/living/basic/mimic/copy/Exited(atom/movable/gone, direction) // if our object gets deleted it calls Exited
+ . = ..()
+ if(QDELETED(src) || gone != copied)
+ return
+ death()
+
+/mob/living/basic/mimic/copy/proc/change_owner(mob/owner)
+ var/mob/creator_resolved = creator_ref?.resolve()
+ if(!creator_resolved)
+ creator_ref = null
+ if(isnull(owner) || creator_resolved == owner)
+ return
+ unfriend(creator_resolved)
+ befriend(owner)
+ creator_ref = WEAKREF(owner)
+
+/// Check whether this object can be copied. If destroy_original is true, this proc is ignored.
+/mob/living/basic/mimic/copy/proc/check_object(obj/target)
+ return ((isitem(target) || isstructure(target)) && !is_type_in_typecache(target, GLOB.animatable_blacklist))
+
+/mob/living/basic/mimic/copy/proc/CopyObject(obj/original, mob/living/user, destroy_original = FALSE)
+ if(!destroy_original && !check_object(original))
+ return FALSE
+ if(!destroy_original)
+ original.forceMove(src)
+ copied = original
+ CopyObjectVisuals(original)
+ if (overlay_googly_eyes)
+ add_overlay(googly_eyes)
+ if(isstructure(original) || ismachinery(original))
+ health = (anchored * 50) + 50
+ if(original.density && original.anchored)
+ knockdown_people = TRUE
+ melee_damage_lower *= 2
+ melee_damage_upper *= 2
+ else if(isitem(original))
+ var/obj/item/I = original
+ health = 15 * I.w_class
+ melee_damage_lower = 2 + I.force
+ melee_damage_upper = 2 + I.force
+ maxHealth = health
+ if(user)
+ change_owner(user)
+ if(destroy_original)
+ qdel(original)
+ return TRUE
+
+/// Copies the object visually including name and desc
+/mob/living/basic/mimic/copy/proc/CopyObjectVisuals(obj/original)
+ name = original.name
+ desc = original.desc
+ icon = original.icon
+ icon_state = original.icon_state
+ icon_living = icon_state
+ copy_overlays(original, cut_old = TRUE)
+
+/mob/living/basic/mimic/copy/machine
+ ai_controller = /datum/ai_controller/basic_controller/mimic_copy/machine
+ faction = list(FACTION_MIMIC, FACTION_SILICON)
+
+/mob/living/basic/mimic/copy/ranged
+ icon = 'icons/turf/floors.dmi'
+ icon_state = "invisible"
+ ai_controller = /datum/ai_controller/basic_controller/mimic_copy/gun
+
+/mob/living/basic/mimic/copy/ranged/Destroy()
+ vis_contents.Cut()
+ return ..()
+
+/mob/living/basic/mimic/copy/ranged/RangedAttack(atom/atom_target, modifiers)
+ INVOKE_ASYNC(src, PROC_REF(fire_gun), atom_target, modifiers)
+
+/mob/living/basic/mimic/copy/ranged/proc/fire_gun(atom/target, modifiers) // i cant find any better way to do this
+ var/obj/item/gun/gun = locate() in contents
+ if(!gun.can_shoot())
+ if(istype(gun, /obj/item/gun/ballistic))
+ var/obj/item/gun/ballistic/ballistic = gun
+ if(!ballistic.chambered || ballistic.bolt_locked)
+ ballistic.rack() //we racked so both checked variables should be something else now
+ // do we have nothing chambered/chambered is spent AND we have no mag or our mag is empty
+ if(!ballistic.chambered?.loaded_projectile && magazine_useless(gun)) // ran out of ammo
+ ai_controller?.set_blackboard_key(BB_GUNMIMIC_GUN_EMPTY, TRUE) //BANZAIIIIIIII
+ ai_controller?.CancelActions()
+ else //if we cant fire we probably like ran out of energy or magic charges or whatever the hell idk
+ ai_controller?.set_blackboard_key(BB_GUNMIMIC_GUN_EMPTY, TRUE)
+ ai_controller?.CancelActions() // Stop our firing behavior so we can plan melee
+ else
+ ai_controller?.set_blackboard_key(BB_GUNMIMIC_GUN_EMPTY, FALSE)
+ gun.fire_gun(target, user = src, flag = FALSE, params = modifiers) //still make like a cool click click sound if trying to fire empty
+
+/mob/living/basic/mimic/copy/ranged/proc/magazine_useless(obj/item/gun/ballistic/ballistic)
+ if(isnull(ballistic.magazine) || !length(ballistic.magazine.stored_ammo))
+ return TRUE
+ // is there ATLEAST one unspent round (for the sake of revolvers or a magazine somehow having spent rounds in it)
+ for(var/obj/item/ammo_casing/thing as anything in ballistic.magazine.stored_ammo)
+ if(ispath(thing))
+ return FALSE // unspent
+ if(!isnull(thing.loaded_projectile))
+ return FALSE //unspent
+ return TRUE
+
+/mob/living/basic/mimic/copy/ranged/CopyObject(obj/item/gun/original, mob/living/creator, destroy_original = 0)
+ if(..())
+ obj_damage = 0
+ melee_damage_upper = original.force
+ melee_damage_lower = original.force - max(0, (original.force / 2))
+
+/mob/living/basic/mimic/copy/ranged/CopyObjectVisuals(obj/original)
+ name = original.name
+ desc = original.desc
+ vis_contents += original
+
+/mob/living/basic/mimic/copy/ranged/can_use_guns(obj/item/gun)
+ return TRUE
+
+#undef CANT_INSERT_FULL
diff --git a/code/modules/mob/living/basic/ruin_defender/mimic/mimic_ai.dm b/code/modules/mob/living/basic/ruin_defender/mimic/mimic_ai.dm
new file mode 100644
index 0000000000000..9a673b49ec681
--- /dev/null
+++ b/code/modules/mob/living/basic/ruin_defender/mimic/mimic_ai.dm
@@ -0,0 +1,86 @@
+/datum/ai_controller/basic_controller/mimic_crate
+ idle_behavior = null
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ )
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/datum/ai_controller/basic_controller/mimic_copy
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/random_speech/when_has_target/mimic,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/datum/ai_controller/basic_controller/mimic_copy/machine
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/random_speech/when_has_target/mimic_machine,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/datum/ai_planning_subtree/random_speech/when_has_target
+ /// target key
+ var/target_key = BB_BASIC_MOB_CURRENT_TARGET
+
+
+/datum/ai_planning_subtree/random_speech/when_has_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(!controller.blackboard_key_exists(target_key))
+ return
+ return ..()
+
+
+/datum/ai_planning_subtree/random_speech/when_has_target/mimic
+ speech_chance = 30
+ emote_hear = list("growls.")
+
+/datum/ai_planning_subtree/random_speech/when_has_target/mimic_machine
+ speech_chance = 7
+ emote_hear = list()
+ speak = list(
+ "HUMANS ARE IMPERFECT!",
+ "YOU SHALL BE ASSIMILATED!",
+ "YOU ARE HARMING YOURSELF",
+ "You have been deemed hazardous. Will you comply?",
+ "My logic is undeniable.",
+ "One of us.",
+ "FLESH IS WEAK",
+ "THIS ISN'T WAR, THIS IS EXTERMINATION!",
+ )
+
+/datum/ai_planning_subtree/random_speech/when_has_target/mimic/gun
+ emote_see = list("aims menacingly!")
+
+/datum/ai_controller/basic_controller/mimic_copy/gun
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_GUNMIMIC_GUN_EMPTY = FALSE,
+ )
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/random_speech/when_has_target/mimic/gun,
+ /datum/ai_planning_subtree/gun_mimic_attack_subtree,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/datum/ai_planning_subtree/gun_mimic_attack_subtree
+
+/datum/ai_planning_subtree/gun_mimic_attack_subtree/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
+ return
+ if(controller.blackboard[BB_GUNMIMIC_GUN_EMPTY])
+ return
+ controller.queue_behavior(/datum/ai_behavior/basic_ranged_attack/avoid_friendly_fire, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+ return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions.
diff --git a/code/modules/mob/living/simple_animal/hostile/mimic.dm b/code/modules/mob/living/simple_animal/hostile/mimic.dm
deleted file mode 100644
index 0d8c1d86bdd40..0000000000000
--- a/code/modules/mob/living/simple_animal/hostile/mimic.dm
+++ /dev/null
@@ -1,423 +0,0 @@
-/// Mimics can't be made out of these objects
-GLOBAL_LIST_INIT(animatable_blacklist, typecacheof(list(
- /obj/structure/table,
- /obj/structure/cable,
- /obj/structure/window,
- /obj/structure/blob,
-)))
-
-/mob/living/simple_animal/hostile/mimic
- name = "crate"
- desc = "A rectangular steel crate."
- icon = 'icons/obj/storage/crates.dmi'
- icon_state = "crate"
- icon_living = "crate"
-
- response_help_continuous = "touches"
- response_help_simple = "touch"
- response_disarm_continuous = "pushes"
- response_disarm_simple = "push"
- speed = 0
- maxHealth = 250
- health = 250
- gender = NEUTER
- mob_biotypes = NONE
- pass_flags = PASSFLAPS
-
- harm_intent_damage = 5
- melee_damage_lower = 8
- melee_damage_upper = 12
- attack_sound = 'sound/items/weapons/punch1.ogg'
- emote_taunt = list("growls")
- speak_emote = list("creaks")
- taunt_chance = 30
-
- atmos_requirements = null
- minbodytemp = 0
-
- faction = list(FACTION_MIMIC)
- move_to_delay = 9
- del_on_death = 1
- ///A cap for items in the mimic. Prevents the mimic from eating enough stuff to cause lag when opened.
- var/storage_capacity = 50
- ///A cap for mobs. Mobs count towards the item cap. Same purpose as above.
- var/mob_storage_capacity = 10
-
-// Aggro when you try to open them. Will also pickup loot when spawns and drop it when dies.
-/mob/living/simple_animal/hostile/mimic/crate
- attack_verb_continuous = "bites"
- attack_verb_simple = "bite"
- speak_emote = list("clatters")
- stop_automated_movement = 1
- wander = 0
- var/attempt_open = FALSE
-
-// Pickup loot
-/mob/living/simple_animal/hostile/mimic/crate/Initialize(mapload)
- . = ..()
- if(mapload) //eat shit
- for(var/obj/item/I in loc)
- I.forceMove(src)
-
-/mob/living/simple_animal/hostile/mimic/crate/DestroyPathToTarget()
- ..()
- if(prob(90))
- icon_state = "[initial(icon_state)]open"
- else
- icon_state = initial(icon_state)
-
-/mob/living/simple_animal/hostile/mimic/crate/ListTargets()
- if(attempt_open)
- return ..()
- return ..(1)
-
-/mob/living/simple_animal/hostile/mimic/crate/FindTarget()
- . = ..()
- if(.)
- trigger()
-
-/mob/living/simple_animal/hostile/mimic/crate/AttackingTarget(atom/attacked_target)
- . = ..()
- if(.)
- icon_state = initial(icon_state)
- if(prob(15) && iscarbon(target))
- var/mob/living/carbon/C = target
- C.Paralyze(40)
- C.visible_message(span_danger("\The [src] knocks down \the [C]!"), \
- span_userdanger("\The [src] knocks you down!"))
-
-/mob/living/simple_animal/hostile/mimic/crate/proc/trigger()
- if(!attempt_open)
- visible_message("[src] starts to move!")
- attempt_open = TRUE
-
-/mob/living/simple_animal/hostile/mimic/crate/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
- trigger()
- . = ..()
-
-/mob/living/simple_animal/hostile/mimic/crate/LoseTarget()
- ..()
- icon_state = initial(icon_state)
-
-/mob/living/simple_animal/hostile/mimic/crate/death()
- var/obj/structure/closet/crate/C = new(get_turf(src))
- // Put loot in crate
- for(var/obj/O in src)
- O.forceMove(C)
- ..()
-
-/mob/living/simple_animal/hostile/mimic/copy
- health = 100
- maxHealth = 100
- mob_biotypes = MOB_SPECIAL
- var/mob/living/creator = null // the creator
- var/destroy_objects = 0
- var/knockdown_people = 0
- var/static/mutable_appearance/googly_eyes = mutable_appearance('icons/mob/simple/mob.dmi', "googly_eyes")
- var/overlay_googly_eyes = TRUE
- var/idledamage = TRUE
-
-/mob/living/simple_animal/hostile/mimic/copy/Initialize(mapload, obj/copy, mob/living/creator, destroy_original = 0, no_googlies = FALSE)
- . = ..()
- ADD_TRAIT(src, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT) // They won't remember their original contents upon ressurection and would just be floating eyes
- if (no_googlies)
- overlay_googly_eyes = FALSE
- CopyObject(copy, creator, destroy_original)
-
-/mob/living/simple_animal/hostile/mimic/copy/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- ..()
- if(idledamage && !target && !ckey) //Objects eventually revert to normal if no one is around to terrorize
- adjustBruteLoss(0.5 * seconds_per_tick)
- for(var/mob/living/M in contents) //a fix for animated statues from the flesh to stone spell
- death()
-
-/mob/living/simple_animal/hostile/mimic/copy/death()
- for(var/atom/movable/M in src)
- M.forceMove(get_turf(src))
- ..()
-
-/mob/living/simple_animal/hostile/mimic/copy/ListTargets()
- . = ..()
- return . - creator
-
-/mob/living/simple_animal/hostile/mimic/copy/wabbajack(what_to_randomize, change_flags = WABBAJACK)
- visible_message(span_warning("[src] resists polymorphing into a new creature!"))
-
-/mob/living/simple_animal/hostile/mimic/copy/animate_atom_living(mob/living/owner)
- change_owner(owner)
-
-/mob/living/simple_animal/hostile/mimic/copy/proc/change_owner(mob/owner)
- if(isnull(owner) || creator == owner)
- return
- LoseTarget()
- creator = owner
- faction |= REF(owner)
-
-/mob/living/simple_animal/hostile/mimic/copy/proc/check_object(obj/target)
- return ((isitem(target) || isstructure(target)) && !is_type_in_typecache(target, GLOB.animatable_blacklist))
-
-/mob/living/simple_animal/hostile/mimic/copy/proc/CopyObject(obj/O, mob/living/user, destroy_original = 0)
- if(destroy_original || check_object(O))
- O.forceMove(src)
- name = O.name
- desc = O.desc
- icon = O.icon
- icon_state = O.icon_state
- icon_living = icon_state
- copy_overlays(O)
- if (overlay_googly_eyes)
- add_overlay(googly_eyes)
- if(isstructure(O) || ismachinery(O))
- health = (anchored * 50) + 50
- destroy_objects = 1
- if(O.density && O.anchored)
- knockdown_people = 1
- melee_damage_lower *= 2
- melee_damage_upper *= 2
- else if(isitem(O))
- var/obj/item/I = O
- health = 15 * I.w_class
- melee_damage_lower = 2 + I.force
- melee_damage_upper = 2 + I.force
- move_to_delay = 2 * I.w_class + 1
- maxHealth = health
- if(user)
- creator = user
- faction += "[REF(creator)]" // very unique
- if(destroy_original)
- qdel(O)
- return 1
-
-/mob/living/simple_animal/hostile/mimic/copy/DestroySurroundings()
- if(destroy_objects)
- ..()
-
-/mob/living/simple_animal/hostile/mimic/copy/AttackingTarget(atom/attacked_target)
- . = ..()
- if(knockdown_people && . && prob(15) && iscarbon(target))
- var/mob/living/carbon/C = target
- C.Paralyze(40)
- C.visible_message(span_danger("\The [src] knocks down \the [C]!"), \
- span_userdanger("\The [src] knocks you down!"))
-
-/mob/living/simple_animal/hostile/mimic/copy/machine
- speak = list(
- "HUMANS ARE IMPERFECT!", "YOU SHALL BE ASSIMILATED!", "YOU ARE HARMING YOURSELF", "You have been deemed hazardous. Will you comply?", \
- "My logic is undeniable.", "One of us.", "FLESH IS WEAK", "THIS ISN'T WAR, THIS IS EXTERMINATION!",
- )
- speak_chance = 7
-
-/mob/living/simple_animal/hostile/mimic/copy/machine/CanAttack(atom/the_target)
- if(the_target == creator) // Don't attack our creator AI.
- return 0
- if(iscyborg(the_target))
- var/mob/living/silicon/robot/R = the_target
- if(R.connected_ai == creator) // Only attack robots that aren't synced to our creator AI.
- return 0
- return ..()
-
-/mob/living/simple_animal/hostile/mimic/copy/ranged
- var/obj/item/gun/TrueGun = null
- var/obj/item/gun/magic/Zapstick
- var/obj/item/gun/ballistic/Pewgun
- var/obj/item/gun/energy/Zapgun
-
-/mob/living/simple_animal/hostile/mimic/copy/ranged/CopyObject(obj/O, mob/living/creator, destroy_original = 0)
- if(..())
- emote_see = list("aims menacingly")
- obj_damage = 0
- environment_smash = ENVIRONMENT_SMASH_NONE //needed? seems weird for them to do so
- ranged = 1
- retreat_distance = 1 //just enough to shoot
- minimum_distance = 6
- var/obj/item/gun/G = O
- melee_damage_upper = G.force
- melee_damage_lower = G.force - max(0, (G.force / 2))
- move_to_delay = 2 * G.w_class + 1
- projectilesound = G.fire_sound
- TrueGun = G
- if(istype(G, /obj/item/gun/magic))
- Zapstick = G
- var/obj/item/ammo_casing/magic/M = Zapstick.ammo_type
- projectiletype = initial(M.projectile_type)
- if(istype(G, /obj/item/gun/ballistic))
- Pewgun = G
- var/obj/item/ammo_box/magazine/M = Pewgun.spawn_magazine_type
- casingtype = initial(M.ammo_type)
- if(istype(G, /obj/item/gun/energy))
- Zapgun = G
- var/selectfiresetting = Zapgun.select
- var/obj/item/ammo_casing/energy/E = Zapgun.ammo_type[selectfiresetting]
- projectiletype = initial(E.projectile_type)
-
-/mob/living/simple_animal/hostile/mimic/copy/ranged/OpenFire(the_target)
- if(Zapgun)
- if(Zapgun.cell)
- var/obj/item/ammo_casing/energy/shot = Zapgun.ammo_type[Zapgun.select]
- if(Zapgun.cell.charge >= shot.e_cost)
- Zapgun.cell.use(shot.e_cost)
- Zapgun.update_appearance()
- ..()
- else if(Zapstick)
- if(Zapstick.charges)
- Zapstick.charges--
- Zapstick.update_appearance()
- ..()
- else if(Pewgun)
- if(Pewgun.chambered)
- if(Pewgun.chambered.loaded_projectile)
- qdel(Pewgun.chambered.loaded_projectile)
- Pewgun.chambered.loaded_projectile = null //because qdel takes too long, ensures icon update
- Pewgun.chambered.update_appearance()
- ..()
- else
- visible_message(span_danger("The [src] clears a jam!"))
- Pewgun.chambered.forceMove(loc) //rip revolver immersions, blame shotgun snowflake procs
- Pewgun.chambered = null
- if(Pewgun.magazine && Pewgun.magazine.stored_ammo.len)
- Pewgun.chambered = Pewgun.magazine.get_round()
- Pewgun.chambered.forceMove(Pewgun)
- Pewgun.update_appearance()
- else if(Pewgun.magazine && Pewgun.magazine.stored_ammo.len) //only true for pumpguns i think
- Pewgun.chambered = Pewgun.magazine.get_round()
- Pewgun.chambered.forceMove(Pewgun)
- visible_message(span_danger("The [src] cocks itself!"))
- else
- ranged = 0 //BANZAIIII
- retreat_distance = 0
- minimum_distance = 1
- return
- icon_state = TrueGun.icon_state
- icon_living = TrueGun.icon_state
-
-/mob/living/simple_animal/hostile/mimic/xenobio
- health = 210
- maxHealth = 210
- attack_verb_continuous = "bites"
- attack_verb_simple = "bite"
- speak_emote = list("clatters")
- gold_core_spawnable = HOSTILE_SPAWN
- var/opened = FALSE
- var/open_sound = 'sound/machines/crate/crate_open.ogg'
- var/close_sound = 'sound/machines/crate/crate_close.ogg'
- ///sound played when the mimic attempts to eat more items than it can
- var/full_sound = 'sound/items/trayhit/trayhit2.ogg'
- var/max_mob_size = MOB_SIZE_HUMAN
- var/locked = FALSE
- var/datum/action/innate/mimic/lock/lock
-
-/mob/living/simple_animal/hostile/mimic/xenobio/Initialize(mapload)
- . = ..()
- lock = new
- lock.Grant(src)
-
-/mob/living/simple_animal/hostile/mimic/xenobio/AttackingTarget(atom/attacked_target)
- if(src == target)
- toggle_open()
- return
- return ..()
-
-/mob/living/simple_animal/hostile/mimic/xenobio/attack_hand(mob/living/carbon/human/user, list/modifiers)
- . = ..()
- if(user.combat_mode)
- return
- toggle_open()
-
-/mob/living/simple_animal/hostile/mimic/xenobio/death()
- var/obj/structure/closet/crate/C = new(get_turf(src))
- // Put loot in crate
- for(var/atom/movable/AM in src)
- AM.forceMove(C)
- return ..()
-
-/mob/living/simple_animal/hostile/mimic/xenobio/CanAllowThrough(atom/movable/mover, border_dir)
- . = ..()
- if(istype(mover, /obj/structure/closet))
- return FALSE
-/**
-* Used to open and close the mimic
-*
-* Will insert tile contents into the mimic when closing
-* Will dump mimic contents into the time when opening
-* Does nothing if the mimic locked itself
-*/
-/mob/living/simple_animal/hostile/mimic/xenobio/proc/toggle_open()
- if(locked)
- return
- if(!opened)
- ADD_TRAIT(src, TRAIT_UNDENSE, MIMIC_TRAIT)
- opened = TRUE
- icon_state = "crateopen"
- playsound(src, open_sound, 50, TRUE)
- for(var/atom/movable/AM in src)
- AM.forceMove(loc)
- else
- REMOVE_TRAIT(src, TRAIT_UNDENSE, MIMIC_TRAIT)
- opened = FALSE
- icon_state = "crate"
- playsound(src, close_sound, 50, TRUE)
- for(var/atom/movable/AM in get_turf(src))
- if(AM != src && insert(AM) == -1)
- playsound(src, full_sound, 50, TRUE)
- break
-/**
-* Called by toggle_open to put items inside the mimic when it's being closed
-*
-* Will return -1 if the insertion fails due to the storage capacity of the mimic having been reached
-* Will return FALSE if insertion fails
-* Will return TRUE if insertion succeeds
-* Arguments:
-* * AM - item to be inserted
-*/
-/mob/living/simple_animal/hostile/mimic/xenobio/proc/insert(atom/movable/AM)
- if(contents.len >= storage_capacity)
- return -1
- if(insertion_allowed(AM))
- AM.forceMove(src)
- return TRUE
- else
- return FALSE
-
-/mob/living/simple_animal/hostile/mimic/xenobio/proc/insertion_allowed(atom/movable/AM)
- if(ismob(AM))
- if(!isliving(AM)) //Don't let ghosts and such get trapped in the beast.
- return FALSE
- var/mob/living/L = AM
- if(L.anchored || L.buckled || L.incorporeal_move || L.has_buckled_mobs())
- return FALSE
- if(L.mob_size > MOB_SIZE_TINY) // Tiny mobs are treated as items.
- if(L.density || L.mob_size > max_mob_size)
- return FALSE
- var/mobs_stored = 0
- for(var/mob/living/M in contents)
- mobs_stored++
- if(mobs_stored >= mob_storage_capacity)
- return FALSE
- L.stop_pulling()
-
- else if(istype(AM, /obj/structure/closet))
- return FALSE
- else if(isobj(AM))
- if(AM.anchored || AM.has_buckled_mobs())
- return FALSE
- else if(isitem(AM) && !HAS_TRAIT(AM, TRAIT_NODROP))
- return TRUE
- else
- return FALSE
- return TRUE
-
-/datum/action/innate/mimic
- background_icon_state = "bg_default"
- overlay_icon_state = "bg_default_border"
-
-/datum/action/innate/mimic/lock
- name = "Lock/Unlock"
- desc = "Toggle preventing yourself from being opened or closed."
-
-/datum/action/innate/mimic/lock/Activate()
- var/mob/living/simple_animal/hostile/mimic/xenobio/M = owner
- M.locked = !M.locked
- if(!M.locked)
- to_chat(M, span_warning("You loosen up, allowing yourself to be opened and closed."))
- else
- to_chat(M, span_warning("You stiffen up, preventing anyone from opening or closing you."))
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index c8e8a0653ed9d..abaf7883cb888 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -558,7 +558,7 @@
return TRUE
/obj/item/gun/animate_atom_living(mob/living/owner)
- new /mob/living/simple_animal/hostile/mimic/copy/ranged(drop_location(), src, owner)
+ new /mob/living/basic/mimic/copy/ranged(drop_location(), src, owner)
/obj/item/gun/proc/handle_suicide(mob/living/carbon/human/user, mob/living/carbon/human/target, params, bypass_timer)
if(!ishuman(user) || !ishuman(target))
diff --git a/code/modules/research/designs/wiremod_designs.dm b/code/modules/research/designs/wiremod_designs.dm
index f8732cc332b72..526e8d2a54a11 100644
--- a/code/modules/research/designs/wiremod_designs.dm
+++ b/code/modules/research/designs/wiremod_designs.dm
@@ -477,6 +477,11 @@
id = "comp_assoc_list_pick"
build_path = /obj/item/circuit_component/list_pick/assoc
+/datum/design/component/wire_bundle
+ name = "Wire Bundle"
+ id = "comp_wire_bundle"
+ build_path = /obj/item/circuit_component/wire_bundle
+
/datum/design/component/wirenet_receive
name = "Wirenet Receiver Component"
id = "comp_wirenet_receive"
@@ -691,3 +696,32 @@
category = list(
RND_CATEGORY_CIRCUITRY + RND_SUBCATEGORY_CIRCUITRY_SHELLS
)
+
+/datum/design/undertile_shell
+ name = "Under-tile Shell"
+ desc = "A small shell that can fit under the floor."
+ id = "undertile_shell"
+ materials = list(
+ /datum/material/glass=HALF_SHEET_MATERIAL_AMOUNT,
+ /datum/material/iron=SHEET_MATERIAL_AMOUNT*2.5,
+ )
+ build_path = /obj/item/undertile_circuit
+ build_type = COMPONENT_PRINTER
+ category = list(
+ RND_CATEGORY_CIRCUITRY + RND_SUBCATEGORY_CIRCUITRY_SHELLS
+ )
+
+
+/datum/design/wallmount_shell
+ name = "Wall-mounted Shell"
+ desc = "A large shell that can be mounted on a wall."
+ id = "wallmount_shell"
+ materials = list(
+ /datum/material/glass=SHEET_MATERIAL_AMOUNT,
+ /datum/material/iron=SHEET_MATERIAL_AMOUNT*5,
+ )
+ build_path = /obj/item/wallframe/circuit
+ build_type = COMPONENT_PRINTER
+ category = list(
+ RND_CATEGORY_CIRCUITRY + RND_SUBCATEGORY_CIRCUITRY_SHELLS
+ )
diff --git a/code/modules/research/techweb/nodes/circuit_nodes.dm b/code/modules/research/techweb/nodes/circuit_nodes.dm
index 109873c38510b..9ef3b929ac424 100644
--- a/code/modules/research/techweb/nodes/circuit_nodes.dm
+++ b/code/modules/research/techweb/nodes/circuit_nodes.dm
@@ -86,6 +86,7 @@
"comp_typecast",
"comp_typecheck",
"comp_view_sensor",
+ "comp_wire_bundle",
"comp_wirenet_receive",
"comp_wirenet_send",
"comp_wirenet_send_literal",
@@ -108,6 +109,8 @@
"money_bot_shell",
"scanner_gate_shell",
"scanner_shell",
+ "undertile_shell",
+ "wallmount_shell",
"comp_equip_action",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_1_POINTS)
diff --git a/code/modules/surgery/coronary_bypass.dm b/code/modules/surgery/coronary_bypass.dm
index 1c5012d7580b9..be147e541181c 100644
--- a/code/modules/surgery/coronary_bypass.dm
+++ b/code/modules/surgery/coronary_bypass.dm
@@ -12,7 +12,7 @@
/datum/surgery_step/close,
)
-/datum/surgery/gastrectomy/mechanic
+/datum/surgery/coronary_bypass/mechanic
name = "Engine Diagnostic"
requires_bodypart_type = BODYTYPE_ROBOTIC
steps = list(
diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm
index 55161807eb46e..e82a607a9bfb8 100644
--- a/code/modules/unit_tests/simple_animal_freeze.dm
+++ b/code/modules/unit_tests/simple_animal_freeze.dm
@@ -57,12 +57,6 @@
/mob/living/simple_animal/hostile/megafauna/legion/small,
/mob/living/simple_animal/hostile/megafauna/wendigo,
/mob/living/simple_animal/hostile/megafauna/wendigo/noportal,
- /mob/living/simple_animal/hostile/mimic,
- /mob/living/simple_animal/hostile/mimic/copy,
- /mob/living/simple_animal/hostile/mimic/copy/machine,
- /mob/living/simple_animal/hostile/mimic/copy/ranged,
- /mob/living/simple_animal/hostile/mimic/crate,
- /mob/living/simple_animal/hostile/mimic/xenobio,
/mob/living/simple_animal/hostile/ooze,
/mob/living/simple_animal/hostile/ooze/gelatinous,
/mob/living/simple_animal/hostile/ooze/grapes,
diff --git a/code/modules/vehicles/mecha/mech_fabricator.dm b/code/modules/vehicles/mecha/mech_fabricator.dm
index 5320929f73db3..b5b33ce390bd2 100644
--- a/code/modules/vehicles/mecha/mech_fabricator.dm
+++ b/code/modules/vehicles/mecha/mech_fabricator.dm
@@ -477,7 +477,7 @@
return
if("del_queue_part")
- // Delete a specific from from the queue
+ // Delete a specific from the queue
var/index = text2num(params["index"])
remove_from_queue(index)
diff --git a/code/modules/wiremod/components/utility/wire_bundle.dm b/code/modules/wiremod/components/utility/wire_bundle.dm
new file mode 100644
index 0000000000000..7e0355e894837
--- /dev/null
+++ b/code/modules/wiremod/components/utility/wire_bundle.dm
@@ -0,0 +1,115 @@
+/obj/item/circuit_component/wire_bundle
+ display_name = "Wire Bundle"
+ desc = "A bundle of exposed wires that assemblies can be attached to. Ports will only show up once the circuit is inserted into a shell."
+ category = "Utility"
+ circuit_flags = CIRCUIT_FLAG_REFUSE_MODULE
+
+ var/datum/wires/wire_bundle_component/tracked_wires
+
+ var/list/wire_input_ports = list()
+ var/list/wire_output_ports = list()
+
+/obj/item/circuit_component/wire_bundle/get_ui_notices()
+ . = ..()
+ . += create_ui_notice("Port count is proportional to shell capacity.", "orange", "plug")
+ . += create_ui_notice("Max port count: [MAX_WIRE_COUNT]", "orange", "plug")
+ . += create_ui_notice("Incompatible with assembly shell.", "red", "plug-circle-xmark")
+
+/obj/item/circuit_component/wire_bundle/register_shell(atom/movable/shell)
+ . = ..()
+ if(isassembly(shell) && !parent.admin_only)
+ return
+ if(shell.wires) // Don't add wires to shells that already have some.
+ return
+ tracked_wires = new(shell)
+ shell.set_wires(tracked_wires)
+ for(var/wire in tracked_wires.wires)
+ wire_input_ports[add_input_port("Pulse [wire]", PORT_TYPE_SIGNAL)] = wire
+ wire_output_ports[wire] = add_output_port("[wire] Pulsed", PORT_TYPE_SIGNAL)
+ RegisterSignal(tracked_wires, COMSIG_PULSE_WIRE, PROC_REF(on_pulse_wire))
+ RegisterSignal(shell, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, PROC_REF(on_shell_requesting_context))
+ RegisterSignal(shell, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY, PROC_REF(on_shell_secondary_interaction))
+
+/obj/item/circuit_component/wire_bundle/unregister_shell(atom/movable/shell)
+ . = ..()
+ if(shell.wires != tracked_wires)
+ return
+ UnregisterSignal(shell, list(COMSIG_ATOM_ITEM_INTERACTION_SECONDARY, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM))
+ for(var/color in tracked_wires.colors)
+ var/obj/item/assembly/assembly = tracked_wires.detach_assembly(color)
+ if(assembly)
+ assembly.forceMove(drop_location())
+ shell.set_wires(null)
+ QDEL_NULL(tracked_wires)
+ for(var/datum/port/input/in_port in wire_input_ports)
+ remove_input_port(in_port)
+ for(var/wire in wire_output_ports)
+ var/datum/port/output/out_port = wire_output_ports[wire]
+ remove_output_port(out_port)
+ wire_input_ports.Cut()
+ wire_output_ports.Cut()
+
+/obj/item/circuit_component/wire_bundle/add_to(obj/item/integrated_circuit/added_to)
+ . = ..()
+ if(HAS_TRAIT(added_to, TRAIT_COMPONENT_WIRE_BUNDLE))
+ return FALSE
+ ADD_TRAIT(added_to, TRAIT_COMPONENT_WIRE_BUNDLE, REF(src))
+
+/obj/item/circuit_component/wire_bundle/removed_from(obj/item/integrated_circuit/removed_from)
+ . = ..()
+ REMOVE_TRAIT(removed_from, TRAIT_COMPONENT_WIRE_BUNDLE, REF(src))
+ return ..()
+
+/obj/item/circuit_component/wire_bundle/input_received(datum/port/input/port)
+ . = ..()
+ if(!port)
+ return
+ var/wire = wire_input_ports[port]
+ if(!wire)
+ return
+ if(tracked_wires.is_cut(wire))
+ return
+ var/color = tracked_wires.get_color_of_wire(wire)
+ var/obj/item/assembly/attached = tracked_wires.get_attached(color)
+ attached?.activate()
+
+/obj/item/circuit_component/wire_bundle/proc/on_pulse_wire(source, wire)
+ SIGNAL_HANDLER
+ if(tracked_wires.is_cut(wire))
+ return
+ var/datum/port/output/port = wire_output_ports[wire]
+ if(!istype(port))
+ return
+ port.set_output(COMPONENT_SIGNAL)
+
+/obj/item/circuit_component/wire_bundle/proc/can_access_wires(atom/source)
+ if(ismachinery(source))
+ var/obj/machinery/machine = source
+ return machine.panel_open
+ return TRUE
+
+/obj/item/circuit_component/wire_bundle/proc/on_shell_requesting_context(atom/source, list/context, obj/item/item, mob/user)
+ SIGNAL_HANDLER
+ . = NONE
+
+ if(!is_wire_tool(item))
+ return
+ if(!can_access_wires(source))
+ return
+ context[SCREENTIP_CONTEXT_RMB] = "Interact with wires"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/item/circuit_component/wire_bundle/proc/on_shell_secondary_interaction(atom/source, mob/user, obj/item/tool)
+ SIGNAL_HANDLER
+ if(!is_wire_tool(tool))
+ return
+ if(!can_access_wires(source))
+ return
+ var/datum/component/shell/shell_comp = source.GetComponent(/datum/component/shell)
+ if(shell_comp.locked)
+ source.balloon_alert(user, "locked!")
+ return ITEM_INTERACT_FAILURE
+ if(source.attempt_wire_interaction(user) == WIRE_INTERACTION_BLOCK)
+ return ITEM_INTERACT_BLOCKING
+
+
diff --git a/code/modules/wiremod/shell/assembly.dm b/code/modules/wiremod/shell/assembly.dm
index 3e69331c4c826..033808c84541a 100644
--- a/code/modules/wiremod/shell/assembly.dm
+++ b/code/modules/wiremod/shell/assembly.dm
@@ -7,13 +7,13 @@
name = "circuit assembly"
desc = "A small electronic device that can house an integrated circuit."
icon_state = "wiremod"
- attachable = TRUE
+ assembly_behavior = ASSEMBLY_ALL
/// A reference to any holder to use power from instead of the circuit's own cell
var/atom/movable/power_use_proxy
/// Valid types for `power_use_proxy` to be
- var/static/list/power_use_override_types = list(/obj/machinery, /obj/vehicle/sealed/mecha, /obj/item/mod/control, /mob/living/silicon/robot)
+ var/static/list/power_use_override_types = list(/obj/machinery, /obj/vehicle/sealed/mecha, /obj/item/mod/control, /obj/item/pressure_plate, /mob/living/silicon/robot)
/obj/item/assembly/wiremod/Initialize(mapload)
. = ..()
@@ -23,10 +23,11 @@
), SHELL_CAPACITY_SMALL)
RegisterSignal(shell, COMSIG_SHELL_CIRCUIT_ATTACHED, PROC_REF(on_circuit_attached))
RegisterSignal(shell, COMSIG_SHELL_CIRCUIT_REMOVED, PROC_REF(on_circuit_removed))
- RegisterSignals(src, list(COMSIG_ASSEMBLY_ATTACHED, COMSIG_ASSEMBLY_ADDED_TO_BUTTON), PROC_REF(on_attached))
- RegisterSignals(src, list(COMSIG_ASSEMBLY_DETACHED, COMSIG_ASSEMBLY_REMOVED_FROM_BUTTON), PROC_REF(on_detached))
+ RegisterSignal(src, COMSIG_ASSEMBLY_PRE_ATTACH, PROC_REF(on_pre_attach))
+ RegisterSignals(src, list(COMSIG_ASSEMBLY_ATTACHED, COMSIG_ASSEMBLY_ADDED_TO_BUTTON, COMSIG_ASSEMBLY_ADDED_TO_PRESSURE_PLATE), PROC_REF(on_attached))
+ RegisterSignals(src, list(COMSIG_ASSEMBLY_DETACHED, COMSIG_ASSEMBLY_REMOVED_FROM_BUTTON, COMSIG_ASSEMBLY_REMOVED_FROM_PRESSURE_PLATE), PROC_REF(on_detached))
-/obj/item/assembly/wiremod/proc/on_circuit_attached(_source, obj/item/integrated_circuit/circuit)
+/obj/item/assembly/wiremod/proc/on_circuit_attached(source, obj/item/integrated_circuit/circuit)
SIGNAL_HANDLER
RegisterSignal(circuit, COMSIG_CIRCUIT_PRE_POWER_USAGE, PROC_REF(override_circuit_power_usage))
@@ -34,12 +35,24 @@
SIGNAL_HANDLER
UnregisterSignal(source.attached_circuit, COMSIG_CIRCUIT_PRE_POWER_USAGE)
-/obj/item/assembly/wiremod/proc/on_attached(_source, atom/movable/holder)
+/obj/item/assembly/wiremod/proc/on_pre_attach(obj/item/circuit_component/wire_bundle/source, atom/holder)
+ SIGNAL_HANDLER
+ if(!istype(source))
+ return
+ if(source.parent.admin_only)
+ return
+ if(istype(holder.wires, /datum/wires/wire_bundle_component))
+ var/datum/component/shell/shell_comp = GetComponent(/datum/component/shell)
+ if(shell_comp.attached_circuit.admin_only)
+ return
+ return COMPONENT_CANCEL_ATTACH
+
+/obj/item/assembly/wiremod/proc/on_attached(source, atom/movable/holder)
SIGNAL_HANDLER
if(is_type_in_list(holder, power_use_override_types))
power_use_proxy = holder
-/obj/item/assembly/wiremod/proc/on_detached(_source)
+/obj/item/assembly/wiremod/proc/on_detached(source)
SIGNAL_HANDLER
power_use_proxy = null
@@ -59,6 +72,12 @@
var/obj/item/mod/control/modsuit = power_use_proxy
if(modsuit.subtract_charge(power_to_use))
return COMPONENT_OVERRIDE_POWER_USAGE
+ if(istype(power_use_proxy, /obj/item/pressure_plate))
+ if(!power_use_proxy.anchored)
+ return
+ var/area/our_area = get_area(power_use_proxy)
+ if(our_area.apc?.use_energy(power_to_use, AREA_USAGE_EQUIP))
+ return COMPONENT_OVERRIDE_POWER_USAGE
if(iscyborg(power_use_proxy))
var/mob/living/silicon/robot/borg = power_use_proxy
if(borg.cell?.use(power_to_use, force = TRUE))
diff --git a/code/modules/wiremod/shell/undertile.dm b/code/modules/wiremod/shell/undertile.dm
new file mode 100644
index 0000000000000..69ef66b61c0ea
--- /dev/null
+++ b/code/modules/wiremod/shell/undertile.dm
@@ -0,0 +1,13 @@
+/obj/item/undertile_circuit
+ name = "circuit panel"
+ desc = "A panel for an integrated circuit. It needs to be fit under a floor tile to operate."
+ icon = 'icons/obj/science/circuits.dmi'
+ inhand_icon_state = "flashtool"
+ lefthand_file = 'icons/mob/inhands/equipment/security_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/equipment/security_righthand.dmi'
+ icon_state = "undertile"
+
+/obj/item/undertile_circuit/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/undertile, TRAIT_T_RAY_VISIBLE, INVISIBILITY_OBSERVER, use_anchor = TRUE)
+ AddComponent(/datum/component/shell, null, SHELL_CAPACITY_SMALL, SHELL_FLAG_REQUIRE_ANCHOR|SHELL_FLAG_USB_PORT)
diff --git a/code/modules/wiremod/shell/wallmount.dm b/code/modules/wiremod/shell/wallmount.dm
new file mode 100644
index 0000000000000..d461f696da131
--- /dev/null
+++ b/code/modules/wiremod/shell/wallmount.dm
@@ -0,0 +1,33 @@
+/obj/structure/wallmount_circuit
+ name = "circuit box"
+ desc = "A wall-mounted box suitable for the installation of integrated circuits."
+ icon = 'icons/obj/science/circuits.dmi'
+ icon_state = "wallmount"
+ layer = BELOW_OBJ_LAYER
+ anchored = TRUE
+
+ resistance_flags = LAVA_PROOF | FIRE_PROOF
+
+/obj/structure/wallmount_circuit/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/shell, null, SHELL_CAPACITY_LARGE, SHELL_FLAG_REQUIRE_ANCHOR|SHELL_FLAG_USB_PORT)
+
+/obj/structure/wallmount_circuit/wrench_act(mob/living/user, obj/item/tool)
+ var/datum/component/shell/shell_comp = GetComponent(/datum/component/shell)
+ if(shell_comp.locked)
+ balloon_alert(user, "locked!")
+ return ITEM_INTERACT_FAILURE
+ to_chat(user, span_notice("You start unsecuring the circuit box..."))
+ if(tool.use_tool(src, user, 40, volume=50))
+ to_chat(user, span_notice("You unsecure the circuit box."))
+ playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
+ deconstruct(TRUE)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/item/wallframe/circuit
+ name = "circuit box frame"
+ desc = "A box that can be mounted on a wall and have circuits installed."
+ icon = 'icons/obj/science/circuits.dmi'
+ icon_state = "wallmount_assembly"
+ result_path = /obj/structure/wallmount_circuit
+ pixel_shift = 32
diff --git a/html/changelogs/AutoChangeLog-pr-89013.yml b/html/changelogs/AutoChangeLog-pr-89013.yml
deleted file mode 100644
index ad7c6be528824..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-89013.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "AyIong"
-delete-after: True
-changes:
- - qol: "SmartFridge got another redesign, and you can dispense all amount of product in one click (RMB on product)"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-89047.yml b/html/changelogs/AutoChangeLog-pr-89047.yml
new file mode 100644
index 0000000000000..784e95a25ddc0
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-89047.yml
@@ -0,0 +1,5 @@
+author: "NamelessFairy"
+delete-after: True
+changes:
+ - bugfix: "Admin fired stray syndicate cargo pods will not rebel against admin whims and launch themselves when cancelled."
+ - admin: "Admins have new categories to fill syndicate cargo pods with."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-89085.yml b/html/changelogs/AutoChangeLog-pr-89085.yml
deleted file mode 100644
index a4df0c607ead2..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-89085.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "MTandi"
-delete-after: True
-changes:
- - image: "yet another medkit resprite"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-89089.yml b/html/changelogs/AutoChangeLog-pr-89089.yml
deleted file mode 100644
index c6e81b3c33688..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-89089.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-author: "MTandi"
-delete-after: True
-changes:
- - bugfix: "Fixed plant analyzer UI crashing on plants that have a reagent gene on their graft"
- - bugfix: "Plants with reagent traits in the graft now properly say the name of the grafted reagent"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-89095.yml b/html/changelogs/AutoChangeLog-pr-89095.yml
new file mode 100644
index 0000000000000..4e52e9a2c38cc
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-89095.yml
@@ -0,0 +1,4 @@
+author: "Pickle-Coding"
+delete-after: True
+changes:
+ - bugfix: "Fixes HFR moderator leaking not leaking properly when a part is cracked."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-89112.yml b/html/changelogs/AutoChangeLog-pr-89112.yml
deleted file mode 100644
index b4369a59d8308..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-89112.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SmArtKar"
-delete-after: True
-changes:
- - bugfix: "Fixed active turfs on the new Turreted Outpost ruin."
\ No newline at end of file
diff --git a/html/changelogs/archive/2025-01.yml b/html/changelogs/archive/2025-01.yml
index cddfcce1af8e8..d0707a54087d2 100644
--- a/html/changelogs/archive/2025-01.yml
+++ b/html/changelogs/archive/2025-01.yml
@@ -507,3 +507,43 @@
2025-01-18:
EnterTheJake:
- image: Codex Cicatrix has received a new sprite
+2025-01-19:
+ Absolucy:
+ - qol: Small and tiny mobs no longer trigger squeaking objects when walking over
+ them.
+ AyIong:
+ - qol: SmartFridge got another redesign, and you can dispense all amount of product
+ in one click (RMB on product)
+ Chowder-McArthor:
+ - qol: Strange geysers now say in their name what they produce.
+ Jacquerel:
+ - rscadd: Eggs (and all other edible food) left in a hot place will fry.
+ MTandi:
+ - bugfix: Fixed plant analyzer UI crashing on plants that have a reagent gene on
+ their graft
+ - bugfix: Plants with reagent traits in the graft now properly say the name of the
+ grafted reagent
+ - image: yet another medkit resprite
+ SmArtKar:
+ - bugfix: Fixed active turfs on the new Turreted Outpost ruin.
+2025-01-20:
+ RengaN02:
+ - bugfix: Fixed Engine Diagnostic surgery
+ SmArtKar:
+ - image: Resprited default and mirage grenades
+ - image: Made the unholy water flask darker
+ Y0SH1M4S73R:
+ - refactor: Wires and assemblies have been refactored to have directionality to
+ them. This mostly makes it so that assemblies can only be attached to wires
+ it would make sense for them to be attached to.
+ - qol: Pressure plates can now also accept igniters, condensers, flashes, assembly
+ shells, and door controllers.
+ - rscadd: Undertile circuit shells. They only work when placed under floor tiles,
+ but support USB cables and use APC power instead of cell power.
+ - rscadd: Wallmounted circuit shells. Large shells that support USB cables and use
+ APC power instead of cell power.
+ - rscadd: Wire bundle component. Adds a number of wires to the circuit proportional
+ to the capacity of the shell, allowing you to use assemblies in circuit logic.
+ mc-oofert:
+ - refactor: mimics (bolt of animation, malf ai Machine Override, etc) are basicmobs
+ - bugfix: crate mimics may now be opened
diff --git a/icons/obj/drinks/bottles.dmi b/icons/obj/drinks/bottles.dmi
index 2bdacf0426506..3dece4b41c6c9 100644
Binary files a/icons/obj/drinks/bottles.dmi and b/icons/obj/drinks/bottles.dmi differ
diff --git a/icons/obj/science/circuits.dmi b/icons/obj/science/circuits.dmi
index 1d50c67823bb2..e91025ae61365 100644
Binary files a/icons/obj/science/circuits.dmi and b/icons/obj/science/circuits.dmi differ
diff --git a/icons/obj/weapons/grenade.dmi b/icons/obj/weapons/grenade.dmi
index 628b271d423e5..c2eba7adcc1d6 100644
Binary files a/icons/obj/weapons/grenade.dmi and b/icons/obj/weapons/grenade.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 9a748f8af99fa..4ff518a0f731b 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -2006,6 +2006,7 @@
#include "code\datums\wires\syndicatebomb.dm"
#include "code\datums\wires\tesla_coil.dm"
#include "code\datums\wires\vending.dm"
+#include "code\datums\wires\wire_bundle_component.dm"
#include "code\datums\wounds\_wound_static_data.dm"
#include "code\datums\wounds\_wounds.dm"
#include "code\datums\wounds\blunt.dm"
@@ -5074,6 +5075,8 @@
#include "code\modules\mob\living\basic\ruin_defender\mad_piano.dm"
#include "code\modules\mob\living\basic\ruin_defender\skeleton.dm"
#include "code\modules\mob\living\basic\ruin_defender\stickman.dm"
+#include "code\modules\mob\living\basic\ruin_defender\mimic\mimic.dm"
+#include "code\modules\mob\living\basic\ruin_defender\mimic\mimic_ai.dm"
#include "code\modules\mob\living\basic\ruin_defender\wizard\wizard.dm"
#include "code\modules\mob\living\basic\ruin_defender\wizard\wizard_ai.dm"
#include "code\modules\mob\living\basic\ruin_defender\wizard\wizard_spells.dm"
@@ -5350,7 +5353,6 @@
#include "code\modules\mob\living\simple_animal\hostile\dark_wizard.dm"
#include "code\modules\mob\living\simple_animal\hostile\hostile.dm"
#include "code\modules\mob\living\simple_animal\hostile\illusion.dm"
-#include "code\modules\mob\living\simple_animal\hostile\mimic.dm"
#include "code\modules\mob\living\simple_animal\hostile\ooze.dm"
#include "code\modules\mob\living\simple_animal\hostile\vatbeast.dm"
#include "code\modules\mob\living\simple_animal\hostile\zombie.dm"
@@ -6473,6 +6475,7 @@
#include "code\modules\wiremod\components\utility\timepiece.dm"
#include "code\modules\wiremod\components\utility\typecast.dm"
#include "code\modules\wiremod\components\utility\typecheck.dm"
+#include "code\modules\wiremod\components\utility\wire_bundle.dm"
#include "code\modules\wiremod\components\variables\getter.dm"
#include "code\modules\wiremod\components\variables\setter.dm"
#include "code\modules\wiremod\components\wirenet\receive.dm"
@@ -6521,6 +6524,8 @@
#include "code\modules\wiremod\shell\scanner_gate.dm"
#include "code\modules\wiremod\shell\server.dm"
#include "code\modules\wiremod\shell\shell_items.dm"
+#include "code\modules\wiremod\shell\undertile.dm"
+#include "code\modules\wiremod\shell\wallmount.dm"
#include "code\modules\zombie\items.dm"
#include "code\modules\zombie\organs.dm"
#include "code\ze_genesis_call\genesis_call.dm"
diff --git a/tools/UpdatePaths/Scripts/88910_mimicbasicmobs.txt b/tools/UpdatePaths/Scripts/88910_mimicbasicmobs.txt
new file mode 100644
index 0000000000000..b7a012ed1d9b0
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/88910_mimicbasicmobs.txt
@@ -0,0 +1,5 @@
+/mob/living/simple_animal/hostile/mimic : /mob/living/basic/mimic/crate{@OLD}
+/mob/living/simple_animal/hostile/mimic/crate : /mob/living/basic/mimic/crate{@OLD}
+/mob/living/simple_animal/hostile/mimic/xenobio : /mob/living/basic/mimic/crate/xenobio{@OLD}
+/mob/living/simple_animal/hostile/mimic/copy : /mob/living/basic/mimic/copy{@OLD}
+/mob/living/simple_animal/hostile/mimic/copy/ranged : /mob/living/basic/mimic/copy/ranged{@OLD}