diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index e8bebbc1100..f5c86f91eb9 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -953,6 +953,14 @@
///sent from mecha action buttons to the mecha they're linked to
#define COMSIG_MECHA_ACTION_TRIGGER "mecha_action_activate"
+///sent from clicking while you have no equipment selected. Sent before cooldown and adjacency checks, so you can use this for infinite range things if you want.
+#define COMSIG_MECHA_MELEE_CLICK "mecha_action_melee_click"
+ /// Prevents click from happening.
+ #define COMPONENT_CANCEL_MELEE_CLICK (1<<0)
+///sent from clicking while you have equipment selected.
+#define COMSIG_MECHA_EQUIPMENT_CLICK "mecha_action_equipment_click"
+ /// Prevents click from happening.
+ #define COMPONENT_CANCEL_EQUIPMENT_CLICK (1<<0)
// /mob/living/carbon/human signals
diff --git a/code/__DEFINES/devices.dm b/code/__DEFINES/devices.dm
index 412e5d416d7..d28e16ff433 100644
--- a/code/__DEFINES/devices.dm
+++ b/code/__DEFINES/devices.dm
@@ -18,3 +18,16 @@
// Used by PDA and cartridge code to reduce repetitiveness of spritesheets
#define PDAIMG(what) {""}
+
+//N-spect scanner defines
+#define INSPECTOR_PRINT_SOUND_MODE_NORMAL 1
+#define INSPECTOR_PRINT_SOUND_MODE_CLASSIC 2
+#define INSPECTOR_PRINT_SOUND_MODE_HONK 3
+#define INSPECTOR_PRINT_SOUND_MODE_FAFAFOGGY 4
+#define BANANIUM_CLOWN_INSPECTOR_PRINT_SOUND_MODE_LAST 4
+#define CLOWN_INSPECTOR_PRINT_SOUND_MODE_LAST 4
+#define INSPECTOR_POWER_USAGE_HONK 15
+#define INSPECTOR_POWER_USAGE_NORMAL 5
+#define INSPECTOR_TIME_MODE_SLOW 1
+#define INSPECTOR_TIME_MODE_FAST 2
+#define INSPECTOR_TIME_MODE_HONK 3
diff --git a/code/__DEFINES/mecha.dm b/code/__DEFINES/mecha.dm
index 4689591960b..9acc9bfa9b9 100644
--- a/code/__DEFINES/mecha.dm
+++ b/code/__DEFINES/mecha.dm
@@ -13,6 +13,8 @@
#define HAS_LIGHTS (1<<6)
#define QUIET_STEPS (1<<7)
#define QUIET_TURNS (1<<8)
+///blocks using equipment and melee attacking.
+#define CANNOT_INTERACT (1<<9)
#define MECHA_MELEE (1 << 0)
#define MECHA_RANGED (1 << 1)
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index b4295b522d4..5ace6d01a99 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -435,6 +435,7 @@ GLOBAL_LIST_INIT(pda_styles, sortList(list(MONO, VT, ORBITRON, SHARE)))
#define BONE_SCAR_FILE "wounds/bone_scar_desc.json"
#define SCAR_LOC_FILE "wounds/scar_loc.json"
#define EXODRONE_FILE "exodrone.json"
+#define CLOWN_NONSENSE_FILE "clown_nonsense.json"
//Fullscreen overlay resolution in tiles.
#define FULLSCREEN_OVERLAY_RESOLUTION_X 15
diff --git a/code/__DEFINES/vehicles.dm b/code/__DEFINES/vehicles.dm
index eff970e61b8..3a2e5fc26ec 100644
--- a/code/__DEFINES/vehicles.dm
+++ b/code/__DEFINES/vehicles.dm
@@ -1,8 +1,18 @@
-//Vehicle control flags
+//Vehicle control flags. control flags describe access to actions in a vehicle.
-#define VEHICLE_CONTROL_PERMISSION 1
-#define VEHICLE_CONTROL_DRIVE 2
-#define VEHICLE_CONTROL_KIDNAPPED 4 //Can't leave vehicle voluntarily, has to resist.
+///controls the vehicles movement
+#define VEHICLE_CONTROL_DRIVE (1<<0)
+///Can't leave vehicle voluntarily, has to resist.
+#define VEHICLE_CONTROL_KIDNAPPED (1<<1)
+///melee attacks/shoves a vehicle may have
+#define VEHICLE_CONTROL_MELEE (1<<2)
+///using equipment/weapons on the vehicle
+#define VEHICLE_CONTROL_EQUIPMENT (1<<3)
+///changing around settings and the like.
+#define VEHICLE_CONTROL_SETTINGS (1<<4)
+
+///ez define for giving a single pilot mech all the flags it needs.
+#define FULL_MECHA_CONTROL ALL
//Ridden vehicle flags
diff --git a/code/_onclick/hud/fullscreen.dm b/code/_onclick/hud/fullscreen.dm
index 284632838ae..973645fba39 100644
--- a/code/_onclick/hud/fullscreen.dm
+++ b/code/_onclick/hud/fullscreen.dm
@@ -111,6 +111,10 @@
layer = CURSE_LAYER
plane = FULLSCREEN_PLANE
+/atom/movable/screen/fullscreen/ivanov_display
+ icon_state = "ivanov"
+ alpha = 180
+
/atom/movable/screen/fullscreen/impaired
icon_state = "impairedoverlay"
diff --git a/code/datums/components/crafting/recipes.dm b/code/datums/components/crafting/recipes.dm
index 7dc84d84e2b..963a82434a7 100644
--- a/code/datums/components/crafting/recipes.dm
+++ b/code/datums/components/crafting/recipes.dm
@@ -1299,6 +1299,17 @@
/datum/reagent/consumable/ice = 10
)
category = CAT_CHEMISTRY
+
+/**
+ * Recipe used for upgrading fake N-spect scanners to bananium HONK-spect scanners
+ */
+/datum/crafting_recipe/clown_scanner_upgrade
+ name = "Bananium HONK-spect scanner"
+ result = /obj/item/inspector/clown/bananium
+ reqs = list(/obj/item/inspector/clown = 1, /obj/item/stack/sticky_tape = 3, /obj/item/stack/sheet/mineral/bananium = 5) //the chainsaw of prank tools
+ tool_paths = list(/obj/item/bikehorn)
+ time = 40 SECONDS
+ category = CAT_MISC
#undef CRAFTING_MACHINERY_CONSUME
#undef CRAFTING_MACHINERY_USE
diff --git a/code/game/objects/items/documents.dm b/code/game/objects/items/documents.dm
index 54367e75bd1..deff2b6e50b 100644
--- a/code/game/objects/items/documents.dm
+++ b/code/game/objects/items/documents.dm
@@ -57,6 +57,11 @@
to_chat(user, "You forge the official seal with a [C.crayon_color] crayon. No one will notice... right?")
update_appearance()
+/**
+ * # N-spect scanner
+ *
+ * Creates reports for area inspection bounties.
+ */
/obj/item/inspector
name = "\improper N-spect scanner"
desc = "Central Command-issued inspection device. Performs inspections according to Nanotrasen protocols when activated, then \
@@ -69,18 +74,124 @@
w_class = WEIGHT_CLASS_TINY
throw_range = 1
throw_speed = 1
+ ///How long it takes to print on time each mode, ordered NORMAL, FAST, HONK
+ var/list/time_list = list(5 SECONDS, 1 SECONDS, 0.1 SECONDS)
+ ///Which print time mode we're on.
+ var/time_mode = INSPECTOR_TIME_MODE_SLOW
+ ///determines the sound that plays when printing a report
+ var/print_sound_mode = INSPECTOR_PRINT_SOUND_MODE_NORMAL
+ ///Power cell used to power the scanner. Paths g
+ var/obj/item/stock_parts/cell/cell = /obj/item/stock_parts/cell/crap
+ ///Cell cover status
+ var/cell_cover_open = FALSE
+ ///Power used per print in cell units
+ var/power_per_print = INSPECTOR_POWER_USAGE_NORMAL
+ ///Power used to say an error message
+ var/power_to_speak = 1
+
+/obj/item/inspector/Initialize()
+ . = ..()
+ if(ispath(cell))
+ cell = new cell(src)
+
+// Clean up the cell on destroy
+/obj/item/clothing/suit/space/Destroy()
+ if(cell)
+ QDEL_NULL(cell)
+ return ..()
+
+// Clean up the cell on destroy
+/obj/item/inspector/handle_atom_del(atom/A)
+ if(A == cell)
+ cell = null
+ return ..()
+
+// support for items that interact with the cell
+/obj/item/inspector/get_cell()
+ return cell
/obj/item/inspector/attack_self(mob/user)
. = ..()
- if(do_after(user, 5 SECONDS, target = user, progress=TRUE))
- print_report()
+ if(do_after(user, time_list[time_mode], target = user, progress=TRUE))
+ print_report(user)
-///Prints out a report for bounty purposes, and plays a short audio blip.
-/obj/item/inspector/proc/print_report()
- // Create our report
+/obj/item/inspector/crowbar_act(mob/living/user, obj/item/tool)
+ . = ..()
+ if(user.combat_mode)
+ return
+ cell_cover_open = !cell_cover_open
+ balloon_alert(user, "You [cell_cover_open ? "open" : "close"] the cell cover on \the [src].")
+ return TRUE
+
+
+/obj/item/inspector/attackby(obj/item/I, mob/user, params)
+ if(cell_cover_open && istype(I, /obj/item/stock_parts/cell))
+ if(cell)
+ to_chat(user, "[src] already has a cell installed.")
+ return
+ if(user.transferItemToLoc(I, src))
+ cell = I
+ to_chat(user, "You successfully install \the [cell] into [src].")
+ return
+ return ..()
+
+/obj/item/inspector/CtrlClick(mob/living/user)
+ if(!user.canUseTopic(src, BE_CLOSE, NO_DEXTERITY, FALSE, !iscyborg(user)) || !cell_cover_open || !cell)
+ return ..()
+ user.visible_message("[user] removes \the [cell] from [src]!", \
+ "You remove [cell].")
+ cell.add_fingerprint(user)
+ user.put_in_hands(cell)
+ cell = null
+
+
+/obj/item/inspector/examine(mob/user)
+ . = ..()
+ if(!cell_cover_open)
+ . += "Its cell cover is closed. It looks like it could be pried out, but doing so would require an appropriate tool."
+ return
+ . += "It's cell cover is open, exposing the cell slot. It looks like it could be pried in, but doing so would require an appropriate tool."
+ if(!cell)
+ . += "The slot for a cell is empty."
+ else
+ . += "\The [cell] is firmly in place. Ctrl-click with an empty hand to remove it."
+
+/**
+ * Create our report
+ *
+ * Arguments:
+ */
+/obj/item/inspector/proc/create_slip()
var/obj/item/paper/report/slip = new(get_turf(src))
slip.generate_report(get_area(src))
- playsound(src, 'sound/machines/high_tech_confirm.ogg', 50, FALSE)
+
+/**
+ * Prints out a report for bounty purposes, and plays a short audio blip.
+ *
+ * Arguments:
+*/
+/obj/item/inspector/proc/print_report(mob/user)
+ if(!cell)
+ to_chat(user, "\The [src] doesn't seem to be on... It feels quite light. Perhaps it lacks a power cell?")
+ return
+ if(cell.charge == 0)
+ to_chat(user, "\The [src] doesn't seem to be on... Perhaps it ran out of power?")
+ return
+ if(!cell.use(power_per_print))
+ if(cell.use(power_to_speak))
+ say("ERROR! POWER CELL CHARGE LEVEL TOO LOW TO PRINT REPORT!")
+ return
+
+ create_slip()
+ switch(print_sound_mode)
+ if(INSPECTOR_PRINT_SOUND_MODE_NORMAL)
+ playsound(src, 'sound/machines/high_tech_confirm.ogg', 50, FALSE)
+ if(INSPECTOR_PRINT_SOUND_MODE_CLASSIC)
+ playsound(src, 'sound/items/biddledeep.ogg', 50, FALSE)
+ if(INSPECTOR_PRINT_SOUND_MODE_HONK)
+ playsound(src, 'sound/items/bikehorn.ogg', 50, FALSE)
+ if(INSPECTOR_PRINT_SOUND_MODE_FAFAFOGGY)
+ playsound(src, pick(list('sound/items/robofafafoggy.ogg', 'sound/items/robofafafoggy2.ogg')), 50, FALSE)
/obj/item/paper/report
name = "encrypted station inspection"
@@ -115,3 +226,223 @@
. += "Wait a minute, this isn't an encrypted inspection report! You should throw it away."
else
. += "Wait a minute, this thing's blank! You should throw it away."
+
+/**
+ * # Fake N-spect scanner
+ *
+ * A clown variant of the N-spect scanner
+ *
+ * This prints fake reports with garbage in them,
+ * can be set to print them instantly with a screwdriver.
+ * By default it plays the old "woody" scanning sound, scanning sounds can be cycled by clicking with a multitool.
+ * Can be crafted into a bananium HONK-spect scanner
+ */
+/obj/item/inspector/clown
+ ///will only cycle through modes with numbers lower than this
+ var/max_mode = CLOWN_INSPECTOR_PRINT_SOUND_MODE_LAST
+ ///names of modes, ordered first to last
+ var/list/mode_names = list("normal", "classic", "honk", "fafafoggy")
+
+/obj/item/inspector/clown/attack(mob/living/M, mob/living/user)
+ . = ..()
+ print_report(user)
+
+/obj/item/inspector/clown/screwdriver_act(mob/living/user, obj/item/tool)
+ if(!cell_cover_open)
+ return ..()
+ cycle_print_time(user)
+ return TRUE
+
+/obj/item/inspector/clown/attackby(obj/item/I, mob/user, params)
+ if(cell_cover_open && istype(I, /obj/item/kitchen/fork))
+ cycle_sound(user)
+ return
+ return ..()
+
+/obj/item/inspector/clown/examine(mob/user)
+ . = ..()
+ if(cell_cover_open)
+ . += "Two weird settings dials are visible within the battery compartment."
+
+/obj/item/inspector/clown/examine_more(mob/user)
+ if(!cell_cover_open)
+ return ..()
+ . = list("Both setting dials are flush with the surface of the battery compartment, and seem to be impossible to move with bare hands.")
+ . += "\tThe first dial is labeled \"SPEED\" and looks a bit like a screw head."
+ . += "\tThe second dial is labeled \"SOUND\". It has four small holes in it. Perhaps it can be turned with a fork?"
+ . += "\tA small bananium part labeled \"ADVANCED WATER CHIP 23000000\" is visible within the battery compartment. It looks completely unlike normal modern electronics, disturbing it would be rather unwise."
+
+
+/obj/item/inspector/clown/proc/cycle_print_time(mob/user)
+ var/message
+ if(time_mode == INSPECTOR_TIME_MODE_FAST)
+ time_mode = INSPECTOR_TIME_MODE_SLOW
+ message = "SLOW."
+ else
+ time_mode = INSPECTOR_TIME_MODE_FAST
+ message = "LIGHTNING FAST."
+
+ balloon_alert(user, "You turn the screw-like dial, setting the device's scanning speed to [message]")
+
+/obj/item/inspector/clown/proc/cycle_sound(mob/user)
+ print_sound_mode++
+ if(print_sound_mode > max_mode)
+ print_sound_mode = INSPECTOR_PRINT_SOUND_MODE_NORMAL
+ balloon_alert(user, "You turn the dial with holes in it, setting the device's bleep setting to [mode_names[print_sound_mode]] mode.")
+
+/obj/item/inspector/clown/create_slip()
+ var/obj/item/paper/fake_report/slip = new(get_turf(src))
+ slip.generate_report(get_area(src))
+
+/**
+ * # Bananium HONK-spect scanner
+ *
+ * An upgraded version of the fake N-spect scanner
+ *
+ * Can print things way faster, at full power the reports printed by this will destroy
+ * themselves and leave water behind when folding is attempted by someone who isn't an
+ * origami master. Printing at full power costs INSPECTOR_POWER_USAGE_HONK cell units
+ * instead of INSPECTOR_POWER_USAGE_NORMAL cell units.
+ */
+/obj/item/inspector/clown/bananium
+ name = "\improper Bananium HONK-spect scanner"
+ desc = "Honkmother-blessed inspection device. Performs inspections according to Clown protocols when activated, then \
+ prints a clowncrypted report regarding the maintenance of the station. Hard to replace."
+ icon = 'icons/obj/tools.dmi'
+ icon_state = "bananium_inspector"
+ w_class = WEIGHT_CLASS_SMALL
+ max_mode = BANANIUM_CLOWN_INSPECTOR_PRINT_SOUND_MODE_LAST
+ ///How many more times can we print?
+ var/paper_charges = 32
+ ///Max value of paper_charges
+ var/max_paper_charges = 32
+ ///How much charges are restored per paper consumed
+ var/charges_per_paper = 1
+
+/obj/item/inspector/clown/bananium/proc/check_settings_legality()
+ if(print_sound_mode == INSPECTOR_PRINT_SOUND_MODE_NORMAL && time_mode == INSPECTOR_TIME_MODE_HONK)
+ if(cell.use(power_to_speak))
+ say("Setting combination forbidden by Geneva convention revision CCXXIII selected, reverting to defaults")
+ time_mode = INSPECTOR_TIME_MODE_SLOW
+ print_sound_mode = INSPECTOR_PRINT_SOUND_MODE_NORMAL
+ power_per_print = INSPECTOR_POWER_USAGE_NORMAL
+
+/obj/item/inspector/clown/bananium/screwdriver_act(mob/living/user, obj/item/tool)
+ . = ..()
+ check_settings_legality()
+ return TRUE
+
+/obj/item/inspector/clown/bananium/attackby(obj/item/I, mob/user, params)
+ . = ..()
+ if(cell_cover_open)
+ check_settings_legality()
+ if(istype(I, /obj/item/paper/fake_report) || paper_charges >= max_paper_charges)
+ to_chat(user, "\The [src] refuses to consume \the [I]!")
+ return
+ if(istype(I, /obj/item/paper))
+ to_chat(user, "\The [src] consumes \the [I]!")
+ paper_charges = min(paper_charges + charges_per_paper, max_paper_charges)
+ qdel(I)
+
+/obj/item/inspector/clown/bananium/Initialize()
+ . = ..()
+ playsound(src, 'sound/effects/angryboat.ogg', 150, FALSE)
+
+/obj/item/inspector/clown/bananium/create_slip()
+ if(time_mode == INSPECTOR_TIME_MODE_HONK)
+ var/obj/item/paper/fake_report/water/slip = new(get_turf(src))
+ slip.generate_report(get_area(src))
+ return
+ return ..()
+
+/obj/item/inspector/clown/bananium/print_report(mob/user)
+ if(time_mode != INSPECTOR_TIME_MODE_HONK)
+ return ..()
+ if(paper_charges == 0)
+ if(cell.use(power_to_speak))
+ say("ERROR! OUT OF PAPER! MAXIMUM PRINTING SPEED UNAVAIBLE! SWITCH TO A SLOWER SPEED TO OR PROVIDE PAPER!")
+ else
+ to_chat(user, "\The [src] doesn't seem to be on... Perhaps it ran out of power?")
+ return
+ paper_charges--
+ return ..()
+
+/obj/item/inspector/clown/bananium/cycle_print_time(mob/user)
+ var/message
+ switch(time_mode)
+ if(INSPECTOR_TIME_MODE_HONK)
+ power_per_print = INSPECTOR_POWER_USAGE_NORMAL
+ time_mode = INSPECTOR_TIME_MODE_SLOW
+ message = "SLOW."
+ if(INSPECTOR_TIME_MODE_SLOW)
+ time_mode = INSPECTOR_TIME_MODE_FAST
+ message = "LIGHTNING FAST."
+ else
+ time_mode = INSPECTOR_TIME_MODE_HONK
+ power_per_print = INSPECTOR_POWER_USAGE_HONK
+ message = "HONK!"
+ balloon_alert(user, "You turn the screw-like dial, setting the device's scanning speed to [message]")
+
+/**
+ * Reports printed by fake N-spect scanner
+ *
+ * Not valid for the bounty.
+ */
+/obj/item/paper/fake_report
+ name = "encrypted station inspection"
+ desc = "Contains no information about the station's current status."
+ icon = 'icons/obj/bureaucracy.dmi'
+ icon_state = "slip"
+ show_written_words = FALSE
+ ///What area the inspector scanned when the report was made. Used to generate the examine text of the report
+ var/area/scanned_area
+
+/obj/item/paper/fake_report/proc/generate_report(area/scan_area)
+ scanned_area = scan_area
+ icon_state = "slipfull"
+
+ var/list/new_info = list()
+ for(var/i in 1 to rand(23, 123))
+ var/roll = rand(0, 1000)
+ switch(roll)
+ if(0 to 900)
+ new_info += pick_list_replacements(CLOWN_NONSENSE_FILE, "honk")
+ if(901 to 999)
+ new_info += pick_list_replacements(CLOWN_NONSENSE_FILE, "non-honk-clown-words")
+ if(1000)
+ new_info += pick_list_replacements(CLOWN_NONSENSE_FILE, "rare")
+ info += new_info.Join()
+
+/obj/item/paper/fake_report/examine(mob/user)
+ . = ..()
+ if(scanned_area?.name)
+ . += "\The [src] contains no data on [scanned_area.name]."
+ else if(scanned_area)
+ . += "\The [src] contains no data on a vague area on station, you should throw it away."
+ else if(info)
+ . += "Wait a minute, this isn't an encrypted inspection report! You should throw it away."
+ else
+ . += "Wait a minute, this thing's blank! You should throw it away."
+
+/**
+ * # Fake report made of water
+ *
+ * Fake report but it turns into water under certain circumstances.
+ *
+ * If someone who isn't an origami master tries to fold it into a paper plane, it will make the floor it's on wet and disappear.
+ * If it is ground, it will turn into 5u water.
+ */
+/obj/item/paper/fake_report/water
+ grind_results = list(/datum/reagent/water = 5)
+
+/obj/item/paper/fake_report/water/AltClick(mob/living/user, obj/item/I)
+ if(!user.canUseTopic(src, BE_CLOSE, NO_DEXTERITY, FALSE, TRUE))
+ return
+ var/datum/action/innate/origami/origami_action = locate() in user.actions
+ if(origami_action?.active) //Origami masters can fold water
+ make_plane(user, I, /obj/item/paperplane/syndicate)
+ else if(do_after(user, 1 SECONDS, target = src, progress=TRUE))
+ var/turf/open/target = get_turf(src)
+ target.MakeSlippery(TURF_WET_WATER, min_wet_time = 10 SECONDS, wet_time_to_add = 5 SECONDS)
+ to_chat(user, "As you try to fold [src] into the shape of a plane, it disintegrates into water!")
+ qdel(src)
diff --git a/code/modules/cargo/blackmarket/blackmarket_items/tools.dm b/code/modules/cargo/blackmarket/blackmarket_items/tools.dm
index 0b2cb16aa82..65543d1fd0b 100644
--- a/code/modules/cargo/blackmarket/blackmarket_items/tools.dm
+++ b/code/modules/cargo/blackmarket/blackmarket_items/tools.dm
@@ -80,3 +80,19 @@
price_max = 200
stock_max = 3
availability_prob = 50
+
+/**
+ * # Fake N-spect scanner black market entry
+ */
+/datum/blackmarket_item/tool/fake_scanner
+ name = "Clowny N-spect scanner"
+ desc = "This UPGRADED N-spect scanner can play FIVE HIGH-QUALITY SOUNDS (fork required for sound adjustment not included) and print reports \
+ LIGHTNING FAST (screwdriver necessary to activate maximum speed not included). We make no claims as to the usefulness of the reports printed by this. \
+ Any and all implied warranties are void if the device is touched, moved, kicked, thrown or modified with bananium sheets. Batteries included. \
+ Crowbar necessary to change batteries and adjust settings not included."
+ item = /obj/item/inspector/clown
+
+ price_min = 230
+ price_max = 323
+ stock_max = 2
+ availability_prob = 50
diff --git a/code/modules/paperwork/paperplane.dm b/code/modules/paperwork/paperplane.dm
index 01140868b01..42ee665ab6e 100644
--- a/code/modules/paperwork/paperplane.dm
+++ b/code/modules/paperwork/paperplane.dm
@@ -123,14 +123,24 @@
if(!Carbon.iscopy && !Carbon.copied)
to_chat(user, "Take off the carbon copy first.")
return
- to_chat(user, "You fold [src] into the shape of a plane!")
- user.temporarilyRemoveItemFromInventory(src)
- var/obj/item/paperplane/plane_type = /obj/item/paperplane
//Origami Master
var/datum/action/innate/origami/origami_action = locate() in user.actions
if(origami_action?.active)
- plane_type = /obj/item/paperplane/syndicate
+ make_plane(user, I, /obj/item/paperplane/syndicate)
+ else
+ make_plane(user, I, /obj/item/paperplane)
+/**
+ * Paper plane folding
+ *
+ * Arguments:
+ * * mob/living/user - who's folding
+ * * obj/item/I - what's being folded
+ * * obj/item/paperplane/plane_type - what it will be folded into (path)
+ */
+/obj/item/paper/proc/make_plane(mob/living/user, obj/item/I, obj/item/paperplane/plane_type = /obj/item/paperplane)
+ to_chat(user, "You fold [src] into the shape of a plane!")
+ user.temporarilyRemoveItemFromInventory(src)
I = new plane_type(loc, src)
if(user.Adjacent(I))
user.put_in_hands(I)
diff --git a/code/modules/research/designs/mecha_designs.dm b/code/modules/research/designs/mecha_designs.dm
index d1de52f6bdb..bb7e24562ad 100644
--- a/code/modules/research/designs/mecha_designs.dm
+++ b/code/modules/research/designs/mecha_designs.dm
@@ -149,6 +149,30 @@
category = list("Exosuit Modules")
departmental_flags = DEPARTMENTAL_FLAG_SCIENCE
+/datum/design/board/savannah_ivanov_main
+ name = "\"Savannah-Ivanov\" Central Control module"
+ desc = "Allows for the construction of a \"Savannah-Ivanov\" Central Control module."
+ id = "savannah_ivanov_main"
+ build_path = /obj/item/circuitboard/mecha/savannah_ivanov/main
+ category = list("Exosuit Modules")
+ departmental_flags = DEPARTMENTAL_FLAG_SCIENCE
+
+/datum/design/board/savannah_ivanov_peri
+ name = "\"Savannah-Ivanov\" Peripherals Control module"
+ desc = "Allows for the construction of a \"Savannah-Ivanov\" Peripheral Control module."
+ id = "savannah_ivanov_peri"
+ build_path = /obj/item/circuitboard/mecha/savannah_ivanov/peripherals
+ category = list("Exosuit Modules")
+ departmental_flags = DEPARTMENTAL_FLAG_SCIENCE
+
+/datum/design/board/savannah_ivanov_targ
+ name = "\"Savannah-Ivanov\" Weapons & Targeting Control module"
+ desc = "Allows for the construction of a \"Savannah-Ivanov\" Weapons & Targeting Control module."
+ id = "savannah_ivanov_targ"
+ build_path = /obj/item/circuitboard/mecha/savannah_ivanov/targeting
+ category = list("Exosuit Modules")
+ departmental_flags = DEPARTMENTAL_FLAG_SCIENCE
+
////////////////////////////////////////
/////////// Mecha Equpment /////////////
////////////////////////////////////////
diff --git a/code/modules/research/designs/mechfabricator_designs.dm b/code/modules/research/designs/mechfabricator_designs.dm
index c63f8b42343..4dca862e83e 100644
--- a/code/modules/research/designs/mechfabricator_designs.dm
+++ b/code/modules/research/designs/mechfabricator_designs.dm
@@ -465,6 +465,124 @@
construction_time = 300
category = list("Phazon")
+//Savannah-Ivanov
+/datum/design/savannah_ivanov_chassis
+ name = "Exosuit Chassis (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_chassis"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/chassis/savannah_ivanov
+ materials = list(/datum/material/iron=20000)
+ construction_time = 100
+ category = list("Savannah-Ivanov")
+
+/datum/design/savannah_ivanov_torso
+ name = "Exosuit Torso (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_torso"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/part/savannah_ivanov_torso
+ materials = list(/datum/material/iron=20000,/datum/material/glass = 7500)
+ construction_time = 200
+ category = list("Savannah-Ivanov")
+
+/datum/design/savannah_ivanov_head
+ name = "Exosuit Head (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_head"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/part/savannah_ivanov_head
+ materials = list(/datum/material/iron=6000,/datum/material/glass = 10000)
+ construction_time = 100
+ category = list("Savannah-Ivanov")
+
+/datum/design/savannah_ivanov_left_arm
+ name = "Exosuit Left Arm (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_left_arm"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/part/savannah_ivanov_left_arm
+ materials = list(/datum/material/iron=15000)
+ construction_time = 150
+ category = list("Savannah-Ivanov")
+
+/datum/design/savannah_ivanov_right_arm
+ name = "Exosuit Right Arm (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_right_arm"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/part/savannah_ivanov_right_arm
+ materials = list(/datum/material/iron=15000)
+ construction_time = 150
+ category = list("Savannah-Ivanov")
+
+/datum/design/savannah_ivanov_chassis
+ name = "Exosuit Chassis (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_chassis"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/chassis/savannah_ivanov
+ materials = list(/datum/material/iron=25000)
+ construction_time = 100
+ category = list("Savannah-Ivanov")
+
+/datum/design/savannah_ivanov_torso
+ name = "Exosuit Torso (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_torso"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/part/savannah_ivanov_torso
+ materials = list(/datum/material/iron=25000, /datum/material/glass = 10000,/datum/material/silver=10000)
+ construction_time = 300
+ category = list("Savannah-Ivanov")
+
+/datum/design/savannah_ivanov_head
+ name = "Exosuit Head (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_head"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/part/savannah_ivanov_head
+ materials = list(/datum/material/iron=10000,/datum/material/glass = 15000,/datum/material/silver=2000)
+ construction_time = 200
+ category = list("Savannah-Ivanov")
+
+/datum/design/savannah_ivanov_left_arm
+ name = "Exosuit Left Arm (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_left_arm"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/part/savannah_ivanov_left_arm
+ materials = list(/datum/material/iron=10000,/datum/material/silver=4000)
+ construction_time = 200
+ category = list("Savannah-Ivanov")
+
+/datum/design/savannah_ivanov_right_arm
+ name = "Exosuit Right Arm (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_right_arm"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/part/savannah_ivanov_right_arm
+ materials = list(/datum/material/iron=10000,/datum/material/silver=4000)
+ construction_time = 200
+ category = list("Savannah-Ivanov")
+
+/datum/design/savannah_ivanov_left_leg
+ name = "Exosuit Left Leg (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_left_leg"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/part/savannah_ivanov_left_leg
+ materials = list(/datum/material/iron=15000,/datum/material/silver=4000)
+ construction_time = 200
+ category = list("Savannah-Ivanov")
+
+/datum/design/savannah_ivanov_right_leg
+ name = "Exosuit Right Leg (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_right_leg"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/part/savannah_ivanov_right_leg
+ materials = list(/datum/material/iron=15000,/datum/material/silver=4000)
+ construction_time = 200
+ category = list("Savannah-Ivanov")
+
+/datum/design/savannah_ivanov_armor
+ name = "Exosuit Armor (\"Savannah-Ivanov\")"
+ id = "savannah_ivanov_armor"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/part/savannah_ivanov_armor
+ materials = list(/datum/material/iron=30000,/datum/material/uranium=25000,/datum/material/titanium=20000)
+ construction_time = 600
+ category = list("Savannah-Ivanov")
+
//Clarke
/datum/design/clarke_chassis
name = "Exosuit Chassis (\"Clarke\")"
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index 6906df97d64..5adb906aa60 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -1523,6 +1523,26 @@
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+/datum/techweb_node/savannah_ivanov
+ id = "mecha_savannah_ivanov"
+ display_name = "EXOSUIT: Savannah-Ivanov"
+ description = "Savannah-Ivanov exosuit designs"
+ prereq_ids = list("adv_mecha", "weaponry", "exp_tools")
+ design_ids = list(
+ "savannah_ivanov_armor",
+ "savannah_ivanov_chassis",
+ "savannah_ivanov_head",
+ "savannah_ivanov_left_arm",
+ "savannah_ivanov_left_leg",
+ "savannah_ivanov_main",
+ "savannah_ivanov_peri",
+ "savannah_ivanov_right_arm",
+ "savannah_ivanov_right_leg",
+ "savannah_ivanov_targ",
+ "savannah_ivanov_torso",
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+
/datum/techweb_node/adv_mecha_tools
id = "adv_mecha_tools"
display_name = "Advanced Exosuit Equipment"
diff --git a/code/modules/vehicles/_vehicle.dm b/code/modules/vehicles/_vehicle.dm
index e2f03958421..0a8014b50f4 100644
--- a/code/modules/vehicles/_vehicle.dm
+++ b/code/modules/vehicles/_vehicle.dm
@@ -23,7 +23,7 @@
var/key_type
///The inserted key, needed on some vehicles to start the engine
var/obj/item/key/inserted_key
- /// Whether the vehicle os currently able to move
+ /// Whether the vehicle is currently able to move
var/canmove = TRUE
var/list/autogrant_actions_passenger //plain list of typepaths
var/list/autogrant_actions_controller //assoc list "[bitflag]" = list(typepaths)
@@ -102,9 +102,10 @@
/obj/vehicle/proc/auto_assign_occupant_flags(mob/M) //override for each type that needs it. Default is assign driver if drivers is not at max.
if(driver_amount() < max_drivers)
- add_control_flags(M, VEHICLE_CONTROL_DRIVE|VEHICLE_CONTROL_PERMISSION)
+ add_control_flags(M, VEHICLE_CONTROL_DRIVE)
/obj/vehicle/proc/remove_occupant(mob/M)
+ SHOULD_CALL_PARENT(TRUE)
if(!istype(M))
return FALSE
remove_control_flags(M, ALL)
diff --git a/code/modules/vehicles/cars/clowncar.dm b/code/modules/vehicles/cars/clowncar.dm
index f2ce09ad06f..f9f637d7ba3 100644
--- a/code/modules/vehicles/cars/clowncar.dm
+++ b/code/modules/vehicles/cars/clowncar.dm
@@ -40,7 +40,7 @@
if(ishuman(M))
var/mob/living/carbon/human/H = M
if(H.mind?.assigned_role == "Clown") //Ensures only clowns can drive the car. (Including more at once)
- add_control_flags(H, VEHICLE_CONTROL_DRIVE|VEHICLE_CONTROL_PERMISSION)
+ add_control_flags(H, VEHICLE_CONTROL_DRIVE)
RegisterSignal(H, COMSIG_MOB_CLICKON, .proc/fire_cannon_at)
M.log_message("has entered [src] as a possible driver", LOG_ATTACK)
return
diff --git a/code/modules/vehicles/mecha/_mecha.dm b/code/modules/vehicles/mecha/_mecha.dm
index 40a14174886..b351be06f37 100644
--- a/code/modules/vehicles/mecha/_mecha.dm
+++ b/code/modules/vehicles/mecha/_mecha.dm
@@ -151,8 +151,8 @@
///Cooldown between using smoke
var/smoke_cooldown = 10 SECONDS
- ///Bool for if the mech is currently phasing
- var/phasing = FALSE
+ ///check for phasing, if it is set to text (to describe how it is phasing: "flying", "phasing") it will let the mech walk through walls.
+ var/phasing = ""
///Power we use every time we phaze through something
var/phasing_energy_drain = 200
///icon_state for flick() when phazing
@@ -252,15 +252,21 @@
/obj/vehicle/sealed/mecha/update_icon_state()
- if((mecha_flags & SILICON_PILOT) && silicon_icon_state)
- icon_state = silicon_icon_state
- return ..()
- if(LAZYLEN(occupants))
- icon_state = base_icon_state
- return ..()
- icon_state = "[base_icon_state]-open"
+ icon_state = get_mecha_occupancy_state()
return ..()
+//override this proc if you need to split up mecha control between multiple people (see savannah_ivanov.dm)
+/obj/vehicle/sealed/mecha/auto_assign_occupant_flags(mob/M)
+ if(driver_amount() < max_drivers)
+ add_control_flags(M, FULL_MECHA_CONTROL)
+
+/obj/vehicle/sealed/mecha/proc/get_mecha_occupancy_state()
+ if((mecha_flags & SILICON_PILOT) && silicon_icon_state)
+ return silicon_icon_state
+ if(LAZYLEN(occupants))
+ return base_icon_state
+ return "[base_icon_state]-open"
+
/obj/vehicle/sealed/mecha/CanPassThrough(atom/blocker, turf/target, blocker_opinion)
if(!phasing || get_charge() <= phasing_energy_drain || throwing)
return ..()
@@ -515,18 +521,18 @@
SIGNAL_HANDLER
if(!isturf(target) && !isturf(target.loc)) // Prevents inventory from being drilled
return
- if(completely_disabled || is_currently_ejecting)
+ if(completely_disabled || is_currently_ejecting || (mecha_flags & CANNOT_INTERACT))
return
var/list/modifiers = params2list(params)
if(isAI(user) == !LAZYACCESS(modifiers, MIDDLE_CLICK))//BASICALLY if a human uses MMB, or an AI doesn't, then do nothing.
return
if(phasing)
- to_chat(occupants, "[icon2html(src, occupants)]Unable to interact with objects while phasing.")
+ balloon_alert(user, "not while [phasing]!")
return
if(user.incapacitated())
return
if(construction_state)
- to_chat(occupants, "[icon2html(src, occupants)]Maintenance protocols in effect.")
+ balloon_alert(user, "end maintenance first!")
return
if(!get_charge())
return
@@ -539,22 +545,37 @@
target = pick(view(3,target))
var/mob/living/livinguser = user
if(selected)
+ if(!(livinguser in return_controllers_with_flag(VEHICLE_CONTROL_EQUIPMENT)))
+ balloon_alert(user, "wrong seat for equipment!")
+ return
if(!Adjacent(target) && (selected.range & MECHA_RANGED))
if(HAS_TRAIT(livinguser, TRAIT_PACIFISM) && selected.harmful)
to_chat(livinguser, "You don't want to harm other living beings!")
return
+ if(SEND_SIGNAL(src, COMSIG_MECHA_EQUIPMENT_CLICK, livinguser, target) & COMPONENT_CANCEL_EQUIPMENT_CLICK)
+ return
INVOKE_ASYNC(selected, /obj/item/mecha_parts/mecha_equipment.proc/action, user, target, params)
return
if((selected.range & MECHA_MELEE) && Adjacent(target))
if(isliving(target) && selected.harmful && HAS_TRAIT(livinguser, TRAIT_PACIFISM))
to_chat(livinguser, "You don't want to harm other living beings!")
return
+ if(SEND_SIGNAL(src, COMSIG_MECHA_EQUIPMENT_CLICK, livinguser, target) & COMPONENT_CANCEL_EQUIPMENT_CLICK)
+ return
INVOKE_ASYNC(selected, /obj/item/mecha_parts/mecha_equipment.proc/action, user, target, params)
return
- if(TIMER_COOLDOWN_CHECK(src, COOLDOWN_MECHA_MELEE_ATTACK) || !Adjacent(target))
+ if(!(livinguser in return_controllers_with_flag(VEHICLE_CONTROL_MELEE)))
+ to_chat(livinguser, "You're in the wrong seat to interact with your hands.")
+ return
+ var/on_cooldown = TIMER_COOLDOWN_CHECK(src, COOLDOWN_MECHA_MELEE_ATTACK)
+ var/adjacent = Adjacent(target)
+ if(SEND_SIGNAL(src, COMSIG_MECHA_MELEE_CLICK, livinguser, target, on_cooldown, adjacent) & COMPONENT_CANCEL_MELEE_CLICK)
+ return
+ if(on_cooldown || !adjacent)
return
if(internal_damage & MECHA_INT_CONTROL_LOST)
target = pick(oview(1,src))
+
target.mech_melee_attack(src, user)
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MELEE_ATTACK, melee_cooldown)
@@ -604,9 +625,10 @@
return FALSE
/obj/vehicle/sealed/mecha/relaymove(mob/living/user, direction)
- if(canmove)
- vehicle_move(direction)
- return TRUE
+ . = TRUE
+ if(!canmove || !(user in return_drivers()))
+ return
+ vehicle_move(direction)
/obj/vehicle/sealed/mecha/vehicle_move(direction, forcerotate = FALSE)
if(!COOLDOWN_FINISHED(src, cooldown_vehicle_move))
@@ -691,7 +713,7 @@
. = ..()
if(phasing) //Theres only one cause for phasing canpass fails
addtimer(VARSET_CALLBACK(src, movedelay, TRUE), movedelay*3)
- to_chat(occupants, "[icon2html(src, occupants)]A dull, universal force is preventing you from phasing here!")
+ to_chat(occupants, "[icon2html(src, occupants)]A dull, universal force is preventing you from [phasing] here!")
spark_system.start()
return
if(.) //mech was thrown/door/whatever
@@ -934,11 +956,11 @@
/obj/vehicle/sealed/mecha/generate_actions()
initialize_passenger_action_type(/datum/action/vehicle/sealed/mecha/mech_eject)
- initialize_passenger_action_type(/datum/action/vehicle/sealed/mecha/mech_toggle_internals)
- initialize_passenger_action_type(/datum/action/vehicle/sealed/mecha/mech_cycle_equip)
- initialize_passenger_action_type(/datum/action/vehicle/sealed/mecha/mech_toggle_lights)
- initialize_passenger_action_type(/datum/action/vehicle/sealed/mecha/mech_view_stats)
- initialize_passenger_action_type(/datum/action/vehicle/sealed/mecha/strafe)
+ initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/mech_toggle_internals, VEHICLE_CONTROL_SETTINGS)
+ initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/mech_cycle_equip, VEHICLE_CONTROL_EQUIPMENT)
+ initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/mech_toggle_lights, VEHICLE_CONTROL_SETTINGS)
+ initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/mech_view_stats, VEHICLE_CONTROL_SETTINGS)
+ initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/strafe, VEHICLE_CONTROL_DRIVE)
/obj/vehicle/sealed/mecha/proc/moved_inside(mob/living/newoccupant)
if(!(newoccupant?.client))
diff --git a/code/modules/vehicles/mecha/combat/savannah_ivanov.dm b/code/modules/vehicles/mecha/combat/savannah_ivanov.dm
new file mode 100644
index 00000000000..f51175b9803
--- /dev/null
+++ b/code/modules/vehicles/mecha/combat/savannah_ivanov.dm
@@ -0,0 +1,393 @@
+///how much time between charge_level going up by 1
+#define SKYFALL_SINGLE_CHARGE_TIME 2 SECONDS
+///enough charge level to take off, basically done charging
+#define SKYFALL_CHARGELEVEL_LAUNCH 5
+
+///the first half of the leap, where the mech is flying upwards.
+#define SKYFALL_LEAP_ARCING_UP 1
+///the second half of the leap, where the mech is flying downwards.
+#define SKYFALL_LEAP_ARCING_DOWN 1
+///how much time you're in the air
+#define TOTAL_SKYFALL_LEAP_TIME 3 SECONDS
+
+/**
+ * ## Savannah-Ivanov!
+ *
+ * A two person mecha that delegates moving to the driver and shooting to the pilot.
+ * ...Hilarious, right?
+ */
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov
+ name = "\improper Savannah-Ivanov"
+ desc = "An insanely overbulked mecha that handily crushes single-pilot opponents. The price is that you need two pilots to use it."
+ icon = 'icons/mecha/coop_mech.dmi'
+ base_icon_state = "savannah_ivanov"
+ icon_state = "savannah_ivanov_0_0"
+ movedelay = 3
+ dir_in = 2 //Facing South.
+ max_integrity = 450 //really tanky, like damn
+ deflect_chance = 25
+ armor = list(MELEE = 45, BULLET = 40, LASER = 30, ENERGY = 30, BOMB = 40, BIO = 0, RAD = 80, FIRE = 100, ACID = 100)
+ max_temperature = 30000
+ infra_luminosity = 3
+ wreckage = /obj/structure/mecha_wreckage/savannah_ivanov
+ internal_damage_threshold = 25
+ max_occupants = 2
+ //no tax on flying, since the power cost is in the leap itself.
+ phasing_energy_drain = 0
+ ///skyfall ability cooldown
+ COOLDOWN_DECLARE(skyfall_cooldown)
+ ///cooldown time between skyfall uses
+ var/skyfall_cooldown_time = 1 MINUTES
+ ///skyfall builds up in charges every 2 seconds, when it reaches 5 charges the ability actually starts
+ var/skyfall_charge_level = 0
+
+ ///ivanov strike ability cooldown
+ COOLDOWN_DECLARE(strike_cooldown)
+ ///cooldown time between strike uses
+ var/strike_cooldown_time = 40 SECONDS
+ ///toggled by ivanov strike, TRUE when signals are hooked to intercept clicks.
+ var/aiming_ivanov = FALSE
+ ///how many rockets can we send with ivanov strike
+ var/rockets_left = 0
+
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/get_mecha_occupancy_state()
+ var/driver_present = driver_amount() != 0
+ var/gunner_present = return_amount_of_controllers_with_flag(VEHICLE_CONTROL_EQUIPMENT) > 0
+ var/leap_state = skyfall_charge_level > 2 ? "leap_" : ""
+ return "[base_icon_state]_[leap_state][gunner_present]_[driver_present]"
+
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/auto_assign_occupant_flags(mob/new_occupant)
+ if(driver_amount() < max_drivers) //movement
+ add_control_flags(new_occupant, VEHICLE_CONTROL_DRIVE|VEHICLE_CONTROL_SETTINGS)
+ else //weapons
+ add_control_flags(new_occupant, VEHICLE_CONTROL_MELEE|VEHICLE_CONTROL_EQUIPMENT)
+
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/generate_actions()
+ initialize_passenger_action_type(/datum/action/vehicle/sealed/mecha/swap_seat)
+ . = ..()
+ initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/skyfall, VEHICLE_CONTROL_DRIVE)
+ initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/ivanov_strike, VEHICLE_CONTROL_EQUIPMENT)
+
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/remove_occupant(mob/getting_out)
+ //gunner getting out ends any ivanov aiming
+ if(aiming_ivanov && (getting_out in return_controllers_with_flag(VEHICLE_CONTROL_EQUIPMENT)))
+ end_missile_targeting(getting_out)
+ . = ..()
+
+/**
+ * ## begin_skyfall_charge
+ *
+ * Proc called by the mecha's ability that starts the skyfall loop
+ */
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/proc/begin_skyfall_charge(mob/pilot)
+ balloon_alert(pilot, "charging skyfall...")
+ INVOKE_ASYNC(src, .proc/skyfall_charge_loop, pilot)
+
+/**
+ * ## skyfall_charge_loop
+ *
+ * The actual skyfall loop itself. Repeatedly calls itself after a do_after, so any interruptions will call abort_skyfall and end the loop
+ * the other way the loop ends is if charge level (var it's ticking up) gets to SKYFALL_CHARGELEVEL_LAUNCH, in which case it ends the loop and does the ability.
+ * arguments:
+ * * pilot: mob that activated the skyfall ability
+ */
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/proc/skyfall_charge_loop(mob/living/pilot)
+ if(!pilot || !(pilot in return_drivers()) || !do_after(pilot, SKYFALL_SINGLE_CHARGE_TIME, target = src))
+ abort_skyfall(pilot)
+ return
+ skyfall_charge_level++
+ switch(skyfall_charge_level)
+ if(1)
+ visible_message("[src] clicks and whirrs for a moment, with a low hum emerging from the legs.")
+ playsound(src, 'sound/items/rped.ogg', 50, TRUE)
+ if(2)
+ visible_message("[src] begins to shake, the sounds of electricity growing louder.")
+ Shake(5, 5, SKYFALL_SINGLE_CHARGE_TIME-1) // -1 gives space between the animates, so they don't interrupt eachother
+ if(3)
+ visible_message("[src] assumes a pose as it rattles violently.")
+ Shake(7, 7, SKYFALL_SINGLE_CHARGE_TIME-1) // -1 gives space between the animates, so they don't interrupt eachother
+ spark_system.start()
+ update_icon_state()
+ if(4)
+ visible_message("[src] sparks and shutters as it finalizes preparation.")
+ playsound(src, 'sound/mecha/skyfall_power_up.ogg', 50, TRUE)
+ Shake(10, 10, SKYFALL_SINGLE_CHARGE_TIME-1) // -1 gives space between the animates, so they don't interrupt eachother
+ spark_system.start()
+ if(SKYFALL_CHARGELEVEL_LAUNCH)
+ visible_message("[src] leaps into the air!")
+ playsound(src, 'sound/weapons/gun/general/rocket_launch.ogg', 50, TRUE)
+ if(skyfall_charge_level != SKYFALL_CHARGELEVEL_LAUNCH)
+ INVOKE_ASYNC(src, .proc/skyfall_charge_loop, pilot)
+ return
+ COOLDOWN_START(src, skyfall_cooldown, skyfall_cooldown_time)
+ var/datum/action/vehicle/sealed/mecha/savannah_action = occupant_actions[pilot][/datum/action/vehicle/sealed/mecha/skyfall]
+ savannah_action.button_icon_state = "mech_savannah_cooldown"
+ savannah_action.UpdateButtonIcon()
+ addtimer(CALLBACK(savannah_action, /datum/action/vehicle/sealed/mecha/skyfall.proc/reset_button_icon), skyfall_cooldown_time)
+ for(var/mob/living/shaken in range(7, src))
+ shake_camera(shaken, 3, 3)
+
+ var/turf/launch_turf = get_turf(src)
+ new /obj/effect/hotspot(launch_turf)
+ launch_turf.hotspot_expose(700, 50, 1)
+ new /obj/effect/skyfall_landingzone(launch_turf, src)
+ resistance_flags |= INDESTRUCTIBLE //not while jumping at least
+ mecha_flags |= QUIET_STEPS|QUIET_TURNS|CANNOT_INTERACT
+ phasing = "flying"
+ movedelay = 1
+ density = FALSE
+ layer = FLY_LAYER
+ animate(src, alpha = 0, time = 8, easing = QUAD_EASING|EASE_IN, flags = ANIMATION_PARALLEL)
+ animate(src, pixel_z = 400, time = 10, easing = QUAD_EASING|EASE_IN, flags = ANIMATION_PARALLEL) //Animate our rising mech (just like pods hehe)
+ addtimer(CALLBACK(src, .proc/begin_landing, pilot), 2 SECONDS)
+
+/**
+ * ## begin_landing
+ *
+ * Called by skyfall_charge_loop after some time if it reaches full charge level.
+ * it's just the animations of the mecha coming down + another timer for the final landing effect
+ * arguments:
+ * * pilot: mob that activated the skyfall ability
+ */
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/proc/begin_landing(mob/living/pilot)
+ animate(src, pixel_z = 0, time = 10, easing = QUAD_EASING|EASE_IN, flags = ANIMATION_PARALLEL)
+ animate(src, alpha = 255, time = 8, easing = QUAD_EASING|EASE_IN, flags = ANIMATION_PARALLEL)
+ addtimer(CALLBACK(src, .proc/land, pilot), 10)
+
+/**
+ * ## begin_landing
+ *
+ * Called by skyfall_charge_loop after some time if it reaches full charge level.
+ * it's just the animations of the mecha coming down + another timer for the final landing effect
+ * arguments:
+ * * pilot: mob that activated the skyfall ability
+ */
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/proc/land(mob/living/pilot)
+ visible_message("[src] lands from above!")
+ playsound(src, 'sound/effects/explosion1.ogg', 50, 1)
+ resistance_flags &= ~INDESTRUCTIBLE
+ mecha_flags &= ~(QUIET_STEPS|QUIET_TURNS|CANNOT_INTERACT)
+ phasing = initial(phasing)
+ movedelay = initial(movedelay)
+ density = TRUE
+ layer = initial(layer)
+ skyfall_charge_level = 0
+ update_icon_state()
+ for(var/mob/living/shaken in range(7, src))
+ shake_camera(shaken, 5, 5)
+ var/turf/landed_on = get_turf(src)
+ for(var/thing in range(1, src))
+ if(isopenturf(thing))
+ var/turf/open/floor/crushed_tile = thing
+ crushed_tile.break_tile()
+ if(isclosedturf(thing) && thing == landed_on)
+ var/turf/closed/crushed_wall = thing
+ crushed_wall.ScrapeAway()
+ if(isobj(thing))
+ var/obj/crushed_object = thing
+ if(crushed_object == src)
+ continue
+ crushed_object.take_damage(150) //same as a hulk punch, makes sense to me
+ if(isliving(thing))
+ var/mob/living/crushed_victim = thing
+ if(crushed_victim in occupants)
+ continue
+ if(crushed_victim in landed_on)
+ to_chat(crushed_victim, "[src] crashes down on you from above!")
+ if(crushed_victim.stat != CONSCIOUS)
+ crushed_victim.gib(FALSE, FALSE, FALSE)
+ else
+ crushed_victim.adjustBruteLoss(80)
+ else
+ to_chat(crushed_victim, "The tremors from [src] landing sends you flying!")
+ var/fly_away_direction = get_dir(src, crushed_victim)
+ crushed_victim.throw_at(get_edge_target_turf(crushed_victim, fly_away_direction), 4, 3)
+ crushed_victim.adjustBruteLoss(15)
+
+/**
+ * ## abort_skyfall
+ *
+ * Called by skyfall_charge_loop if the charging is interrupted.
+ * Applies cooldown and resets charge level
+ * arguments:
+ * * pilot: mob that failed the skyfall ability
+ */
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/proc/abort_skyfall(mob/pilot)
+ if(pilot)
+ balloon_alert(pilot, "skyfall aborted")
+ COOLDOWN_START(src, skyfall_cooldown, skyfall_charge_level * 10 SECONDS) //so aborting skyfall later in the process imposes a longer cooldown
+ skyfall_charge_level = 0
+ update_icon_state()
+
+/**
+ * ## start_missile_targeting
+ *
+ * Called by the ivanov strike datum action, hooks signals into clicking to call drop_missile
+ * Plus other flavor like the overlay
+ * arguments:
+ * * gunner: mob that activated the ivanov strike
+ * * silent: whether to send feedback messages.
+ */
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/proc/start_missile_targeting(mob/gunner)
+ balloon_alert(gunner, "missile mode on (click to target)")
+ aiming_ivanov = TRUE
+ rockets_left = 3
+ RegisterSignal(src, COMSIG_MECHA_MELEE_CLICK, .proc/on_melee_click)
+ RegisterSignal(src, COMSIG_MECHA_EQUIPMENT_CLICK, .proc/on_equipment_click)
+ gunner.client.mouse_override_icon = 'icons/effects/mouse_pointers/supplypod_down_target.dmi'
+ gunner.update_mouse_pointer()
+ gunner.overlay_fullscreen("ivanov", /atom/movable/screen/fullscreen/ivanov_display, 1)
+ SEND_SOUND(gunner, 'sound/machines/terminal_on.ogg') //spammable so I don't want to make it audible to anyone else
+
+/**
+ * ## end_missile_targeting
+ *
+ * Called by the ivanov strike datum action or other actions that would end targetting
+ * Unhooks signals into clicking to call drop_missile plus other flavor like the overlay
+ * arguments:
+ * * gunner: mob that activated the ivanov strike
+ * * silent: whether to send feedback messages.
+ */
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/proc/end_missile_targeting(mob/gunner)
+ aiming_ivanov = FALSE
+ rockets_left = 0
+ UnregisterSignal(src, list(COMSIG_MECHA_MELEE_CLICK, COMSIG_MECHA_EQUIPMENT_CLICK))
+ gunner.client.mouse_override_icon = null
+ gunner.update_mouse_pointer()
+ gunner.clear_fullscreen("ivanov")
+
+///signal called from clicking with no equipment
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/proc/on_melee_click(datum/source, mob/living/pilot, atom/target, on_cooldown, is_adjacent)
+ SIGNAL_HANDLER
+ if(!target)
+ return
+ drop_missile(pilot, get_turf(target))
+
+///signal called from clicking with equipment
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/proc/on_equipment_click(datum/source, mob/living/pilot, atom/target)
+ SIGNAL_HANDLER
+ if(!target)
+ return
+ drop_missile(pilot, get_turf(target))
+
+/**
+ * ## drop_missile
+ *
+ * Called via intercepted clicks when the missile ability is active
+ * Spawns a droppod and starts the cooldown of the missile strike ability
+ * arguments:
+ * * gunner: mob that activated the ivanov strike
+ * * target_turf: turf of the atom that was clicked on
+ */
+/obj/vehicle/sealed/mecha/combat/savannah_ivanov/proc/drop_missile(mob/gunner, turf/target_turf)
+ rockets_left -= 1
+ if(rockets_left <= 0)
+ end_missile_targeting(gunner)
+ SEND_SOUND(gunner, 'sound/machines/triple_beep.ogg')
+ COOLDOWN_START(src, strike_cooldown, strike_cooldown_time)
+ podspawn(list(
+ "target" = target_turf,
+ "style" = STYLE_MISSILE,
+ "effectMissile" = TRUE,
+ "explosionSize" = list(0,0,1,2)
+ ))
+ var/datum/action/vehicle/sealed/mecha/strike_action = occupant_actions[gunner][/datum/action/vehicle/sealed/mecha/ivanov_strike]
+ strike_action.button_icon_state = "mech_ivanov_cooldown"
+ strike_action.UpdateButtonIcon()
+ addtimer(CALLBACK(strike_action, /datum/action/vehicle/sealed/mecha/ivanov_strike.proc/reset_button_icon), strike_cooldown_time)
+
+
+///Savannah Skyfall
+/datum/action/vehicle/sealed/mecha/skyfall
+ name = "Savannah Skyfall"
+ button_icon_state = "mech_savannah"
+
+/datum/action/vehicle/sealed/mecha/skyfall/Trigger()
+ if(!owner || !chassis || !(owner in chassis.occupants))
+ return
+ var/obj/vehicle/sealed/mecha/combat/savannah_ivanov/savannah_mecha = chassis
+ if(savannah_mecha.phasing)
+ to_chat(owner, "You're already airborne!")
+ return
+ if(!COOLDOWN_FINISHED(savannah_mecha, skyfall_cooldown))
+ var/timeleft = COOLDOWN_TIMELEFT(savannah_mecha, skyfall_cooldown)
+ to_chat(owner, "You need to wait [DisplayTimeText(timeleft, 1)] before attempting to Skyfall.")
+ return
+ if(savannah_mecha.skyfall_charge_level)
+ savannah_mecha.abort_skyfall(owner)
+ return
+ savannah_mecha.begin_skyfall_charge(owner)
+
+/**
+ * ## reset_button_icon
+ *
+ * called after an addtimer when the cooldown is finished with the skyfall, resets the icon
+ */
+/datum/action/vehicle/sealed/mecha/skyfall/proc/reset_button_icon()
+ button_icon_state = "mech_savannah"
+ UpdateButtonIcon()
+
+/datum/action/vehicle/sealed/mecha/ivanov_strike
+ name = "Ivanov Strike"
+ button_icon_state = "mech_ivanov"
+
+/datum/action/vehicle/sealed/mecha/ivanov_strike/Trigger()
+ if(!owner || !chassis || !(owner in chassis.occupants))
+ return
+ var/obj/vehicle/sealed/mecha/combat/savannah_ivanov/ivanov_mecha = chassis
+ if(!COOLDOWN_FINISHED(ivanov_mecha, strike_cooldown))
+ var/timeleft = COOLDOWN_TIMELEFT(ivanov_mecha, strike_cooldown)
+ to_chat(owner, "You need to wait [DisplayTimeText(timeleft, 1)] before firing another Ivanov Strike.")
+ return
+ if(ivanov_mecha.aiming_ivanov)
+ ivanov_mecha.end_missile_targeting(owner)
+ else
+ ivanov_mecha.start_missile_targeting(owner)
+
+/**
+ * ## reset_button_icon
+ *
+ * called after an addtimer when the cooldown is finished with the ivanov strike, resets the icon
+ */
+/datum/action/vehicle/sealed/mecha/ivanov_strike/proc/reset_button_icon()
+ button_icon_state = "mech_ivanov"
+ UpdateButtonIcon()
+
+//misc effects
+
+///a simple indicator of where the skyfall is going to land.
+/obj/effect/skyfall_landingzone
+ name = "Landing Zone Indicator"
+ desc = "A holographic projection designating the landing zone of something. It's probably best to stand back."
+ icon = 'icons/mob/telegraphing/telegraph_96x96.dmi'
+ icon_state = "target_largebox"
+ layer = BELOW_MOB_LAYER
+ pixel_x = -32
+ pixel_y = -32
+ alpha = 0
+ ///reference to mecha following
+ var/obj/vehicle/sealed/mecha/combat/mecha
+
+/obj/effect/skyfall_landingzone/Initialize(mapload, obj/vehicle/sealed/mecha/combat/mecha)
+ . = ..()
+ src.mecha = mecha
+ animate(src, alpha = 255, TOTAL_SKYFALL_LEAP_TIME/2, easing = CIRCULAR_EASING|EASE_OUT)
+ RegisterSignal(mecha, COMSIG_MOVABLE_MOVED, .proc/follow)
+ QDEL_IN(src, TOTAL_SKYFALL_LEAP_TIME) //when the animations land
+
+/obj/effect/skyfall_landingzone/Destroy(force)
+ . = ..()
+ UnregisterSignal(mecha, COMSIG_MOVABLE_MOVED)
+ mecha = null
+
+///called when the mecha moves
+/obj/effect/skyfall_landingzone/proc/follow(datum/source_mecha)
+ SIGNAL_HANDLER
+ forceMove(get_turf(source_mecha))
+
+#undef SKYFALL_SINGLE_CHARGE_TIME
+#undef SKYFALL_CHARGELEVEL_LAUNCH
+
+#undef SKYFALL_LEAP_ARCING_UP
+#undef SKYFALL_LEAP_ARCING_DOWN
+#undef TOTAL_SKYFALL_LEAP_TIME
diff --git a/code/modules/vehicles/mecha/mech_fabricator.dm b/code/modules/vehicles/mecha/mech_fabricator.dm
index 47d17130875..edc21af1d13 100644
--- a/code/modules/vehicles/mecha/mech_fabricator.dm
+++ b/code/modules/vehicles/mecha/mech_fabricator.dm
@@ -53,6 +53,7 @@
"Durand",
"H.O.N.K",
"Phazon",
+ "Savannah-Ivanov",
"Exosuit Equipment",
"Exosuit Ammunition",
"Cyborg Upgrade Modules",
diff --git a/code/modules/vehicles/mecha/mecha_actions.dm b/code/modules/vehicles/mecha/mecha_actions.dm
index d3ba5d6f0f3..7497b8c87fb 100644
--- a/code/modules/vehicles/mecha/mecha_actions.dm
+++ b/code/modules/vehicles/mecha/mecha_actions.dm
@@ -33,6 +33,7 @@
/datum/action/vehicle/sealed/mecha/mech_toggle_internals/Trigger()
if(!owner || !chassis || !(owner in chassis.occupants))
return
+
chassis.use_internal_tank = !chassis.use_internal_tank
button_icon_state = "mech_internals_[chassis.use_internal_tank ? "on" : "off"]"
chassis.balloon_alert(owner, "taking air from [chassis.use_internal_tank ? "internal airtank" : "environment"]")
@@ -88,6 +89,7 @@
/datum/action/vehicle/sealed/mecha/mech_toggle_lights/Trigger()
if(!owner || !chassis || !(owner in chassis.occupants))
return
+
if(!(chassis.mecha_flags & HAS_LIGHTS))
chassis.balloon_alert(owner, "the mech lights are broken!")
return
@@ -108,6 +110,7 @@
/datum/action/vehicle/sealed/mecha/mech_view_stats/Trigger()
if(!owner || !chassis || !(owner in chassis.occupants))
return
+
var/datum/browser/popup = new(owner , "exosuit")
popup.set_content(chassis.get_stats_html(owner))
popup.open()
@@ -124,8 +127,13 @@
chassis.toggle_strafe()
/obj/vehicle/sealed/mecha/AltClick(mob/living/user)
- if((user in occupants) && user.canUseTopic(src))
- toggle_strafe()
+ if(!(user in occupants) || !user.canUseTopic(src))
+ return
+ if(!(user in return_controllers_with_flag(VEHICLE_CONTROL_DRIVE)))
+ to_chat(user, "You're in the wrong seat to control movement.")
+ return
+
+ toggle_strafe()
/obj/vehicle/sealed/mecha/proc/toggle_strafe()
if(!(mecha_flags & CANSTRAFE))
@@ -215,14 +223,14 @@
return
var/new_damtype
switch(chassis.damtype)
- if("tox")
- new_damtype = "brute"
+ if(TOX)
+ new_damtype = BRUTE
chassis.balloon_alert(owner, "your punches will now deal brute damage")
- if("brute")
- new_damtype = "fire"
+ if(BRUTE)
+ new_damtype = BURN
chassis.balloon_alert(owner, "your punches will now deal burn damage")
- if("fire")
- new_damtype = "tox"
+ if(BURN)
+ new_damtype = TOX
chassis.balloon_alert(owner,"your punches will now deal toxin damage")
chassis.damtype = new_damtype
button_icon_state = "mech_damtype_[new_damtype]"
@@ -236,7 +244,37 @@
/datum/action/vehicle/sealed/mecha/mech_toggle_phasing/Trigger()
if(!owner || !chassis || !(owner in chassis.occupants))
return
- chassis.phasing = !chassis.phasing
+ chassis.phasing = chassis.phasing ? "" : "phasing"
button_icon_state = "mech_phasing_[chassis.phasing ? "on" : "off"]"
chassis.balloon_alert(owner, "[chassis.phasing ? "enabled" : "disabled"] phasing")
UpdateButtonIcon()
+
+///swap seats, for two person mecha
+/datum/action/vehicle/sealed/mecha/swap_seat
+ name = "Switch Seats"
+ button_icon_state = "mech_seat_swap"
+
+/datum/action/vehicle/sealed/mecha/swap_seat/Trigger()
+ if(!owner || !chassis || !(owner in chassis.occupants))
+ return
+
+ if(chassis.occupants.len == chassis.max_occupants)
+ chassis.balloon_alert(owner, "other seat occupied!")
+ return
+ var/list/drivers = chassis.return_drivers()
+ chassis.balloon_alert(owner, "moving to other seat...")
+ chassis.is_currently_ejecting = TRUE
+ if(!do_after(owner, chassis.has_gravity() ? chassis.exit_delay : 0 , target = chassis))
+ chassis.balloon_alert(owner, "interrupted!")
+ chassis.is_currently_ejecting = FALSE
+ return
+ chassis.is_currently_ejecting = FALSE
+ if(owner in drivers)
+ chassis.balloon_alert(owner, "controlling gunner seat")
+ chassis.remove_control_flags(owner, VEHICLE_CONTROL_DRIVE|VEHICLE_CONTROL_SETTINGS)
+ chassis.add_control_flags(owner, VEHICLE_CONTROL_MELEE|VEHICLE_CONTROL_EQUIPMENT)
+ else
+ chassis.balloon_alert(owner, "controlling pilot seat")
+ chassis.remove_control_flags(owner, VEHICLE_CONTROL_MELEE|VEHICLE_CONTROL_EQUIPMENT)
+ chassis.add_control_flags(owner, VEHICLE_CONTROL_DRIVE|VEHICLE_CONTROL_SETTINGS)
+ chassis.update_icon_state()
diff --git a/code/modules/vehicles/mecha/mecha_construction_paths.dm b/code/modules/vehicles/mecha/mecha_construction_paths.dm
index b8476b9cd9e..5520cbef901 100644
--- a/code/modules/vehicles/mecha/mecha_construction_paths.dm
+++ b/code/modules/vehicles/mecha/mecha_construction_paths.dm
@@ -1202,6 +1202,148 @@
"You slowly place the bluespace anomaly core into its socket and close its chamber.")
return TRUE
+//savannah_ivanov
+
+/datum/component/construction/unordered/mecha_chassis/savannah_ivanov
+ result = /datum/component/construction/mecha/savannah_ivanov
+ steps = list(
+ /obj/item/mecha_parts/part/savannah_ivanov_torso,
+ /obj/item/mecha_parts/part/savannah_ivanov_head,
+ /obj/item/mecha_parts/part/savannah_ivanov_left_arm,
+ /obj/item/mecha_parts/part/savannah_ivanov_right_arm,
+ /obj/item/mecha_parts/part/savannah_ivanov_left_leg,
+ /obj/item/mecha_parts/part/savannah_ivanov_right_leg
+ )
+
+/datum/component/construction/mecha/savannah_ivanov
+ result = /obj/vehicle/sealed/mecha/combat/savannah_ivanov
+ base_icon = "savannah_ivanov"
+
+ circuit_control = /obj/item/circuitboard/mecha/savannah_ivanov/main
+ circuit_periph = /obj/item/circuitboard/mecha/savannah_ivanov/peripherals
+ circuit_weapon = /obj/item/circuitboard/mecha/savannah_ivanov/targeting
+
+ inner_plating = /obj/item/stack/sheet/plasteel
+ inner_plating_amount = 10
+
+ outer_plating = /obj/item/mecha_parts/part/savannah_ivanov_armor
+ outer_plating_amount = 1
+
+/datum/component/construction/mecha/savannah_ivanov/custom_action(obj/item/I, mob/living/user, diff)
+ . = ..()
+ if(!.)
+ return FALSE
+
+ switch(index)
+ if(1)
+ user.visible_message("[user] connects [parent] hydraulic systems.", "You connect [parent] hydraulic systems.")
+ if(2)
+ if(diff==FORWARD)
+ user.visible_message("[user] activates [parent] hydraulic systems.", "You activate [parent] hydraulic systems.")
+ else
+ user.visible_message("[user] disconnects [parent] hydraulic systems.", "You disconnect [parent] hydraulic systems.")
+ if(3)
+ if(diff==FORWARD)
+ user.visible_message("[user] adds the wiring to [parent].", "You add the wiring to [parent].")
+ else
+ user.visible_message("[user] deactivates [parent] hydraulic systems.", "You deactivate [parent] hydraulic systems.")
+ if(4)
+ if(diff==FORWARD)
+ user.visible_message("[user] adjusts the wiring of [parent].", "You adjust the wiring of [parent].")
+ else
+ user.visible_message("[user] removes the wiring from [parent].", "You remove the wiring from [parent].")
+ if(5)
+ if(diff==FORWARD)
+ user.visible_message("[user] installs [I] into [parent].", "You install [I] into [parent].")
+ else
+ user.visible_message("[user] disconnects the wiring of [parent].", "You disconnect the wiring of [parent].")
+ if(6)
+ if(diff==FORWARD)
+ user.visible_message("[user] secures the mainboard.", "You secure the mainboard.")
+ else
+ user.visible_message("[user] removes the central control module from [parent].", "You remove the central computer mainboard from [parent].")
+ if(7)
+ if(diff==FORWARD)
+ user.visible_message("[user] installs [I] into [parent].", "You install [I] into [parent].")
+ else
+ user.visible_message("[user] unfastens the mainboard.", "You unfasten the mainboard.")
+ if(8)
+ if(diff==FORWARD)
+ user.visible_message("[user] secures the ivanov peripherals control module.", "You secure the ivanov peripherals control module.")
+ else
+ user.visible_message("[user] removes the ivanov peripherals control module from [parent].", "You remove the ivanov peripherals control module from [parent].")
+ if(9)
+ if(diff==FORWARD)
+ user.visible_message("[user] installs [I] into [parent].", "You install [I] into [parent].")
+ else
+ user.visible_message("[user] unfastens the ivanov peripherals control module.", "You unfasten the ivanov peripherals control module.")
+ if(10)
+ if(diff==FORWARD)
+ user.visible_message("[user] secures the ivanov weapon control module.", "You secure the ivanov weapon control module.")
+ else
+ user.visible_message("[user] removes the ivanov weapon control module from [parent].", "You remove the ivanov weapon control module from [parent].")
+ if(11)
+ if(diff==FORWARD)
+ user.visible_message("[user] installs [I] to [parent].", "You install [I] to [parent].")
+ else
+ user.visible_message("[user] unfastens the ivanov weapon control module.", "You unfasten the ivanov weapon control module.")
+ if(12)
+ if(diff==FORWARD)
+ user.visible_message("[user] secures the scanner module.", "You secure the scanner module.")
+ else
+ user.visible_message("[user] removes the scanner module from [parent].", "You remove the scanner module from [parent].")
+ if(13)
+ if(diff==FORWARD)
+ user.visible_message("[user] installs [I] to [parent].", "You install [I] to [parent].")
+ else
+ user.visible_message("[user] unfastens the scanner module.", "You unfasten the scanner module.")
+ if(14)
+ if(diff==FORWARD)
+ user.visible_message("[user] secures the capacitor.", "You secure the capacitor.")
+ else
+ user.visible_message("[user] removes the capacitor from [parent].", "You remove the capacitor from [parent].")
+ if(15)
+ if(diff==FORWARD)
+ user.visible_message("[user] installs [I] into [parent].", "You install [I] into [parent].")
+ else
+ user.visible_message("[user] unfastens the capacitor.", "You unfasten the capacitor.")
+ if(16)
+ if(diff==FORWARD)
+ user.visible_message("[user] secures the power cell.", "You secure the power cell.")
+ else
+ user.visible_message("[user] pries the power cell from [parent].", "You pry the power cell from [parent].")
+ if(17)
+ if(diff==FORWARD)
+ user.visible_message("[user] installs the internal armor layer to [parent].", "You install the internal armor layer to [parent].")
+ else
+ user.visible_message("[user] unfastens the power cell.", "You unfasten the power cell.")
+ if(18)
+ if(diff==FORWARD)
+ user.visible_message("[user] secures the internal armor layer.", "You secure the internal armor layer.")
+ else
+ user.visible_message("[user] pries internal armor layer from [parent].", "You pry internal armor layer from [parent].")
+ if(19)
+ if(diff==FORWARD)
+ user.visible_message("[user] welds the internal armor layer to [parent].", "You weld the internal armor layer to [parent].")
+ else
+ user.visible_message("[user] unfastens the internal armor layer.", "You unfasten the internal armor layer.")
+ if(20)
+ if(diff==FORWARD)
+ user.visible_message("[user] installs [I] to [parent].", "You install [I] to [parent].")
+ else
+ user.visible_message("[user] cuts the internal armor layer from [parent].", "You cut the internal armor layer from [parent].")
+ if(21)
+ if(diff==FORWARD)
+ user.visible_message("[user] secures Savannah-Ivanov Armor Plates.", "You secure Savannah-Ivanov Armor Plates.")
+ else
+ user.visible_message("[user] pries Savannah-Ivanov Armor Plates from [parent].", "You pry Savannah-Ivanov Armor Plates from [parent].")
+ if(22)
+ if(diff==FORWARD)
+ user.visible_message("[user] welds Savannah-Ivanov Armor Plates to [parent].", "You weld Savannah-Ivanov Armor Plates to [parent].")
+ else
+ user.visible_message("[user] unfastens Savannah-Ivanov Armor Plates.", "You unfasten Savannah-Ivanov Armor Plates.")
+ return TRUE
+
//ODYSSEUS
/datum/component/construction/unordered/mecha_chassis/odysseus
diff --git a/code/modules/vehicles/mecha/mecha_parts.dm b/code/modules/vehicles/mecha/mecha_parts.dm
index 2a9a1306bb9..9d6fb629b15 100644
--- a/code/modules/vehicles/mecha/mecha_parts.dm
+++ b/code/modules/vehicles/mecha/mecha_parts.dm
@@ -291,6 +291,46 @@
desc="Phazon armor plates. They are layered with plasma to protect the pilot from the stress of phasing and have unusual properties."
icon_state = "phazon_armor"
+// Savannah-Ivanov
+
+/obj/item/mecha_parts/chassis/savannah_ivanov
+ name = "\improper Savannah-Ivanov chassis"
+ construct_type = /datum/component/construction/unordered/mecha_chassis/savannah_ivanov
+
+/obj/item/mecha_parts/part/savannah_ivanov_torso
+ name="\improper Savannah-Ivanov torso"
+ desc="A Savannah-Ivanov torso part. It's missing a huge chunk of space..."
+ icon_state = "savannah_ivanov_harness"
+
+/obj/item/mecha_parts/part/savannah_ivanov_head
+ name="\improper Savannah-Ivanov head"
+ desc="A Savannah-Ivanov head. It's sensors have been adjusted to support graceful landings."
+ icon_state = "savannah_ivanov_head"
+
+/obj/item/mecha_parts/part/savannah_ivanov_left_arm
+ name="\improper Savannah-Ivanov left arm"
+ desc="A Savannah-Ivanov left arm. Hidden rocket fabrication included in the wrists."
+ icon_state = "savannah_ivanov_l_arm"
+
+/obj/item/mecha_parts/part/savannah_ivanov_right_arm
+ name="\improper Savannah-Ivanov right arm"
+ desc="A Savannah-Ivanov left arm. Hidden rocket fabrication included in the wrists."
+ icon_state = "savannah_ivanov_r_arm"
+
+/obj/item/mecha_parts/part/savannah_ivanov_left_leg
+ name="\improper Savannah-Ivanov left leg"
+ desc="A Savannah-Ivanov left leg. In production they were designed to carry more than two passengers, so the leaping functionality was added as to not waste potential."
+ icon_state = "savannah_ivanov_l_leg"
+
+/obj/item/mecha_parts/part/savannah_ivanov_right_leg
+ name="\improper Savannah-Ivanov right leg"
+ desc="A Savannah-Ivanov left leg. In production they were designed to carry more than two passengers, so the leaping functionality was added as to not waste potential."
+ icon_state = "savannah_ivanov_r_leg"
+
+/obj/item/mecha_parts/part/savannah_ivanov_armor
+ name="Savannah-Ivanov armor"
+ desc="Savannah-Ivanov armor plates. They are uniquely shaped and reinforced to deal with the stresses of two pilots, grandiose leaps, and missiles."
+ icon_state = "savannah_ivanov_armor"
///////// Circuitboards
@@ -379,3 +419,15 @@
/obj/item/circuitboard/mecha/clarke/main
name = "Clarke Central Control module (Exosuit Board)"
icon_state = "mainboard"
+
+/obj/item/circuitboard/mecha/savannah_ivanov/peripherals
+ name = "Savannah Peripherals Control module (Exosuit Board)"
+ icon_state = "mcontroller"
+
+/obj/item/circuitboard/mecha/savannah_ivanov/targeting
+ name = "Ivanov Weapon Control and Targeting module (Exosuit Board)"
+ icon_state = "mcontroller"
+
+/obj/item/circuitboard/mecha/savannah_ivanov/main
+ name = "Savannah-Ivanov Combination Control Lock module (Exosuit Board)"
+ icon_state = "mainboard"
diff --git a/code/modules/vehicles/mecha/mecha_wreckage.dm b/code/modules/vehicles/mecha/mecha_wreckage.dm
index 5d7f776c466..87afa82a3d9 100644
--- a/code/modules/vehicles/mecha/mecha_wreckage.dm
+++ b/code/modules/vehicles/mecha/mecha_wreckage.dm
@@ -207,7 +207,17 @@
/obj/item/mecha_parts/part/phazon_left_leg,
/obj/item/mecha_parts/part/phazon_right_leg)
-
+/obj/structure/mecha_wreckage/savannah_ivanov
+ name = "\improper Savannah-Ivanov wreckage"
+ icon = 'icons/mecha/coop_mech.dmi'
+ icon_state = "savannah_ivanov-broken"
+ parts = list(
+ /obj/item/mecha_parts/part/savannah_ivanov_torso,
+ /obj/item/mecha_parts/part/savannah_ivanov_head,
+ /obj/item/mecha_parts/part/savannah_ivanov_left_arm,
+ /obj/item/mecha_parts/part/savannah_ivanov_right_arm,
+ /obj/item/mecha_parts/part/savannah_ivanov_left_leg,
+ /obj/item/mecha_parts/part/savannah_ivanov_right_leg)
/obj/structure/mecha_wreckage/odysseus
name = "\improper Odysseus wreckage"
diff --git a/code/modules/vehicles/vehicle_actions.dm b/code/modules/vehicles/vehicle_actions.dm
index 90427c9fad0..6b317d0e5be 100644
--- a/code/modules/vehicles/vehicle_actions.dm
+++ b/code/modules/vehicles/vehicle_actions.dm
@@ -1,7 +1,23 @@
//VEHICLE DEFAULT HANDLING
+
+/**
+ * ## generate_actions
+ *
+ * You override this with initialize_passenger_action_type and initialize_controller_action_type calls
+ * To give passengers actions when they enter the vehicle.
+ * Read the documentation on the aforementioned procs to learn the difference
+ */
/obj/vehicle/proc/generate_actions()
return
+/**
+ * ## generate_action_type
+ *
+ * A small proc to properly set up each action path.
+ * args:
+ * * actiontype: typepath of the action the proc sets up.
+ * returns created and set up action instance
+ */
/obj/vehicle/proc/generate_action_type(actiontype)
var/datum/action/vehicle/A = new actiontype
if(!istype(A))
@@ -9,45 +25,97 @@
A.vehicle_target = src
return A
+/**
+ * ## initialize_passenger_action_type
+ *
+ * Gives any passenger that enters the mech this action.
+ * They will lose it when they disembark.
+ * args:
+ * * actiontype: typepath of the action you want to give occupants.
+ */
/obj/vehicle/proc/initialize_passenger_action_type(actiontype)
autogrant_actions_passenger += actiontype
for(var/i in occupants)
grant_passenger_actions(i) //refresh
+/**
+ * ## initialize_controller_action_type
+ *
+ * Gives any passenger that enters the mech this action... IF they have the correct vehicle control flag.
+ * This is used so passengers cannot press buttons only drivers should have, for example.
+ * args:
+ * * actiontype: typepath of the action you want to give occupants.
+ */
/obj/vehicle/proc/initialize_controller_action_type(actiontype, control_flag)
LAZYINITLIST(autogrant_actions_controller["[control_flag]"])
autogrant_actions_controller["[control_flag]"] += actiontype
for(var/i in occupants)
grant_controller_actions(i) //refresh
-/obj/vehicle/proc/grant_action_type_to_mob(actiontype, mob/m)
- if(isnull(LAZYACCESS(occupants, m)) || !actiontype)
+/**
+ * ## grant_action_type_to_mob
+ *
+ * As on the tin, it does all the annoying small stuff and sanity needed
+ * to GRANT an action to a mob.
+ * args:
+ * * actiontype: typepath of the action you want to give to grant_to.
+ * * grant_to: the mob we're giving actiontype to
+ * returns TRUE if successfully granted
+ */
+/obj/vehicle/proc/grant_action_type_to_mob(actiontype, mob/grant_to)
+ if(isnull(LAZYACCESS(occupants, grant_to)) || !actiontype)
return FALSE
- LAZYINITLIST(occupant_actions[m])
- if(occupant_actions[m][actiontype])
+ LAZYINITLIST(occupant_actions[grant_to])
+ if(occupant_actions[grant_to][actiontype])
return TRUE
var/datum/action/action = generate_action_type(actiontype)
- action.Grant(m)
- occupant_actions[m][action.type] = action
+ action.Grant(grant_to)
+ occupant_actions[grant_to][action.type] = action
return TRUE
-/obj/vehicle/proc/remove_action_type_from_mob(actiontype, mob/m)
- if(isnull(LAZYACCESS(occupants, m)) || !actiontype)
+/**
+ * ## remove_action_type_from_mob
+ *
+ * As on the tin, it does all the annoying small stuff and sanity needed
+ * to REMOVE an action to a mob.
+ * args:
+ * * actiontype: typepath of the action you want to give to grant_to.
+ * * take_from: the mob we're taking actiontype to
+ * returns TRUE if successfully removed
+ */
+/obj/vehicle/proc/remove_action_type_from_mob(actiontype, mob/take_from)
+ if(isnull(LAZYACCESS(occupants, take_from)) || !actiontype)
return FALSE
- LAZYINITLIST(occupant_actions[m])
- if(occupant_actions[m][actiontype])
- var/datum/action/action = occupant_actions[m][actiontype]
- action.Remove(m)
- occupant_actions[m] -= actiontype
+ LAZYINITLIST(occupant_actions[take_from])
+ if(occupant_actions[take_from][actiontype])
+ var/datum/action/action = occupant_actions[take_from][actiontype]
+ action.Remove(take_from)
+ occupant_actions[take_from] -= actiontype
return TRUE
-/obj/vehicle/proc/grant_passenger_actions(mob/M)
+/**
+ * ## grant_passenger_actions
+ *
+ * Called on every passenger that enters the vehicle, goes through the list of actions it needs to give...
+ * and does that.
+ * args:
+ * * grant_to: mob that needs to get every action the vehicle grants
+ */
+/obj/vehicle/proc/grant_passenger_actions(mob/grant_to)
for(var/v in autogrant_actions_passenger)
- grant_action_type_to_mob(v, M)
+ grant_action_type_to_mob(v, grant_to)
-/obj/vehicle/proc/remove_passenger_actions(mob/M)
+/**
+ * ## remove_passenger_actions
+ *
+ * Called on every passenger that exits the vehicle, goes through the list of actions it needs to remove...
+ * and does that.
+ * args:
+ * * take_from: mob that needs to get every action the vehicle grants
+ */
+/obj/vehicle/proc/remove_passenger_actions(mob/take_from)
for(var/v in autogrant_actions_passenger)
- remove_action_type_from_mob(v, M)
+ remove_action_type_from_mob(v, take_from)
/obj/vehicle/proc/grant_controller_actions(mob/M)
if(!istype(M) || isnull(LAZYACCESS(occupants, M)))
diff --git a/icons/hud/screen_full.dmi b/icons/hud/screen_full.dmi
index 7bf58d5e2af..89be83c9b93 100644
Binary files a/icons/hud/screen_full.dmi and b/icons/hud/screen_full.dmi differ
diff --git a/icons/mecha/coop_mech.dmi b/icons/mecha/coop_mech.dmi
new file mode 100644
index 00000000000..2cd91ceb55c
Binary files /dev/null and b/icons/mecha/coop_mech.dmi differ
diff --git a/icons/mecha/mech_construct.dmi b/icons/mecha/mech_construct.dmi
index a809eff594b..c25eb971f2e 100644
Binary files a/icons/mecha/mech_construct.dmi and b/icons/mecha/mech_construct.dmi differ
diff --git a/icons/mecha/mech_construction.dmi b/icons/mecha/mech_construction.dmi
index 558e06be01f..c0f59412335 100644
Binary files a/icons/mecha/mech_construction.dmi and b/icons/mecha/mech_construction.dmi differ
diff --git a/icons/mob/actions/actions_mecha.dmi b/icons/mob/actions/actions_mecha.dmi
index 72bbbad9708..943e882ac94 100644
Binary files a/icons/mob/actions/actions_mecha.dmi and b/icons/mob/actions/actions_mecha.dmi differ
diff --git a/icons/mob/telegraphing/telegraph_96x96.dmi b/icons/mob/telegraphing/telegraph_96x96.dmi
new file mode 100644
index 00000000000..b6264812fac
Binary files /dev/null and b/icons/mob/telegraphing/telegraph_96x96.dmi differ
diff --git a/icons/obj/tools.dmi b/icons/obj/tools.dmi
index 63b4603fd0f..5cc32e7af07 100644
Binary files a/icons/obj/tools.dmi and b/icons/obj/tools.dmi differ
diff --git a/sound/effects/angryboat.ogg b/sound/effects/angryboat.ogg
new file mode 100644
index 00000000000..0cf6d1723fc
Binary files /dev/null and b/sound/effects/angryboat.ogg differ
diff --git a/sound/items/robofafafoggy.ogg b/sound/items/robofafafoggy.ogg
new file mode 100644
index 00000000000..16d8af77fc5
Binary files /dev/null and b/sound/items/robofafafoggy.ogg differ
diff --git a/sound/items/robofafafoggy2.ogg b/sound/items/robofafafoggy2.ogg
new file mode 100644
index 00000000000..11bea909b6d
Binary files /dev/null and b/sound/items/robofafafoggy2.ogg differ
diff --git a/sound/mecha/skyfall_power_up.ogg b/sound/mecha/skyfall_power_up.ogg
new file mode 100644
index 00000000000..4199c0a6129
Binary files /dev/null and b/sound/mecha/skyfall_power_up.ogg differ
diff --git a/strings/clown_nonsense.json b/strings/clown_nonsense.json
new file mode 100644
index 00000000000..bb565efc24d
--- /dev/null
+++ b/strings/clown_nonsense.json
@@ -0,0 +1,38 @@
+{
+ "honk": [
+ "honk",
+ "HONK",
+ "HONK!",
+ "HONK!!!",
+ "HENK",
+ "HOOOOOOONK"
+ ],
+
+ "rare": [
+ "HOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOONK!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
+ "damn",
+ ":))))))))))))))))))))))))",
+ " :) ",
+ "Hink",
+ "HAHAHAA HEEHEEHEE",
+ " Laugh sound. ",
+ "Extreme Ruckus at the Party"
+ ],
+
+ "non-honk-clown-words": [
+ "bananas",
+ "chapiteau",
+ "entrée",
+ "pranking",
+ "peel",
+ "banana peel",
+ "party",
+ "juggling",
+ "... ",
+ "cat",
+ "balagan",
+ "pie",
+ "slip"
+ ]
+
+}
diff --git a/tgstation.dme b/tgstation.dme
index ab1a4152b77..b59c4621ff4 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -3543,6 +3543,7 @@
#include "code\modules\vehicles\mecha\combat\marauder.dm"
#include "code\modules\vehicles\mecha\combat\phazon.dm"
#include "code\modules\vehicles\mecha\combat\reticence.dm"
+#include "code\modules\vehicles\mecha\combat\savannah_ivanov.dm"
#include "code\modules\vehicles\mecha\equipment\mecha_equipment.dm"
#include "code\modules\vehicles\mecha\equipment\tools\medical_tools.dm"
#include "code\modules\vehicles\mecha\equipment\tools\mining_tools.dm"