")
+
+#define ANNOUNCEMENT_COLORS list("default", "green", "blue", "pink", "yellow", "orange", "red", "purple")
+
/**
* Make a big red text announcement to
*
@@ -13,14 +30,14 @@
* * text - required, the text to announce
* * title - optional, the title of the announcement.
* * sound - optional, the sound played accompanying the announcement
- * * type - optional, the type of the announcement, for some "preset" announcement templates. "Priority", "Captain", "Syndicate Captain"
+ * * type - optional, the type of the announcement, for some "preset" announcement templates. See __DEFINES/announcements.dm
* * sender_override - optional, modifies the sender of the announcement
* * has_important_message - is this message critical to the game (and should not be overridden by station traits), or not
* * players - a list of all players to send the message to. defaults to all players (not including new players)
* * encode_title - if TRUE, the title will be HTML encoded
* * encode_text - if TRUE, the text will be HTML encoded
*/
-/proc/priority_announce(text, title = "", sound, type, sender_override, has_important_message = FALSE, list/mob/players, encode_title = TRUE, encode_text = TRUE)
+/proc/priority_announce(text, title = "", sound, type, sender_override, has_important_message = FALSE, list/mob/players, encode_title = TRUE, encode_text = TRUE, color_override)
if(!text)
return
@@ -31,65 +48,66 @@
if(!length(text))
return
- var/announcement
+ var/list/announcement_strings = list()
+
if(!sound)
sound = SSstation.announcer.get_rand_alert_sound()
else if(SSstation.announcer.event_sounds[sound])
sound = SSstation.announcer.event_sounds[sound]
- if(type == "Priority")
- announcement += "
" + text, "[command_name()]", "Station Announcements", null)
+ else
+ GLOB.news_network.submit_article(text, "[command_name()] Update", "Station Announcements", null)
/proc/print_command_report(text = "", title = null, announce=TRUE)
if(!title)
title = "Classified [command_name()] Update"
if(announce)
- priority_announce("A report has been downloaded and printed out at all communications consoles.", "Incoming Classified Message", SSstation.announcer.get_rand_report_sound(), has_important_message = TRUE)
+ priority_announce(
+ text = "A report has been downloaded and printed out at all communications consoles.",
+ title = "Incoming Classified Message",
+ sound = SSstation.announcer.get_rand_report_sound(),
+ has_important_message = TRUE,
+ )
- var/datum/comm_message/M = new
- M.title = title
- M.content = text
+ var/datum/comm_message/message = new
+ message.title = title
+ message.content = text
- SScommunications.send_message(M)
+ SScommunications.send_message(message)
/**
* Sends a minor annoucement to players.
@@ -103,8 +121,9 @@
* players - optional, a list mobs to send the announcement to. If unset, sends to all palyers.
* sound_override - optional, use the passed sound file instead of the default notice sounds.
* should_play_sound - Whether the notice sound should be played or not.
+ * color_override - optional, use the passed color instead of the default notice color.
*/
-/proc/minor_announce(message, title = "Attention:", alert, html_encode = TRUE, list/players = null, sound_override = null, should_play_sound = TRUE)
+/proc/minor_announce(message, title = "Attention:", alert = FALSE, html_encode = TRUE, list/players, sound_override, should_play_sound = TRUE, color_override)
if(!message)
return
@@ -112,16 +131,80 @@
title = html_encode(title)
message = html_encode(message)
+ var/list/minor_announcement_strings = list()
+ if(title != null && title != "")
+ minor_announcement_strings += ANNOUNCEMENT_HEADER(MINOR_ANNOUNCEMENT_TITLE(title))
+ minor_announcement_strings += MINOR_ANNOUNCEMENT_TEXT(message)
+
+ var/finalized_announcement
+ if(color_override)
+ finalized_announcement = CHAT_ALERT_COLORED_SPAN(color_override, jointext(minor_announcement_strings, ""))
+ else
+ finalized_announcement = CHAT_ALERT_DEFAULT_SPAN(jointext(minor_announcement_strings, ""))
+
+ var/custom_sound = sound_override || (alert ? 'sound/misc/notice1.ogg' : 'sound/misc/notice2.ogg')
+ dispatch_announcement_to_players(finalized_announcement, players, custom_sound, should_play_sound)
+
+/// Sends an announcement about the level changing to players. Uses the passed in datum and the subsystem's previous security level to generate the message.
+/proc/level_announce(datum/security_level/selected_level, previous_level_number)
+ var/current_level_number = selected_level.number_level
+ var/current_level_name = selected_level.name
+ var/current_level_color = selected_level.announcement_color
+ var/current_level_sound = selected_level.sound
+
+ var/title
+ var/message
+
+ if(current_level_number > previous_level_number)
+ title = "Attention! Security level elevated to [current_level_name]:"
+ message = selected_level.elevating_to_announcement
+ else
+ title = "Attention! Security level lowered to [current_level_name]:"
+ message = selected_level.lowering_to_announcement
+
+ var/list/level_announcement_strings = list()
+ level_announcement_strings += ANNOUNCEMENT_HEADER(MINOR_ANNOUNCEMENT_TITLE(title))
+ level_announcement_strings += MINOR_ANNOUNCEMENT_TEXT(message)
+
+ var/finalized_announcement = CHAT_ALERT_COLORED_SPAN(current_level_color, jointext(level_announcement_strings, ""))
+
+ dispatch_announcement_to_players(finalized_announcement, GLOB.player_list, current_level_sound)
+
+/// Proc that just generates a custom header based on variables fed into `priority_announce()`
+/// Will return a string.
+/proc/generate_unique_announcement_header(title, sender_override)
+ var/list/returnable_strings = list()
+ if(isnull(sender_override))
+ returnable_strings += MAJOR_ANNOUNCEMENT_TITLE("[command_name()] Update")
+ else
+ returnable_strings += MAJOR_ANNOUNCEMENT_TITLE(sender_override)
+
+ if(length(title) > 0)
+ returnable_strings += SUBHEADER_ANNOUNCEMENT_TITLE(title)
+
+ return jointext(returnable_strings, "")
+
+/// Proc that just dispatches the announcement to our applicable audience. Only the announcement is a mandatory arg.
+/proc/dispatch_announcement_to_players(announcement, list/players, sound_override = null, should_play_sound = TRUE)
if(!players)
players = GLOB.player_list
+ var/sound_to_play = !isnull(sound_override) ? sound_override : 'sound/misc/notice2.ogg'
+
for(var/mob/target in players)
- if(isnewplayer(target))
+ if(isnewplayer(target) || !target.can_hear())
continue
- if(!target.can_hear())
+
+ to_chat(target, announcement)
+ if(!should_play_sound)
continue
- to_chat(target, "[span_minorannounce("[title] [message]")] ")
- if(should_play_sound && target.client?.prefs.read_preference(/datum/preference/toggle/sound_announcements))
- var/sound_to_play = sound_override || (alert ? 'sound/misc/notice1.ogg' : 'sound/misc/notice2.ogg')
+ if(target.client?.prefs.read_preference(/datum/preference/toggle/sound_announcements))
SEND_SOUND(target, sound(sound_to_play))
+
+#undef MAJOR_ANNOUNCEMENT_TITLE
+#undef MAJOR_ANNOUNCEMENT_TEXT
+#undef MINOR_ANNOUNCEMENT_TITLE
+#undef MINOR_ANNOUNCEMENT_TEXT
+#undef CHAT_ALERT_DEFAULT_SPAN
+#undef CHAT_ALERT_COLORED_SPAN
diff --git a/code/__HELPERS/spatial_info.dm b/code/__HELPERS/spatial_info.dm
index 033d5a60b7989..5a0d5c3f00237 100644
--- a/code/__HELPERS/spatial_info.dm
+++ b/code/__HELPERS/spatial_info.dm
@@ -18,7 +18,7 @@
icon_state = null
density = FALSE
move_resist = INFINITY
- invisibility = 0
+ invisibility = INVISIBILITY_NONE
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
logging = null
held_items = null //all of these are list objects that should not exist for something like us
diff --git a/code/__HELPERS/stack_trace.dm b/code/__HELPERS/stack_trace.dm
index 5c220d7a74a46..bb2d78de11080 100644
--- a/code/__HELPERS/stack_trace.dm
+++ b/code/__HELPERS/stack_trace.dm
@@ -1,4 +1,4 @@
/// gives us the stack trace from CRASH() without ending the current proc.
/// Do not call directly, use the [stack_trace] macro instead.
/proc/_stack_trace(message, file, line)
- CRASH("[message] ([file]:[line])")
+ CRASH("[message][WORKAROUND_IDENTIFIER][json_encode(list(file, line))][WORKAROUND_IDENTIFIER]")
diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm
index 345d850f3c0dc..7639f1b6a07fe 100644
--- a/code/__HELPERS/text.dm
+++ b/code/__HELPERS/text.dm
@@ -1191,3 +1191,8 @@ GLOBAL_LIST_INIT(binary, list("0","1"))
text2num(semver_regex.group[2]),
text2num(semver_regex.group[3]),
)
+
+/// Returns TRUE if the input_text ends with the ending
+/proc/endswith(input_text, ending)
+ var/input_length = LAZYLEN(ending)
+ return !!findtext(input_text, ending, -input_length)
diff --git a/code/_globalvars/admin.dm b/code/_globalvars/admin.dm
index f614061366cbe..363b84b923d32 100644
--- a/code/_globalvars/admin.dm
+++ b/code/_globalvars/admin.dm
@@ -12,3 +12,19 @@ GLOBAL_VAR(stickbanadminexemptiontimerid) //stores the timerid of the callback t
GLOBAL_LIST_INIT_TYPED(smites, /datum/smite, init_smites())
GLOBAL_VAR_INIT(admin_notice, "") // Admin notice that all clients see when joining the server
+
+// A list of all the special byond lists that need to be handled different by vv
+GLOBAL_LIST_INIT(vv_special_lists, init_special_list_names())
+
+/proc/init_special_list_names()
+ var/list/output = list()
+ var/obj/sacrifice = new
+ for(var/varname in sacrifice.vars)
+ var/value = sacrifice.vars[varname]
+ if(!islist(value))
+ if(!isdatum(value) && hascall(value, "Cut"))
+ output += varname
+ continue
+ if(isnull(locate(REF(value))))
+ output += varname
+ return output
diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm
index 708614adea662..b9137f90cbe05 100644
--- a/code/_globalvars/bitfields.dm
+++ b/code/_globalvars/bitfields.dm
@@ -51,7 +51,6 @@ DEFINE_BITFIELD(appearance_flags, list(
DEFINE_BITFIELD(area_flags, list(
"ABDUCTOR_PROOF" = ABDUCTOR_PROOF,
- "AREA_USES_STARLIGHT" = AREA_USES_STARLIGHT,
"BLOBS_ALLOWED" = BLOBS_ALLOWED,
"BLOCK_SUICIDE" = BLOCK_SUICIDE,
"CAVES_ALLOWED" = CAVES_ALLOWED,
diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm
index 5ee1f4e643954..d72ca6fb5c95b 100644
--- a/code/_globalvars/lists/mobs.dm
+++ b/code/_globalvars/lists/mobs.dm
@@ -6,11 +6,35 @@ GLOBAL_LIST_EMPTY(deadmins) //all ckeys who have used the de-admin verb.
GLOBAL_LIST_EMPTY(directory) //all ckeys with associated client
GLOBAL_LIST_EMPTY(stealthminID) //reference list with IDs that store ckeys, for stealthmins
-GLOBAL_LIST_INIT(dangerous_turfs, typecacheof(list(
- /turf/open/lava,
- /turf/open/chasm,
- /turf/open/space,
- /turf/open/openspace)))
+/// List of types of abstract mob which shouldn't usually exist in the world on its own if we're spawning random mobs
+GLOBAL_LIST_INIT(abstract_mob_types, list(
+ /mob/living/basic/blob_minion,
+ /mob/living/basic/construct,
+ /mob/living/basic/heretic_summon,
+ /mob/living/basic/mining,
+ /mob/living/basic/pet,
+ /mob/living/basic,
+ /mob/living/basic/spider,
+ /mob/living/carbon/alien/adult,
+ /mob/living/carbon/alien,
+ /mob/living/carbon/human/consistent,
+ /mob/living/carbon/human/dummy/consistent,
+ /mob/living/carbon/human/dummy,
+ /mob/living/carbon/human/species,
+ /mob/living/carbon,
+ /mob/living/silicon,
+ /mob/living/simple_animal/bot,
+ /mob/living/simple_animal/hostile/asteroid/elite,
+ /mob/living/simple_animal/hostile/asteroid,
+ /mob/living/simple_animal/hostile/guardian,
+ /mob/living/simple_animal/hostile/megafauna,
+ /mob/living/simple_animal/hostile/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/pet,
+ /mob/living/simple_animal/soulscythe, // As mimic, can't exist if spawned outside an item
+ /mob/living/simple_animal,
+))
//Since it didn't really belong in any other category, I'm putting this here
diff --git a/code/_globalvars/lists/names.dm b/code/_globalvars/lists/names.dm
index 01e6f3b59dc1f..b2662b5e804a6 100644
--- a/code/_globalvars/lists/names.dm
+++ b/code/_globalvars/lists/names.dm
@@ -24,6 +24,7 @@ GLOBAL_LIST_INIT(nightmare_names, world.file2list("strings/names/nightmare.txt")
GLOBAL_LIST_INIT(megacarp_first_names, world.file2list("strings/names/megacarp1.txt"))
GLOBAL_LIST_INIT(megacarp_last_names, world.file2list("strings/names/megacarp2.txt"))
GLOBAL_LIST_INIT(cyberauth_names, world.file2list("strings/names/cyberauth.txt"))
+GLOBAL_LIST_INIT(syndicate_monkey_names, world.file2list("strings/names/syndicate_monkey.txt"))
GLOBAL_LIST_INIT(verbs, world.file2list("strings/names/verbs.txt"))
GLOBAL_LIST_INIT(ing_verbs, world.file2list("strings/names/ing_verbs.txt"))
diff --git a/code/_globalvars/lists/plumbing.dm b/code/_globalvars/lists/plumbing.dm
new file mode 100644
index 0000000000000..10953d5ab2486
--- /dev/null
+++ b/code/_globalvars/lists/plumbing.dm
@@ -0,0 +1,17 @@
+/// List of plumbing layers as name => bitflag
+GLOBAL_LIST_INIT(plumbing_layers, list(
+ "First Layer" = FIRST_DUCT_LAYER,
+ "Second Layer" = SECOND_DUCT_LAYER,
+ "Default Layer" = THIRD_DUCT_LAYER,
+ "Fourth Layer" = FOURTH_DUCT_LAYER,
+ "Fifth Layer" = FIFTH_DUCT_LAYER,
+))
+
+/// Reverse of plumbing_layers, as "[bitflag]" => name
+GLOBAL_LIST_INIT(plumbing_layer_names, list(
+ "[FIRST_DUCT_LAYER]" = "First Layer",
+ "[SECOND_DUCT_LAYER]" = "Second Layer",
+ "[THIRD_DUCT_LAYER]" = "Default Layer",
+ "[FOURTH_DUCT_LAYER]" = "Fourth Layer",
+ "[FIFTH_DUCT_LAYER]" = "Fifth Layer",
+))
diff --git a/code/_globalvars/lists/reagents.dm b/code/_globalvars/lists/reagents.dm
index dec2724cfebd1..0a9bb7fd7f037 100644
--- a/code/_globalvars/lists/reagents.dm
+++ b/code/_globalvars/lists/reagents.dm
@@ -1,3 +1,42 @@
+//Pills & Patches
+/// List of containers the Chem Master machine can print
+GLOBAL_LIST_INIT(reagent_containers, list(
+ CAT_CONDIMENTS = list(
+ /obj/item/reagent_containers/cup/bottle,
+ /obj/item/reagent_containers/condiment/flour,
+ /obj/item/reagent_containers/condiment/sugar,
+ /obj/item/reagent_containers/condiment/rice,
+ /obj/item/reagent_containers/condiment/cornmeal,
+ /obj/item/reagent_containers/condiment/milk,
+ /obj/item/reagent_containers/condiment/soymilk,
+ /obj/item/reagent_containers/condiment/yoghurt,
+ /obj/item/reagent_containers/condiment/saltshaker,
+ /obj/item/reagent_containers/condiment/peppermill,
+ /obj/item/reagent_containers/condiment/soysauce,
+ /obj/item/reagent_containers/condiment/bbqsauce,
+ /obj/item/reagent_containers/condiment/enzyme,
+ /obj/item/reagent_containers/condiment/hotsauce,
+ /obj/item/reagent_containers/condiment/coldsauce,
+ /obj/item/reagent_containers/condiment/mayonnaise,
+ /obj/item/reagent_containers/condiment/ketchup,
+ /obj/item/reagent_containers/condiment/olive_oil,
+ /obj/item/reagent_containers/condiment/vegetable_oil,
+ /obj/item/reagent_containers/condiment/peanut_butter,
+ /obj/item/reagent_containers/condiment/cherryjelly,
+ /obj/item/reagent_containers/condiment/honey,
+ /obj/item/reagent_containers/condiment/pack,
+ ),
+ CAT_TUBES = list(
+ /obj/item/reagent_containers/cup/tube
+ ),
+ CAT_PILLS = typecacheof(list(
+ /obj/item/reagent_containers/pill/style
+ )),
+ CAT_PATCHES = typecacheof(list(
+ /obj/item/reagent_containers/pill/patch/style
+ )),
+))
+
/// list of all /datum/chemical_reaction datums indexed by their typepath. Use this for general lookup stuff
GLOBAL_LIST(chemical_reactions_list)
/// list of all /datum/chemical_reaction datums. Used during chemical reactions. Indexed by REACTANT types
diff --git a/code/_globalvars/lists/xenobiology.dm b/code/_globalvars/lists/xenobiology.dm
index 1e247a6279d39..f641e9d5eccde 100644
--- a/code/_globalvars/lists/xenobiology.dm
+++ b/code/_globalvars/lists/xenobiology.dm
@@ -73,11 +73,9 @@ GLOBAL_LIST_INIT_TYPED(cell_line_tables, /list, list(
CELL_LINE_TABLE_WALKING_MUSHROOM = list(/datum/micro_organism/cell_line/walking_mushroom = 1),
CELL_LINE_TABLE_QUEEN_BEE = list(/datum/micro_organism/cell_line/queen_bee = 1),
CELL_LINE_TABLE_BUTTERFLY = list(/datum/micro_organism/cell_line/butterfly = 1),
- CELL_LINE_TABLE_LEAPER = list(/datum/micro_organism/cell_line/leaper = 1),
CELL_LINE_TABLE_MEGA_ARACHNID = list(/datum/micro_organism/cell_line/mega_arachnid = 1),
CELL_LINE_TABLE_ALGAE = list(
/datum/micro_organism/cell_line/frog = 2,
- /datum/micro_organism/cell_line/leaper = 2,
/datum/micro_organism/cell_line/mega_arachnid = 1,
/datum/micro_organism/cell_line/queen_bee = 1,
/datum/micro_organism/cell_line/butterfly = 1,
diff --git a/code/_globalvars/logging.dm b/code/_globalvars/logging.dm
index 4d0b069867976..fc6919c3a3b86 100644
--- a/code/_globalvars/logging.dm
+++ b/code/_globalvars/logging.dm
@@ -60,9 +60,17 @@ GLOBAL_PROTECT(admin_activities)
GLOBAL_LIST_EMPTY(bombers)
GLOBAL_PROTECT(bombers)
-/// All signals here in format: "[src] used [REF(src)] @ location [src.loc]: [freq]/[code]"
-GLOBAL_LIST_EMPTY(lastsignalers)
-GLOBAL_PROTECT(lastsignalers)
+/// Investigate log for signaler usage, use the add_to_signaler_investigate_log proc
+GLOBAL_LIST_EMPTY(investigate_signaler)
+GLOBAL_PROTECT(investigate_signaler)
+
+/// Used to add a text log to the signaler investigation log.
+/// Do not add to the list directly; if the list is too large it can cause lag when an admin tries to view it.
+/proc/add_to_signaler_investigate_log(text)
+ var/log_length = length(GLOB.investigate_signaler)
+ if(log_length >= INVESTIGATE_SIGNALER_LOG_MAX_LENGTH)
+ GLOB.investigate_signaler = GLOB.investigate_signaler.Copy((INVESTIGATE_SIGNALER_LOG_MAX_LENGTH - log_length) + 2)
+ GLOB.investigate_signaler += list(text)
/// Stores who uploaded laws to which silicon-based lifeform, and what the law was
GLOBAL_LIST_EMPTY(lawchanges)
diff --git a/code/_globalvars/phobias.dm b/code/_globalvars/phobias.dm
index b37c61bc6815f..36a4329ffbf72 100644
--- a/code/_globalvars/phobias.dm
+++ b/code/_globalvars/phobias.dm
@@ -65,9 +65,9 @@ GLOBAL_LIST_INIT(phobia_mobs, list(
/mob/living/simple_animal/parrot,
)),
"conspiracies" = typecacheof(list(
- /mob/living/simple_animal/bot/secbot,
- /mob/living/simple_animal/drone,
+ /mob/living/basic/drone,
/mob/living/basic/pet/penguin,
+ /mob/living/simple_animal/bot/secbot,
)),
"doctors" = typecacheof(list(/mob/living/simple_animal/bot/medbot)),
"heresy" = typecacheof(list(
@@ -79,14 +79,14 @@ GLOBAL_LIST_INIT(phobia_mobs, list(
)),
"lizards" = typecacheof(list(/mob/living/basic/lizard)),
"robots" = typecacheof(list(
+ /mob/living/basic/drone,
/mob/living/silicon/ai,
/mob/living/silicon/robot,
/mob/living/simple_animal/bot,
- /mob/living/simple_animal/drone,
)),
"security" = typecacheof(list(/mob/living/simple_animal/bot/secbot)),
"spiders" = typecacheof(list(/mob/living/basic/spider/giant)),
- "skeletons" = typecacheof(list(/mob/living/simple_animal/hostile/skeleton)),
+ "skeletons" = typecacheof(list(/mob/living/basic/skeleton)),
"snakes" = typecacheof(list(/mob/living/basic/snake)),
"the supernatural" = typecacheof(list(
/mob/dead/observer,
@@ -97,13 +97,12 @@ GLOBAL_LIST_INIT(phobia_mobs, list(
/mob/living/basic/ghost,
/mob/living/basic/heretic_summon,
/mob/living/basic/revenant,
+ /mob/living/basic/shade,
+ /mob/living/basic/skeleton,
+ /mob/living/basic/wizard,
/mob/living/simple_animal/bot/mulebot/paranormal,
- /mob/living/simple_animal/hostile/construct,
/mob/living/simple_animal/hostile/dark_wizard,
- /mob/living/simple_animal/hostile/skeleton,
- /mob/living/simple_animal/hostile/wizard,
/mob/living/simple_animal/hostile/zombie,
- /mob/living/simple_animal/shade,
)),
))
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index dc64545a58e84..1db45879260f7 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -61,6 +61,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_RADIMMUNE" = TRAIT_RADIMMUNE,
"TRAIT_GENELESS" = TRAIT_GENELESS,
"TRAIT_VIRUSIMMUNE" = TRAIT_VIRUSIMMUNE,
+ "TRAIT_UNHUSKABLE" = TRAIT_UNHUSKABLE,
"TRAIT_PIERCEIMMUNE" = TRAIT_PIERCEIMMUNE,
"TRAIT_NODISMEMBER" = TRAIT_NODISMEMBER,
"TRAIT_NOFIRE" = TRAIT_NOFIRE,
@@ -213,6 +214,8 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_XRAY_HEARING" = TRAIT_XRAY_HEARING,
"TRAIT_TENTACLE_IMMUNE" = TRAIT_TENTACLE_IMMUNE,
"TRAIT_OVERWATCH_IMMUNE" = TRAIT_OVERWATCH_IMMUNE,
+ "TRAIT_NO_TRANSFORM" = TRAIT_NO_TRANSFORM,
+ "TRAIT_NO_PLASMA_TRANSFORM" = TRAIT_NO_PLASMA_TRANSFORM,
),
/obj/item/bodypart = list(
"TRAIT_PARALYSIS" = TRAIT_PARALYSIS,
diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm
index 0dbc6380233df..e3bfa2adf5a38 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -260,7 +260,7 @@
if(2 to INFINITY)
var/obj/dummy = new(get_turf(here))
dummy.pass_flags |= PASSTABLE
- dummy.invisibility = INVISIBILITY_ABSTRACT
+ dummy.SetInvisibility(INVISIBILITY_ABSTRACT)
for(var/i in 1 to reach) //Limit it to that many tries
var/turf/T = get_step(dummy, get_dir(dummy, there))
if(dummy.CanReach(there))
@@ -277,7 +277,10 @@
/**
- * Translates into [atom/proc/attack_hand], etc.
+ * UnarmedAttack: The higest level of mob click chain discounting click itself.
+ *
+ * This handles, just "clicking on something" without an item. It translates
+ * into [atom/proc/attack_hand], [atom/proc/attack_animal] etc.
*
* Note: proximity_flag here is used to distinguish between normal usage (flag=1),
* and usage when clicking on things telekinetically (flag=0). This proc will
diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm
index 7faf83a89fcf6..f68776f5ec9da 100644
--- a/code/_onclick/hud/alert.dm
+++ b/code/_onclick/hud/alert.dm
@@ -502,7 +502,7 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
alerttooltipstyle = "cult"
var/static/image/narnar
var/angle = 0
- var/mob/living/simple_animal/hostile/construct/Cviewer = null
+ var/mob/living/basic/construct/Cviewer
/atom/movable/screen/alert/bloodsense/Initialize(mapload, datum/hud/hud_owner)
. = ..()
@@ -776,24 +776,26 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
dead_owner.reenter_corpse()
/atom/movable/screen/alert/notify_action
- name = "Body created"
- desc = "A body was created. You can enter it."
+ name = "Something interesting is happening!"
+ desc = "This can be clicked on to perform an action."
icon_state = "template"
- timeout = 300
- var/atom/target = null
+ timeout = 30 SECONDS
+ /// The target to use the action on
+ var/atom/target
+ /// Which on click action to use
var/action = NOTIFY_JUMP
/atom/movable/screen/alert/notify_action/Click()
. = ..()
- if(!.)
- return
- if(!target)
+ if(isnull(target))
return
+
var/mob/dead/observer/ghost_owner = owner
if(!istype(ghost_owner))
return
+
switch(action)
- if(NOTIFY_ATTACK)
+ if(NOTIFY_PLAY)
target.attack_ghost(ghost_owner)
if(NOTIFY_JUMP)
var/turf/target_turf = get_turf(target)
diff --git a/code/_onclick/hud/drones.dm b/code/_onclick/hud/drones.dm
index 1cb8ade6311e7..ad3604a19e7b2 100644
--- a/code/_onclick/hud/drones.dm
+++ b/code/_onclick/hud/drones.dm
@@ -29,19 +29,17 @@
/datum/hud/dextrous/drone/persistent_inventory_update()
if(!mymob)
return
- var/mob/living/simple_animal/drone/D = mymob
+ var/mob/living/basic/drone/drone = mymob
if(hud_shown)
- if(D.internal_storage)
- D.internal_storage.screen_loc = ui_drone_storage
- D.client.screen += D.internal_storage
- if(D.head)
- D.head.screen_loc = ui_drone_head
- D.client.screen += D.head
+ if(!isnull(drone.internal_storage))
+ drone.internal_storage.screen_loc = ui_drone_storage
+ drone.client.screen += drone.internal_storage
+ if(!isnull(drone.head))
+ drone.head.screen_loc = ui_drone_head
+ drone.client.screen += drone.head
else
- if(D.internal_storage)
- D.internal_storage.screen_loc = null
- if(D.head)
- D.head.screen_loc = null
+ drone.internal_storage?.screen_loc = null
+ drone.head?.screen_loc = null
..()
diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm
index 2318b35970e1d..b915e3dc19c86 100644
--- a/code/_onclick/hud/hud.dm
+++ b/code/_onclick/hud/hud.dm
@@ -98,6 +98,11 @@ GLOBAL_LIST_INIT(available_ui_styles, list(
// subtypes can override this to force a specific UI style
var/ui_style
+ // List of weakrefs to objects that we add to our screen that we don't expect to DO anything
+ // They typically use * in their render target. They exist solely so we can reuse them,
+ // and avoid needing to make changes to all idk 300 consumers if we want to change the appearance
+ var/list/asset_refs_for_reuse = list()
+
/datum/hud/New(mob/owner)
mymob = owner
@@ -130,6 +135,11 @@ GLOBAL_LIST_INIT(available_ui_styles, list(
owner.overlay_fullscreen("see_through_darkness", /atom/movable/screen/fullscreen/see_through_darkness)
+ // Register onto the global spacelight appearances
+ // So they can be render targeted by anything in the world
+ for(var/obj/starlight_appearance/starlight as anything in GLOB.starlight_objects)
+ register_reuse(starlight)
+
RegisterSignal(SSmapping, COMSIG_PLANE_OFFSET_INCREASE, PROC_REF(on_plane_increase))
RegisterSignal(mymob, COMSIG_MOB_LOGIN, PROC_REF(client_refresh))
RegisterSignal(mymob, COMSIG_MOB_LOGOUT, PROC_REF(clear_client))
@@ -245,6 +255,8 @@ GLOBAL_LIST_INIT(available_ui_styles, list(
/datum/hud/proc/on_plane_increase(datum/source, old_max_offset, new_max_offset)
SIGNAL_HANDLER
+ for(var/i in old_max_offset + 1 to new_max_offset)
+ register_reuse(GLOB.starlight_objects[i + 1])
build_plane_groups(old_max_offset + 1, new_max_offset)
/// Creates the required plane masters to fill out new z layers (because each "level" of multiz gets its own plane master set)
@@ -373,6 +385,7 @@ GLOBAL_LIST_INIT(available_ui_styles, list(
reorganize_alerts(screenmob)
screenmob.reload_fullscreen()
update_parallax_pref(screenmob)
+ update_reuse(screenmob)
// ensure observers get an accurate and up-to-date view
if (!viewmob)
@@ -424,6 +437,22 @@ GLOBAL_LIST_INIT(available_ui_styles, list(
ui_style = new_ui_style
build_hand_slots()
+/datum/hud/proc/register_reuse(atom/movable/screen/reuse)
+ asset_refs_for_reuse += WEAKREF(reuse)
+ mymob?.client?.screen += reuse
+
+/datum/hud/proc/unregister_reuse(atom/movable/screen/reuse)
+ asset_refs_for_reuse -= WEAKREF(reuse)
+ mymob?.client?.screen -= reuse
+
+/datum/hud/proc/update_reuse(mob/show_to)
+ for(var/datum/weakref/screen_ref as anything in asset_refs_for_reuse)
+ var/atom/movable/screen/reuse = screen_ref.resolve()
+ if(isnull(reuse))
+ asset_refs_for_reuse -= screen_ref
+ continue
+ show_to.client?.screen += reuse
+
//Triggered when F12 is pressed (Unless someone changed something in the DMF)
/mob/verb/button_pressed_F12()
set name = "F12"
diff --git a/code/_onclick/hud/parallax/parallax.dm b/code/_onclick/hud/parallax/parallax.dm
index 8d4e68c911d2f..506226b41ead0 100644
--- a/code/_onclick/hud/parallax/parallax.dm
+++ b/code/_onclick/hud/parallax/parallax.dm
@@ -360,7 +360,7 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_layer)
var/turf/posobj = get_turf(boss?.eye)
if(!posobj)
return
- invisibility = is_station_level(posobj.z) ? 0 : INVISIBILITY_ABSTRACT
+ SetInvisibility(is_station_level(posobj.z) ? INVISIBILITY_NONE : INVISIBILITY_ABSTRACT, id=type)
/atom/movable/screen/parallax_layer/planet/update_o()
return //Shit won't move
diff --git a/code/_onclick/hud/parallax/random_layer.dm b/code/_onclick/hud/parallax/random_layer.dm
index c0d161db4889b..379c0cbe2d9bb 100644
--- a/code/_onclick/hud/parallax/random_layer.dm
+++ b/code/_onclick/hud/parallax/random_layer.dm
@@ -1,4 +1,4 @@
-/// Parallax layers that vary between rounds. Has come code to make sure we all have the same one
+/// Parallax layers that vary between rounds. Has some code to make sure we all have the same one
/atom/movable/screen/parallax_layer/random
blend_mode = BLEND_OVERLAY
speed = 2
@@ -12,23 +12,24 @@
/// Make this layer unique, with color or position or something
/atom/movable/screen/parallax_layer/random/proc/get_random_look()
+ return
/// Copy a parallax instance to ensure parity between everyones parallax
/atom/movable/screen/parallax_layer/random/proc/copy_parallax(atom/movable/screen/parallax_layer/random/twin)
+ return
/// For applying minor effects related to parallax. If you want big stuff, put it in a station trait or something
/atom/movable/screen/parallax_layer/random/proc/apply_global_effects()
+ return
-/// Gassy background with a few random colors, also tints starlight!
+/// Gassy background with a few random colors
/atom/movable/screen/parallax_layer/random/space_gas
icon_state = "space_gas"
/// The colors we can be
var/possible_colors = list(COLOR_TEAL, COLOR_GREEN, COLOR_SILVER, COLOR_YELLOW, COLOR_CYAN, COLOR_ORANGE, COLOR_PURPLE)
- /// The color we are. If starlight_color is not set, we also become the starlight color
+ /// The color we are
var/parallax_color
- /// The color we give to starlight
- var/starlight_color
/atom/movable/screen/parallax_layer/random/space_gas/get_random_look()
parallax_color = parallax_color || pick(possible_colors)
@@ -37,13 +38,13 @@
parallax_color = twin.parallax_color
add_atom_colour(parallax_color, ADMIN_COLOUR_PRIORITY)
-/atom/movable/screen/parallax_layer/random/space_gas/apply_global_effects()
- GLOB.starlight_color = starlight_color || parallax_color
-
/// Space gas but green for the radioactive nebula station trait
/atom/movable/screen/parallax_layer/random/space_gas/radioactive
parallax_color = list(0,0,0,0, 0,2,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0) //very vibrant green
- starlight_color = COLOR_VIBRANT_LIME
+
+/atom/movable/screen/parallax_layer/random/space_gas/radioactive/apply_global_effects()
+ . = ..()
+ set_base_starlight("#189156")
/// Big asteroid rocks appear in the background
/atom/movable/screen/parallax_layer/random/asteroids
diff --git a/code/_onclick/hud/radial.dm b/code/_onclick/hud/radial.dm
index 5bc75d85f471e..c7a4cb9ab0a6d 100644
--- a/code/_onclick/hud/radial.dm
+++ b/code/_onclick/hud/radial.dm
@@ -345,9 +345,13 @@ GLOBAL_LIST_EMPTY(radial_menus)
Choices should be a list where list keys are movables or text used for element names and return value
and list values are movables/icons/images used for element icons
*/
-/proc/show_radial_menu(mob/user, atom/anchor, list/choices, uniqueid, radius, datum/callback/custom_check, require_near = FALSE, tooltips = FALSE, no_repeat_close = FALSE, radial_slice_icon = "radial_slice")
+/proc/show_radial_menu(mob/user, atom/anchor, list/choices, uniqueid, radius, datum/callback/custom_check, require_near = FALSE, tooltips = FALSE, no_repeat_close = FALSE, radial_slice_icon = "radial_slice", autopick_single_option = TRUE)
if(!user || !anchor || !length(choices))
return
+
+ if(length(choices)==1 && autopick_single_option)
+ return choices[1]
+
if(!uniqueid)
uniqueid = "defmenu_[REF(user)]_[REF(anchor)]"
diff --git a/code/_onclick/hud/rendering/plane_master.dm b/code/_onclick/hud/rendering/plane_master.dm
index fb301a93451cc..4ae1c573ad4af 100644
--- a/code/_onclick/hud/rendering/plane_master.dm
+++ b/code/_onclick/hud/rendering/plane_master.dm
@@ -288,6 +288,9 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/plane_master)
// You aren't the source? don't change yourself
return
RegisterSignal(SSmapping, COMSIG_PLANE_OFFSET_INCREASE, PROC_REF(on_offset_increase))
+ RegisterSignal(SSdcs, COMSIG_NARSIE_SUMMON_UPDATE, PROC_REF(narsie_modified))
+ if(GLOB.narsie_summon_count >= 1)
+ narsie_start_midway(GLOB.narsie_effect_last_modified) // We assume we're on the start, so we can use this number
offset_increase(0, SSmapping.max_plane_offset)
/atom/movable/screen/plane_master/parallax/proc/on_offset_increase(datum/source, old_offset, new_offset)
@@ -332,6 +335,33 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/plane_master)
// If we're outside bounds AND we're the 0th plane, we need to show cause parallax is hacked to hell
return offset != 0 && is_outside_bounds
+/// Starts the narsie animation midway, so we can catch up to everyone else quickly
+/atom/movable/screen/plane_master/parallax/proc/narsie_start_midway(start_time)
+ var/time_elapsed = world.time - start_time
+ narsie_summoned_effect(max(16 SECONDS - time_elapsed, 0))
+
+/// Starts the narsie animation, make us grey, then red
+/atom/movable/screen/plane_master/parallax/proc/narsie_modified(datum/source, new_count)
+ SIGNAL_HANDLER
+ if(new_count >= 1)
+ narsie_summoned_effect(16 SECONDS)
+ else
+ narsie_unsummoned()
+
+/atom/movable/screen/plane_master/parallax/proc/narsie_summoned_effect(animate_time)
+ if(GLOB.narsie_summon_count >= 2)
+ var/static/list/nightmare_parallax = list(255,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,1, -130,0,0,0)
+ animate(src, color = nightmare_parallax, time = animate_time)
+ return
+
+ var/static/list/grey_parallax = list(0.4,0.4,0.4,0, 0.4,0.4,0.4,0, 0.4,0.4,0.4,0, 0,0,0,1, -0.1,-0.1,-0.1,0)
+ // We're gonna animate ourselves grey
+ // Then, once it's done, about 40 seconds into the event itself, we're gonna start doin some shit. see below
+ animate(src, color = grey_parallax, time = animate_time)
+
+/atom/movable/screen/plane_master/parallax/proc/narsie_unsummoned()
+ animate(src, color = null, time = 8 SECONDS)
+
/atom/movable/screen/plane_master/gravpulse
name = "Gravpulse"
documentation = "Ok so this one's fun. Basically, we want to be able to distort the game plane when a grav annom is around.\
diff --git a/code/_onclick/hud/rendering/render_plate.dm b/code/_onclick/hud/rendering/render_plate.dm
index 9d5b5db99338e..26541d488e252 100644
--- a/code/_onclick/hud/rendering/render_plate.dm
+++ b/code/_onclick/hud/rendering/render_plate.dm
@@ -79,6 +79,10 @@
/atom/movable/screen/plane_master/rendering_plate/game_plate/Initialize(mapload, datum/hud/hud_owner)
. = ..()
add_filter("displacer", 1, displacement_map_filter(render_source = OFFSET_RENDER_TARGET(GRAVITY_PULSE_RENDER_TARGET, offset), size = 10))
+ if(check_holidays(HALLOWEEN))
+ // Makes things a tad greyscale (leaning purple) and drops low colors for vibes
+ // We're basically using alpha as better constant here btw
+ add_filter("spook_color", 2, color_matrix_filter(list(0.75,0.13,0.13,0, 0.13,0.7,0.13,0, 0.13,0.13,0.75,0, -0.06,-0.09,-0.08,1, 0,0,0,0)))
// Blackness renders weird when you view down openspace, because of transforms and borders and such
// This is a consequence of not using lummy's grouped transparency, but I couldn't get that to work without totally fucking up
diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm
index aeb42361c0573..46ca2f53904e8 100644
--- a/code/_onclick/other_mobs.dm
+++ b/code/_onclick/other_mobs.dm
@@ -9,33 +9,66 @@
else if (secondary_result != SECONDARY_ATTACK_CALL_NORMAL)
CRASH("resolve_right_click_attack (probably attack_hand_secondary) did not return a SECONDARY_ATTACK_* define.")
-/*
- Humans:
- Adds an exception for gloves, to allow special glove types like the ninja ones.
+/**
+ * Checks if this mob is in a valid state to punch someone.
+ */
+/mob/living/proc/can_unarmed_attack()
+ return !HAS_TRAIT(src, TRAIT_HANDS_BLOCKED)
+
+/mob/living/carbon/can_unarmed_attack()
+ . = ..()
+ if(!.)
+ return FALSE
- Otherwise pretty standard.
-*/
-/mob/living/carbon/human/UnarmedAttack(atom/A, proximity_flag, list/modifiers)
- if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED))
- if(src == A)
- check_self_for_injuries()
- return
if(!has_active_hand()) //can't attack without a hand.
var/obj/item/bodypart/check_arm = get_active_hand()
if(check_arm?.bodypart_disabled)
to_chat(src, span_warning("Your [check_arm.name] is in no condition to be used."))
- return
+ return FALSE
to_chat(src, span_notice("You look at your arm and sigh."))
- return
+ return FALSE
- //This signal is needed to prevent gloves of the north star + hulk.
- if(SEND_SIGNAL(src, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, A, proximity_flag, modifiers) & COMPONENT_CANCEL_ATTACK_CHAIN)
- return
- SEND_SIGNAL(src, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, A, proximity_flag, modifiers)
+ return TRUE
+
+/mob/living/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
+ // The sole reason for this signal needing to exist is making FotNS incompatible with Hulk.
+ // Note that it is send before [proc/can_unarmed_attack] is called, keep this in mind.
+ var/sigreturn = SEND_SIGNAL(src, COMSIG_LIVING_EARLY_UNARMED_ATTACK, attack_target, modifiers)
+ if(sigreturn & COMPONENT_CANCEL_ATTACK_CHAIN)
+ return TRUE
+ if(sigreturn & COMPONENT_SKIP_ATTACK)
+ return FALSE
- if(!right_click_attack_chain(A, modifiers) && !dna?.species?.spec_unarmedattack(src, A, modifiers)) //Because species like monkeys dont use attack hand
- A.attack_hand(src, modifiers)
+ if(!can_unarmed_attack())
+ return FALSE
+
+ sigreturn = SEND_SIGNAL(src, COMSIG_LIVING_UNARMED_ATTACK, attack_target, proximity_flag, modifiers)
+ if(sigreturn & COMPONENT_CANCEL_ATTACK_CHAIN)
+ return TRUE
+ if(sigreturn & COMPONENT_SKIP_ATTACK)
+ return FALSE
+
+ if(!right_click_attack_chain(attack_target, modifiers))
+ resolve_unarmed_attack(attack_target, modifiers)
+ return TRUE
+
+/mob/living/carbon/human/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
+ // Humans can always check themself regardless of having their hands blocked or w/e
+ if(src == attack_target && !combat_mode && HAS_TRAIT(src, TRAIT_HANDS_BLOCKED))
+ check_self_for_injuries()
+ return TRUE
+
+ return ..()
+
+/mob/living/carbon/resolve_unarmed_attack(atom/attack_target, list/modifiers)
+ return attack_target.attack_paw(src, modifiers)
+
+/mob/living/carbon/human/resolve_unarmed_attack(atom/attack_target, list/modifiers)
+ if(!ISADVANCEDTOOLUSER(src))
+ return ..()
+
+ return attack_target.attack_hand(src, modifiers)
/mob/living/carbon/human/resolve_right_click_attack(atom/target, list/modifiers)
return target.attack_hand_secondary(src, modifiers)
@@ -119,17 +152,6 @@
Animals & All Unspecified
*/
-// If the UnarmedAttack chain is blocked
-#define LIVING_UNARMED_ATTACK_BLOCKED(target_atom) (HAS_TRAIT(src, TRAIT_HANDS_BLOCKED) \
- || SEND_SIGNAL(src, COMSIG_LIVING_UNARMED_ATTACK, target_atom, proximity_flag, modifiers) & COMPONENT_CANCEL_ATTACK_CHAIN)
-
-/mob/living/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
- if(LIVING_UNARMED_ATTACK_BLOCKED(attack_target))
- return FALSE
- if(!right_click_attack_chain(attack_target, modifiers))
- resolve_unarmed_attack(attack_target, modifiers)
- return TRUE
-
/**
* Called when the unarmed attack hasn't been stopped by the LIVING_UNARMED_ATTACK_BLOCKED macro or the right_click_attack_chain proc.
* This will call an attack proc that can vary from mob type to mob type on the target.
@@ -241,14 +263,14 @@
Drones
*/
-/mob/living/simple_animal/drone/resolve_unarmed_attack(atom/attack_target, proximity_flag, list/modifiers)
+/mob/living/basic/drone/resolve_unarmed_attack(atom/attack_target, proximity_flag, list/modifiers)
attack_target.attack_drone(src, modifiers)
-/mob/living/simple_animal/drone/resolve_right_click_attack(atom/target, list/modifiers)
+/mob/living/basic/drone/resolve_right_click_attack(atom/target, list/modifiers)
return target.attack_drone_secondary(src, modifiers)
/// Defaults to attack_hand. Override it when you don't want drones to do same stuff as humans.
-/atom/proc/attack_drone(mob/living/simple_animal/drone/user, list/modifiers)
+/atom/proc/attack_drone(mob/living/basic/drone/user, list/modifiers)
attack_hand(user, modifiers)
/**
@@ -256,7 +278,7 @@
* Defaults to attack_hand_secondary.
* When overriding it, remember that it ought to return a SECONDARY_ATTACK_* value.
*/
-/atom/proc/attack_drone_secondary(mob/living/simple_animal/drone/user, list/modifiers)
+/atom/proc/attack_drone_secondary(mob/living/basic/drone/user, list/modifiers)
return attack_hand_secondary(user, modifiers)
/*
@@ -295,8 +317,6 @@
GiveTarget(attack_target)
INVOKE_ASYNC(src, PROC_REF(AttackingTarget), attack_target)
-#undef LIVING_UNARMED_ATTACK_BLOCKED
-
/*
New Players:
Have no reason to click on anything at all.
diff --git a/code/controllers/subsystem/blackbox.dm b/code/controllers/subsystem/blackbox.dm
index ffe16da11ecef..3b7a6c65844e9 100644
--- a/code/controllers/subsystem/blackbox.dm
+++ b/code/controllers/subsystem/blackbox.dm
@@ -14,7 +14,9 @@ SUBSYSTEM_DEF(blackbox)
"time_dilation_current" = 3,
"science_techweb_unlock" = 2,
"round_end_stats" = 2,
- "testmerged_prs" = 2) //associative list of any feedback variables that have had their format changed since creation and their current version, remember to update this
+ "testmerged_prs" = 2,
+ "dynamic_threat" = 2,
+ ) //associative list of any feedback variables that have had their format changed since creation and their current version, remember to update this
/datum/controller/subsystem/blackbox/Initialize()
triggertime = world.time
diff --git a/code/controllers/subsystem/circuit_component.dm b/code/controllers/subsystem/circuit_component.dm
index 3ef1be5a3aa32..bae302ed9d0a3 100644
--- a/code/controllers/subsystem/circuit_component.dm
+++ b/code/controllers/subsystem/circuit_component.dm
@@ -39,7 +39,7 @@ SUBSYSTEM_DEF(circuit_component)
* Those that registered first will be executed first and those registered last will be executed last.
*/
/datum/controller/subsystem/circuit_component/proc/add_callback(datum/port/input, datum/callback/to_call)
- if(instant_run_tick == world.time && (TICK_USAGE - instant_run_start_cpu_usage) < instant_run_max_cpu_usage)
+ if(instant_run_tick == world.time && (TICK_USAGE - instant_run_start_cpu_usage) <= instant_run_max_cpu_usage)
instant_run_callbacks_to_run += to_call
return
@@ -80,7 +80,7 @@ SUBSYSTEM_DEF(circuit_component)
else
instant_run_tick = 0
- if((TICK_USAGE - instant_run_start_cpu_usage) < instant_run_max_cpu_usage)
+ if((TICK_USAGE - instant_run_start_cpu_usage) <= instant_run_max_cpu_usage)
return received_inputs
else
return null
diff --git a/code/controllers/subsystem/communications.dm b/code/controllers/subsystem/communications.dm
index c727b17f0ef43..5d2d5a2e7f409 100644
--- a/code/controllers/subsystem/communications.dm
+++ b/code/controllers/subsystem/communications.dm
@@ -38,9 +38,9 @@ SUBSYSTEM_DEF(communications)
else
var/list/message_data = user.treat_message(input)
if(syndicate)
- priority_announce(html_decode(message_data["message"]), null, 'sound/misc/announce_syndi.ogg', "Syndicate Captain", has_important_message = TRUE, players = players)
+ priority_announce(html_decode(message_data["message"]), null, 'sound/misc/announce_syndi.ogg', ANNOUNCEMENT_TYPE_SYNDICATE, has_important_message = TRUE, players = players, color_override = "red")
else
- priority_announce(html_decode(message_data["message"]), null, 'sound/misc/announce.ogg', "Captain", has_important_message = TRUE, players = players)
+ priority_announce(html_decode(message_data["message"]), null, 'sound/misc/announce.ogg', ANNOUNCEMENT_TYPE_CAPTAIN, has_important_message = TRUE, players = players)
COOLDOWN_START(src, nonsilicon_message_cooldown, COMMUNICATION_COOLDOWN)
user.log_talk(input, LOG_SAY, tag="priority announcement")
message_admins("[ADMIN_LOOKUPFLW(user)] has made a priority announcement.")
diff --git a/code/controllers/subsystem/events.dm b/code/controllers/subsystem/events.dm
index 941e74257498a..12eadcc5fe735 100644
--- a/code/controllers/subsystem/events.dm
+++ b/code/controllers/subsystem/events.dm
@@ -2,15 +2,19 @@ SUBSYSTEM_DEF(events)
name = "Events"
init_order = INIT_ORDER_EVENTS
runlevels = RUNLEVEL_GAME
-
- var/list/control = list() //list of all datum/round_event_control. Used for selecting events based on weight and occurrences.
- var/list/running = list() //list of all existing /datum/round_event
+ ///list of all datum/round_event_control. Used for selecting events based on weight and occurrences.
+ var/list/control = list()
+ ///list of all existing /datum/round_event currently being run.
+ var/list/running = list()
+ ///cache of currently running events, for lag checking.
var/list/currentrun = list()
-
- var/scheduled = 0 //The next world.time that a naturally occuring random event can be selected.
- var/frequency_lower = 1800 //3 minutes lower bound.
- var/frequency_upper = 6000 //10 minutes upper bound. Basically an event will happen every 3 to 10 minutes.
-
+ ///The next world.time that a naturally occuring random event can be selected.
+ var/scheduled = 0
+ ///The lower bound for how soon another random event can be scheduled.
+ var/frequency_lower = 2.5 MINUTES
+ ///The upper bound for how soon another random event can be scheduled.
+ var/frequency_upper = 7 MINUTES
+ ///Will wizard events be included in the event pool?
var/wizardmode = FALSE
/datum/controller/subsystem/events/Initialize()
@@ -25,7 +29,6 @@ SUBSYSTEM_DEF(events)
fill_holidays()
return SS_INIT_SUCCESS
-
/datum/controller/subsystem/events/fire(resumed = FALSE)
if(!resumed)
checkEvent() //only check these if we aren't resuming a paused fire
@@ -60,46 +63,41 @@ SUBSYSTEM_DEF(events)
if(!CONFIG_GET(flag/allow_random_events))
return
- var/players_amt = get_active_player_count(alive_check = 1, afk_check = 1, human_check = 1)
+ var/players_amt = get_active_player_count(alive_check = TRUE, afk_check = TRUE, human_check = TRUE)
// Only alive, non-AFK human players count towards this.
- var/sum_of_weights = 0
- for(var/datum/round_event_control/E in control)
- if(!E.can_spawn_event(players_amt))
+ var/list/event_roster = list()
+
+ for(var/datum/round_event_control/event_to_check in control)
+ if(!event_to_check.can_spawn_event(players_amt))
continue
- if(E.weight < 0) //for round-start events etc.
- var/res = TriggerEvent(E)
+ if(event_to_check.weight < 0) //for round-start events etc.
+ var/res = TriggerEvent(event_to_check)
if(res == EVENT_INTERRUPTED)
continue //like it never happened
if(res == EVENT_CANT_RUN)
return
- sum_of_weights += E.weight
-
- sum_of_weights = rand(0,sum_of_weights) //reusing this variable. It now represents the 'weight' we want to select
-
- for(var/datum/round_event_control/E in control)
- if(!E.can_spawn_event(players_amt))
- continue
- sum_of_weights -= E.weight
+ else
+ event_roster[event_to_check] = event_to_check.weight
- if(sum_of_weights <= 0) //we've hit our goal
- if(TriggerEvent(E))
- return
+ var/datum/round_event_control/event_to_run = pick_weight(event_roster)
+ TriggerEvent(event_to_run)
-/datum/controller/subsystem/events/proc/TriggerEvent(datum/round_event_control/E)
- . = E.preRunEvent()
+///Does the last pre-flight checks for the passed event, and runs it if the event is ready.
+/datum/controller/subsystem/events/proc/TriggerEvent(datum/round_event_control/event_to_trigger)
+ . = event_to_trigger.preRunEvent()
if(. == EVENT_CANT_RUN)//we couldn't run this event for some reason, set its max_occurrences to 0
- E.max_occurrences = 0
+ event_to_trigger.max_occurrences = 0
else if(. == EVENT_READY)
- E.run_event(random = TRUE)
-
+ event_to_trigger.run_event(random = TRUE)
+///Toggles whether or not wizard events will be in the event pool, and sends a notification to the admins.
/datum/controller/subsystem/events/proc/toggleWizardmode()
wizardmode = !wizardmode
message_admins("Summon Events has been [wizardmode ? "enabled, events will occur every [SSevents.frequency_lower / 600] to [SSevents.frequency_upper / 600] minutes" : "disabled"]!")
log_game("Summon Events was [wizardmode ? "enabled" : "disabled"]!")
-
+///Sets the event frequency bounds back to their initial value.
/datum/controller/subsystem/events/proc/resetFrequency()
frequency_lower = initial(frequency_lower)
frequency_upper = initial(frequency_upper)
diff --git a/code/controllers/subsystem/lighting.dm b/code/controllers/subsystem/lighting.dm
index 9f2f2879558aa..59ff294e959a2 100644
--- a/code/controllers/subsystem/lighting.dm
+++ b/code/controllers/subsystem/lighting.dm
@@ -6,6 +6,7 @@ SUBSYSTEM_DEF(lighting)
var/static/list/sources_queue = list() // List of lighting sources queued for update.
var/static/list/corners_queue = list() // List of lighting corners queued for update.
var/static/list/objects_queue = list() // List of lighting objects queued for update.
+ var/static/list/current_sources = list()
#ifdef VISUALIZE_LIGHT_UPDATES
var/allow_duped_values = FALSE
var/allow_duped_corners = FALSE
@@ -30,11 +31,13 @@ SUBSYSTEM_DEF(lighting)
if(!init_tick_checks)
MC_SPLIT_TICK
- var/list/queue
- var/i = 0
+ if(!resumed)
+ current_sources = sources_queue
+ sources_queue = list()
// UPDATE SOURCE QUEUE
- queue = sources_queue
+ var/i = 0
+ var/list/queue = current_sources
while(i < length(queue)) //we don't use for loop here because i cannot be changed during an iteration
i += 1
diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm
index 66acc72aa8f15..cc2c014ab220f 100644
--- a/code/controllers/subsystem/mapping.dm
+++ b/code/controllers/subsystem/mapping.dm
@@ -877,7 +877,8 @@ GLOBAL_LIST_EMPTY(the_station_areas)
/datum/controller/subsystem/mapping/proc/generate_offset_lists(gen_from, new_offset)
create_plane_offsets(gen_from, new_offset)
for(var/offset in gen_from to new_offset)
- GLOB.fullbright_overlays += create_fullbright_overlay(offset)
+ GLOB.starlight_objects += starlight_object(offset)
+ GLOB.starlight_overlays += starlight_overlay(offset)
for(var/datum/gas/gas_type as anything in GLOB.meta_gas_info)
var/list/gas_info = GLOB.meta_gas_info[gas_type]
diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm
index 6ff9d39d5313f..6e18d35dd8ff4 100644
--- a/code/controllers/subsystem/movement/movement_types.dm
+++ b/code/controllers/subsystem/movement/movement_types.dm
@@ -301,7 +301,7 @@
* repath_delay - How often we're allowed to recalculate our path
* max_path_length - The maximum number of steps we can take in a given path to search (default: 30, 0 = infinite)
* miminum_distance - Minimum distance to the target before path returns, could be used to get near a target, but not right to it - for an AI mob with a gun, for example
- * id - An ID card representing what access we have and what doors we can open
+ * access - A list representing what access we have and what doors we can open
* simulated_only - Whether we consider turfs without atmos simulation (AKA do we want to ignore space)
* avoid - If we want to avoid a specific turf, like if we're a mulebot who already got blocked by some turf
* skip_first - Whether or not to delete the first item in the path. This would be done because the first item is the starting tile, which can break things
@@ -318,7 +318,7 @@
repath_delay,
max_path_length,
minimum_distance,
- obj/item/card/id/id,
+ list/access,
simulated_only,
turf/avoid,
skip_first,
@@ -339,7 +339,7 @@
repath_delay,
max_path_length,
minimum_distance,
- id,
+ access,
simulated_only,
avoid,
skip_first,
@@ -352,8 +352,8 @@
var/max_path_length
///Minimum distance to the target before path returns
var/minimum_distance
- ///An ID card representing what access we have and what doors we can open. Kill me
- var/obj/item/card/id/id
+ ///A list representing what access we have and what doors we can open.
+ var/list/access
///Whether we consider turfs without atmos simulation (AKA do we want to ignore space)
var/simulated_only
///A perticular turf to avoid
@@ -366,30 +366,28 @@
COOLDOWN_DECLARE(repath_cooldown)
///Bool used to determine if we're already making a path in JPS. this prevents us from re-pathing while we're already busy.
var/is_pathing = FALSE
- ///Callback to invoke once we make a path
- var/datum/callback/on_finish_callback
+ ///Callbacks to invoke once we make a path
+ var/list/datum/callback/on_finish_callbacks = list()
/datum/move_loop/has_target/jps/New(datum/movement_packet/owner, datum/controller/subsystem/movement/controller, atom/moving, priority, flags, datum/extra_info)
. = ..()
- on_finish_callback = CALLBACK(src, PROC_REF(on_finish_pathing))
+ on_finish_callbacks += CALLBACK(src, PROC_REF(on_finish_pathing))
-/datum/move_loop/has_target/jps/setup(delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, obj/item/card/id/id, simulated_only, turf/avoid, skip_first, list/initial_path)
+/datum/move_loop/has_target/jps/setup(delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, list/access, simulated_only, turf/avoid, skip_first, list/initial_path)
. = ..()
if(!.)
return
src.repath_delay = repath_delay
src.max_path_length = max_path_length
src.minimum_distance = minimum_distance
- src.id = id
+ src.access = access
src.simulated_only = simulated_only
src.avoid = avoid
src.skip_first = skip_first
movement_path = initial_path?.Copy()
- if(isidcard(id))
- RegisterSignal(id, COMSIG_QDELETING, PROC_REF(handle_no_id)) //I prefer erroring to harddels. If this breaks anything consider making id info into a datum or something
-/datum/move_loop/has_target/jps/compare_loops(datum/move_loop/loop_type, priority, flags, extra_info, delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, obj/item/card/id/id, simulated_only, turf/avoid, skip_first, initial_path)
- if(..() && repath_delay == src.repath_delay && max_path_length == src.max_path_length && minimum_distance == src.minimum_distance && id == src.id && simulated_only == src.simulated_only && avoid == src.avoid)
+/datum/move_loop/has_target/jps/compare_loops(datum/move_loop/loop_type, priority, flags, extra_info, delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, list/access, simulated_only, turf/avoid, skip_first, initial_path)
+ if(..() && repath_delay == src.repath_delay && max_path_length == src.max_path_length && minimum_distance == src.minimum_distance && access ~= src.access && simulated_only == src.simulated_only && avoid == src.avoid)
return TRUE
return FALSE
@@ -403,21 +401,16 @@
movement_path = null
/datum/move_loop/has_target/jps/Destroy()
- id = null //Kill me
avoid = null
- on_finish_callback = null
+ on_finish_callbacks = null
return ..()
-/datum/move_loop/has_target/jps/proc/handle_no_id()
- SIGNAL_HANDLER
- id = null
-
///Tries to calculate a new path for this moveloop.
/datum/move_loop/has_target/jps/proc/recalculate_path()
if(!COOLDOWN_FINISHED(src, repath_cooldown))
return
COOLDOWN_START(src, repath_cooldown, repath_delay)
- if(SSpathfinder.pathfind(moving, target, max_path_length, minimum_distance, id, simulated_only, avoid, skip_first, on_finish = on_finish_callback))
+ if(SSpathfinder.pathfind(moving, target, max_path_length, minimum_distance, access, simulated_only, avoid, skip_first, on_finish = on_finish_callbacks))
is_pathing = TRUE
SEND_SIGNAL(src, COMSIG_MOVELOOP_JPS_REPATH)
diff --git a/code/controllers/subsystem/nightshift.dm b/code/controllers/subsystem/nightshift.dm
index 78583cef3503f..b8df42742e43c 100644
--- a/code/controllers/subsystem/nightshift.dm
+++ b/code/controllers/subsystem/nightshift.dm
@@ -24,7 +24,12 @@ SUBSYSTEM_DEF(nightshift)
check_nightshift()
/datum/controller/subsystem/nightshift/proc/announce(message)
- priority_announce(message, sound='sound/misc/notice2.ogg', sender_override="Automated Lighting System Announcement")
+ priority_announce(
+ text = message,
+ sound = 'sound/misc/notice2.ogg',
+ sender_override = "Automated Lighting System Announcement",
+ color_override = "grey",
+ )
/datum/controller/subsystem/nightshift/proc/check_nightshift()
var/emergency = SSsecurity_level.get_current_level_as_number() >= SEC_LEVEL_RED
diff --git a/code/controllers/subsystem/pathfinder.dm b/code/controllers/subsystem/pathfinder.dm
index c503826b9bd87..fa1a7af5c8598 100644
--- a/code/controllers/subsystem/pathfinder.dm
+++ b/code/controllers/subsystem/pathfinder.dm
@@ -8,6 +8,10 @@ SUBSYSTEM_DEF(pathfinder)
var/list/datum/pathfind/active_pathing = list()
/// List of pathfind datums being ACTIVELY processed. exists to make subsystem stats readable
var/list/datum/pathfind/currentrun = list()
+ /// List of uncheccked source_to_map entries
+ var/list/currentmaps = list()
+ /// Assoc list of target turf -> list(/datum/path_map) centered on the turf
+ var/list/source_to_maps = list()
var/static/space_type_cache
/datum/controller/subsystem/pathfinder/Initialize()
@@ -23,6 +27,7 @@ SUBSYSTEM_DEF(pathfinder)
/datum/controller/subsystem/pathfinder/fire(resumed)
if(!resumed)
src.currentrun = active_pathing.Copy()
+ src.currentmaps = deep_copy_list(source_to_maps)
// Dies of sonic speed from caching datum var reads
var/list/currentrun = src.currentrun
@@ -38,10 +43,165 @@ SUBSYSTEM_DEF(pathfinder)
// Next please
currentrun.len--
+ // Go over our existing pathmaps, clear out the ones we aren't using
+ var/list/currentmaps = src.currentmaps
+ var/oldest_time = world.time - MAP_REUSE_SLOWEST
+ while(length(currentmaps))
+ var/turf/source = currentmaps[length(currentmaps)]
+ var/list/datum/path_map/owned_maps = currentmaps[source]
+ for(var/datum/path_map/map as anything in owned_maps)
+ if(map.creation_time < oldest_time && !map.building)
+ source_to_maps[source] -= map
+ owned_maps.len--
+ if(MC_TICK_CHECK)
+ return
+ if(!length(source_to_maps[source]))
+ source_to_maps -= source
+
+ currentmaps.len--
+
/// Initiates a pathfind. Returns true if we're good, FALSE if something's failed
-/datum/controller/subsystem/pathfinder/proc/pathfind(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_safety=TRUE, datum/callback/on_finish)
- var/datum/pathfind/path = new(caller, end, id, max_distance, mintargetdist, simulated_only, exclude, skip_first, diagonal_safety, on_finish)
+/datum/controller/subsystem/pathfinder/proc/pathfind(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, access = list(), simulated_only = TRUE, turf/exclude, skip_first = TRUE, diagonal_handling = DIAGONAL_REMOVE_CLUNKY, list/datum/callback/on_finish)
+ var/datum/pathfind/jps/path = new()
+ path.setup(caller, access, max_distance, simulated_only, exclude, on_finish, end, mintargetdist, skip_first, diagonal_handling)
+ if(path.start())
+ active_pathing += path
+ return TRUE
+ return FALSE
+
+/// Initiates a swarmed pathfind. Returns TRUE if we're good, FALSE if something's failed
+/// If a valid pathmap exists for the TARGET turf we'll use that, otherwise we have to build a new one
+/datum/controller/subsystem/pathfinder/proc/swarmed_pathfind(atom/movable/caller, atom/end, max_distance = 30, mintargetdist = 0, age = MAP_REUSE_INSTANT, access = list(), simulated_only = TRUE, turf/exclude, skip_first = TRUE, list/datum/callback/on_finish)
+ var/turf/target = get_turf(end)
+ var/datum/can_pass_info/pass_info = new(caller, access)
+ // If there's a map we can use already, use it
+ var/datum/path_map/valid_map = get_valid_map(pass_info, target, simulated_only, exclude, age, include_building = TRUE)
+ if(valid_map && valid_map.expand(max_distance))
+ path_map_passalong(on_finish, get_turf(caller), mintargetdist, skip_first, valid_map)
+ return TRUE
+
+ // Otherwise we're gonna make a new one, and turn it into a path for the callbacks passed into us
+ var/list/datum/callback/pass_in = list()
+ pass_in += CALLBACK(GLOBAL_PROC, /proc/path_map_passalong, on_finish, get_turf(caller), mintargetdist, skip_first)
+ // And to allow subsequent calls to reuse the same map, we'll put a placeholder in the cache, and fill it up when the pathing finishes
+ var/datum/path_map/empty = new()
+ empty.pass_info = new(caller, access)
+ empty.start = target
+ empty.pass_space = simulated_only
+ empty.avoid = exclude
+ empty.building = TRUE
+ path_map_cache(target, empty)
+ pass_in += CALLBACK(src, PROC_REF(path_map_fill), target, empty)
+ if(!SSpathfinder.can_pass_build_map(pass_info, target, max_distance, simulated_only, exclude, pass_in))
+ return FALSE
+ return TRUE
+
+/// We generate a path for the passed in callbacks, and then pipe it over
+/proc/path_map_passalong(list/datum/callback/return_callbacks, turf/target, mintargetdist = 0, skip_first = TRUE, datum/path_map/hand_back)
+ var/list/requested_path
+ if(istype(hand_back, /datum/path_map))
+ requested_path = hand_back.get_path_from(target, skip_first, mintargetdist)
+ for(var/datum/callback/return_callback as anything in return_callbacks)
+ return_callback.Invoke(requested_path)
+
+/// Caches the passed in path_map, allowing for reuse in future
+/datum/controller/subsystem/pathfinder/proc/path_map_cache(turf/target, datum/path_map/hand_back)
+ // Cache our path_map
+ if(!target || !hand_back)
+ return
+ source_to_maps[target] += list(hand_back)
+
+/datum/controller/subsystem/pathfinder/proc/path_map_fill(turf/target, datum/path_map/fill_into, datum/path_map/hand_back)
+ fill_into.building = FALSE
+ if(!fill_into.compare_against(hand_back))
+ source_to_maps[target] -= fill_into
+ return
+ fill_into.copy_from(hand_back)
+ fill_into.creation_time = hand_back.creation_time
+ // If we aren't in the source list anymore don't go trying to clear it out yeah?
+ if(!source_to_maps[target] || !(fill_into in source_to_maps[target]))
+ return
+ // Let's remove anything we're better than
+ for(var/datum/path_map/same_target as anything in source_to_maps[target])
+ if(fill_into == same_target || !same_target.compare_against(hand_back))
+ continue
+ // If it's still being made it'll be fresher then us
+ if(same_target.building)
+ continue
+ // We assume that we are fresher, and that's all we care about
+ // If it's being expanded it'll get updated when that finishes, then clear when all the refs drop
+ source_to_maps[target] -= same_target
+
+/// Initiates a SSSP run. Returns true if we're good, FALSE if something's failed
+/datum/controller/subsystem/pathfinder/proc/build_map(atom/movable/caller, turf/source, max_distance = 30, access = list(), simulated_only = TRUE, turf/exclude, list/datum/callback/on_finish)
+ var/datum/pathfind/sssp/path = new()
+ path.setup(caller, access, source, max_distance, simulated_only, exclude, on_finish)
+ if(path.start())
+ active_pathing += path
+ return TRUE
+ return FALSE
+
+/// Initiates a SSSP run from a pass_info datum. Returns true if we're good, FALSE if something's failed
+/datum/controller/subsystem/pathfinder/proc/can_pass_build_map(datum/can_pass_info/pass_info, turf/source, max_distance = 30, simulated_only = TRUE, turf/exclude, list/datum/callback/on_finish)
+ var/datum/pathfind/sssp/path = new()
+ path.setup_from_canpass(pass_info, source, max_distance, simulated_only, exclude, on_finish)
if(path.start())
active_pathing += path
return TRUE
return FALSE
+
+/// Begins to handle a pathfinding run based off the input /datum/pathfind datum
+/// You should not use this, it exists to allow for shenanigans. You do not know how to do shenanigans
+/datum/controller/subsystem/pathfinder/proc/run_pathfind(datum/pathfind/run)
+ active_pathing += run
+ return TRUE
+
+/// Takes a set of pathfind info, returns the first valid pathmap that would work if one exists
+/// Optionally takes a max age to accept (defaults to 0 seconds) and a minimum acceptable range
+/// If include_building is true and we can only find a building path, ew'll use that instead. tho we will wait for it to finish first
+/datum/controller/subsystem/pathfinder/proc/get_valid_map(datum/can_pass_info/pass_info, turf/target, simulated_only = TRUE, turf/exclude, age = MAP_REUSE_INSTANT, min_range = -INFINITY, include_building = FALSE)
+ // Walk all the maps that match our caller's turf OR our target's
+ // Then hold onto em. If their cache time is short we can reuse/expand them, if not we'll have to make a new one
+ var/oldest_time = world.time - age
+ /// Backup return value used if no finished pathmaps are found
+ var/datum/path_map/constructing
+ for(var/datum/path_map/shared_source as anything in source_to_maps[target])
+ if(!shared_source.compare_against_args(pass_info, target, simulated_only, exclude))
+ continue
+ var/max_dist = 0
+ if(shared_source.distances.len)
+ max_dist = shared_source.distances[shared_source.distances.len]
+ if(max_dist < min_range)
+ continue
+ if(oldest_time > shared_source.creation_time && !shared_source.building)
+ continue
+ if(shared_source.building)
+ if(include_building)
+ constructing = constructing || shared_source
+ continue
+
+ return shared_source
+ if(constructing)
+ UNTIL(constructing.building == FALSE)
+ return constructing
+ return null
+
+/// Takes a set of pathfind info, returns all valid pathmaps that would work
+/// Takes an optional minimum range arg
+/datum/controller/subsystem/pathfinder/proc/get_valid_maps(datum/can_pass_info/pass_info, turf/target, simulated_only = TRUE, turf/exclude, age = MAP_REUSE_INSTANT, min_range = -INFINITY, include_building = FALSE)
+ // Walk all the maps that match our caller's turf OR our target's
+ // Then hold onto em. If their cache time is short we can reuse/expand them, if not we'll have to make a new one
+ var/list/valid_maps = list()
+ var/oldest_time = world.time - age
+ for(var/datum/path_map/shared_source as anything in source_to_maps[target])
+ if(shared_source.compare_against_args(pass_info, target, simulated_only, exclude))
+ continue
+ var/max_dist = shared_source.distances[shared_source.distances.len]
+ if(max_dist < min_range)
+ continue
+ if(oldest_time > shared_source.creation_time)
+ continue
+ if(!include_building && shared_source.building)
+ continue
+ valid_maps += shared_source
+ return valid_maps
diff --git a/code/controllers/subsystem/persistence.dm b/code/controllers/subsystem/persistence.dm
deleted file mode 100644
index cd4817710d6f8..0000000000000
--- a/code/controllers/subsystem/persistence.dm
+++ /dev/null
@@ -1,573 +0,0 @@
-#define FILE_RECENT_MAPS "data/RecentMaps.json"
-
-#define KEEP_ROUNDS_MAP 3
-
-SUBSYSTEM_DEF(persistence)
- name = "Persistence"
- init_order = INIT_ORDER_PERSISTENCE
- flags = SS_NO_FIRE
-
- ///instantiated wall engraving components
- var/list/wall_engravings = list()
- ///all saved persistent engravings loaded from JSON
- var/list/saved_engravings = list()
- ///tattoo stories that we're saving.
- var/list/prison_tattoos_to_save = list()
- ///tattoo stories that have been selected for this round.
- var/list/prison_tattoos_to_use = list()
- var/list/saved_messages = list()
- var/list/saved_modes = list(1,2,3)
- var/list/saved_maps = list()
- var/list/blocked_maps = list()
- var/list/saved_trophies = list()
- var/list/picture_logging_information = list()
- var/list/obj/structure/sign/picture_frame/photo_frames
- var/list/obj/item/storage/photo_album/photo_albums
- var/rounds_since_engine_exploded = 0
- var/delam_highscore = 0
- var/tram_hits_this_round = 0
- var/tram_hits_last_round = 0
-
-/datum/controller/subsystem/persistence/Initialize()
- load_poly()
- load_wall_engravings()
- load_prisoner_tattoos()
- load_trophies()
- load_recent_maps()
- load_photo_persistence()
- load_randomized_recipes()
- load_custom_outfits()
- load_delamination_counter()
- load_tram_counter()
- load_adventures()
- return SS_INIT_SUCCESS
-
-///Collects all data to persist.
-/datum/controller/subsystem/persistence/proc/collect_data()
- save_wall_engravings()
- save_prisoner_tattoos()
- collect_trophies()
- collect_maps()
- save_photo_persistence() //THIS IS PERSISTENCE, NOT THE LOGGING PORTION.
- save_randomized_recipes()
- save_scars()
- save_custom_outfits()
- save_delamination_counter()
- if(SStramprocess.can_fire)
- save_tram_counter()
-
-///Loads up Poly's speech buffer.
-/datum/controller/subsystem/persistence/proc/load_poly()
- for(var/mob/living/simple_animal/parrot/poly/P in GLOB.alive_mob_list)
- twitterize(P.speech_buffer, "polytalk")
- break //Who's been duping the bird?!
-
-///Loads all engravings, and places a select amount in maintenance and the prison.
-/datum/controller/subsystem/persistence/proc/load_wall_engravings()
- var/json_file = file(ENGRAVING_SAVE_FILE)
- if(!fexists(json_file))
- return
-
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
-
- if(json["version"] < ENGRAVING_PERSISTENCE_VERSION)
- update_wall_engravings(json)
-
- saved_engravings = json["entries"]
-
- if(!saved_engravings.len)
- log_world("Failed to load engraved messages on map [SSmapping.config.map_name]")
- return
-
- var/list/viable_turfs = get_area_turfs(/area/station/maintenance, subtypes = TRUE) + get_area_turfs(/area/station/security/prison, subtypes = TRUE)
- var/list/turfs_to_pick_from = list()
-
- for(var/turf/T as anything in viable_turfs)
- if(!isclosedturf(T))
- continue
- turfs_to_pick_from += T
-
- var/successfully_loaded_engravings = 0
-
- for(var/iteration in 1 to rand(MIN_PERSISTENT_ENGRAVINGS, MAX_PERSISTENT_ENGRAVINGS))
- var/engraving = pick_n_take(saved_engravings)
- if(!islist(engraving))
- stack_trace("something's wrong with the engraving data! one of the saved engravings wasn't a list!")
- continue
-
- var/turf/closed/engraved_wall = pick(turfs_to_pick_from)
-
- if(HAS_TRAIT(engraved_wall, TRAIT_NOT_ENGRAVABLE))
- continue
-
- engraved_wall.AddComponent(/datum/component/engraved, engraving["story"], FALSE, engraving["story_value"])
- successfully_loaded_engravings++
- turfs_to_pick_from -= engraved_wall
-
- log_world("Loaded [successfully_loaded_engravings] engraved messages on map [SSmapping.config.map_name]")
-
-///Saves all new engravings in the world.
-/datum/controller/subsystem/persistence/proc/save_wall_engravings()
- var/list/saved_data = list()
-
- saved_data["version"] = ENGRAVING_PERSISTENCE_VERSION
- saved_data["entries"] = list()
-
-
- var/json_file = file(ENGRAVING_SAVE_FILE)
- if(fexists(json_file))
- var/list/old_json = json_decode(file2text(json_file))
- if(old_json)
- saved_data["entries"] = old_json["entries"]
-
- for(var/datum/component/engraved/engraving in wall_engravings)
- if(!engraving.persistent_save)
- continue
- var/area/engraved_area = get_area(engraving.parent)
- if(!(engraved_area.area_flags & PERSISTENT_ENGRAVINGS))
- continue
- saved_data["entries"] += engraving.save_persistent()
-
- fdel(json_file)
-
- WRITE_FILE(json_file, json_encode(saved_data))
-
-///This proc can update entries if the format has changed at some point.
-/datum/controller/subsystem/persistence/proc/update_wall_engravings(json)
- for(var/engraving_entry in json["entries"])
- continue //no versioning yet
-
- //Save it to the file
- var/json_file = file(ENGRAVING_SAVE_FILE)
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(json))
-
- return json
-
-///Loads all tattoos, and select a few based on the amount of prisoner spawn positions.
-/datum/controller/subsystem/persistence/proc/load_prisoner_tattoos()
- var/json_file = file(PRISONER_TATTOO_SAVE_FILE)
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
-
- if(json["version"] < TATTOO_PERSISTENCE_VERSION)
- update_prisoner_tattoos(json)
-
- var/datum/job/prisoner_datum = SSjob.name_occupations[JOB_PRISONER]
- if(!prisoner_datum)
- return
- var/iterations_allowed = prisoner_datum.spawn_positions
-
- var/list/entries = json["entries"]
- if(entries.len)
- for(var/index in 1 to iterations_allowed)
- prison_tattoos_to_use += list(entries[rand(1, entries.len)])
-
- log_world("Loaded [prison_tattoos_to_use.len] prison tattoos")
-
-///Saves all tattoos, so they can appear on prisoners in future rounds
-/datum/controller/subsystem/persistence/proc/save_prisoner_tattoos()
- var/json_file = file(PRISONER_TATTOO_SAVE_FILE)
- var/list/saved_data = list()
- var/list/entries = list()
-
- if(fexists(json_file))
- var/list/old_json = json_decode(file2text(json_file))
- if(old_json)
- entries += old_json["entries"] //Save the old if its there
-
- entries += prison_tattoos_to_save
-
- saved_data["version"] = ENGRAVING_PERSISTENCE_VERSION
- saved_data["entries"] = entries
-
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(saved_data))
-
-///This proc can update entries if the format has changed at some point.
-/datum/controller/subsystem/persistence/proc/update_prisoner_tattoos(json)
- for(var/tattoo_entry in json["entries"])
- continue //no versioning yet
-
- //Save it to the file
- var/json_file = file(PRISONER_TATTOO_SAVE_FILE)
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(json))
-
- return json
-
-/// Loads the trophies from the source file, and places a few in trophy display cases.
-/datum/controller/subsystem/persistence/proc/load_trophies()
- var/list/raw_saved_trophies = list()
- if(fexists("data/npc_saves/TrophyItems.json"))
- var/json_file = file("data/npc_saves/TrophyItems.json")
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
- raw_saved_trophies = json["data"]
- fdel("data/npc_saves/TrophyItems.json")
- else
- var/json_file = file("data/trophy_items.json")
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
- raw_saved_trophies = json["data"]
-
- for(var/raw_json in raw_saved_trophies)
- var/datum/trophy_data/parsed_trophy_data = new
- parsed_trophy_data.load_from_json(raw_json)
- saved_trophies += parsed_trophy_data
-
- set_up_trophies()
-
-///trophy data datum, for admin manipulation
-/datum/trophy_data
- ///path of the item the trophy will try to mimic, null if path_string is invalid
- var/path
- ///the message that appears under the item
- var/message
- ///the key of the one who placed the item in the trophy case
- var/placer_key
-
-/datum/trophy_data/proc/load_from_json(list/json_data)
- path = json_data["path"]
- message = json_data["message"]
- placer_key = json_data["placer_key"]
-
-/datum/trophy_data/proc/to_json()
- var/list/new_data = list()
- new_data["path"] = path
- new_data["message"] = message
- new_data["placer_key"] = placer_key
- new_data["is_valid"] = text2path(path) ? TRUE : FALSE
- return new_data
-
-/// Returns a list for the admin trophy panel.
-/datum/controller/subsystem/persistence/proc/trophy_ui_data()
- var/list/ui_data = list()
- for(var/datum/trophy_data/data in saved_trophies)
- var/list/pdata = data.to_json()
- pdata["ref"] = REF(data)
- ui_data += list(pdata)
-
- return ui_data
-
-/// Loads up the amount of times maps appeared to alter their appearance in voting and rotation.
-/datum/controller/subsystem/persistence/proc/load_recent_maps()
- var/map_sav = FILE_RECENT_MAPS
- if(!fexists(FILE_RECENT_MAPS))
- return
- var/list/json = json_decode(file2text(map_sav))
- if(!json)
- return
- saved_maps = json["data"]
-
- //Convert the mapping data to a shared blocking list, saves us doing this in several places later.
- for(var/map in config.maplist)
- var/datum/map_config/VM = config.maplist[map]
- var/run = 0
- if(VM.map_name == SSmapping.config.map_name)
- run++
- for(var/name in SSpersistence.saved_maps)
- if(VM.map_name == name)
- run++
- if(run >= 2) //If run twice in the last KEEP_ROUNDS_MAP + 1 (including current) rounds, disable map for voting and rotation.
- blocked_maps += VM.map_name
-
-/// Puts trophies into trophy cases.
-/datum/controller/subsystem/persistence/proc/set_up_trophies()
-
- var/list/valid_trophies = list()
-
- for(var/datum/trophy_data/data in saved_trophies)
-
- if(!data) //sanity for incorrect deserialization
- continue
-
- var/path = text2path(data.path)
- if(!path) //If the item no longer exist, ignore it
- continue
-
- valid_trophies += data
-
- for(var/obj/structure/displaycase/trophy/trophy_case in GLOB.trophy_cases)
- if(!valid_trophies.len)
- break
-
- if(trophy_case.showpiece)
- continue
-
- trophy_case.set_up_trophy(pick_n_take(valid_trophies))
-
-///Loads up the photo album source file.
-/datum/controller/subsystem/persistence/proc/get_photo_albums()
- var/album_path = file("data/photo_albums.json")
- if(fexists(album_path))
- return json_decode(file2text(album_path))
-
-///Loads up the photo frames source file.
-/datum/controller/subsystem/persistence/proc/get_photo_frames()
- var/frame_path = file("data/photo_frames.json")
- if(fexists(frame_path))
- return json_decode(file2text(frame_path))
-
-/// Removes the identifier of a persistent photo frame from the json.
-/datum/controller/subsystem/persistence/proc/remove_photo_frames(identifier)
- var/frame_path = file("data/photo_frames.json")
- if(!fexists(frame_path))
- return
-
- var/frame_json = json_decode(file2text(frame_path))
- frame_json -= identifier
-
- frame_json = json_encode(frame_json)
- fdel(frame_path)
- WRITE_FILE(frame_path, frame_json)
-
-///Loads photo albums, and populates them; also loads and applies frames to picture frames.
-/datum/controller/subsystem/persistence/proc/load_photo_persistence()
- var/album_path = file("data/photo_albums.json")
- var/frame_path = file("data/photo_frames.json")
- if(fexists(album_path))
- var/list/json = json_decode(file2text(album_path))
- if(json.len)
- for(var/i in photo_albums)
- var/obj/item/storage/photo_album/A = i
- if(!A.persistence_id)
- continue
- if(json[A.persistence_id])
- A.populate_from_id_list(json[A.persistence_id])
-
- if(fexists(frame_path))
- var/list/json = json_decode(file2text(frame_path))
- if(json.len)
- for(var/i in photo_frames)
- var/obj/structure/sign/picture_frame/PF = i
- if(!PF.persistence_id)
- continue
- if(json[PF.persistence_id])
- PF.load_from_id(json[PF.persistence_id])
-
-///Saves the contents of photo albums and the picture frames.
-/datum/controller/subsystem/persistence/proc/save_photo_persistence()
- var/album_path = file("data/photo_albums.json")
- var/frame_path = file("data/photo_frames.json")
-
- var/list/frame_json = list()
- var/list/album_json = list()
-
- if(fexists(album_path))
- album_json = json_decode(file2text(album_path))
- fdel(album_path)
-
- for(var/i in photo_albums)
- var/obj/item/storage/photo_album/A = i
- if(!istype(A) || !A.persistence_id)
- continue
- var/list/L = A.get_picture_id_list()
- album_json[A.persistence_id] = L
-
- album_json = json_encode(album_json)
-
- WRITE_FILE(album_path, album_json)
-
- if(fexists(frame_path))
- frame_json = json_decode(file2text(frame_path))
- fdel(frame_path)
-
- for(var/i in photo_frames)
- var/obj/structure/sign/picture_frame/F = i
- if(!istype(F) || !F.persistence_id)
- continue
- frame_json[F.persistence_id] = F.get_photo_id()
-
- frame_json = json_encode(frame_json)
-
- WRITE_FILE(frame_path, frame_json)
-
-///Collects trophies from all existing trophy cases.
-/datum/controller/subsystem/persistence/proc/collect_trophies()
- for(var/trophy_case in GLOB.trophy_cases)
- save_trophy(trophy_case)
-
- var/json_file = file("data/trophy_items.json")
- var/list/file_data = list()
- var/list/converted_data = list()
-
- for(var/datum/trophy_data/data in saved_trophies)
- converted_data += list(data.to_json())
-
- converted_data = remove_duplicate_trophies(converted_data)
-
- file_data["data"] = converted_data
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(file_data))
-
-///gets the list of json trophies, and deletes the ones with an identical path and message
-/datum/controller/subsystem/persistence/proc/remove_duplicate_trophies(list/trophies)
- var/list/ukeys = list()
- . = list()
- for(var/trophy in trophies)
- var/tkey = "[trophy["path"]]-[trophy["message"]]"
- if(ukeys[tkey])
- continue
- else
- . += list(trophy)
- ukeys[tkey] = TRUE
-
-///If there is a trophy in the trophy case, saved it, if the trophy was not a holo trophy and has a message attached.
-/datum/controller/subsystem/persistence/proc/save_trophy(obj/structure/displaycase/trophy/trophy_case)
- if(!trophy_case.holographic_showpiece && trophy_case.showpiece && trophy_case.trophy_message)
- var/datum/trophy_data/data = new
- data.path = trophy_case.showpiece.type
- data.message = trophy_case.trophy_message
- data.placer_key = trophy_case.placer_key
- saved_trophies += data
-
-///Updates the list of the most recent maps.
-/datum/controller/subsystem/persistence/proc/collect_maps()
- if(length(saved_maps) > KEEP_ROUNDS_MAP) //Get rid of extras from old configs.
- saved_maps.Cut(KEEP_ROUNDS_MAP+1)
- var/mapstosave = min(length(saved_maps)+1, KEEP_ROUNDS_MAP)
- if(length(saved_maps) < mapstosave) //Add extras if too short, one per round.
- saved_maps += mapstosave
- for(var/i = mapstosave; i > 1; i--)
- saved_maps[i] = saved_maps[i-1]
- saved_maps[1] = SSmapping.config.map_name
- var/json_file = file(FILE_RECENT_MAPS)
- var/list/file_data = list()
- file_data["data"] = saved_maps
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(file_data))
-
-///Loads all randomized recipes.
-/datum/controller/subsystem/persistence/proc/load_randomized_recipes()
- var/json_file = file("data/RandomizedChemRecipes.json")
- var/json
- if(fexists(json_file))
- json = json_decode(file2text(json_file))
-
- for(var/randomized_type in subtypesof(/datum/chemical_reaction/randomized))
- var/datum/chemical_reaction/randomized/R = new randomized_type
- var/loaded = FALSE
- if(R.persistent && json)
- var/list/recipe_data = json["[R.type]"]
- if(recipe_data)
- if(R.LoadOldRecipe(recipe_data) && (daysSince(R.created) <= R.persistence_period))
- loaded = TRUE
- if(!loaded) //We do not have information for whatever reason, just generate new one
- if(R.persistent)
- log_game("Resetting persistent [randomized_type] random recipe.")
- R.GenerateRecipe()
-
- if(!R.HasConflicts()) //Might want to try again if conflicts happened in the future.
- add_chemical_reaction(R)
- else
- log_game("Randomized recipe [randomized_type] resulted in conflicting recipes.")
-
-///Saves all randomized recipes.
-/datum/controller/subsystem/persistence/proc/save_randomized_recipes()
- var/json_file = file("data/RandomizedChemRecipes.json")
- var/list/file_data = list()
-
- //asert globchems done
- for(var/randomized_type in subtypesof(/datum/chemical_reaction/randomized))
- var/datum/chemical_reaction/randomized/R = get_chemical_reaction(randomized_type) //ew, would be nice to add some simple tracking
- if(R?.persistent)
- var/list/recipe_data = R.SaveOldRecipe()
- file_data["[R.type]"] = recipe_data
-
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(file_data))
-
-///Saves all scars for everyone's original characters
-/datum/controller/subsystem/persistence/proc/save_scars()
- for(var/i in GLOB.joined_player_list)
- var/mob/living/carbon/human/ending_human = get_mob_by_ckey(i)
- if(!istype(ending_human) || !ending_human.mind?.original_character_slot_index || !ending_human.client?.prefs.read_preference(/datum/preference/toggle/persistent_scars))
- continue
-
- var/mob/living/carbon/human/original_human = ending_human.mind.original_character.resolve()
-
- if(!original_human)
- continue
-
- if(original_human.stat == DEAD || !original_human.all_scars || original_human != ending_human)
- original_human.save_persistent_scars(TRUE)
- else
- original_human.save_persistent_scars()
-
-///Loads the custom outfits of every admin.
-/datum/controller/subsystem/persistence/proc/load_custom_outfits()
- var/file = file("data/custom_outfits.json")
- if(!fexists(file))
- return
- var/outfits_json = file2text(file)
- var/list/outfits = json_decode(outfits_json)
- if(!islist(outfits))
- return
-
- for(var/outfit_data in outfits)
- if(!islist(outfit_data))
- continue
-
- var/outfittype = text2path(outfit_data["outfit_type"])
- if(!ispath(outfittype, /datum/outfit))
- continue
- var/datum/outfit/outfit = new outfittype
- if(!outfit.load_from(outfit_data))
- continue
- GLOB.custom_outfits += outfit
-
-///Saves each admin's custom outfit list
-/datum/controller/subsystem/persistence/proc/save_custom_outfits()
- var/file = file("data/custom_outfits.json")
- fdel(file)
-
- var/list/data = list()
- for(var/datum/outfit/outfit in GLOB.custom_outfits)
- data += list(outfit.get_json_data())
-
- WRITE_FILE(file, json_encode(data))
-
-/// Location where we save the information about how many rounds it has been since the engine blew up/tram hits
-#define DELAMINATION_COUNT_FILEPATH "data/rounds_since_delamination.txt"
-#define DELAMINATION_HIGHSCORE_FILEPATH "data/delamination_highscore.txt"
-#define TRAM_COUNT_FILEPATH "data/tram_hits_last_round.txt"
-
-/datum/controller/subsystem/persistence/proc/load_delamination_counter()
- if (!fexists(DELAMINATION_COUNT_FILEPATH))
- return
- rounds_since_engine_exploded = text2num(file2text(DELAMINATION_COUNT_FILEPATH))
- if (fexists(DELAMINATION_HIGHSCORE_FILEPATH))
- delam_highscore = text2num(file2text(DELAMINATION_HIGHSCORE_FILEPATH))
- for (var/obj/machinery/incident_display/sign as anything in GLOB.map_delamination_counters)
- sign.update_delam_count(rounds_since_engine_exploded, delam_highscore)
-
-/datum/controller/subsystem/persistence/proc/save_delamination_counter()
- rustg_file_write("[rounds_since_engine_exploded + 1]", DELAMINATION_COUNT_FILEPATH)
- if((rounds_since_engine_exploded + 1) > delam_highscore)
- rustg_file_write("[rounds_since_engine_exploded + 1]", DELAMINATION_HIGHSCORE_FILEPATH)
-
-/datum/controller/subsystem/persistence/proc/load_tram_counter()
- if(!fexists(TRAM_COUNT_FILEPATH))
- return
- tram_hits_last_round = text2num(file2text(TRAM_COUNT_FILEPATH))
-
-/datum/controller/subsystem/persistence/proc/save_tram_counter()
- rustg_file_write("[tram_hits_this_round]", TRAM_COUNT_FILEPATH)
-
-#undef DELAMINATION_COUNT_FILEPATH
-#undef DELAMINATION_HIGHSCORE_FILEPATH
-#undef TRAM_COUNT_FILEPATH
-#undef FILE_RECENT_MAPS
-#undef KEEP_ROUNDS_MAP
diff --git a/code/controllers/subsystem/persistence/_persistence.dm b/code/controllers/subsystem/persistence/_persistence.dm
new file mode 100644
index 0000000000000..586723c92e594
--- /dev/null
+++ b/code/controllers/subsystem/persistence/_persistence.dm
@@ -0,0 +1,105 @@
+#define FILE_RECENT_MAPS "data/RecentMaps.json"
+#define KEEP_ROUNDS_MAP 3
+
+SUBSYSTEM_DEF(persistence)
+ name = "Persistence"
+ init_order = INIT_ORDER_PERSISTENCE
+ flags = SS_NO_FIRE
+
+ ///instantiated wall engraving components
+ var/list/wall_engravings = list()
+ ///all saved persistent engravings loaded from JSON
+ var/list/saved_engravings = list()
+ ///tattoo stories that we're saving.
+ var/list/prison_tattoos_to_save = list()
+ ///tattoo stories that have been selected for this round.
+ var/list/prison_tattoos_to_use = list()
+ var/list/saved_messages = list()
+ var/list/saved_modes = list(1,2,3)
+ var/list/saved_maps = list()
+ var/list/blocked_maps = list()
+ var/list/saved_trophies = list()
+ var/list/picture_logging_information = list()
+ var/list/obj/structure/sign/picture_frame/photo_frames
+ var/list/obj/item/storage/photo_album/photo_albums
+ var/rounds_since_engine_exploded = 0
+ var/delam_highscore = 0
+ var/tram_hits_this_round = 0
+ var/tram_hits_last_round = 0
+
+/datum/controller/subsystem/persistence/Initialize()
+ load_poly()
+ load_wall_engravings()
+ load_prisoner_tattoos()
+ load_trophies()
+ load_recent_maps()
+ load_photo_persistence()
+ load_randomized_recipes()
+ load_custom_outfits()
+ load_delamination_counter()
+ load_tram_counter()
+ load_adventures()
+ return SS_INIT_SUCCESS
+
+///Collects all data to persist.
+/datum/controller/subsystem/persistence/proc/collect_data()
+ save_wall_engravings()
+ save_prisoner_tattoos()
+ collect_trophies()
+ collect_maps()
+ save_photo_persistence() //THIS IS PERSISTENCE, NOT THE LOGGING PORTION.
+ save_randomized_recipes()
+ save_scars()
+ save_custom_outfits()
+ save_delamination_counter()
+ if(SStransport.can_fire)
+ for(var/datum/transport_controller/linear/tram/transport as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
+ save_tram_history(transport.specific_transport_id)
+ save_tram_counter()
+
+///Loads up Poly's speech buffer.
+/datum/controller/subsystem/persistence/proc/load_poly()
+ for(var/mob/living/simple_animal/parrot/poly/P in GLOB.alive_mob_list)
+ twitterize(P.speech_buffer, "polytalk")
+ break //Who's been duping the bird?!
+
+/// Loads up the amount of times maps appeared to alter their appearance in voting and rotation.
+/datum/controller/subsystem/persistence/proc/load_recent_maps()
+ var/map_sav = FILE_RECENT_MAPS
+ if(!fexists(FILE_RECENT_MAPS))
+ return
+ var/list/json = json_decode(file2text(map_sav))
+ if(!json)
+ return
+ saved_maps = json["data"]
+
+ //Convert the mapping data to a shared blocking list, saves us doing this in several places later.
+ for(var/map in config.maplist)
+ var/datum/map_config/VM = config.maplist[map]
+ var/run = 0
+ if(VM.map_name == SSmapping.config.map_name)
+ run++
+ for(var/name in SSpersistence.saved_maps)
+ if(VM.map_name == name)
+ run++
+ if(run >= 2) //If run twice in the last KEEP_ROUNDS_MAP + 1 (including current) rounds, disable map for voting and rotation.
+ blocked_maps += VM.map_name
+
+///Updates the list of the most recent maps.
+/datum/controller/subsystem/persistence/proc/collect_maps()
+ if(length(saved_maps) > KEEP_ROUNDS_MAP) //Get rid of extras from old configs.
+ saved_maps.Cut(KEEP_ROUNDS_MAP+1)
+ var/mapstosave = min(length(saved_maps)+1, KEEP_ROUNDS_MAP)
+ if(length(saved_maps) < mapstosave) //Add extras if too short, one per round.
+ saved_maps += mapstosave
+ for(var/i = mapstosave; i > 1; i--)
+ saved_maps[i] = saved_maps[i-1]
+ saved_maps[1] = SSmapping.config.map_name
+ var/json_file = file(FILE_RECENT_MAPS)
+ var/list/file_data = list()
+ file_data["data"] = saved_maps
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
+
+#undef FILE_RECENT_MAPS
+#undef KEEP_ROUNDS_MAP
diff --git a/code/controllers/subsystem/persistence/counter_delamination.dm b/code/controllers/subsystem/persistence/counter_delamination.dm
new file mode 100644
index 0000000000000..81dca300c7e1e
--- /dev/null
+++ b/code/controllers/subsystem/persistence/counter_delamination.dm
@@ -0,0 +1,20 @@
+/// Location where we save the information about how many rounds it has been since the engine blew up
+#define DELAMINATION_COUNT_FILEPATH "data/rounds_since_delamination.txt"
+#define DELAMINATION_HIGHSCORE_FILEPATH "data/delamination_highscore.txt"
+
+/datum/controller/subsystem/persistence/proc/load_delamination_counter()
+ if(!fexists(DELAMINATION_COUNT_FILEPATH))
+ return
+ rounds_since_engine_exploded = text2num(file2text(DELAMINATION_COUNT_FILEPATH))
+ if(fexists(DELAMINATION_HIGHSCORE_FILEPATH))
+ delam_highscore = text2num(file2text(DELAMINATION_HIGHSCORE_FILEPATH))
+ for(var/obj/machinery/incident_display/sign as anything in GLOB.map_delamination_counters)
+ sign.update_delam_count(rounds_since_engine_exploded, delam_highscore)
+
+/datum/controller/subsystem/persistence/proc/save_delamination_counter()
+ rustg_file_write("[rounds_since_engine_exploded + 1]", DELAMINATION_COUNT_FILEPATH)
+ if((rounds_since_engine_exploded + 1) > delam_highscore)
+ rustg_file_write("[rounds_since_engine_exploded + 1]", DELAMINATION_HIGHSCORE_FILEPATH)
+
+#undef DELAMINATION_COUNT_FILEPATH
+#undef DELAMINATION_HIGHSCORE_FILEPATH
diff --git a/code/controllers/subsystem/persistence/counter_tram_hits.dm b/code/controllers/subsystem/persistence/counter_tram_hits.dm
new file mode 100644
index 0000000000000..806d5d5b5c2cc
--- /dev/null
+++ b/code/controllers/subsystem/persistence/counter_tram_hits.dm
@@ -0,0 +1,64 @@
+/// Location where we save the information about how many times the tram hit on previous round
+#define TRAM_COUNT_FILEPATH "data/tram_hits_last_round.txt"
+#define MAX_TRAM_SAVES 4
+
+// Loads historical tram data
+/datum/controller/subsystem/persistence/proc/load_tram_history(specific_transport_id)
+ var/list/raw_saved_trams = list()
+ var/json_file = file("data/tram_data/[specific_transport_id].json")
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+ raw_saved_trams = json["data"]
+
+ var/list/previous_tram_data = list()
+ for(var/raw_json in raw_saved_trams)
+ var/datum/tram_mfg_info/parsed_tram_data = new
+ parsed_tram_data.load_from_json(raw_json)
+ previous_tram_data += parsed_tram_data
+ return previous_tram_data
+
+// Saves historical tram data
+/datum/controller/subsystem/persistence/proc/save_tram_history(specific_transport_id)
+ var/list/packaged_tram_data = list()
+ for(var/datum/transport_controller/linear/tram/transport as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
+ if(transport.specific_transport_id == specific_transport_id)
+ packaged_tram_data = package_tram_data(transport)
+ break
+
+ var/json_file = file("data/tram_data/[specific_transport_id].json")
+ var/list/file_data = list()
+ var/list/converted_data = list()
+
+ for(var/datum/tram_mfg_info/data in packaged_tram_data)
+ converted_data += list(data.export_to_json())
+
+ file_data["data"] = converted_data
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
+
+/datum/controller/subsystem/persistence/proc/package_tram_data(datum/transport_controller/linear/tram/tram_controller)
+ var/list/packaged_data = list()
+ var/list/tram_list = tram_controller.tram_history
+ if(!isnull(tram_list))
+ while(tram_list.len > MAX_TRAM_SAVES)
+ tram_list.Cut(1,2)
+
+ for(var/datum/tram_mfg_info/data as anything in tram_list)
+ packaged_data += data
+
+ packaged_data += tram_controller.tram_registration
+ return packaged_data
+
+/datum/controller/subsystem/persistence/proc/load_tram_counter()
+ if(!fexists(TRAM_COUNT_FILEPATH))
+ return
+ tram_hits_last_round = text2num(file2text(TRAM_COUNT_FILEPATH))
+
+/datum/controller/subsystem/persistence/proc/save_tram_counter()
+ rustg_file_write("[tram_hits_this_round]", TRAM_COUNT_FILEPATH)
+
+#undef TRAM_COUNT_FILEPATH
+#undef MAX_TRAM_SAVES
diff --git a/code/controllers/subsystem/persistence/custom_outfits.dm b/code/controllers/subsystem/persistence/custom_outfits.dm
new file mode 100644
index 0000000000000..942f874fde5e5
--- /dev/null
+++ b/code/controllers/subsystem/persistence/custom_outfits.dm
@@ -0,0 +1,32 @@
+///Loads the custom outfits of every admin.
+/datum/controller/subsystem/persistence/proc/load_custom_outfits()
+ var/file = file("data/custom_outfits.json")
+ if(!fexists(file))
+ return
+ var/outfits_json = file2text(file)
+ var/list/outfits = json_decode(outfits_json)
+ if(!islist(outfits))
+ return
+
+ for(var/outfit_data in outfits)
+ if(!islist(outfit_data))
+ continue
+
+ var/outfittype = text2path(outfit_data["outfit_type"])
+ if(!ispath(outfittype, /datum/outfit))
+ continue
+ var/datum/outfit/outfit = new outfittype
+ if(!outfit.load_from(outfit_data))
+ continue
+ GLOB.custom_outfits += outfit
+
+///Saves each admin's custom outfit list
+/datum/controller/subsystem/persistence/proc/save_custom_outfits()
+ var/file = file("data/custom_outfits.json")
+ fdel(file)
+
+ var/list/data = list()
+ for(var/datum/outfit/outfit in GLOB.custom_outfits)
+ data += list(outfit.get_json_data())
+
+ WRITE_FILE(file, json_encode(data))
diff --git a/code/controllers/subsystem/persistence/engravings.dm b/code/controllers/subsystem/persistence/engravings.dm
new file mode 100644
index 0000000000000..f47fc7fbba124
--- /dev/null
+++ b/code/controllers/subsystem/persistence/engravings.dm
@@ -0,0 +1,84 @@
+///Loads all engravings, and places a select amount in maintenance and the prison.
+/datum/controller/subsystem/persistence/proc/load_wall_engravings()
+ var/json_file = file(ENGRAVING_SAVE_FILE)
+ if(!fexists(json_file))
+ return
+
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+
+ if(json["version"] < ENGRAVING_PERSISTENCE_VERSION)
+ update_wall_engravings(json)
+
+ saved_engravings = json["entries"]
+
+ if(!saved_engravings.len)
+ log_world("Failed to load engraved messages on map [SSmapping.config.map_name]")
+ return
+
+ var/list/viable_turfs = get_area_turfs(/area/station/maintenance, subtypes = TRUE) + get_area_turfs(/area/station/security/prison, subtypes = TRUE)
+ var/list/turfs_to_pick_from = list()
+
+ for(var/turf/T as anything in viable_turfs)
+ if(!isclosedturf(T))
+ continue
+ turfs_to_pick_from += T
+
+ var/successfully_loaded_engravings = 0
+
+ for(var/iteration in 1 to rand(MIN_PERSISTENT_ENGRAVINGS, MAX_PERSISTENT_ENGRAVINGS))
+ var/engraving = pick_n_take(saved_engravings)
+ if(!islist(engraving))
+ stack_trace("something's wrong with the engraving data! one of the saved engravings wasn't a list!")
+ continue
+
+ var/turf/closed/engraved_wall = pick(turfs_to_pick_from)
+
+ if(HAS_TRAIT(engraved_wall, TRAIT_NOT_ENGRAVABLE))
+ continue
+
+ engraved_wall.AddComponent(/datum/component/engraved, engraving["story"], FALSE, engraving["story_value"])
+ successfully_loaded_engravings++
+ turfs_to_pick_from -= engraved_wall
+
+ log_world("Loaded [successfully_loaded_engravings] engraved messages on map [SSmapping.config.map_name]")
+
+///Saves all new engravings in the world.
+/datum/controller/subsystem/persistence/proc/save_wall_engravings()
+ var/list/saved_data = list()
+
+ saved_data["version"] = ENGRAVING_PERSISTENCE_VERSION
+ saved_data["entries"] = list()
+
+
+ var/json_file = file(ENGRAVING_SAVE_FILE)
+ if(fexists(json_file))
+ var/list/old_json = json_decode(file2text(json_file))
+ if(old_json)
+ saved_data["entries"] = old_json["entries"]
+
+ for(var/datum/component/engraved/engraving in wall_engravings)
+ if(!engraving.persistent_save)
+ continue
+ var/area/engraved_area = get_area(engraving.parent)
+ if(!(engraved_area.area_flags & PERSISTENT_ENGRAVINGS))
+ continue
+ saved_data["entries"] += engraving.save_persistent()
+
+ fdel(json_file)
+
+ WRITE_FILE(json_file, json_encode(saved_data))
+
+///This proc can update entries if the format has changed at some point.
+/datum/controller/subsystem/persistence/proc/update_wall_engravings(json)
+ for(var/engraving_entry in json["entries"])
+ continue //no versioning yet
+
+ //Save it to the file
+ var/json_file = file(ENGRAVING_SAVE_FILE)
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(json))
+
+ return json
+
diff --git a/code/controllers/subsystem/persistence/photo_albums.dm b/code/controllers/subsystem/persistence/photo_albums.dm
new file mode 100644
index 0000000000000..3aee856b2c21c
--- /dev/null
+++ b/code/controllers/subsystem/persistence/photo_albums.dm
@@ -0,0 +1,86 @@
+///Loads up the photo album source file.
+/datum/controller/subsystem/persistence/proc/get_photo_albums()
+ var/album_path = file("data/photo_albums.json")
+ if(fexists(album_path))
+ return json_decode(file2text(album_path))
+
+///Loads up the photo frames source file.
+/datum/controller/subsystem/persistence/proc/get_photo_frames()
+ var/frame_path = file("data/photo_frames.json")
+ if(fexists(frame_path))
+ return json_decode(file2text(frame_path))
+
+/// Removes the identifier of a persistent photo frame from the json.
+/datum/controller/subsystem/persistence/proc/remove_photo_frames(identifier)
+ var/frame_path = file("data/photo_frames.json")
+ if(!fexists(frame_path))
+ return
+
+ var/frame_json = json_decode(file2text(frame_path))
+ frame_json -= identifier
+
+ frame_json = json_encode(frame_json)
+ fdel(frame_path)
+ WRITE_FILE(frame_path, frame_json)
+
+///Loads photo albums, and populates them; also loads and applies frames to picture frames.
+/datum/controller/subsystem/persistence/proc/load_photo_persistence()
+ var/album_path = file("data/photo_albums.json")
+ var/frame_path = file("data/photo_frames.json")
+ if(fexists(album_path))
+ var/list/json = json_decode(file2text(album_path))
+ if(json.len)
+ for(var/i in photo_albums)
+ var/obj/item/storage/photo_album/A = i
+ if(!A.persistence_id)
+ continue
+ if(json[A.persistence_id])
+ A.populate_from_id_list(json[A.persistence_id])
+
+ if(fexists(frame_path))
+ var/list/json = json_decode(file2text(frame_path))
+ if(json.len)
+ for(var/i in photo_frames)
+ var/obj/structure/sign/picture_frame/PF = i
+ if(!PF.persistence_id)
+ continue
+ if(json[PF.persistence_id])
+ PF.load_from_id(json[PF.persistence_id])
+
+///Saves the contents of photo albums and the picture frames.
+/datum/controller/subsystem/persistence/proc/save_photo_persistence()
+ var/album_path = file("data/photo_albums.json")
+ var/frame_path = file("data/photo_frames.json")
+
+ var/list/frame_json = list()
+ var/list/album_json = list()
+
+ if(fexists(album_path))
+ album_json = json_decode(file2text(album_path))
+ fdel(album_path)
+
+ for(var/i in photo_albums)
+ var/obj/item/storage/photo_album/A = i
+ if(!istype(A) || !A.persistence_id)
+ continue
+ var/list/L = A.get_picture_id_list()
+ album_json[A.persistence_id] = L
+
+ album_json = json_encode(album_json)
+
+ WRITE_FILE(album_path, album_json)
+
+ if(fexists(frame_path))
+ frame_json = json_decode(file2text(frame_path))
+ fdel(frame_path)
+
+ for(var/i in photo_frames)
+ var/obj/structure/sign/picture_frame/F = i
+ if(!istype(F) || !F.persistence_id)
+ continue
+ frame_json[F.persistence_id] = F.get_photo_id()
+
+ frame_json = json_encode(frame_json)
+
+ WRITE_FILE(frame_path, frame_json)
+
diff --git a/code/controllers/subsystem/persistence/recipes.dm b/code/controllers/subsystem/persistence/recipes.dm
new file mode 100644
index 0000000000000..84145b26aabc3
--- /dev/null
+++ b/code/controllers/subsystem/persistence/recipes.dm
@@ -0,0 +1,39 @@
+///Loads all randomized recipes.
+/datum/controller/subsystem/persistence/proc/load_randomized_recipes()
+ var/json_file = file("data/RandomizedChemRecipes.json")
+ var/json
+ if(fexists(json_file))
+ json = json_decode(file2text(json_file))
+
+ for(var/randomized_type in subtypesof(/datum/chemical_reaction/randomized))
+ var/datum/chemical_reaction/randomized/R = new randomized_type
+ var/loaded = FALSE
+ if(R.persistent && json)
+ var/list/recipe_data = json["[R.type]"]
+ if(recipe_data)
+ if(R.LoadOldRecipe(recipe_data) && (daysSince(R.created) <= R.persistence_period))
+ loaded = TRUE
+ if(!loaded) //We do not have information for whatever reason, just generate new one
+ if(R.persistent)
+ log_game("Resetting persistent [randomized_type] random recipe.")
+ R.GenerateRecipe()
+
+ if(!R.HasConflicts()) //Might want to try again if conflicts happened in the future.
+ add_chemical_reaction(R)
+ else
+ log_game("Randomized recipe [randomized_type] resulted in conflicting recipes.")
+
+///Saves all randomized recipes.
+/datum/controller/subsystem/persistence/proc/save_randomized_recipes()
+ var/json_file = file("data/RandomizedChemRecipes.json")
+ var/list/file_data = list()
+
+ //asert globchems done
+ for(var/randomized_type in subtypesof(/datum/chemical_reaction/randomized))
+ var/datum/chemical_reaction/randomized/R = get_chemical_reaction(randomized_type) //ew, would be nice to add some simple tracking
+ if(R?.persistent)
+ var/list/recipe_data = R.SaveOldRecipe()
+ file_data["[R.type]"] = recipe_data
+
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
diff --git a/code/controllers/subsystem/persistence/scars.dm b/code/controllers/subsystem/persistence/scars.dm
new file mode 100644
index 0000000000000..fa378f24dd437
--- /dev/null
+++ b/code/controllers/subsystem/persistence/scars.dm
@@ -0,0 +1,17 @@
+///Saves all scars for everyone's original characters
+/datum/controller/subsystem/persistence/proc/save_scars()
+ for(var/i in GLOB.joined_player_list)
+ var/mob/living/carbon/human/ending_human = get_mob_by_ckey(i)
+ if(!istype(ending_human) || !ending_human.mind?.original_character_slot_index || !ending_human.client?.prefs.read_preference(/datum/preference/toggle/persistent_scars))
+ continue
+
+ var/mob/living/carbon/human/original_human = ending_human.mind.original_character.resolve()
+
+ if(!original_human)
+ continue
+
+ if(original_human.stat == DEAD || !original_human.all_scars || original_human != ending_human)
+ original_human.save_persistent_scars(TRUE)
+ else
+ original_human.save_persistent_scars()
+
diff --git a/code/controllers/subsystem/persistence/tattoos.dm b/code/controllers/subsystem/persistence/tattoos.dm
new file mode 100644
index 0000000000000..45dd31c7f5c94
--- /dev/null
+++ b/code/controllers/subsystem/persistence/tattoos.dm
@@ -0,0 +1,55 @@
+///Loads all tattoos, and select a few based on the amount of prisoner spawn positions.
+/datum/controller/subsystem/persistence/proc/load_prisoner_tattoos()
+ var/json_file = file(PRISONER_TATTOO_SAVE_FILE)
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+
+ if(json["version"] < TATTOO_PERSISTENCE_VERSION)
+ update_prisoner_tattoos(json)
+
+ var/datum/job/prisoner_datum = SSjob.name_occupations[JOB_PRISONER]
+ if(!prisoner_datum)
+ return
+ var/iterations_allowed = prisoner_datum.spawn_positions
+
+ var/list/entries = json["entries"]
+ if(entries.len)
+ for(var/index in 1 to iterations_allowed)
+ prison_tattoos_to_use += list(entries[rand(1, entries.len)])
+
+ log_world("Loaded [prison_tattoos_to_use.len] prison tattoos")
+
+///Saves all tattoos, so they can appear on prisoners in future rounds
+/datum/controller/subsystem/persistence/proc/save_prisoner_tattoos()
+ var/json_file = file(PRISONER_TATTOO_SAVE_FILE)
+ var/list/saved_data = list()
+ var/list/entries = list()
+
+ if(fexists(json_file))
+ var/list/old_json = json_decode(file2text(json_file))
+ if(old_json)
+ entries += old_json["entries"] //Save the old if its there
+
+ entries += prison_tattoos_to_save
+
+ saved_data["version"] = ENGRAVING_PERSISTENCE_VERSION
+ saved_data["entries"] = entries
+
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(saved_data))
+
+///This proc can update entries if the format has changed at some point.
+/datum/controller/subsystem/persistence/proc/update_prisoner_tattoos(json)
+ for(var/tattoo_entry in json["entries"])
+ continue //no versioning yet
+
+ //Save it to the file
+ var/json_file = file(PRISONER_TATTOO_SAVE_FILE)
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(json))
+
+ return json
+
diff --git a/code/controllers/subsystem/persistence/trophies.dm b/code/controllers/subsystem/persistence/trophies.dm
new file mode 100644
index 0000000000000..515ed38608501
--- /dev/null
+++ b/code/controllers/subsystem/persistence/trophies.dm
@@ -0,0 +1,125 @@
+///trophy data datum, for admin manipulation
+/datum/trophy_data
+ ///path of the item the trophy will try to mimic, null if path_string is invalid
+ var/path
+ ///the message that appears under the item
+ var/message
+ ///the key of the one who placed the item in the trophy case
+ var/placer_key
+
+/// Loads the trophies from the source file, and places a few in trophy display cases.
+/datum/controller/subsystem/persistence/proc/load_trophies()
+ var/list/raw_saved_trophies = list()
+ if(fexists("data/npc_saves/TrophyItems.json"))
+ var/json_file = file("data/npc_saves/TrophyItems.json")
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+ raw_saved_trophies = json["data"]
+ fdel("data/npc_saves/TrophyItems.json")
+ else
+ var/json_file = file("data/trophy_items.json")
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+ raw_saved_trophies = json["data"]
+
+ for(var/raw_json in raw_saved_trophies)
+ var/datum/trophy_data/parsed_trophy_data = new
+ parsed_trophy_data.load_from_json(raw_json)
+ saved_trophies += parsed_trophy_data
+
+ set_up_trophies()
+
+/datum/trophy_data/proc/load_from_json(list/json_data)
+ path = json_data["path"]
+ message = json_data["message"]
+ placer_key = json_data["placer_key"]
+
+/datum/trophy_data/proc/to_json()
+ var/list/new_data = list()
+ new_data["path"] = path
+ new_data["message"] = message
+ new_data["placer_key"] = placer_key
+ new_data["is_valid"] = text2path(path) ? TRUE : FALSE
+ return new_data
+
+/// Returns a list for the admin trophy panel.
+/datum/controller/subsystem/persistence/proc/trophy_ui_data()
+ var/list/ui_data = list()
+ for(var/datum/trophy_data/data in saved_trophies)
+ var/list/pdata = data.to_json()
+ pdata["ref"] = REF(data)
+ ui_data += list(pdata)
+
+ return ui_data
+
+
+/// Puts trophies into trophy cases.
+/datum/controller/subsystem/persistence/proc/set_up_trophies()
+
+ var/list/valid_trophies = list()
+
+ for(var/datum/trophy_data/data in saved_trophies)
+
+ if(!data) //sanity for incorrect deserialization
+ continue
+
+ var/path = text2path(data.path)
+ if(!path) //If the item no longer exist, ignore it
+ continue
+
+ valid_trophies += data
+
+ for(var/obj/structure/displaycase/trophy/trophy_case in GLOB.trophy_cases)
+ if(!valid_trophies.len)
+ break
+
+ if(trophy_case.showpiece)
+ continue
+
+ trophy_case.set_up_trophy(pick_n_take(valid_trophies))
+
+///Collects trophies from all existing trophy cases.
+/datum/controller/subsystem/persistence/proc/collect_trophies()
+ for(var/trophy_case in GLOB.trophy_cases)
+ save_trophy(trophy_case)
+
+ var/json_file = file("data/trophy_items.json")
+ var/list/file_data = list()
+ var/list/converted_data = list()
+
+ for(var/datum/trophy_data/data in saved_trophies)
+ converted_data += list(data.to_json())
+
+ converted_data = remove_duplicate_trophies(converted_data)
+
+ file_data["data"] = converted_data
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
+
+///gets the list of json trophies, and deletes the ones with an identical path and message
+/datum/controller/subsystem/persistence/proc/remove_duplicate_trophies(list/trophies)
+ var/list/ukeys = list()
+ . = list()
+ for(var/trophy in trophies)
+ var/tkey = "[trophy["path"]]-[trophy["message"]]"
+ if(ukeys[tkey])
+ continue
+ else
+ . += list(trophy)
+ ukeys[tkey] = TRUE
+
+///If there is a trophy in the trophy case, saved it, if the trophy was not a holo trophy and has a message attached.
+/datum/controller/subsystem/persistence/proc/save_trophy(obj/structure/displaycase/trophy/trophy_case)
+ if(!trophy_case.holographic_showpiece && trophy_case.showpiece && trophy_case.trophy_message)
+ var/datum/trophy_data/data = new
+ data.path = trophy_case.showpiece.type
+ data.message = trophy_case.trophy_message
+ data.placer_key = trophy_case.placer_key
+ saved_trophies += data
+
diff --git a/code/controllers/subsystem/persistent_paintings.dm b/code/controllers/subsystem/persistent_paintings.dm
index 4eb77bd21be8c..35656e2a56d4d 100644
--- a/code/controllers/subsystem/persistent_paintings.dm
+++ b/code/controllers/subsystem/persistent_paintings.dm
@@ -113,6 +113,11 @@ SUBSYSTEM_DEF(persistent_paintings)
/// A list of /datum/paintings saved or ready to be saved this round.
var/list/paintings = list()
+ /// A list of paintings' data for paintings that are currently stored in the library.
+ var/list/cached_painting_data = list()
+ /// A list of paintings' data for paintings that are currently stored in the library, with admin metadata
+ var/list/admin_painting_data = list()
+
///The list of available frame reskins (they are purely cosmetic) and the associated patronage amount required for them.
var/list/frame_types_by_patronage_tier = list(
"simple" = 0,
@@ -127,8 +132,8 @@ SUBSYSTEM_DEF(persistent_paintings)
"gold" = PATRONAGE_EXCELLENT_FRAME,
"diamond" = PATRONAGE_AMAZING_FRAME,
"rainbow" = PATRONAGE_SUPERB_FRAME,
- "supermatter" = PATRONAGE_LEGENDARY_FRAME
- )
+ "supermatter" = PATRONAGE_LEGENDARY_FRAME,
+ )
/datum/controller/subsystem/persistent_paintings/Initialize()
var/json_file = file("data/paintings.json")
@@ -142,8 +147,29 @@ SUBSYSTEM_DEF(persistent_paintings)
for(var/obj/structure/sign/painting/painting_frame as anything in painting_frames)
painting_frame.load_persistent()
+ cache_paintings()
+
return SS_INIT_SUCCESS
+/datum/controller/subsystem/persistent_paintings/proc/cache_paintings()
+ cached_painting_data = list()
+ admin_painting_data = list()
+
+ for(var/datum/painting/painting as anything in paintings)
+ cached_painting_data += list(list(
+ "title" = painting.title,
+ "creator" = painting.creator_name,
+ "md5" = painting.md5,
+ "ref" = REF(painting),
+ "width" = painting.width,
+ "height" = painting.height,
+ "ratio" = painting.width/painting.height,
+ ))
+
+ var/list/pdata = painting.to_json()
+ pdata["ref"] = REF(painting)
+ admin_painting_data += pdata
+
/**
* Generates painting data ready to be consumed by ui.
* Args:
@@ -152,31 +178,27 @@ SUBSYSTEM_DEF(persistent_paintings)
* * search_text : text to search for if the PAINTINGS_FILTER_SEARCH_TITLE or PAINTINGS_FILTER_SEARCH_CREATOR filters are enabled.
*/
/datum/controller/subsystem/persistent_paintings/proc/painting_ui_data(filter=NONE, admin=FALSE, search_text)
- . = list()
var/searching = filter & (PAINTINGS_FILTER_SEARCH_TITLE|PAINTINGS_FILTER_SEARCH_CREATOR) && search_text
- for(var/datum/painting/painting as anything in paintings)
- if(filter & PAINTINGS_FILTER_AI_PORTRAIT && ((painting.width != 24 && painting.width != 23) || (painting.height != 24 && painting.height != 23)))
+
+ if(!searching)
+ return admin ? admin_painting_data : cached_painting_data
+
+ var/list/filtered_paintings = list()
+ var/list/searched_paintings = admin ? admin_painting_data : cached_painting_data
+
+ for(var/painting as anything in searched_paintings)
+ if(filter & PAINTINGS_FILTER_AI_PORTRAIT && ((painting["width"] != 24 && painting["width"] != 23) || (painting["height"] != 24 && painting["height"] != 23)))
continue
if(searching)
var/haystack_text = ""
if(filter & PAINTINGS_FILTER_SEARCH_TITLE)
- haystack_text = painting.title
+ haystack_text = painting["title"]
else if(filter & PAINTINGS_FILTER_SEARCH_CREATOR)
- haystack_text = painting.creator_name
+ haystack_text = painting["creator"]
if(!findtext(haystack_text, search_text))
continue
- if(admin)
- var/list/pdata = painting.to_json()
- pdata["ref"] = REF(painting)
- . += list(pdata)
- else
- . += list(list(
- "title" = painting.title,
- "creator" = painting.creator_name,
- "md5" = painting.md5,
- "ref" = REF(painting),
- "ratio" = painting.width/painting.height,
- ))
+ filtered_paintings += painting
+ return filtered_paintings
/// Returns paintings with given tag.
/datum/controller/subsystem/persistent_paintings/proc/get_paintings_with_tag(tag_name)
@@ -309,6 +331,7 @@ SUBSYSTEM_DEF(persistent_paintings)
var/payload = json_encode(all_data)
fdel(json_file)
WRITE_FILE(json_file, payload)
+ cache_paintings()
#undef PAINTINGS_DATA_FORMAT_VERSION
#undef PATRONAGE_OK_FRAME
diff --git a/code/controllers/subsystem/processing/quirks.dm b/code/controllers/subsystem/processing/quirks.dm
index e27d920c23b3c..59ef8e952b2ba 100644
--- a/code/controllers/subsystem/processing/quirks.dm
+++ b/code/controllers/subsystem/processing/quirks.dm
@@ -20,7 +20,7 @@ GLOBAL_LIST_INIT_TYPED(quirk_blacklist, /list/datum/quirk, list(
list(/datum/quirk/mute, /datum/quirk/softspoken),
list(/datum/quirk/poor_aim, /datum/quirk/bighands),
list(/datum/quirk/bilingual, /datum/quirk/foreigner),
- list(/datum/quirk/spacer_born, /datum/quirk/paraplegic, /datum/quirk/item_quirk/settler),
+ list(/datum/quirk/spacer_born, /datum/quirk/item_quirk/settler),
list(/datum/quirk/photophobia, /datum/quirk/nyctophobia),
list(/datum/quirk/item_quirk/settler, /datum/quirk/freerunning),
list(/datum/quirk/numb, /datum/quirk/selfaware),
diff --git a/code/controllers/subsystem/processing/tramprocess.dm b/code/controllers/subsystem/processing/tramprocess.dm
deleted file mode 100644
index b497cce8b8caf..0000000000000
--- a/code/controllers/subsystem/processing/tramprocess.dm
+++ /dev/null
@@ -1,15 +0,0 @@
-PROCESSING_SUBSYSTEM_DEF(tramprocess)
- name = "Tram Process"
- wait = 0.5
- /// only used on maps with trams, so only enabled by such.
- can_fire = FALSE
-
- ///how much time a tram can take per movement before we notify admins and slow down the tram. in milliseconds
- var/max_time = 15
-
- ///how many times the tram can move costing over max_time milliseconds before it gets slowed down
- var/max_exceeding_moves = 5
-
- ///how many times the tram can move costing less than half max_time milliseconds before we speed it back up again.
- ///is only used if the tram has been slowed down for exceeding max_time
- var/max_cheap_moves = 5
diff --git a/code/controllers/subsystem/queuelinks.dm b/code/controllers/subsystem/queuelinks.dm
new file mode 100644
index 0000000000000..6a3b828882162
--- /dev/null
+++ b/code/controllers/subsystem/queuelinks.dm
@@ -0,0 +1,68 @@
+/atom/proc/MatchedLinks(id, list/partners)
+
+SUBSYSTEM_DEF(queuelinks)
+ name = "Queue Links"
+ flags = SS_NO_FIRE
+ init_order = INIT_ORDER_QUEUELINKS
+ ///assoc list of pending queues, id = /datum/queue_link
+ var/list/queues = list()
+
+/datum/controller/subsystem/queuelinks/Initialize()
+ return SS_INIT_SUCCESS
+
+///Creates or adds to a queue with the id supplied, if the queue is now or above the size of the queue, calls MatchedLinks and clears queue.
+/// queues with a size of 0 wait never pop until something is added with an actual queue_max
+/datum/controller/subsystem/queuelinks/proc/add_to_queue(atom/what, id, queue_max = 0)
+ if(!isatom(what))
+ CRASH("Attempted to add a non-atom to queue; [what]!")
+ if(isnull(id))
+ CRASH("Attempted to add to queue with no ID; [what]")
+
+ var/datum/queue_link/link
+ if(isnull(queues[id]))
+ link = new /datum/queue_link(id)
+ queues[id] = link
+ else
+ link = queues[id]
+
+ if(link.add(what, queue_max))
+ queues -= id
+
+/datum/queue_link
+ /// atoms in our queue
+ var/list/partners = list()
+ /// how much length until we pop, only incrementable, 0 means the queue will not pop until a maximum is set
+ var/queue_max = 0
+ /// id
+ var/id
+
+/datum/queue_link/New(new_id)
+ id = new_id
+ return ..()
+
+///adds an atom to the queue, if we are popping this returns TRUE
+/datum/queue_link/proc/add(atom/what, max = 0)
+ . = FALSE
+ if(what in partners)
+ return
+ partners += what
+
+ if(queue_max != 0 && max != 0 && max != queue_max)
+ CRASH("Tried to change queue size to [max] from [queue_max]!")
+ else if(!queue_max)
+ queue_max = max
+
+ if(!queue_max || length(partners) < queue_max)
+ return
+
+ pop()
+ return TRUE
+
+/datum/queue_link/proc/pop()
+ for(var/atom/item as anything in partners)
+ item.MatchedLinks(id, partners)
+ qdel(src)
+
+/datum/queue_link/Destroy()
+ . = ..()
+ partners = null
diff --git a/code/controllers/subsystem/security_level.dm b/code/controllers/subsystem/security_level.dm
index d4fb13d99889f..df5ea642ce370 100644
--- a/code/controllers/subsystem/security_level.dm
+++ b/code/controllers/subsystem/security_level.dm
@@ -39,7 +39,10 @@ SUBSYSTEM_DEF(security_level)
if(!selected_level)
CRASH("set_level was called with an invalid security level([new_level])")
- announce_security_level(selected_level) // We want to announce BEFORE updating to the new level
+ if(SSnightshift.can_fire && (selected_level.number_level >= SEC_LEVEL_RED || current_security_level.number_level >= SEC_LEVEL_RED))
+ SSnightshift.next_fire = world.time + 7 SECONDS // Fire nightshift after the security level announcement is complete
+
+ level_announce(selected_level, current_security_level.number_level) // We want to announce BEFORE updating to the new level
SSsecurity_level.current_security_level = selected_level
@@ -53,21 +56,8 @@ SUBSYSTEM_DEF(security_level)
SSshuttle.emergency.alert_coeff_change(selected_level.shuttle_call_time_mod)
SEND_SIGNAL(src, COMSIG_SECURITY_LEVEL_CHANGED, selected_level.number_level)
- SSnightshift.check_nightshift()
SSblackbox.record_feedback("tally", "security_level_changes", 1, selected_level.name)
-/**
- * Handles announcements of the newly set security level
- *
- * Arguments:
- * * selected_level - The new security level that has been set
- */
-/datum/controller/subsystem/security_level/proc/announce_security_level(datum/security_level/selected_level)
- if(selected_level.number_level > current_security_level.number_level) // We are elevating to this level.
- minor_announce(selected_level.elevating_to_announcemnt, "Attention! Security level elevated to [selected_level.name]:", sound_override = selected_level.sound)
- else // Going down
- minor_announce(selected_level.lowering_to_announcement, "Attention! Security level lowered to [selected_level.name]:", sound_override = selected_level.sound)
-
/**
* Returns the current security level as a number
*/
diff --git a/code/controllers/subsystem/shuttle.dm b/code/controllers/subsystem/shuttle.dm
index aac1ed16660c8..7665a51f30c06 100644
--- a/code/controllers/subsystem/shuttle.dm
+++ b/code/controllers/subsystem/shuttle.dm
@@ -261,13 +261,27 @@ SUBSYSTEM_DEF(shuttle)
message_admins(msg)
log_shuttle("[msg] Alive: [alive], Roundstart: [total], Threshold: [threshold]")
emergency_no_recall = TRUE
- priority_announce("Catastrophic casualties detected: crisis shuttle protocols activated - jamming recall signals across all frequencies.")
+ priority_announce(
+ text = "Catastrophic casualties detected: crisis shuttle protocols activated - jamming recall signals across all frequencies.",
+ title = "Emergency Shuttle Dispatched",
+ sound = ANNOUNCER_SHUTTLECALLED,
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "orange",
+ )
if(emergency.timeLeft(1) > emergency_call_time * ALERT_COEFF_AUTOEVAC_CRITICAL)
emergency.request(null, set_coefficient = ALERT_COEFF_AUTOEVAC_CRITICAL)
/datum/controller/subsystem/shuttle/proc/block_recall(lockout_timer)
+ if(isnull(lockout_timer))
+ CRASH("Emergency shuttle block was called, but missing a value for the lockout duration")
if(admin_emergency_no_recall)
- priority_announce("Error!", "Emergency Shuttle Uplink Alert", 'sound/misc/announce_dig.ogg')
+ priority_announce(
+ text = "Emergency shuttle uplink interference detected, shuttle call disabled while the system reinitializes. Estimated restore in [DisplayTimeText(lockout_timer, round_seconds_to = 60)].",
+ title = "Uplink Interference",
+ sound = 'sound/misc/announce_dig.ogg',
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "grey",
+ )
addtimer(CALLBACK(src, PROC_REF(unblock_recall)), lockout_timer)
return
emergency_no_recall = TRUE
@@ -275,7 +289,13 @@ SUBSYSTEM_DEF(shuttle)
/datum/controller/subsystem/shuttle/proc/unblock_recall()
if(admin_emergency_no_recall)
- priority_announce("Error!", "Emergency Shuttle Uplink Alert", 'sound/misc/announce_dig.ogg')
+ priority_announce(
+ text= "Emergency shuttle uplink services are now back online.",
+ title = "Uplink Restored",
+ sound = 'sound/misc/announce_dig.ogg',
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "green",
+ )
return
emergency_no_recall = FALSE
@@ -366,7 +386,7 @@ SUBSYSTEM_DEF(shuttle)
call_reason = trim(html_encode(call_reason))
- var/emergency_reason = "\nNature of emergency:\n\n[call_reason]"
+ var/emergency_reason = "\n\nNature of emergency:\n[call_reason]"
emergency.request(
signal_origin = signal_origin,
@@ -503,15 +523,23 @@ SUBSYSTEM_DEF(shuttle)
emergency.mode = SHUTTLE_STRANDED
emergency.timer = null
emergency.sound_played = FALSE
- priority_announce("Hostile environment detected. \
- Departure has been postponed indefinitely pending \
- conflict resolution.", null, 'sound/misc/notice1.ogg', "Priority")
+ priority_announce(
+ text = "Departure has been postponed indefinitely pending conflict resolution.",
+ title = "Hostile Environment Detected",
+ sound = 'sound/misc/notice1.ogg',
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "grey",
+ )
if(!emergency_no_escape && (emergency.mode == SHUTTLE_STRANDED))
emergency.mode = SHUTTLE_DOCKED
emergency.setTimer(emergency_dock_time)
- priority_announce("Hostile environment resolved. \
- You have 3 minutes to board the Emergency Shuttle.",
- null, ANNOUNCER_SHUTTLEDOCK, "Priority")
+ priority_announce(
+ text = "You have [DisplayTimeText(emergency_dock_time)] to board the emergency shuttle.",
+ title = "Hostile Environment Resolved",
+ sound = 'sound/misc/announce_dig.ogg',
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "green",
+ )
//try to move/request to dock_home if possible, otherwise dock_away. Mainly used for admin buttons
/datum/controller/subsystem/shuttle/proc/toggleShuttle(shuttle_id, dock_home, dock_away, timed)
diff --git a/code/controllers/subsystem/sounds.dm b/code/controllers/subsystem/sounds.dm
index 9067c077da7cb..2bc253c5af744 100644
--- a/code/controllers/subsystem/sounds.dm
+++ b/code/controllers/subsystem/sounds.dm
@@ -23,8 +23,12 @@ SUBSYSTEM_DEF(sounds)
/// higher reserve position - decremented and incremented to reserve sound channels, anything above this is reserved. The channel at this index is the highest unreserved channel.
var/channel_reserve_high
+ /// All valid sound files in the sound directory
+ var/list/all_sounds
+
/datum/controller/subsystem/sounds/Initialize()
setup_available_channels()
+ find_all_available_sounds()
return SS_INIT_SUCCESS
/datum/controller/subsystem/sounds/proc/setup_available_channels()
@@ -37,6 +41,26 @@ SUBSYSTEM_DEF(sounds)
channel_random_low = 1
channel_reserve_high = length(channel_list)
+/datum/controller/subsystem/sounds/proc/find_all_available_sounds()
+ all_sounds = list()
+ // Put more common extensions first to speed this up a bit
+ var/static/list/valid_file_extensions = list(
+ ".ogg",
+ ".wav",
+ ".mid",
+ ".midi",
+ ".mod",
+ ".it",
+ ".s3m",
+ ".xm",
+ ".oxm",
+ ".raw",
+ ".wma",
+ ".aiff",
+ )
+
+ all_sounds = pathwalk("sound/", valid_file_extensions)
+
/// Removes a channel from using list.
/datum/controller/subsystem/sounds/proc/free_sound_channel(channel)
var/text_channel = num2text(channel)
@@ -78,7 +102,7 @@ SUBSYSTEM_DEF(sounds)
CRASH("Attempted to reserve sound channel without datum using the managed proc.")
.= reserve_channel()
if(!.)
- return FALSE
+ CRASH("No more sound channels can be reserved")
var/text_channel = num2text(.)
using_channels[text_channel] = D
LAZYINITLIST(using_channels_by_datum[D])
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index 252e69595f35f..b3c449a6bb914 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -285,7 +285,7 @@ SUBSYSTEM_DEF(ticker)
to_chat(world, span_notice("and..."))
for(var/holidayname in GLOB.holidays)
var/datum/holiday/holiday = GLOB.holidays[holidayname]
- to_chat(world, "
[holiday.greet()]
")
+ to_chat(world, span_info(holiday.greet()))
PostSetup()
diff --git a/code/controllers/subsystem/transport.dm b/code/controllers/subsystem/transport.dm
new file mode 100644
index 0000000000000..db8d19fa060a4
--- /dev/null
+++ b/code/controllers/subsystem/transport.dm
@@ -0,0 +1,236 @@
+PROCESSING_SUBSYSTEM_DEF(transport)
+ name = "Transport"
+ wait = 0.05 SECONDS
+ /// only used on maps with trams, so only enabled by such.
+ can_fire = FALSE
+
+ ///associative list of the form: list(lift_id = list(all transport_controller datums attached to lifts of that type))
+ var/list/transports_by_type = list()
+ var/list/nav_beacons = list()
+ var/list/crossing_signals = list()
+ var/list/sensors = list()
+ var/list/doors = list()
+ var/list/displays = list()
+ ///how much time a tram can take per movement before we notify admins and slow down the tram. in milliseconds
+ var/max_time = 15
+ ///how many times the tram can move costing over max_time milliseconds before it gets slowed down
+ var/max_exceeding_moves = 5
+ ///how many times the tram can move costing less than half max_time milliseconds before we speed it back up again.
+ ///is only used if the tram has been slowed down for exceeding max_time
+ var/max_cheap_moves = 5
+
+/**
+ * Registers the subsystem to listen for incoming requests from paired devices
+ *
+ * When a new device (such as a button, tram, signal etc) comes online
+ * it calls this proc with the subsystem enabling two-way communication using
+ * signals.
+ *
+ * Arguments: new_unit: the starting point to find a beacon
+ * unit_name: the friendly name of this device
+ * id_tag: a unique identifier for this device, set on init
+ */
+/datum/controller/subsystem/processing/transport/proc/hello(atom/new_unit, unit_name, id_tag)
+ RegisterSignal(new_unit, COMSIG_TRANSPORT_REQUEST, PROC_REF(incoming_request))
+ log_transport("Sub: Registered new transport component [unit_name] [id_tag].")
+
+/datum/controller/subsystem/processing/transport/Recover()
+ _listen_lookup = SStransport._listen_lookup
+
+/**
+ * Performs the request received from a registered transport device
+ *
+ * Currently the only supported request type is tram dispatch
+ * so there's no var for what type of request it is
+ *
+ * The subsystem will validate and process, then send a success
+ * or fail response to the device that made the request,
+ * with info relevant to the request such as destination
+ * or error details (if the request is rejected/fails)
+ *
+ * Arguments: source: the device sending the request
+ * transport_id: the transport this request is for, such as tram line 1 or 2
+ * platform: the requested destination to dispatch the tram
+ * options: additional flags for the request (ie: bypass doors, emagged request)
+ */
+/datum/controller/subsystem/processing/transport/proc/incoming_request(obj/source, transport_id, platform, options)
+ SIGNAL_HANDLER
+
+ log_transport("Sub: Received request from [source.name] [source.id_tag]. Contents: [transport_id] [platform] [options]")
+ var/relevant
+ var/request_flags = options
+ var/datum/transport_controller/linear/tram/transport_controller
+ var/obj/effect/landmark/transport/nav_beacon/tram/platform/destination
+ for(var/datum/transport_controller/linear/tram/candidate_controller as anything in transports_by_type[TRANSPORT_TYPE_TRAM])
+ if(candidate_controller.specific_transport_id == transport_id)
+ transport_controller = candidate_controller
+ break
+
+ // We make a list of relevant devices (that should act/respond to this request) for when we send the signal at the end
+ LAZYADD(relevant, source)
+
+ // Check for various failure states
+ // The transport controller datum is qdel'd
+ if(isnull(transport_controller))
+ log_transport("Sub: Transport [transport_id] has no controller datum! Someone deleted it or something catastrophic happened.")
+ SEND_TRANSPORT_SIGNAL(COMSIG_TRANSPORT_RESPONSE, relevant, REQUEST_FAIL, BROKEN_BEYOND_REPAIR)
+ log_transport("Sub: Sending response to [source.id_tag]. Contents: [REQUEST_FAIL] [INTERNAL_ERROR]. Info: [SUB_TS_STATUS].")
+ return
+
+ // Non operational (such as power loss) or the controls cabinet is missing/destroyed
+ if(!transport_controller.controller_operational || !transport_controller.paired_cabinet)
+ SEND_TRANSPORT_SIGNAL(COMSIG_TRANSPORT_RESPONSE, relevant, REQUEST_FAIL, NOT_IN_SERVICE)
+ log_transport("Sub: Sending response to [source.id_tag]. Contents: [REQUEST_FAIL] [NOT_IN_SERVICE]. Info: TC-[!transport_controller][!transport_controller.controller_operational][!transport_controller.paired_cabinet].")
+ return
+
+ // Someone emergency stopped the tram, or something went wrong and it needs to reset its landmarks.
+ if(transport_controller.controller_status & SYSTEM_FAULT || transport_controller.controller_status & EMERGENCY_STOP)
+ SEND_TRANSPORT_SIGNAL(COMSIG_TRANSPORT_RESPONSE, relevant, REQUEST_FAIL, INTERNAL_ERROR)
+ log_transport("Sub: Sending response to [source.id_tag]. Contents: [REQUEST_FAIL] [INTERNAL_ERROR]. Info: [SUB_TS_STATUS].")
+ return
+
+ // Controller is 'active' (not accepting requests right now) someone already pushed button, hit by a rod, etc.
+ if(transport_controller.controller_active)
+ SEND_TRANSPORT_SIGNAL(COMSIG_TRANSPORT_RESPONSE, relevant, REQUEST_FAIL, TRANSPORT_IN_USE)
+ log_transport("Sub: Sending response to [source.id_tag]. Contents: [REQUEST_FAIL] [TRANSPORT_IN_USE]. Info: [TC_TA_INFO].")
+ return
+
+ // We've made it this far, tram is physically fine so let's trip plan
+ // This is based on the destination nav beacon, the logical location
+ // If Something Happens and the location the controller thinks it's at
+ // gets out of sync with it's actual physical location, it can be reset
+
+ // Since players can set the platform ID themselves, make sure it's a valid platform we're aware of
+ var/network = LAZYACCESS(nav_beacons, transport_id)
+ for(var/obj/effect/landmark/transport/nav_beacon/tram/platform/potential_destination in network)
+ if(potential_destination.platform_code == platform)
+ destination = potential_destination
+ break
+
+ // The platform in the request doesn't exist (ie: Can't send a tram to East Wing when the map is Birdshot)
+ if(!destination)
+ SEND_TRANSPORT_SIGNAL(COMSIG_TRANSPORT_RESPONSE, relevant, REQUEST_FAIL, INVALID_PLATFORM)
+ log_transport("Sub: Sending response to [source.id_tag]. Contents: [REQUEST_FAIL] [INVALID_PLATFORM]. Info: RD0.")
+ return
+
+ // The controller thinks the tram is already there
+ if(transport_controller.idle_platform == destination) //did you even look?
+ SEND_TRANSPORT_SIGNAL(COMSIG_TRANSPORT_RESPONSE, relevant, REQUEST_FAIL, NO_CALL_REQUIRED)
+ log_transport("Sub: Sending response to [source.id_tag]. Contents: [REQUEST_FAIL] [NO_CALL_REQUIRED]. Info: RD1.")
+ return
+
+ // Calculate the trip data, which will be stored on the controller datum, passed to the transport modules making up the tram
+ // If for some reason the controller can't determine the distance/direction it needs to go, send a failure message
+ if(!transport_controller.calculate_route(destination))
+ SEND_TRANSPORT_SIGNAL(COMSIG_TRANSPORT_RESPONSE, relevant, REQUEST_FAIL, INTERNAL_ERROR)
+ log_transport("Sub: Sending response to [source.id_tag]. Contents: [REQUEST_FAIL] [INTERNAL_ERROR]. Info: NV0.")
+ return
+
+ // At this point we're sending the tram somewhere, so send a success response to the devices
+ SEND_TRANSPORT_SIGNAL(COMSIG_TRANSPORT_RESPONSE, relevant, REQUEST_SUCCESS, destination.name)
+ log_transport("Sub: Sending response to [source.id_tag]. Contents: [REQUEST_SUCCESS] [destination.name].")
+
+ // Since this is a signal and we're done with the request, do the rest async
+ INVOKE_ASYNC(src, PROC_REF(dispatch_transport), transport_controller, request_flags)
+
+/**
+ * Dispatches the transport on a validated trip
+ *
+ * The subsystem at this point has confirmed a valid trip
+ * Start the transport, wake up machinery running on
+ * the subsystem (signals, etc.)
+ *
+ * Make tram go, basically.
+ *
+ * Arguments: transport_controller: the transport controller datum we're giving orders to
+ * destination: destination we're sending it to
+ * request_flags: additional flags for the request (ie: bypass doors, emagged request)
+ */
+/datum/controller/subsystem/processing/transport/proc/dispatch_transport(datum/transport_controller/linear/tram/transport_controller, destination, request_flags)
+ log_transport("Sub: Sending dispatch request to [transport_controller.specific_transport_id]. [request_flags ? "Contents: [request_flags]." : "No request flags."]")
+
+ // This will generally be caught in the request validation, however an admin may try to force move the tram, or other actions bypassing the request process.
+ if(transport_controller.idle_platform == transport_controller.destination_platform)
+ log_transport("Sub: [transport_controller.specific_transport_id] dispatch failed. Info: DE-1 Transport Controller idle and destination are the same.")
+ return
+
+ // Set active, so no more requests will be accepted until we're in a safe state to change destination.
+ transport_controller.set_active(TRUE)
+ pre_departure(transport_controller, request_flags)
+
+/**
+ * Pre-departure checks for the tram
+ *
+ * We do things slighly different based on the request_flags such as
+ * door crushing, emag related things
+ *
+ * Arguments: transport_controller: the transport controller datum we're giving orders to
+ * request_flags: additional flags for the request (ie: bypass doors, emagged request)
+ */
+/datum/controller/subsystem/processing/transport/proc/pre_departure(datum/transport_controller/linear/tram/transport_controller, request_flags)
+ log_transport("Sub: [transport_controller.specific_transport_id] start pre-departure. Info: [SUB_TS_STATUS]")
+
+ // Tram Malfunction event
+ if(transport_controller.controller_status & COMM_ERROR)
+ request_flags |= BYPASS_SENSORS
+
+ // Lock the physical controls of the tram
+ transport_controller.set_status_code(PRE_DEPARTURE, TRUE)
+ transport_controller.set_status_code(CONTROLS_LOCKED, TRUE)
+
+ // Tram door actions
+ log_transport("Sub: [transport_controller.specific_transport_id] requested door close. Info: [SUB_TS_STATUS].")
+ if(request_flags & RAPID_MODE || request_flags & BYPASS_SENSORS || transport_controller.controller_status & BYPASS_SENSORS) // bypass for unsafe, rapid departure
+ transport_controller.cycle_doors(CYCLE_CLOSED, BYPASS_DOOR_CHECKS)
+ if(request_flags & RAPID_MODE)
+ log_transport("Sub: [transport_controller.specific_transport_id] rapid mode enabled, bypassing validation.")
+ transport_controller.dispatch_transport()
+ return
+ else
+ transport_controller.set_status_code(DOORS_READY, FALSE)
+ transport_controller.cycle_doors(CYCLE_CLOSED)
+
+ addtimer(CALLBACK(src, PROC_REF(validate_and_dispatch), transport_controller), 3 SECONDS)
+
+/**
+ * Operational checks, then start moving
+ *
+ * Some check failures aren't worth halting the tram for, like no blocking the doors forever
+ * Crush them instead!
+ *
+ * Arguments: transport_controller: the transport controller datum we're giving orders to
+ * attempt: how many attempts to start moving we've made
+ */
+/datum/controller/subsystem/processing/transport/proc/validate_and_dispatch(datum/transport_controller/linear/tram/transport_controller, attempt)
+ log_transport("Sub: [transport_controller.specific_transport_id] start pre-departure validation. Attempts: [attempt ? attempt : 0].")
+ var/current_attempt
+ if(attempt)
+ current_attempt = attempt
+ else
+ current_attempt = 0
+
+ if(current_attempt >= 4)
+ log_transport("Sub: [transport_controller.specific_transport_id] pre-departure validation failed, but dispatching tram anyways. Info: [SUB_TS_STATUS].")
+ transport_controller.dispatch_transport()
+ return
+
+ current_attempt++
+
+ transport_controller.update_status()
+ if(!(transport_controller.controller_status & DOORS_READY))
+ addtimer(CALLBACK(src, PROC_REF(validate_and_dispatch), transport_controller, current_attempt), 3 SECONDS)
+ return
+ else
+
+ transport_controller.dispatch_transport()
+ log_transport("Sub: [transport_controller.specific_transport_id] pre-departure passed.")
+
+/// Give a list of destinations to the tram controls
+/datum/controller/subsystem/processing/transport/proc/detailed_destination_list(specific_transport_id)
+ . = list()
+ for(var/obj/effect/landmark/transport/nav_beacon/tram/platform/destination as anything in SStransport.nav_beacons[specific_transport_id])
+ var/list/this_destination = list()
+ this_destination["name"] = destination.name
+ this_destination["dest_icons"] = destination.tgui_icons
+ this_destination["id"] = destination.platform_code
+ . += list(this_destination)
diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm
index d81c72cc92008..69f8fd42dcf8d 100644
--- a/code/datums/actions/action.dm
+++ b/code/datums/actions/action.dm
@@ -79,16 +79,17 @@
/// Grants the action to the passed mob, making it the owner
/datum/action/proc/Grant(mob/grant_to)
- if(!grant_to)
+ if(isnull(grant_to))
Remove(owner)
return
- if(owner)
- if(owner == grant_to)
- return
- Remove(owner)
- SEND_SIGNAL(src, COMSIG_ACTION_GRANTED, grant_to)
- SEND_SIGNAL(grant_to, COMSIG_MOB_GRANTED_ACTION, src)
+ if(grant_to == owner)
+ return // We already have it
+ var/mob/previous_owner = owner
owner = grant_to
+ if(!isnull(previous_owner))
+ Remove(previous_owner)
+ SEND_SIGNAL(src, COMSIG_ACTION_GRANTED, owner)
+ SEND_SIGNAL(owner, COMSIG_MOB_GRANTED_ACTION, src)
RegisterSignal(owner, COMSIG_QDELETING, PROC_REF(clear_ref), override = TRUE)
// Register some signals based on our check_flags
@@ -120,27 +121,29 @@
LAZYREMOVE(remove_from?.actions, src) // We aren't always properly inserted into the viewers list, gotta make sure that action's cleared
viewers = list()
- if(owner)
- SEND_SIGNAL(src, COMSIG_ACTION_REMOVED, owner)
- SEND_SIGNAL(owner, COMSIG_MOB_REMOVED_ACTION, src)
- UnregisterSignal(owner, COMSIG_QDELETING)
-
- // Clean up our check_flag signals
- UnregisterSignal(owner, list(
- COMSIG_LIVING_SET_BODY_POSITION,
- COMSIG_MOB_STATCHANGE,
- SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED),
- SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED),
- SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED),
- SIGNAL_ADDTRAIT(TRAIT_MAGICALLY_PHASED),
- SIGNAL_REMOVETRAIT(TRAIT_HANDS_BLOCKED),
- SIGNAL_REMOVETRAIT(TRAIT_IMMOBILIZED),
- SIGNAL_REMOVETRAIT(TRAIT_INCAPACITATED),
- SIGNAL_REMOVETRAIT(TRAIT_MAGICALLY_PHASED),
- ))
-
- if(target == owner)
- RegisterSignal(target, COMSIG_QDELETING, PROC_REF(clear_ref))
+ if(isnull(owner))
+ return
+ SEND_SIGNAL(src, COMSIG_ACTION_REMOVED, owner)
+ SEND_SIGNAL(owner, COMSIG_MOB_REMOVED_ACTION, src)
+ UnregisterSignal(owner, COMSIG_QDELETING)
+
+ // Clean up our check_flag signals
+ UnregisterSignal(owner, list(
+ COMSIG_LIVING_SET_BODY_POSITION,
+ COMSIG_MOB_STATCHANGE,
+ SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED),
+ SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED),
+ SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED),
+ SIGNAL_ADDTRAIT(TRAIT_MAGICALLY_PHASED),
+ SIGNAL_REMOVETRAIT(TRAIT_HANDS_BLOCKED),
+ SIGNAL_REMOVETRAIT(TRAIT_IMMOBILIZED),
+ SIGNAL_REMOVETRAIT(TRAIT_INCAPACITATED),
+ SIGNAL_REMOVETRAIT(TRAIT_MAGICALLY_PHASED),
+ ))
+
+ if(target == owner)
+ RegisterSignal(target, COMSIG_QDELETING, PROC_REF(clear_ref))
+ if (owner == remove_from)
owner = null
/// Actually triggers the effects of the action.
diff --git a/code/datums/actions/mobs/fire_breath.dm b/code/datums/actions/mobs/fire_breath.dm
index 254a6081425e1..45b6538c01836 100644
--- a/code/datums/actions/mobs/fire_breath.dm
+++ b/code/datums/actions/mobs/fire_breath.dm
@@ -2,63 +2,121 @@
name = "Fire Breath"
button_icon = 'icons/effects/magic.dmi'
button_icon_state = "fireball"
- desc = "Allows you to shoot fire towards a target."
+ desc = "Breathe a line of flames towards the target."
cooldown_time = 3 SECONDS
/// The range of the fire
var/fire_range = 15
/// The sound played when you use this ability
var/fire_sound = 'sound/magic/fireball.ogg'
- /// If the fire should be icey fire
- var/ice_breath = FALSE
+ /// Time to wait between spawning each fire turf
+ var/fire_delay = 1.5 DECISECONDS
+ /// How hot is our fire
+ var/fire_temperature = DRAKE_FIRE_TEMP
+ /// 'How much' fire do we expose the turf to?
+ var/fire_volume = DRAKE_FIRE_EXPOSURE
+ /// How much damage do you take when engulfed?
+ var/fire_damage = 20
+ /// How much damage to mechs take when engulfed?
+ var/mech_damage = 45
/datum/action/cooldown/mob_cooldown/fire_breath/Activate(atom/target_atom)
- StartCooldown(360 SECONDS, 360 SECONDS)
attack_sequence(target_atom)
StartCooldown()
return TRUE
+/// Apply our specific fire breathing shape, in proc form so we can override it in subtypes
/datum/action/cooldown/mob_cooldown/fire_breath/proc/attack_sequence(atom/target)
playsound(owner.loc, fire_sound, 200, TRUE)
- fire_line(target, 0)
+ fire_line(target)
+/// Breathe fire in a line towards the target, optionally rotated at an offset from the target
/datum/action/cooldown/mob_cooldown/fire_breath/proc/fire_line(atom/target, offset)
- SLEEP_CHECK_DEATH(0, owner)
- var/list/turfs = line_target(offset, fire_range, target)
- // This proc sleeps
- INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(dragon_fire_line), owner, /* burn_turfs = */ turfs, /* frozen = */ ice_breath)
+ if (isnull(target))
+ return
+ var/turf/target_turf = get_ranged_target_turf_direct(owner, target, fire_range, offset)
+ var/list/turfs = get_line(owner, target_turf) - get_turf(owner)
+ INVOKE_ASYNC(src, PROC_REF(progressive_fire_line), turfs)
-/datum/action/cooldown/mob_cooldown/fire_breath/proc/line_target(offset, range, atom/target)
- if(!target)
+/// Creates fire with a delay on the list of targetted turfs
+/datum/action/cooldown/mob_cooldown/fire_breath/proc/progressive_fire_line(list/burn_turfs)
+ if (QDELETED(owner) || owner.stat == DEAD)
return
- var/turf/T = get_ranged_target_turf_direct(owner, target, range, offset)
- return (get_line(owner, T) - get_turf(owner))
+ // Guys we have already hit, no double dipping
+ var/list/hit_list = list(owner) // also don't burn ourselves
+ for(var/turf/target_turf in burn_turfs)
+ if (target_turf.is_blocked_turf(exclude_mobs = TRUE))
+ return
+ burn_turf(target_turf, hit_list, owner)
+ sleep(fire_delay)
+
+/// Finally spawn the actual fire, spawns the fire hotspot in case you want to recolour it or something
+/datum/action/cooldown/mob_cooldown/fire_breath/proc/burn_turf(turf/fire_turf, list/hit_list, mob/living/source)
+ var/obj/effect/hotspot/fire_hotspot = new /obj/effect/hotspot(fire_turf)
+ fire_turf.hotspot_expose(fire_temperature, fire_volume, TRUE)
+
+ for(var/mob/living/barbecued in fire_turf.contents)
+ if(barbecued in hit_list)
+ continue
+ hit_list |= barbecued
+ on_burn_mob(barbecued, source)
+
+ for(var/obj/vehicle/sealed/mecha/robotron in fire_turf.contents)
+ if(robotron in hit_list)
+ continue
+ hit_list |= robotron
+ robotron.take_damage(mech_damage, BURN, FIRE)
+ return fire_hotspot
+
+/// Do something unpleasant to someone we set on fire
+/datum/action/cooldown/mob_cooldown/fire_breath/proc/on_burn_mob(mob/living/barbecued, mob/living/source)
+ to_chat(barbecued, span_userdanger("You are burned by [source]'s fire breath!"))
+ barbecued.adjustFireLoss(fire_damage)
+
+/// Shoot three lines of fire in a sort of fork pattern approximating a cone
/datum/action/cooldown/mob_cooldown/fire_breath/cone
name = "Fire Cone"
- desc = "Allows you to shoot fire towards a target with surrounding lines of fire."
+ desc = "Breathe several lines of fire directed at a target."
/// The angles relative to the target that shoot lines of fire
var/list/angles = list(-40, 0, 40)
/datum/action/cooldown/mob_cooldown/fire_breath/cone/attack_sequence(atom/target)
playsound(owner.loc, fire_sound, 200, TRUE)
for(var/offset in angles)
- INVOKE_ASYNC(src, PROC_REF(fire_line), target, offset)
+ fire_line(target, offset)
+/// Shoot fire in a whole bunch of directions
/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire
name = "Mass Fire"
button_icon = 'icons/effects/fire.dmi'
button_icon_state = "1"
- desc = "Allows you to shoot fire in all directions."
+ desc = "Breathe flames in all directions."
cooldown_time = 3 SECONDS
+ click_to_activate = FALSE
+ /// How many fire lines do we produce to turn a full circle?
+ var/sectors = 12
+ /// How long do we wait between each spin?
+ var/breath_delay = 2.5 SECONDS
+ /// How many full circles do we perform?
+ var/total_spins = 3
+
+/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire/Activate(atom/target_atom)
+ target_atom = get_step(owner, owner.dir) // Just shoot it forwards, we don't need to click on someone for this one
+ return ..()
/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire/attack_sequence(atom/target)
- shoot_mass_fire(target, 12, 2.5 SECONDS, 3)
+ var/queued_spins = 0
+ for (var/i in 1 to total_spins)
+ var/delay = queued_spins * breath_delay
+ queued_spins++
+ addtimer(CALLBACK(src, PROC_REF(fire_spin), target, queued_spins), delay)
-/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire/proc/shoot_mass_fire(atom/target, spiral_count, delay_time, times)
- SLEEP_CHECK_DEATH(0, owner)
- for(var/i = 1 to times)
- playsound(owner.loc, fire_sound, 200, TRUE)
- var/increment = 360 / spiral_count
- for(var/j = 1 to spiral_count)
- INVOKE_ASYNC(src, PROC_REF(fire_line), target, j * increment + i * increment / 2)
- SLEEP_CHECK_DEATH(delay_time, owner)
+/// Breathe fire in a circle, with a slight angle offset based on which of our several circles it is
+/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire/proc/fire_spin(target, spin_count)
+ if (QDELETED(owner) || owner.stat == DEAD)
+ return // Too dead to spin
+ playsound(owner.loc, fire_sound, 200, TRUE)
+ var/angle_increment = 360 / sectors
+ var/additional_offset = spin_count * angle_increment / 2
+ for (var/i in 1 to sectors)
+ fire_line(target, (angle_increment * i) + (additional_offset))
diff --git a/code/datums/actions/mobs/mobcooldown.dm b/code/datums/actions/mobs/mobcooldown.dm
index 17561f2f45a54..a495859494ffb 100644
--- a/code/datums/actions/mobs/mobcooldown.dm
+++ b/code/datums/actions/mobs/mobcooldown.dm
@@ -3,7 +3,7 @@
button_icon = 'icons/mob/actions/actions_items.dmi'
button_icon_state = "sniper_zoom"
desc = "Click this ability to attack."
- check_flags = AB_CHECK_CONSCIOUS
+ check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
cooldown_time = 5 SECONDS
text_cooldown = TRUE
click_to_activate = TRUE
diff --git a/code/datums/actions/mobs/sneak.dm b/code/datums/actions/mobs/sneak.dm
index 3fed4f4d500c7..738bb7b70cf5d 100644
--- a/code/datums/actions/mobs/sneak.dm
+++ b/code/datums/actions/mobs/sneak.dm
@@ -5,7 +5,6 @@
button_icon_state = "sniper_zoom"
background_icon_state = "bg_alien"
overlay_icon_state = "bg_alien_border"
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED | AB_CHECK_INCAPACITATED
cooldown_time = 0.5 SECONDS
melee_cooldown_time = 0 SECONDS
click_to_activate = FALSE
diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm
index 7a30648030272..04493b44e5d3a 100644
--- a/code/datums/ai/_ai_controller.dm
+++ b/code/datums/ai/_ai_controller.dm
@@ -334,7 +334,7 @@ multiple modular subtrees with behaviors
set_ai_status(AI_STATUS_ON) //Can't do anything while player is connected
RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained))
-/// Use this proc to define how your controller defines what access the pawn has for the sake of pathfinding, likely pointing to whatever ID slot is relevant
+/// Use this proc to define how your controller defines what access the pawn has for the sake of pathfinding. Return the access list you want to use
/datum/ai_controller/proc/get_access()
return
@@ -619,6 +619,7 @@ multiple modular subtrees with behaviors
// We found the value that's been deleted, it was an assoc value. Clear it out entirely
else if(associated_value == source)
next_to_clear -= inner_value
+ SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(inner_value))
index += 1
diff --git a/code/datums/ai/babies/babies_behaviors.dm b/code/datums/ai/babies/babies_behaviors.dm
index 716922cc7b292..553b192a180e4 100644
--- a/code/datums/ai/babies/babies_behaviors.dm
+++ b/code/datums/ai/babies/babies_behaviors.dm
@@ -32,7 +32,7 @@
partner = other
//shyness check. we're not shy in front of things that share a faction with us.
- else if(isliving(other) && !pawn_mob.faction_check_mob(other))
+ else if(isliving(other) && !pawn_mob.faction_check_atom(other))
finish_action(controller, FALSE)
return
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm
index 551cb12f3b145..07d2a17cc8de2 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm
@@ -55,7 +55,7 @@
var/list/airlocks = SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/door/airlock)
for(var/i in 1 to run_distance)
var/turf/test_destination = get_ranged_target_turf_direct(source, target, range = i, offset = angle)
- if(test_destination.is_blocked_turf(exclude_mobs = !source.density, source_atom = source, ignore_atoms = airlocks))
+ if(test_destination.is_blocked_turf(source_atom = source, ignore_atoms = airlocks))
break
return_turf = test_destination
return return_turf
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/set_travel_destination.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/set_travel_destination.dm
new file mode 100644
index 0000000000000..207df4424577d
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/set_travel_destination.dm
@@ -0,0 +1,13 @@
+/datum/ai_behavior/set_travel_destination
+
+/datum/ai_behavior/set_travel_destination/perform(seconds_per_tick, datum/ai_controller/controller, target_key, location_key)
+ . = ..()
+ var/atom/target = controller.blackboard[target_key]
+
+ if(QDELETED(target))
+ finish_action(controller, FALSE, target_key)
+ return
+
+ controller.set_blackboard_key(location_key, target)
+
+ finish_action(controller, TRUE, target_key)
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/wounded_targetting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/wounded_targetting.dm
new file mode 100644
index 0000000000000..46037fdc076ee
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/wounded_targetting.dm
@@ -0,0 +1,11 @@
+/// Picks targets based on which one has the lowest health
+/datum/ai_behavior/find_potential_targets/most_wounded
+
+/datum/ai_behavior/find_potential_targets/most_wounded/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
+ var/list/living_targets = list()
+ for(var/mob/living/living_target in filtered_targets)
+ living_targets += filtered_targets
+ if(living_targets.len)
+ sortTim(living_targets, GLOBAL_PROC_REF(cmp_mob_health))
+ return pop(living_targets)
+ return ..()
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm b/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm
new file mode 100644
index 0000000000000..59ff88b4879be
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm
@@ -0,0 +1,50 @@
+#define REINFORCEMENTS_COOLDOWN (30 SECONDS)
+
+/// Calls all nearby mobs that share a faction to give backup in combat
+/datum/ai_planning_subtree/call_reinforcements
+ /// Blackboard key containing something to say when calling reinforcements (takes precedence over emotes)
+ var/say_key = BB_REINFORCEMENTS_SAY
+ /// Blackboard key containing an emote to perform when calling reinforcements
+ var/emote_key = BB_REINFORCEMENTS_EMOTE
+ /// Reinforcement-calling behavior to use
+ var/call_type = /datum/ai_behavior/call_reinforcements
+
+/datum/ai_planning_subtree/call_reinforcements/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ if (!decide_to_call(controller) || controller.blackboard[BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN] > world.time)
+ return
+
+ var/call_say = controller.blackboard[BB_REINFORCEMENTS_SAY]
+ var/call_emote = controller.blackboard[BB_REINFORCEMENTS_EMOTE]
+
+ if(!isnull(call_say))
+ controller.queue_behavior(/datum/ai_behavior/perform_speech, call_say)
+ else if(!isnull(call_emote))
+ controller.queue_behavior(/datum/ai_behavior/perform_emote, call_emote)
+ else
+ controller.queue_behavior(/datum/ai_behavior/perform_emote, "cries for help!")
+
+ controller.queue_behavior(call_type)
+
+/// Decides when to call reinforcements, can be overridden for alternate behavior
+/datum/ai_planning_subtree/call_reinforcements/proc/decide_to_call(datum/ai_controller/controller)
+ return controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET) && istype(controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET], /mob)
+
+/// Call out to all mobs in the specified range for help
+/datum/ai_behavior/call_reinforcements
+ /// Range to call reinforcements from
+ var/reinforcements_range = 15
+
+/datum/ai_behavior/call_reinforcements/perform(seconds_per_tick, datum/ai_controller/controller)
+ . = ..()
+
+ var/mob/pawn_mob = controller.pawn
+ for(var/mob/other_mob in oview(reinforcements_range, pawn_mob))
+ if(pawn_mob.faction_check_atom(other_mob) && !isnull(other_mob.ai_controller))
+ // Add our current target to their retaliate list so that they'll attack our aggressor
+ other_mob.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
+ other_mob.ai_controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENT_TARGET, pawn_mob)
+
+ controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN, world.time + REINFORCEMENTS_COOLDOWN)
+
+#undef REINFORCEMENTS_COOLDOWN
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/go_for_swim.dm b/code/datums/ai/basic_mobs/basic_subtrees/go_for_swim.dm
new file mode 100644
index 0000000000000..12c77119f3e18
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/go_for_swim.dm
@@ -0,0 +1,59 @@
+#define DEFAULT_TIME_SWIMMER 30 SECONDS
+
+///subtree to go and swim!
+/datum/ai_planning_subtree/go_for_swim
+
+/datum/ai_planning_subtree/go_for_swim/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(controller.blackboard_key_exists(BB_SWIM_ALTERNATE_TURF))
+ controller.queue_behavior(/datum/ai_behavior/travel_towards/swimming, BB_SWIM_ALTERNATE_TURF)
+
+ if(isnull(controller.blackboard[BB_KEY_SWIM_TIME]))
+ controller.set_blackboard_key(BB_KEY_SWIM_TIME, DEFAULT_TIME_SWIMMER)
+
+ var/mob/living/living_pawn = controller.pawn
+ var/turf/our_turf = get_turf(living_pawn)
+
+ // we have been taken out of water!
+ controller.set_blackboard_key(BB_CURRENTLY_SWIMMING, iswaterturf(our_turf))
+
+ if(controller.blackboard[BB_KEY_SWIM_TIME] < world.time)
+ controller.queue_behavior(/datum/ai_behavior/find_and_set/swim_alternate, BB_SWIM_ALTERNATE_TURF, /turf/open)
+ return
+
+ // have some fun in the water
+ if(controller.blackboard[BB_CURRENTLY_SWIMMING] && SPT_PROB(5, seconds_per_tick))
+ controller.queue_behavior(/datum/ai_behavior/perform_emote, "splashes water all around!")
+
+
+///find land if its time to get out of water, otherwise find water
+/datum/ai_behavior/find_and_set/swim_alternate
+
+/datum/ai_behavior/find_and_set/swim_alternate/search_tactic(datum/ai_controller/controller, locate_path, search_range)
+ var/mob/living/living_pawn = controller.pawn
+ if(QDELETED(living_pawn))
+ return null
+ var/look_for_land = controller.blackboard[BB_CURRENTLY_SWIMMING]
+ var/list/possible_turfs = list()
+ for(var/turf/possible_turf in oview(search_range, living_pawn))
+ if(isclosedturf(possible_turf) || isspaceturf(possible_turf) || isopenspaceturf(possible_turf))
+ continue
+ if(possible_turf.is_blocked_turf())
+ continue
+ if(look_for_land == iswaterturf(possible_turf))
+ continue
+ possible_turfs += possible_turf
+
+ if(!length(possible_turfs))
+ return null
+
+ return(pick(possible_turfs))
+
+/datum/ai_behavior/travel_towards/swimming
+ clear_target = TRUE
+
+/datum/ai_behavior/travel_towards/swimming/finish_action(datum/ai_controller/controller, succeeded, target_key)
+ . = ..()
+ var/time_to_add = controller.blackboard[BB_KEY_SWIMMER_COOLDOWN] ? controller.blackboard[BB_KEY_SWIMMER_COOLDOWN] : DEFAULT_TIME_SWIMMER
+ controller.set_blackboard_key(BB_KEY_SWIM_TIME, world.time + time_to_add )
+
+#undef DEFAULT_TIME_SWIMMER
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/prepare_travel_to_destination.dm b/code/datums/ai/basic_mobs/basic_subtrees/prepare_travel_to_destination.dm
new file mode 100644
index 0000000000000..2718ca630ff7a
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/prepare_travel_to_destination.dm
@@ -0,0 +1,23 @@
+
+///Subtree that checks if we are on the target atom's tile, and sets it as a travel target if not
+///The target is taken from the blackboard. This one always requires a specific implementation.
+/datum/ai_planning_subtree/prepare_travel_to_destination
+ var/target_key
+ var/travel_destination_key = BB_TRAVEL_DESTINATION
+
+/datum/ai_planning_subtree/prepare_travel_to_destination/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/atom/target = controller.blackboard[target_key]
+
+ //Target is deleted, or we are already standing on it
+ if(QDELETED(target) || (isturf(target) && controller.pawn.loc == target) || (target.loc == controller.pawn.loc))
+ return
+
+ //Already set with this value, return
+ if(controller.blackboard[target_key] == controller.blackboard[travel_destination_key])
+ return
+
+ controller.queue_behavior(/datum/ai_behavior/set_travel_destination, target_key, travel_destination_key)
+ return //continue planning regardless of success
+
+/datum/ai_planning_subtree/prepare_travel_to_destination/trader
+ target_key = BB_SHOP_SPOT
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_wounded_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_wounded_target.dm
new file mode 100644
index 0000000000000..8840c1ea3a76c
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_wounded_target.dm
@@ -0,0 +1,6 @@
+/// Selects the most wounded potential target that we can see
+/datum/ai_planning_subtree/simple_find_wounded_target
+
+/datum/ai_planning_subtree/simple_find_wounded_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ controller.queue_behavior(/datum/ai_behavior/find_potential_targets/most_wounded, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/travel_to_point.dm b/code/datums/ai/basic_mobs/basic_subtrees/travel_to_point.dm
index 9ce7cc95c07da..0b5e5d4776f9e 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/travel_to_point.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/travel_to_point.dm
@@ -16,3 +16,6 @@
/datum/ai_planning_subtree/travel_to_point/and_clear_target
travel_behaviour = /datum/ai_behavior/travel_towards/stop_on_arrival
+
+/datum/ai_planning_subtree/travel_to_point/and_clear_target/reinforce
+ location_key = BB_BASIC_MOB_REINFORCEMENT_TARGET
diff --git a/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm b/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm
index bc4a554bc024b..d8b7e49c23cdd 100644
--- a/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm
+++ b/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm
@@ -15,10 +15,12 @@
/datum/targetting_datum/basic
/// When we do our basic faction check, do we look for exact faction matches?
var/check_factions_exactly = FALSE
- /// Minimum status to attack living beings
- var/stat_attack = CONSCIOUS
- ///Whether we care for seeing the target or not
+ /// Whether we care for seeing the target or not
var/ignore_sight = FALSE
+ /// Blackboard key containing the minimum stat of a living mob to target
+ var/minimum_stat_key = BB_TARGET_MINIMUM_STAT
+ /// If this blackboard key is TRUE, makes us only target wounded mobs
+ var/target_wounded_key
/datum/targetting_datum/basic/can_attack(mob/living/living_mob, atom/the_target, vision_range)
var/datum/ai_controller/basic_controller/our_controller = living_mob.ai_controller
@@ -54,7 +56,9 @@
var/mob/living/living_target = the_target
if(faction_check(our_controller, living_mob, living_target))
return FALSE
- if(living_target.stat > stat_attack)
+ if(living_target.stat > our_controller.blackboard[minimum_stat_key])
+ return FALSE
+ if(target_wounded_key && our_controller.blackboard[target_wounded_key] && living_target.health == living_target.maxHealth)
return FALSE
return TRUE
@@ -81,7 +85,7 @@
/datum/targetting_datum/basic/proc/faction_check(datum/ai_controller/controller, mob/living/living_mob, mob/living/the_target)
if (controller.blackboard[BB_ALWAYS_IGNORE_FACTION] || controller.blackboard[BB_TEMPORARILY_IGNORE_FACTION])
return FALSE
- return living_mob.faction_check_mob(the_target, exact_match = check_factions_exactly)
+ return living_mob.faction_check_atom(the_target, exact_match = check_factions_exactly)
/// Subtype more forgiving for items.
/// Careful, this can go wrong and keep a mob hyper-focused on an item it can't lose aggro on
@@ -121,8 +125,8 @@
find_smaller = FALSE
inclusive = FALSE
-/datum/targetting_datum/basic/attack_until_dead
- stat_attack = HARD_CRIT
+/// Makes the mob only attack their own faction. Useful mostly if their attacks do something helpful (e.g. healing touch).
+/datum/targetting_datum/basic/same_faction
-/datum/targetting_datum/basic/attack_even_if_dead
- stat_attack = DEAD
+/datum/targetting_datum/basic/same_faction/faction_check(mob/living/living_mob, mob/living/the_target)
+ return !..() // inverts logic to ONLY target mobs that share a faction
diff --git a/code/datums/ai/basic_mobs/targetting_datums/dont_target_friends.dm b/code/datums/ai/basic_mobs/targetting_datums/dont_target_friends.dm
index f1bd411fd298e..e2081bf308e9c 100644
--- a/code/datums/ai/basic_mobs/targetting_datums/dont_target_friends.dm
+++ b/code/datums/ai/basic_mobs/targetting_datums/dont_target_friends.dm
@@ -1,43 +1,25 @@
/// Don't target an atom in our friends list (or turfs), anything else is fair game
-/datum/targetting_datum/not_friends
+/datum/targetting_datum/basic/not_friends
/// Stop regarding someone as a valid target once they pass this stat level, setting it to DEAD means you will happily attack corpses
var/attack_until_past_stat = HARD_CRIT
/// If we can try to closed turfs or not
var/attack_closed_turf = FALSE
///Returns true or false depending on if the target can be attacked by the mob
-/datum/targetting_datum/not_friends/can_attack(mob/living/living_mob, atom/target, vision_range)
- if (!target)
- return FALSE
- if (attack_closed_turf)
- if (isopenturf(target))
- return FALSE
- else
- if (isturf(target))
- return FALSE
-
- if (ismob(target))
- var/mob/mob_target = target
- if (mob_target.status_flags & GODMODE)
- return FALSE
- if (mob_target.stat > attack_until_past_stat)
- return FALSE
+/datum/targetting_datum/basic/not_friends/can_attack(mob/living/living_mob, atom/target, vision_range)
+ if(attack_closed_turf && isclosedturf(target))
+ return TRUE
- if (living_mob.see_invisible < target.invisibility)
- return FALSE
- if (isturf(target.loc) && living_mob.z != target.z) // z check will always fail if target is in a mech
- return FALSE
- if (!living_mob.ai_controller) // How did you get here?
+ if(target in living_mob.ai_controller.blackboard[BB_FRIENDS_LIST])
return FALSE
- if (!(target in living_mob.ai_controller.blackboard[BB_FRIENDS_LIST]))
- // We don't have any friends, anything's fair game
- // OR This is not our friend, fire at will
- return TRUE
+ return ..()
+///friends dont care about factions
+/datum/targetting_datum/basic/not_friends/faction_check(mob/living/living_mob, mob/living/the_target)
return FALSE
-/datum/targetting_datum/not_friends/attack_closed_turfs
+/datum/targetting_datum/basic/not_friends/attack_closed_turfs
attack_closed_turf = TRUE
/// Subtype that allows us to target items while deftly avoiding attacking our allies. Be careful when it comes to targetting items as an AI could get trapped targetting something it can't destroy.
diff --git a/code/datums/ai/dog/dog_controller.dm b/code/datums/ai/dog/dog_controller.dm
index 6883642b68918..a95e3d57b6b20 100644
--- a/code/datums/ai/dog/dog_controller.dm
+++ b/code/datums/ai/dog/dog_controller.dm
@@ -2,7 +2,7 @@
blackboard = list(
BB_DOG_HARASS_HARM = TRUE,
BB_VISION_RANGE = AI_DOG_VISION_RANGE,
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends(),
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_dog
@@ -19,7 +19,7 @@
blackboard = list(
BB_DOG_HARASS_HARM = TRUE,
BB_VISION_RANGE = AI_DOG_VISION_RANGE,
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends(),
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends,
// Find nearby mobs with tongs in hand.
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/holding_object(/obj/item/kitchen/tongs),
BB_BABIES_PARTNER_TYPES = list(/mob/living/basic/pet/dog),
@@ -42,4 +42,4 @@
if(!istype(corgi_pawn))
return
- return corgi_pawn.access_card
+ return corgi_pawn.access_card.GetAccess()
diff --git a/code/datums/ai/generic/find_and_set.dm b/code/datums/ai/generic/find_and_set.dm
index dc941ec33fae1..8ecc6df7cfbbb 100644
--- a/code/datums/ai/generic/find_and_set.dm
+++ b/code/datums/ai/generic/find_and_set.dm
@@ -129,9 +129,26 @@
continue
if (living_pawn.see_invisible < dead_pal.invisibility)
continue
- if (!living_pawn.faction_check_mob(dead_pal))
+ if (!living_pawn.faction_check_atom(dead_pal))
continue
nearby_bodies += dead_pal
if (nearby_bodies.len)
return pick(nearby_bodies)
+
+/**
+ * A variant that looks for a human who is not dead or incapacitated, and has a mind
+ */
+/datum/ai_behavior/find_and_set/conscious_person
+
+/datum/ai_behavior/find_and_set/conscious_person/search_tactic(datum/ai_controller/controller, locate_path, search_range)
+ var/list/customers = list()
+ for(var/mob/living/carbon/human/target in oview(search_range, controller.pawn))
+ if(IS_DEAD_OR_INCAP(target) || !target.mind)
+ continue
+ customers += target
+
+ if(customers.len)
+ return pick(customers)
+
+ return null
diff --git a/code/datums/ai/idle_behaviors/idle_random_walk.dm b/code/datums/ai/idle_behaviors/idle_random_walk.dm
index d99957f419bb1..8924da5c30bb0 100644
--- a/code/datums/ai/idle_behaviors/idle_random_walk.dm
+++ b/code/datums/ai/idle_behaviors/idle_random_walk.dm
@@ -10,7 +10,10 @@
if(SPT_PROB(walk_chance, seconds_per_tick) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !living_pawn.pulledby)
var/move_dir = pick(GLOB.alldirs)
- living_pawn.Move(get_step(living_pawn, move_dir), move_dir)
+ var/turf/destination_turf = get_step(living_pawn, move_dir)
+ if(!destination_turf?.can_cross_safely(living_pawn))
+ return
+ living_pawn.Move(destination_turf, move_dir)
/datum/idle_behavior/idle_random_walk/less_walking
walk_chance = 10
@@ -25,6 +28,20 @@
return
return ..()
+/// Only walk if we are not on the target's location
+/datum/idle_behavior/idle_random_walk/not_while_on_target
+ ///What is the spot we have to stand on?
+ var/target_key
+
+/datum/idle_behavior/idle_random_walk/not_while_on_target/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller)
+ var/atom/target = controller.blackboard[target_key]
+
+ //Don't move, if we are are already standing on it
+ if(!QDELETED(target) && ((isturf(target) && controller.pawn.loc == target) || (target.loc == controller.pawn.loc)))
+ return
+
+ return ..()
+
/// walk randomly however stick near a target
/datum/idle_behavior/walk_near_target
/// chance to walk
@@ -55,7 +72,7 @@
var/turf/possible_step = get_step(living_pawn, direction)
if(get_dist(possible_step, target) > minimum_distance)
continue
- if(possible_step.is_blocked_turf())
+ if(possible_step.is_blocked_turf() || !possible_step.can_cross_safely(living_pawn))
continue
possible_turfs += possible_step
diff --git a/code/datums/ai/movement/ai_movement_basic_avoidance.dm b/code/datums/ai/movement/ai_movement_basic_avoidance.dm
index 371fb9dbc4bdf..ad5b51a0ca372 100644
--- a/code/datums/ai/movement/ai_movement_basic_avoidance.dm
+++ b/code/datums/ai/movement/ai_movement_basic_avoidance.dm
@@ -17,8 +17,8 @@
. = ..()
var/turf/target_turf = get_step_towards(source.moving, source.target)
- if(is_type_in_typecache(target_turf, GLOB.dangerous_turfs))
- . = FALSE
+ if(!target_turf?.can_cross_safely(source.moving))
+ . = MOVELOOP_SKIP_STEP
return .
/// Move immediately and don't update our facing
diff --git a/code/datums/ai/movement/ai_movement_dumb.dm b/code/datums/ai/movement/ai_movement_dumb.dm
index a38024ff2cc8d..041f1a967c2ad 100644
--- a/code/datums/ai/movement/ai_movement_dumb.dm
+++ b/code/datums/ai/movement/ai_movement_dumb.dm
@@ -15,6 +15,6 @@
. = ..()
var/turf/target_turf = get_step_towards(source.moving, source.target)
- if(is_type_in_typecache(target_turf, GLOB.dangerous_turfs))
- . = FALSE
+ if(!target_turf?.can_cross_safely(source.moving))
+ . = MOVELOOP_SKIP_STEP
return .
diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm
index da46735ec363d..1508339913300 100644
--- a/code/datums/ai/movement/ai_movement_jps.dm
+++ b/code/datums/ai/movement/ai_movement_jps.dm
@@ -15,7 +15,7 @@
repath_delay = 0.5 SECONDS,
max_path_length = AI_MAX_PATH_LENGTH,
minimum_distance = controller.get_minimum_distance(),
- id = controller.get_access(),
+ access = controller.get_access(),
subsystem = SSai_movement,
extra_info = controller,
)
@@ -28,5 +28,5 @@
SIGNAL_HANDLER
var/datum/ai_controller/controller = source.extra_info
- source.id = controller.get_access()
+ source.access = controller.get_access()
source.minimum_distance = controller.get_minimum_distance()
diff --git a/code/datums/ai/objects/mod.dm b/code/datums/ai/objects/mod.dm
index ff3a8c6d16972..2bb555d281bf7 100644
--- a/code/datums/ai/objects/mod.dm
+++ b/code/datums/ai/objects/mod.dm
@@ -28,7 +28,7 @@
queue_behavior(/datum/ai_behavior/mod_attach)
/datum/ai_controller/mod/get_access()
- return id_card
+ return id_card.GetAccess()
/datum/ai_behavior/mod_attach
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT|AI_BEHAVIOR_MOVE_AND_PERFORM
diff --git a/code/datums/ai/oldhostile/hostile_tameable.dm b/code/datums/ai/oldhostile/hostile_tameable.dm
index d75eb3087317d..5c96eca17da43 100644
--- a/code/datums/ai/oldhostile/hostile_tameable.dm
+++ b/code/datums/ai/oldhostile/hostile_tameable.dm
@@ -62,7 +62,7 @@
if(!istype(simple_pawn))
return
- return simple_pawn.access_card
+ return simple_pawn.access_card.GetAccess()
/datum/ai_controller/hostile_friend/proc/on_ridden_driver_move(atom/movable/movable_parent, mob/living/user, direction)
SIGNAL_HANDLER
diff --git a/code/datums/brain_damage/creepy_trauma.dm b/code/datums/brain_damage/creepy_trauma.dm
index fb93c45f398c8..8da6a8b3c0a0c 100644
--- a/code/datums/brain_damage/creepy_trauma.dm
+++ b/code/datums/brain_damage/creepy_trauma.dm
@@ -67,6 +67,7 @@
/datum/brain_trauma/special/obsessed/on_lose()
..()
owner.mind.remove_antag_datum(/datum/antagonist/obsessed)
+ owner.clear_mood_event("creeping")
if(obsession)
UnregisterSignal(obsession, COMSIG_MOB_EYECONTACT)
diff --git a/code/datums/brain_damage/imaginary_friend.dm b/code/datums/brain_damage/imaginary_friend.dm
index fc230632bdd2e..61175593ce4d7 100644
--- a/code/datums/brain_damage/imaginary_friend.dm
+++ b/code/datums/brain_damage/imaginary_friend.dm
@@ -45,15 +45,26 @@
/datum/brain_trauma/special/imaginary_friend/proc/make_friend()
friend = new(get_turf(owner), owner)
+/// Tries an orbit poll for the imaginary friend
/datum/brain_trauma/special/imaginary_friend/proc/get_ghost()
- set waitfor = FALSE
- var/list/mob/dead/observer/candidates = poll_candidates_for_mob("Do you want to play as [owner.real_name]'s imaginary friend?", ROLE_PAI, null, 7.5 SECONDS, friend, POLL_IGNORE_IMAGINARYFRIEND)
- if(LAZYLEN(candidates))
- var/mob/dead/observer/C = pick(candidates)
- friend.key = C.key
- friend_initialized = TRUE
- else
+ var/datum/callback/to_call = CALLBACK(src, PROC_REF(add_friend))
+ owner.AddComponent(/datum/component/orbit_poll, \
+ ignore_key = POLL_IGNORE_IMAGINARYFRIEND, \
+ job_bans = ROLE_PAI, \
+ title = "[owner.real_name]'s imaginary friend", \
+ to_call = to_call, \
+ )
+
+/// Yay more friends!
+/datum/brain_trauma/special/imaginary_friend/proc/add_friend(mob/dead/observer/ghost)
+ if(isnull(ghost))
qdel(src)
+ return
+
+ friend.key = ghost.key
+ friend_initialized = TRUE
+ friend.log_message("became [key_name(owner)]'s split personality.", LOG_GAME)
+ message_admins("[ADMIN_LOOKUPFLW(friend)] became [ADMIN_LOOKUPFLW(owner)]'s split personality.")
/mob/camera/imaginary_friend
name = "imaginary friend"
@@ -72,8 +83,7 @@
var/mob/living/owner
var/bubble_icon = "default"
- var/datum/action/innate/imaginary_join/join
- var/datum/action/innate/imaginary_hide/hide
+
/mob/camera/imaginary_friend/Login()
. = ..()
@@ -94,10 +104,11 @@
*/
/mob/camera/imaginary_friend/Initialize(mapload)
. = ..()
- join = new
- join.Grant(src)
- hide = new
- hide.Grant(src)
+ var/static/list/grantable_actions = list(
+ /datum/action/innate/imaginary_join,
+ /datum/action/innate/imaginary_hide,
+ )
+ grant_actions_by_list(grantable_actions)
/// Links this imaginary friend to the provided mob
/mob/camera/imaginary_friend/proc/attach_to_owner(mob/living/imaginary_friend_owner)
diff --git a/code/datums/brain_damage/split_personality.dm b/code/datums/brain_damage/split_personality.dm
index f2120505f72f4..92992865e4480 100644
--- a/code/datums/brain_damage/split_personality.dm
+++ b/code/datums/brain_damage/split_personality.dm
@@ -32,17 +32,26 @@
var/datum/action/cooldown/spell/personality_commune/owner_spell = new(src)
owner_spell.Grant(owner_backseat)
-
+/// Attempts to get a ghost to play the personality
/datum/brain_trauma/severe/split_personality/proc/get_ghost()
- set waitfor = FALSE
- var/list/mob/dead/observer/candidates = poll_candidates_for_mob("Do you want to play as [owner.real_name]'s [poll_role]?", ROLE_PAI, null, 7.5 SECONDS, stranger_backseat, POLL_IGNORE_SPLITPERSONALITY)
- if(LAZYLEN(candidates))
- var/mob/dead/observer/C = pick(candidates)
- stranger_backseat.key = C.key
- stranger_backseat.log_message("became [key_name(owner)]'s split personality.", LOG_GAME)
- message_admins("[ADMIN_LOOKUPFLW(stranger_backseat)] became [ADMIN_LOOKUPFLW(owner)]'s split personality.")
- else
+ var/datum/callback/to_call = CALLBACK(src, PROC_REF(schism))
+ owner.AddComponent(/datum/component/orbit_poll, \
+ ignore_key = POLL_IGNORE_SPLITPERSONALITY, \
+ job_bans = ROLE_PAI, \
+ title = "[owner.real_name]'s [poll_role]", \
+ to_call = to_call, \
+ )
+
+/// Ghost poll has concluded
+/datum/brain_trauma/severe/split_personality/proc/schism(mob/dead/observer/ghost)
+ if(isnull(ghost))
qdel(src)
+ return
+
+ stranger_backseat.key = ghost.key
+ stranger_backseat.log_message("became [key_name(owner)]'s split personality.", LOG_GAME)
+ message_admins("[ADMIN_LOOKUPFLW(stranger_backseat)] became [ADMIN_LOOKUPFLW(owner)]'s split personality.")
+
/datum/brain_trauma/severe/split_personality/on_life(seconds_per_tick, times_fired)
if(owner.stat == DEAD)
@@ -247,12 +256,20 @@
gain_text = span_warning("Crap, that was one drink too many. You black out...")
lose_text = "You wake up very, very confused and hungover. All you can remember is drinking a lot of alcohol... what happened?"
poll_role = "blacked out drunkard"
+ random_gain = FALSE
/// Duration of effect, tracked in seconds, not deciseconds. qdels when reaching 0.
var/duration_in_seconds = 180
/datum/brain_trauma/severe/split_personality/blackout/on_gain()
. = ..()
RegisterSignal(owner, COMSIG_ATOM_SPLASHED, PROC_REF(on_splashed))
+ notify_ghosts(
+ "[owner] is blacking out!",
+ source = owner,
+ action = NOTIFY_ORBIT,
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ header = "Bro I'm not even drunk right now",
+ )
/datum/brain_trauma/severe/split_personality/blackout/on_lose()
. = ..()
@@ -265,7 +282,9 @@
qdel(src)
/datum/brain_trauma/severe/split_personality/blackout/on_life(seconds_per_tick, times_fired)
- if(current_controller == OWNER)
+ if(current_controller == OWNER && stranger_backseat)//we should only start transitioning after the other personality has entered
+ owner.overlay_fullscreen("fade_to_black", /atom/movable/screen/fullscreen/blind)
+ owner.clear_fullscreen("fade_to_black", animated = 4 SECONDS)
switch_personalities()
if(owner.stat == DEAD)
if(current_controller != OWNER)
@@ -275,6 +294,18 @@
if(duration_in_seconds <= 0)
qdel(src)
return
+ else if(duration_in_seconds <= 50)
+ to_chat(owner, span_warning("You have 50 seconds left before sobering up!"))
+ if(prob(10) && !HAS_TRAIT(owner, TRAIT_DISCOORDINATED_TOOL_USER))
+ ADD_TRAIT(owner, TRAIT_DISCOORDINATED_TOOL_USER, TRAUMA_TRAIT)
+ owner.balloon_alert(owner, "dexterity reduced temporarily!")
+ //We then send a callback to automatically re-add the trait
+ addtimer(TRAIT_CALLBACK_REMOVE(owner, TRAIT_DISCOORDINATED_TOOL_USER, TRAUMA_TRAIT), 10 SECONDS)
+ addtimer(CALLBACK(owner, TYPE_PROC_REF(/atom, balloon_alert), owner, "dexterity regained!"), 10 SECONDS)
+ if(prob(15))
+ playsound(owner,'sound/effects/sf_hiccup_male_01.ogg', 50)
+ owner.emote("hiccup")
+ owner.adjustStaminaLoss(-5) //too drunk to feel anything
duration_in_seconds -= seconds_per_tick
/mob/living/split_personality/blackout
diff --git a/code/datums/components/appearance_on_aggro.dm b/code/datums/components/appearance_on_aggro.dm
index 33a3d7c2e90d6..8c0df88e6fdbc 100644
--- a/code/datums/components/appearance_on_aggro.dm
+++ b/code/datums/components/appearance_on_aggro.dm
@@ -45,7 +45,6 @@
return
current_target = target
- RegisterSignal(target, COMSIG_QDELETING, PROC_REF(on_clear_target))
if (!isnull(aggro_overlay) || !isnull(aggro_state))
source.update_appearance(UPDATE_ICON)
if (!isnull(alpha_on_aggro))
@@ -61,7 +60,6 @@
revert_appearance(parent)
/datum/component/appearance_on_aggro/proc/revert_appearance(mob/living/source)
- UnregisterSignal(current_target, COMSIG_QDELETING)
current_target = null
if (!isnull(aggro_overlay) || !isnull(aggro_state))
source.update_appearance(UPDATE_ICON)
diff --git a/code/datums/components/bloody_spreader.dm b/code/datums/components/bloody_spreader.dm
new file mode 100644
index 0000000000000..951136c890c01
--- /dev/null
+++ b/code/datums/components/bloody_spreader.dm
@@ -0,0 +1,45 @@
+/datum/component/bloody_spreader
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ // How many bloodening instances are left. Deleted on zero.
+ var/blood_left
+ // We will spread this blood DNA to targets!
+ var/list/blood_dna
+ // Blood splashed around everywhere will carry these diseases. Oh no...
+ var/list/diseases
+
+/datum/component/bloody_spreader/Initialize(blood_left, list/blood_dna, list/diseases)
+ if(!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+ var/list/signals_to_add = list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_BLOB_ACT, COMSIG_ATOM_HULK_ATTACK, COMSIG_ATOM_ATTACKBY)
+ if(ismovable(parent))
+ signals_to_add += list(COMSIG_MOVABLE_BUMP, COMSIG_MOVABLE_IMPACT)
+ if(isitem(parent))
+ signals_to_add += list(COMSIG_ITEM_ATTACK, COMSIG_ITEM_ATTACK_ATOM, COMSIG_ITEM_HIT_REACT, COMSIG_ITEM_ATTACK_SELF, COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED)
+ var/atom/atom_parent = parent
+ if(atom_parent.atom_storage)
+ signals_to_add += list(COMSIG_STORAGE_STORED_ITEM)
+ else if(isstructure(parent))
+ signals_to_add += list(COMSIG_ATOM_ATTACK_HAND)
+
+ RegisterSignals(parent, signals_to_add, PROC_REF(spread_yucky_blood))
+
+ if(isclothing(parent))
+ parent.AddComponent(/datum/component/bloodysoles)
+
+ src.blood_left = blood_left
+ src.blood_dna = blood_dna
+ src.diseases = diseases
+
+/datum/component/bloody_spreader/proc/spread_yucky_blood(atom/parent, atom/bloody_fool)
+ SIGNAL_HANDLER
+ bloody_fool.add_blood_DNA(blood_dna, diseases)
+
+/datum/component/bloody_spreader/InheritComponent(/datum/component/new_comp, i_am_original, blood_left = 0)
+
+ if(!i_am_original)
+ return
+
+ if(src.blood_left >= INFINITY)
+ return
+
+ src.blood_left += blood_left
diff --git a/code/datums/components/bumpattack.dm b/code/datums/components/bumpattack.dm
index ec8cd272a4462..a305b43c25bb7 100644
--- a/code/datums/components/bumpattack.dm
+++ b/code/datums/components/bumpattack.dm
@@ -64,7 +64,7 @@
var/obj/item/our_weapon = proxy_weapon || parent
if(!istype(our_weapon))
CRASH("[our_weapon] somehow failed istype")
- if(!TIMER_COOLDOWN_CHECK(src, COOLDOWN_BUMP_ATTACK))
+ if(TIMER_COOLDOWN_FINISHED(src, COOLDOWN_BUMP_ATTACK))
TIMER_COOLDOWN_START(src, COOLDOWN_BUMP_ATTACK, attack_cooldown)
INVOKE_ASYNC(target, TYPE_PROC_REF(/atom, attackby), our_weapon, bumper)
bumper.visible_message(span_danger("[bumper] charges into [target], attacking with [our_weapon]!"), span_danger("You charge into [target], attacking with [our_weapon]!"), vision_distance = COMBAT_MESSAGE_RANGE)
diff --git a/code/datums/components/butchering.dm b/code/datums/components/butchering.dm
index 689de30db378a..183203ed709f5 100644
--- a/code/datums/components/butchering.dm
+++ b/code/datums/components/butchering.dm
@@ -257,16 +257,16 @@
if(!(slot & source.slot_flags))
return
butchering_enabled = TRUE
- RegisterSignal(user, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, PROC_REF(butcher_target))
+ RegisterSignal(user, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(butcher_target))
///Same as disable_butchering but for worn items
/datum/component/butchering/wearable/proc/worn_disable_butchering(obj/item/source, mob/user)
SIGNAL_HANDLER
butchering_enabled = FALSE
- UnregisterSignal(user, COMSIG_HUMAN_EARLY_UNARMED_ATTACK)
+ UnregisterSignal(user, COMSIG_LIVING_UNARMED_ATTACK)
/datum/component/butchering/wearable/proc/butcher_target(mob/user, atom/target, proximity)
SIGNAL_HANDLER
if(!isliving(target))
- return
- onItemAttack(parent, target, user)
+ return NONE
+ return onItemAttack(parent, target, user)
diff --git a/code/datums/components/crafting/crafting.dm b/code/datums/components/crafting/crafting.dm
index 9f17b3d1d7e66..79f101cb74c1c 100644
--- a/code/datums/components/crafting/crafting.dm
+++ b/code/datums/components/crafting/crafting.dm
@@ -108,7 +108,6 @@
continue
. += AM
-
/datum/component/personal_crafting/proc/get_surroundings(atom/a, list/blacklist=null)
. = list()
.["tool_behaviour"] = list()
@@ -123,14 +122,15 @@
if(isstack(item))
var/obj/item/stack/stack = item
.["other"][item.type] += stack.amount
- else if(is_reagent_container(item) && item.is_drainable() && length(item.reagents.reagent_list)) //some container that has some reagents inside it that can be drained
- var/obj/item/reagent_containers/container = item
- for(var/datum/reagent/reagent as anything in container.reagents.reagent_list)
- .["other"][reagent.type] += reagent.volume
- else //a reagent container that is empty can also be used as a tool. e.g. glass bottle can be used as a rolling pin
- if(item.tool_behaviour)
- .["tool_behaviour"] += item.tool_behaviour
+ else
.["other"][item.type] += 1
+ if(is_reagent_container(item) && item.is_drainable() && length(item.reagents.reagent_list)) //some container that has some reagents inside it that can be drained
+ var/obj/item/reagent_containers/container = item
+ for(var/datum/reagent/reagent as anything in container.reagents.reagent_list)
+ .["other"][reagent.type] += reagent.volume
+ else //a reagent container that is empty can also be used as a tool. e.g. glass bottle can be used as a rolling pin
+ if(item.tool_behaviour)
+ .["tool_behaviour"] += item.tool_behaviour
else if (ismachinery(object))
LAZYADDASSOCLIST(.["machinery"], object.type, object)
else if (isstructure(object))
diff --git a/code/datums/components/crafting/slapcrafting.dm b/code/datums/components/crafting/slapcrafting.dm
index 32a901dc73e60..e08fa3ad6c6ab 100644
--- a/code/datums/components/crafting/slapcrafting.dm
+++ b/code/datums/components/crafting/slapcrafting.dm
@@ -172,16 +172,20 @@
var/amount = initial(cur_recipe.reqs[reagent_ingredient])
string_ingredient_list += "[amount] unit[amount > 1 ? "s" : ""] of [initial(reagent_ingredient.name)]\n"
- // Redundant!
- if(parent.type == valid_type)
- continue
var/atom/ingredient = valid_type
var/amount = initial(cur_recipe.reqs[ingredient])
+
+ // If we're about to describe the ingredient that the component is based on, lower the described amount by 1 or remove it outright.
+ if(parent.type == valid_type)
+ if(amount > 1)
+ amount--
+ else
+ continue
string_ingredient_list += "[amount > 1 ? ("[amount]" + " of") : "a"] [initial(ingredient.name)]\n"
// If we did find ingredients then add them onto the list.
if(length(string_ingredient_list))
- to_chat(user, span_boldnotice("Ingredients:"))
+ to_chat(user, span_boldnotice("Extra Ingredients:"))
to_chat(user, examine_block(span_notice(string_ingredient_list)))
var/list/tool_list = ""
diff --git a/code/datums/components/crafting/weapon_ammo.dm b/code/datums/components/crafting/weapon_ammo.dm
index 206adbaefb1c4..437bfaa2e9202 100644
--- a/code/datums/components/crafting/weapon_ammo.dm
+++ b/code/datums/components/crafting/weapon_ammo.dm
@@ -72,18 +72,6 @@
time = 1.2 SECONDS
category = CAT_WEAPON_AMMO
-/datum/crafting_recipe/laserslug
- name = "Scatter Laser Shell"
- result = /obj/item/ammo_casing/shotgun/laserslug
- reqs = list(
- /obj/item/ammo_casing/shotgun/techshell = 1,
- /obj/item/stock_parts/capacitor/adv = 1,
- /obj/item/stock_parts/micro_laser/high = 1,
- )
- tool_behaviors = list(TOOL_SCREWDRIVER)
- time = 0.5 SECONDS
- category = CAT_WEAPON_AMMO
-
/datum/crafting_recipe/trashball
name = "Trashball"
always_available = FALSE
diff --git a/code/datums/components/cult_ritual_item.dm b/code/datums/components/cult_ritual_item.dm
index 7565268cef6df..a155e904c7ff6 100644
--- a/code/datums/components/cult_ritual_item.dm
+++ b/code/datums/components/cult_ritual_item.dm
@@ -310,12 +310,23 @@
if(!initial(rune_to_scribe.no_scribe_boost) && (our_turf.type in turfs_that_boost_us))
scribe_mod *= 0.5
+ var/scribe_started = initial(rune_to_scribe.started_creating)
+ var/scribe_failed = initial(rune_to_scribe.failed_to_create)
+ if(scribe_started)
+ var/datum/callback/startup = CALLBACK(GLOBAL_PROC, scribe_started)
+ startup.Invoke()
+ var/datum/callback/failed
+ if(scribe_failed)
+ failed = CALLBACK(GLOBAL_PROC, scribe_failed)
+
SEND_SOUND(cultist, sound('sound/weapons/slice.ogg', 0, 1, 10))
if(!do_after(cultist, scribe_mod, target = get_turf(cultist), timed_action_flags = IGNORE_SLOWDOWNS))
cleanup_shields()
+ failed?.Invoke()
return FALSE
if(!can_scribe_rune(tool, cultist))
cleanup_shields()
+ failed?.Invoke()
return FALSE
cultist.visible_message(
@@ -357,10 +368,23 @@
if(!check_if_in_ritual_site(cultist, cult_team))
return FALSE
var/area/summon_location = get_area(cultist)
- priority_announce("Figments from an eldritch god are being summoned by [cultist.real_name] into [summon_location.get_original_area_name()] from an unknown dimension. Disrupt the ritual at all costs!", "Central Command Higher Dimensional Affairs", sound = 'sound/ambience/antag/bloodcult/bloodcult_scribe.ogg', has_important_message = TRUE)
+ priority_announce(
+ text = "Figments from an eldritch god are being summoned by [cultist.real_name] into [summon_location.get_original_area_name()] from an unknown dimension. Disrupt the ritual at all costs!",
+ sound = 'sound/ambience/antag/bloodcult/bloodcult_scribe.ogg',
+ sender_override = "[command_name()] Higher Dimensional Affairs",
+ has_important_message = TRUE,
+ )
for(var/shielded_turf in spiral_range_turfs(1, cultist, 1))
LAZYADD(shields, new /obj/structure/emergency_shield/cult/narsie(shielded_turf))
+ notify_ghosts(
+ "[cultist] has begun scribing a Nar'Sie rune!",
+ source = cultist,
+ action = NOTIFY_ORBIT,
+ header = "Maranax Infirmux!",
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ )
+
return TRUE
/*
diff --git a/code/datums/components/deadchat_control.dm b/code/datums/components/deadchat_control.dm
index 48096e4767064..2a00b2f955dc6 100644
--- a/code/datums/components/deadchat_control.dm
+++ b/code/datums/components/deadchat_control.dm
@@ -39,7 +39,12 @@
if(deadchat_mode & ANARCHY_MODE) // Choose one, please.
stack_trace("deadchat_control component added to [parent.type] with both democracy and anarchy modes enabled.")
timerid = addtimer(CALLBACK(src, PROC_REF(democracy_loop)), input_cooldown, TIMER_STOPPABLE | TIMER_LOOP)
- notify_ghosts("[parent] is now deadchat controllable!", source = parent, action = NOTIFY_ORBIT, header="Something Interesting!")
+ notify_ghosts(
+ "[parent] is now deadchat controllable!",
+ source = parent,
+ action = NOTIFY_ORBIT,
+ header = "Something Interesting!",
+ )
if(!ismob(parent) && !SSpoints_of_interest.is_valid_poi(parent))
SSpoints_of_interest.make_point_of_interest(parent)
generated_point_of_interest = TRUE
diff --git a/code/datums/components/deployable.dm b/code/datums/components/deployable.dm
index e2c6b84703e46..14fc6747bd6dd 100644
--- a/code/datums/components/deployable.dm
+++ b/code/datums/components/deployable.dm
@@ -5,7 +5,8 @@
* If attaching this to something:
* Set deploy_time to a number in seconds for the deploy delay
* Set thing_to_be_deployed to an obj path for the thing that gets spawned
- * Lastly, set delete_on_use to TRUE or FALSE if you want the object you're deploying with to get deleted when used
+ * Multiple deployments and deployments work together to allow a thing to be placed down several times. If multiple deployments is false then don't worry about deployments
+ * Direction setting true means the object spawned will face the direction of the person who deployed it, false goes to the default direction
*/
/datum/component/deployable
@@ -13,23 +14,29 @@
var/deploy_time
/// The object that gets spawned if deployed successfully
var/obj/thing_to_be_deployed
- /// If the item used to deploy gets deleted on use or not
- var/delete_on_use
+ /// Can the parent be deployed multiple times
+ var/multiple_deployments
+ /// How many times we can deploy the parent, if multiple deployments is set to true and this gets below zero, the parent will be deleted
+ var/deployments
/// If the component adds a little bit into the parent's description
var/add_description_hint
+ /// If the direction of the thing we place is changed upon placing
+ var/direction_setting
/// Used in getting the name of the deployed object
var/deployed_name
-/datum/component/deployable/Initialize(deploy_time = 5 SECONDS, thing_to_be_deployed, delete_on_use = TRUE, add_description_hint = TRUE)
+/datum/component/deployable/Initialize(deploy_time = 5 SECONDS, thing_to_be_deployed, multiple_deployments = FALSE, deployments = 1, add_description_hint = TRUE, direction_setting = TRUE)
. = ..()
if(!isitem(parent))
return COMPONENT_INCOMPATIBLE
src.deploy_time = deploy_time
src.thing_to_be_deployed = thing_to_be_deployed
- src.delete_on_use = delete_on_use
src.add_description_hint = add_description_hint
+ src.direction_setting = direction_setting
+ src.deployments = deployments
+ src.multiple_deployments = multiple_deployments
if(add_description_hint)
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(examine))
@@ -40,23 +47,23 @@
/datum/component/deployable/proc/examine(datum/source, mob/user, list/examine_list)
SIGNAL_HANDLER
-
- examine_list += span_notice("[source.p_they()] look[source.p_s()] like [source.p_they()] can be deployed into \a [deployed_name].")
+ examine_list += span_notice("It can be used in hand to deploy into [((deployments > 1) && multiple_deployments) ? "[deployments]" : "a"] [deployed_name].")
/datum/component/deployable/proc/on_attack_hand(datum/source, mob/user, location, direction)
SIGNAL_HANDLER
INVOKE_ASYNC(src, PROC_REF(deploy), source, user, location, direction)
/datum/component/deployable/proc/deploy(obj/source, mob/user, location, direction) //If there's no user, location and direction are used
- var/obj/deployed_object //Used for spawning the deployed object
- var/turf/deploy_location //Where our deployed_object gets put
- var/new_direction //What direction do we want our deployed object in
- if(user)
- if(!ishuman(user))
- return
+ // The object we are going to create
+ var/atom/deployed_object
+ // The turf our object is going to be deployed to
+ var/turf/deploy_location
+ // What direction will the deployed object be placed facing
+ var/new_direction
+ if(user)
deploy_location = get_step(user, user.dir) //Gets spawn location for thing_to_be_deployed if there is a user
- if(deploy_location.is_blocked_turf(TRUE))
+ if(deploy_location.is_blocked_turf(TRUE, parent))
source.balloon_alert(user, "insufficient room to deploy here.")
return
new_direction = user.dir //Gets the direction for thing_to_be_deployed if there is a user
@@ -64,16 +71,18 @@
playsound(source, 'sound/items/ratchet.ogg', 50, TRUE)
if(!do_after(user, deploy_time))
return
- else //If there is for some reason no user, then the location and direction are set here
+ else // If there is for some reason no user, then the location and direction are set here
deploy_location = location
new_direction = direction
deployed_object = new thing_to_be_deployed(deploy_location)
deployed_object.setDir(new_direction)
- //Sets the integrity of the new deployed machine to that of the object it came from
- deployed_object.modify_max_integrity(source.max_integrity)
- deployed_object.update_icon_state()
+ // Sets the direction of the resulting object if the variable says to
+ if(direction_setting)
+ deployed_object.update_icon_state()
+
+ deployments -= 1
- if(delete_on_use)
+ if(!multiple_deployments || deployments < 1)
qdel(source)
diff --git a/code/datums/components/energized.dm b/code/datums/components/energized.dm
new file mode 100644
index 0000000000000..41262e23efc6a
--- /dev/null
+++ b/code/datums/components/energized.dm
@@ -0,0 +1,119 @@
+/datum/component/energized
+ can_transfer = FALSE
+ ///what we give to connect_loc by default, makes slippable mobs moving over us slip
+ var/static/list/default_connections = list(
+ COMSIG_ATOM_ENTERED = PROC_REF(toast),
+ )
+ /// Inbound station
+ var/inbound
+ /// Outbound station
+ var/outbound
+ /// Transport ID of the tram
+ var/specific_transport_id = TRAMSTATION_LINE_1
+ /// Weakref to the tram
+ var/datum/weakref/transport_ref
+
+/datum/component/energized/Initialize(plate_inbound, plate_outbound, plate_transport_id)
+ . = ..()
+
+ if(isnull(plate_inbound))
+ return
+
+ inbound = plate_inbound
+ if(isnull(plate_outbound))
+ return
+
+ outbound = plate_outbound
+ if(isnull(plate_transport_id))
+ return
+
+ specific_transport_id = plate_transport_id
+ find_tram()
+
+/datum/component/energized/proc/find_tram()
+ for(var/datum/transport_controller/linear/transport as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
+ if(transport.specific_transport_id == specific_transport_id)
+ transport_ref = WEAKREF(transport)
+ break
+
+/datum/component/energized/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_ATOM_ENTERED, PROC_REF(toast))
+
+/datum/component/energized/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_ATOM_ENTERED)
+ return ..()
+
+
+/datum/component/energized/proc/toast(turf/open/floor/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ SIGNAL_HANDLER
+
+ if(!source.broken && !source.burnt)
+ return
+
+ if(!isliving(arrived))
+ return
+
+ if(prob(85))
+ if(prob(25))
+ do_sparks(1, FALSE, source)
+ playsound(src, SFX_SPARKS, 40, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ source.audible_message(span_danger("[parent] makes an electric crackle..."))
+ return
+
+ var/mob/living/future_tram_victim = arrived
+ var/datum/transport_controller/linear/tram/tram = transport_ref?.resolve()
+
+ // Check for stopped states.
+ if(isnull(tram) || !tram.controller_operational || !inbound || !outbound)
+ return FALSE
+
+ var/obj/structure/transport/linear/tram/tram_part = tram.return_closest_platform_to(parent)
+
+ if(QDELETED(tram_part))
+ return FALSE
+
+ if(isnull(source))
+ return FALSE
+
+ // Everything will be based on position and travel direction
+ var/plate_pos
+ var/tram_pos
+ var/tram_velocity_sign // 1 for positive axis movement, -1 for negative
+ // Try to be agnostic about N-S vs E-W movement
+ if(tram.travel_direction & (NORTH|SOUTH))
+ plate_pos = source.y
+ tram_pos = source.y
+ tram_velocity_sign = tram.travel_direction & NORTH ? 1 : -1
+ else
+ plate_pos = source.x
+ tram_pos = source.x
+ tram_velocity_sign = tram.travel_direction & EAST ? 1 : -1
+
+ // How far away are we? negative if already passed.
+ var/approach_distance = tram_velocity_sign * (plate_pos - (tram_pos + (DEFAULT_TRAM_LENGTH * 0.5)))
+
+ // Check if our victim is in the active path of the tram.
+ if(!tram.controller_active)
+ return FALSE
+ if(approach_distance < 0)
+ return FALSE
+ if((tram.travel_direction & WEST) && inbound < tram.destination_platform.platform_code)
+ return FALSE
+ if((tram.travel_direction & EAST) && outbound > tram.destination_platform.platform_code)
+ return FALSE
+ if(approach_distance >= AMBER_THRESHOLD_NORMAL)
+ return FALSE
+
+ // Finally the interesting part where they ACTUALLY get hit!
+ notify_ghosts(
+ "[future_tram_victim] has fallen in the path of an oncoming tram!",
+ source = future_tram_victim,
+ action = NOTIFY_ORBIT,
+ header = "Electrifying!",
+ )
+ playsound(src, SFX_SPARKS, 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ source.audible_message(span_danger("[parent] makes a loud electric crackle!"))
+ to_chat(future_tram_victim, span_userdanger("You hear a loud electric crackle!"))
+ future_tram_victim.electrocute_act(15, src, 1)
+ return TRUE
diff --git a/code/datums/components/explodable.dm b/code/datums/components/explodable.dm
index 7ebb112d30537..3093c995ced90 100644
--- a/code/datums/components/explodable.dm
+++ b/code/datums/components/explodable.dm
@@ -91,13 +91,16 @@
detonate()
///Called when you attack a specific body part of the thing this is equipped on. Useful for exploding pants.
-/datum/component/explodable/proc/explodable_attack_zone(datum/source, damage, damagetype, def_zone)
+/datum/component/explodable/proc/explodable_attack_zone(datum/source, damage, damagetype, def_zone, ...)
SIGNAL_HANDLER
if(!def_zone)
return
if(damagetype != BURN) //Don't bother if it's not fire.
return
+ if(isbodypart(def_zone))
+ var/obj/item/bodypart/hitting = def_zone
+ def_zone = hitting.body_zone
if(!is_hitting_zone(def_zone)) //You didn't hit us! ha!
return
detonate()
diff --git a/code/datums/components/explode_on_attack.dm b/code/datums/components/explode_on_attack.dm
new file mode 100644
index 0000000000000..0560184f2ba0a
--- /dev/null
+++ b/code/datums/components/explode_on_attack.dm
@@ -0,0 +1,40 @@
+/**
+ * Bombs the user after an attack
+ */
+/datum/component/explode_on_attack
+ /// range of bomb impact
+ var/impact_range
+ /// should we be destroyed after the explosion?
+ var/destroy_on_explode
+ /// list of mobs we wont bomb on attack
+ var/list/mob_type_dont_bomb
+
+/datum/component/explode_on_attack/Initialize(impact_range = 1, destroy_on_explode = TRUE, list/mob_type_dont_bomb = list())
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.impact_range = impact_range
+ src.destroy_on_explode = destroy_on_explode
+ src.mob_type_dont_bomb = mob_type_dont_bomb
+
+/datum/component/explode_on_attack/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(bomb_target))
+
+/datum/component/explode_on_attack/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_HOSTILE_PRE_ATTACKINGTARGET)
+
+
+/datum/component/explode_on_attack/proc/bomb_target(mob/living/owner, atom/victim)
+ SIGNAL_HANDLER
+
+ if(!isliving(victim))
+ return
+
+ if(is_type_in_typecache(victim, mob_type_dont_bomb))
+ return
+
+ explosion(owner, light_impact_range = impact_range, explosion_cause = src)
+
+ if(destroy_on_explode && owner)
+ qdel(owner)
+ return COMPONENT_HOSTILE_NO_ATTACK
+
diff --git a/code/datums/components/fantasy/suffixes.dm b/code/datums/components/fantasy/suffixes.dm
index c8809efae491a..69d9d0a7ebbb9 100644
--- a/code/datums/components/fantasy/suffixes.dm
+++ b/code/datums/components/fantasy/suffixes.dm
@@ -103,30 +103,35 @@
. = ..()
// This is set up to be easy to add to these lists as I expect it will need modifications
var/static/list/possible_mobtypes
- if(!possible_mobtypes)
- // The base list of allowed mob/species types
- possible_mobtypes = zebra_typecacheof(list(
- /mob/living/simple_animal = TRUE,
- /mob/living/carbon = TRUE,
- /datum/species = TRUE,
- // Some types to remove them and their subtypes
- /mob/living/carbon/human/species = FALSE,
- /mob/living/simple_animal/hostile/asteroid/elite = FALSE,
- /mob/living/simple_animal/hostile/megafauna = FALSE,
- ))
- // Some particular types to disallow if they're too broad/abstract
- // Not in the above typecache generator because it includes subtypes and this doesn't.
- possible_mobtypes -= list(
- /mob/living/simple_animal/hostile,
+ if(isnull(possible_mobtypes))
+ possible_mobtypes = list()
+ var/list/mob_subtype_whitelist = list(
+ /mob/living/basic,
+ /mob/living/carbon,
+ /mob/living/simple_animal,
)
+ for(var/type in mob_subtype_whitelist)
+ possible_mobtypes += subtypesof(type)
- var/mob/picked_mobtype = pick(possible_mobtypes)
- // This works even with the species picks since we're only accessing the name
+ var/list/mob_subtype_blacklist = list(
+ /mob/living/simple_animal/hostile/asteroid/elite,
+ /mob/living/simple_animal/hostile/megafauna,
+ )
+ for(var/type in mob_subtype_blacklist)
+ possible_mobtypes -= subtypesof(type)
+
+ possible_mobtypes -= GLOB.abstract_mob_types
+ var/mob/picked_mobtype = pick(possible_mobtypes)
var/obj/item/master = comp.parent
- var/max_mobs = max(CEILING(comp.quality/2, 1), 1)
- var/spawn_delay = 300 - 30 * comp.quality
- comp.appliedComponents += master.AddComponent(/datum/component/summoning, list(picked_mobtype), 100, max_mobs, spawn_delay)
+ var/max_mobs = max(CEILING(comp.quality / 2, 1), 1)
+ var/spawn_delay = 30 SECONDS - (3 SECONDS * comp.quality)
+ comp.appliedComponents += master.AddComponent(\
+ /datum/component/summoning,\
+ mob_types = list(picked_mobtype),\
+ max_mobs = max_mobs,\
+ spawn_delay = spawn_delay,\
+ )
return "[newName] of [initial(picked_mobtype.name)] summoning"
/datum/fantasy_affix/shrapnel
diff --git a/code/datums/components/fishing_spot.dm b/code/datums/components/fishing_spot.dm
index f88c27a713530..05456380235af 100644
--- a/code/datums/components/fishing_spot.dm
+++ b/code/datums/components/fishing_spot.dm
@@ -36,7 +36,7 @@
if(HAS_TRAIT(user,TRAIT_GONE_FISHING) || rod.currently_hooked_item)
user.balloon_alert(user, "already fishing")
return COMPONENT_NO_AFTERATTACK
- var/denial_reason = fish_source.reason_we_cant_fish(rod, user)
+ var/denial_reason = fish_source.reason_we_cant_fish(rod, user, parent)
if(denial_reason)
to_chat(user, span_warning(denial_reason))
return COMPONENT_NO_AFTERATTACK
diff --git a/code/datums/components/focused_attacker.dm b/code/datums/components/focused_attacker.dm
index eda6bd1797912..8635973f2632e 100644
--- a/code/datums/components/focused_attacker.dm
+++ b/code/datums/components/focused_attacker.dm
@@ -24,12 +24,12 @@
/datum/component/focused_attacker/RegisterWithParent()
if (isliving(parent))
- RegisterSignals(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK), PROC_REF(pre_mob_attack))
+ RegisterSignal(parent, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(pre_mob_attack))
else
RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK, PROC_REF(pre_item_attack))
/datum/component/focused_attacker/UnregisterFromParent()
- UnregisterSignal(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_ITEM_PRE_ATTACK))
+ UnregisterSignal(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_ITEM_PRE_ATTACK))
/// Before a mob attacks, try increasing its attack power
/datum/component/focused_attacker/proc/pre_mob_attack(mob/living/attacker, atom/target)
diff --git a/code/datums/components/food/edible.dm b/code/datums/components/food/edible.dm
index 7c86ddc22437b..1e724f0e404aa 100644
--- a/code/datums/components/food/edible.dm
+++ b/code/datums/components/food/edible.dm
@@ -223,6 +223,8 @@ Behavior that's still missing from this component that original food items had t
examine_list += span_green("You find this meal [quality_label].")
else if (quality == 0)
examine_list += span_notice("You find this meal edible.")
+ else if (quality <= FOOD_QUALITY_DANGEROUS)
+ examine_list += span_warning("You may die from eating this meal.")
else if (quality <= TOXIC_FOOD_QUALITY_THRESHOLD)
examine_list += span_warning("You find this meal disgusting!")
else
@@ -485,19 +487,25 @@ Behavior that's still missing from this component that original food items had t
return TRUE
///Checks whether or not the eater can actually consume the food
-/datum/component/edible/proc/CanConsume(mob/living/eater, mob/living/feeder)
+/datum/component/edible/proc/CanConsume(mob/living/carbon/eater, mob/living/feeder)
if(!iscarbon(eater))
return FALSE
- var/mob/living/carbon/C = eater
- var/covered = ""
- if(C.is_mouth_covered(ITEM_SLOT_HEAD))
- covered = "headgear"
- else if(C.is_mouth_covered(ITEM_SLOT_MASK))
- covered = "mask"
- if(covered)
+ if(eater.is_mouth_covered())
eater.balloon_alert(feeder, "mouth is covered!")
return FALSE
- if(SEND_SIGNAL(eater, COMSIG_CARBON_ATTEMPT_EAT, parent) & COMSIG_CARBON_BLOCK_EAT)
+
+ var/atom/food = parent
+
+ if(food.flags_1 & HOLOGRAM_1)
+ if(eater == feeder)
+ to_chat(eater, span_notice("You try to take a bite out of [food], but it fades away!"))
+ else
+ to_chat(feeder, span_notice("You try to feed [eater] [food], but it fades away!"))
+
+ qdel(food)
+ return FALSE
+
+ if(SEND_SIGNAL(eater, COMSIG_CARBON_ATTEMPT_EAT, food) & COMSIG_CARBON_BLOCK_EAT)
return
return TRUE
@@ -526,34 +534,47 @@ Behavior that's still missing from this component that original food items had t
if(!ishuman(eater))
return FALSE
var/mob/living/carbon/human/gourmand = eater
+
+ if(istype(parent, /obj/item/food))
+ var/obj/item/food/food = parent
+ if(food.venue_value >= FOOD_PRICE_EXOTIC)
+ gourmand.add_mob_memory(/datum/memory/good_food, food = parent)
+
//Bruh this breakfast thing is cringe and shouldve been handled separately from food-types, remove this in the future (Actually, just kill foodtypes in general)
if((foodtypes & BREAKFAST) && world.time - SSticker.round_start_time < STOP_SERVING_BREAKFAST)
gourmand.add_mood_event("breakfast", /datum/mood_event/breakfast)
last_check_time = world.time
- var/food_quality = get_perceived_food_quality(gourmand, parent)
+ var/food_quality = get_perceived_food_quality(gourmand)
+ if(food_quality <= FOOD_QUALITY_DANGEROUS && (foodtypes & gourmand.get_allergic_foodtypes())) // Only cause anaphylaxis if we're ACTUALLY allergic, otherwise it just tastes horrible
+ if(gourmand.ForceContractDisease(new /datum/disease/anaphylaxis(), make_copy = FALSE, del_on_fail = TRUE))
+ to_chat(gourmand, span_warning("You feel your throat start to itch."))
+ gourmand.add_mood_event("allergic_food", /datum/mood_event/allergic_food)
+ return
+
if(food_quality <= TOXIC_FOOD_QUALITY_THRESHOLD)
to_chat(gourmand,span_warning("What the hell was that thing?!"))
gourmand.adjust_disgust(25 + 30 * fraction)
gourmand.add_mood_event("toxic_food", /datum/mood_event/disgusting_food)
- else if(food_quality < 0)
+ return
+
+ if(food_quality < 0)
to_chat(gourmand,span_notice("That didn't taste very good..."))
gourmand.adjust_disgust(11 + 15 * fraction)
gourmand.add_mood_event("gross_food", /datum/mood_event/gross_food)
- else if(food_quality > 0)
- food_quality = min(food_quality, FOOD_QUALITY_TOP)
- var/atom/owner = parent
- var/timeout_mod = owner.reagents.get_average_purity(/datum/reagent/consumable) * 2 // mood event duration is 100% at average purity of 50%
- var/event = GLOB.food_quality_events[food_quality]
- gourmand.add_mood_event("quality_food", event, timeout_mod)
- gourmand.adjust_disgust(-5 + -2 * food_quality * fraction)
- var/quality_label = GLOB.food_quality_description[food_quality]
- to_chat(gourmand, span_notice("That's \an [quality_label] meal."))
+ return
- if(istype(parent, /obj/item/food))
- var/obj/item/food/food = parent
- if(food.venue_value >= FOOD_PRICE_EXOTIC)
- gourmand.add_mob_memory(/datum/memory/good_food, food = parent)
+ if(food_quality == 0)
+ return // meh
+
+ food_quality = min(food_quality, FOOD_QUALITY_TOP)
+ var/atom/owner = parent
+ var/timeout_mod = owner.reagents.get_average_purity(/datum/reagent/consumable) * 2 // mood event duration is 100% at average purity of 50%
+ var/event = GLOB.food_quality_events[food_quality]
+ gourmand.add_mood_event("quality_food", event, timeout_mod)
+ gourmand.adjust_disgust(-5 + -2 * food_quality * fraction)
+ var/quality_label = GLOB.food_quality_description[food_quality]
+ to_chat(gourmand, span_notice("That's \an [quality_label] meal."))
/// Get the complexity of the crafted food
/datum/component/edible/proc/get_recipe_complexity()
@@ -580,8 +601,12 @@ Behavior that's still missing from this component that original food items had t
return DISLIKED_FOOD_QUALITY_CHANGE
if(FOOD_TOXIC)
return TOXIC_FOOD_QUALITY_THRESHOLD
+ if(FOOD_ALLERGIC)
+ return FOOD_QUALITY_DANGEROUS
if(ishuman(eater))
+ if(foodtypes & eater.get_allergic_foodtypes())
+ return FOOD_QUALITY_DANGEROUS
if(count_matching_foodtypes(foodtypes, eater.get_toxic_foodtypes())) //if the food is toxic, we don't care about anything else
return TOXIC_FOOD_QUALITY_THRESHOLD
if(HAS_TRAIT(eater, TRAIT_AGEUSIA)) //if you can't taste it, it doesn't taste good
@@ -617,24 +642,29 @@ Behavior that's still missing from this component that original food items had t
qdel(parent)
///Ability to feed food to puppers
-/datum/component/edible/proc/UseByAnimal(datum/source, mob/user)
+/datum/component/edible/proc/UseByAnimal(datum/source, mob/living/basic/pet/dog/doggy)
SIGNAL_HANDLER
- var/atom/owner = parent
+ if(!isdog(doggy))
+ return
- if(!isdog(user))
+ var/atom/food = parent
+
+ if(food.flags_1 & HOLOGRAM_1)
+ to_chat(doggy, span_notice("You try to take a bite out of [food], but it fades away!"))
+ qdel(food)
return
- var/mob/living/L = user
+
if(bitecount == 0 || prob(50))
- L.manual_emote("nibbles away at \the [parent].")
+ doggy.manual_emote("nibbles away at \the [food].")
bitecount++
. = COMPONENT_CANCEL_ATTACK_CHAIN
- L.taste(owner.reagents) // why should carbons get all the fun?
- if(bitecount >= 5)
- var/satisfaction_text = pick("burps from enjoyment.", "yaps for more!", "woofs twice.", "looks at the area where \the [parent] was.")
- L.manual_emote(satisfaction_text)
- qdel(parent)
+ doggy.taste(food.reagents) // why should carbons get all the fun?
+ if(bitecount >= 5)
+ var/satisfaction_text = pick("burps from enjoyment.", "yaps for more!", "woofs twice.", "looks at the area where \the [food] was.")
+ doggy.manual_emote(satisfaction_text)
+ qdel(food)
///Ability to feed food to puppers
/datum/component/edible/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
@@ -657,10 +687,16 @@ Behavior that's still missing from this component that original food items had t
/datum/component/edible/proc/on_ooze_eat(datum/source, mob/eater, edible_flags)
SIGNAL_HANDLER
+ var/atom/food = parent
+
+ if(food.flags_1 & HOLOGRAM_1)
+ to_chat(eater, span_notice("You try to take a bite out of [food], but it fades away!"))
+ qdel(food)
+ return COMPONENT_ATOM_EATEN
+
if(foodtypes & edible_flags)
- var/atom/eaten_food = parent
- eaten_food.reagents.trans_to(eater, eaten_food.reagents.total_volume, transferred_by = eater)
- eater.visible_message(span_warning("[src] eats [eaten_food]!"), span_notice("You eat [eaten_food]."))
+ food.reagents.trans_to(eater, food.reagents.total_volume, transferred_by = eater)
+ eater.visible_message(span_warning("[src] eats [food]!"), span_notice("You eat [food]."))
playsound(get_turf(eater),'sound/items/eatfood.ogg', rand(30,50), TRUE)
- qdel(eaten_food)
+ qdel(food)
return COMPONENT_ATOM_EATEN
diff --git a/code/datums/components/food/germ_sensitive.dm b/code/datums/components/food/germ_sensitive.dm
index 22ba793c1ce3a..d0acc49714ab5 100644
--- a/code/datums/components/food/germ_sensitive.dm
+++ b/code/datums/components/food/germ_sensitive.dm
@@ -25,7 +25,7 @@ GLOBAL_LIST_INIT(floor_diseases, list(
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(examine))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(handle_movement))
- RegisterSignal(parent, COMSIG_ATOM_WASHED, PROC_REF(wash)) //Wash germs off dirty things
+ RegisterSignal(parent, COMSIG_COMPONENT_CLEAN_ACT, PROC_REF(wash)) //Wash germs off dirty things
RegisterSignals(parent, list(
COMSIG_ITEM_DROPPED, //Dropped into the world
@@ -46,13 +46,13 @@ GLOBAL_LIST_INIT(floor_diseases, list(
/datum/component/germ_sensitive/UnregisterFromParent()
REMOVE_TRAIT(parent, TRAIT_GERM_SENSITIVE, REF(src))
UnregisterSignal(parent, list(
+ COMSIG_ATOM_ENTERED,
COMSIG_ATOM_EXAMINE,
- COMSIG_MOVABLE_MOVED,
- COMSIG_ATOM_WASHED,
- COMSIG_ITEM_DROPPED,
COMSIG_ATOM_EXITED,
+ COMSIG_COMPONENT_CLEAN_ACT,
+ COMSIG_ITEM_DROPPED,
COMSIG_ITEM_PICKUP,
- COMSIG_ATOM_ENTERED,
+ COMSIG_MOVABLE_MOVED,
))
/datum/component/germ_sensitive/Destroy()
@@ -117,8 +117,10 @@ GLOBAL_LIST_INIT(floor_diseases, list(
parent.AddComponent(/datum/component/infective, new random_disease, weak = TRUE)
/datum/component/germ_sensitive/proc/wash()
+ SIGNAL_HANDLER
if(infective)
infective = FALSE
qdel(parent.GetComponent(/datum/component/infective))
+ return COMPONENT_CLEANED
#undef GERM_EXPOSURE_DELAY
diff --git a/code/datums/components/food/ghost_edible.dm b/code/datums/components/food/ghost_edible.dm
index 25207800a7426..ff524ed7a1c71 100644
--- a/code/datums/components/food/ghost_edible.dm
+++ b/code/datums/components/food/ghost_edible.dm
@@ -23,7 +23,12 @@
src.bite_chance = bite_chance
src.minimum_scale = minimum_scale
initial_reagent_volume = atom_parent.reagents.total_volume
- notify_ghosts("[parent] is edible by ghosts!", source = parent, action = NOTIFY_ORBIT, header="Something Tasty!")
+ notify_ghosts(
+ "[parent] is edible by ghosts!",
+ source = parent,
+ action = NOTIFY_ORBIT,
+ header="Something Tasty!",
+ )
/datum/component/ghost_edible/RegisterWithParent()
START_PROCESSING(SSdcs, src)
diff --git a/code/datums/components/ground_sinking.dm b/code/datums/components/ground_sinking.dm
index 0fb5fb9eac620..9b70cb0ab6006 100644
--- a/code/datums/components/ground_sinking.dm
+++ b/code/datums/components/ground_sinking.dm
@@ -66,7 +66,7 @@
if(ground_sinking_start_timer)
deltimer(ground_sinking_start_timer)
-/// When you take damage, reset the cooldown and start processing
+/// When you move, reset the cooldown and start processing
/datum/component/ground_sinking/proc/on_moved(mob/living/basic/living_target, atom/OldLoc, Dir, Forced)
SIGNAL_HANDLER
if(sinked || is_sinking)
@@ -79,18 +79,24 @@
/datum/component/ground_sinking/proc/start_sinking(mob/living/basic/living_target)
if(!sinked || is_sinking)
is_sinking = TRUE
- INVOKE_ASYNC(src, PROC_REF(sink_once), living_target)
+ INVOKE_ASYNC(src, PROC_REF(sinking_progress), living_target)
-/datum/component/ground_sinking/proc/sink_once(mob/living/basic/living_target)
+/// Makes the mob try to sink three times. Unsinks if interrupted.
+/datum/component/ground_sinking/proc/sinking_progress(mob/living/basic/living_target)
living_target.visible_message(span_notice("[living_target] starts sinking into the ground!"))
for(var/i in 1 to 3)
- if(do_after(living_target, sink_speed, living_target))
- sink_count += 1
- living_target.icon_state = "[target_icon_state]_burried_[sink_count]"
+ if(QDELETED(living_target))
+ return
+ if(!do_after(living_target, sink_speed, living_target))
+ unsink()
+ return
+ sink_count += 1
+ living_target.icon_state = "[target_icon_state]_burried_[sink_count]"
sink_count = 0
- is_sinked(living_target)
+ finish_sinking(living_target)
-/datum/component/ground_sinking/proc/is_sinked(mob/living/basic/living_target)
+/// The mob has fully sunk, updates its regeneration, damage resistance and density
+/datum/component/ground_sinking/proc/finish_sinking(mob/living/basic/living_target)
sinked = TRUE
is_sinking = FALSE
living_target.density = FALSE
@@ -98,8 +104,11 @@
if(heal_when_sinked)
start_regenerating()
+/// The mob pops out of the ground
/datum/component/ground_sinking/proc/unsink()
var/mob/living/basic/living_target = parent
+ if(QDELETED(parent))
+ return
if(sinked && heal_when_sinked)
stop_regenerating()
living_target.icon_state = target_icon_state
@@ -107,6 +116,7 @@
living_target.density = TRUE
sinked = FALSE
+/// The mop starts regaining health
/datum/component/ground_sinking/proc/start_regenerating()
var/mob/living/basic/living_parent = parent
if (living_parent.stat == DEAD)
@@ -122,6 +132,7 @@
animate(filter, alpha = 200, time = 0.5 SECONDS, loop = -1)
animate(alpha = 0, time = 0.5 SECONDS)
+/// Stops regaining health
/datum/component/ground_sinking/proc/stop_regenerating()
STOP_PROCESSING(SSobj, src)
var/mob/living/basic/living_parent = parent
diff --git a/code/datums/components/gunpoint.dm b/code/datums/components/gunpoint.dm
index 8898795c6f933..c248b4f18c04d 100644
--- a/code/datums/components/gunpoint.dm
+++ b/code/datums/components/gunpoint.dm
@@ -172,25 +172,27 @@
qdel(src)
///If the shooter is hit by an attack, they have a 50% chance to flinch and fire. If it hit the arm holding the trigger, it's an 80% chance to fire instead
-/datum/component/gunpoint/proc/flinch(attacker, damage, damagetype, def_zone)
+/datum/component/gunpoint/proc/flinch(mob/living/source, damage_amount, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction, attacking_item)
SIGNAL_HANDLER
- var/mob/living/shooter = parent
- if(attacker == shooter)
- return // somehow this wasn't checked for months but no one tried punching themselves to initiate the shot, amazing
+ if(!attack_direction) // No fliching from yourself
+ return
var/flinch_chance = 50
- var/gun_hand = LEFT_HANDS
+ var/gun_hand = (source.get_held_index_of_item(weapon) % 2) ? BODY_ZONE_L_ARM : BODY_ZONE_R_ARM
- if(shooter.held_items[RIGHT_HANDS] == weapon)
- gun_hand = RIGHT_HANDS
+ if(isbodypart(def_zone))
+ var/obj/item/bodypart/hitting = def_zone
+ def_zone = hitting.body_zone
- if((def_zone == BODY_ZONE_L_ARM && gun_hand == LEFT_HANDS) || (def_zone == BODY_ZONE_R_ARM && gun_hand == RIGHT_HANDS))
+ if(def_zone == gun_hand)
flinch_chance = 80
if(prob(flinch_chance))
- shooter.visible_message(span_danger("[shooter] flinches!"), \
- span_danger("You flinch!"))
+ source.visible_message(
+ span_danger("[source] flinches!"),
+ span_danger("You flinch!"),
+ )
INVOKE_ASYNC(src, PROC_REF(trigger_reaction))
#undef GUNPOINT_DELAY_STAGE_2
diff --git a/code/datums/components/healing_touch.dm b/code/datums/components/healing_touch.dm
index 029b0f660ef33..a81de7ab04a78 100644
--- a/code/datums/components/healing_touch.dm
+++ b/code/datums/components/healing_touch.dm
@@ -159,7 +159,7 @@
if(show_health && !iscarbon(target))
var/formatted_string = format_string("%TARGET% now has [target.health]/[target.maxHealth] health.", healer, target)
- healer.visible_message(span_danger(formatted_string))
+ to_chat(healer, span_danger(formatted_string))
/// Reformats the passed string with the replacetext keys
/datum/component/healing_touch/proc/format_string(string, atom/source, atom/target)
diff --git a/code/datums/components/holderloving.dm b/code/datums/components/holderloving.dm
index 0d2a5c4b3d972..0670fa6086e2c 100644
--- a/code/datums/components/holderloving.dm
+++ b/code/datums/components/holderloving.dm
@@ -37,6 +37,7 @@
COMSIG_ITEM_EQUIPPED,
COMSIG_ATOM_ENTERED,
COMSIG_ATOM_EXITED,
+ COMSIG_ITEM_STORED,
), PROC_REF(check_my_loc))
/datum/component/holderloving/UnregisterFromParent()
@@ -46,6 +47,7 @@
COMSIG_ITEM_EQUIPPED,
COMSIG_ATOM_ENTERED,
COMSIG_ATOM_EXITED,
+ COMSIG_ITEM_STORED,
))
/datum/component/holderloving/PostTransfer()
diff --git a/code/datums/components/leash.dm b/code/datums/components/leash.dm
index d04a482dbf66c..ef0b278c79928 100644
--- a/code/datums/components/leash.dm
+++ b/code/datums/components/leash.dm
@@ -109,6 +109,14 @@
if (get_dist(parent, owner) <= distance)
return
+ var/atom/movable/atom_parent = parent
+ if (isnull(owner.loc))
+ atom_parent.moveToNullspace() // If our parent is in nullspace I guess we gotta go there too
+ return
+ if (isnull(atom_parent.loc))
+ force_teleport_back("in nullspace") // If we're in nullspace, get outta there
+ return
+
SEND_SIGNAL(parent, COMSIG_LEASH_PATH_STARTED)
current_path_tick += 1
diff --git a/code/datums/components/mob_chain.dm b/code/datums/components/mob_chain.dm
index 8312d9d550476..2ff7c4f19677c 100644
--- a/code/datums/components/mob_chain.dm
+++ b/code/datums/components/mob_chain.dm
@@ -43,7 +43,7 @@
RegisterSignal(parent, COMSIG_QDELETING, PROC_REF(on_deletion))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
RegisterSignal(parent, COMSIG_ATOM_CAN_BE_PULLED, PROC_REF(on_pulled))
- RegisterSignals(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, COMSIG_MOB_ATTACK_RANGED), PROC_REF(on_attack))
+ RegisterSignals(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_MOB_ATTACK_RANGED), PROC_REF(on_attack))
RegisterSignal(parent, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, PROC_REF(on_glide_size_changed))
if (vary_icon_state)
RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON_STATE, PROC_REF(on_update_icon_state))
@@ -61,7 +61,6 @@
COMSIG_ATOM_CAN_BE_PULLED,
COMSIG_ATOM_UPDATE_ICON_STATE,
COMSIG_CARBON_LIMB_DAMAGED,
- COMSIG_HUMAN_EARLY_UNARMED_ATTACK,
COMSIG_LIVING_ADJUST_BRUTE_DAMAGE,
COMSIG_LIVING_ADJUST_BURN_DAMAGE,
COMSIG_LIVING_ADJUST_CLONE_DAMAGE,
diff --git a/code/datums/components/omen.dm b/code/datums/components/omen.dm
index f499d22968dbb..8b7a751ef2704 100644
--- a/code/datums/components/omen.dm
+++ b/code/datums/components/omen.dm
@@ -7,30 +7,47 @@
* Omens are removed once the victim is either maimed by one of the possible injuries, or if they receive a blessing (read: bashing with a bible) from the chaplain.
*/
/datum/component/omen
- dupe_mode = COMPONENT_DUPE_UNIQUE
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
/// Whatever's causing the omen, if there is one. Destroying the vessel won't stop the omen, but we destroy the vessel (if one exists) upon the omen ending
var/obj/vessel
- /// If the omen is permanent, it will never go away
- var/permanent = FALSE
+ /// How many incidents are left. If 0 exactly, it will get deleted.
+ var/incidents_left = INFINITY
/// Base probability of negative events. Cursed are half as unlucky.
var/luck_mod = 1
/// Base damage from negative events. Cursed take 25% of this damage.
var/damage_mod = 1
-/datum/component/omen/Initialize(obj/vessel, permanent, luck_mod, damage_mod)
+/datum/component/omen/Initialize(obj/vessel, incidents_left = 1, luck_mod, damage_mod)
if(!isliving(parent))
return COMPONENT_INCOMPATIBLE
if(istype(vessel))
src.vessel = vessel
RegisterSignal(vessel, COMSIG_QDELETING, PROC_REF(vessel_qdeleting))
- if(!isnull(permanent))
- src.permanent = permanent
+ if(!isnull(incidents_left))
+ src.incidents_left = incidents_left
if(!isnull(luck_mod))
src.luck_mod = luck_mod
if(!isnull(damage_mod))
src.damage_mod = damage_mod
+/**
+ * This is a omen eat omen world! The stronger omen survives.
+ */
+/datum/component/omen/InheritComponent(obj/vessel, incidents_left, luck_mod, damage_mod)
+ // If we have more incidents left the new one gets deleted.
+ if(src.incidents_left > incidents_left)
+ return // make slimes get nurtiton from plasmer
+ // Otherwise we set our incidents remaining to the higher, newer value.
+ src.incidents_left = incidents_left
+ // The new omen is weaker than our current omen? Let's split the difference.
+ if(src.luck_mod > luck_mod)
+ src.luck_mod += luck_mod * 0.5
+ if(src.damage_mod > damage_mod)
+ src.luck_mod += luck_mod * 0.5
+ // This means that if you had a strong temporary omen and it was replaced by a weaker but permanent omen, the latter is made worse.
+ // Feature!
+
/datum/component/omen/Destroy(force)
var/mob/living/person = parent
to_chat(person, span_nicegreen("You feel a horrible omen lifted off your shoulders!"))
@@ -52,6 +69,11 @@
/datum/component/omen/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_ON_CARBON_SLIP, COMSIG_MOVABLE_MOVED, COMSIG_CARBON_MOOD_UPDATE, COMSIG_LIVING_DEATH))
+/datum/component/omen/proc/consume_omen()
+ incidents_left--
+ if(incidents_left < 1)
+ qdel(src)
+
/**
* check_accident() is called each step we take
*
@@ -71,11 +93,23 @@
INVOKE_ASYNC(living_guy, TYPE_PROC_REF(/mob, emote), "scream")
living_guy.adjust_fire_stacks(20)
living_guy.ignite_mob(silent = TRUE)
- if(!permanent)
- qdel(src)
+ consume_omen()
return
- if(!prob(8 * luck_mod))
+ var/effective_luck = luck_mod
+
+ // If there's nobody to witness the misfortune, make it less likely.
+ // This way, we allow for people to be able to get into hilarious situations without making the game nigh unplayable most of the time.
+
+ var/has_watchers = FALSE
+ for(var/mob/viewer in viewers(our_guy, world.view))
+ if(viewer.client)
+ has_watchers = TRUE
+ break
+ if(!has_watchers)
+ effective_luck *= 0.5
+
+ if(!prob(8 * effective_luck))
return
var/our_guy_pos = get_turf(living_guy)
@@ -88,21 +122,13 @@
INVOKE_ASYNC(src, PROC_REF(slam_airlock), darth_airlock)
return
- if(istype(our_guy_pos, /turf/open/floor/noslip/tram_plate/energized))
- var/turf/open/floor/noslip/tram_plate/energized/future_tram_victim = our_guy_pos
- if(future_tram_victim.toast(living_guy))
- if(!permanent)
- qdel(src)
- return
-
for(var/turf/the_turf as anything in get_adjacent_open_turfs(living_guy))
if(istype(the_turf, /turf/open/floor/glass/reinforced/tram)) // don't fall off the tram bridge, we want to hit you instead
return
- if(the_turf.zPassOut(living_guy, DOWN) && living_guy.can_z_move(DOWN, the_turf, z_move_flags = ZMOVE_FALL_FLAGS))
+ if(living_guy.can_z_move(DOWN, the_turf, z_move_flags = ZMOVE_FALL_FLAGS))
to_chat(living_guy, span_warning("A malevolent force guides you towards the edge..."))
living_guy.throw_at(the_turf, 1, 10, force = MOVE_FORCE_EXTREMELY_STRONG)
- if(!permanent)
- qdel(src)
+ consume_omen()
return
for(var/obj/machinery/vending/darth_vendor in the_turf)
@@ -110,8 +136,7 @@
continue
to_chat(living_guy, span_warning("A malevolent force tugs at the [darth_vendor]..."))
INVOKE_ASYNC(darth_vendor, TYPE_PROC_REF(/obj/machinery/vending, tilt), living_guy)
- if(!permanent)
- qdel(src)
+ consume_omen()
return
for(var/obj/machinery/light/evil_light in the_turf)
@@ -127,8 +152,7 @@
evil_light.Beam(living_guy, icon_state = "lightning[rand(1,12)]", time = 0.5 SECONDS)
living_guy.electrocute_act(35 * (damage_mod * 0.5), evil_light, flags = SHOCK_NOGLOVES)
INVOKE_ASYNC(living_guy, TYPE_PROC_REF(/mob, emote), "scream")
- if(!permanent && prob(33.3))
- qdel(src)
+ consume_omen()
for(var/obj/structure/mirror/evil_mirror in the_turf)
to_chat(living_guy, span_warning("You pass by the mirror and glance at it..."))
@@ -139,38 +163,37 @@
if(1)
to_chat(living_guy, span_warning("The mirror explodes into a million pieces! Wait, does that mean you're even more unlucky?"))
evil_mirror.take_damage(evil_mirror.max_integrity, BRUTE, MELEE, FALSE)
- if(prob(50 * luck_mod)) // sometimes
+ if(prob(50 * effective_luck)) // sometimes
luck_mod += 0.25
damage_mod += 0.25
if(2 to 3)
to_chat(living_guy, span_big(span_hypnophrase("Oh god, you can't see your reflection!!")))
- if(isvampire(living_guy)) // not so living i suppose
+ if(HAS_TRAIT(living_guy, TRAIT_NO_MIRROR_REFLECTION)) // not so living i suppose
to_chat(living_guy, span_green("Well, obviously."))
return
INVOKE_ASYNC(living_guy, TYPE_PROC_REF(/mob, emote), "scream")
if(4 to 5)
- if(isvampire(living_guy))
+ if(HAS_TRAIT(living_guy, TRAIT_NO_MIRROR_REFLECTION))
to_chat(living_guy, span_warning("You don't see anything of notice. Huh."))
return
to_chat(living_guy, span_userdanger("You see your reflection, but it is grinning malevolently and staring directly at you!"))
INVOKE_ASYNC(living_guy, TYPE_PROC_REF(/mob, emote), "scream")
living_guy.set_jitter_if_lower(25 SECONDS)
- if(prob(7 * luck_mod))
+ if(prob(7 * effective_luck))
to_chat(living_guy, span_warning("You are completely shocked by this turn of events!"))
- var/mob/living/carbon/carbon_guy = living_guy
to_chat(living_guy, span_userdanger("You clutch at your heart!"))
+ var/mob/living/carbon/carbon_guy = living_guy
if(istype(carbon_guy))
carbon_guy.set_heartattack(status = TRUE)
- if(!permanent && prob(33.3))
- qdel(src)
+ consume_omen()
/datum/component/omen/proc/slam_airlock(obj/machinery/door/airlock/darth_airlock)
. = darth_airlock.close(force_crush = TRUE)
- if(. && !permanent && !prob(66.6))
- qdel(src)
+ if(.)
+ consume_omen()
/// If we get knocked down, see if we have a really bad slip and bash our head hard
/datum/component/omen/proc/check_slip(mob/living/our_guy, amount)
@@ -188,8 +211,7 @@
our_guy.visible_message(span_danger("[our_guy] hits [our_guy.p_their()] head really badly falling down!"), span_userdanger("You hit your head really badly falling down!"))
the_head.receive_damage(75 * damage_mod, damage_source = "slipping")
our_guy.adjustOrganLoss(ORGAN_SLOT_BRAIN, 100 * damage_mod)
- if(!permanent)
- qdel(src)
+ consume_omen()
return
@@ -197,19 +219,22 @@
/datum/component/omen/proc/check_bless(mob/living/our_guy, category)
SIGNAL_HANDLER
- if(permanent)
+ if(incidents_left == INFINITY)
return
if(!("blessing" in our_guy.mob_mood.mood_events))
return
+ playsound(our_guy, 'sound/effects/pray_chaplain.ogg', 40, TRUE)
+ to_chat(our_guy, span_green("You feel fantastic!"))
+
qdel(src)
/// Severe deaths. Normally lifts the curse.
/datum/component/omen/proc/check_death(mob/living/our_guy)
SIGNAL_HANDLER
- if(permanent)
+ if(incidents_left == INFINITY)
return
qdel(src)
@@ -234,7 +259,7 @@
/datum/component/omen/smite
/datum/component/omen/smite/check_death(mob/living/our_guy)
- if(!permanent)
+ if(incidents_left == INFINITY)
return ..()
death_explode(our_guy)
@@ -245,8 +270,8 @@
* Has only a 50% chance of bad things happening, and takes only 25% of normal damage.
*/
/datum/component/omen/quirk
- permanent = TRUE
- luck_mod = 0.5 // 50% chance of bad things happening
+ incidents_left = INFINITY
+ luck_mod = 0.3 // 30% chance of bad things happening
damage_mod = 0.25 // 25% of normal damage
/datum/component/omen/quirk/RegisterWithParent()
diff --git a/code/datums/components/orbit_poll.dm b/code/datums/components/orbit_poll.dm
new file mode 100644
index 0000000000000..91b27d4b66797
--- /dev/null
+++ b/code/datums/components/orbit_poll.dm
@@ -0,0 +1,109 @@
+/**
+ * A replacement for the standard poll_ghost_candidate.
+ * Use this to subtly ask players to join - it picks from orbiters.
+ * Please use named arguments for this.
+ *
+ * @params ignore_key - Required so it doesn't spam
+ * @params job_bans - You can insert a list or single items here.
+ * @params cb - Invokes this proc and appends the poll winner as the last argument, mob/dead/observer/ghost
+ * @params title - Optional. Useful if the role name does not match the parent.
+ *
+ * @usage
+ * ```
+ * var/datum/callback/cb = CALLBACK(src, PROC_REF(do_stuff), arg1, arg2)
+ * AddComponent(/datum/component/orbit_poll, \
+ * ignore_key = POLL_IGNORE_EXAMPLE, \
+ * job_bans = ROLE_EXAMPLE or list(ROLE_EXAMPLE, ROLE_EXAMPLE2), \
+ * title = "Use this if you want something other than the parent name", \
+ * to_call = cb, \
+ * )
+ */
+/datum/component/orbit_poll
+ /// Prevent players with this ban from being selected
+ var/list/job_bans = list()
+ /// Title of the role to announce after it's done
+ var/title
+ /// Proc to invoke whenever the poll is complete
+ var/datum/callback/to_call
+
+/datum/component/orbit_poll/Initialize( \
+ ignore_key, \
+ list/job_bans, \
+ datum/callback/to_call, \
+ title, \
+ header = "Ghost Poll", \
+ custom_message, \
+ timeout = 20 SECONDS \
+)
+ . = ..()
+ if (!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ var/atom/owner = parent
+
+ src.job_bans |= job_bans
+ src.title = title || owner.name
+ src.to_call = to_call
+
+ var/message = custom_message || "[capitalize(src.title)] is looking for volunteers"
+
+ notify_ghosts(
+ "[message]. An orbiter will be chosen in [DisplayTimeText(timeout)].\n",
+ action = NOTIFY_ORBIT,
+ enter_link = "(Ignore)",
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ header = "Volunteers requested",
+ ignore_key = ignore_key,
+ source = parent,
+ )
+
+ addtimer(CALLBACK(src, PROC_REF(end_poll)), timeout, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE|TIMER_DELETE_ME)
+
+/datum/component/orbit_poll/Topic(href, list/href_list)
+ if(!href_list["ignore"])
+ return
+
+ var/mob/user = usr
+
+ var/ignore_key = href_list["ignore"]
+ if(tgui_alert(user, "Ignore further [title] alerts?", "Ignore Alert", list("Yes", "No"), 20 SECONDS, TRUE) != "Yes")
+ return
+
+ GLOB.poll_ignore[ignore_key] |= user.ckey
+
+/// Concludes the poll, picking one of the orbiters
+/datum/component/orbit_poll/proc/end_poll()
+ if(QDELETED(parent))
+ return
+
+ var/list/candidates = list()
+ var/atom/owner = parent
+
+ var/datum/component/orbiter/orbiter_comp = owner.GetComponent(/datum/component/orbiter)
+ if(isnull(orbiter_comp))
+ phone_home()
+ return
+
+ for(var/mob/dead/observer/ghost as anything in orbiter_comp.orbiter_list)
+ if(QDELETED(ghost) || isnull(ghost.client))
+ continue
+ if(is_banned_from(ghost.ckey, job_bans))
+ continue
+
+ candidates += ghost
+
+ if(!length(candidates))
+ phone_home()
+ return
+
+ var/mob/dead/observer/chosen = pick(candidates)
+
+ if(chosen)
+ deadchat_broadcast("[key_name(chosen, include_name = FALSE)] was selected for the role ([title]).", "Ghost Poll: ", parent)
+
+ phone_home(chosen)
+
+/// Make sure to call your parents my dude
+/datum/component/orbit_poll/proc/phone_home(mob/dead/observer/chosen)
+ to_call.Invoke(chosen)
+ qdel(src)
diff --git a/code/datums/components/pet_commands/pet_commands_basic.dm b/code/datums/components/pet_commands/pet_commands_basic.dm
index ff29b8f37d2df..263f9b17f5b84 100644
--- a/code/datums/components/pet_commands/pet_commands_basic.dm
+++ b/code/datums/components/pet_commands/pet_commands_basic.dm
@@ -41,13 +41,15 @@
radial_icon = 'icons/testing/turf_analysis.dmi'
radial_icon_state = "red_arrow"
speech_commands = list("heel", "follow")
+ ///the behavior we use to follow
+ var/follow_behavior = /datum/ai_behavior/pet_follow_friend
/datum/pet_command/follow/set_command_active(mob/living/parent, mob/living/commander)
. = ..()
set_command_target(parent, commander)
/datum/pet_command/follow/execute_action(datum/ai_controller/controller)
- controller.queue_behavior(/datum/ai_behavior/pet_follow_friend, BB_CURRENT_PET_TARGET)
+ controller.queue_behavior(follow_behavior, BB_CURRENT_PET_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/**
@@ -96,6 +98,22 @@
parent.emote("spin")
return SUBTREE_RETURN_FINISH_PLANNING
+/**
+ * # Pet Command: Use ability
+ * Use an an ability that does not require any targets
+ */
+/datum/pet_command/untargetted_ability
+ ///untargetted ability we will use
+ var/ability_key
+
+/datum/pet_command/untargetted_ability/execute_action(datum/ai_controller/controller)
+ var/datum/action/cooldown/ability = controller.blackboard[ability_key]
+ if(!ability?.IsAvailable())
+ return
+ controller.queue_behavior(/datum/ai_behavior/use_mob_ability, ability_key)
+ controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
/**
* # Pet Command: Attack
* Tells a pet to chase and bite the next thing you point at
@@ -183,11 +201,10 @@
UnregisterSignal(unfriended, COMSIG_ATOM_WAS_ATTACKED)
/datum/pet_command/protect_owner/execute_action(datum/ai_controller/controller)
- var/datum/targetting_datum/basic/targetting = controller.blackboard[BB_TARGETTING_DATUM]
var/mob/living/victim = controller.blackboard[BB_CURRENT_PET_TARGET]
if(QDELETED(victim))
return
- if(victim.stat > targetting.stat_attack)
+ if(victim.stat > controller.blackboard[BB_TARGET_MINIMUM_STAT])
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
return
controller.queue_behavior(protect_behavior, BB_CURRENT_PET_TARGET, BB_PET_TARGETTING_DATUM)
diff --git a/code/datums/components/pinata.dm b/code/datums/components/pinata.dm
index 8f6866c5d9498..1056200e3e270 100644
--- a/code/datums/components/pinata.dm
+++ b/code/datums/components/pinata.dm
@@ -33,7 +33,7 @@
return
return COMPONENT_INCOMPATIBLE
-/datum/component/pinata/proc/damage_inflicted(obj/target, damage, damage_type)
+/datum/component/pinata/proc/damage_inflicted(obj/target, damage, damage_type, ...)
SIGNAL_HANDLER
if(damage < minimum_damage || damage_type == STAMINA || damage_type == OXY)
return
diff --git a/code/datums/components/plumbing/IV_drip.dm b/code/datums/components/plumbing/IV_drip.dm
deleted file mode 100644
index e14c07a9fe002..0000000000000
--- a/code/datums/components/plumbing/IV_drip.dm
+++ /dev/null
@@ -1,34 +0,0 @@
-///Component for IVs that tracks the current person being IV'd. Input received through plumbing is instead routed to the whoever is attached
-/datum/component/plumbing/iv_drip
- demand_connects = SOUTH
- supply_connects = NORTH
-
- methods = INJECT
-
-/datum/component/plumbing/iv_drip/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
- . = ..()
-
- set_recipient_reagents_holder(null)
-
-/datum/component/plumbing/iv_drip/RegisterWithParent()
- . = ..()
-
- RegisterSignal(parent, COMSIG_IV_ATTACH, PROC_REF(update_attached))
- RegisterSignal(parent, COMSIG_IV_DETACH, PROC_REF(clear_attached))
-
-/datum/component/plumbing/iv_drip/UnregisterFromParent()
- UnregisterSignal(parent, COMSIG_IV_ATTACH)
- UnregisterSignal(parent, COMSIG_IV_DETACH)
-
-///When an IV is attached, we will use whoever is attached as our receiving container
-/datum/component/plumbing/iv_drip/proc/update_attached(datum/source, mob/living/attachee)
- SIGNAL_HANDLER
-
- if(attachee?.reagents)
- set_recipient_reagents_holder(attachee.reagents)
-
-///IV has been detached, so clear the holder
-/datum/component/plumbing/iv_drip/proc/clear_attached(datum/source)
- SIGNAL_HANDLER
-
- set_recipient_reagents_holder(null)
diff --git a/code/datums/components/plumbing/_plumbing.dm b/code/datums/components/plumbing/_plumbing.dm
index af27b4bbb4611..3de02174273c7 100644
--- a/code/datums/components/plumbing/_plumbing.dm
+++ b/code/datums/components/plumbing/_plumbing.dm
@@ -75,7 +75,7 @@
if(!demand_connects || !reagents)
return PROCESS_KILL
- if(reagents.total_volume < reagents.maximum_volume)
+ if(!reagents.holder_full())
for(var/D in GLOB.cardinals)
if(D & demand_connects)
send_request(D)
@@ -100,7 +100,7 @@
//find the duct to take from
var/datum/ductnet/net
if(!ducts.Find(num2text(dir)))
- return
+ return FALSE
net = ducts[num2text(dir)]
//find all valid suppliers in the duct
@@ -110,14 +110,16 @@
valid_suppliers += supplier
var/suppliersLeft = valid_suppliers.len
if(!suppliersLeft)
- return
+ return FALSE
//take an equal amount from each supplier
var/currentRequest
+ var/target_volume = reagents.total_volume + amount
for(var/datum/component/plumbing/give as anything in valid_suppliers)
- currentRequest = amount / suppliersLeft
+ currentRequest = (target_volume - reagents.total_volume) / suppliersLeft
give.transfer_to(src, currentRequest, reagent, net)
suppliersLeft--
+ return TRUE
///returns TRUE when they can give the specified amount and reagent. called by process request
/datum/component/plumbing/proc/can_give(amount, reagent, datum/ductnet/net)
@@ -131,6 +133,8 @@
else if(reagents.total_volume) //take whatever
return TRUE
+ return FALSE
+
///this is where the reagent is actually transferred and is thus the finish point of our process()
/datum/component/plumbing/proc/transfer_to(datum/component/plumbing/target, amount, reagent, datum/ductnet/net)
if(!reagents || !target || !target.reagents)
@@ -391,6 +395,10 @@
demand_connects = NORTH
supply_connects = SOUTH
+/datum/component/plumbing/iv_drip
+ demand_connects = SOUTH
+ supply_connects = NORTH
+
/datum/component/plumbing/manifold/change_ducting_layer(obj/caller, obj/changer, new_layer)
return
@@ -411,3 +419,8 @@
return (buffer.mode == READY) ? ..() : FALSE
#undef READY
+
+///Lazily demand from any direction. Overlays won't look good, and the aquarium sprite occupies about the entire 32x32 area anyway.
+/datum/component/plumbing/aquarium
+ demand_connects = SOUTH|NORTH|EAST|WEST
+ use_overlays = FALSE
diff --git a/code/datums/components/plumbing/reaction_chamber.dm b/code/datums/components/plumbing/reaction_chamber.dm
index f728ce315e6ee..707f7cf6f9c1b 100644
--- a/code/datums/components/plumbing/reaction_chamber.dm
+++ b/code/datums/components/plumbing/reaction_chamber.dm
@@ -10,29 +10,43 @@
/datum/component/plumbing/reaction_chamber/can_give(amount, reagent, datum/ductnet/net)
. = ..()
var/obj/machinery/plumbing/reaction_chamber/reaction_chamber = parent
- if(!. || !reaction_chamber.emptying || reagents.is_reacting == TRUE)
+ if(!. || !reaction_chamber.emptying || reagents.is_reacting)
return FALSE
/datum/component/plumbing/reaction_chamber/send_request(dir)
var/obj/machinery/plumbing/reaction_chamber/chamber = parent
if(chamber.emptying)
return
+ var/required_amount = 0
+ var/total_present_amount = 0
+ var/total_required_amount = 0
+ //take in reagents
+ var/datum/reagent/present_reagent
var/present_amount
var/diff
for(var/required_reagent in chamber.required_reagents)
- //find how much amount is already present if at all
- present_amount = 0
+ //compute total required amount from all reagents
+ required_amount = chamber.required_reagents[required_reagent]
+ total_required_amount += required_amount
+
+ //find how much amount is already present if at all and get the reagent reference
+ present_reagent = null
for(var/datum/reagent/containg_reagent as anything in reagents.reagent_list)
if(required_reagent == containg_reagent.type)
- present_amount = containg_reagent.volume
+ present_reagent = containg_reagent
break
+ present_amount = present_reagent ? present_reagent.volume : 0
- //compute how much more is needed and round it
- diff = chamber.required_reagents[required_reagent] - present_amount
- if(diff > CHEMICAL_QUANTISATION_LEVEL)
- process_request(min(diff, MACHINE_REAGENT_TRANSFER), required_reagent, dir)
+ //compute how much more is needed and round it. early return only if the request succeded
+ diff = min(required_amount - present_amount, MACHINE_REAGENT_TRANSFER)
+ if(diff >= CHEMICAL_VOLUME_ROUNDING && process_request(diff, required_reagent, dir))
return
+ total_present_amount += present_reagent ? present_reagent.volume : 0
+
+ //do we have close enough
+ if(total_required_amount - total_present_amount >= CHEMICAL_VOLUME_ROUNDING) //nope
+ return
reagents.flags &= ~NO_REACT
reagents.handle_reactions()
diff --git a/code/datums/components/plundering_attacks.dm b/code/datums/components/plundering_attacks.dm
new file mode 100644
index 0000000000000..f55fa42b0717b
--- /dev/null
+++ b/code/datums/components/plundering_attacks.dm
@@ -0,0 +1,54 @@
+/**
+ * Component that makes basic mobs' melee attacks steal money from the target's ID card.
+ * Plundered money is stored and dropped on death or removal of the component.
+ */
+/datum/component/plundering_attacks
+ /// How many credits do we steal per attack?
+ var/plunder_amount = 25
+ /// How much plunder do we have stored?
+ var/plunder_stored = 0
+
+
+/datum/component/plundering_attacks/Initialize(
+ plunder_amount = 25,
+)
+ . = ..()
+ if(!isbasicmob(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.plunder_amount = plunder_amount
+
+/datum/component/plundering_attacks/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(attempt_plunder))
+ RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(drop_plunder))
+
+/datum/component/plundering_attacks/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(parent, list(COMSIG_HOSTILE_POST_ATTACKINGTARGET, COMSIG_LIVING_DEATH))
+ drop_plunder()
+
+/datum/component/plundering_attacks/proc/attempt_plunder(mob/living/attacker, mob/living/carbon/human/target, success)
+ SIGNAL_HANDLER
+ if(!istype(target) || !success)
+ return
+ var/obj/item/card/id/id_card = target.wear_id?.GetID()
+ if(isnull(id_card))
+ return
+ var/datum/bank_account/account_to_rob = id_card.registered_account
+ if(isnull(account_to_rob) || account_to_rob.account_balance == 0)
+ return
+
+ var/amount_to_steal = plunder_amount
+ if(account_to_rob.account_balance < plunder_amount) //If there isn't enough, just take what's left
+ amount_to_steal = account_to_rob.account_balance
+ plunder_stored += amount_to_steal
+ account_to_rob.adjust_money(-amount_to_steal)
+ account_to_rob.bank_card_talk("Transaction confirmed! Transferred [amount_to_steal] credits to \!")
+
+/datum/component/plundering_attacks/proc/drop_plunder()
+ SIGNAL_HANDLER
+ if(plunder_stored == 0)
+ return
+ new /obj/item/holochip(get_turf(parent), plunder_stored)
+ plunder_stored = 0
diff --git a/code/datums/components/pry_open_door.dm b/code/datums/components/pry_open_door.dm
deleted file mode 100644
index 17e445d25ca8b..0000000000000
--- a/code/datums/components/pry_open_door.dm
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Attached to a basic mob.
- * Causes attacks on doors to attempt to open them.
- */
-/datum/component/pry_open_door
- /// Odds the attack opens the door
- var/open_chance
- /// Time it takes to open a door with force
- var/force_wait
-
-/datum/component/pry_open_door/Initialize(open_chance = 100, force_wait = 10 SECONDS)
- . = ..()
-
- if(!isbasicmob(parent))
- return COMPONENT_INCOMPATIBLE
- src.open_chance = open_chance
- src.force_wait = force_wait
-
-/datum/component/pry_open_door/RegisterWithParent()
- RegisterSignal(parent, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(hostile_attackingtarget))
-
-/datum/component/pry_open_door/UnregisterFromParent()
- UnregisterSignal(parent, COMSIG_HOSTILE_POST_ATTACKINGTARGET)
-
-/datum/component/pry_open_door/proc/hostile_attackingtarget(mob/living/basic/attacker, atom/target, success)
- SIGNAL_HANDLER
-
- if(!success)
- return
- if(istype(target, /obj/machinery/door/airlock) && prob(open_chance))
- var/obj/machinery/door/airlock/airlock_target = target
- INVOKE_ASYNC(src, PROC_REF(open_door), attacker, airlock_target)
-
-/datum/component/pry_open_door/proc/open_door(mob/living/basic/attacker, obj/machinery/door/airlock/airlock_target)
- if(airlock_target.locked)
- to_chat(attacker, span_warning("The airlock's bolts prevent it from being forced!"))
- return
- else if(!airlock_target.allowed(attacker) && airlock_target.hasPower())
- attacker.visible_message(span_warning("We start forcing the [airlock_target] open."), \
- span_hear("You hear a metal screeching sound."))
- playsound(airlock_target, 'sound/machines/airlock_alien_prying.ogg', 100, TRUE)
- if(!do_after(attacker, force_wait, airlock_target))
- return
- if(airlock_target.locked)
- return
- attacker.visible_message(span_warning("We force the [airlock_target] to open."))
- airlock_target.open(BYPASS_DOOR_CHECKS)
- else if(!airlock_target.hasPower())
- attacker.visible_message(span_warning("We force the [airlock_target] to open."))
- airlock_target.open(FORCING_DOOR_CHECKS)
- else
- airlock_target.open(DEFAULT_DOOR_CHECKS)
diff --git a/code/datums/components/punchcooldown.dm b/code/datums/components/punchcooldown.dm
index 7c2ae1047fef2..80a72784c095d 100644
--- a/code/datums/components/punchcooldown.dm
+++ b/code/datums/components/punchcooldown.dm
@@ -1,6 +1,6 @@
///Your favourite Jojoke. Used for the gloves of the north star.
/datum/component/wearertargeting/punchcooldown
- signals = list(COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_LIVING_SLAP_MOB)
+ signals = list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_LIVING_SLAP_MOB)
mobtype = /mob/living/carbon
proctype = PROC_REF(reducecooldown)
valid_slots = list(ITEM_SLOT_GLOVES)
@@ -13,7 +13,7 @@
return
RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, PROC_REF(changewarcry))
-///Called on COMSIG_HUMAN_MELEE_UNARMED_ATTACK. Yells the warcry and and reduces punch cooldown.
+///Called on COMSIG_LIVING_UNARMED_ATTACK. Yells the warcry and and reduces punch cooldown.
/datum/component/wearertargeting/punchcooldown/proc/reducecooldown(mob/living/carbon/M, atom/target)
if((M.combat_mode && isliving(target)) || istype(M.get_active_held_item(), /obj/item/hand_item/slapper))
M.changeNext_move(CLICK_CD_RAPID)
diff --git a/code/datums/components/recharging_attacks.dm b/code/datums/components/recharging_attacks.dm
new file mode 100644
index 0000000000000..b9127c4e018bb
--- /dev/null
+++ b/code/datums/components/recharging_attacks.dm
@@ -0,0 +1,66 @@
+/// Reduces the cooldown of a given action upon landing attacks, critting, or killing mobs.
+/datum/component/recharging_attacks
+ /// The target of the most recent attack
+ var/last_target
+ /// The stat of the most recently attacked mob
+ var/last_stat
+ /// The action to recharge when attacking
+ var/datum/action/cooldown/recharged_action
+ /// The amount of cooldown to refund on a successful attack
+ var/attack_refund
+ /// The amount of cooldown to refund when putting a target into critical
+ var/crit_refund
+
+/datum/component/recharging_attacks/Initialize(
+ datum/action/cooldown/recharged_action,
+ attack_refund = 1 SECONDS,
+ crit_refund = 5 SECONDS,
+)
+ . = ..()
+ if (!isbasicmob(parent) || !istype(recharged_action))
+ return COMPONENT_INCOMPATIBLE
+ src.recharged_action = recharged_action
+ src.attack_refund = attack_refund
+ src.crit_refund = crit_refund
+
+/datum/component/recharging_attacks/Destroy()
+ UnregisterSignal(recharged_action, COMSIG_QDELETING)
+ recharged_action = null
+ return ..()
+
+/datum/component/recharging_attacks/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(set_old_stat))
+ RegisterSignal(parent, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(check_stat))
+ RegisterSignal(recharged_action, COMSIG_QDELETING, PROC_REF(on_action_qdel))
+
+/datum/component/recharging_attacks/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(parent, list(COMSIG_HOSTILE_PRE_ATTACKINGTARGET, COMSIG_HOSTILE_POST_ATTACKINGTARGET))
+ if(recharged_action)
+ UnregisterSignal(recharged_action, COMSIG_QDELETING)
+
+/datum/component/recharging_attacks/proc/set_old_stat(mob/attacker, mob/attacked)
+ SIGNAL_HANDLER
+ if(!isliving(attacked))
+ return
+ last_target = attacked
+ last_stat = attacked.stat
+
+/datum/component/recharging_attacks/proc/check_stat(mob/living/attacker, mob/living/attacked, success)
+ SIGNAL_HANDLER
+ if(!isliving(attacked) || attacked != last_target || attacker.faction_check_atom(attacked))
+ return
+
+ var/final_refund = attack_refund
+ if(QDELETED(attacked) || (attacked.stat == DEAD && last_stat != DEAD)) //The target is dead and we killed them - full refund
+ final_refund = recharged_action.cooldown_time
+ else if(attacked.stat > CONSCIOUS && last_stat == CONSCIOUS) //We knocked the target unconscious - partial refund
+ final_refund = crit_refund
+
+ recharged_action.next_use_time -= final_refund
+ recharged_action.build_all_button_icons()
+
+/datum/component/recharging_attacks/proc/on_action_qdel()
+ SIGNAL_HANDLER
+ qdel(src)
diff --git a/code/datums/components/regenerator.dm b/code/datums/components/regenerator.dm
index a6182935a3f08..b988676456549 100644
--- a/code/datums/components/regenerator.dm
+++ b/code/datums/components/regenerator.dm
@@ -9,8 +9,16 @@
/datum/component/regenerator
/// You will only regain health if you haven't been hurt for this many seconds
var/regeneration_delay
- /// Health to regenerate per second
- var/health_per_second
+ /// Brute reagined every second
+ var/brute_per_second
+ /// Burn reagined every second
+ var/burn_per_second
+ /// Toxin reagined every second
+ var/tox_per_second
+ /// Oxygen reagined every second
+ var/oxy_per_second
+ /// If TRUE, we'll try to heal wounds as well. Useless for non-humans.
+ var/heals_wounds = FALSE
/// List of damage types we don't care about, in case you want to only remove this with fire damage or something
var/list/ignore_damage_types
/// Colour of regeneration animation, or none if you don't want one
@@ -18,12 +26,25 @@
/// When this timer completes we start restoring health, it is a timer rather than a cooldown so we can do something on its completion
var/regeneration_start_timer
-/datum/component/regenerator/Initialize(regeneration_delay = 6 SECONDS, health_per_second = 2, ignore_damage_types = list(STAMINA), outline_colour = COLOR_PALE_GREEN)
+/datum/component/regenerator/Initialize(
+ regeneration_delay = 6 SECONDS,
+ brute_per_second = 2,
+ burn_per_second = 0,
+ tox_per_second = 0,
+ oxy_per_second = 0,
+ heals_wounds = FALSE,
+ ignore_damage_types = list(STAMINA),
+ outline_colour = COLOR_PALE_GREEN,
+)
if (!isliving(parent))
return COMPONENT_INCOMPATIBLE
src.regeneration_delay = regeneration_delay
- src.health_per_second = health_per_second
+ src.brute_per_second = brute_per_second
+ src.burn_per_second = burn_per_second
+ src.tox_per_second = tox_per_second
+ src.oxy_per_second = oxy_per_second
+ src.heals_wounds = heals_wounds
src.ignore_damage_types = ignore_damage_types
src.outline_colour = outline_colour
@@ -45,27 +66,22 @@
deltimer(regeneration_start_timer)
/// When you take damage, reset the cooldown and start processing
-/datum/component/regenerator/proc/on_take_damage(datum/source, damage, damagetype)
+/datum/component/regenerator/proc/on_take_damage(datum/source, damage, damagetype, ...)
SIGNAL_HANDLER
- if (damage <= 0)
- return
- if (locate(damagetype) in ignore_damage_types)
+ if (damagetype in ignore_damage_types)
return
stop_regenerating()
- if(regeneration_start_timer)
- deltimer(regeneration_start_timer)
- regeneration_start_timer = addtimer(CALLBACK(src, PROC_REF(start_regenerating)), regeneration_delay, TIMER_STOPPABLE)
+ regeneration_start_timer = addtimer(CALLBACK(src, PROC_REF(start_regenerating)), regeneration_delay, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE)
/// Start processing health regeneration, and show animation if provided
/datum/component/regenerator/proc/start_regenerating()
- var/mob/living/living_parent = parent
- if (living_parent.stat == DEAD)
- return
- if (living_parent.health == living_parent.maxHealth)
+ if (!should_be_regenning(parent))
return
+ var/mob/living/living_parent = parent
living_parent.visible_message(span_notice("[living_parent]'s wounds begin to knit closed!"))
START_PROCESSING(SSobj, src)
+ regeneration_start_timer = null
if (!outline_colour)
return
living_parent.add_filter(REGENERATION_FILTER, 2, list("type" = "outline", "color" = outline_colour, "alpha" = 0, "size" = 1))
@@ -81,13 +97,44 @@
living_parent.remove_filter(REGENERATION_FILTER)
/datum/component/regenerator/process(seconds_per_tick = SSMOBS_DT)
- var/mob/living/living_parent = parent
- if (living_parent.stat == DEAD)
+ if (!should_be_regenning(parent))
stop_regenerating()
return
- if (living_parent.health == living_parent.maxHealth)
- stop_regenerating()
- return
- living_parent.heal_overall_damage(health_per_second * seconds_per_tick)
+
+ var/mob/living/living_parent = parent
+ // Heal bonus for being in crit. Only applies to carbons
+ var/heal_mod = HAS_TRAIT(living_parent, TRAIT_CRITICAL_CONDITION) ? 2 : 1
+
+ var/need_mob_update = FALSE
+ if(brute_per_second)
+ need_mob_update += living_parent.adjustBruteLoss(-1 * heal_mod * brute_per_second * seconds_per_tick, updating_health = FALSE)
+ if(burn_per_second)
+ need_mob_update += living_parent.adjustFireLoss(-1 * heal_mod * burn_per_second * seconds_per_tick, updating_health = FALSE)
+ if(tox_per_second)
+ need_mob_update += living_parent.adjustToxLoss(-1 * heal_mod * tox_per_second * seconds_per_tick, updating_health = FALSE)
+ if(oxy_per_second)
+ need_mob_update += living_parent.adjustOxyLoss(-1 * heal_mod * oxy_per_second * seconds_per_tick, updating_health = FALSE)
+
+ if(heals_wounds && iscarbon(parent))
+ var/mob/living/carbon/carbon_parent = living_parent
+ for(var/datum/wound/iter_wound as anything in carbon_parent.all_wounds)
+ if(SPT_PROB(2 - (iter_wound.severity / 2), seconds_per_tick))
+ iter_wound.remove_wound()
+ need_mob_update++
+
+ if(need_mob_update)
+ living_parent.updatehealth()
+
+/// Checks if the passed mob is in a valid state to be regenerating
+/datum/component/regenerator/proc/should_be_regenning(mob/living/who)
+ if(who.stat == DEAD)
+ return FALSE
+ if(heals_wounds && iscarbon(who))
+ var/mob/living/carbon/carbon_who = who
+ if(length(carbon_who.all_wounds) > 0)
+ return TRUE
+ if(who.health != who.maxHealth)
+ return TRUE
+ return FALSE
#undef REGENERATION_FILTER
diff --git a/code/datums/components/riding/riding.dm b/code/datums/components/riding/riding.dm
index 14358a5c5a388..d1a8f827cafe4 100644
--- a/code/datums/components/riding/riding.dm
+++ b/code/datums/components/riding/riding.dm
@@ -26,12 +26,16 @@
var/list/directional_vehicle_layers = list()
/// same as above but instead of layer you have a list(px, py)
var/list/directional_vehicle_offsets = list()
+ /// planes of the rider
+ var/list/directional_rider_planes = list()
/// allow typecache for only certain turfs, forbid to allow all but those. allow only certain turfs will take precedence.
var/list/allowed_turf_typecache
/// allow typecache for only certain turfs, forbid to allow all but those. allow only certain turfs will take precedence.
var/list/forbid_turf_typecache
/// We don't need roads where we're going if this is TRUE, allow normal movement in space tiles
var/override_allow_spacemove = FALSE
+ /// can anyone other than the rider unbuckle the rider?
+ var/can_force_unbuckle = TRUE
/**
* Ride check flags defined for the specific riding component types, so we know if we need arms, legs, or whatever.
@@ -55,7 +59,6 @@
if(potion_boost)
vehicle_move_delay = round(CONFIG_GET(number/movedelay/run_delay) * 0.85, 0.01)
-
/datum/component/riding/RegisterWithParent()
. = ..()
RegisterSignal(parent, COMSIG_ATOM_DIR_CHANGE, PROC_REF(vehicle_turned))
@@ -64,7 +67,8 @@
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(vehicle_moved))
RegisterSignal(parent, COMSIG_MOVABLE_BUMP, PROC_REF(vehicle_bump))
RegisterSignal(parent, COMSIG_BUCKLED_CAN_Z_MOVE, PROC_REF(riding_can_z_move))
-
+ if(!can_force_unbuckle)
+ RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(force_unbuckle))
/**
* This proc handles all of the proc calls to things like set_vehicle_dir_layer() that a type of riding datum needs to call on creation
*
@@ -79,6 +83,9 @@
/datum/component/riding/proc/vehicle_mob_unbuckle(datum/source, mob/living/rider, force = FALSE)
SIGNAL_HANDLER
+ handle_unbuckle(rider)
+
+/datum/component/riding/proc/handle_unbuckle(mob/living/rider)
var/atom/movable/movable_parent = parent
restore_position(rider)
unequip_buckle_inhands(rider)
@@ -94,6 +101,7 @@
var/atom/movable/movable_parent = parent
handle_vehicle_layer(movable_parent.dir)
handle_vehicle_offsets(movable_parent.dir)
+ handle_rider_plane(movable_parent.dir)
if(rider.pulling == source)
rider.stop_pulling()
@@ -118,9 +126,20 @@
. = AM.layer
AM.layer = .
+/datum/component/riding/proc/handle_rider_plane(dir)
+ var/atom/movable/movable_parent = parent
+ var/target_plane = directional_rider_planes["[dir]"]
+ if(isnull(target_plane))
+ return
+ for(var/mob/buckled_mob in movable_parent.buckled_mobs)
+ SET_PLANE_EXPLICIT(buckled_mob, target_plane, movable_parent)
+
/datum/component/riding/proc/set_vehicle_dir_layer(dir, layer)
directional_vehicle_layers["[dir]"] = layer
+/datum/component/riding/proc/set_rider_dir_plane(dir, plane)
+ directional_rider_planes["[dir]"] = plane
+
/// This is called after the ridden atom is successfully moved and is used to handle icon stuff
/datum/component/riding/proc/vehicle_moved(datum/source, oldloc, dir, forced)
SIGNAL_HANDLER
@@ -135,6 +154,7 @@
return // runtimed with piggy's without this, look into this more
handle_vehicle_offsets(dir)
handle_vehicle_layer(dir)
+ handle_rider_plane(dir)
/// Turning is like moving
/datum/component/riding/proc/vehicle_turned(datum/source, _old_dir, new_dir)
@@ -222,11 +242,14 @@
//BUCKLE HOOKS
/datum/component/riding/proc/restore_position(mob/living/buckled_mob)
- if(buckled_mob)
- buckled_mob.pixel_x = buckled_mob.base_pixel_x
- buckled_mob.pixel_y = buckled_mob.base_pixel_y
- if(buckled_mob.client)
- buckled_mob.client.view_size.resetToDefault()
+ if(isnull(buckled_mob))
+ return
+ buckled_mob.pixel_x = buckled_mob.base_pixel_x
+ buckled_mob.pixel_y = buckled_mob.base_pixel_y
+ var/atom/source = parent
+ SET_PLANE_EXPLICIT(buckled_mob, initial(buckled_mob.plane), source)
+ if(buckled_mob.client)
+ buckled_mob.client.view_size.resetToDefault()
//MOVEMENT
/datum/component/riding/proc/turf_check(turf/next, turf/current)
@@ -273,3 +296,10 @@
/datum/component/riding/proc/riding_can_z_move(atom/movable/movable_parent, direction, turf/start, turf/destination, z_move_flags, mob/living/rider)
SIGNAL_HANDLER
return COMPONENT_RIDDEN_ALLOW_Z_MOVE
+
+/datum/component/riding/proc/force_unbuckle(atom/movable/source, mob/living/living_hitter)
+ SIGNAL_HANDLER
+
+ if((living_hitter in source.buckled_mobs))
+ return
+ return COMPONENT_CANCEL_ATTACK_CHAIN
diff --git a/code/datums/components/riding/riding_mob.dm b/code/datums/components/riding/riding_mob.dm
index a546508e50547..900a164831488 100644
--- a/code/datums/components/riding/riding_mob.dm
+++ b/code/datums/components/riding/riding_mob.dm
@@ -5,7 +5,8 @@
var/can_be_driven = TRUE
/// If TRUE, this creature's abilities can be triggered by the rider while mounted
var/can_use_abilities = FALSE
- var/list/shared_action_buttons = list()
+ /// list of blacklisted abilities that cant be shared
+ var/list/blacklist_abilities = list()
/datum/component/riding/creature/Initialize(mob/living/riding_mob, force = FALSE, ride_check_flags = NONE, potion_boost = FALSE)
if(!isliving(parent))
@@ -60,6 +61,8 @@
// for fireman carries, check if the ridden is stunned/restrained
else if((ride_check_flags & CARRIER_NEEDS_ARM) && (HAS_TRAIT(living_parent, TRAIT_RESTRAINED) || living_parent.incapacitated(IGNORE_RESTRAINTS|IGNORE_GRAB)))
. = FALSE
+ else if((ride_check_flags & JUST_FRIEND_RIDERS) && !(living_parent.faction.Find(REF(rider))))
+ . = FALSE
if(. || !consequences)
return
@@ -156,6 +159,7 @@
for(var/mob/yeet_mob in user.buckled_mobs)
force_dismount(yeet_mob, (!user.combat_mode)) // gentle on help, byeeee if not
+
/// If the ridden creature has abilities, and some var yet to be made is set to TRUE, the rider will be able to control those abilities
/datum/component/riding/creature/proc/setup_abilities(mob/living/rider)
if(!isliving(parent))
@@ -164,6 +168,8 @@
var/mob/living/ridden_creature = parent
for(var/datum/action/action as anything in ridden_creature.actions)
+ if(is_type_in_list(action, blacklist_abilities))
+ continue
action.GiveAction(rider)
/// Takes away the riding parent's abilities from the rider
@@ -212,7 +218,7 @@
/datum/component/riding/creature/human/RegisterWithParent()
. = ..()
- RegisterSignal(parent, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, PROC_REF(on_host_unarmed_melee))
+ RegisterSignal(parent, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_host_unarmed_melee))
RegisterSignal(parent, COMSIG_LIVING_SET_BODY_POSITION, PROC_REF(check_carrier_fall_over))
/datum/component/riding/creature/human/log_riding(mob/living/living_parent, mob/living/rider)
@@ -234,12 +240,13 @@
return ..()
/// If the carrier shoves the person they're carrying, force the carried mob off
-/datum/component/riding/creature/human/proc/on_host_unarmed_melee(mob/living/carbon/human/human_parent, atom/target, proximity, modifiers)
+/datum/component/riding/creature/human/proc/on_host_unarmed_melee(mob/living/source, atom/target, proximity, modifiers)
SIGNAL_HANDLER
- if(LAZYACCESS(modifiers, RIGHT_CLICK) && (target in human_parent.buckled_mobs))
+ if(LAZYACCESS(modifiers, RIGHT_CLICK) && (target in source.buckled_mobs))
force_dismount(target)
return COMPONENT_CANCEL_ATTACK_CHAIN
+ return NONE
/// If the carrier gets knocked over, force the rider(s) off and see if someone got hurt
/datum/component/riding/creature/human/proc/check_carrier_fall_over(mob/living/carbon/human/human_parent)
@@ -483,3 +490,35 @@
set_vehicle_dir_layer(NORTH, OBJ_LAYER)
set_vehicle_dir_layer(EAST, OBJ_LAYER)
set_vehicle_dir_layer(WEST, OBJ_LAYER)
+
+/datum/component/riding/creature/leaper
+ can_force_unbuckle = FALSE
+ can_use_abilities = TRUE
+ blacklist_abilities = list(/datum/action/cooldown/toggle_seethrough)
+ ride_check_flags = JUST_FRIEND_RIDERS
+
+/datum/component/riding/creature/leaper/handle_specials()
+ . = ..()
+ set_riding_offsets(RIDING_OFFSET_ALL, list(TEXT_NORTH = list(17, 46), TEXT_SOUTH = list(17,51), TEXT_EAST = list(27, 46), TEXT_WEST = list(6, 46)))
+ set_rider_dir_plane(SOUTH, GAME_PLANE_UPPER)
+ set_rider_dir_plane(NORTH, GAME_PLANE)
+ set_rider_dir_plane(EAST, GAME_PLANE_UPPER)
+ set_rider_dir_plane(WEST, GAME_PLANE_UPPER)
+
+/datum/component/riding/creature/leaper/Initialize(mob/living/riding_mob, force = FALSE, ride_check_flags = NONE, potion_boost = FALSE)
+ . = ..()
+ RegisterSignal(riding_mob, COMSIG_MOB_POINTED, PROC_REF(attack_pointed))
+
+/datum/component/riding/creature/leaper/proc/attack_pointed(mob/living/rider, atom/pointed)
+ SIGNAL_HANDLER
+ if(!isclosedturf(pointed))
+ return
+ var/mob/living/basic/basic_parent = parent
+ if(!basic_parent.CanReach(pointed))
+ return
+ basic_parent.melee_attack(pointed)
+
+
+/datum/component/riding/leaper/handle_unbuckle(mob/living/rider)
+ . = ..()
+ UnregisterSignal(rider, COMSIG_MOB_POINTED)
diff --git a/code/datums/components/scope.dm b/code/datums/components/scope.dm
index 4de77f44cd94c..853820eaa7313 100644
--- a/code/datums/components/scope.dm
+++ b/code/datums/components/scope.dm
@@ -3,6 +3,10 @@
var/range_modifier = 1
/// Fullscreen object we use for tracking the shots.
var/atom/movable/screen/fullscreen/cursor_catcher/scope/tracker
+ /// The owner of the tracker's ckey. For comparing with the current owner mob, in case the client has left it (e.g. ghosted).
+ var/tracker_owner_ckey
+ /// Are we zooming currently?
+ var/zooming
/datum/component/scope/Initialize(range_modifier)
if(!isgun(parent))
@@ -101,21 +105,35 @@
return target_turf
/**
- * We start zooming by hiding the mouse pointer, adding our tracker overlay and starting our processing.
+ * Wrapper for zoom(), so in case we runtime we do not get stuck in a bad state
*
* Arguments:
* * user: The mob we are starting zooming on.
*/
/datum/component/scope/proc/start_zooming(mob/user)
- if(!user.client)
+ if(zoom(user))
+ zooming = TRUE
+
+/**
+ * We start zooming by hiding the mouse pointer, adding our tracker overlay and starting our processing.
+ *
+ * Arguments:
+ * * user: The mob we are starting zooming on.
+*/
+/datum/component/scope/proc/zoom(mob/user)
+ if(isnull(user.client))
+ return
+ if(zooming)
return
user.client.mouse_override_icon = 'icons/effects/mouse_pointers/scope_hide.dmi'
user.update_mouse_pointer()
user.playsound_local(parent, 'sound/weapons/scope.ogg', 75, TRUE)
tracker = user.overlay_fullscreen("scope", /atom/movable/screen/fullscreen/cursor_catcher/scope, 0)
tracker.assign_to_mob(user, range_modifier)
- RegisterSignal(user, COMSIG_MOB_SWAP_HANDS, PROC_REF(stop_zooming))
+ tracker_owner_ckey = user.ckey
+ RegisterSignals(user, list(COMSIG_MOB_SWAP_HANDS, COMSIG_QDELETING), PROC_REF(stop_zooming))
START_PROCESSING(SSprojectiles, src)
+ return TRUE
/**
* We stop zooming, canceling processing, resetting stuff back to normal and deleting our tracker.
@@ -126,15 +144,31 @@
/datum/component/scope/proc/stop_zooming(mob/user)
SIGNAL_HANDLER
+ if(!zooming)
+ return
+
STOP_PROCESSING(SSprojectiles, src)
- UnregisterSignal(user, COMSIG_MOB_SWAP_HANDS)
+ UnregisterSignal(user, list(COMSIG_MOB_SWAP_HANDS, COMSIG_QDELETING))
+
+ zooming = FALSE
+
+ user.playsound_local(parent, 'sound/weapons/scope.ogg', 75, TRUE, frequency = -1)
+ user.clear_fullscreen("scope")
+
+ // if the client has ended up in another mob, find that mob so we can fix their cursor
+ var/mob/true_user
+ if(user.ckey != tracker_owner_ckey)
+ true_user = get_mob_by_ckey(tracker_owner_ckey)
+
+ if(!isnull(true_user))
+ user = true_user
+
if(user.client)
animate(user.client, 0.2 SECONDS, pixel_x = 0, pixel_y = 0)
user.client.mouse_override_icon = null
user.update_mouse_pointer()
- user.playsound_local(parent, 'sound/weapons/scope.ogg', 75, TRUE, frequency = -1)
tracker = null
- user.clear_fullscreen("scope")
+ tracker_owner_ckey = null
/atom/movable/screen/fullscreen/cursor_catcher/scope
icon_state = "scope"
diff --git a/code/datums/components/seclight_attachable.dm b/code/datums/components/seclight_attachable.dm
index b3f36fe041ad0..65c64fdb8ce9d 100644
--- a/code/datums/components/seclight_attachable.dm
+++ b/code/datums/components/seclight_attachable.dm
@@ -160,7 +160,7 @@
var/successful_toggle = light.toggle_light(user)
if(!successful_toggle)
return TRUE
- user.balloon_alert(user, "[light.name] toggled [light.on ? "on":"off"]")
+ user.balloon_alert(user, "[light.name] toggled [light.light_on ? "on":"off"]")
update_light()
return TRUE
@@ -270,7 +270,7 @@
if(!light)
return
- var/overlay_state = "[light_overlay][light.on ? "_on":""]"
+ var/overlay_state = "[light_overlay][light.light_on ? "_on":""]"
var/mutable_appearance/flashlight_overlay = mutable_appearance(light_overlay_icon, overlay_state)
flashlight_overlay.pixel_x = overlay_x
flashlight_overlay.pixel_y = overlay_y
@@ -288,7 +288,7 @@
var/base_state = source.base_icon_state || initial(source.icon_state)
// Updates our icon state based on our light state.
if(light)
- source.icon_state = "[base_state]-[light_icon_state][light.on ? "-on":""]"
+ source.icon_state = "[base_state]-[light_icon_state][light.light_on ? "-on":""]"
// Reset their icon state when if we've got no light.
else if(source.icon_state != base_state)
diff --git a/code/datums/components/seethrough_mob.dm b/code/datums/components/seethrough_mob.dm
index b52cfb334ab16..4359c454f1a10 100644
--- a/code/datums/components/seethrough_mob.dm
+++ b/code/datums/components/seethrough_mob.dm
@@ -39,7 +39,7 @@
render_source_atom.render_source = "*transparent_bigmob[personal_uid]"
- var/datum/action/toggle_seethrough/action = new(src)
+ var/datum/action/cooldown/toggle_seethrough/action = new(src)
action.Grant(parent)
/datum/component/seethrough_mob/Destroy(force, silent)
@@ -114,22 +114,22 @@
else
untrick_mob()
-/datum/action/toggle_seethrough
+/datum/action/cooldown/toggle_seethrough
name = "Toggle Seethrough"
desc = "Allows you to see behind your massive body and click through it."
button_icon = 'icons/mob/actions/actions_xeno.dmi'
button_icon_state = "alien_sneak"
background_icon_state = "bg_alien"
+ cooldown_time = 1 SECONDS
+ melee_cooldown_time = 0
-/datum/action/toggle_seethrough/Remove(mob/remove_from)
- var/datum/component/seethrough_mob/seethroughComp = target
- if(seethroughComp.is_active)
- seethroughComp.untrick_mob()
+/datum/action/cooldown/toggle_seethrough/Remove(mob/remove_from)
+ var/datum/component/seethrough_mob/transparency = target
+ if(transparency.is_active)
+ transparency.untrick_mob()
return ..()
-/datum/action/toggle_seethrough/Trigger(trigger_flags)
- . = ..()
- if(!.)
- return
- var/datum/component/seethrough_mob/seethroughComp = target
- seethroughComp.toggle_active()
+/datum/action/cooldown/toggle_seethrough/Activate(atom/t)
+ StartCooldown()
+ var/datum/component/seethrough_mob/transparency = target
+ transparency.toggle_active()
diff --git a/code/datums/components/shovel_hands.dm b/code/datums/components/shovel_hands.dm
index e4ee2d644d377..34eaf8a98b60a 100644
--- a/code/datums/components/shovel_hands.dm
+++ b/code/datums/components/shovel_hands.dm
@@ -14,10 +14,10 @@
/datum/component/shovel_hands/RegisterWithParent()
. = ..()
- RegisterSignals(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET), PROC_REF(dig))
+ RegisterSignals(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET), PROC_REF(dig))
/datum/component/shovel_hands/UnregisterFromParent()
- UnregisterSignal(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
+ UnregisterSignal(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
return ..()
/datum/component/shovel_hands/Destroy(force, silent)
diff --git a/code/datums/components/shy.dm b/code/datums/components/shy.dm
index 845da7e0f7da1..5743322dea18d 100644
--- a/code/datums/components/shy.dm
+++ b/code/datums/components/shy.dm
@@ -46,7 +46,7 @@
/datum/component/shy/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOB_CLICKON, PROC_REF(on_clickon))
RegisterSignal(parent, COMSIG_LIVING_TRY_PULL, PROC_REF(on_try_pull))
- RegisterSignals(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_EARLY_UNARMED_ATTACK), PROC_REF(on_unarmed_attack))
+ RegisterSignal(parent, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_unarmed_attack))
RegisterSignal(parent, COMSIG_TRY_STRIP, PROC_REF(on_try_strip))
RegisterSignal(parent, COMSIG_TRY_ALT_ACTION, PROC_REF(on_try_alt_action))
@@ -54,7 +54,7 @@
UnregisterSignal(parent, list(
COMSIG_MOB_CLICKON,
COMSIG_LIVING_TRY_PULL,
- COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_EARLY_UNARMED_ATTACK,
+ COMSIG_LIVING_UNARMED_ATTACK,
COMSIG_TRY_STRIP,
COMSIG_TRY_ALT_ACTION,
))
@@ -137,4 +137,3 @@
return is_shy(target) && COMPONENT_CANT_ALT_ACTION
#undef SHY_COMPONENT_CACHE_TIME
-
diff --git a/code/datums/components/shy_in_room.dm b/code/datums/components/shy_in_room.dm
index 2413c9abafd72..023dbaff71973 100644
--- a/code/datums/components/shy_in_room.dm
+++ b/code/datums/components/shy_in_room.dm
@@ -17,7 +17,7 @@
/datum/component/shy_in_room/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOB_CLICKON, PROC_REF(on_clickon))
RegisterSignal(parent, COMSIG_LIVING_TRY_PULL, PROC_REF(on_try_pull))
- RegisterSignals(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_EARLY_UNARMED_ATTACK), PROC_REF(on_unarmed_attack))
+ RegisterSignal(parent, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_unarmed_attack))
RegisterSignal(parent, COMSIG_TRY_STRIP, PROC_REF(on_try_strip))
RegisterSignal(parent, COMSIG_TRY_ALT_ACTION, PROC_REF(on_try_alt_action))
@@ -27,7 +27,6 @@
COMSIG_MOB_CLICKON,
COMSIG_LIVING_TRY_PULL,
COMSIG_LIVING_UNARMED_ATTACK,
- COMSIG_HUMAN_EARLY_UNARMED_ATTACK,
COMSIG_TRY_STRIP,
COMSIG_TRY_ALT_ACTION,
))
@@ -73,4 +72,3 @@
/datum/component/shy_in_room/proc/on_try_alt_action(datum/source, atom/target)
SIGNAL_HANDLER
return is_shy(target) && COMPONENT_CANT_ALT_ACTION
-
diff --git a/code/datums/components/singularity.dm b/code/datums/components/singularity.dm
index 75fd4fd0abc95..41b11a219c0e7 100644
--- a/code/datums/components/singularity.dm
+++ b/code/datums/components/singularity.dm
@@ -101,7 +101,7 @@
)
AddComponent(/datum/component/connect_loc_behalf, parent, loc_connections)
- RegisterSignal(parent, COMSIG_ATOM_BULLET_ACT, PROC_REF(consume_bullets))
+ RegisterSignal(parent, COMSIG_ATOM_PRE_BULLET_ACT, PROC_REF(consume_bullets))
if (notify_admins)
admin_investigate_setup()
@@ -127,7 +127,7 @@
COMSIG_ATOM_ATTACK_PAW,
COMSIG_ATOM_BLOB_ACT,
COMSIG_ATOM_BSA_BEAM,
- COMSIG_ATOM_BULLET_ACT,
+ COMSIG_ATOM_PRE_BULLET_ACT,
COMSIG_ATOM_BUMPED,
COMSIG_MOVABLE_PRE_MOVE,
COMSIG_ATOM_ATTACKBY,
@@ -180,6 +180,7 @@
SIGNAL_HANDLER
qdel(projectile)
+ return COMPONENT_BULLET_BLOCKED
/// Calls singularity_act on the thing passed, usually destroying the object
/datum/component/singularity/proc/default_singularity_act(atom/thing)
diff --git a/code/datums/components/slippery.dm b/code/datums/components/slippery.dm
index be1d674eb4d0b..2119c8ad7e191 100644
--- a/code/datums/components/slippery.dm
+++ b/code/datums/components/slippery.dm
@@ -1,17 +1,33 @@
-/// Slippery component, for making anything slippery. Of course.
+/**
+ * # Slip behaviour component
+ *
+ * Add this component to an object to make it a slippery object, slippery objects make mobs that cross them fall over.
+ * Items with this component that get picked up may give their parent mob the slip behaviour.
+ *
+ * Here is a simple example of adding the component behaviour to an object.area
+ *
+ * AddComponent(/datum/component/slippery, 80, (NO_SLIP_WHEN_WALKING | SLIDE))
+ *
+ * This adds slippery behaviour to the parent atom, with a 80 decisecond (~8 seconds) knockdown
+ * The lube flags control how the slip behaves, in this case, the mob wont slip if it's in walking mode (NO_SLIP_WHEN_WALKING)
+ * and if they do slip, they will slide a few tiles (SLIDE)
+ *
+ *
+ * This component has configurable behaviours, see the [Initialize proc for the argument listing][/datum/component/slippery/proc/Initialize].
+ */
/datum/component/slippery
dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
- /// If the slip forces you to drop held items.
+ /// If the slip forces the crossing mob to drop held items.
var/force_drop_items = FALSE
- /// How long the slip keeps you knocked down.
+ /// How long the slip keeps the crossing mob knocked over (they can still crawl and use weapons) for.
var/knockdown_time = 0
- /// How long the slip paralyzes for.
+ /// How long the slip paralyzes (prevents the crossing mob doing anything) for.
var/paralyze_time = 0
/// Flags for how slippery the parent is. See [__DEFINES/mobs.dm]
var/lube_flags
- /// Optional callback providing an additional chance to prevent slippage
+ /// Optional callback allowing you to define custom conditions for slipping
var/datum/callback/can_slip_callback
- /// A proc callback to call on slip.
+ /// Optional call back that is called when a mob slips on this component
var/datum/callback/on_slip_callback
/// If parent is an item, this is the person currently holding/wearing the parent (or the parent if no one is holding it)
var/mob/living/holder
@@ -30,6 +46,20 @@
/// The connect_loc_behalf component for the holder_connections list.
var/datum/weakref/holder_connect_loc_behalf
+/**
+ * Initialize the slippery component behaviour
+ *
+ * When applied to any atom in the game this will apply slipping behaviours to that atom
+ *
+ * Arguments:
+ * * knockdown - Length of time the knockdown applies (Deciseconds)
+ * * lube_flags - Controls the slip behaviour, they are listed starting [here][SLIDE]
+ * * datum/callback/on_slip_callback - Callback to define further custom controls on when slipping is applied
+ * * paralyze - length of time to paralyze the crossing mob for (Deciseconds)
+ * * force_drop - should the crossing mob drop items in it's hands or not
+ * * slot_whitelist - flags controlling where on a mob this item can be equipped to make the parent mob slippery full list [here][ITEM_SLOT_OCLOTHING]
+ * * datum/callback/on_slip_callback - Callback to add custom behaviours as the crossing mob is slipped
+ */
/datum/component/slippery/Initialize(
knockdown,
lube_flags = NONE,
@@ -115,11 +145,12 @@
src.can_slip_callback = can_slip_callback
if(slot_whitelist)
src.slot_whitelist = slot_whitelist
-/*
+/**
* The proc that does the sliping. Invokes the slip callback we have set.
*
- * source - the source of the signal
- * AM - the atom/movable that is being slipped.
+ * Arguments
+ * * source - the source of the signal
+ * * arrived - the atom/movable that is being slipped.
*/
/datum/component/slippery/proc/Slip(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
SIGNAL_HANDLER
@@ -137,14 +168,15 @@
if(victim.slip(knockdown_time, parent, lube_flags, paralyze_time, force_drop_items))
on_slip_callback?.Invoke(victim)
-/*
+/**
* Gets called when COMSIG_ITEM_EQUIPPED is sent to parent.
* This proc register slip signals to the equipper.
* If we have a slot whitelist, we only register the signals if the slot is valid (ex: clown PDA only slips in ID or belt slot).
*
- * source - the source of the signal
- * equipper - the mob we're equipping the slippery thing to
- * slot - the slot we're equipping the slippery thing to on the equipper.
+ * Arguments
+ * * source - the source of the signal
+ * * equipper - the mob we're equipping the slippery thing to
+ * * slot - the slot we're equipping the slippery thing to on the equipper.
*/
/datum/component/slippery/proc/on_equip(datum/source, mob/equipper, slot)
SIGNAL_HANDLER
@@ -155,12 +187,13 @@
AddComponent(/datum/component/connect_loc_behalf, holder, holder_connections)
RegisterSignal(holder, COMSIG_QDELETING, PROC_REF(holder_deleted))
-/*
+/**
* Detects if the holder mob is deleted.
* If our holder mob is the holder set in this component, we null it.
*
- * source - the source of the signal
- * possible_holder - the mob being deleted.
+ * Arguments:
+ * * source - the source of the signal
+ * * possible_holder - the mob being deleted.
*/
/datum/component/slippery/proc/holder_deleted(datum/source, datum/possible_holder)
SIGNAL_HANDLER
@@ -168,12 +201,13 @@
if(possible_holder == holder)
holder = null
-/*
+/**
* Gets called when COMSIG_ITEM_DROPPED is sent to parent.
* Makes our holder mob un-slippery.
*
- * source - the source of the signal
- * user - the mob that was formerly wearing our slippery item.
+ * Arguments:
+ * * source - the source of the signal
+ * * user - the mob that was formerly wearing our slippery item.
*/
/datum/component/slippery/proc/on_drop(datum/source, mob/user)
SIGNAL_HANDLER
@@ -185,12 +219,13 @@
holder = null
-/*
+/**
* The slip proc, but for equipped items.
* Slips the person who crossed us if we're lying down and unbuckled.
*
- * source - the source of the signal
- * AM - the atom/movable that slipped on us.
+ * Arguments:
+ * * source - the source of the signal
+ * * arrived - the atom/movable that slipped on us.
*/
/datum/component/slippery/proc/Slip_on_wearer(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
SIGNAL_HANDLER
diff --git a/code/datums/components/spirit_holding.dm b/code/datums/components/spirit_holding.dm
index a4d0e02913740..37186825d621a 100644
--- a/code/datums/components/spirit_holding.dm
+++ b/code/datums/components/spirit_holding.dm
@@ -7,7 +7,7 @@
///bool on if this component is currently polling for observers to inhabit the item
var/attempting_awakening = FALSE
///mob contained in the item.
- var/mob/living/simple_animal/shade/bound_spirit
+ var/mob/living/basic/shade/bound_spirit
/datum/component/spirit_holding/Initialize()
if(!ismovable(parent)) //you may apply this to mobs, i take no responsibility for how that works out
@@ -37,41 +37,46 @@
///signal fired on self attacking parent
/datum/component/spirit_holding/proc/on_attack_self(datum/source, mob/user)
SIGNAL_HANDLER
- INVOKE_ASYNC(src, PROC_REF(attempt_spirit_awaken), user)
-/**
- * attempt_spirit_awaken: called from on_attack_self, polls ghosts to possess the item in the form
- * of a mob sitting inside the item itself
- *
- * Arguments:
- * * awakener: user who interacted with the blade
- */
-/datum/component/spirit_holding/proc/attempt_spirit_awaken(mob/awakener)
+ var/atom/thing = parent
+
if(attempting_awakening)
- to_chat(awakener, span_warning("You are already trying to awaken [parent]!"))
+ thing.balloon_alert(user, "already channeling!")
return
if(!(GLOB.ghost_role_flags & GHOSTROLE_STATION_SENTIENCE))
- to_chat(awakener, span_warning("Anomalous otherworldly energies block you from awakening [parent]!"))
+ thing.balloon_alert(user, "spirits are unwilling!")
+ to_chat(user, span_warning("Anomalous otherworldly energies block you from awakening [parent]!"))
return
attempting_awakening = TRUE
- to_chat(awakener, span_notice("You attempt to wake the spirit of [parent]..."))
-
- var/mob/dead/observer/candidates = poll_ghost_candidates("Do you want to play as the spirit of [awakener.real_name]'s blade?", ROLE_PAI, FALSE, 100, POLL_IGNORE_POSSESSED_BLADE)
- if(!LAZYLEN(candidates))
- to_chat(awakener, span_warning("[parent] is dormant. Maybe you can try again later."))
+ thing.balloon_alert(user, "channeling...")
+
+ var/datum/callback/to_call = CALLBACK(src, PROC_REF(affix_spirit), user)
+ parent.AddComponent(/datum/component/orbit_poll, \
+ ignore_key = POLL_IGNORE_POSSESSED_BLADE, \
+ job_bans = ROLE_PAI, \
+ to_call = to_call, \
+ title = "Spirit of [user.real_name]'s blade", \
+ )
+
+/// On conclusion of the ghost poll
+/datum/component/spirit_holding/proc/affix_spirit(mob/awakener, mob/dead/observer/ghost)
+ var/atom/thing = parent
+
+ if(isnull(ghost))
+ thing.balloon_alert(awakener, "silence...")
attempting_awakening = FALSE
return
- //Immediately unregister to prevent making a new spirit
+ // Immediately unregister to prevent making a new spirit
UnregisterSignal(parent, COMSIG_ITEM_ATTACK_SELF)
- var/mob/dead/observer/chosen_spirit = pick(candidates)
if(QDELETED(parent)) //if the thing that we're conjuring a spirit in has been destroyed, don't create a spirit
- to_chat(chosen_spirit, span_userdanger("The new vessel for your spirit has been destroyed! You remain an unbound ghost."))
+ to_chat(ghost, span_userdanger("The new vessel for your spirit has been destroyed! You remain an unbound ghost."))
return
+
bound_spirit = new(parent)
- bound_spirit.ckey = chosen_spirit.ckey
+ bound_spirit.ckey = ghost.ckey
bound_spirit.fully_replace_character_name(null, "The spirit of [parent]")
bound_spirit.status_flags |= GODMODE
bound_spirit.copy_languages(awakener, LANGUAGE_MASTER) //Make sure the sword can understand and communicate with the awakener.
diff --git a/code/datums/components/style/style.dm b/code/datums/components/style/style.dm
index 3894f77311e17..240dbaae4ace1 100644
--- a/code/datums/components/style/style.dm
+++ b/code/datums/components/style/style.dm
@@ -105,7 +105,7 @@
RegisterSignal(parent, COMSIG_MOB_EMOTED("flip"), PROC_REF(on_flip))
RegisterSignal(parent, COMSIG_MOB_EMOTED("spin"), PROC_REF(on_spin))
RegisterSignal(parent, COMSIG_MOB_ITEM_ATTACK, PROC_REF(on_attack))
- RegisterSignal(parent, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, PROC_REF(on_punch))
+ RegisterSignal(parent, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_punch))
RegisterSignal(SSdcs, COMSIG_GLOB_MOB_DEATH, PROC_REF(on_death))
RegisterSignal(parent, COMSIG_LIVING_RESONATOR_BURST, PROC_REF(on_resonator_burst))
RegisterSignal(parent, COMSIG_LIVING_PROJECTILE_PARRIED, PROC_REF(on_projectile_parry))
@@ -132,7 +132,7 @@
UnregisterSignal(parent, COMSIG_MOB_MINED)
UnregisterSignal(parent, COMSIG_MOB_APPLY_DAMAGE)
UnregisterSignal(parent, list(COMSIG_MOB_EMOTED("flip"), COMSIG_MOB_EMOTED("spin")))
- UnregisterSignal(parent, list(COMSIG_MOB_ITEM_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK))
+ UnregisterSignal(parent, list(COMSIG_MOB_ITEM_ATTACK, COMSIG_LIVING_UNARMED_ATTACK))
UnregisterSignal(SSdcs, COMSIG_GLOB_MOB_DEATH)
UnregisterSignal(parent, COMSIG_LIVING_RESONATOR_BURST)
UnregisterSignal(parent, COMSIG_LIVING_PROJECTILE_PARRIED)
@@ -453,7 +453,7 @@
// Negative effects
-/datum/component/style/proc/on_take_damage()
+/datum/component/style/proc/on_take_damage(...)
SIGNAL_HANDLER
point_multiplier = round(max(point_multiplier - 0.3, 1), 0.1)
diff --git a/code/datums/components/summoning.dm b/code/datums/components/summoning.dm
index 54eea882e8a26..220a4baca5f9a 100644
--- a/code/datums/components/summoning.dm
+++ b/code/datums/components/summoning.dm
@@ -1,16 +1,32 @@
/datum/component/summoning
+ /// Types of mob we can create
var/list/mob_types = list()
- var/spawn_chance // chance for the mob to spawn on hit in percent
+ /// Percentage chance to spawn a mob
+ var/spawn_chance
+ /// Maximum mobs we can have active at once
var/max_mobs
- var/spawn_delay // delay in spawning between mobs (deciseconds)
+ /// Cooldown between spawning mobs
+ var/spawn_delay
+ /// Text to display when spawning a mob
var/spawn_text
+ /// Sound to play when spawning a mob
var/spawn_sound
+ /// Factions to assign to a summoned mob
var/list/faction
-
- var/last_spawned_time = 0
+ /// Cooldown tracker for when we can summon another mob
+ COOLDOWN_DECLARE(summon_cooldown)
+ /// List containing all of our mobs
var/list/spawned_mobs = list()
-/datum/component/summoning/Initialize(mob_types, spawn_chance=100, max_mobs=3, spawn_delay=100, spawn_text="appears out of nowhere", spawn_sound='sound/magic/summon_magic.ogg', faction)
+/datum/component/summoning/Initialize(
+ mob_types,
+ spawn_chance = 100,
+ max_mobs = 3,
+ spawn_delay = 10 SECONDS,
+ spawn_text = "appears out of nowhere",
+ spawn_sound = 'sound/magic/summon_magic.ogg',
+ list/faction,
+)
if(!isitem(parent) && !ishostile(parent) && !isgun(parent) && !ismachinery(parent) && !isstructure(parent) && !isprojectilespell(parent))
return COMPONENT_INCOMPATIBLE
@@ -54,26 +70,22 @@
do_spawn_mob(get_turf(target), firer)
/datum/component/summoning/proc/do_spawn_mob(atom/spawn_location, summoner)
- if(spawned_mobs.len >= max_mobs)
- return
- if(last_spawned_time > world.time)
+ if(length(spawned_mobs) >= max_mobs || !COOLDOWN_FINISHED(src, summon_cooldown) || !prob(spawn_chance))
return
- if(!prob(spawn_chance))
- return
- last_spawned_time = world.time + spawn_delay
+ COOLDOWN_START(src, summon_cooldown, spawn_delay)
var/chosen_mob_type = pick(mob_types)
- var/mob/living/simple_animal/L = new chosen_mob_type(spawn_location)
- if(ishostile(L))
- var/mob/living/simple_animal/hostile/H = L
- H.friends += summoner // do not attack our summon boy
- spawned_mobs += L
+ var/mob/living/summoned = new chosen_mob_type(spawn_location)
+ if(ishostile(summoned))
+ var/mob/living/simple_animal/hostile/angry_boy = summoned
+ angry_boy.friends |= summoner // do not attack our summon boy
+ spawned_mobs |= summoned
if(faction != null)
- L.faction = faction
- RegisterSignal(L, COMSIG_LIVING_DEATH, PROC_REF(on_spawned_death)) // so we can remove them from the list, etc (for mobs with corpses)
- playsound(spawn_location,spawn_sound, 50, TRUE)
- spawn_location.visible_message(span_danger("[L] [spawn_text]."))
+ summoned.faction = faction.Copy()
+ RegisterSignals(summoned, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING), PROC_REF(on_spawned_death))
+ spawn_location.visible_message(span_danger("[summoned] [spawn_text]!"))
+/// When a spawned thing dies, remove it from our list
/datum/component/summoning/proc/on_spawned_death(mob/killed, gibbed)
SIGNAL_HANDLER
-
+ UnregisterSignal(killed, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING))
spawned_mobs -= killed
diff --git a/code/datums/components/supermatter_crystal.dm b/code/datums/components/supermatter_crystal.dm
index 38968d1e3d1f5..3c37bba33cb8d 100644
--- a/code/datums/components/supermatter_crystal.dm
+++ b/code/datums/components/supermatter_crystal.dm
@@ -302,7 +302,12 @@
consumed_mob.investigate_log("has been dusted by [atom_source].", INVESTIGATE_DEATHS)
if(istype(consumed_mob, /mob/living/simple_animal/parrot/poly)) // Dusting Poly creates a power surge
force_event(/datum/round_event_control/supermatter_surge/poly, "Poly's revenge")
- notify_ghosts("[consumed_mob] has been dusted by [atom_source]!", source = atom_source, action = NOTIFY_JUMP, header = "Polytechnical Difficulties")
+ notify_ghosts(
+ "[consumed_mob] has been dusted by [atom_source]!",
+ source = atom_source,
+ header = "Polytechnical Difficulties",
+ notify_flags = NOTIFY_CATEGORY_DEFAULT,
+ )
consumed_mob.dust(force = TRUE)
matter_increase += 100 * object_size
if(is_clown_job(consumed_mob.mind?.assigned_role))
diff --git a/code/datums/components/torn_wall.dm b/code/datums/components/torn_wall.dm
new file mode 100644
index 0000000000000..ebe0fe9a0a6e9
--- /dev/null
+++ b/code/datums/components/torn_wall.dm
@@ -0,0 +1,99 @@
+
+#define TORN_WALL_RUINED 2
+#define TORN_WALL_DAMAGED 1
+#define TORN_WALL_INITIAL 0
+
+/**
+ * Component applied to a wall to progressively destroy it.
+ * If component is applied to something which already has it, stage increases.
+ * Wall is destroyed on third application.
+ * Can be fixed using a welder
+ */
+/datum/component/torn_wall
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ var/current_stage = TORN_WALL_INITIAL
+
+/datum/component/torn_wall/Initialize()
+ . = ..()
+ if (!iswallturf(parent) || isindestructiblewall(parent))
+ return COMPONENT_INCOMPATIBLE
+
+/datum/component/torn_wall/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined))
+ RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_WELDER), PROC_REF(on_welded))
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_update_overlays))
+ RegisterSignal(parent, COMSIG_TURF_CHANGE, PROC_REF(on_turf_changed))
+ apply_visuals()
+
+/datum/component/torn_wall/UnregisterFromParent()
+ var/atom/atom_parent = parent
+ UnregisterSignal(parent, list(
+ COMSIG_ATOM_EXAMINE,
+ COMSIG_ATOM_TOOL_ACT(TOOL_WELDER),
+ COMSIG_ATOM_UPDATE_OVERLAYS,
+ COMSIG_TURF_CHANGE,
+ ))
+ atom_parent.update_appearance(UPDATE_ICON)
+
+/datum/component/torn_wall/InheritComponent(datum/component/C, i_am_original)
+ increase_stage()
+
+/// Play a fun animation and make our wall look damaged
+/datum/component/torn_wall/proc/apply_visuals()
+ var/atom/atom_parent = parent
+ playsound(atom_parent, 'sound/effects/bang.ogg', 50, vary = TRUE)
+ atom_parent.update_appearance(UPDATE_ICON)
+ atom_parent.Shake(shake_interval = 0.1 SECONDS, duration = 0.5 SECONDS)
+
+/// Make the effect more dramatic
+/datum/component/torn_wall/proc/increase_stage()
+ current_stage++
+ if (current_stage != TORN_WALL_RUINED)
+ apply_visuals()
+ return
+ var/turf/closed/wall/attached_wall = parent
+ playsound(attached_wall, 'sound/effects/meteorimpact.ogg', 100, vary = TRUE)
+ attached_wall.dismantle_wall(devastated = TRUE)
+
+/// Fix it up on weld
+/datum/component/torn_wall/proc/on_welded(atom/source, mob/user, obj/item/tool)
+ SIGNAL_HANDLER
+ INVOKE_ASYNC(src, PROC_REF(try_repair), source, user, tool)
+ return COMPONENT_BLOCK_TOOL_ATTACK
+
+/// Fix us up
+/datum/component/torn_wall/proc/try_repair(atom/source, mob/user, obj/item/tool)
+ source.balloon_alert(user, "repairing...")
+ if(!tool.use_tool(source, user, 5 SECONDS, amount = 2, volume = 50))
+ source.balloon_alert(user, "interrupted!")
+ return
+ current_stage--
+ if (current_stage < TORN_WALL_INITIAL)
+ qdel(src)
+ return
+ source.update_appearance(UPDATE_ICON)
+ source.tool_act(user, tool, TOOL_WELDER, is_right_clicking = FALSE) // Keep going
+
+/// Give them a hint
+/datum/component/torn_wall/proc/on_examined(atom/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ var/intensity = (current_stage == TORN_WALL_INITIAL) ? "slightly" : "badly"
+ examine_list += span_notice("It looks [intensity] damaged.")
+ examine_list += span_info("You may be able to repair it using a welding tool.")
+
+/// Show a little crack on here
+/datum/component/torn_wall/proc/on_update_overlays(turf/source, list/overlays)
+ SIGNAL_HANDLER
+ var/mutable_appearance/crack = mutable_appearance('icons/turf/overlays.dmi', "explodable", source.layer + 0.1)
+ if (current_stage == TORN_WALL_INITIAL)
+ crack.alpha *= 0.5
+ overlays += crack
+
+/// If the wall becomes any other turf, delete us. Transforming into a different works fine as a fix.
+/datum/component/torn_wall/proc/on_turf_changed()
+ SIGNAL_HANDLER
+ qdel(src)
+
+#undef TORN_WALL_RUINED
+#undef TORN_WALL_DAMAGED
+#undef TORN_WALL_INITIAL
diff --git a/code/datums/components/trader/trader.dm b/code/datums/components/trader/trader.dm
new file mode 100644
index 0000000000000..b10041385277d
--- /dev/null
+++ b/code/datums/components/trader/trader.dm
@@ -0,0 +1,457 @@
+#define TRADER_RADIAL_BUY "TRADER_RADIAL_BUY"
+#define TRADER_RADIAL_SELL "TRADER_RADIAL_SELL"
+#define TRADER_RADIAL_TALK "TRADER_RADIAL_TALK"
+#define TRADER_RADIAL_LORE "TRADER_RADIAL_LORE"
+#define TRADER_RADIAL_NO "TRADER_RADIAL_NO"
+#define TRADER_RADIAL_YES "TRADER_RADIAL_YES"
+#define TRADER_RADIAL_OUT_OF_STOCK "TRADER_RADIAL_OUT_OF_STOCK"
+#define TRADER_RADIAL_DISCUSS_BUY "TRADER_RADIAL_DISCUSS_BUY"
+#define TRADER_RADIAL_DISCUSS_SELL "TRADER_RADIAL_DISCUSS_SELL"
+
+#define TRADER_OPTION_BUY "Buy"
+#define TRADER_OPTION_SELL "Sell"
+#define TRADER_OPTION_TALK "Talk"
+#define TRADER_OPTION_LORE "Lore"
+#define TRADER_OPTION_NO "No"
+#define TRADER_OPTION_YES "Yes"
+#define TRADER_OPTION_BUYING "Buying?"
+#define TRADER_OPTION_SELLING "Selling?"
+
+//The defines below show the index the info is located in the product_info entry list
+
+#define TRADER_PRODUCT_INFO_PRICE 1
+#define TRADER_PRODUCT_INFO_QUANTITY 2
+//Only valid for wanted_items
+#define TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION 3
+
+/**
+ * # Trader NPC Component
+ * Manages the barks and the stocks of the traders
+ * Also manages the interactive radial menu
+ */
+/datum/component/trader
+
+ /**
+ * Format; list(TYPEPATH = list(PRICE, QUANTITY))
+ * Associated list of items the NPC sells with how much they cost and the quantity available before a restock
+ * This list is filled by Initialize(), if you want to change the starting products, modify initial_products()
+ * *
+ */
+ var/list/obj/item/products = list()
+ /**
+ * A list of wanted items that the trader would wish to buy, each typepath has a assigned value, quantity and additional flavor text
+ *
+ * CHILDREN OF TYPEPATHS INCLUDED IN WANTED_ITEMS WILL BE TREATED AS THE PARENT IF NO ENTRY EXISTS FOR THE CHILDREN
+ *
+ * As an additional note; if you include multiple children of a typepath; the typepath with the most children should be placed after all other typepaths
+ * Bad; list(/obj/item/milk = list(100, 1, ""), /obj/item/milk/small = list(50, 2, ""))
+ * Good; list(/obj/item/milk/small = list(50, 2, ""), /obj/item/milk = list(100, 1, ""))
+ * This is mainly because sell_item() uses a istype(item_being_sold, item_in_entry) to determine what parent should the child be automatically considered as
+ * If /obj/item/milk/small/spooky was being sold; /obj/item/milk/small would be the first to check against rather than /obj/item/milk
+ *
+ * Format; list(TYPEPATH = list(PRICE, QUANTITY, ADDITIONAL_DESCRIPTION))
+ * Associated list of items able to be sold to the NPC with the money given for them.
+ * The price given should be the "base" price; any price manipulation based on variables should be done with apply_sell_price_mods()
+ * ADDITIONAL_DESCRIPTION is any additional text added to explain how the variables of the item effect the price; if it's stack based, it's final price depends how much is in the stack
+ * EX; /obj/item/stack/sheet/mineral/diamond = list(500, INFINITY, ", per 100 cm3 sheet of diamond")
+ * This list is filled by Initialize(), if you want to change the starting wanted items, modify initial_wanteds()
+ */
+ var/list/wanted_items = list()
+
+ ///Contains images of all radial icons
+ var/static/list/radial_icons_cache = list()
+
+ ///Contains information of a specific trader
+ var/datum/trader_data/trader_data
+
+/*
+Can accept both a type path, and an instance of a datum. Type path has priority.
+*/
+/datum/component/trader/Initialize(trader_data_path = null, trader_data = null)
+ . = ..()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ if(ispath(trader_data_path, /datum/trader_data))
+ trader_data = new trader_data_path
+ if(isnull(trader_data))
+ CRASH("Initialised trader component with no trader data.")
+
+ src.trader_data = trader_data
+
+ radial_icons_cache = list(
+ TRADER_RADIAL_BUY = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_buy"),
+ TRADER_RADIAL_SELL = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_sell"),
+ TRADER_RADIAL_TALK = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_talk"),
+ TRADER_RADIAL_LORE = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_lore"),
+ TRADER_RADIAL_DISCUSS_BUY = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_buying"),
+ TRADER_RADIAL_DISCUSS_SELL = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_selling"),
+ TRADER_RADIAL_YES = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"),
+ TRADER_RADIAL_NO = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no"),
+ TRADER_RADIAL_OUT_OF_STOCK = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_center"),
+ )
+
+ restock_products()
+ renew_item_demands()
+
+/datum/component/trader/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_attack_hand))
+
+/datum/component/trader/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_ATOM_ATTACK_HAND)
+
+///If our trader is alive, and the customer left clicks them with an empty hand without combat mode
+/datum/component/trader/proc/on_attack_hand(atom/source, mob/living/carbon/customer)
+ SIGNAL_HANDLER
+ if(!can_trade(customer) || customer.combat_mode)
+ return
+ var/list/npc_options = list()
+ if(length(products))
+ npc_options[TRADER_OPTION_BUY] = radial_icons_cache[TRADER_RADIAL_BUY]
+ if(length(wanted_items))
+ npc_options[TRADER_OPTION_SELL] = radial_icons_cache[TRADER_RADIAL_SELL]
+ if(length(trader_data.say_phrases))
+ npc_options[TRADER_OPTION_TALK] = radial_icons_cache[TRADER_RADIAL_TALK]
+ if(!length(npc_options))
+ return
+
+ var/mob/living/trader = parent
+ trader.face_atom(customer)
+
+ INVOKE_ASYNC(src, PROC_REF(open_npc_options), customer, npc_options)
+
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/**
+ * Generates a radial of the initial radials of the NPC
+ * Called via asynch, due to the sleep caused by show_radial_menu
+ * Arguments:
+ * * customer - (Mob REF) The mob trying to buy something
+ */
+/datum/component/trader/proc/open_npc_options(mob/living/carbon/customer, list/npc_options)
+ if(!can_trade(customer))
+ return
+ var/npc_result = show_radial_menu(customer, parent, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE)
+ switch(npc_result)
+ if(TRADER_OPTION_BUY)
+ buy_item(customer)
+ if(TRADER_OPTION_SELL)
+ try_sell(customer)
+ if(TRADER_OPTION_TALK)
+ discuss(customer)
+
+/**
+ * Checks if the customer is ok to use the radial
+ *
+ * Checks if the customer is not a mob or is incapacitated or not adjacent to the source of the radial, in those cases returns FALSE, otherwise returns TRUE
+ * Arguments:
+ * * customer - (Mob REF) The mob checking the menu
+ */
+/datum/component/trader/proc/check_menu(mob/customer)
+ if(!istype(customer))
+ return FALSE
+ if(IS_DEAD_OR_INCAP(customer) || !customer.Adjacent(parent))
+ return FALSE
+ return TRUE
+
+/**
+ * Generates a radial of the items the NPC sells and lets the user try to buy one
+ * Arguments:
+ * * customer - (Mob REF) The mob trying to buy something
+ */
+/datum/component/trader/proc/buy_item(mob/customer)
+ if(!can_trade(customer))
+ return
+
+ if(!LAZYLEN(products))
+ return
+
+ var/list/display_names = list()
+ var/list/items = list()
+ var/list/product_info
+
+ for(var/obj/item/thing as anything in products)
+ display_names["[initial(thing.name)]"] = thing
+
+ if(!radial_icons_cache[thing])
+ radial_icons_cache[thing] = image(icon = initial(thing.icon), icon_state = initial(thing.icon_state_preview) ? initial(thing.icon_state_preview) : initial(thing.icon_state))
+
+ var/image/item_image = radial_icons_cache[thing]
+ product_info = products[thing]
+
+ if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //out of stock
+ item_image.overlays += radial_icons_cache[TRADER_RADIAL_OUT_OF_STOCK]
+
+ items += list("[initial(thing.name)]" = item_image)
+
+ var/pick = show_radial_menu(customer, parent, items, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE)
+ if(!pick || !can_trade(customer))
+ return
+
+ var/obj/item/item_to_buy = display_names[pick]
+ var/mob/living/trader = parent
+ trader.face_atom(customer)
+ product_info = products[item_to_buy]
+
+ if(!product_info[TRADER_PRODUCT_INFO_QUANTITY])
+ trader.say("[initial(item_to_buy.name)] appears to be out of stock.")
+ return
+
+ trader.say("It will cost you [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name] to buy \the [initial(item_to_buy.name)]. Are you sure you want to buy it?")
+ var/list/npc_options = list(
+ TRADER_OPTION_YES = radial_icons_cache[TRADER_RADIAL_YES],
+ TRADER_OPTION_NO = radial_icons_cache[TRADER_RADIAL_NO],
+ )
+
+ var/buyer_will_buy = show_radial_menu(customer, trader, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE)
+ if(buyer_will_buy != TRADER_OPTION_YES || !can_trade(customer))
+ return
+
+ trader.face_atom(customer)
+
+ if(!spend_buyer_offhand_money(customer, product_info[TRADER_PRODUCT_INFO_PRICE]))
+ trader.say(trader_data.return_trader_phrase(NO_CASH_PHRASE))
+ return
+
+ item_to_buy = new item_to_buy(get_turf(customer))
+ customer.put_in_hands(item_to_buy)
+ playsound(trader, trader_data.sell_sound, 50, TRUE)
+ log_econ("[item_to_buy] has been sold to [customer] (typepath used for product info; [item_to_buy.type]) by [trader] for [product_info[TRADER_PRODUCT_INFO_PRICE]] cash.")
+ product_info[TRADER_PRODUCT_INFO_QUANTITY] -= 1
+ trader.say(trader_data.return_trader_phrase(BUY_PHRASE))
+
+///Calculates the value of money in the hand of the buyer and spends it if it's sufficient
+/datum/component/trader/proc/spend_buyer_offhand_money(mob/customer, the_cost)
+ var/value = 0
+ var/obj/item/holochip/cash = customer.is_holding_item_of_type(/obj/item/holochip)
+ if(cash)
+ value += cash.credits
+ if((value >= the_cost) && cash)
+ return cash.spend(the_cost)
+ return FALSE //Purchase unsuccessful
+
+/**
+ * Tries to call sell_item on one of the customer's held items, if fail gives a chat message
+ *
+ * Gets both items in the customer's hands, and then tries to call sell_item on them, if both fail, he gives a chat message
+ * Arguments:
+ * * customer - (Mob REF) The mob trying to sell something
+ */
+/datum/component/trader/proc/try_sell(mob/customer)
+ if(!can_trade(customer))
+ return
+ var/sold_item = FALSE
+ for(var/obj/item/an_item in customer.held_items)
+ if(sell_item(customer, an_item))
+ sold_item = TRUE
+ break
+ if(!sold_item && can_trade(customer)) //only talk if you are not dead or in combat
+ var/mob/living/trader = parent
+ trader.say(trader_data.return_trader_phrase(ITEM_REJECTED_PHRASE))
+
+
+/**
+ * Checks if an item is in the list of wanted items and if it is after a Yes/No radial returns generate_cash with the value of the item for the NPC
+ * Arguments:
+ * * customer - (Mob REF) The mob trying to sell something
+ * * selling - (Item REF) The item being sold
+ */
+/datum/component/trader/proc/sell_item(mob/customer, obj/item/selling)
+ if(isnull(selling))
+ return FALSE
+ var/list/product_info
+ //Keep track of the typepath; rather mundane but it's required for correctly modifying the wanted_items
+ //should a product be sellable because even if it doesn't have a entry because it's a child of a parent that is present on the list
+ var/typepath_for_product_info
+
+ if(selling.type in wanted_items)
+ product_info = wanted_items[selling.type]
+ typepath_for_product_info = selling.type
+ else //Assume wanted_items is setup in the correct way; read wanted_items documentation for more info
+ for(var/typepath in wanted_items)
+ if(!istype(selling, typepath))
+ continue
+
+ product_info = wanted_items[typepath]
+ typepath_for_product_info = typepath
+ break
+
+ if(!product_info) //Nothing interesting to sell
+ return FALSE
+
+ var/mob/living/trader = parent
+
+ if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0)
+ trader.say(trader_data.return_trader_phrase(TRADER_HAS_ENOUGH_ITEM_PHRASE))
+ return FALSE
+
+ var/cost = apply_sell_price_mods(selling, product_info[TRADER_PRODUCT_INFO_PRICE])
+ if(cost <= 0)
+ trader.say(trader_data.return_trader_phrase(ITEM_IS_WORTHLESS_PHRASE))
+ return FALSE
+
+ trader.say(trader_data.return_trader_phrase(INTERESTED_PHRASE))
+ trader.say("You will receive [cost] [trader_data.currency_name] for the [selling].")
+ var/list/npc_options = list(
+ TRADER_OPTION_YES = radial_icons_cache[TRADER_RADIAL_YES],
+ TRADER_OPTION_NO = radial_icons_cache[TRADER_RADIAL_NO],
+ )
+
+ trader.face_atom(customer)
+
+ var/npc_result = show_radial_menu(customer, trader, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE)
+ if(!can_trade(customer))
+ return
+ if(npc_result != TRADER_OPTION_YES)
+ trader.say(trader_data.return_trader_phrase(ITEM_SELLING_CANCELED_PHRASE))
+ return TRUE
+
+ trader.say(trader_data.return_trader_phrase(ITEM_SELLING_ACCEPTED_PHRASE))
+ playsound(trader, trader_data.sell_sound, 50, TRUE)
+ log_econ("[selling] has been sold to [trader] (typepath used for product info; [typepath_for_product_info]) by [customer] for [cost] cash.")
+ exchange_sold_items(selling, cost, typepath_for_product_info)
+ generate_cash(cost, customer)
+ return TRUE
+
+/**
+ * Modifies the 'base' price of a item based on certain variables
+ *
+ * Arguments:
+ * * Reference to the item; this is the item being sold
+ * * Original cost; the original cost of the item, to be manipulated depending on the variables of the item, one example is using item.amount if it's a stack
+ */
+/datum/component/trader/proc/apply_sell_price_mods(obj/item/selling, original_cost)
+ if(isstack(selling))
+ var/obj/item/stack/stackoverflow = selling
+ original_cost *= stackoverflow.amount
+ return original_cost
+
+/**
+ * Handles modifying/deleting the items to ensure that a proper amount is converted into cash; put into it's own proc to make the children of this not override a 30+ line sell_item()
+ *
+ * Arguments:
+ * * selling - (Item REF) this is the item being sold
+ * * value_exchanged_for - (Number) the "value", useful for a scenario where you want to remove enough items equal to the value
+ * * original_typepath - (Typepath) For scenarios where a children of a parent is being sold but we want to modify the parent's product information
+ */
+/datum/component/trader/proc/exchange_sold_items(obj/item/selling, value_exchanged_for, original_typepath)
+ var/list/product_info = wanted_items[original_typepath]
+ if(isstack(selling))
+ var/obj/item/stack/the_stack = selling
+ var/actually_sold = min(the_stack.amount, product_info[TRADER_PRODUCT_INFO_QUANTITY])
+ the_stack.use(actually_sold)
+ product_info[TRADER_PRODUCT_INFO_QUANTITY] -= (actually_sold)
+ else
+ qdel(selling)
+ product_info[TRADER_PRODUCT_INFO_QUANTITY] -= 1
+
+/**
+ * Creates an item equal to the value set by the proc and puts it in the user's hands if possible
+ * Arguments:
+ * * value - A number; The amount of cash that will be on the holochip
+ * * customer - Reference to a mob; The mob we put the holochip in hands of
+ */
+/datum/component/trader/proc/generate_cash(value, mob/customer)
+ var/obj/item/holochip/chip = new /obj/item/holochip(get_turf(customer), value)
+ customer.put_in_hands(chip)
+
+///Talk about what items are being sold/wanted by the trader and in what quantity or lore
+/datum/component/trader/proc/discuss(mob/customer)
+ var/list/npc_options = list(
+ TRADER_OPTION_LORE = radial_icons_cache[TRADER_RADIAL_LORE],
+ TRADER_OPTION_SELLING = radial_icons_cache[TRADER_RADIAL_DISCUSS_SELL],
+ TRADER_OPTION_BUYING = radial_icons_cache[TRADER_RADIAL_DISCUSS_BUY],
+ )
+ var/pick = show_radial_menu(customer, parent, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE)
+ if(!can_trade(customer))
+ return
+ switch(pick)
+ if(TRADER_OPTION_LORE)
+ var/mob/living/trader = parent
+ trader.say(trader_data.return_trader_phrase(TRADER_LORE_PHRASE))
+ if(TRADER_OPTION_BUYING)
+ trader_buys_what(customer)
+ if(TRADER_OPTION_SELLING)
+ trader_sells_what(customer)
+
+///Displays to the customer what the trader is willing to buy and how much until a restock happens
+/datum/component/trader/proc/trader_buys_what(mob/customer)
+ if(!can_trade(customer))
+ return
+ if(!length(wanted_items))
+ var/mob/living/trader = parent
+ trader.say(trader_data.return_trader_phrase(TRADER_NOT_BUYING_ANYTHING))
+ return
+
+ var/list/buy_info = list(span_green("I'm willing to buy the following:"))
+
+ var/list/product_info
+ for(var/obj/item/thing as anything in wanted_items)
+ product_info = wanted_items[thing]
+ var/tern_op_result = (product_info[TRADER_PRODUCT_INFO_QUANTITY] == INFINITY ? "as many as I can." : "[product_info[TRADER_PRODUCT_INFO_QUANTITY]]") //Coder friendly string concat
+ if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //Zero demand
+ buy_info += span_notice("• [span_red("(DOESN'T WANT MORE)")] [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name][product_info[TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION]]; willing to buy [span_red("[tern_op_result]")] more.")
+ else
+ buy_info += span_notice("• [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name][product_info[TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION]]; willing to buy [span_green("[tern_op_result]")]")
+
+ to_chat(customer, examine_block(buy_info.Join("\n")))
+
+///Displays to the customer what the trader is selling and how much is in stock
+/datum/component/trader/proc/trader_sells_what(mob/customer)
+ if(!can_trade(customer))
+ return
+ var/mob/living/trader = parent
+ if(!length(products))
+ trader.say(trader_data.return_trader_phrase(TRADER_NOT_SELLING_ANYTHING))
+ return
+ var/list/sell_info = list(span_green("I'm currently selling the following:"))
+ var/list/product_info
+ for(var/obj/item/thing as anything in products)
+ product_info = products[thing]
+ var/tern_op_result = (product_info[TRADER_PRODUCT_INFO_QUANTITY] == INFINITY ? "an infinite amount" : "[product_info[TRADER_PRODUCT_INFO_QUANTITY]]") //Coder friendly string concat
+ if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //Out of stock
+ sell_info += span_notice("• [span_red("(OUT OF STOCK)")] [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name]; [span_red("[tern_op_result]")] left in stock")
+ else
+ sell_info += span_notice("• [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name]; [span_green("[tern_op_result]")] left in stock")
+ to_chat(customer, examine_block(sell_info.Join("\n")))
+
+///Sets quantity of all products to initial(quanity); this proc is currently called during initialize
+/datum/component/trader/proc/restock_products()
+ products = trader_data.initial_products.Copy()
+
+///Sets quantity of all wanted_items to initial(quanity); this proc is currently called during initialize
+/datum/component/trader/proc/renew_item_demands()
+ wanted_items = trader_data.initial_wanteds.Copy()
+
+///Returns if the trader is conscious and its combat mode is disabled.
+/datum/component/trader/proc/can_trade(mob/customer)
+ var/mob/living/trader = parent
+ if(trader.combat_mode)
+ trader.balloon_alert(customer, "in combat!")
+ return FALSE
+ if(IS_DEAD_OR_INCAP(trader))
+ trader.balloon_alert(customer, "indisposed!")
+ return FALSE
+ return TRUE
+
+#undef TRADER_RADIAL_BUY
+#undef TRADER_RADIAL_SELL
+#undef TRADER_RADIAL_TALK
+#undef TRADER_RADIAL_LORE
+#undef TRADER_RADIAL_DISCUSS_BUY
+#undef TRADER_RADIAL_DISCUSS_SELL
+#undef TRADER_RADIAL_NO
+#undef TRADER_RADIAL_YES
+#undef TRADER_RADIAL_OUT_OF_STOCK
+#undef TRADER_PRODUCT_INFO_PRICE
+#undef TRADER_PRODUCT_INFO_QUANTITY
+#undef TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION
+
+#undef TRADER_OPTION_BUY
+#undef TRADER_OPTION_SELL
+#undef TRADER_OPTION_TALK
+#undef TRADER_OPTION_LORE
+#undef TRADER_OPTION_NO
+#undef TRADER_OPTION_YES
+#undef TRADER_OPTION_BUYING
+#undef TRADER_OPTION_SELLING
diff --git a/code/datums/components/vision_hurting.dm b/code/datums/components/vision_hurting.dm
new file mode 100644
index 0000000000000..acf2d186bb433
--- /dev/null
+++ b/code/datums/components/vision_hurting.dm
@@ -0,0 +1,26 @@
+/// A component that damages eyes that look at the owner
+/datum/component/vision_hurting
+ var/damage_per_second
+ var/message
+
+/datum/component/vision_hurting/Initialize(damage_per_second=1, message="Your eyes burn as you look at")
+ if(!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.damage_per_second = damage_per_second
+ src.message = message
+
+ START_PROCESSING(SSdcs, src)
+
+/datum/component/vision_hurting/process(seconds_per_tick)
+ for(var/mob/living/carbon/viewer in viewers(parent))
+ if(viewer.is_blind() || viewer.get_eye_protection() >= damage_per_second)
+ continue
+ var/obj/item/organ/internal/eyes/burning_orbs = locate() in viewer.organs
+ if(!burning_orbs)
+ continue
+ burning_orbs.apply_organ_damage(damage_per_second * seconds_per_tick)
+ if(SPT_PROB(50, seconds_per_tick))
+ to_chat(viewer, span_userdanger("[message] [parent]!"))
+ if(SPT_PROB(20, seconds_per_tick))
+ viewer.emote("scream")
diff --git a/code/datums/components/wet_floor.dm b/code/datums/components/wet_floor.dm
index 0b3b92fd2e3c9..d1f5b0fb1b86e 100644
--- a/code/datums/components/wet_floor.dm
+++ b/code/datums/components/wet_floor.dm
@@ -11,8 +11,11 @@
var/current_overlay
var/permanent = FALSE
var/last_process = 0
+ /// Should we display an overlay for this component? Useful mainly for turfs
+ /// that already look wets or just don't need the visuals for any other reason.
+ var/should_display_overlay = TRUE
-/datum/component/wet_floor/InheritComponent(datum/newcomp, orig, strength, duration_minimum, duration_add, duration_maximum, _permanent)
+/datum/component/wet_floor/InheritComponent(datum/newcomp, orig, strength, duration_minimum, duration_add, duration_maximum, _permanent, _should_display_overlay)
if(!newcomp) //We are getting passed the arguments of a would-be new component, but not a new component
add_wet(arglist(args.Copy(3)))
else //We are being passed in a full blown component
@@ -22,10 +25,11 @@
for(var/i in WF.time_left_list)
add_wet(text2num(i), WF.time_left_list[i])
-/datum/component/wet_floor/Initialize(strength, duration_minimum, duration_add, duration_maximum, _permanent = FALSE)
+/datum/component/wet_floor/Initialize(strength, duration_minimum, duration_add, duration_maximum, _permanent = FALSE, _should_display_overlay = TRUE)
if(!isopenturf(parent))
return COMPONENT_INCOMPATIBLE
- add_wet(strength, duration_minimum, duration_add, duration_maximum)
+ should_display_overlay = _should_display_overlay
+ add_wet(strength, duration_minimum, duration_add, duration_maximum, _permanent, _should_display_overlay)
permanent = _permanent
if(!permanent)
START_PROCESSING(SSwet_floors, src)
@@ -50,6 +54,15 @@
return ..()
/datum/component/wet_floor/proc/update_overlay()
+ if(!should_display_overlay)
+ if(!current_overlay)
+ return
+
+ var/turf/parent_turf = parent
+ parent_turf.cut_overlay(current_overlay)
+ current_overlay = null
+ return
+
var/intended
if(!isfloorturf(parent))
intended = generic_turf_overlay
@@ -62,9 +75,9 @@
else
intended = water_overlay
if(current_overlay != intended)
- var/turf/T = parent
- T.cut_overlay(current_overlay)
- T.add_overlay(intended)
+ var/turf/parent_turf = parent
+ parent_turf.cut_overlay(current_overlay)
+ parent_turf.add_overlay(intended)
current_overlay = intended
/datum/component/wet_floor/proc/AfterSlip(mob/living/slipped)
@@ -163,7 +176,7 @@
//NB it's possible we get deleted after this, due to inherit
-/datum/component/wet_floor/proc/add_wet(type, duration_minimum = 0, duration_add = 0, duration_maximum = MAXIMUM_WET_TIME, _permanent = FALSE)
+/datum/component/wet_floor/proc/add_wet(type, duration_minimum = 0, duration_add = 0, duration_maximum = MAXIMUM_WET_TIME, _permanent = FALSE, _should_display_overlay = TRUE)
var/static/list/allowed_types = list(TURF_WET_WATER, TURF_WET_LUBE, TURF_WET_ICE, TURF_WET_PERMAFROST, TURF_WET_SUPERLUBE)
if(duration_minimum <= 0 || !type)
return FALSE
@@ -179,6 +192,8 @@
permanent = TRUE
STOP_PROCESSING(SSwet_floors, src)
+ should_display_overlay = _should_display_overlay
+
/datum/component/wet_floor/proc/_do_add_wet(type, duration_minimum, duration_add, duration_maximum)
var/time = 0
if(LAZYACCESS(time_left_list, "[type]"))
diff --git a/code/datums/diseases/_MobProcs.dm b/code/datums/diseases/_MobProcs.dm
index e64e91c5533ba..1aa8b6654cd45 100644
--- a/code/datums/diseases/_MobProcs.dm
+++ b/code/datums/diseases/_MobProcs.dm
@@ -98,7 +98,7 @@
if(HAS_TRAIT(src, TRAIT_VIRUS_RESISTANCE) && prob(75))
return
- if(((disease.spread_flags & DISEASE_SPREAD_AIRBORNE) || force_spread) && prob((50*disease.spreading_modifier) - 1))
+ if(((disease.spread_flags & DISEASE_SPREAD_AIRBORNE) || force_spread) && prob(min((50*disease.spreading_modifier - 1), 50)))
ForceContractDisease(disease)
/mob/living/carbon/AirborneContractDisease(datum/disease/disease, force_spread)
diff --git a/code/datums/diseases/anaphylaxis.dm b/code/datums/diseases/anaphylaxis.dm
new file mode 100644
index 0000000000000..12d408ad2159a
--- /dev/null
+++ b/code/datums/diseases/anaphylaxis.dm
@@ -0,0 +1,83 @@
+/datum/disease/anaphylaxis
+ form = "Shock"
+ name = "Anaphylaxis"
+ desc = "Patient is undergoing a life-threatening allergic reaction and will die if not treated."
+ max_stages = 3
+ cure_text = "Epinephrine"
+ cures = list(/datum/reagent/medicine/epinephrine)
+ cure_chance = 20
+ agent = "Allergy"
+ viable_mobtypes = list(/mob/living/carbon/human)
+ disease_flags = CURABLE
+ severity = DISEASE_SEVERITY_DANGEROUS
+ spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS
+ spread_text = "None"
+ visibility_flags = HIDDEN_PANDEMIC
+ bypasses_immunity = TRUE
+ stage_prob = 5
+
+/datum/disease/anaphylaxis/stage_act(seconds_per_tick, times_fired)
+ . = ..()
+ if(!.)
+ return
+
+ if(HAS_TRAIT(affected_mob, TRAIT_TOXINLOVER)) // You are no fun
+ cure()
+ return
+
+ // Cool them enough to feel cold to the touch, and then some, because temperature mechanics are dumb
+ affected_mob.adjust_bodytemperature(-10 * seconds_per_tick * stage, min_temp = BODYTEMP_COLD_DAMAGE_LIMIT - 70)
+
+ switch(stage)
+ // early symptoms: mild shakes and dizziness
+ if(1)
+ if(affected_mob.num_hands >= 1 && SPT_PROB(5, seconds_per_tick))
+ to_chat(affected_mob, span_warning("You feel your hand[affected_mob.num_hands == 1 ? "":"s"] start to shake."))
+ affected_mob.adjust_jitter_up_to(4 SECONDS * seconds_per_tick, 1 MINUTES)
+ if(affected_mob.num_legs >= 1 && SPT_PROB(5, seconds_per_tick))
+ to_chat(affected_mob, span_warning("You feel your leg[affected_mob.num_hands == 1 ? "":"s"] start to shake."))
+ affected_mob.adjust_jitter_up_to(4 SECONDS * seconds_per_tick, 1 MINUTES)
+ if(SPT_PROB(2, seconds_per_tick))
+ affected_mob.adjust_dizzy_up_to(5 SECONDS * seconds_per_tick, 1 MINUTES)
+ if(SPT_PROB(1, seconds_per_tick))
+ to_chat(affected_mob, span_danger("Your throat itches."))
+
+ // warning symptoms: violent shakes, dizziness, blurred vision, difficulty breathing
+ if(2)
+ affected_mob.apply_damage(0.33 * seconds_per_tick, TOX, spread_damage = TRUE)
+
+ if(affected_mob.num_hands >= 1 && SPT_PROB(5, seconds_per_tick))
+ to_chat(affected_mob, span_warning("You feel your hand[affected_mob.num_hands == 1 ? "":"s"] shake violently."))
+ affected_mob.adjust_jitter_up_to(8 SECONDS * seconds_per_tick, 1 MINUTES)
+ if(prob(20))
+ affected_mob.drop_all_held_items()
+ if(affected_mob.num_legs >= 1 && SPT_PROB(5, seconds_per_tick))
+ to_chat(affected_mob, span_warning("You feel your leg[affected_mob.num_hands == 1 ? "":"s"] shake violently."))
+ affected_mob.adjust_jitter_up_to(8 SECONDS * seconds_per_tick, 1 MINUTES)
+ if(prob(40) && affected_mob.getStaminaLoss() < 75)
+ affected_mob.adjustStaminaLoss(15)
+ if(affected_mob.get_organ_slot(ORGAN_SLOT_EYES) && SPT_PROB(4, seconds_per_tick))
+ affected_mob.adjust_eye_blur(4 SECONDS * seconds_per_tick)
+ to_chat(affected_mob, span_warning("It's getting harder to see clearly."))
+ if(!HAS_TRAIT(affected_mob, TRAIT_NOBREATH) && SPT_PROB(4, seconds_per_tick))
+ affected_mob.apply_damage(2 * seconds_per_tick, OXY)
+ affected_mob.losebreath += (2 * seconds_per_tick)
+ to_chat(affected_mob, span_warning("It's getting harder to breathe."))
+ if(SPT_PROB(2, seconds_per_tick))
+ affected_mob.adjust_drowsiness_up_to(3 SECONDS * seconds_per_tick, 30 SECONDS)
+ if(SPT_PROB(2, seconds_per_tick))
+ affected_mob.adjust_dizzy_up_to(5 SECONDS * seconds_per_tick, 1 MINUTES)
+ affected_mob.adjust_confusion_up_to(1 SECONDS * seconds_per_tick, 10 SECONDS)
+ if(SPT_PROB(2, seconds_per_tick))
+ affected_mob.vomit(MOB_VOMIT_MESSAGE|MOB_VOMIT_HARM)
+ affected_mob.Stun(2 SECONDS) // The full 20 second vomit stun would be lethal
+ if(SPT_PROB(1, seconds_per_tick))
+ affected_mob.emote("cough")
+ if(SPT_PROB(1, seconds_per_tick))
+ to_chat(affected_mob, span_danger("Your throat feels sore."))
+
+ // "you are too late" symptoms: death.
+ if(3)
+ affected_mob.apply_damage(3 * seconds_per_tick, TOX, spread_damage = TRUE)
+ affected_mob.apply_damage(1 * seconds_per_tick, OXY)
+ affected_mob.Unconscious(3 SECONDS * seconds_per_tick)
diff --git a/code/datums/dna.dm b/code/datums/dna.dm
index 599bc9ce1b2ef..756c0e715aeb8 100644
--- a/code/datums/dna.dm
+++ b/code/datums/dna.dm
@@ -502,6 +502,8 @@ GLOBAL_LIST_INIT(total_uf_len_by_block, populate_total_uf_len_by_block())
if(ispath(mrace))
new_race = new mrace
else if(istype(mrace))
+ if(QDELING(mrace))
+ CRASH("someone is calling set_species() and is passing it a qdeling species datum, this is VERY bad, stop it")
new_race = mrace
else
CRASH("set_species called with an invalid mrace [mrace]")
@@ -802,6 +804,8 @@ GLOBAL_LIST_INIT(total_uf_len_by_block, populate_total_uf_len_by_block())
/proc/scramble_dna(mob/living/carbon/M, ui=FALSE, se=FALSE, uf=FALSE, probability)
if(!M.has_dna())
CRASH("[M] does not have DNA")
+ if(HAS_TRAIT(M, TRAIT_NO_DNA_SCRAMBLE))
+ return
if(se)
for(var/i=1, i <= DNA_MUTATION_BLOCKS, i++)
if(prob(probability))
diff --git a/code/datums/elements/ai_swap_combat_mode.dm b/code/datums/elements/ai_swap_combat_mode.dm
new file mode 100644
index 0000000000000..2b7167000fcfa
--- /dev/null
+++ b/code/datums/elements/ai_swap_combat_mode.dm
@@ -0,0 +1,66 @@
+/**
+ * Attached to a mob with an AI controller, updates combat mode when the affected mob acquires or loses targets
+ */
+/datum/element/ai_swap_combat_mode
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// The message we yell when we enter combat mode
+ var/list/battle_start_barks
+ /// A one liner said when we exit combat mode
+ var/list/battle_end_barks
+ /// The chance to yell the above lines
+ var/speech_chance
+ /// Target key
+ var/target_key
+
+/datum/element/ai_swap_combat_mode/Attach(datum/target, target_key, list/battle_start_barks = null, list/battle_end_barks = null)
+ . = ..()
+ if(!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+ var/mob/living/living_target = target
+ if(!living_target.ai_controller)
+ return ELEMENT_INCOMPATIBLE
+
+ if(isnull(battle_start_barks))
+ battle_start_barks = list("En Garde!",)
+
+ if(isnull(battle_end_barks))
+ battle_end_barks = list("Never should have come here",)
+
+ src.battle_start_barks = battle_start_barks
+ src.battle_end_barks = battle_end_barks
+ src.target_key = target_key
+ RegisterSignal(target, COMSIG_AI_BLACKBOARD_KEY_SET(target_key), PROC_REF(on_target_gained))
+ RegisterSignal(target, COMSIG_AI_BLACKBOARD_KEY_CLEARED(target_key), PROC_REF(on_target_cleared))
+
+/datum/element/ai_swap_combat_mode/Detach(datum/source)
+ . = ..()
+ UnregisterSignal(source, list(
+ COMSIG_AI_BLACKBOARD_KEY_SET(target_key),
+ COMSIG_AI_BLACKBOARD_KEY_CLEARED(target_key),
+ ))
+
+/// When the mob gains a target, and it was not already in combat mode, enter it
+/datum/element/ai_swap_combat_mode/proc/on_target_gained(mob/living/source)
+ SIGNAL_HANDLER
+
+ if(swap_mode(source, TRUE))
+ INVOKE_ASYNC(src, PROC_REF(speak_bark), source, battle_start_barks)
+
+/// When the mob loses its target, and it was not already out of combat mode, exit it
+/datum/element/ai_swap_combat_mode/proc/on_target_cleared(mob/living/source)
+ SIGNAL_HANDLER
+
+ if(swap_mode(source, FALSE))
+ INVOKE_ASYNC(src, PROC_REF(speak_bark), source, battle_end_barks)
+
+///Says a quip, if the RNG allows it
+/datum/element/ai_swap_combat_mode/proc/speak_bark(mob/living/source, line)
+ source.say(pick(line))
+
+///If the combat mode would be changed into a different state, updates it and returns TRUE, otherwise returns FALSE
+/datum/element/ai_swap_combat_mode/proc/swap_mode(mob/living/source, new_mode)
+ if(source.combat_mode == new_mode)
+ return FALSE
+ source.set_combat_mode(new_mode)
+ return TRUE
diff --git a/code/datums/elements/amputating_limbs.dm b/code/datums/elements/amputating_limbs.dm
index 16a99e96f6c19..8684a76f47fc3 100644
--- a/code/datums/elements/amputating_limbs.dm
+++ b/code/datums/elements/amputating_limbs.dm
@@ -32,16 +32,16 @@
src.minimum_stat = minimum_stat
src.snip_chance = snip_chance
src.target_zones = target_zones
- RegisterSignals(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET), PROC_REF(try_amputate))
+ RegisterSignals(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET), PROC_REF(try_amputate))
/datum/element/amputating_limbs/Detach(datum/source)
- UnregisterSignal(source, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
+ UnregisterSignal(source, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
return ..()
/// Called when you click on literally anything with your hands, see if it is an injured carbon and then try to cut it up
-/datum/element/amputating_limbs/proc/try_amputate(mob/living/surgeon, atom/victim)
+/datum/element/amputating_limbs/proc/try_amputate(mob/living/surgeon, atom/victim, proximity, modifiers)
SIGNAL_HANDLER
- if (!iscarbon(victim) || HAS_TRAIT(victim, TRAIT_NODISMEMBER) || !prob(snip_chance))
+ if (!proximity || !iscarbon(victim) || HAS_TRAIT(victim, TRAIT_NODISMEMBER) || !prob(snip_chance))
return
var/mob/living/carbon/limbed_victim = victim
diff --git a/code/datums/elements/basic_eating.dm b/code/datums/elements/basic_eating.dm
index 86f4be63cac3c..297e77fa060ea 100644
--- a/code/datums/elements/basic_eating.dm
+++ b/code/datums/elements/basic_eating.dm
@@ -12,10 +12,12 @@
var/damage_amount
/// Type of hurt to apply
var/damage_type
+ /// Whether to flavor it as drinking rather than eating.
+ var/drinking
/// Types the animal can eat.
var/list/food_types
-/datum/element/basic_eating/Attach(datum/target, heal_amt = 0, damage_amount = 0, damage_type = null, food_types = list())
+/datum/element/basic_eating/Attach(datum/target, heal_amt = 0, damage_amount = 0, damage_type = null, drinking = FALSE, food_types = list())
. = ..()
if(!isliving(target))
@@ -24,6 +26,7 @@
src.heal_amt = heal_amt
src.damage_amount = damage_amount
src.damage_type = damage_type
+ src.drinking = drinking
src.food_types = food_types
//this lets players eat
@@ -37,7 +40,12 @@
/datum/element/basic_eating/proc/on_unarm_attack(mob/living/eater, atom/target, proximity, modifiers)
SIGNAL_HANDLER
- try_eating(eater, target)
+ if(!proximity)
+ return NONE
+
+ if(try_eating(eater, target))
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+ return NONE
/datum/element/basic_eating/proc/on_pre_attackingtarget(mob/living/eater, atom/target)
SIGNAL_HANDLER
@@ -45,8 +53,12 @@
/datum/element/basic_eating/proc/try_eating(mob/living/eater, atom/target)
if(!is_type_in_list(target, food_types))
- return
- var/eat_verb = pick("bite","chew","nibble","gnaw","gobble","chomp")
+ return FALSE
+ var/eat_verb
+ if(drinking)
+ eat_verb = pick("slurp","sip","guzzle","drink","quaff","suck")
+ else
+ eat_verb = pick("bite","chew","nibble","gnaw","gobble","chomp")
if (heal_amt > 0)
var/healed = heal_amt && eater.health < eater.maxHealth
@@ -54,17 +66,21 @@
eater.heal_overall_damage(heal_amt)
eater.visible_message(span_notice("[eater] [eat_verb]s [target]."), span_notice("You [eat_verb] [target][healed ? ", restoring some health" : ""]."))
finish_eating(eater, target)
- return
+ return TRUE
if (damage_amount > 0 && damage_type)
eater.apply_damage(damage_amount, damage_type)
eater.visible_message(span_notice("[eater] [eat_verb]s [target], and seems to hurt itself."), span_notice("You [eat_verb] [target], hurting yourself in the process."))
finish_eating(eater, target)
- return
+ return TRUE
eater.visible_message(span_notice("[eater] [eat_verb]s [target]."), span_notice("You [eat_verb] [target]."))
finish_eating(eater, target)
+ return TRUE
/datum/element/basic_eating/proc/finish_eating(mob/living/eater, atom/target)
- playsound(eater.loc,'sound/items/eatfood.ogg', rand(10,50), TRUE)
+ if(drinking)
+ playsound(eater.loc,'sound/items/drink.ogg', rand(10,50), TRUE)
+ else
+ playsound(eater.loc,'sound/items/eatfood.ogg', rand(10,50), TRUE)
qdel(target)
diff --git a/code/datums/elements/dextrous.dm b/code/datums/elements/dextrous.dm
index 335c7c196d1ce..47b6089f19784 100644
--- a/code/datums/elements/dextrous.dm
+++ b/code/datums/elements/dextrous.dm
@@ -50,8 +50,13 @@
/// Try picking up items
/datum/element/dextrous/proc/on_hand_clicked(mob/living/hand_haver, atom/target, proximity, modifiers)
SIGNAL_HANDLER
+ if(!proximity)
+ if(isitem(target))
+ var/obj/item/obj_item = target
+ if(!obj_item.atom_storage && !(obj_item.item_flags & IN_STORAGE))
+ return NONE
if (!isitem(target) && hand_haver.combat_mode)
- return
+ return NONE
if (LAZYACCESS(modifiers, RIGHT_CLICK))
INVOKE_ASYNC(target, TYPE_PROC_REF(/atom, attack_hand_secondary), hand_haver, modifiers)
else
diff --git a/code/datums/elements/door_pryer.dm b/code/datums/elements/door_pryer.dm
new file mode 100644
index 0000000000000..a17687407e39c
--- /dev/null
+++ b/code/datums/elements/door_pryer.dm
@@ -0,0 +1,70 @@
+/**
+ * Attached to a basic mob.
+ * Causes attacks on doors to attempt to open them.
+ */
+/datum/element/door_pryer
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// Time it takes to open a door with force
+ var/pry_time
+ /// Interaction key for if we force a door open
+ var/interaction_key
+
+/datum/element/door_pryer/Attach(datum/target, pry_time = 10 SECONDS, interaction_key = null)
+ . = ..()
+ if (!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+ src.pry_time = pry_time
+ src.interaction_key = interaction_key
+ RegisterSignal(target, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_attack))
+
+/datum/element/door_pryer/Detach(datum/source)
+ . = ..()
+ UnregisterSignal(source, COMSIG_LIVING_UNARMED_ATTACK)
+
+/// If we're targetting an airlock, open it
+/datum/element/door_pryer/proc/on_attack(mob/living/basic/attacker, atom/target, proximity_flag)
+ SIGNAL_HANDLER
+ if(!proximity_flag || !istype(target, /obj/machinery/door/airlock))
+ return NONE
+ var/obj/machinery/door/airlock/airlock_target = target
+ if (!airlock_target.density)
+ return NONE // It's already open numbnuts
+
+ if(DOING_INTERACTION_WITH_TARGET(attacker, target) || (!isnull(interaction_key) && DOING_INTERACTION(attacker, interaction_key)))
+ attacker.balloon_alert(attacker, "busy!")
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+ if (airlock_target.locked || airlock_target.welded || airlock_target.seal)
+ if (!attacker.combat_mode)
+ airlock_target.balloon_alert(attacker, "it's sealed!")
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+ return // Attack the door
+
+ INVOKE_ASYNC(src, PROC_REF(open_door), attacker, airlock_target)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/// Try opening the door, and if we can't then try forcing it
+/datum/element/door_pryer/proc/open_door(mob/living/basic/attacker, obj/machinery/door/airlock/airlock_target)
+ if (!airlock_target.hasPower())
+ attacker.visible_message(span_warning("[attacker] forces the [airlock_target] to open."))
+ airlock_target.open(FORCING_DOOR_CHECKS)
+ return
+
+ if (airlock_target.allowed(attacker))
+ airlock_target.open(DEFAULT_DOOR_CHECKS)
+ return
+
+ attacker.visible_message(\
+ message = span_warning("[attacker] starts forcing the [airlock_target] open!"),
+ blind_message = span_hear("You hear a metal screeching sound."),
+ )
+ playsound(airlock_target, 'sound/machines/airlock_alien_prying.ogg', 100, TRUE)
+ airlock_target.balloon_alert(attacker, "prying...")
+ if(!do_after(attacker, pry_time, airlock_target))
+ airlock_target.balloon_alert(attacker, "interrupted!")
+ return
+ if(airlock_target.locked)
+ return
+ attacker.visible_message(span_warning("[attacker] forces the [airlock_target] to open."))
+ airlock_target.open(BYPASS_DOOR_CHECKS)
diff --git a/code/datums/elements/food/food_trash.dm b/code/datums/elements/food/food_trash.dm
index 0a9d55406093d..6df36c82c4c41 100644
--- a/code/datums/elements/food/food_trash.dm
+++ b/code/datums/elements/food/food_trash.dm
@@ -52,8 +52,8 @@
if(istype(source, /obj/item/food/grown) && ispath(trash, /obj/item/food))
var/obj/item/food/grown/plant = source
- var/reagent_purity = plant.seed.get_reagent_purity()
- trash_item = new trash(edible_object.drop_location(), reagent_purity)
+ trash_item = new trash(edible_object.drop_location())
+ trash_item.reagents?.set_all_reagents_purity(plant.seed.get_reagent_purity())
else
trash_item = generate_trash_procpath ? call(source, generate_trash_procpath)() : new trash(edible_object.drop_location())
diff --git a/code/datums/elements/human_biter.dm b/code/datums/elements/human_biter.dm
new file mode 100644
index 0000000000000..852dea12320a8
--- /dev/null
+++ b/code/datums/elements/human_biter.dm
@@ -0,0 +1,28 @@
+/// Allows carbons with heads to attempt to bite mobs if attacking with cuffed hands / missing arms
+/datum/element/human_biter
+
+/datum/element/human_biter/Attach(datum/target)
+ . = ..()
+ if(!iscarbon(target))
+ return ELEMENT_INCOMPATIBLE
+
+ RegisterSignal(target, COMSIG_LIVING_EARLY_UNARMED_ATTACK, PROC_REF(try_bite))
+
+/datum/element/human_biter/Detach(datum/source, ...)
+ . = ..()
+ UnregisterSignal(source, COMSIG_LIVING_EARLY_UNARMED_ATTACK)
+
+/datum/element/human_biter/proc/try_bite(mob/living/carbon/human/source, atom/target, proximity_flag, modifiers)
+ SIGNAL_HANDLER
+
+ if(!proximity_flag || !source.combat_mode || LAZYACCESS(modifiers, RIGHT_CLICK) || !isliving(target))
+ return NONE
+
+ // If we can attack like normal, just go ahead and do that
+ if(source.can_unarmed_attack())
+ return NONE
+
+ if(target.attack_paw(source, modifiers))
+ return COMPONENT_CANCEL_ATTACK_CHAIN // bite successful!
+
+ return COMPONENT_SKIP_ATTACK // we will fail anyways if we try to attack normally, so skip the rest
diff --git a/code/datums/elements/light_eaten.dm b/code/datums/elements/light_eaten.dm
index 88aad2c555c0e..39550a9912463 100644
--- a/code/datums/elements/light_eaten.dm
+++ b/code/datums/elements/light_eaten.dm
@@ -23,6 +23,7 @@
target.set_light_power(0)
target.set_light_range(0)
target.set_light_on(FALSE)
+ target.update_icon()
/datum/element/light_eaten/Detach(datum/source)
UnregisterSignal(source, list(
@@ -54,11 +55,9 @@
/// Prevents the light from turning on while the light power is greater than 0.
/datum/element/light_eaten/proc/block_light_on(atom/eaten_light, new_on)
SIGNAL_HANDLER
- if(!new_on)
- return NONE
- if(eaten_light.light_power <= 0)
- return NONE
- return COMPONENT_BLOCK_LIGHT_UPDATE
+ if(new_on)
+ return COMPONENT_BLOCK_LIGHT_UPDATE
+ return NONE
/// Signal handler for light eater flavortext
/datum/element/light_eaten/proc/on_examine(atom/eaten_light, mob/examiner, list/examine_text)
diff --git a/code/datums/elements/light_eater.dm b/code/datums/elements/light_eater.dm
index b4f7cf1042f3d..50f88cb9e9b23 100644
--- a/code/datums/elements/light_eater.dm
+++ b/code/datums/elements/light_eater.dm
@@ -4,6 +4,10 @@
* The temporary equivalent is [/datum/component/light_eater]
*/
/datum/element/light_eater
+ var/static/list/blacklisted_areas = typecacheof(list(
+ /turf/open/space,
+ /turf/open/lava,
+ ))
/datum/element/light_eater/Attach(datum/target)
if(isatom(target))
@@ -83,8 +87,9 @@
* - [eater][/datum]: The light eater eating the morsel. This is the datum that the element is attached to that started this chain.
*/
/datum/element/light_eater/proc/devour(atom/morsel, datum/eater)
- var/static/list/undevourable = typecacheof(list(/turf/open/space))
- if(is_type_in_typecache(morsel, undevourable))
+ if(is_type_in_typecache(morsel, blacklisted_areas))
+ return FALSE
+ if(istransparentturf(morsel))
return FALSE
if(morsel.light_power <= 0 || morsel.light_range <= 0 || !morsel.light_on)
return FALSE
diff --git a/code/datums/elements/mob_grabber.dm b/code/datums/elements/mob_grabber.dm
index a85c5dc48b25a..cc766f24887b9 100644
--- a/code/datums/elements/mob_grabber.dm
+++ b/code/datums/elements/mob_grabber.dm
@@ -13,18 +13,19 @@
return ELEMENT_INCOMPATIBLE
src.minimum_stat = minimum_stat
src.steal_from_others = steal_from_others
- RegisterSignals(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET), PROC_REF(grab_mob))
+ RegisterSignals(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET), PROC_REF(grab_mob))
/datum/element/mob_grabber/Detach(datum/source)
- UnregisterSignal(source, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
+ UnregisterSignal(source, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
. = ..()
/// Try and grab something we attacked
-/datum/element/mob_grabber/proc/grab_mob(mob/living/source, mob/living/target)
+/datum/element/mob_grabber/proc/grab_mob(mob/living/source, mob/living/target, proximity, modifiers)
SIGNAL_HANDLER
- if (!isliving(target) || !source.Adjacent(target) || target.stat < minimum_stat)
- return
+ if (!isliving(target) || !proximity || target.stat < minimum_stat)
+ return NONE
var/atom/currently_pulled = target.pulledby
if (!isnull(currently_pulled) && (!steal_from_others || currently_pulled == source))
- return
+ return NONE
INVOKE_ASYNC(target, TYPE_PROC_REF(/mob/living, grabbedby), source)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
diff --git a/code/datums/elements/move_force_on_death.dm b/code/datums/elements/move_force_on_death.dm
new file mode 100644
index 0000000000000..af2560d000c3f
--- /dev/null
+++ b/code/datums/elements/move_force_on_death.dm
@@ -0,0 +1,49 @@
+/**
+ * Element to change a mob's move forces on death and reset them on living
+ */
+/datum/element/change_force_on_death
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ ///Our move force
+ var/move_force
+ /// our resist move force
+ var/move_resist
+ /// how much we resist pulling
+ var/pull_force
+
+/datum/element/change_force_on_death/Attach(datum/target, move_force, move_resist, pull_force)
+ . = ..()
+
+ if(!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+
+ RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_death))
+ RegisterSignal(target, COMSIG_LIVING_REVIVE, PROC_REF(on_revive))
+
+ if(!isnull(move_force))
+ src.move_force = move_force
+ if(!isnull(move_resist))
+ src.move_resist = move_resist
+ if(!isnull(pull_force))
+ src.pull_force = pull_force
+
+/datum/element/change_force_on_death/Detach(datum/target)
+ . = ..()
+ UnregisterSignal(target, list(COMSIG_LIVING_DEATH, COMSIG_LIVING_REVIVE))
+
+/datum/element/change_force_on_death/proc/on_death(mob/living/source)
+ SIGNAL_HANDLER
+
+ if(!isnull(move_force))
+ source.move_force = move_force
+ if(!isnull(move_resist))
+ source.move_resist = move_resist
+ if(!isnull(pull_force))
+ source.pull_force = pull_force
+
+/datum/element/change_force_on_death/proc/on_revive(mob/living/source)
+ SIGNAL_HANDLER
+
+ source.move_force = initial(source.move_force)
+ source.move_resist = initial(source.move_resist)
+ source.pull_force = initial(source.pull_force)
diff --git a/code/datums/elements/structure_repair.dm b/code/datums/elements/structure_repair.dm
index d3b26eed815be..1f57a3d1730f9 100644
--- a/code/datums/elements/structure_repair.dm
+++ b/code/datums/elements/structure_repair.dm
@@ -25,11 +25,11 @@
return ..()
/// If the target is of a valid type, interrupt the attack chain to repair it instead
-/datum/element/structure_repair/proc/try_repair(mob/living/fixer, atom/target)
+/datum/element/structure_repair/proc/try_repair(mob/living/fixer, atom/target, proximity)
SIGNAL_HANDLER
- if (!is_type_in_typecache(target, structure_types_typecache))
- return
+ if (!proximity || !is_type_in_typecache(target, structure_types_typecache))
+ return NONE
if (target.get_integrity() >= target.max_integrity)
target.balloon_alert(fixer, "not damaged!")
diff --git a/code/datums/elements/tear_wall.dm b/code/datums/elements/tear_wall.dm
deleted file mode 100644
index 0d24bbda28985..0000000000000
--- a/code/datums/elements/tear_wall.dm
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Attached to a basic mob that will then be able to tear down a wall after some time.
- */
-/datum/element/tear_wall
- element_flags = ELEMENT_BESPOKE
- argument_hash_start_idx = 3
- /// The rate at which we can break regular walls
- var/regular_tear_time
- /// The rate at which we can break reinforced walls
- var/reinforced_tear_time
-
-/datum/element/tear_wall/Attach(datum/target, regular_tear_time = 2 SECONDS, reinforced_tear_time = 4 SECONDS)
- . = ..()
- if(!isbasicmob(target))
- return ELEMENT_INCOMPATIBLE
-
- src.regular_tear_time = regular_tear_time
- src.reinforced_tear_time = reinforced_tear_time
- RegisterSignal(target, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(attack_wall))
-
-/datum/element/bonus_damage/Detach(datum/source)
- UnregisterSignal(source, COMSIG_HOSTILE_POST_ATTACKINGTARGET)
- return ..()
-
-/// Checks if we are attacking a wall
-/datum/element/tear_wall/proc/attack_wall(mob/living/basic/attacker, atom/target, success)
- SIGNAL_HANDLER
-
- if(!iswallturf(target))
- return
- var/turf/closed/wall/thewall = target
- var/prying_time = regular_tear_time
- if(istype(thewall, /turf/closed/wall/r_wall))
- prying_time = reinforced_tear_time
- INVOKE_ASYNC(src, PROC_REF(async_attack_wall), attacker, thewall, prying_time)
-
-/// Performs taking down the wall
-/datum/element/tear_wall/proc/async_attack_wall(mob/living/basic/attacker, turf/closed/wall/thewall, prying_time)
- if(DOING_INTERACTION_WITH_TARGET(attacker, thewall))
- attacker.balloon_alert(attacker, "busy!")
- return
- to_chat(attacker, span_warning("You begin tearing through the wall..."))
- playsound(attacker, 'sound/machines/airlock_alien_prying.ogg', 100, TRUE)
- if(do_after(attacker, prying_time, target = thewall))
- if(isopenturf(thewall))
- return
- thewall.dismantle_wall(1)
- playsound(attacker, 'sound/effects/meteorimpact.ogg', 100, TRUE)
diff --git a/code/datums/elements/undertile.dm b/code/datums/elements/undertile.dm
index e1fdef4d413d1..ecc621a57e42f 100644
--- a/code/datums/elements/undertile.dm
+++ b/code/datums/elements/undertile.dm
@@ -31,12 +31,14 @@
src.use_alpha = use_alpha
src.use_anchor = use_anchor
-
///called when a tile has been covered or uncovered
/datum/element/undertile/proc/hide(atom/movable/source, underfloor_accessibility)
SIGNAL_HANDLER
- source.invisibility = underfloor_accessibility < UNDERFLOOR_VISIBLE ? invisibility_level : 0
+ if(underfloor_accessibility < UNDERFLOOR_VISIBLE)
+ source.SetInvisibility(invisibility_level, id=type)
+ else
+ source.RemoveInvisibility(type)
var/turf/T = get_turf(source)
@@ -73,11 +75,10 @@
if(use_anchor)
source.set_anchored(FALSE)
-
/datum/element/undertile/Detach(atom/movable/source, visibility_trait, invisibility_level = INVISIBILITY_MAXIMUM)
. = ..()
hide(source, UNDERFLOOR_INTERACTABLE)
-
+ source.RemoveInvisibility(type)
#undef ALPHA_UNDERTILE
diff --git a/code/datums/elements/wall_smasher.dm b/code/datums/elements/wall_smasher.dm
index 3c6c1d92da73c..ba8a689253ce3 100644
--- a/code/datums/elements/wall_smasher.dm
+++ b/code/datums/elements/wall_smasher.dm
@@ -17,7 +17,7 @@
return ELEMENT_INCOMPATIBLE
src.strength_flag = strength_flag
- RegisterSignals(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_EARLY_UNARMED_ATTACK), PROC_REF(on_unarm_attack)) // Players
+ RegisterSignal(target, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_unarm_attack)) // Players
RegisterSignal(target, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(on_pre_attackingtarget)) // AI
if (isanimal_or_basicmob(target))
@@ -25,7 +25,7 @@
animal_target.environment_smash = strength_flag
/datum/element/wall_smasher/Detach(datum/target)
- UnregisterSignal(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
+ UnregisterSignal(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
if (isanimal_or_basicmob(target))
var/mob/living/simple_animal/animal_target = target
animal_target.environment_smash = initial(animal_target.environment_smash)
@@ -34,19 +34,19 @@
/datum/element/wall_smasher/proc/on_unarm_attack(mob/living/puncher, atom/target, proximity, modifiers)
SIGNAL_HANDLER
- try_smashing(puncher, target)
+ return try_smashing(puncher, target)
/datum/element/wall_smasher/proc/on_pre_attackingtarget(mob/living/puncher, atom/target)
SIGNAL_HANDLER
- try_smashing(puncher, target)
+ return try_smashing(puncher, target)
/datum/element/wall_smasher/proc/try_smashing(mob/living/puncher, atom/target)
if (!isturf(target))
- return
+ return NONE
if (isfloorturf(target))
- return
+ return NONE
if (isindestructiblewall(target))
- return
+ return NONE
puncher.changeNext_move(CLICK_CD_MELEE)
puncher.do_attack_animation(target)
diff --git a/code/datums/elements/wall_tearer.dm b/code/datums/elements/wall_tearer.dm
new file mode 100644
index 0000000000000..75e892dc4cb85
--- /dev/null
+++ b/code/datums/elements/wall_tearer.dm
@@ -0,0 +1,84 @@
+/// Returned if we can rip up this target
+#define WALL_TEAR_ALLOWED TRUE
+/// Returned if we can't rip up this target
+#define WALL_TEAR_INVALID FALSE
+/// Returned if we can't rip up the target but still don't want to attack it
+#define WALL_TEAR_FAIL_CANCEL_CHAIN -1
+
+/**
+ * Allows attached mobs to destroy walls over time, a little less unreasonable than the instant wall deletion of wall_smasher
+ */
+/datum/element/wall_tearer
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// Whether we can break reinforced walls
+ var/allow_reinforced
+ /// How long it takes for us to destroy a wall completely (its a 3 step process so this will be divided by three)
+ var/tear_time
+ /// How much longer it takes to break reinforced walls
+ var/reinforced_multiplier
+ /// What interaction key do we use for our interaction
+ var/do_after_key
+
+/datum/element/wall_tearer/Attach(datum/target, allow_reinforced = TRUE, tear_time = 2 SECONDS, reinforced_multiplier = 2, do_after_key = null)
+ . = ..()
+ if (!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+ src.allow_reinforced = allow_reinforced
+ src.tear_time = tear_time
+ src.reinforced_multiplier = reinforced_multiplier
+ src.do_after_key = do_after_key
+ RegisterSignal(target, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_attacked_wall))
+
+/datum/element/wall_tearer/Detach(datum/source)
+ . = ..()
+ UnregisterSignal(source, COMSIG_LIVING_UNARMED_ATTACK)
+
+/// Try to tear up a wall
+/datum/element/wall_tearer/proc/on_attacked_wall(mob/living/tearer, atom/target, proximity_flag)
+ SIGNAL_HANDLER
+ if (!proximity_flag)
+ return NONE
+ if (DOING_INTERACTION_WITH_TARGET(tearer, target) || (!isnull(do_after_key) && DOING_INTERACTION(tearer, do_after_key)))
+ tearer.balloon_alert(tearer, "busy!")
+ return COMPONENT_HOSTILE_NO_ATTACK
+ var/is_valid = validate_target(target, tearer)
+ if (is_valid != WALL_TEAR_ALLOWED)
+ return is_valid == WALL_TEAR_FAIL_CANCEL_CHAIN ? COMPONENT_HOSTILE_NO_ATTACK : NONE
+ INVOKE_ASYNC(src, PROC_REF(rip_and_tear), tearer, target)
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+/datum/element/wall_tearer/proc/rip_and_tear(mob/living/tearer, atom/target)
+ // We need to do this three times to actually destroy it
+ var/rip_time = (istype(target, /turf/closed/wall/r_wall) ? tear_time * reinforced_multiplier : tear_time) / 3
+ if (rip_time > 0)
+ tearer.visible_message(span_warning("[tearer] begins tearing through [target]!"))
+ playsound(tearer, 'sound/machines/airlock_alien_prying.ogg', vol = 100, vary = TRUE)
+ target.balloon_alert(tearer, "tearing...")
+ if (!do_after(tearer, delay = rip_time, target = target, interaction_key = do_after_key))
+ tearer.balloon_alert(tearer, "interrupted!")
+ return
+ // Might have been replaced, removed, or reinforced during our do_after
+ var/is_valid = validate_target(target, tearer)
+ if (is_valid != WALL_TEAR_ALLOWED)
+ return
+ tearer.do_attack_animation(target)
+ target.AddComponent(/datum/component/torn_wall)
+ is_valid = validate_target(target, tearer) // And now we might have just destroyed it
+ if (is_valid == WALL_TEAR_ALLOWED)
+ tearer.UnarmedAttack(target, proximity_flag = TRUE)
+
+/// Check if the target atom is a wall we can actually rip up
+/datum/element/wall_tearer/proc/validate_target(atom/target, mob/living/tearer)
+ if (!isclosedturf(target) || isindestructiblewall(target))
+ return WALL_TEAR_INVALID
+
+ var/reinforced = istype(target, /turf/closed/wall/r_wall)
+ if (!allow_reinforced && reinforced)
+ target.balloon_alert(tearer, "it's too strong!")
+ return WALL_TEAR_FAIL_CANCEL_CHAIN
+ return WALL_TEAR_ALLOWED
+
+#undef WALL_TEAR_ALLOWED
+#undef WALL_TEAR_INVALID
+#undef WALL_TEAR_FAIL_CANCEL_CHAIN
diff --git a/code/datums/elements/weather_listener.dm b/code/datums/elements/weather_listener.dm
index 61778e2a1865c..0028f57ff3d05 100644
--- a/code/datums/elements/weather_listener.dm
+++ b/code/datums/elements/weather_listener.dm
@@ -35,7 +35,7 @@
/datum/element/weather_listener/proc/handle_z_level_change(datum/source, turf/old_loc, turf/new_loc)
SIGNAL_HANDLER
var/list/fitting_z_levels = SSmapping.levels_by_trait(weather_trait)
- if(!(new_loc.z in fitting_z_levels))
+ if(!(new_loc?.z in fitting_z_levels))
return
var/datum/component/our_comp = source.AddComponent(\
/datum/component/area_sound_manager, \
diff --git a/code/datums/emotes.dm b/code/datums/emotes.dm
index 8cc784d81f0c4..bdc644ea75179 100644
--- a/code/datums/emotes.dm
+++ b/code/datums/emotes.dm
@@ -101,7 +101,7 @@
var/dchatmsg = "[user] [msg]"
var/tmp_sound = get_sound(user)
- if(tmp_sound && should_play_sound(user, intentional) && !TIMER_COOLDOWN_CHECK(user, type))
+ if(tmp_sound && should_play_sound(user, intentional) && TIMER_COOLDOWN_FINISHED(user, type))
TIMER_COOLDOWN_START(user, type, audio_cooldown)
playsound(user, tmp_sound, 50, vary)
diff --git a/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm b/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm
index 84e79a088b94c..0a7f3d6a58b92 100644
--- a/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm
+++ b/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm
@@ -354,6 +354,18 @@
icon_file = 'icons/mob/clothing/under/shorts_pants_shirts.dmi'
json_config = 'code/datums/greyscale/json_configs/buttondown_skirt_worn.json'
+//
+// LABCOATS
+//
+
+/datum/greyscale_config/labcoat
+ name = "Labcoat"
+ icon_file = 'icons/obj/clothing/suits/labcoat.dmi'
+ json_config = 'code/datums/greyscale/json_configs/labcoat.json'
+
+/datum/greyscale_config/labcoat/worn
+ name = "Labcoat (Worn)"
+ icon_file = 'icons/mob/clothing/suits/labcoat.dmi'
//
// SUITS
diff --git a/code/datums/greyscale/json_configs/labcoat.json b/code/datums/greyscale/json_configs/labcoat.json
new file mode 100644
index 0000000000000..f95b86893b0e8
--- /dev/null
+++ b/code/datums/greyscale/json_configs/labcoat.json
@@ -0,0 +1,54 @@
+{
+ "labcoat_job": [
+ {
+ "type": "icon_state",
+ "icon_state": "labcoat_job",
+ "blend_mode": "overlay",
+ "color_ids": [ 1 ]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "sash",
+ "blend_mode": "overlay",
+ "color_ids": [ 2 ]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "shoulder",
+ "blend_mode": "overlay",
+ "color_ids": [ 3 ]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "back",
+ "blend_mode": "overlay",
+ "color_ids": [ 4 ]
+ }
+ ],
+ "labcoat_job_t": [
+ {
+ "type": "icon_state",
+ "icon_state": "labcoat_job_t",
+ "blend_mode": "overlay",
+ "color_ids": [ 1 ]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "sash_t",
+ "blend_mode": "overlay",
+ "color_ids": [ 2 ]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "shoulder",
+ "blend_mode": "overlay",
+ "color_ids": [ 3 ]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "back_t",
+ "blend_mode": "overlay",
+ "color_ids": [ 4 ]
+ }
+ ]
+}
diff --git a/code/datums/looping_sounds/item_sounds.dm b/code/datums/looping_sounds/item_sounds.dm
index d38dd6737a302..f8ed5d89b20c9 100644
--- a/code/datums/looping_sounds/item_sounds.dm
+++ b/code/datums/looping_sounds/item_sounds.dm
@@ -40,3 +40,7 @@
end_volume = 35
volume = 40
ignore_walls = FALSE
+
+/datum/looping_sound/beesmoke
+ mid_sounds = list('sound/weapons/beesmoke.ogg' = 1)
+ volume = 5
diff --git a/code/datums/mapgen/_MapGenerator.dm b/code/datums/mapgen/_MapGenerator.dm
deleted file mode 100644
index cecc0c65d1ba4..0000000000000
--- a/code/datums/mapgen/_MapGenerator.dm
+++ /dev/null
@@ -1,10 +0,0 @@
-///This type is responsible for any map generation behavior that is done in areas, override this to allow for area-specific map generation. This generation is ran by areas in initialize.
-/datum/map_generator
-
-///This proc will be ran by areas on Initialize, and provides the areas turfs as argument to allow for generation.
-/datum/map_generator/proc/generate_terrain(list/turfs, area/generate_in)
- return
-
-/// Populate terrain with flora, fauna, features and basically everything that isn't a turf
-/datum/map_generator/proc/populate_terrain(list/turfs, area/generate_in)
- return
diff --git a/code/datums/martial/_martial.dm b/code/datums/martial/_martial.dm
index a3c62f5ed3fd9..c91ae511788d2 100644
--- a/code/datums/martial/_martial.dm
+++ b/code/datums/martial/_martial.dm
@@ -101,7 +101,3 @@
if(help_verb)
remove_verb(holder_living, help_verb)
return
-
-///Gets called when a projectile hits the owner. Returning anything other than BULLET_ACT_HIT will stop the projectile from hitting the mob.
-/datum/martial_art/proc/on_projectile_hit(mob/living/attacker, obj/projectile/P, def_zone)
- return BULLET_ACT_HIT
diff --git a/code/datums/martial/cqc.dm b/code/datums/martial/cqc.dm
index e5b959b340f50..cae4cdf14b210 100644
--- a/code/datums/martial/cqc.dm
+++ b/code/datums/martial/cqc.dm
@@ -102,7 +102,8 @@
var/atom/throw_target = get_edge_target_turf(defender, attacker.dir)
defender.throw_at(throw_target, 1, 14, attacker)
defender.apply_damage(10, attacker.get_attack_type())
- defender.adjustStaminaLoss(45)
+ if(defender.body_position == LYING_DOWN && !defender.IsUnconscious())
+ defender.adjustStaminaLoss(45)
log_combat(attacker, defender, "kicked (CQC)")
. = TRUE
diff --git a/code/datums/martial/sleeping_carp.dm b/code/datums/martial/sleeping_carp.dm
index cfbb37e08a909..f8f05248b27fa 100644
--- a/code/datums/martial/sleeping_carp.dm
+++ b/code/datums/martial/sleeping_carp.dm
@@ -1,6 +1,6 @@
#define STRONG_PUNCH_COMBO "HH"
#define LAUNCH_KICK_COMBO "HD"
-#define DROP_KICK_COMBO "HG"
+#define DROP_KICK_COMBO "DD"
/datum/martial_art/the_sleeping_carp
name = "The Sleeping Carp"
@@ -8,18 +8,21 @@
allow_temp_override = FALSE
help_verb = /mob/living/proc/sleeping_carp_help
display_combos = TRUE
+ var/list/scarp_traits = list(TRAIT_NOGUNS, TRAIT_HARDLY_WOUNDED, TRAIT_NODISMEMBER, TRAIT_HEAVY_SLEEPER)
/datum/martial_art/the_sleeping_carp/teach(mob/living/target, make_temporary = FALSE)
. = ..()
if(!.)
return
- target.add_traits(list(TRAIT_NOGUNS, TRAIT_HARDLY_WOUNDED, TRAIT_NODISMEMBER), SLEEPING_CARP_TRAIT)
+ target.add_traits(scarp_traits, SLEEPING_CARP_TRAIT)
RegisterSignal(target, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby))
+ RegisterSignal(target, COMSIG_ATOM_PRE_BULLET_ACT, PROC_REF(hit_by_projectile))
target.faction |= FACTION_CARP //:D
/datum/martial_art/the_sleeping_carp/on_remove(mob/living/target)
- target.remove_traits(list(TRAIT_NOGUNS, TRAIT_HARDLY_WOUNDED, TRAIT_NODISMEMBER), SLEEPING_CARP_TRAIT)
+ target.remove_traits(scarp_traits, SLEEPING_CARP_TRAIT)
UnregisterSignal(target, COMSIG_ATOM_ATTACKBY)
+ UnregisterSignal(target, COMSIG_ATOM_PRE_BULLET_ACT)
target.faction -= FACTION_CARP //:(
. = ..()
@@ -47,12 +50,12 @@
defender.visible_message(span_danger("[attacker] [atk_verb]s [defender]!"), \
span_userdanger("[attacker] [atk_verb]s you!"), null, null, attacker)
to_chat(attacker, span_danger("You [atk_verb] [defender]!"))
- playsound(get_turf(defender), 'sound/weapons/punch1.ogg', 25, TRUE, -1)
+ playsound(defender, 'sound/weapons/punch1.ogg', 25, TRUE, -1)
log_combat(attacker, defender, "strong punched (Sleeping Carp)")
defender.apply_damage(20, attacker.get_attack_type(), affecting)
return
-///Crashing Wave Kick: Punch Shove combo, throws people seven tiles backwards
+///Crashing Wave Kick: Harm Disarm combo, throws people seven tiles backwards
/datum/martial_art/the_sleeping_carp/proc/launchKick(mob/living/attacker, mob/living/defender)
attacker.do_attack_animation(defender, ATTACK_EFFECT_KICK)
defender.visible_message(span_warning("[attacker] kicks [defender] square in the chest, sending them flying!"), \
@@ -64,55 +67,81 @@
log_combat(attacker, defender, "launchkicked (Sleeping Carp)")
return
-///Keelhaul: Harm Grab combo, knocks people down, deals stamina damage while they're on the floor
+///Keelhaul: Disarm Disarm combo, knocks people down and deals substantial stamina damage, and also discombobulates them. Knocks objects out of their hands if they're already on the ground.
/datum/martial_art/the_sleeping_carp/proc/dropKick(mob/living/attacker, mob/living/defender)
attacker.do_attack_animation(defender, ATTACK_EFFECT_KICK)
playsound(get_turf(attacker), 'sound/effects/hit_kick.ogg', 50, TRUE, -1)
if(defender.body_position == STANDING_UP)
- defender.apply_damage(10, attacker.get_attack_type(), BODY_ZONE_HEAD, wound_bonus = CANT_WOUND)
- defender.apply_damage(40, STAMINA, BODY_ZONE_HEAD)
defender.Knockdown(4 SECONDS)
defender.visible_message(span_warning("[attacker] kicks [defender] in the head, sending them face first into the floor!"), \
span_userdanger("You are kicked in the head by [attacker], sending you crashing to the floor!"), span_hear("You hear a sickening sound of flesh hitting flesh!"), COMBAT_MESSAGE_RANGE, attacker)
else
- defender.apply_damage(5, attacker.get_attack_type(), BODY_ZONE_HEAD, wound_bonus = CANT_WOUND)
- defender.apply_damage(40, STAMINA, BODY_ZONE_HEAD)
defender.drop_all_held_items()
defender.visible_message(span_warning("[attacker] kicks [defender] in the head!"), \
span_userdanger("You are kicked in the head by [attacker]!"), span_hear("You hear a sickening sound of flesh hitting flesh!"), COMBAT_MESSAGE_RANGE, attacker)
+ defender.apply_damage(40, STAMINA)
+ defender.adjust_dizzy_up_to(10 SECONDS, 10 SECONDS)
+ defender.adjust_temp_blindness_up_to(2 SECONDS, 10 SECONDS)
log_combat(attacker, defender, "dropkicked (Sleeping Carp)")
return
/datum/martial_art/the_sleeping_carp/grab_act(mob/living/attacker, mob/living/defender)
+ if(!can_deflect(attacker)) //allows for deniability
+ return ..()
+
add_to_streak("G", defender)
if(check_streak(attacker, defender))
return TRUE
- log_combat(attacker, defender, "grabbed (Sleeping Carp)")
+ var/grab_log_description = "grabbed"
+ attacker.do_attack_animation(defender, ATTACK_EFFECT_PUNCH)
+ playsound(defender, 'sound/weapons/punch1.ogg', 25, TRUE, -1)
+ if(defender.stat != DEAD && !defender.IsUnconscious() && defender.getStaminaLoss() >= 80) //We put our target to sleep.
+ defender.visible_message(
+ span_danger("[attacker] carefully pinch a nerve in [defender]'s neck, knocking them out cold"),
+ span_userdanger("[attacker] pinches something in your neck, and you fall unconscious!"),
+ )
+ grab_log_description = "grabbed and nerve pinched"
+ defender.Unconscious(10 SECONDS)
+ defender.apply_damage(20, STAMINA)
+ log_combat(attacker, defender, "[grab_log_description] (Sleeping Carp)")
return ..()
/datum/martial_art/the_sleeping_carp/harm_act(mob/living/attacker, mob/living/defender)
add_to_streak("H", defender)
if(check_streak(attacker, defender))
return TRUE
+
var/obj/item/bodypart/affecting = defender.get_bodypart(defender.get_random_valid_zone(attacker.zone_selected))
attacker.do_attack_animation(defender, ATTACK_EFFECT_PUNCH)
var/atk_verb = pick("kick", "chop", "hit", "slam")
defender.visible_message(span_danger("[attacker] [atk_verb]s [defender]!"), \
span_userdanger("[attacker] [atk_verb]s you!"), null, null, attacker)
to_chat(attacker, span_danger("You [atk_verb] [defender]!"))
- defender.apply_damage(rand(10,15), BRUTE, affecting, wound_bonus = CANT_WOUND)
- playsound(get_turf(defender), 'sound/weapons/punch1.ogg', 25, TRUE, -1)
+
+ defender.apply_damage(rand(10,15), attacker.get_attack_type(), affecting, wound_bonus = CANT_WOUND)
+ playsound(defender, 'sound/weapons/punch1.ogg', 25, TRUE, -1)
log_combat(attacker, defender, "punched (Sleeping Carp)")
+
return TRUE
/datum/martial_art/the_sleeping_carp/disarm_act(mob/living/attacker, mob/living/defender)
+ if(!can_deflect(attacker)) //allows for deniability
+ return ..()
+
add_to_streak("D", defender)
if(check_streak(attacker, defender))
return TRUE
+
+ attacker.do_attack_animation(defender, ATTACK_EFFECT_PUNCH)
+ playsound(defender, 'sound/weapons/punch1.ogg', 25, TRUE, -1)
+ defender.apply_damage(20, STAMINA)
log_combat(attacker, defender, "disarmed (Sleeping Carp)")
+
return ..()
/datum/martial_art/the_sleeping_carp/proc/can_deflect(mob/living/carp_user)
+ if(!can_use(carp_user) || !carp_user.combat_mode)
+ return FALSE
if(carp_user.incapacitated(IGNORE_GRAB)) //NO STUN
return FALSE
if(!(carp_user.mobility_flags & MOBILITY_USE)) //NO UNABLE TO USE
@@ -124,17 +153,20 @@
return FALSE
return TRUE
-/datum/martial_art/the_sleeping_carp/on_projectile_hit(mob/living/carp_user, obj/projectile/P, def_zone)
- . = ..()
+/datum/martial_art/the_sleeping_carp/proc/hit_by_projectile(mob/living/carp_user, obj/projectile/hitting_projectile, def_zone)
+ SIGNAL_HANDLER
+
if(!can_deflect(carp_user))
- return BULLET_ACT_HIT
- if(carp_user.throw_mode)
- carp_user.visible_message(span_danger("[carp_user] effortlessly swats the projectile aside! They can block bullets with their bare hands!"), span_userdanger("You deflect the projectile!"))
- playsound(get_turf(carp_user), pick('sound/weapons/bulletflyby.ogg', 'sound/weapons/bulletflyby2.ogg', 'sound/weapons/bulletflyby3.ogg'), 75, TRUE)
- P.firer = carp_user
- P.set_angle(rand(0, 360))//SHING
- return BULLET_ACT_FORCE_PIERCE
- return BULLET_ACT_HIT
+ return NONE
+
+ carp_user.visible_message(
+ span_danger("[carp_user] effortlessly swats [hitting_projectile] aside! [carp_user.p_They()] can block bullets with [carp_user.p_their()] bare hands!"),
+ span_userdanger("You deflect [hitting_projectile]!"),
+ )
+ playsound(carp_user, pick('sound/weapons/bulletflyby.ogg', 'sound/weapons/bulletflyby2.ogg', 'sound/weapons/bulletflyby3.ogg'), 75, TRUE)
+ hitting_projectile.firer = carp_user
+ hitting_projectile.set_angle(rand(0, 360))//SHING
+ return COMPONENT_BULLET_PIERCED
///Signal from getting attacked with an item, for a special interaction with touch spells
/datum/martial_art/the_sleeping_carp/proc/on_attackby(mob/living/carp_user, obj/item/attack_weapon, mob/attacker, params)
@@ -158,12 +190,13 @@
set category = "Sleeping Carp"
to_chat(usr, "You retreat inward and recall the teachings of the Sleeping Carp...\n\
- [span_notice("Gnashing Teeth")]: Punch Punch. Deal additional damage every second (consecutive) punch!\n\
+ [span_notice("Gnashing Teeth")]: Punch Punch. Deal additional damage every second (consecutive) punch! Very good chance to wound!\n\
[span_notice("Crashing Wave Kick")]: Punch Shove. Launch your opponent away from you with incredible force!\n\
- [span_notice("Keelhaul")]: Punch Grab. Kick an opponent to the floor, knocking them down! If your opponent is already prone, this move will disarm them and deal additional stamina damage to them.\n\
- While in throw mode (and not stunned, not a hulk, and not in a mech), you can reflect all projectiles that come your way, sending them back at the people who fired them! \
- Also, you are more resilient against suffering wounds in combat, and your limbs cannot be dismembered. This grants you extra staying power during extended combat, especially against slashing and other bleeding weapons. \
- You are not invincible, however- while you may not suffer debilitating wounds often, you must still watch your health and should have appropriate medical supplies for use during downtime. \
+ [span_notice("Keelhaul")]: Shove Shove. Nonlethally kick an opponent to the floor, knocking them down, discombobulating them and dealing substantial stamina damage. If they're already prone, disarm them as well.\n\
+ [span_notice("Grabs and Shoves")]: While in combat mode, your typical grab and shove do decent stamina damage. If you grab someone who has substantial amounts of stamina damage, you knock them out!\n\
+ While in combat mode (and not stunned, not a hulk, and not in a mech), you can reflect all projectiles that come your way, sending them back at the people who fired them! \n\
+ Also, you are more resilient against suffering wounds in combat, and your limbs cannot be dismembered. This grants you extra staying power during extended combat, especially against slashing and other bleeding weapons. \n\
+ You are not invincible, however- while you may not suffer debilitating wounds often, you must still watch your health and should have appropriate medical supplies for use during downtime. \n\
In addition, your training has imbued you with a loathing of guns, and you can no longer use them.")
diff --git a/code/datums/materials/_material.dm b/code/datums/materials/_material.dm
index 06d26f31ea308..53e661d39d838 100644
--- a/code/datums/materials/_material.dm
+++ b/code/datums/materials/_material.dm
@@ -162,11 +162,17 @@ Simple datum which is instanced once per type and is used for every object of sa
// We assume no parallax means no space means no light
if(SSmapping.level_trait(on.z, ZTRAIT_NOPARALLAX))
return
- on.set_light(2, 0.75, get_starlight_color())
+ if(!starlight_color)
+ on.RegisterSignal(SSdcs, COMSIG_STARLIGHT_COLOR_CHANGED, TYPE_PROC_REF(/turf, material_starlight_changed))
+ RegisterSignal(on, COMSIG_QDELETING, PROC_REF(lit_turf_deleted))
+ on.set_light(2, 1, starlight_color || GLOB.starlight_color, l_height = LIGHTING_HEIGHT_SPACE)
-///Gets the space color and possible changed color if space is different
-/datum/material/proc/get_starlight_color()
- return starlight_color || GLOB.starlight_color
+/turf/proc/material_starlight_changed(datum/source, old_star, new_star)
+ if(light_color == old_star)
+ set_light_color(new_star)
+
+/datum/material/proc/lit_turf_deleted(turf/source)
+ source.set_light(0, 0, null)
/datum/material/proc/get_greyscale_config_for(datum/greyscale_config/config_path)
if(!config_path)
@@ -220,6 +226,9 @@ Simple datum which is instanced once per type and is used for every object of sa
/datum/material/proc/on_removed_turf(turf/T, amount, material_flags)
if(alpha < 255)
T.RemoveElement(/datum/element/turf_z_transparency)
+ // yeets glow
+ T.UnregisterSignal(SSdcs, COMSIG_STARLIGHT_COLOR_CHANGED)
+ T.set_light(0, 0, null)
/**
* This proc is called when the mat is found in an item that's consumed by accident. see /obj/item/proc/on_accidental_consumption.
diff --git a/code/datums/materials/meat.dm b/code/datums/materials/meat.dm
index 68d3c1d77ab30..a742a9c71296e 100644
--- a/code/datums/materials/meat.dm
+++ b/code/datums/materials/meat.dm
@@ -18,25 +18,45 @@
/datum/material/meat/on_removed(atom/source, amount, material_flags)
. = ..()
qdel(source.GetComponent(/datum/component/edible))
+ qdel(source.GetComponent(/datum/component/blood_walk))
+ qdel(source.GetComponent(/datum/component/bloody_spreader))
/datum/material/meat/on_applied_obj(obj/O, amount, material_flags)
. = ..()
- make_edible(O, amount, material_flags)
+ make_meaty(O, amount, material_flags)
/datum/material/meat/on_applied_turf(turf/T, amount, material_flags)
. = ..()
- make_edible(T, amount, material_flags)
+ make_meaty(T, amount, material_flags)
-/datum/material/meat/proc/make_edible(atom/source, amount, material_flags)
+/datum/material/meat/proc/make_meaty(atom/source, amount, material_flags)
var/nutriment_count = 3 * (amount / SHEET_MATERIAL_AMOUNT)
var/oil_count = 2 * (amount / SHEET_MATERIAL_AMOUNT)
source.AddComponent(/datum/component/edible, \
initial_reagents = list(/datum/reagent/consumable/nutriment = nutriment_count, /datum/reagent/consumable/nutriment/fat/oil = oil_count), \
foodtypes = RAW | MEAT | GROSS, \
eat_time = 3 SECONDS, \
- tastes = list("Fleshy"))
+ tastes = list("Meaty"))
+ source.AddComponent(
+ /datum/component/bloody_spreader,\
+ blood_left = (nutriment_count + oil_count) * 0.3,\
+ blood_dna = list("meaty DNA" = "MT-"),\
+ diseases = null,\
+ )
+
+ // Turfs can't handle the meaty goodness of blood walk.
+ if(!ismovable(source))
+ return
+
+ source.AddComponent(
+ /datum/component/blood_walk,\
+ blood_type = /obj/effect/decal/cleanable/blood,\
+ blood_spawn_chance = 35,\
+ max_blood = (nutriment_count + oil_count) * 0.3,\
+ )
+
/datum/material/meat/mob_meat
init_flags = MATERIAL_INIT_BESPOKE
var/subjectname = ""
diff --git a/code/datums/mocking/client.dm b/code/datums/mocking/client.dm
index 8e09883ae2177..72a0eddd5da63 100644
--- a/code/datums/mocking/client.dm
+++ b/code/datums/mocking/client.dm
@@ -27,14 +27,22 @@
var/tgui_say
var/typing_indicators
+/datum/client_interface/New()
+ ..()
+ var/static/mock_client_uid = 0
+ mock_client_uid++
+
+ src.key = "[key]_[mock_client_uid]"
+ ckey = ckey(key)
+
+ GLOB.directory[ckey] = src
+
+/datum/client_interface/Destroy(force, ...)
+ GLOB.directory -= ckey
+ return ..()
+
/datum/client_interface/proc/IsByondMember()
return FALSE
-/datum/client_interface/New(key)
- ..()
- if(key)
- src.key = key
- ckey = ckey(key)
-
/datum/client_interface/proc/set_macros()
return
diff --git a/code/datums/mood_events/food_events.dm b/code/datums/mood_events/food_events.dm
index 7d2dcc439de46..7d33e7e57ce06 100644
--- a/code/datums/mood_events/food_events.dm
+++ b/code/datums/mood_events/food_events.dm
@@ -13,6 +13,11 @@
mood_change = -6
timeout = 4 MINUTES
+/datum/mood_event/allergic_food
+ description = "My throat itches."
+ mood_change = -2
+ timeout = 4 MINUTES
+
/datum/mood_event/breakfast
description = "Nothing like a hearty breakfast to start the shift."
mood_change = 2
diff --git a/code/datums/mutations/chameleon.dm b/code/datums/mutations/chameleon.dm
index 9cd155594ec83..3de361ba5485e 100644
--- a/code/datums/mutations/chameleon.dm
+++ b/code/datums/mutations/chameleon.dm
@@ -14,7 +14,7 @@
return
owner.alpha = CHAMELEON_MUTATION_DEFAULT_TRANSPARENCY
RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
- RegisterSignal(owner, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, PROC_REF(on_attack_hand))
+ RegisterSignal(owner, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_attack_hand))
/datum/mutation/human/chameleon/on_life(seconds_per_tick, times_fired)
owner.alpha = max(owner.alpha - (12.5 * (GET_MUTATION_POWER(src)) * seconds_per_tick), 0)
@@ -54,4 +54,4 @@
if(..())
return
owner.alpha = 255
- UnregisterSignal(owner, list(COMSIG_MOVABLE_MOVED, COMSIG_HUMAN_EARLY_UNARMED_ATTACK))
+ UnregisterSignal(owner, list(COMSIG_MOVABLE_MOVED, COMSIG_LIVING_UNARMED_ATTACK))
diff --git a/code/datums/mutations/hulk.dm b/code/datums/mutations/hulk.dm
index ba04fc07c54ec..73198e8afb272 100644
--- a/code/datums/mutations/hulk.dm
+++ b/code/datums/mutations/hulk.dm
@@ -29,26 +29,27 @@
part.variable_color = "#00aa00"
owner.update_body_parts()
owner.add_mood_event("hulk", /datum/mood_event/hulk)
- RegisterSignal(owner, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, PROC_REF(on_attack_hand))
+ RegisterSignal(owner, COMSIG_LIVING_EARLY_UNARMED_ATTACK, PROC_REF(on_attack_hand))
RegisterSignal(owner, COMSIG_MOB_SAY, PROC_REF(handle_speech))
RegisterSignal(owner, COMSIG_MOB_CLICKON, PROC_REF(check_swing))
/datum/mutation/human/hulk/proc/on_attack_hand(mob/living/carbon/human/source, atom/target, proximity, modifiers)
SIGNAL_HANDLER
- if(!proximity)
- return
- if(!source.combat_mode || LAZYACCESS(modifiers, RIGHT_CLICK))
- return
- if(target.attack_hulk(owner))
- if(world.time > (last_scream + scream_delay))
- last_scream = world.time
- INVOKE_ASYNC(src, PROC_REF(scream_attack), source)
- log_combat(source, target, "punched", "hulk powers")
- source.do_attack_animation(target, ATTACK_EFFECT_SMASH)
- source.changeNext_move(CLICK_CD_MELEE)
-
- return COMPONENT_CANCEL_ATTACK_CHAIN
+ if(!source.combat_mode || !proximity || LAZYACCESS(modifiers, RIGHT_CLICK))
+ return NONE
+ if(!source.can_unarmed_attack())
+ return COMPONENT_SKIP_ATTACK
+ if(!target.attack_hulk(owner))
+ return NONE
+
+ if(world.time > (last_scream + scream_delay))
+ last_scream = world.time
+ INVOKE_ASYNC(src, PROC_REF(scream_attack), source)
+ log_combat(source, target, "punched", "hulk powers")
+ source.do_attack_animation(target, ATTACK_EFFECT_SMASH)
+ source.changeNext_move(CLICK_CD_MELEE)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
/datum/mutation/human/hulk/proc/scream_attack(mob/living/carbon/human/source)
source.say("WAAAAAAAAAAAAAAGH!", forced="hulk")
@@ -90,7 +91,7 @@
part.variable_color = null
owner.update_body_parts()
owner.clear_mood_event("hulk")
- UnregisterSignal(owner, COMSIG_HUMAN_EARLY_UNARMED_ATTACK)
+ UnregisterSignal(owner, COMSIG_LIVING_EARLY_UNARMED_ATTACK)
UnregisterSignal(owner, COMSIG_MOB_SAY)
UnregisterSignal(owner, COMSIG_MOB_CLICKON)
diff --git a/code/datums/mutations/touch.dm b/code/datums/mutations/touch.dm
index 3f798ba52b216..743f2b3fbf83b 100644
--- a/code/datums/mutations/touch.dm
+++ b/code/datums/mutations/touch.dm
@@ -38,7 +38,7 @@
///This var decides if the spell should chain, dictated by presence of power chromosome
var/chain = FALSE
///Affects damage, should do about 1 per limb
- var/zap_power = 3e6
+ var/zap_power = 7500
///Range of tesla shock bounces
var/zap_range = 7
///flags that dictate what the tesla shock can interact with, Can only damage mobs, Cannot damage machines or generate energy
@@ -60,7 +60,7 @@
span_userdanger("[caster] electrocutes you!"),
)
if(chain)
- tesla_zap(victim, zap_range, zap_power, zap_flags)
+ tesla_zap(source = victim, zap_range = zap_range, power = zap_power, cutoff = 1e3, zap_flags = zap_flags)
carbon_victim.visible_message(span_danger("An arc of electricity explodes out of [victim]!"))
return TRUE
@@ -72,7 +72,7 @@
span_userdanger("[caster] electrocutes you!"),
)
if(chain)
- tesla_zap(victim, zap_range, zap_power, zap_flags)
+ tesla_zap(source = victim, zap_range = zap_range, power = zap_power, cutoff = 1e3, zap_flags = zap_flags)
living_victim.visible_message(span_danger("An arc of electricity explodes out of [victim]!"))
return TRUE
diff --git a/code/datums/quirks/negative_quirks/food_allergy.dm b/code/datums/quirks/negative_quirks/food_allergy.dm
new file mode 100644
index 0000000000000..c2f4eae4d0ed2
--- /dev/null
+++ b/code/datums/quirks/negative_quirks/food_allergy.dm
@@ -0,0 +1,45 @@
+GLOBAL_LIST_INIT(possible_food_allergies, list(
+ "Alcohol" = ALCOHOL,
+ "Bugs" = BUGS,
+ "Dairy" = DAIRY,
+ "Fruit" = FRUIT,
+ "Grain" = GRAIN,
+ "Meat" = MEAT,
+ "Nuts" = NUTS,
+ "Seafood" = SEAFOOD,
+ "Sugar" = SUGAR,
+ "Vegetables" = VEGETABLES,
+))
+
+/datum/quirk/item_quirk/food_allergic
+ name = "Food Allergy"
+ desc = "Ever since you were a kid, you've been allergic to certain foods."
+ icon = FA_ICON_SHRIMP
+ value = -2
+ gain_text = span_danger("You feel your immune system shift.")
+ lose_text = span_notice("You feel your immune system phase back into perfect shape.")
+ medical_record_text = "Patient's immune system responds violently to certain food."
+ hardcore_value = 1
+ quirk_flags = QUIRK_HUMAN_ONLY
+ mail_goodies = list(/obj/item/reagent_containers/hypospray/medipen)
+ /// Footype flags that will trigger the allergy
+ var/target_foodtypes = NONE
+
+/datum/quirk/item_quirk/food_allergic/add(client/client_source)
+ if(target_foodtypes != NONE) // Already set, don't care
+ return
+
+ var/desired_allergy = client_source?.prefs.read_preference(/datum/preference/choiced/food_allergy) || "Random"
+ if(desired_allergy != "Random")
+ target_foodtypes = GLOB.possible_food_allergies[desired_allergy]
+ if(target_foodtypes != NONE) // Got a preference, don't care
+ return
+
+ target_foodtypes = pick(flatten_list(GLOB.possible_food_allergies))
+
+/datum/quirk/item_quirk/food_allergic/add_unique(client/client_source)
+ var/what_are_we_actually_killed_by = english_list(bitfield_to_list(target_foodtypes, FOOD_FLAGS_IC)) // This should never be more than one thing but just in case we can support it
+ to_chat(client_source.mob, span_info("You are allergic to [what_are_we_actually_killed_by]. Watch what you eat!"))
+
+ var/obj/item/clothing/accessory/dogtag/allergy/dogtag = new(quirk_holder, what_are_we_actually_killed_by)
+ give_item_to_holder(dogtag, list(LOCATION_BACKPACK = ITEM_SLOT_BACKPACK, LOCATION_HANDS = ITEM_SLOT_HANDS), flavour_text = "Keep it close around the kitchen.")
diff --git a/code/datums/quirks/negative_quirks/glass_jaw.dm b/code/datums/quirks/negative_quirks/glass_jaw.dm
index 33ad19add6ddb..567753f6feecf 100644
--- a/code/datums/quirks/negative_quirks/glass_jaw.dm
+++ b/code/datums/quirks/negative_quirks/glass_jaw.dm
@@ -34,14 +34,16 @@
/datum/quirk/glass_jaw/proc/punch_out(mob/living/carbon/source, damage, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction, attacking_item)
SIGNAL_HANDLER
- if((damagetype != BRUTE) || (def_zone != BODY_ZONE_HEAD))
+
+ if(isbodypart(def_zone))
+ var/obj/item/bodypart/hitting = def_zone
+ def_zone = hitting.body_zone
+ if(damagetype != BRUTE || def_zone != BODY_ZONE_HEAD)
return
- var/actual_damage = damage - (damage * blocked/100)
- //only roll for knockouts at 5 damage or more
- if(actual_damage < 5)
+ if(damage < 5)
return
//blunt items are more likely to knock out, but sharp ones are still capable of doing it
- if(prob(CEILING(actual_damage * (sharpness & (SHARP_EDGED|SHARP_POINTY) ? 0.65 : 1), 1)))
+ if(prob(CEILING(damage * (sharpness & (SHARP_EDGED|SHARP_POINTY) ? 0.65 : 1), 1)))
//don't display the message if little mac is already KO'd
if(!source.IsUnconscious())
source.visible_message(
diff --git a/code/datums/recipe.dm b/code/datums/recipe.dm
index de3d3d9b96d70..2149c5452ead2 100644
--- a/code/datums/recipe.dm
+++ b/code/datums/recipe.dm
@@ -89,7 +89,6 @@
for (var/obj/O in (container.contents-result_obj))
if (O.reagents)
O.reagents.del_reagent(/datum/reagent/consumable/nutriment)
- O.reagents.update_total()
O.reagents.trans_to(result_obj, O.reagents.total_volume)
qdel(O)
container.reagents.clear_reagents()
diff --git a/code/datums/ruins/icemoon.dm b/code/datums/ruins/icemoon.dm
index 6197330340d4d..36ce615e9bd69 100644
--- a/code/datums/ruins/icemoon.dm
+++ b/code/datums/ruins/icemoon.dm
@@ -12,6 +12,12 @@
// above ground only
+/datum/map_template/ruin/icemoon/gas
+ name = "Lizard Gas Station"
+ id = "lizgasruin"
+ description = "A gas station. It appears to have been recently open and is in mint condition."
+ suffix = "icemoon_surface_gas.dmm"
+
/datum/map_template/ruin/icemoon/lust
name = "Ruin of Lust"
id = "lust"
diff --git a/code/datums/ruins/space.dm b/code/datums/ruins/space.dm
index a371b248b25e7..726b42431a184 100644
--- a/code/datums/ruins/space.dm
+++ b/code/datums/ruins/space.dm
@@ -382,6 +382,12 @@
name = "The Faceoff"
description = "What do you get when a meeting of the enemy corporations get crashed?"
+/datum/map_template/ruin/space/meatstation
+ id = "meatderelict"
+ suffix = "meatderelict.dmm"
+ name = "Bioresearch Outpost"
+ description = "A bioresearch experiment gone wrong."
+
/datum/map_template/ruin/space/ghost_restaurant
id = "space_ghost_restaurant.dmm"
suffix = "space_ghost_restaurant.dmm"
diff --git a/code/datums/saymode.dm b/code/datums/saymode.dm
index 78ec5e6af38a5..e2425ce92b157 100644
--- a/code/datums/saymode.dm
+++ b/code/datums/saymode.dm
@@ -77,8 +77,8 @@
/datum/saymode/binary/handle_message(mob/living/user, message, datum/language/language)
if(isdrone(user))
- var/mob/living/simple_animal/drone/D = user
- D.drone_chat(message)
+ var/mob/living/basic/drone/drone_user = user
+ drone_user.drone_chat(message)
return FALSE
if(user.binarycheck())
user.robot_talk(message)
@@ -96,17 +96,3 @@
AI.holopad_talk(message, language)
return FALSE
return TRUE
-
-/datum/saymode/mafia
- key = "j"
- mode = MODE_MAFIA
-
-/datum/saymode/mafia/handle_message(mob/living/user, message, datum/language/language)
- var/datum/mafia_controller/MF = GLOB.mafia_game
- if (!MF)
- return TRUE
- var/datum/mafia_role/R = MF.player_role_lookup[user]
- if(!R || R.team != "mafia")
- return TRUE
- MF.send_message(span_changeling("[R.body.real_name]: [message]"), "mafia")
- return FALSE
diff --git a/code/datums/signals.dm b/code/datums/signals.dm
index 9a43c88fa7e31..97334253b4e5c 100644
--- a/code/datums/signals.dm
+++ b/code/datums/signals.dm
@@ -33,22 +33,24 @@
var/list/target_procs = (procs[target] ||= list())
var/list/lookup = (target._listen_lookup ||= list())
- if(!override && target_procs[signal_type])
- var/override_message = "[signal_type] overridden. Use override = TRUE to suppress this warning.\nTarget: [target] ([target.type]) Proc: [proctype]"
- log_signal(override_message)
- stack_trace(override_message)
-
+ var/exists = target_procs[signal_type]
target_procs[signal_type] = proctype
+
+ if(exists)
+ if(!override)
+ var/override_message = "[signal_type] overridden. Use override = TRUE to suppress this warning.\nTarget: [target] ([target.type]) Proc: [proctype]"
+ log_signal(override_message)
+ stack_trace(override_message)
+ return
+
var/list/looked_up = lookup[signal_type]
if(isnull(looked_up)) // Nothing has registered here yet
lookup[signal_type] = src
- else if(looked_up == src) // We already registered here
- return
- else if(!length(looked_up)) // One other thing registered here
- lookup[signal_type] = list((looked_up) = TRUE, (src) = TRUE)
+ else if(!islist(looked_up)) // One other thing registered here
+ lookup[signal_type] = list(looked_up, src)
else // Many other things have registered here
- looked_up[src] = TRUE
+ looked_up += src
/// Registers multiple signals to the same proc.
/datum/proc/RegisterSignals(datum/target, list/signal_types, proctype, override = FALSE)
diff --git a/code/datums/skills/fitness.dm b/code/datums/skills/fitness.dm
new file mode 100644
index 0000000000000..a7c49a481ac83
--- /dev/null
+++ b/code/datums/skills/fitness.dm
@@ -0,0 +1,19 @@
+/datum/skill/fitness
+ name = "Fitness"
+ title = "Fitness"
+ desc = "Twinkle twinkle little star, hit the gym and lift the bar."
+ /// The skill value modifier effects the max duration that is possible for /datum/status_effect/exercised
+ modifiers = list(SKILL_VALUE_MODIFIER = list(2 MINUTES, 3 MINUTES, 4 MINUTES, 5 MINUTES, 6 MINUTES, 7 MINUTES, 10 MINUTES))
+ // skill_item_path - your mob sprite gets bigger to showoff so we don't get a special item
+
+/datum/skill/fitness/level_gained(datum/mind/mind, new_level, old_level, silent)
+ . = ..()
+ var/size_boost = (new_level == SKILL_LEVEL_LEGENDARY) ? 0.25 : 0.05
+ var/gym_size = RESIZE_DEFAULT_SIZE + size_boost
+ mind.current.update_transform(gym_size)
+
+/datum/skill/fitness/level_lost(datum/mind/mind, new_level, old_level, silent)
+ . = ..()
+ var/size_boost = (new_level == SKILL_LEVEL_LEGENDARY) ? 0.25 : 0.05
+ var/gym_size = RESIZE_DEFAULT_SIZE + size_boost
+ mind.current.update_transform(RESIZE_DEFAULT_SIZE / gym_size)
diff --git a/code/datums/sprite_accessories.dm b/code/datums/sprite_accessories.dm
index 2a5f3acc92d67..012211b2421a6 100644
--- a/code/datums/sprite_accessories.dm
+++ b/code/datums/sprite_accessories.dm
@@ -85,6 +85,7 @@
//////////////////////
/datum/sprite_accessory/hair
icon = 'icons/mob/human/human_face.dmi' // default icon for all hairs
+ var/y_offset = 0 // Y offset to apply so we can have hair that reaches above the player sprite's visual bounding box
// please make sure they're sorted alphabetically and, where needed, categorized
// try to capitalize the names please~
@@ -103,6 +104,11 @@
name = "Afro (Large)"
icon_state = "hair_bigafro"
+/datum/sprite_accessory/hair/afro_huge
+ name = "Afro (Huge)"
+ icon_state = "hair_hugeafro"
+ y_offset = 6
+
/datum/sprite_accessory/hair/allthefuzz
name = "All The Fuzz"
icon_state = "hair_allthefuzz"
diff --git a/code/datums/station_traits/_station_trait.dm b/code/datums/station_traits/_station_trait.dm
index 1205673c565f9..8e8303c036681 100644
--- a/code/datums/station_traits/_station_trait.dm
+++ b/code/datums/station_traits/_station_trait.dm
@@ -65,9 +65,9 @@
qdel(src)
///Called by decals if they can be colored, to see if we got some cool colors for them. Only takes the first station trait
-/proc/request_station_colors(atom/thing_to_color, pattern = PATTERN_DEFAULT)
+/proc/request_station_colors(atom/thing_to_color, pattern)
for(var/datum/station_trait/trait in SSstation.station_traits)
- var/decal_color = trait.get_decal_color(thing_to_color, pattern)
+ var/decal_color = trait.get_decal_color(thing_to_color, pattern || PATTERN_DEFAULT)
if(decal_color)
return decal_color
return null
diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm
index 14369c04888f0..5101242f42831 100644
--- a/code/datums/status_effects/buffs.dm
+++ b/code/datums/status_effects/buffs.dm
@@ -165,9 +165,73 @@
/datum/status_effect/exercised
id = "Exercised"
- duration = 1200
+ duration = 30 SECONDS
+ status_type = STATUS_EFFECT_REFRESH // New effects will add to total duration
alert_type = null
processing_speed = STATUS_EFFECT_NORMAL_PROCESS
+ alert_type = /atom/movable/screen/alert/status_effect/exercised
+ /// Having any of these reagents in your system extends the duration
+ var/static/list/supplementary_reagents_bonus = list(
+ /datum/reagent/consumable/ethanol/protein_blend = 30 SECONDS, // protein shakes are very robust
+ /datum/reagent/consumable/eggwhite = 20 SECONDS,
+ /datum/reagent/consumable/eggyolk = 15 SECONDS,
+ /datum/reagent/consumable/nutriment/protein = 15 SECONDS,
+ /datum/reagent/consumable/nutriment/vitamin = 10 SECONDS,
+ /datum/reagent/consumable/rice = 10 SECONDS,
+ /datum/reagent/consumable/milk = 10 SECONDS,
+ /datum/reagent/consumable/soymilk = 5 SECONDS, // darn vegans!
+ /datum/reagent/consumable/nutraslop = 5 SECONDS, // prison food to bulk up with
+ // time for the bad stuff
+ /datum/reagent/consumable/sugar = -5 SECONDS,
+ /datum/reagent/consumable/monkey_energy = -5 SECONDS,
+ /datum/reagent/consumable/nutriment/fat = -5 SECONDS,
+ )
+
+/datum/status_effect/exercised/proc/workout_duration(mob/living/new_owner, bonus_time)
+ if(!bonus_time || !new_owner.mind)
+ return 0 SECONDS
+
+ var/modifier = 1
+ if(HAS_TRAIT(new_owner, TRAIT_HULK))
+ modifier += 0.5
+
+ if(HAS_TRAIT(new_owner, TRAIT_FAT)) // less xp until you get into shape
+ modifier -= 0.5
+
+ if(new_owner.reagents.has_reagent(/datum/reagent/drug/pumpup)) // steriods? yes please!
+ modifier += 3
+
+ var/food_boost = 0
+ for(var/datum/reagent/workout_reagent in supplementary_reagents_bonus)
+ if(new_owner.reagents.has_reagent(workout_reagent))
+ food_boost += supplementary_reagents_bonus[workout_reagent]
+
+ var/skill_level_boost = (new_owner.mind.get_skill_level(/datum/skill/fitness) - 1) * 5 SECONDS
+ bonus_time = (bonus_time + food_boost + skill_level_boost) * modifier
+
+ var/exhaustion_limit = new_owner.mind.get_skill_modifier(/datum/skill/fitness, SKILL_VALUE_MODIFIER) + world.time
+ if(duration + bonus_time >= exhaustion_limit)
+ duration = exhaustion_limit
+ to_chat(new_owner, span_userdanger("Your muscles are exhausted! Might be a good idea to sleep..."))
+ new_owner.emote("scream")
+ return // exhaustion_limit
+
+ return bonus_time
+
+/datum/status_effect/exercised/tick(seconds_between_ticks)
+ owner.reagents.metabolize(owner, seconds_between_ticks * SSMOBS_DT, 0) // doubles the metabolization rate
+
+/datum/status_effect/exercised/on_creation(mob/living/new_owner, bonus_time)
+ duration += workout_duration(new_owner, bonus_time)
+ return ..()
+
+/datum/status_effect/exercised/refresh(mob/living/new_owner, bonus_time)
+ duration += workout_duration(new_owner, bonus_time)
+
+/atom/movable/screen/alert/status_effect/exercised
+ name = "Exercise"
+ desc = "You feel well exercised! Sleeping will improve your fitness."
+ icon_state = "exercised"
//Hippocratic Oath: Applied when the Rod of Asclepius is activated.
/datum/status_effect/hippocratic_oath
diff --git a/code/datums/status_effects/debuffs/debuffs.dm b/code/datums/status_effects/debuffs/debuffs.dm
index 44ad9313c8fe1..e748d2d117dab 100644
--- a/code/datums/status_effects/debuffs/debuffs.dm
+++ b/code/datums/status_effects/debuffs/debuffs.dm
@@ -2,6 +2,8 @@
#define HEALING_SLEEP_DEFAULT 0.2
/// The sleep healing multipler for organ passive healing (since organs heal slowly)
#define HEALING_SLEEP_ORGAN_MULTIPLIER 5
+/// The sleep multipler for fitness xp conversion
+#define SLEEP_QUALITY_WORKOUT_MULTIPLER 10
//Largely negative status effects go here, even if they have small benificial effects
//STUN EFFECTS
@@ -163,51 +165,51 @@
/datum/status_effect/incapacitating/sleeping/tick(seconds_between_ticks)
if(owner.maxHealth)
var/health_ratio = owner.health / owner.maxHealth
- var/healing = HEALING_SLEEP_DEFAULT
+ var/sleep_quality = HEALING_SLEEP_DEFAULT
// having high spirits helps us recover
if(owner.mob_mood)
switch(owner.mob_mood.sanity_level)
if(SANITY_LEVEL_GREAT)
- healing = 0.2
+ sleep_quality = 0.2
if(SANITY_LEVEL_NEUTRAL)
- healing = 0.1
+ sleep_quality = 0.1
if(SANITY_LEVEL_DISTURBED)
- healing = 0
+ sleep_quality = 0
if(SANITY_LEVEL_UNSTABLE)
- healing = 0
+ sleep_quality = 0
if(SANITY_LEVEL_CRAZY)
- healing = -0.1
+ sleep_quality = -0.1
if(SANITY_LEVEL_INSANE)
- healing = -0.2
+ sleep_quality = -0.2
var/turf/rest_turf = get_turf(owner)
var/is_sleeping_in_darkness = rest_turf.get_lumcount() <= LIGHTING_TILE_IS_DARK
// sleeping with a blindfold or in the dark helps us rest
if(owner.is_blind_from(EYES_COVERED) || is_sleeping_in_darkness)
- healing += 0.1
+ sleep_quality += 0.1
// sleeping in silence is always better
if(HAS_TRAIT(owner, TRAIT_DEAF))
- healing += 0.1
+ sleep_quality += 0.1
// check for beds
if((locate(/obj/structure/bed) in owner.loc))
- healing += 0.2
+ sleep_quality += 0.2
else if((locate(/obj/structure/table) in owner.loc))
- healing += 0.1
+ sleep_quality += 0.1
// don't forget the bedsheet
if(locate(/obj/item/bedsheet) in owner.loc)
- healing += 0.1
+ sleep_quality += 0.1
// you forgot the pillow
if(locate(/obj/item/pillow) in owner.loc)
- healing += 0.1
+ sleep_quality += 0.1
var/need_mob_update = FALSE
- if(healing > 0)
+ if(sleep_quality > 0)
if(iscarbon(owner))
var/mob/living/carbon/carbon_owner = owner
for(var/obj/item/organ/target_organ as anything in carbon_owner.organs)
@@ -216,14 +218,22 @@
continue
// organ regeneration is very low so we crank up the healing rate to give a good bonus
- var/healing_bonus = target_organ.healing_factor * healing * HEALING_SLEEP_ORGAN_MULTIPLIER
+ var/healing_bonus = target_organ.healing_factor * sleep_quality * HEALING_SLEEP_ORGAN_MULTIPLIER
target_organ.apply_organ_damage(-healing_bonus * target_organ.maxHealth)
+ var/datum/status_effect/exercised/exercised = carbon_owner.has_status_effect(/datum/status_effect/exercised)
+ if(exercised && carbon_owner.mind)
+ // the better you sleep, the more xp you gain
+ carbon_owner.mind.adjust_experience(/datum/skill/fitness, seconds_between_ticks * sleep_quality * SLEEP_QUALITY_WORKOUT_MULTIPLER)
+ carbon_owner.adjust_timed_status_effect(-1 * seconds_between_ticks * sleep_quality * SLEEP_QUALITY_WORKOUT_MULTIPLER, /datum/status_effect/exercised)
+ if(prob(2))
+ to_chat(carbon_owner, span_notice("You feel your fitness improving!"))
+
if(health_ratio > 0.8) // only heals minor physical damage
- need_mob_update += owner.adjustBruteLoss(-0.4 * healing * seconds_between_ticks, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
- need_mob_update += owner.adjustFireLoss(-0.4 * healing * seconds_between_ticks, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
- need_mob_update += owner.adjustToxLoss(-0.2 * healing * seconds_between_ticks, updating_health = FALSE, forced = TRUE, required_biotype = MOB_ORGANIC)
- need_mob_update += owner.adjustStaminaLoss(min(-0.4 * healing * seconds_between_ticks, -0.4 * HEALING_SLEEP_DEFAULT * seconds_between_ticks), updating_stamina = FALSE)
+ need_mob_update += owner.adjustBruteLoss(-0.4 * sleep_quality * seconds_between_ticks, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
+ need_mob_update += owner.adjustFireLoss(-0.4 * sleep_quality * seconds_between_ticks, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
+ need_mob_update += owner.adjustToxLoss(-0.2 * sleep_quality * seconds_between_ticks, updating_health = FALSE, forced = TRUE, required_biotype = MOB_ORGANIC)
+ need_mob_update += owner.adjustStaminaLoss(min(-0.4 * sleep_quality * seconds_between_ticks, -0.4 * HEALING_SLEEP_DEFAULT * seconds_between_ticks), updating_stamina = FALSE)
if(need_mob_update)
owner.updatehealth()
// Drunkenness gets reduced by 0.3% per tick (6% per 2 seconds)
@@ -601,7 +611,7 @@
alert_type = null
/datum/status_effect/spasms/tick(seconds_between_ticks)
- if(owner.stat >= UNCONSCIOUS)
+ if(owner.stat >= UNCONSCIOUS || owner.incapacitated() || HAS_TRAIT(owner, TRAIT_HANDS_BLOCKED) || HAS_TRAIT(owner, TRAIT_IMMOBILIZED))
return
if(!prob(15))
return
@@ -611,8 +621,6 @@
to_chat(owner, span_warning("Your leg spasms!"))
step(owner, pick(GLOB.cardinals))
if(2)
- if(owner.incapacitated())
- return
var/obj/item/held_item = owner.get_active_held_item()
if(!held_item)
return
@@ -641,8 +649,6 @@
owner.ClickOn(owner)
owner.set_combat_mode(FALSE)
if(5)
- if(owner.incapacitated())
- return
var/obj/item/held_item = owner.get_active_held_item()
var/list/turf/targets = list()
for(var/turf/nearby_turfs in oview(owner, 3))
@@ -1035,3 +1041,4 @@
#undef HEALING_SLEEP_DEFAULT
#undef HEALING_SLEEP_ORGAN_MULTIPLIER
+#undef SLEEP_QUALITY_WORKOUT_MULTIPLER
diff --git a/code/datums/status_effects/debuffs/drunk.dm b/code/datums/status_effects/debuffs/drunk.dm
index 3bbb992675ebb..2664bbb4214de 100644
--- a/code/datums/status_effects/debuffs/drunk.dm
+++ b/code/datums/status_effects/debuffs/drunk.dm
@@ -191,13 +191,15 @@
/datum/status_effect/inebriated/drunk/proc/attempt_to_blackout()
var/mob/living/carbon/drunkard = owner
+ if(drunkard.has_trauma_type(/datum/brain_trauma/severe/split_personality/blackout))// prevent ping spamming
+ if(prob(10))
+ to_chat(owner, span_warning("You stumbled and fall over!"))
+ owner.slip(1 SECONDS)
+ return
if(drunkard.gain_trauma(/datum/brain_trauma/severe/split_personality/blackout, TRAUMA_LIMIT_ABSOLUTE))
- drunk_value -= 50 //So that the drunk personality can spice things up without being killed by liver failure
+ drunk_value -= 70 //So that the drunk personality can spice things up without being killed by liver failure
return
- else if(drunkard.has_trauma_type(/datum/brain_trauma/severe/split_personality/blackout) && prob(10))
- to_chat(owner, span_warning("You stumbled and fall over!"))
- owner.slip(1 SECONDS)
- else if(SSshuttle.emergency.mode == SHUTTLE_DOCKED && is_station_level(owner.z))// Don't put us in a deep sleep if the shuttle's here. QoL, mainly.
+ if(SSshuttle.emergency.mode == SHUTTLE_DOCKED && is_station_level(owner.z))// Don't put us in a deep sleep if the shuttle's here. QoL, mainly.
to_chat(owner, span_warning("You're so tired... but you can't miss that shuttle..."))
else
owner.Sleeping(90 SECONDS)
diff --git a/code/datums/status_effects/debuffs/fire_stacks.dm b/code/datums/status_effects/debuffs/fire_stacks.dm
index 6f31faaefaae6..8f52165cdbb12 100644
--- a/code/datums/status_effects/debuffs/fire_stacks.dm
+++ b/code/datums/status_effects/debuffs/fire_stacks.dm
@@ -147,10 +147,8 @@
if(!on_fire)
return TRUE
- if(HAS_TRAIT(owner, TRAIT_HUSK))
- adjust_stacks(-2 * seconds_between_ticks)
- else
- adjust_stacks(owner.fire_stack_decay_rate * seconds_between_ticks)
+ var/decay_multiplier = HAS_TRAIT(owner, TRAIT_HUSK) ? 2 : 1 // husks decay twice as fast
+ adjust_stacks(owner.fire_stack_decay_rate * decay_multiplier * seconds_between_ticks)
if(stacks <= 0)
qdel(src)
diff --git a/code/datums/status_effects/neutral.dm b/code/datums/status_effects/neutral.dm
index 130e91f63a91c..64c78bc7fbb90 100644
--- a/code/datums/status_effects/neutral.dm
+++ b/code/datums/status_effects/neutral.dm
@@ -334,6 +334,22 @@
owner.emote("surrender")
+///For when you need to make someone be prompted for surrender, but not forever
+/datum/status_effect/surrender_timed
+ id = "surrender_timed"
+ duration = 30 SECONDS
+ status_type = STATUS_EFFECT_UNIQUE
+ alert_type = null
+
+/datum/status_effect/surrender_timed/on_apply()
+ owner.apply_status_effect(/datum/status_effect/grouped/surrender, REF(src))
+ return ..()
+
+/datum/status_effect/surrender_timed/on_remove()
+ owner.remove_status_effect(/datum/status_effect/grouped/surrender, REF(src))
+ return ..()
+
+
/*
* A status effect used for preventing caltrop message spam
*
diff --git a/code/datums/storage/storage.dm b/code/datums/storage/storage.dm
index a61a8725a306e..342aec50d0127 100644
--- a/code/datums/storage/storage.dm
+++ b/code/datums/storage/storage.dm
@@ -433,10 +433,13 @@ GLOBAL_LIST_EMPTY(cached_storage_typecaches)
if(!can_insert(to_insert, user, force = force))
return FALSE
+ SEND_SIGNAL(resolve_location, COMSIG_STORAGE_STORED_ITEM, to_insert, user, force)
+
to_insert.item_flags |= IN_STORAGE
to_insert.forceMove(resolve_location)
item_insertion_feedback(user, to_insert, override)
resolve_location.update_appearance()
+ SEND_SIGNAL(to_insert, COMSIG_ITEM_STORED)
return TRUE
/**
@@ -973,6 +976,10 @@ GLOBAL_LIST_EMPTY(cached_storage_typecaches)
/datum/storage/proc/open_storage_attackby_secondary(datum/source, atom/weapon, mob/user)
SIGNAL_HANDLER
+ if(istype(weapon, /obj/item/chameleon))
+ var/obj/item/chameleon/chameleon_weapon = weapon
+ chameleon_weapon.make_copy(source, user)
+
return open_storage_on_signal(source, user)
/// Signal handler to open up the storage when we recieve a signal.
diff --git a/code/datums/storage/subtypes/surgery_tray.dm b/code/datums/storage/subtypes/surgery_tray.dm
index 42b369b4ce922..c5b07b15fe483 100644
--- a/code/datums/storage/subtypes/surgery_tray.dm
+++ b/code/datums/storage/subtypes/surgery_tray.dm
@@ -2,6 +2,7 @@
max_total_storage = 30
max_specific_storage = WEIGHT_CLASS_NORMAL
max_slots = 14
+ animated = FALSE
/datum/storage/surgery_tray/New()
. = ..()
diff --git a/code/datums/weather/weather_types/ash_storm.dm b/code/datums/weather/weather_types/ash_storm.dm
index 92a1ba1eed65b..bb4e5af63f3ad 100644
--- a/code/datums/weather/weather_types/ash_storm.dm
+++ b/code/datums/weather/weather_types/ash_storm.dm
@@ -66,7 +66,7 @@
return FALSE
/datum/weather/ash_storm/weather_act(mob/living/victim)
- victim.adjustFireLoss(4)
+ victim.adjustFireLoss(4, required_bodytype = BODYTYPE_ORGANIC)
/datum/weather/ash_storm/end()
GLOB.ash_storm_sounds -= weak_sounds
diff --git a/code/datums/wires/airlock.dm b/code/datums/wires/airlock.dm
index 8f768d407c206..4fda6f26984fe 100644
--- a/code/datums/wires/airlock.dm
+++ b/code/datums/wires/airlock.dm
@@ -69,14 +69,14 @@
/datum/wires/airlock/interactable(mob/user)
if(!..())
return FALSE
- var/obj/machinery/door/airlock/A = holder
- if(!issilicon(user) && A.isElectrified())
+ var/obj/machinery/door/airlock/airlock = holder
+ if(!issilicon(user) && !isdrone(user) && airlock.isElectrified())
var/mob/living/carbon/carbon_user = user
if (!istype(carbon_user) || carbon_user.should_electrocute(src))
return FALSE
- if(A.is_secure())
+ if(airlock.is_secure())
return FALSE
- if(A.panel_open)
+ if(airlock.panel_open)
return TRUE
/datum/wires/airlock/get_status()
diff --git a/code/datums/wires/explosive.dm b/code/datums/wires/explosive.dm
index 800b5b884449f..c8d576cf4f357 100644
--- a/code/datums/wires/explosive.dm
+++ b/code/datums/wires/explosive.dm
@@ -144,11 +144,3 @@
/datum/wires/explosive/pizza/explode()
var/obj/item/pizzabox/P = holder
P.bomb.detonate()
-
-
-/datum/wires/explosive/gibtonite
- holder_type = /obj/item/gibtonite
-
-/datum/wires/explosive/gibtonite/explode()
- var/obj/item/gibtonite/P = holder
- P.GibtoniteReaction(null, "A wire signal has primed a")
diff --git a/code/datums/wounds/bones.dm b/code/datums/wounds/bones.dm
index 9b8da5177ca17..80c1c0a755713 100644
--- a/code/datums/wounds/bones.dm
+++ b/code/datums/wounds/bones.dm
@@ -61,9 +61,9 @@
/datum/wound/blunt/bone/set_victim(new_victim)
if (victim)
- UnregisterSignal(victim, COMSIG_HUMAN_EARLY_UNARMED_ATTACK)
+ UnregisterSignal(victim, COMSIG_LIVING_UNARMED_ATTACK)
if (new_victim)
- RegisterSignal(new_victim, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, PROC_REF(attack_with_hurt_hand))
+ RegisterSignal(new_victim, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(attack_with_hurt_hand))
return ..()
@@ -114,8 +114,8 @@
/datum/wound/blunt/bone/proc/attack_with_hurt_hand(mob/M, atom/target, proximity)
SIGNAL_HANDLER
- if(victim.get_active_hand() != limb || !victim.combat_mode || !ismob(target) || severity <= WOUND_SEVERITY_MODERATE)
- return
+ if(victim.get_active_hand() != limb || !proximity || !victim.combat_mode || !ismob(target) || severity <= WOUND_SEVERITY_MODERATE)
+ return NONE
// With a severe or critical wound, you have a 15% or 30% chance to proc pain on hit
if(prob((severity - 1) * 15))
@@ -131,6 +131,7 @@
limb.receive_damage(brute=rand(3,7))
return COMPONENT_CANCEL_ATTACK_CHAIN
+ return NONE
/datum/wound/blunt/bone/receive_damage(wounding_type, wounding_dmg, wound_bonus)
if(!victim || wounding_dmg < WOUND_MINIMUM_DAMAGE)
diff --git a/code/game/alternate_appearance.dm b/code/game/alternate_appearance.dm
index 28b7a4f52479d..9f3da121b1217 100644
--- a/code/game/alternate_appearance.dm
+++ b/code/game/alternate_appearance.dm
@@ -149,7 +149,7 @@ GLOBAL_LIST_EMPTY(active_alternate_appearances)
/datum/atom_hud/alternate_appearance/basic/blessed_aware/mobShouldSee(mob/M)
if(M.mind?.holy_role)
return TRUE
- if (istype(M, /mob/living/simple_animal/hostile/construct/wraith))
+ if (istype(M, /mob/living/basic/construct/wraith))
return TRUE
if(isrevenant(M) || IS_WIZARD(M))
return TRUE
diff --git a/code/game/area/areas/centcom.dm b/code/game/area/areas/centcom.dm
index 45a67de4c1c16..012e7a170f726 100644
--- a/code/game/area/areas/centcom.dm
+++ b/code/game/area/areas/centcom.dm
@@ -208,6 +208,18 @@
name = "Syndicate Elite Squad"
icon_state = "syndie-elite"
+//MAFIA
+/area/centcom/mafia
+ name = "Mafia Minigame"
+ icon_state = "mafia"
+ static_lighting = FALSE
+
+ base_lighting_alpha = 255
+ requires_power = FALSE
+ has_gravity = STANDARD_GRAVITY
+ flags_1 = NONE
+ area_flags = BLOCK_SUICIDE | UNIQUE_AREA
+
//CAPTURE THE FLAG
/area/centcom/ctf
name = "Capture the Flag"
diff --git a/code/game/area/areas/misc.dm b/code/game/area/areas/misc.dm
index c7273c75aac04..e6c4224c82093 100644
--- a/code/game/area/areas/misc.dm
+++ b/code/game/area/areas/misc.dm
@@ -22,7 +22,10 @@
/area/space/nearstation
icon_state = "space_near"
- area_flags = UNIQUE_AREA | AREA_USES_STARLIGHT
+ area_flags = UNIQUE_AREA
+ static_lighting = TRUE
+ base_lighting_alpha = 0
+ base_lighting_color = null
/area/misc/start
name = "start area"
diff --git a/code/game/area/areas/ruins/icemoon.dm b/code/game/area/areas/ruins/icemoon.dm
index f8808903ef456..8afa3fae2d08f 100644
--- a/code/game/area/areas/ruins/icemoon.dm
+++ b/code/game/area/areas/ruins/icemoon.dm
@@ -1,5 +1,8 @@
// Icemoon Ruins
+/area/ruin/powered/lizard_gas
+ name = "\improper Lizard Gas Station"
+
/area/ruin/unpowered/buried_library
name = "\improper Buried Library"
diff --git a/code/game/area/areas/ruins/space.dm b/code/game/area/areas/ruins/space.dm
index 1f41ac8e2e277..df72a1e0fe44e 100644
--- a/code/game/area/areas/ruins/space.dm
+++ b/code/game/area/areas/ruins/space.dm
@@ -20,7 +20,7 @@
// Ruin solars define, /area/solars was moved to /area/station/solars, causing the solars specific areas to lose their properties
/area/ruin/space/solars
requires_power = FALSE
- area_flags = UNIQUE_AREA | AREA_USES_STARLIGHT
+ area_flags = UNIQUE_AREA
flags_1 = NONE
ambience_index = AMBIENCE_ENGI
airlock_wires = /datum/wires/airlock/engineering
@@ -310,7 +310,7 @@
icon = 'icons/area/areas_ruins.dmi' // Solars inheriet areas_misc.dmi, not areas_ruin.dmi
icon_state = "os_charlie_solars"
requires_power = FALSE
- area_flags = UNIQUE_AREA | AREA_USES_STARLIGHT
+ area_flags = UNIQUE_AREA
sound_environment = SOUND_AREA_SPACE
/area/ruin/space/ancientstation/charlie/storage
@@ -534,7 +534,7 @@
/area/ruin/space/djstation/solars
name = "\improper DJ Station Solars"
icon_state = "DJ"
- area_flags = UNIQUE_AREA | AREA_USES_STARLIGHT
+ area_flags = UNIQUE_AREA
has_gravity = STANDARD_GRAVITY
/area/ruin/space/djstation/service
@@ -598,6 +598,13 @@
/area/ruin/space/has_grav/derelictsulaco
name = "\improper Derelict Sulaco"
+/area/ruin/space/has_grav/powered/biooutpost
+ name = "\improper Bioresearch Outpost"
+ area_flags = UNIQUE_AREA | NOTELEPORT
+
+/area/ruin/space/has_grav/powered/biooutpost/vault
+ name = "\improper Bioresearch Outpost Secure Testing"
+
// Space Ghost Kitchen
/area/ruin/space/space_ghost_restaurant
name = "\improper Space Ghost Restaurant"
diff --git a/code/game/area/areas/station/solars.dm b/code/game/area/areas/station/solars.dm
index 234e020e8d418..57376e2fb17be 100644
--- a/code/game/area/areas/station/solars.dm
+++ b/code/game/area/areas/station/solars.dm
@@ -5,7 +5,7 @@
/area/station/solars
icon_state = "panels"
requires_power = FALSE
- area_flags = UNIQUE_AREA | AREA_USES_STARLIGHT
+ area_flags = UNIQUE_AREA
flags_1 = NONE
ambience_index = AMBIENCE_ENGI
airlock_wires = /datum/wires/airlock/engineering
diff --git a/code/game/atom_defense.dm b/code/game/atom_defense.dm
index f5759110a6d9a..df7ad78b81cc4 100644
--- a/code/game/atom_defense.dm
+++ b/code/game/atom_defense.dm
@@ -146,4 +146,6 @@
/// A cut-out proc for [/atom/proc/bullet_act] so living mobs can have their own armor behavior checks without causing issues with needing their own on_hit call
/atom/proc/check_projectile_armor(def_zone, obj/projectile/impacting_projectile, is_silent)
+ if(uses_integrity)
+ return clamp(PENETRATE_ARMOUR(get_armor_rating(impacting_projectile.armor_flag), impacting_projectile.armour_penetration), 0, 100)
return 0
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index edca877d90511..339aef1feab90 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -110,6 +110,8 @@
var/light_dir = NORTH
///Boolean variable for toggleable lights. Has no effect without the proper light_system, light_range and light_power values.
var/light_on = TRUE
+ /// How many tiles "up" this light is. 1 is typical, should only really change this if it's a floor light
+ var/light_height = LIGHTING_HEIGHT
///Bitflags to determine lighting-related atom properties.
var/light_flags = NONE
///Our light source. Don't fuck with this directly unless you have a good reason!
@@ -187,6 +189,9 @@
/// How this atom should react to having its astar blocking checked
var/can_astar_pass = CANASTARPASS_DENSITY
+ VAR_PRIVATE/list/invisibility_sources
+ VAR_PRIVATE/current_invisibility_priority = -INFINITY
+
/**
* Called when an atom is created in byond (built in engine proc)
*
@@ -583,19 +588,33 @@
/**
* React to a hit by a projectile object
*
- * Default behaviour is to send the [COMSIG_ATOM_BULLET_ACT] and then call [on_hit][/obj/projectile/proc/on_hit] on the projectile.
- *
* @params
- * hitting_projectile - projectile
- * def_zone - zone hit
- * piercing_hit - is this hit piercing or normal?
+ * * hitting_projectile - projectile
+ * * def_zone - zone hit
+ * * piercing_hit - is this hit piercing or normal?
*/
/atom/proc/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
+ SHOULD_CALL_PARENT(TRUE)
+
+ var/sigreturn = SEND_SIGNAL(src, COMSIG_ATOM_PRE_BULLET_ACT, hitting_projectile, def_zone)
+ if(sigreturn & COMPONENT_BULLET_PIERCED)
+ return BULLET_ACT_FORCE_PIERCE
+ if(sigreturn & COMPONENT_BULLET_BLOCKED)
+ return BULLET_ACT_BLOCK
+ if(sigreturn & COMPONENT_BULLET_ACTED)
+ return BULLET_ACT_HIT
+
SEND_SIGNAL(src, COMSIG_ATOM_BULLET_ACT, hitting_projectile, def_zone)
- // This armor check only matters for the visuals and messages in on_hit(), it's not actually used to reduce damage since
- // only living mobs use armor to reduce damage, but on_hit() is going to need the value no matter what is shot.
- var/visual_armor_check = check_projectile_armor(def_zone, hitting_projectile)
- . = hitting_projectile.on_hit(src, visual_armor_check, def_zone, piercing_hit)
+ if(QDELETED(hitting_projectile)) // Signal deleted it?
+ return BULLET_ACT_BLOCK
+
+ return hitting_projectile.on_hit(
+ target = src,
+ // This armor check only matters for the visuals and messages in on_hit(), it's not actually used to reduce damage since
+ // only living mobs use armor to reduce damage, but on_hit() is going to need the value no matter what is shot.
+ blocked = check_projectile_armor(def_zone, hitting_projectile),
+ pierce_hit = piercing_hit,
+ )
///Return true if we're inside the passed in atom
/atom/proc/in_contents_of(container)//can take class or object instance as argument
@@ -671,10 +690,10 @@
if(!(reagent_sigreturn & STOP_GENERIC_REAGENT_EXAMINE))
if(reagents.flags & TRANSPARENT)
if(reagents.total_volume)
- . += "It contains [round(reagents.total_volume, 0.01)] units of various reagents[user_sees_reagents ? ":" : "."]"
+ . += "It contains [reagents.total_volume] units of various reagents[user_sees_reagents ? ":" : "."]"
if(user_sees_reagents) //Show each individual reagent for detailed examination
for(var/datum/reagent/current_reagent as anything in reagents.reagent_list)
- . += "• [FLOOR(current_reagent.volume, CHEMICAL_QUANTISATION_LEVEL)] units of [current_reagent.name]"
+ . += "• [round(current_reagent.volume, CHEMICAL_VOLUME_ROUNDING)] units of [current_reagent.name]"
if(reagents.is_reacting)
. += span_warning("It is currently reacting!")
. += span_notice("The solution's pH is [round(reagents.ph, 0.01)] and has a temperature of [reagents.chem_temp]K.")
@@ -738,7 +757,6 @@
/// Updates the icon of the atom
/atom/proc/update_icon(updates=ALL)
- SIGNAL_HANDLER
SHOULD_CALL_PARENT(TRUE)
. = NONE
@@ -752,19 +770,54 @@
SSvis_overlays.remove_vis_overlay(src, managed_vis_overlays)
var/list/new_overlays = update_overlays(updates)
- if (managed_overlays)
- if (length(overlays) == (islist(managed_overlays) ? length(managed_overlays) : 1))
- overlays = null
- POST_OVERLAY_CHANGE(src)
- else
- cut_overlay(managed_overlays)
- managed_overlays = null
- if(length(new_overlays))
- if (length(new_overlays) == 1)
- managed_overlays = new_overlays[1]
- else
- managed_overlays = new_overlays
- add_overlay(new_overlays)
+ var/nulls = 0
+ for(var/i in 1 to length(new_overlays))
+ var/atom/maybe_not_an_atom = new_overlays[i]
+ if(isnull(maybe_not_an_atom))
+ nulls++
+ continue
+ if(istext(maybe_not_an_atom) || isicon(maybe_not_an_atom))
+ continue
+ new_overlays[i] = maybe_not_an_atom.appearance
+ if(nulls)
+ for(var/i in 1 to nulls)
+ new_overlays -= null
+
+ var/identical = FALSE
+ var/new_length = length(new_overlays)
+ if(!managed_overlays && !new_length)
+ identical = TRUE
+ else if(!islist(managed_overlays))
+ if(new_length == 1 && managed_overlays == new_overlays[1])
+ identical = TRUE
+ else if(length(managed_overlays) == new_length)
+ identical = TRUE
+ for(var/i in 1 to length(managed_overlays))
+ if(managed_overlays[i] != new_overlays[i])
+ identical = FALSE
+ break
+
+ if(!identical)
+ var/full_control = FALSE
+ if(managed_overlays)
+ full_control = length(overlays) == (islist(managed_overlays) ? length(managed_overlays) : 1)
+ if(full_control)
+ overlays = null
+ else
+ cut_overlay(managed_overlays)
+
+ switch(length(new_overlays))
+ if(0)
+ if(full_control)
+ POST_OVERLAY_CHANGE(src)
+ managed_overlays = null
+ if(1)
+ add_overlay(new_overlays)
+ managed_overlays = new_overlays[1]
+ else
+ add_overlay(new_overlays)
+ managed_overlays = new_overlays
+
. |= UPDATE_OVERLAYS
. |= SEND_SIGNAL(src, COMSIG_ATOM_UPDATED_ICON, updates, .)
@@ -1222,8 +1275,15 @@
if(light_system == STATIC_LIGHT)
set_light(l_dir = var_value)
. = TRUE
+ if(NAMEOF(src, light_height))
+ if(light_system == STATIC_LIGHT)
+ set_light(l_height = var_value)
+ . = TRUE
if(NAMEOF(src, light_on))
- set_light_on(var_value)
+ if(light_system == STATIC_LIGHT)
+ set_light(l_on = var_value)
+ else
+ set_light_on(var_value)
. = TRUE
if(NAMEOF(src, light_flags))
set_light_flags(var_value)
@@ -2092,16 +2152,14 @@
* For turfs this will only be used if pathing_pass_method is TURF_PATHING_PASS_PROC
*
* Arguments:
- * * ID- An ID card representing what access we have (and thus if we can open things like airlocks or windows to pass through them). The ID card's physical location does not matter, just the reference
- * * to_dir- What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions
- * * caller- The movable we're checking pass flags for, if we're making any such checks
- * * no_id: When true, doors with public access will count as impassible
+ * * to_dir - What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions
+ * * pass_info - Datum that stores info about the thing that's trying to pass us
*
* IMPORTANT NOTE: /turf/proc/LinkBlockedWithAccess assumes that overrides of CanAStarPass will always return true if density is FALSE
* If this is NOT you, ensure you edit your can_astar_pass variable. Check __DEFINES/path.dm
**/
-/atom/proc/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
- if(caller && (caller.pass_flags & pass_flags_self))
+/atom/proc/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
+ if(pass_info.pass_flags & pass_flags_self)
return TRUE
. = !density
@@ -2162,3 +2220,71 @@
segment = -segment
SEND_SIGNAL(src, COMSIG_ATOM_SPIN_ANIMATION, speed, loops, segments, segment)
do_spin_animation(speed, loops, segments, segment, parallel)
+
+#define INVISIBILITY_VALUE 1
+#define INVISIBILITY_PRIORITY 2
+
+/atom/proc/RecalculateInvisibility()
+ PRIVATE_PROC(TRUE)
+
+ if(!invisibility_sources)
+ current_invisibility_priority = -INFINITY
+ invisibility = initial(invisibility)
+ return
+
+ var/highest_priority
+ var/list/highest_priority_invisibility_data
+ for(var/entry in invisibility_sources)
+ var/list/priority_data
+ if(islist(entry))
+ priority_data = entry
+ else
+ priority_data = invisibility_sources[entry]
+
+ var/priority = priority_data[INVISIBILITY_PRIORITY]
+ if(highest_priority > priority) // In the case of equal priorities, we use the last thing in the list so that more recent changes apply first
+ continue
+
+ highest_priority = priority
+ highest_priority_invisibility_data = priority_data
+
+ current_invisibility_priority = highest_priority
+ invisibility = highest_priority_invisibility_data[INVISIBILITY_VALUE]
+
+/**
+ * Sets invisibility according to priority.
+ * If you want to be able to undo the value you set back to what it would be otherwise,
+ * you should provide an id here and remove it using RemoveInvisibility(id)
+ */
+/atom/proc/SetInvisibility(desired_value, id, priority=0)
+ if(!invisibility_sources)
+ invisibility_sources = list()
+
+ if(id)
+ invisibility_sources[id] = list(desired_value, priority)
+ else
+ invisibility_sources += list(list(desired_value, priority))
+
+ if(current_invisibility_priority > priority)
+ return
+
+ RecalculateInvisibility()
+
+/// Removes the specified invisibility source from the tracker
+/atom/proc/RemoveInvisibility(id)
+ if(!invisibility_sources)
+ return
+
+ var/list/priority_data = invisibility_sources[id]
+ invisibility_sources -= id
+
+ if(length(invisibility_sources) == 0)
+ invisibility_sources = null
+
+ if(current_invisibility_priority > priority_data[INVISIBILITY_PRIORITY])
+ return
+
+ RecalculateInvisibility()
+
+#undef INVISIBILITY_VALUE
+#undef INVISIBILITY_PRIORITY
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index eaa6cfe13781b..7d681f02b34ee 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -18,6 +18,8 @@
var/initial_language_holder = /datum/language_holder
/// Holds all languages this mob can speak and understand
VAR_PRIVATE/datum/language_holder/language_holder
+ /// The list of factions this atom belongs to
+ var/list/faction
var/verb_say = "says"
var/verb_ask = "asks"
@@ -273,7 +275,11 @@
QDEL_NULL(em_block)
// We're gonna build a light, and mask it with the base turf's appearance
// grab a 32x32 square of it
- var/mutable_appearance/light = new(GLOB.fullbright_overlays[GET_TURF_PLANE_OFFSET(generate_for) + 1])
+ // I would like to use GLOB.starbright_overlays here
+ // But that breaks down for... some? reason. I think recieving a render relay breaks keep_together or something
+ // So we're just gonna accept that this'll break with starlight color changing. hardly matters since this is really only for offset stuff, but I'd love to fix it someday
+ var/mutable_appearance/light = new(GLOB.starlight_objects[GET_TURF_PLANE_OFFSET(generate_for) + 1])
+ light.render_target = ""
light.appearance_flags |= KEEP_TOGETHER
// Now apply a copy of the turf, set to multiply
// This will multiply against our light, so we only light up the bits that aren't "on" the wall
@@ -423,7 +429,7 @@
else
to_chat(src, span_warning("You are not Superman."))
return FALSE
- if(!(z_move_flags & ZMOVE_IGNORE_OBSTACLES) && !(start.zPassOut(src, direction, destination, (z_move_flags & ZMOVE_ALLOW_ANCHORED)) && destination.zPassIn(src, direction, start)))
+ if((!(z_move_flags & ZMOVE_IGNORE_OBSTACLES) && !(start.zPassOut(direction) && destination.zPassIn(direction))) || (!(z_move_flags & ZMOVE_ALLOW_ANCHORED) && anchored))
if(z_move_flags & ZMOVE_FEEDBACK)
to_chat(rider || src, span_warning("You couldn't move there!"))
return FALSE
@@ -1650,3 +1656,20 @@
*/
/atom/movable/proc/keybind_face_direction(direction)
setDir(direction)
+
+/**
+ * Check if the other atom/movable has any factions the same as us. Defined at the atom/movable level so it can be defined for just about anything.
+ *
+ * If exact match is set, then all our factions must match exactly
+ */
+/atom/movable/proc/faction_check_atom(atom/movable/target, exact_match)
+ if(!exact_match)
+ return faction_check(faction, target.faction, FALSE)
+
+ var/list/faction_src = LAZYCOPY(faction)
+ var/list/faction_target = LAZYCOPY(target.faction)
+ if(!("[REF(src)]" in faction_target)) //if they don't have our ref faction, remove it from our factions list.
+ faction_src -= "[REF(src)]" //if we don't do this, we'll never have an exact match.
+ if(!("[REF(target)]" in faction_src))
+ faction_target -= "[REF(target)]" //same thing here.
+ return faction_check(faction_src, faction_target, TRUE)
diff --git a/code/game/communications.dm b/code/game/communications.dm
index 743a6955d718d..0be3ccf17bc0d 100644
--- a/code/game/communications.dm
+++ b/code/game/communications.dm
@@ -130,6 +130,7 @@ GLOBAL_LIST_INIT(reverseradiochannels, list(
))
/datum/radio_frequency
+ /// The frequency of this radio frequency. Of course.
var/frequency
/// List of filters -> list of devices
var/list/list/datum/weakref/devices = list()
@@ -178,6 +179,7 @@ GLOBAL_LIST_INIT(reverseradiochannels, list(
device.receive_signal(signal)
CHECK_TICK
+/// Handles adding a listener to the radio frequency.
/datum/radio_frequency/proc/add_listener(obj/device, filter as text|null)
if (!filter)
filter = "_default"
@@ -190,6 +192,7 @@ GLOBAL_LIST_INIT(reverseradiochannels, list(
devices[filter] = devices_line = list()
devices_line += new_listener
+/// Handles removing a listener from this radio frequency.
/datum/radio_frequency/proc/remove_listener(obj/device)
for(var/devices_filter in devices)
var/list/devices_line = devices[devices_filter]
@@ -199,15 +202,27 @@ GLOBAL_LIST_INIT(reverseradiochannels, list(
if(!devices_line.len)
devices -= devices_filter
+/**
+ * Proc for reacting to a received `/datum/signal`. To be implemented as needed,
+ * does nothing by default.
+ */
/obj/proc/receive_signal(datum/signal/signal)
set waitfor = FALSE
return
/datum/signal
+ /// The source of this signal.
var/obj/source
+ /// The frequency on which this signal was emitted.
var/frequency = 0
+ /// The method through which this signal was transmitted.
+ /// See all of the `TRANSMISSION_X` in `code/__DEFINES/radio.dm` for
+ /// all of the possible options.
var/transmission_method
+ /// The data carried through this signal. Defaults to `null`, otherwise it's
+ /// an associative list of (string, any).
var/list/data
+ /// Logging data, used for logging purposes. Makes sense, right?
var/logging_data
/datum/signal/New(data, transmission_method = TRANSMISSION_RADIO, logging_data = null)
diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm
index e5f6d9cd3d276..c05d1f38666b0 100644
--- a/code/game/gamemodes/dynamic/dynamic.dm
+++ b/code/game/gamemodes/dynamic/dynamic.dm
@@ -322,7 +322,7 @@ GLOBAL_LIST_EMPTY(dynamic_forced_rulesets)
if(length(SScommunications.command_report_footnotes))
. += generate_report_footnote()
- print_command_report(., "Central Command Status Summary", announce=FALSE)
+ print_command_report(., "[command_name()] Status Summary", announce=FALSE)
if(greenshift)
priority_announce("Thanks to the tireless efforts of our security and intelligence divisions, there are currently no credible threats to [station_name()]. All station construction projects have been authorized. Have a secure shift!", "Security Report", SSstation.announcer.get_rand_report_sound())
else
@@ -426,6 +426,26 @@ GLOBAL_LIST_EMPTY(dynamic_forced_rulesets)
generate_budgets()
set_cooldowns()
log_dynamic("Dynamic Mode initialized with a Threat Level of... [threat_level]! ([round_start_budget] round start budget)")
+ SSblackbox.record_feedback(
+ "associative",
+ "dynamic_threat",
+ 1,
+ list(
+ "server_name" = CONFIG_GET(string/serversqlname),
+ "forced_threat_level" = GLOB.dynamic_forced_threat_level,
+ "threat_level" = threat_level,
+ "max_threat" = (SSticker.totalPlayersReady < low_pop_player_threshold) ? LERP(low_pop_maximum_threat, max_threat_level, SSticker.totalPlayersReady / low_pop_player_threshold) : max_threat_level,
+ "player_count" = SSticker.totalPlayersReady,
+ "round_start_budget" = round_start_budget,
+ "parameters" = list(
+ "threat_curve_centre" = threat_curve_centre,
+ "threat_curve_width" = threat_curve_width,
+ "forced_extended" = GLOB.dynamic_forced_extended,
+ "no_stacking" = GLOB.dynamic_no_stacking,
+ "stacking_limit" = GLOB.dynamic_stacking_limit,
+ ),
+ ),
+ )
return TRUE
/datum/game_mode/dynamic/proc/setup_shown_threat()
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
index 887bf04276a49..531be325dc050 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
@@ -52,7 +52,7 @@
/datum/dynamic_ruleset/latejoin/infiltrator
name = "Syndicate Infiltrator"
- antag_datum = /datum/antagonist/traitor
+ antag_datum = /datum/antagonist/traitor/infiltrator
antag_flag = ROLE_SYNDICATE_INFILTRATOR
antag_flag_override = ROLE_TRAITOR
protected_roles = list(
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
index dda198db431c9..3d3bbb6f690e3 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
@@ -61,9 +61,6 @@
if (isnull(creature.client)) // Are they connected?
trimmed_list.Remove(creature)
continue
- if (isnull(creature.mind))
- trimmed_list.Remove(creature)
- continue
if(creature.client.get_remaining_days(minimum_required_age) > 0)
trimmed_list.Remove(creature)
continue
@@ -73,6 +70,10 @@
if (is_banned_from(creature.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE)))
trimmed_list.Remove(creature)
continue
+
+ if (isnull(creature.mind))
+ continue
+
if (restrict_ghost_roles && (creature.mind.assigned_role.title in GLOB.exp_specialmap[EXP_TYPE_SPECIAL])) // Are they playing a ghost role?
trimmed_list.Remove(creature)
continue
@@ -176,7 +177,12 @@
if(makeBody)
new_character = generate_ruleset_body(applicant)
finish_setup(new_character, i)
- notify_ghosts("[applicant.name] has been picked for the ruleset [name]!", source = new_character, action = NOTIFY_ORBIT, header="Something Interesting!")
+ notify_ghosts(
+ "[applicant.name] has been picked for the ruleset [name]!",
+ source = new_character,
+ action = NOTIFY_ORBIT,
+ header = "Something Interesting!",
+ )
/datum/dynamic_ruleset/midround/from_ghosts/proc/generate_ruleset_body(mob/applicant)
var/mob/living/carbon/human/new_character = make_body(applicant)
@@ -218,7 +224,7 @@
/datum/dynamic_ruleset/midround/from_living/autotraitor
name = "Syndicate Sleeper Agent"
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
- antag_datum = /datum/antagonist/traitor
+ antag_datum = /datum/antagonist/traitor/infiltrator/sleeper_agent
antag_flag = ROLE_SLEEPER_AGENT
antag_flag_override = ROLE_TRAITOR
protected_roles = list(
@@ -256,7 +262,7 @@
var/mob/M = pick(candidates)
assigned += M
candidates -= M
- var/datum/antagonist/traitor/newTraitor = new
+ var/datum/antagonist/traitor/infiltrator/sleeper_agent/newTraitor = new
M.mind.add_antag_datum(newTraitor)
message_admins("[ADMIN_LOOKUPFLW(M)] was selected by the [name] ruleset and has been made into a midround traitor.")
log_dynamic("[key_name(M)] was selected by the [name] ruleset and has been made into a midround traitor.")
@@ -581,7 +587,7 @@
var/datum/mind/player_mind = new /datum/mind(applicant.key)
player_mind.active = TRUE
- var/mob/living/simple_animal/hostile/space_dragon/S = new (pick(spawn_locs))
+ var/mob/living/basic/space_dragon/S = new (pick(spawn_locs))
player_mind.transfer_to(S)
player_mind.add_antag_datum(/datum/antagonist/space_dragon)
diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm
index 61037fe4c774d..39dcb81920ae3 100644
--- a/code/game/gamemodes/game_mode.dm
+++ b/code/game/gamemodes/game_mode.dm
@@ -129,7 +129,7 @@
for(var/dead_dudes_job in reopened_jobs)
reopened_job_report_positions = "[reopened_job_report_positions ? "[reopened_job_report_positions]\n":""][dead_dudes_job]"
- var/suicide_command_report = "Central Command Human Resources Board \
+ var/suicide_command_report = "[command_name()] Human Resources Board \
Notice of Personnel Change\
To personnel management staff aboard [station_name()]:
\
Our medical staff have detected a series of anomalies in the vital sensors \
@@ -200,10 +200,10 @@
/datum/game_mode/proc/generate_station_goals(greenshift)
var/goal_budget = greenshift ? INFINITY : CONFIG_GET(number/station_goal_budget)
var/list/possible = subtypesof(/datum/station_goal)
+ // Remove all goals that require space if space is not present
if(SSmapping.is_planetary())
- for(var/datum/station_goal/goal in possible)
- if(goal.requires_space)
- ///Removes all goals that require space if space is not present
+ for(var/datum/station_goal/goal as anything in possible)
+ if(initial(goal.requires_space))
possible -= goal
var/goal_weights = 0
while(possible.len && goal_weights < goal_budget)
diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm
index 444eff1700677..73cb940512650 100644
--- a/code/game/machinery/_machinery.dm
+++ b/code/game/machinery/_machinery.dm
@@ -680,10 +680,25 @@
/obj/machinery/attack_paw(mob/living/user, list/modifiers)
if(!user.combat_mode)
return attack_hand(user)
+
user.changeNext_move(CLICK_CD_MELEE)
user.do_attack_animation(src, ATTACK_EFFECT_PUNCH)
- var/damage = take_damage(4, BRUTE, MELEE, 1)
- user.visible_message(span_danger("[user] smashes [src] with [user.p_their()] paws[damage ? "." : ", without leaving a mark!"]"), null, null, COMBAT_MESSAGE_RANGE)
+ var/damage = take_damage(damage_amount = 4, damage_type = BRUTE, damage_flag = MELEE, sound_effect = TRUE, attack_dir = get_dir(user, src))
+
+ var/hit_with_what_noun = "paws"
+ var/obj/item/bodypart/arm/arm = user.get_active_hand()
+ if(!isnull(arm))
+ hit_with_what_noun = arm.appendage_noun // hit with "their hand"
+ if(user.usable_hands > 1)
+ hit_with_what_noun += plural_s(hit_with_what_noun) // hit with "their hands"
+
+ user.visible_message(
+ span_danger("[user] smashes [src] with [user.p_their()] [hit_with_what_noun][damage ? "." : ", without leaving a mark!"]"),
+ span_danger("You smash [src] with your [hit_with_what_noun][damage ? "." : ", without leaving a mark!"]"),
+ span_hear("You hear a [damage ? "smash" : "thud"]."),
+ COMBAT_MESSAGE_RANGE,
+ )
+ return TRUE
/obj/machinery/attack_hulk(mob/living/carbon/user)
. = ..()
@@ -738,7 +753,7 @@
return
update_last_used(user)
-/obj/machinery/tool_act(mob/living/user, obj/item/tool, tool_type)
+/obj/machinery/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
if(SEND_SIGNAL(user, COMSIG_TRY_USE_MACHINE, src) & COMPONENT_CANT_USE_MACHINE_TOOLS)
return TOOL_ACT_MELEE_CHAIN_BLOCKING
. = ..()
@@ -1114,9 +1129,9 @@
/obj/machinery/zap_act(power, zap_flags)
if(prob(85) && (zap_flags & ZAP_MACHINE_EXPLOSIVE) && !(resistance_flags & INDESTRUCTIBLE))
- explosion(src, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 4, flame_range = 2, adminlog = FALSE, smoke = FALSE)
+ explosion(src, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 4, flame_range = 2, adminlog = TRUE, smoke = FALSE)
else if(zap_flags & ZAP_OBJ_DAMAGE)
- take_damage(power * 6.25e-7, BURN, ENERGY)
+ take_damage(power * 2.5e-4, BURN, ENERGY)
if(prob(40))
emp_act(EMP_LIGHT)
power -= power * 5e-4
diff --git a/code/game/machinery/barsigns.dm b/code/game/machinery/barsigns.dm
index b8cee2553565a..5ac2d4932911e 100644
--- a/code/game/machinery/barsigns.dm
+++ b/code/game/machinery/barsigns.dm
@@ -37,8 +37,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/barsign, 32)
update_appearance()
/obj/machinery/barsign/update_icon_state()
- if(!(machine_stat & (NOPOWER|BROKEN)) && chosen_sign && chosen_sign.icon)
- icon_state = chosen_sign.icon
+ if(!(machine_stat & (NOPOWER|BROKEN)) && chosen_sign && chosen_sign.icon_state)
+ icon_state = chosen_sign.icon_state
else
icon_state = "empty"
@@ -64,7 +64,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/barsign, 32)
return
if(chosen_sign && chosen_sign.light_mask)
- . += emissive_appearance(icon, "[chosen_sign.icon]-light-mask", src)
+ . += emissive_appearance(icon, "[chosen_sign.icon_state]-light-mask", src)
/obj/machinery/barsign/update_appearance(updates=ALL)
. = ..()
@@ -202,7 +202,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/barsign, 32)
/// User-visible name of the sign.
var/name
/// Icon state associated with this sign
- var/icon
+ var/icon_state
/// Description shown in the sign's examine text.
var/desc
/// Hidden from list of selectable options.
@@ -222,172 +222,178 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/barsign, 32)
/datum/barsign/maltesefalcon
name = "Maltese Falcon"
- icon = "maltesefalcon"
+ icon_state = "maltesefalcon"
desc = "The Maltese Falcon, Space Bar and Grill."
neon_color = "#5E8EAC"
/datum/barsign/thebark
name = "The Bark"
- icon = "thebark"
+ icon_state = "thebark"
desc = "Ian's bar of choice."
neon_color = "#f7a604"
/datum/barsign/harmbaton
name = "The Harmbaton"
- icon = "theharmbaton"
+ icon_state = "theharmbaton"
desc = "A great dining experience for both security members and assistants."
neon_color = "#ff7a4d"
/datum/barsign/thesingulo
name = "The Singulo"
- icon = "thesingulo"
+ icon_state = "thesingulo"
desc = "Where people go that'd rather not be called by their name."
neon_color = "#E600DB"
/datum/barsign/thedrunkcarp
name = "The Drunk Carp"
- icon = "thedrunkcarp"
+ icon_state = "thedrunkcarp"
desc = "Don't drink and swim."
neon_color = "#a82196"
/datum/barsign/scotchservinwill
name = "Scotch Servin Willy's"
- icon = "scotchservinwill"
+ icon_state = "scotchservinwill"
desc = "Willy sure moved up in the world from clown to bartender."
neon_color = "#fee4bf"
/datum/barsign/officerbeersky
name = "Officer Beersky's"
- icon = "officerbeersky"
+ icon_state = "officerbeersky"
desc = "Man eat a dong, these drinks are great."
neon_color = "#16C76B"
/datum/barsign/thecavern
name = "The Cavern"
- icon = "thecavern"
+ icon_state = "thecavern"
desc = "Fine drinks while listening to some fine tunes."
neon_color = "#0fe500"
/datum/barsign/theouterspess
name = "The Outer Spess"
- icon = "theouterspess"
+ icon_state = "theouterspess"
desc = "This bar isn't actually located in outer space."
neon_color = "#30f3cc"
/datum/barsign/slipperyshots
name = "Slippery Shots"
- icon = "slipperyshots"
+ icon_state = "slipperyshots"
desc = "Slippery slope to drunkeness with our shots!"
neon_color = "#70DF00"
/datum/barsign/thegreytide
name = "The Grey Tide"
- icon = "thegreytide"
+ icon_state = "thegreytide"
desc = "Abandon your toolboxing ways and enjoy a lazy beer!"
neon_color = "#00F4D6"
/datum/barsign/honkednloaded
name = "Honked 'n' Loaded"
- icon = "honkednloaded"
+ icon_state = "honkednloaded"
desc = "Honk."
neon_color = "#FF998A"
/datum/barsign/thenest
name = "The Nest"
- icon = "thenest"
+ icon_state = "thenest"
desc = "A good place to retire for a drink after a long night of crime fighting."
neon_color = "#4d6796"
/datum/barsign/thecoderbus
name = "The Coderbus"
- icon = "thecoderbus"
+ icon_state = "thecoderbus"
desc = "A very controversial bar known for its wide variety of constantly-changing drinks."
neon_color = "#ffffff"
/datum/barsign/theadminbus
name = "The Adminbus"
- icon = "theadminbus"
+ icon_state = "theadminbus"
desc = "An establishment visited mainly by space-judges. It isn't bombed nearly as much as court hearings."
neon_color = "#ffffff"
/datum/barsign/oldcockinn
name = "The Old Cock Inn"
- icon = "oldcockinn"
+ icon_state = "oldcockinn"
desc = "Something about this sign fills you with despair."
neon_color = "#a4352b"
/datum/barsign/thewretchedhive
name = "The Wretched Hive"
- icon = "thewretchedhive"
+ icon_state = "thewretchedhive"
desc = "Legally obligated to instruct you to check your drinks for acid before consumption."
neon_color = "#26b000"
/datum/barsign/robustacafe
name = "The Robusta Cafe"
- icon = "robustacafe"
+ icon_state = "robustacafe"
desc = "Holder of the 'Most Lethal Barfights' record 5 years uncontested."
neon_color = "#c45f7a"
/datum/barsign/emergencyrumparty
name = "The Emergency Rum Party"
- icon = "emergencyrumparty"
+ icon_state = "emergencyrumparty"
desc = "Recently relicensed after a long closure."
neon_color = "#f90011"
/datum/barsign/combocafe
name = "The Combo Cafe"
- icon = "combocafe"
+ icon_state = "combocafe"
desc = "Renowned system-wide for their utterly uncreative drink combinations."
neon_color = "#33ca40"
/datum/barsign/vladssaladbar
name = "Vlad's Salad Bar"
- icon = "vladssaladbar"
+ icon_state = "vladssaladbar"
desc = "Under new management. Vlad was always a bit too trigger happy with that shotgun."
neon_color = "#306900"
/datum/barsign/theshaken
name = "The Shaken"
- icon = "theshaken"
+ icon_state = "theshaken"
desc = "This establishment does not serve stirred drinks."
neon_color = "#dcd884"
/datum/barsign/thealenath
name = "The Ale' Nath"
- icon = "thealenath"
+ icon_state = "thealenath"
desc = "All right, buddy. I think you've had EI NATH. Time to get a cab."
neon_color = "#ed0000"
/datum/barsign/thealohasnackbar
name = "The Aloha Snackbar"
- icon = "alohasnackbar"
+ icon_state = "alohasnackbar"
desc = "A tasteful, inoffensive tiki bar sign."
neon_color = ""
/datum/barsign/thenet
name = "The Net"
- icon = "thenet"
+ icon_state = "thenet"
desc = "You just seem to get caught up in it for hours."
neon_color = "#0e8a00"
/datum/barsign/maidcafe
name = "Maid Cafe"
- icon = "maidcafe"
+ icon_state = "maidcafe"
desc = "Welcome back, master!"
neon_color = "#ff0051"
/datum/barsign/the_lightbulb
name = "The Lightbulb"
- icon = "the_lightbulb"
+ icon_state = "the_lightbulb"
desc = "A cafe popular among moths and moffs. Once shut down for a week after the bartender used mothballs to protect her spare uniforms."
neon_color = "#faff82"
/datum/barsign/goose
name = "The Loose Goose"
- icon = "goose"
+ icon_state = "goose"
desc = "Drink till you puke and/or break the laws of reality!"
neon_color = "#00cc33"
+/datum/barsign/maltroach
+ name = "Maltroach"
+ icon_state = "maltroach"
+ desc = "Mothroaches politely greet you into the bar, or are they greeting eachother?"
+ neon_color = "#649e8a"
+
// Hidden signs list below this point
/datum/barsign/hiddensigns
@@ -395,19 +401,19 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/barsign, 32)
/datum/barsign/hiddensigns/empbarsign
name = "EMP'd"
- icon = "empbarsign"
+ icon_state = "empbarsign"
desc = "Something has gone very wrong."
rename_area = FALSE
/datum/barsign/hiddensigns/syndibarsign
name = "Syndi Cat"
- icon = "syndibarsign"
+ icon_state = "syndibarsign"
desc = "Syndicate or die."
neon_color = "#ff0000"
/datum/barsign/hiddensigns/signoff
name = "Off"
- icon = "empty"
+ icon_state = "empty"
desc = "This sign doesn't seem to be on."
rename_area = FALSE
light_mask = FALSE
diff --git a/code/game/machinery/computer/accounting.dm b/code/game/machinery/computer/accounting.dm
index 178c407cb58dc..475bf404c1ce0 100644
--- a/code/game/machinery/computer/accounting.dm
+++ b/code/game/machinery/computer/accounting.dm
@@ -21,9 +21,10 @@
for(var/current_account as anything in SSeconomy.bank_accounts_by_id)
var/datum/bank_account/current_bank_account = SSeconomy.bank_accounts_by_id[current_account]
+ var/job_title = current_bank_account.account_job?.title
player_accounts += list(list(
"name" = current_bank_account.account_holder,
- "job" = current_bank_account.account_job.title,
+ "job" = job_title ? job_title : "No Job", // because this can be null
"balance" = round(current_bank_account.account_balance),
"modifier" = round((current_bank_account.payday_modifier * 0.9), 0.1),
))
diff --git a/code/game/machinery/computer/arcade/orion.dm b/code/game/machinery/computer/arcade/orion.dm
index 3bf880a7582e0..945cbb5593ad2 100644
--- a/code/game/machinery/computer/arcade/orion.dm
+++ b/code/game/machinery/computer/arcade/orion.dm
@@ -502,7 +502,7 @@ GLOBAL_LIST_INIT(orion_events, generate_orion_events())
obj_flags |= EMAGGED
return TRUE
-/mob/living/basic/syndicate/ranged/smg/orion
+/mob/living/basic/trooper/syndicate/ranged/smg/orion
name = "spaceport security"
desc = "Premier corporate security forces for all spaceports found along the Orion Trail."
faction = list(FACTION_ORION)
diff --git a/code/game/machinery/computer/arcade/orion_event.dm b/code/game/machinery/computer/arcade/orion_event.dm
index ea81b67532d14..97590059a526e 100644
--- a/code/game/machinery/computer/arcade/orion_event.dm
+++ b/code/game/machinery/computer/arcade/orion_event.dm
@@ -525,7 +525,7 @@
game.say("WEEWOO! WEEWOO! Spaceport security en route!")
playsound(game, 'sound/items/weeoo1.ogg', 100, FALSE)
for(var/i in 1 to 3)
- var/mob/living/basic/syndicate/ranged/smg/orion/spaceport_security = new(get_turf(game))
+ var/mob/living/basic/trooper/syndicate/ranged/smg/orion/spaceport_security = new(get_turf(game))
spaceport_security.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, usr)
game.fuel += fuel
game.food += food
diff --git a/code/game/machinery/computer/camera.dm b/code/game/machinery/computer/camera.dm
index 54e622fb12d38..6bc5c6f60d2fc 100644
--- a/code/game/machinery/computer/camera.dm
+++ b/code/game/machinery/computer/camera.dm
@@ -50,6 +50,8 @@
/obj/machinery/computer/security/ui_interact(mob/user, datum/tgui/ui)
. = ..()
+ if(!user.can_perform_action(src, NEED_DEXTERITY)) //prevents monkeys from using camera consoles
+ return
// Update UI
ui = SStgui.try_update_ui(user, src, ui)
diff --git a/code/game/machinery/computer/launchpad_control.dm b/code/game/machinery/computer/launchpad_control.dm
index 5590297c54a3c..13f54f2798091 100644
--- a/code/game/machinery/computer/launchpad_control.dm
+++ b/code/game/machinery/computer/launchpad_control.dm
@@ -79,20 +79,6 @@
return FALSE
return TRUE
-/// Performs checks on whether or not the launch pad can be used.
-/// Returns `null` if there are no errors, otherwise will return the error string.
-/obj/machinery/computer/launchpad/proc/teleport_checks(obj/machinery/launchpad/pad)
- if(QDELETED(pad))
- return "ERROR: Launchpad not responding. Check launchpad integrity."
- if(!pad.isAvailable())
- return "ERROR: Launchpad not operative. Make sure the launchpad is ready and powered."
- if(pad.teleporting)
- return "ERROR: Launchpad busy."
- var/turf/pad_turf = get_turf(pad)
- if(pad_turf && is_centcom_level(pad_turf.z))
- return "ERROR: Launchpad not operative. Heavy area shielding makes teleporting impossible."
- return null
-
/obj/machinery/computer/launchpad/proc/get_pad(number)
var/obj/machinery/launchpad/pad = launchpads[number]
return pad
@@ -168,7 +154,7 @@
selected_id = null
. = TRUE
if("launch")
- var/checks = teleport_checks(current_pad)
+ var/checks = current_pad.teleport_checks()
if(isnull(checks))
current_pad.doteleport(usr, TRUE)
else
@@ -176,7 +162,7 @@
. = TRUE
if("pull")
- var/checks = teleport_checks(current_pad)
+ var/checks = current_pad.teleport_checks()
if(isnull(checks))
current_pad.doteleport(usr, FALSE)
else
diff --git a/code/game/machinery/computer/robot.dm b/code/game/machinery/computer/robot.dm
index 00b446c2b276f..0c8b6e58d6e7e 100644
--- a/code/game/machinery/computer/robot.dm
+++ b/code/game/machinery/computer/robot.dm
@@ -67,15 +67,15 @@
data["cyborgs"] += list(cyborg_data)
data["drones"] = list()
- for(var/mob/living/simple_animal/drone/D in GLOB.drones_list)
- if(D.hacked)
+ for(var/mob/living/basic/drone/drone in GLOB.drones_list)
+ if(drone.hacked)
continue
- if(!is_valid_z_level(current_turf, get_turf(D)))
+ if(!is_valid_z_level(current_turf, get_turf(drone)))
continue
var/list/drone_data = list(
- name = D.name,
- status = D.stat,
- ref = REF(D)
+ name = drone.name,
+ status = drone.stat,
+ ref = REF(drone)
)
data["drones"] += list(drone_data)
@@ -148,7 +148,7 @@
if("killdrone")
if(allowed(usr))
- var/mob/living/simple_animal/drone/drone = locate(params["ref"]) in GLOB.mob_list
+ var/mob/living/basic/drone/drone = locate(params["ref"]) in GLOB.mob_list
if(drone.hacked)
to_chat(usr, span_danger("ERROR: [drone] is not responding to external commands."))
else
diff --git a/code/game/machinery/deployable.dm b/code/game/machinery/deployable.dm
index e4d01a6aa36bc..a4280dc0f090f 100644
--- a/code/game/machinery/deployable.dm
+++ b/code/game/machinery/deployable.dm
@@ -234,7 +234,7 @@
/obj/item/deployable_turret_folded/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/deployable, 5 SECONDS, /obj/machinery/deployable_turret/hmg, delete_on_use = TRUE)
+ AddComponent(/datum/component/deployable, 5 SECONDS, /obj/machinery/deployable_turret/hmg)
#undef SINGLE
#undef VERTICAL
diff --git a/code/game/machinery/dna_infuser/organ_sets/roach_organs.dm b/code/game/machinery/dna_infuser/organ_sets/roach_organs.dm
index 4aa96728358e4..3c2d461e08c81 100644
--- a/code/game/machinery/dna_infuser/organ_sets/roach_organs.dm
+++ b/code/game/machinery/dna_infuser/organ_sets/roach_organs.dm
@@ -56,11 +56,11 @@
greyscale_config = /datum/greyscale_config/mutant_organ
greyscale_colors = ROACH_COLORS
- /// Timer ID for resetting the damage resistance applied from attacks from behind
- var/defense_timerid
/// Bodypart overlay applied to the chest the heart is in
var/datum/bodypart_overlay/simple/roach_shell/roach_shell
+ COOLDOWN_DECLARE(harden_effect_cd)
+
/obj/item/organ/internal/heart/roach/Initialize(mapload)
. = ..()
AddElement(/datum/element/noticable_organ, "has hardened, somewhat translucent skin.")
@@ -78,7 +78,8 @@
var/mob/living/carbon/human/human_owner = organ_owner
- RegisterSignal(human_owner, COMSIG_MOB_APPLY_DAMAGE, PROC_REF(modify_damage))
+ RegisterSignal(human_owner, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS, PROC_REF(modify_damage))
+ RegisterSignal(human_owner, COMSIG_MOB_AFTER_APPLY_DAMAGE, PROC_REF(do_block_effect))
human_owner.physiology.knockdown_mod *= 3
var/obj/item/bodypart/chest/chest = human_owner.get_bodypart(BODY_ZONE_CHEST)
@@ -92,50 +93,55 @@
var/mob/living/carbon/human/human_owner = organ_owner
- UnregisterSignal(human_owner, COMSIG_MOB_APPLY_DAMAGE)
+ UnregisterSignal(human_owner, list(COMSIG_MOB_APPLY_DAMAGE_MODIFIERS, COMSIG_MOB_AFTER_APPLY_DAMAGE))
human_owner.physiology.knockdown_mod /= 3
- if(defense_timerid)
- reset_damage(human_owner)
-
var/obj/item/bodypart/chest/chest = human_owner.get_bodypart(BODY_ZONE_CHEST)
chest.remove_bodypart_overlay(roach_shell)
human_owner.update_body_parts()
/**
- * Signal proc for [COMSIG_MOB_APPLY_DAMAGE]
+ * Signal proc for [COMSIG_MOB_APPLY_DAMAGE_MODIFIERS]
*
- * Being hit with brute damage in the back will impart a large damage resistance bonus for a very short period.
+ * Adds a 0.5 modifier to attacks from the back
*/
-/obj/item/organ/internal/heart/roach/proc/modify_damage(datum/source, damage, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction, obj/item/attacking_item)
+/obj/item/organ/internal/heart/roach/proc/modify_damage(mob/living/carbon/human/source, list/damage_mods, damage_amount, damagetype, def_zone, sharpness, attack_direction, obj/item/attacking_item)
SIGNAL_HANDLER
- if(!ishuman(owner) || !attack_direction || damagetype != BRUTE || owner.stat >= UNCONSCIOUS)
+ if(!is_blocking(source, damage_amount, damagetype, attack_direction))
return
- var/mob/living/carbon/human/human_owner = owner
- // No tactical spinning
- if(human_owner.flags_1 & IS_SPINNING_1)
- return
+ damage_mods += 0.5
+
+/**
+ * Signal proc for [COMSIG_MOB_AFTER_APPLY_DAMAGE]
+ *
+ * Does a special effect if we blocked damage with our back
+ */
+/obj/item/organ/internal/heart/roach/proc/do_block_effect(mob/living/carbon/human/source, damage_dealt, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction, obj/item/attacking_item)
+ SIGNAL_HANDLER
- // If we're lying down, or were attacked from the back, we get armor.
- var/should_armor_up = (human_owner.body_position == LYING_DOWN) || (human_owner.dir & attack_direction)
- if(!should_armor_up)
+ if(!is_blocking(source, damage_dealt, damagetype, attack_direction))
return
- // Take 50% less damage from attack behind us
- if(!defense_timerid)
- human_owner.physiology.brute_mod /= 2
- human_owner.visible_message(span_warning("[human_owner]'s back hardens against the blow!"))
- playsound(human_owner, 'sound/effects/constructform.ogg', 25, vary = TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ if(COOLDOWN_FINISHED(src, harden_effect_cd))
+ source.visible_message(span_warning("[source]'s back hardens against the blow!"))
+ playsound(source, 'sound/effects/constructform.ogg', 25, vary = TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
- defense_timerid = addtimer(CALLBACK(src, PROC_REF(reset_damage), owner), 5 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE)
+ COOLDOWN_START(src, harden_effect_cd, 5 SECONDS) // Cooldown resets EVERY time we get hit
-/obj/item/organ/internal/heart/roach/proc/reset_damage(mob/living/carbon/human/human_owner)
- defense_timerid = null
- if(!QDELETED(human_owner))
- human_owner.physiology.brute_mod *= 2
- human_owner.visible_message(span_warning("[human_owner]'s back softens again."))
+/// Checks if the passed mob is in a valid state to be blocking damage with the roach shell
+/obj/item/organ/internal/heart/roach/proc/is_blocking(mob/living/carbon/human/blocker, damage_amount, damagetype, attack_direction)
+ if(damage_amount < 5 || damagetype != BRUTE || !attack_direction)
+ return
+ if(!ishuman(blocker) || blocker.stat >= UNCONSCIOUS)
+ return FALSE
+ // No tactical spinning
+ if(blocker.flags_1 & IS_SPINNING_1)
+ return FALSE
+ if(blocker.body_position == LYING_DOWN || (blocker.dir & attack_direction))
+ return TRUE
+ return FALSE
// Simple overlay so we can add a roach shell to guys with roach hearts
/datum/bodypart_overlay/simple/roach_shell
diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm
index 33ad787116c0f..e6883e09a1ee3 100644
--- a/code/game/machinery/doors/airlock.dm
+++ b/code/game/machinery/doors/airlock.dm
@@ -46,13 +46,6 @@
// Wires for the airlock are located in the datum folder, inside the wires datum folder.
-#define AIRLOCK_CLOSED 1
-#define AIRLOCK_CLOSING 2
-#define AIRLOCK_OPEN 3
-#define AIRLOCK_OPENING 4
-#define AIRLOCK_DENY 5
-#define AIRLOCK_EMAG 6
-
#define AIRLOCK_FRAME_CLOSED "closed"
#define AIRLOCK_FRAME_CLOSING "closing"
#define AIRLOCK_FRAME_OPEN "open"
@@ -1397,9 +1390,9 @@
assemblytype = initial(airlock.assemblytype)
update_appearance()
-/obj/machinery/door/airlock/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
+/obj/machinery/door/airlock/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
//Airlock is passable if it is open (!density), bot has access, and is not bolted shut or powered off)
- return !density || (check_access(ID) && !locked && hasPower() && !no_id)
+ return !density || (check_access_list(pass_info.access) && !locked && hasPower() && !pass_info.no_id)
/obj/machinery/door/airlock/emag_act(mob/user, obj/item/card/emag/emag_card)
if(!operating && density && hasPower() && !(obj_flags & EMAGGED))
@@ -2474,14 +2467,6 @@
operating = FALSE
return TRUE
-
-#undef AIRLOCK_CLOSED
-#undef AIRLOCK_CLOSING
-#undef AIRLOCK_OPEN
-#undef AIRLOCK_OPENING
-#undef AIRLOCK_DENY
-#undef AIRLOCK_EMAG
-
#undef AIRLOCK_SECURITY_NONE
#undef AIRLOCK_SECURITY_IRON
#undef AIRLOCK_SECURITY_PLASTEEL_I_S
diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm
index b99b9f4024691..83f40b0c7b965 100644
--- a/code/game/machinery/doors/door.dm
+++ b/code/game/machinery/doors/door.dm
@@ -55,7 +55,7 @@
/// Current elevator status for processing
var/elevator_status
/// What specific lift ID do we link with?
- var/elevator_linked_id
+ var/transport_linked_id
/datum/armor/machinery_door
melee = 30
@@ -78,7 +78,7 @@
air_update_turf(TRUE, TRUE)
register_context()
if(elevator_mode)
- if(elevator_linked_id)
+ if(transport_linked_id)
elevator_status = LIFT_PLATFORM_LOCKED
GLOB.elevator_doors += src
else
@@ -500,12 +500,13 @@
if(isalien(future_pancake)) //For xenos
future_pancake.adjustBruteLoss(DOOR_CRUSH_DAMAGE * 1.5) //Xenos go into crit after aproximately the same amount of crushes as humans.
future_pancake.emote("roar")
- else if(ishuman(future_pancake)) //For humans
+ else if(ismonkey(future_pancake)) //For monkeys
+ future_pancake.emote("screech")
future_pancake.adjustBruteLoss(DOOR_CRUSH_DAMAGE)
- future_pancake.emote("scream")
future_pancake.Paralyze(100)
- else if(ismonkey(future_pancake)) //For monkeys
+ else if(ishuman(future_pancake)) //For humans
future_pancake.adjustBruteLoss(DOOR_CRUSH_DAMAGE)
+ future_pancake.emote("scream")
future_pancake.Paralyze(100)
else //for simple_animals & borgs
future_pancake.adjustBruteLoss(DOOR_CRUSH_DAMAGE)
diff --git a/code/game/machinery/doors/firedoor.dm b/code/game/machinery/doors/firedoor.dm
index d7254e7f6b162..bd740b0f0e8ae 100644
--- a/code/game/machinery/doors/firedoor.dm
+++ b/code/game/machinery/doors/firedoor.dm
@@ -270,6 +270,7 @@
/obj/machinery/door/firedoor/proc/adjacent_change(turf/changed, path, list/new_baseturfs, flags, list/post_change_callbacks)
SIGNAL_HANDLER
post_change_callbacks += CALLBACK(src, PROC_REF(CalculateAffectingAreas))
+ post_change_callbacks += CALLBACK(src, PROC_REF(process_results), changed) //check the atmosphere of the changed turf so we don't hold onto alarm if a wall is built
/obj/machinery/door/firedoor/proc/check_atmos(turf/checked_turf)
var/datum/gas_mixture/environment = checked_turf.return_air()
@@ -727,7 +728,7 @@
if(!(border_dir == dir)) //Make sure looking at appropriate border
return TRUE
-/obj/machinery/door/firedoor/border_only/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
+/obj/machinery/door/firedoor/border_only/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
return !density || (dir != to_dir)
/obj/machinery/door/firedoor/border_only/proc/on_exit(datum/source, atom/movable/leaving, direction)
diff --git a/code/game/machinery/doors/windowdoor.dm b/code/game/machinery/doors/windowdoor.dm
index 30b4e04c4cc3f..c70d7a751edd2 100644
--- a/code/game/machinery/doors/windowdoor.dm
+++ b/code/game/machinery/doors/windowdoor.dm
@@ -186,8 +186,8 @@
return TRUE
//used in the AStar algorithm to determinate if the turf the door is on is passable
-/obj/machinery/door/window/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
- return !density || (dir != to_dir) || (check_access(ID) && hasPower() && !no_id)
+/obj/machinery/door/window/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
+ return !density || (dir != to_dir) || (check_access_list(pass_info.access) && hasPower() && !pass_info.no_id)
/obj/machinery/door/window/proc/on_exit(datum/source, atom/movable/leaving, direction)
SIGNAL_HANDLER
diff --git a/code/game/machinery/gigabeacon.dm b/code/game/machinery/gigabeacon.dm
index b1e3b2f7cda1c..2154abffc11f0 100644
--- a/code/game/machinery/gigabeacon.dm
+++ b/code/game/machinery/gigabeacon.dm
@@ -13,7 +13,7 @@
. = ..()
var/turf/T = loc
Beacon = new(T)
- Beacon.invisibility = INVISIBILITY_MAXIMUM
+ Beacon.SetInvisibility(INVISIBILITY_MAXIMUM)
AddElement(/datum/element/undertile, TRAIT_T_RAY_VISIBLE)
@@ -25,6 +25,6 @@
if(QDELETED(Beacon)) //Don't move it out of nullspace BACK INTO THE GAME for the love of god
var/turf/T = loc
Beacon = new(T)
- Beacon.invisibility = INVISIBILITY_MAXIMUM
+ Beacon.SetInvisibility(INVISIBILITY_MAXIMUM)
else if (Beacon.loc != loc)
Beacon.forceMove(loc)
diff --git a/code/game/machinery/incident_display.dm b/code/game/machinery/incident_display.dm
index 5ec7c50ecfd62..97557c5611c1a 100644
--- a/code/game/machinery/incident_display.dm
+++ b/code/game/machinery/incident_display.dm
@@ -84,8 +84,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/incident_display/tram, 32)
. = ..()
GLOB.map_delamination_counters += src
update_delam_count(SSpersistence.rounds_since_engine_exploded, SSpersistence.delam_highscore)
- for(var/obj/structure/industrial_lift/tram/tram as anything in GLOB.lifts)
- RegisterSignal(tram, COMSIG_TRAM_COLLISION, PROC_REF(update_tram_count))
+ RegisterSignal(SStransport, COMSIG_TRAM_COLLISION, PROC_REF(update_tram_count))
update_appearance()
@@ -190,22 +189,19 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/incident_display/tram, 32)
. = ..()
if(machine_stat & NOPOWER)
icon_state = "stat_display_blank"
- set_light(0)
+ set_light(l_on = FALSE)
return
if(machine_stat & BROKEN)
icon_state = "stat_display_broken"
- set_light(l_range = 1.7, l_power = 1.5, l_color = LIGHT_COLOR_DARK_BLUE)
- return
-
- if(sign_features == (DISPLAY_DELAM + DISPLAY_TRAM))
+ else if(sign_features == (DISPLAY_DELAM + DISPLAY_TRAM))
icon_state = "stat_display_dual"
else if(sign_features == DISPLAY_DELAM)
icon_state = "stat_display_delam"
else if(sign_features == DISPLAY_TRAM)
icon_state = "stat_display_tram"
- set_light(l_range = 1.7, l_power = 1.5, l_color = LIGHT_COLOR_FAINT_BLUE)
+ set_light(l_range = 1.7, l_power = 1.5, l_color = LIGHT_COLOR_FAINT_CYAN, l_on = TRUE)
/obj/machinery/incident_display/update_overlays()
. = ..()
@@ -220,9 +216,9 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/incident_display/tram, 32)
var/delam_display_color
. += delam_base_emissive
if(!last_delam)
- delam_display_color = LIGHT_COLOR_INTENSE_RED
+ delam_display_color = COLOR_DISPLAY_RED
else
- delam_display_color = LIGHT_COLOR_HOLY_MAGIC
+ delam_display_color = COLOR_DISPLAY_YELLOW
var/delam_pos1 = last_delam % 10
var/mutable_appearance/delam_pos1_overlay = mutable_appearance(icon, "num_[delam_pos1]")
@@ -255,7 +251,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/incident_display/tram, 32)
if(last_delam == delam_record)
var/mutable_appearance/delam_trend_overlay = mutable_appearance(icon, TREND_RISING)
var/mutable_appearance/delam_trend_emissive = emissive_appearance(icon, "[TREND_RISING]_e", src, alpha = src.alpha)
- delam_trend_overlay.color = LIGHT_COLOR_VIVID_GREEN
+ delam_trend_overlay.color = COLOR_DISPLAY_GREEN
delam_trend_overlay.pixel_w = 1
delam_trend_emissive.pixel_w = 1
delam_trend_overlay.pixel_z = 6
@@ -265,7 +261,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/incident_display/tram, 32)
else
var/mutable_appearance/delam_trend_overlay = mutable_appearance(icon, TREND_FALLING)
var/mutable_appearance/delam_trend_emissive = emissive_appearance(icon, "[TREND_FALLING]_e", src, alpha = src.alpha)
- delam_trend_overlay.color = LIGHT_COLOR_INTENSE_RED
+ delam_trend_overlay.color = COLOR_DISPLAY_RED
delam_trend_overlay.pixel_w = 1
delam_trend_emissive.pixel_w = 1
delam_trend_overlay.pixel_z = 6
@@ -275,7 +271,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/incident_display/tram, 32)
if(sign_features & DISPLAY_TRAM)
var/mutable_appearance/tram_base_emissive = emissive_appearance(icon, "tram_base_emissive", src, alpha = src.alpha)
- var/tram_display_color = LIGHT_COLOR_BABY_BLUE
+ var/tram_display_color = COLOR_DISPLAY_BLUE
var/tram_pos1 = hit_count % 10
var/mutable_appearance/tram_pos1_overlay = mutable_appearance(icon, "num_[tram_pos1]")
@@ -309,7 +305,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/incident_display/tram, 32)
if(hit_count > SSpersistence.tram_hits_last_round)
var/mutable_appearance/tram_trend_overlay = mutable_appearance(icon, TREND_RISING)
var/mutable_appearance/tram_trend_emissive = emissive_appearance(icon, "[TREND_RISING]_e", src, alpha = src.alpha)
- tram_trend_overlay.color = LIGHT_COLOR_INTENSE_RED
+ tram_trend_overlay.color = COLOR_DISPLAY_RED
tram_trend_overlay.pixel_w = 1
tram_trend_emissive.pixel_w = 1
tram_trend_overlay.pixel_z = -4
@@ -319,7 +315,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/incident_display/tram, 32)
else
var/mutable_appearance/tram_trend_overlay = mutable_appearance(icon, TREND_FALLING)
var/mutable_appearance/tram_trend_emissive = emissive_appearance(icon, "[TREND_FALLING]_e", src, alpha = src.alpha)
- tram_trend_overlay.color = LIGHT_COLOR_VIVID_GREEN
+ tram_trend_overlay.color = COLOR_DISPLAY_GREEN
tram_trend_overlay.pixel_w = 1
tram_trend_emissive.pixel_w = 1
tram_trend_overlay.pixel_z = -4
diff --git a/code/game/machinery/iv_drip.dm b/code/game/machinery/iv_drip.dm
index 7c73d2b6f6567..5b1c0e35a844a 100644
--- a/code/game/machinery/iv_drip.dm
+++ b/code/game/machinery/iv_drip.dm
@@ -20,13 +20,10 @@
icon = 'icons/obj/medical/iv_drip.dmi'
icon_state = "iv_drip"
base_icon_state = "iv_drip"
- ///icon_state for the reagent fill overlay
- var/fill_icon_state = "reagent"
- ///The thresholds used to determine the reagent fill icon
- var/list/fill_icon_thresholds = list(0,10,25,50,75,80,90)
anchored = FALSE
mouse_drag_pointer = MOUSE_ACTIVE_POINTER
use_power = NO_POWER_USE
+
///What are we sticking our needle in?
var/atom/attached
///Are we donating or injecting?
@@ -41,16 +38,8 @@
var/internal_list_reagents
///How many reagents can we hold?
var/internal_volume_maximum = 100
- ///Typecache of containers we accept
- var/static/list/drip_containers = typecacheof(list(
- /obj/item/reagent_containers/blood,
- /obj/item/reagent_containers/cup,
- /obj/item/reagent_containers/chem_pack,
- ))
// If the blood draining tab should be greyed out
var/inject_only = FALSE
- // Whether the injection maintained by the plumbing network
- var/inject_from_plumbing = FALSE
/obj/machinery/iv_drip/Initialize(mapload)
. = ..()
@@ -82,9 +71,6 @@
else if(!inject_only)
context[SCREENTIP_CONTEXT_RMB] = "Change direction"
- if(istype(src, /obj/machinery/iv_drip/plumbing))
- return CONTEXTUAL_SCREENTIP_SET
-
if(transfer_rate > MIN_IV_TRANSFER_RATE)
context[SCREENTIP_CONTEXT_ALT_LMB] = "Set flow to min"
else
@@ -92,38 +78,38 @@
return CONTEXTUAL_SCREENTIP_SET
-/obj/machinery/iv_drip/ui_data(mob/user)
- var/list/data = list()
+/obj/machinery/iv_drip/ui_static_data(mob/user)
+ . = list()
+ .["transferStep"] = IV_TRANSFER_RATE_STEP
+ .["maxTransferRate"] = MAX_IV_TRANSFER_RATE
+ .["minTransferRate"] = MIN_IV_TRANSFER_RATE
- data["hasInternalStorage"] = use_internal_storage
- data["hasContainer"] = reagent_container ? TRUE : FALSE
- data["canRemoveContainer"] = !use_internal_storage
+/obj/machinery/iv_drip/ui_data(mob/user)
+ . = list()
- data["mode"] = mode == IV_INJECTING ? TRUE : FALSE
- data["canDraw"] = inject_only || (attached && !isliving(attached)) ? FALSE : TRUE
- data["injectFromPlumbing"] = inject_from_plumbing
+ .["hasInternalStorage"] = use_internal_storage
+ .["hasContainer"] = reagent_container ? TRUE : FALSE
+ .["canRemoveContainer"] = !use_internal_storage
- data["canAdjustTransfer"] = inject_from_plumbing && mode == IV_INJECTING ? FALSE : TRUE
- data["transferRate"] = transfer_rate
- data["transferStep"] = IV_TRANSFER_RATE_STEP
- data["maxTransferRate"] = MAX_IV_TRANSFER_RATE
- data["minTransferRate"] = MIN_IV_TRANSFER_RATE
+ .["mode"] = mode == IV_INJECTING ? TRUE : FALSE
+ .["canDraw"] = inject_only || (attached && !isliving(attached)) ? FALSE : TRUE
+ .["transferRate"] = transfer_rate
- data["hasObjectAttached"] = attached ? TRUE : FALSE
+ .["hasObjectAttached"] = attached ? TRUE : FALSE
if(attached)
- data["objectName"] = attached.name
+ .["objectName"] = attached.name
var/datum/reagents/drip_reagents = get_reagents()
if(drip_reagents)
- data["containerCurrentVolume"] = round(drip_reagents.total_volume, IV_TRANSFER_RATE_STEP)
- data["containerMaxVolume"] = drip_reagents.maximum_volume
- data["containerReagentColor"] = mix_color_from_reagents(drip_reagents.reagent_list)
-
- return data
+ .["containerCurrentVolume"] = round(drip_reagents.total_volume, IV_TRANSFER_RATE_STEP)
+ .["containerMaxVolume"] = drip_reagents.maximum_volume
+ .["containerReagentColor"] = mix_color_from_reagents(drip_reagents.reagent_list)
/obj/machinery/iv_drip/ui_act(action, params)
- if(..())
- return TRUE
+ . = ..()
+ if(.)
+ return
+
switch(action)
if("changeMode")
toggle_mode()
@@ -140,18 +126,9 @@
/// Sets the transfer rate to the provided value
/obj/machinery/iv_drip/proc/set_transfer_rate(new_rate)
- if(inject_from_plumbing && mode == IV_INJECTING)
- return
transfer_rate = round(clamp(new_rate, MIN_IV_TRANSFER_RATE, MAX_IV_TRANSFER_RATE), IV_TRANSFER_RATE_STEP)
update_appearance(UPDATE_ICON)
-/// Toggles transfer rate between min and max rate
-/obj/machinery/iv_drip/proc/toggle_transfer_rate()
- if(transfer_rate > MIN_IV_TRANSFER_RATE)
- set_transfer_rate(MIN_IV_TRANSFER_RATE)
- else
- set_transfer_rate(MAX_IV_TRANSFER_RATE)
-
/obj/machinery/iv_drip/update_icon_state()
if(transfer_rate > 0 && attached)
icon_state = "[base_icon_state]_[mode ? "injecting" : "donating"]"
@@ -170,12 +147,16 @@
if(!container_reagents)
return
+ //The thresholds used to determine the reagent fill icon
+ var/static/list/fill_icon_thresholds = list(0, 10, 25, 50, 75, 80, 90)
+
var/threshold = null
for(var/i in 1 to fill_icon_thresholds.len)
if(ROUND_UP(100 * container_reagents.total_volume / container_reagents.maximum_volume) >= fill_icon_thresholds[i])
threshold = i
+
if(threshold)
- var/fill_name = "[fill_icon_state][fill_icon_thresholds[threshold]]"
+ var/fill_name = "reagent[fill_icon_thresholds[threshold]]"
var/mutable_appearance/filling = mutable_appearance(icon, fill_name)
filling.color = mix_color_from_reagents(container_reagents.reagent_list)
. += filling
@@ -204,6 +185,13 @@
if(use_internal_storage)
return ..()
+ //Typecache of containers we accept
+ var/static/list/drip_containers = typecacheof(list(
+ /obj/item/reagent_containers/blood,
+ /obj/item/reagent_containers/cup,
+ /obj/item/reagent_containers/chem_pack,
+ ))
+
if(is_type_in_typecache(W, drip_containers) || IS_EDIBLE(W))
if(reagent_container)
to_chat(user, span_warning("[reagent_container] is already loaded on [src]!"))
@@ -223,14 +211,11 @@
/obj/machinery/iv_drip/proc/can_use_alt_click(mob/user)
if(!can_interact(user))
return FALSE
- if(istype(src, /obj/machinery/iv_drip/plumbing)) // AltClick is used for rotation there
- return FALSE
- return TRUE
/obj/machinery/iv_drip/AltClick(mob/user)
if(!can_use_alt_click(user))
return ..()
- toggle_transfer_rate()
+ set_transfer_rate(transfer_rate > MIN_IV_TRANSFER_RATE ? MIN_IV_TRANSFER_RATE : MAX_IV_TRANSFER_RATE)
/obj/machinery/iv_drip/deconstruct(disassembled = TRUE)
if(!(flags_1 & NODECONSTRUCT_1))
@@ -258,7 +243,7 @@
if(!drip_reagents)
return PROCESS_KILL
- if(transfer_rate == 0)
+ if(!transfer_rate)
return
// Give reagents
@@ -364,9 +349,7 @@
if(!isliving(usr))
to_chat(usr, span_warning("You can't do that!"))
return
- if(!usr.can_perform_action(src))
- return
- if(usr.incapacitated())
+ if(!usr.can_perform_action(src) || usr.incapacitated())
return
if(inject_only)
mode = IV_INJECTING
@@ -423,29 +406,6 @@
AddElement(/datum/element/update_icon_blocker)
. = ..()
-///modified IV that can be anchored and takes plumbing in- and output
-/obj/machinery/iv_drip/plumbing
- name = "automated IV drip"
- desc = "A modified IV drip with plumbing connects. Reagents received from the connect are injected directly into their bloodstream, blood that is drawn goes to the internal storage and then into the ducting."
- icon_state = "plumb"
- base_icon_state = "plumb"
- density = TRUE
- use_internal_storage = TRUE
- inject_from_plumbing = TRUE
-
-/obj/machinery/iv_drip/plumbing/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/plumbing/iv_drip, anchored)
- AddComponent(/datum/component/simple_rotation)
-
-/obj/machinery/iv_drip/plumbing/wrench_act(mob/living/user, obj/item/tool)
- . = ..()
- default_unfasten_wrench(user, tool)
- return TOOL_ACT_TOOLTYPE_SUCCESS
-
-/obj/machinery/iv_drip/plumbing/deconstruct(disassembled = TRUE)
- qdel(src)
-
/atom/movable/screen/alert/iv_connected
name = "IV Connected"
desc = "You have an IV connected to your arm. Remember to remove it or drag the IV stand with you before moving, or else it will rip out!"
diff --git a/code/game/machinery/launch_pad.dm b/code/game/machinery/launch_pad.dm
index 5400cccf2f2e9..910f3802bfd6b 100644
--- a/code/game/machinery/launch_pad.dm
+++ b/code/game/machinery/launch_pad.dm
@@ -8,29 +8,31 @@
active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 2.5
hud_possible = list(DIAG_LAUNCHPAD_HUD)
circuit = /obj/item/circuitboard/machine/launchpad
+ /// The beam icon
var/icon_teleport = "lpad-beam"
- var/stationary = TRUE //to prevent briefcase pad deconstruction and such
+ /// To prevent briefcase pad deconstruction and such
+ var/stationary = TRUE
+ /// What to name the launchpad in the console
var/display_name = "Launchpad"
+ /// The speed of the teleportation
var/teleport_speed = 35
+ /// Max range of the launchpad
var/range = 10
- var/teleporting = FALSE //if it's in the process of teleporting
+ /// If it's in the process of teleporting
+ var/teleporting = FALSE
+ /// The power efficiency of the launchpad
var/power_efficiency = 1
+ /// Current x target
var/x_offset = 0
+ /// Current y target
var/y_offset = 0
+ /// The icon to use for the indicator
var/indicator_icon = "launchpad_target"
/// Determines if the bluespace launchpad is blatantly obvious on teleportation.
var/hidden = FALSE
/// The beam on teleportation
var/teleport_beam = "sm_arc_supercharged"
-/obj/machinery/launchpad/RefreshParts()
- . = ..()
- var/max_range_multiplier = 0
- for(var/datum/stock_part/servo/servo in component_parts)
- max_range_multiplier += servo.tier
- range = initial(range)
- range *= max_range_multiplier
-
/obj/machinery/launchpad/Initialize(mapload)
. = ..()
prepare_huds()
@@ -39,23 +41,19 @@
update_hud()
+/obj/machinery/launchpad/RefreshParts()
+ . = ..()
+ var/max_range_multiplier = 0
+ for(var/datum/stock_part/servo/servo in component_parts)
+ max_range_multiplier += servo.tier
+ range = initial(range)
+ range *= max_range_multiplier
+
/obj/machinery/launchpad/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents)
if(same_z_layer && !QDELETED(src))
update_hud()
return ..()
-/obj/machinery/launchpad/proc/update_hud()
- var/image/holder = hud_list[DIAG_LAUNCHPAD_HUD]
- var/mutable_appearance/target = mutable_appearance('icons/effects/effects.dmi', "launchpad_target", ABOVE_OPEN_TURF_LAYER, src, GAME_PLANE)
- holder.appearance = target
-
- update_indicator()
-
- if(stationary)
- AddComponent(/datum/component/usb_port, list(
- /obj/item/circuit_component/bluespace_launchpad,
- ))
-
/obj/machinery/launchpad/Destroy()
for(var/datum/atom_hud/data/diagnostic/diag_hud in GLOB.huds)
diag_hud.remove_atom_from_hud(src)
@@ -66,25 +64,24 @@
if(in_range(user, src) || isobserver(user))
. += span_notice("The status display reads: Maximum range: [range] units.")
-/obj/machinery/launchpad/attackby(obj/item/I, mob/user, params)
- if(stationary)
- if(default_deconstruction_screwdriver(user, "lpad-idle-open", "lpad-idle", I))
- update_indicator()
- return
+/obj/machinery/launchpad/attackby(obj/item/weapon, mob/user, params)
+ if(!stationary)
+ return ..()
- if(panel_open)
- if(I.tool_behaviour == TOOL_MULTITOOL)
- if(!multitool_check_buffer(user, I))
- return
- var/obj/item/multitool/M = I
- M.set_buffer(src)
- balloon_alert(user, "saved to multitool buffer")
- return 1
+ if(default_deconstruction_screwdriver(user, "lpad-idle-open", "lpad-idle", weapon))
+ update_indicator()
+ return
- if(default_deconstruction_crowbar(I))
+ if(panel_open && weapon.tool_behaviour == TOOL_MULTITOOL)
+ if(!multitool_check_buffer(user, weapon))
return
+ var/obj/item/multitool/multi = weapon
+ multi.set_buffer(src)
+ balloon_alert(user, "saved to buffer")
+ return TRUE
- return ..()
+ if(default_deconstruction_crowbar(weapon))
+ return
/obj/machinery/launchpad/attack_ghost(mob/dead/observer/ghost)
. = ..()
@@ -95,17 +92,30 @@
var/turf/target = locate(target_x, target_y, z)
ghost.forceMove(target)
-/obj/machinery/launchpad/proc/isAvailable()
- if(machine_stat & NOPOWER)
- return FALSE
- if(panel_open)
+/// Updates diagnostic huds
+/obj/machinery/launchpad/proc/update_hud()
+ var/image/holder = hud_list[DIAG_LAUNCHPAD_HUD]
+ var/mutable_appearance/target = mutable_appearance('icons/effects/effects.dmi', "launchpad_target", ABOVE_OPEN_TURF_LAYER, src, GAME_PLANE)
+ holder.appearance = target
+
+ update_indicator()
+
+ if(stationary)
+ AddComponent(/datum/component/usb_port, list(
+ /obj/item/circuit_component/bluespace_launchpad,
+ ))
+
+/// Whether this launchpad can send or receive.
+/obj/machinery/launchpad/proc/is_available()
+ if(QDELETED(src) || !is_operational || panel_open)
return FALSE
return TRUE
+/// Updates the indicator icon.
/obj/machinery/launchpad/proc/update_indicator()
var/image/holder = hud_list[DIAG_LAUNCHPAD_HUD]
var/turf/target_turf
- if(isAvailable())
+ if(is_available())
target_turf = locate(x + x_offset, y + y_offset, z)
if(target_turf)
holder.icon_state = indicator_icon
@@ -113,6 +123,7 @@
else
holder.icon_state = null
+/// Sets the offset of the launchpad.
/obj/machinery/launchpad/proc/set_offset(x, y)
if(teleporting)
return
@@ -132,15 +143,18 @@
. = ..()
animate(src, alpha = 0, flags = ANIMATION_PARALLEL, time = BEAM_FADE_TIME)
-
+/// Checks if the launchpad can teleport.
/obj/machinery/launchpad/proc/teleport_checks()
- if(!isAvailable())
+ if(!is_available())
return "ERROR: Launchpad not operative. Make sure the launchpad is ready and powered."
+
if(teleporting)
return "ERROR: Launchpad busy."
- var/turf/pad_turf = get_turf(src)
- if(pad_turf && is_centcom_level(pad_turf.z))
+
+ var/area/surrounding = get_area(src)
+ if(is_centcom_level(z) || istype(surrounding, /area/shuttle))
return "ERROR: Launchpad not operative. Heavy area shielding makes teleporting impossible."
+
return null
/// Performs the teleport.
@@ -179,7 +193,7 @@
indicator_icon = "launchpad_target"
update_indicator()
- if(QDELETED(src) || !isAvailable())
+ if(!is_available())
return
teleporting = FALSE
@@ -277,7 +291,7 @@
briefcase = null
return ..()
-/obj/machinery/launchpad/briefcase/isAvailable()
+/obj/machinery/launchpad/briefcase/is_available()
if(closed)
return FALSE
if(panel_open)
diff --git a/code/game/machinery/lightswitch.dm b/code/game/machinery/lightswitch.dm
index 54c098de9fd38..e315e6f3154e3 100644
--- a/code/game/machinery/lightswitch.dm
+++ b/code/game/machinery/lightswitch.dm
@@ -12,6 +12,8 @@
var/area/area = null
///Range of the light emitted when powered, but off
var/light_on_range = 1
+ /// Should this lightswitch automatically rename itself to match the area it's in?
+ var/autoname = TRUE
MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/light_switch, 26)
@@ -29,12 +31,22 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/light_switch, 26)
area = GLOB.areas_by_type[area]
if(!area)
area = get_area(src)
- if(!name)
+ if(autoname)
name = "light switch ([area.name])"
find_and_hang_on_wall(custom_drop_callback = CALLBACK(src, PROC_REF(deconstruct), TRUE))
-
+ register_context()
update_appearance()
+/obj/machinery/light_switch/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ if(isnull(held_item))
+ context[SCREENTIP_CONTEXT_LMB] = area.lightswitch ? "Flick off" : "Flick on"
+ return CONTEXTUAL_SCREENTIP_SET
+ if(held_item.tool_behaviour != TOOL_SCREWDRIVER)
+ context[SCREENTIP_CONTEXT_RMB] = "Deconstruct"
+ return CONTEXTUAL_SCREENTIP_SET
+ return .
+
/obj/machinery/light_switch/update_appearance(updates=ALL)
. = ..()
luminosity = (machine_stat & NOPOWER) ? 0 : 1
@@ -62,6 +74,13 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/light_switch, 26)
. = ..()
set_lights(!area.lightswitch)
+/obj/machinery/light_switch/attackby_secondary(obj/item/weapon, mob/user, params)
+ if(weapon.tool_behaviour == TOOL_SCREWDRIVER)
+ to_chat(user, "You pop \the [src] off the wall.")
+ deconstruct()
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+ return ..()
+
/obj/machinery/light_switch/proc/set_lights(status)
if(area.lightswitch == status)
return
diff --git a/code/game/machinery/modular_shield.dm b/code/game/machinery/modular_shield.dm
index 3f08760166aec..f4e15cfa87813 100644
--- a/code/game/machinery/modular_shield.dm
+++ b/code/game/machinery/modular_shield.dm
@@ -1,6 +1,6 @@
/obj/machinery/modular_shield_generator
name = "Modular Shield Generator"
- desc = "A forcefield generator, it seems more stationary than its cousins."
+ desc = "A forcefield generator, it seems more stationary than its cousins. It cant handle G-force and will require frequent reboots when built on mobile craft."
icon = 'icons/obj/machines/modular_shield_generator.dmi'
icon_state = "gen_recovering_closed"
density = TRUE
@@ -162,6 +162,10 @@
return
activate_shields()
+/obj/machinery/modular_shield_generator/onShuttleMove(turf/newT, turf/oldT, list/movement_force, move_dir, obj/docking_port/stationary/old_dock, obj/docking_port/mobile/moving_dock)
+ . = ..()
+ if(active)
+ deactivate_shields()
///generates the forcefield based on the given radius and calls calculate_regen to update the regen value accordingly
/obj/machinery/modular_shield_generator/proc/activate_shields()
@@ -438,17 +442,21 @@
/obj/machinery/modular_shield/module/wrench_act(mob/living/user, obj/item/tool)
. = ..()
- if(default_change_direction_wrench(user, tool))
- if(shield_generator)
- LAZYREMOVE(shield_generator.connected_modules, (src))
- shield_generator.calculate_boost()
- shield_generator = null
- update_icon_state()
- if(connected_node)
- LAZYREMOVE(connected_node.connected_through_us, (src))
- connected_node = null
- connected_turf = get_step(loc, dir)
- return TRUE
+ if(!default_change_direction_wrench(user, tool))
+ return FALSE
+
+ if(shield_generator)
+ LAZYREMOVE(shield_generator.connected_modules, (src))
+ shield_generator.calculate_boost()
+ shield_generator = null
+ update_icon_state()
+
+ if(connected_node)
+ LAZYREMOVE(connected_node.connected_through_us, (src))
+ connected_node = null
+
+ connected_turf = get_step(loc, dir)
+ return TRUE
/obj/machinery/modular_shield/module/crowbar_act(mob/living/user, obj/item/tool)
. = ..()
@@ -514,16 +522,26 @@
return
icon_state = "node_on_[panel_open ? "open" : "closed"]"
-/obj/machinery/modular_shield/module/node/setDir(new_dir)
- . = ..()
+
+/obj/machinery/modular_shield/module/node/wrench_act(mob/living/user, obj/item/tool)
+
+ if(!default_change_direction_wrench(user, tool))
+ return FALSE
disconnect_connected_through_us()
- if(isnull(shield_generator))
- return
- LAZYREMOVE(shield_generator.connected_modules, (src))
- shield_generator.calculate_boost()
- shield_generator = null
- update_icon_state()
+
+ if(shield_generator)
+ LAZYREMOVE(shield_generator.connected_modules, (src))
+ shield_generator.calculate_boost()
+ shield_generator = null
+ update_icon_state()
+
+ if(connected_node)
+ LAZYREMOVE(connected_node.connected_through_us, (src))
+ connected_node = null
+
+ connected_turf = get_step(loc, dir)
+ return TRUE
//after trying to connect to a machine infront of us, we will try to link anything connected to us to a generator
/obj/machinery/modular_shield/module/node/try_connect(user)
diff --git a/code/game/machinery/pipe/construction.dm b/code/game/machinery/pipe/construction.dm
index fc646a3483e24..22f66fd1de73d 100644
--- a/code/game/machinery/pipe/construction.dm
+++ b/code/game/machinery/pipe/construction.dm
@@ -104,6 +104,17 @@ Buildable meters
//Flipping handled manually due to custom handling for trinary pipes
AddComponent(/datum/component/simple_rotation, ROTATION_NO_FLIPPING)
+
+ // Only 'normal' pipes
+ if(type != /obj/item/pipe/quaternary)
+ return ..()
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/ghettojetpack, /datum/crafting_recipe/pipegun, /datum/crafting_recipe/smoothbore_disabler, /datum/crafting_recipe/improvised_pneumatic_cannon)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
return ..()
/obj/item/pipe/proc/make_from_existing(obj/machinery/atmospherics/make_from)
diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm
index 701d62223695a..11f4bb1dd5567 100644
--- a/code/game/machinery/porta_turret/portable_turret.dm
+++ b/code/game/machinery/porta_turret/portable_turret.dm
@@ -40,6 +40,8 @@ DEFINE_BITFIELD(turret_flags, list(
armor_type = /datum/armor/machinery_porta_turret
base_icon_state = "standard"
blocks_emissive = EMISSIVE_BLOCK_UNIQUE
+ // Same faction mobs will never be shot at, no matter the other settings
+ faction = list(FACTION_TURRET)
///if TRUE this will cause the turret to stop working if the stored_gun var is null in process()
var/uses_stored = TRUE
@@ -89,8 +91,6 @@ DEFINE_BITFIELD(turret_flags, list(
var/on = TRUE
/// Determines if our projectiles hit our faction
var/ignore_faction = FALSE
- /// Same faction mobs will never be shot at, no matter the other settings
- var/list/faction = list(FACTION_TURRET)
/// The spark system, used for generating... sparks?
var/datum/effect_system/spark_spread/spark_system
/// The turret will try to shoot from a turf in that direction when in a wall
@@ -326,7 +326,7 @@ DEFINE_BITFIELD(turret_flags, list(
//This code handles moving the turret around. After all, it's a portable turret!
if(!anchored && !isinspace())
set_anchored(TRUE)
- invisibility = INVISIBILITY_MAXIMUM
+ RemoveInvisibility(id=type)
update_appearance()
to_chat(user, span_notice("You secure the exterior bolts on the turret."))
if(has_cover)
@@ -336,7 +336,7 @@ DEFINE_BITFIELD(turret_flags, list(
set_anchored(FALSE)
to_chat(user, span_notice("You unsecure the exterior bolts on the turret."))
power_change()
- invisibility = 0
+ SetInvisibility(INVISIBILITY_NONE, id=type)
qdel(cover) //deletes the cover, and the turret instance itself becomes its own cover.
else if(I.GetID())
@@ -407,7 +407,7 @@ DEFINE_BITFIELD(turret_flags, list(
. = ..()
if(.)
power_change()
- invisibility = 0
+ SetInvisibility(INVISIBILITY_NONE, id=type)
spark_system.start() //creates some sparks because they look cool
qdel(cover) //deletes the cover - no need on keeping it there!
@@ -514,7 +514,7 @@ DEFINE_BITFIELD(turret_flags, list(
return
if(machine_stat & BROKEN)
return
- invisibility = 0
+ SetInvisibility(INVISIBILITY_NONE, id=type)
raising = 1
if(cover)
flick("popup", cover)
@@ -539,7 +539,7 @@ DEFINE_BITFIELD(turret_flags, list(
if(cover)
cover.icon_state = "turretCover"
raised = 0
- invisibility = 2
+ SetInvisibility(2, id=type)
update_appearance()
/obj/machinery/porta_turret/proc/assess_perp(mob/living/carbon/human/perp)
diff --git a/code/game/machinery/porta_turret/portable_turret_cover.dm b/code/game/machinery/porta_turret/portable_turret_cover.dm
index 86b1df20e7fea..082881fc2fa91 100644
--- a/code/game/machinery/porta_turret/portable_turret_cover.dm
+++ b/code/game/machinery/porta_turret/portable_turret_cover.dm
@@ -16,7 +16,7 @@
/obj/machinery/porta_turret_cover/Destroy()
if(parent_turret)
parent_turret.cover = null
- parent_turret.invisibility = 0
+ parent_turret.RemoveInvisibility(type)
parent_turret = null
return ..()
@@ -43,12 +43,12 @@
if(!parent_turret.anchored)
parent_turret.set_anchored(TRUE)
to_chat(user, span_notice("You secure the exterior bolts on the turret."))
- parent_turret.invisibility = 0
+ parent_turret.SetInvisibility(INVISIBILITY_NONE, id=type, priority=INVISIBILITY_PRIORITY_TURRET_COVER)
parent_turret.update_appearance()
else
parent_turret.set_anchored(FALSE)
to_chat(user, span_notice("You unsecure the exterior bolts on the turret."))
- parent_turret.invisibility = INVISIBILITY_MAXIMUM
+ parent_turret.SetInvisibility(INVISIBILITY_MAXIMUM, id=type, priority=INVISIBILITY_PRIORITY_TURRET_COVER)
parent_turret.update_appearance()
qdel(src)
return
diff --git a/code/game/machinery/recycler.dm b/code/game/machinery/recycler.dm
index 99e7540a35220..9e7c53392d5a9 100644
--- a/code/game/machinery/recycler.dm
+++ b/code/game/machinery/recycler.dm
@@ -129,6 +129,10 @@
return //I don't know how you called Crossed() but stop it.
if(morsel.resistance_flags & INDESTRUCTIBLE)
return
+ if(morsel.flags_1 & HOLOGRAM_1)
+ visible_message(span_notice("[morsel] fades away!"))
+ qdel(morsel)
+ return
var/list/to_eat = (issilicon(morsel) ? list(morsel) : morsel.get_all_contents()) //eating borg contents leads to many bad things
diff --git a/code/game/machinery/shieldgen.dm b/code/game/machinery/shieldgen.dm
index adb1c27ce8100..0ce5e8b3d63ac 100644
--- a/code/game/machinery/shieldgen.dm
+++ b/code/game/machinery/shieldgen.dm
@@ -130,9 +130,10 @@
/obj/structure/emergency_shield/cult/barrier/proc/Toggle()
set_density(!density)
air_update_turf(TRUE, !density)
- invisibility = initial(invisibility)
if(!density)
- invisibility = INVISIBILITY_OBSERVER
+ SetInvisibility(INVISIBILITY_OBSERVER, id=type)
+ else
+ RemoveInvisibility(type)
/obj/machinery/shieldgen
name = "anti-breach shielding projector"
diff --git a/code/game/machinery/status_display.dm b/code/game/machinery/status_display.dm
index cbfb61a4a03c6..763647248518b 100644
--- a/code/game/machinery/status_display.dm
+++ b/code/game/machinery/status_display.dm
@@ -30,9 +30,9 @@
var/message2 = ""
/// Normal text color
- var/text_color = "#09F"
+ var/text_color = COLOR_DISPLAY_BLUE
/// Color for headers, eg. "- ETA -"
- var/header_text_color = "#2CF"
+ var/header_text_color = COLOR_DISPLAY_PURPLE
/obj/item/wallframe/status_display
name = "status display frame"
@@ -151,7 +151,7 @@
)
set_light(0)
return
- set_light(1.5, 0.7, LIGHT_COLOR_BLUE) // blue light
+ set_light(1.5, 0.7, LIGHT_COLOR_FAINT_CYAN) // blue light
/obj/machinery/status_display/update_overlays(updates)
. = ..()
@@ -374,8 +374,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/status_display/evac, 32)
/obj/machinery/status_display/supply
name = "supply display"
current_mode = SD_MESSAGE
- text_color = "#F90"
- header_text_color = "#FC2"
+ text_color = COLOR_DISPLAY_ORANGE
+ header_text_color = COLOR_DISPLAY_YELLOW
/obj/machinery/status_display/supply/process()
if(machine_stat & NOPOWER)
@@ -409,8 +409,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/status_display/evac, 32)
current_mode = SD_MESSAGE
var/shuttle_id
- text_color = "#0F5"
- header_text_color = "#2FC"
+ text_color = COLOR_DISPLAY_GREEN
+ header_text_color = COLOR_DISPLAY_CYAN
/obj/machinery/status_display/shuttle/process()
if(!shuttle_id || (machine_stat & NOPOWER))
diff --git a/code/game/machinery/syndicatebomb.dm b/code/game/machinery/syndicatebomb.dm
index 516f5ec8453a0..9ff9ee7f76b31 100644
--- a/code/game/machinery/syndicatebomb.dm
+++ b/code/game/machinery/syndicatebomb.dm
@@ -251,7 +251,12 @@
if(isnull(payload) || istype(payload, /obj/machinery/syndicatebomb/training))
return
- notify_ghosts("\A [src] has been activated at [get_area(src)]!", source = src, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "Bomb Planted")
+ notify_ghosts(
+ "\A [src] has been activated at [get_area(src)]!",
+ source = src,
+ action = NOTIFY_ORBIT,
+ header = "Bomb Planted",
+ )
user.add_mob_memory(/datum/memory/bomb_planted/syndicate, antagonist = src)
log_bomber(user, "has primed a", src, "for detonation (Payload: [payload.name])")
payload.adminlog = "The [name] that [key_name(user)] had primed detonated!"
diff --git a/code/game/machinery/telecomms/broadcasting.dm b/code/game/machinery/telecomms/broadcasting.dm
index 2ba6856ce1f67..97ecd19e470ed 100644
--- a/code/game/machinery/telecomms/broadcasting.dm
+++ b/code/game/machinery/telecomms/broadcasting.dm
@@ -1,67 +1,26 @@
-/*
-
- Here is the big, bad function that broadcasts a message given the appropriate
- parameters.
-
- @param M:
- Reference to the mob/speaker, stored in signal.data["mob"]
-
- @param vmask:
- Boolean value if the mob is "hiding" its identity via voice mask, stored in
- signal.data["vmask"]
-
- @param vmessage:
- If specified, will display this as the message; such as "chimpering"
- for monkeys if the mob is not understood. Stored in signal.data["vmessage"].
-
- @param radio:
- Reference to the radio broadcasting the message, stored in signal.data["radio"]
-
- @param message:
- The actual string message to display to mobs who understood mob M. Stored in
- signal.data["message"]
-
- @param name:
- The name to display when a mob receives the message. signal.data["name"]
-
- @param job:
- The name job to display for the AI when it receives the message. signal.data["job"]
-
- @param realname:
- The "real" name associated with the mob. signal.data["realname"]
-
- @param vname:
- If specified, will use this name when mob M is not understood. signal.data["vname"]
-
- @param data:
- If specified:
- 1 -- Will only broadcast to intercoms
- 2 -- Will only broadcast to intercoms and station-bounced radios
- 3 -- Broadcast to syndicate frequency
- 4 -- AI can't track down this person. Useful for imitation broadcasts where you can't find the actual mob
-
- @param compression:
- If 0, the signal is audible
- If nonzero, the signal may be partially inaudible or just complete gibberish.
-
- @param level:
- The list of Z levels that the sending radio is broadcasting to. Having 0 in the list broadcasts on all levels
-
- @param freq
- The frequency of the signal
-
-**/
-
// Subtype of /datum/signal with additional processing information.
/datum/signal/subspace
transmission_method = TRANSMISSION_SUBSPACE
+ /// The type of server this signal is meant to be relayed to.
+ /// Not exclusive, the bus will usually try to send it through
+ /// more signals, but for that look for
+ /// `/obj/machinery/telecomms/bus/receive_information()`
var/server_type = /obj/machinery/telecomms/server
+ /// The signal that was the origin of this one, in case it was a copy.
var/datum/signal/subspace/original
+ /// The levels on which this signal can be received. Generally set by
+ /// a broadcaster, a relay or a message server.
+ /// If this list contains `0`, then it will be receivable on every single
+ /// z-level.
var/list/levels
/datum/signal/subspace/New(data)
src.data = data || list()
+/**
+ * Handles creating a new subspace signal that's a hard copy of this one, linked
+ * to this current signal via the `original` value, so that it can be traced back.
+ */
/datum/signal/subspace/proc/copy()
var/datum/signal/subspace/copy = new
copy.original = src
@@ -73,18 +32,26 @@
copy.data = data.Copy()
return copy
+/**
+ * Handles marking the current signal, as well as its original signal,
+ * and their original signals (recursively) as done, in their `data["done"]`.
+ */
/datum/signal/subspace/proc/mark_done()
var/datum/signal/subspace/current = src
while (current)
current.data["done"] = TRUE
current = current.original
+/**
+ * Handles sending this signal to every available receiver and mainframe.
+ */
/datum/signal/subspace/proc/send_to_receivers()
- for(var/obj/machinery/telecomms/receiver/R in GLOB.telecomms_list)
- R.receive_signal(src)
- for(var/obj/machinery/telecomms/allinone/R in GLOB.telecomms_list)
- R.receive_signal(src)
+ for(var/obj/machinery/telecomms/receiver/receiver in GLOB.telecomms_list)
+ receiver.receive_signal(src)
+ for(var/obj/machinery/telecomms/allinone/all_in_one_receiver in GLOB.telecomms_list)
+ all_in_one_receiver.receive_signal(src)
+/// Handles broadcasting this signal out, to be implemented by subtypes.
/datum/signal/subspace/proc/broadcast()
set waitfor = FALSE
@@ -92,9 +59,14 @@
// Despite "subspace" in the name, these transmissions can also be RADIO
// (intercoms and SBRs) or SUPERSPACE (CentCom).
/datum/signal/subspace/vocal
+ /// The virtualspeaker associated with this vocal transmission.
var/atom/movable/virtualspeaker/virt
+ /// The language this vocal transmission was sent in.
var/datum/language/language
+#define COMPRESSION_VOCAL_SIGNAL_MIN 35
+#define COMPRESSION_VOCAL_SIGNAL_MAX 65
+
/datum/signal/subspace/vocal/New(
obj/source, // the originating radio
frequency, // the frequency the signal is taking place on
@@ -113,13 +85,16 @@
"name" = speaker.name,
"job" = speaker.job,
"message" = message,
- "compression" = rand(35, 65),
+ "compression" = rand(COMPRESSION_VOCAL_SIGNAL_MIN, COMPRESSION_VOCAL_SIGNAL_MAX),
"language" = lang_instance.name,
"spans" = spans,
"mods" = message_mods
)
levels = SSmapping.get_connected_levels(get_turf(source))
+#undef COMPRESSION_VOCAL_SIGNAL_MIN
+#undef COMPRESSION_VOCAL_SIGNAL_MAX
+
/datum/signal/subspace/vocal/copy()
var/datum/signal/subspace/vocal/copy = new(source, frequency, virt, language)
copy.original = src
@@ -127,6 +102,10 @@
copy.levels = levels
return copy
+/// Past this amount of compression, the resulting gibberish will actually
+/// replace characters, making it even harder to understand.
+#define COMPRESSION_REPLACE_CHARACTER_THRESHOLD 30
+
/// This is the meat function for making radios hear vocal transmissions.
/datum/signal/subspace/vocal/broadcast()
set waitfor = FALSE
@@ -137,7 +116,7 @@
return
var/compression = data["compression"]
if(compression > 0)
- message = Gibberish(message, compression >= 30)
+ message = Gibberish(message, compression >= COMPRESSION_REPLACE_CHARACTER_THRESHOLD)
var/list/signal_reaches_every_z_level = levels
@@ -206,8 +185,8 @@
var/spans_part = ""
if(length(spans))
spans_part = "(spans:"
- for(var/S in spans)
- spans_part = "[spans_part] [S]"
+ for(var/span in spans)
+ spans_part = "[spans_part] [span]"
spans_part = "[spans_part] ) "
var/lang_name = data["language"]
@@ -220,4 +199,6 @@
else
log_telecomms("[virt.source] [log_text] [loc_name(get_turf(virt.source))]")
- QDEL_IN(virt, 50) // Make extra sure the virtualspeaker gets qdeleted
+ QDEL_IN(virt, 5 SECONDS) // Make extra sure the virtualspeaker gets qdeleted
+
+#undef COMPRESSION_REPLACE_CHARACTER_THRESHOLD
diff --git a/code/game/machinery/telecomms/computers/logbrowser.dm b/code/game/machinery/telecomms/computers/logbrowser.dm
index b258a8d6f6ea8..e202a508ecf0c 100644
--- a/code/game/machinery/telecomms/computers/logbrowser.dm
+++ b/code/game/machinery/telecomms/computers/logbrowser.dm
@@ -41,7 +41,7 @@
// Send selected server data
var/list/server_out = list()
server_out["name"] = SelectedServer.name
- server_out["traffic"] = SelectedServer.totaltraffic
+ server_out["traffic"] = SelectedServer.total_traffic
// Get the messages on this server
var/list/packets = list()
for(var/datum/comm_log_entry/packet in SelectedServer.log_entries)
diff --git a/code/game/machinery/telecomms/computers/message.dm b/code/game/machinery/telecomms/computers/message.dm
index e2a669804a1fb..ab2a3f8cefc86 100644
--- a/code/game/machinery/telecomms/computers/message.dm
+++ b/code/game/machinery/telecomms/computers/message.dm
@@ -20,13 +20,16 @@
var/obj/machinery/telecomms/message_server/linkedServer = null
/// Sparks effect - For emag
var/datum/effect_system/spark_spread/spark_system
- /// Computer properties
- var/screen = MSG_MON_SCREEN_MAIN // 0 = Main menu, 1 = Message Logs, 2 = Hacked screen, 3 = Custom Message
- var/message = "System bootup complete. Please select an option." // The message that shows on the main menu.
- var/auth = FALSE // Are they authenticated?
- /// Error, Success & Notice messages
+ /// Computer properties.
+ /// 0 = Main menu, 1 = Message Logs, 2 = Hacked screen, 3 = Custom Message
+ var/screen = MSG_MON_SCREEN_MAIN
+ /// The message that shows on the main menu.
+ var/message = "System bootup complete. Please select an option."
+ /// Error message to display in the interface.
var/error_message = ""
+ /// Notice message to display in the interface.
var/notice_message = ""
+ /// Success message to display in the interface.
var/success_message = ""
/// Decrypt password
var/password = ""
@@ -89,7 +92,7 @@
"error_message" = error_message,
"notice_message" = notice_message,
"success_message" = success_message,
- "auth" = auth,
+ "auth" = authenticated,
"server_status" = !LINKED_SERVER_NONRESPONSIVE,
)
@@ -109,7 +112,7 @@
if(MSG_MON_SCREEN_REQUEST_LOGS)
var/list/request_list = list()
for(var/datum/data_rc_msg/rc in linkedServer.rc_msgs)
- request_list += list(list("ref" = REF(rc), "message" = rc.message, "stamp" = rc.stamp, "id_auth" = rc.id_auth, "departament" = rc.send_dpt))
+ request_list += list(list("ref" = REF(rc), "message" = rc.message, "stamp" = rc.stamp, "id_auth" = rc.id_auth, "departament" = rc.sender_department))
data["requests"] = request_list
return data
@@ -126,15 +129,15 @@
if("auth")
var/authPass = params["auth_password"]
- if(auth)
- auth = FALSE
+ if(authenticated)
+ authenticated = FALSE
return TRUE
if(linkedServer.decryptkey != authPass)
error_message = "ALERT: Incorrect decryption key!"
return TRUE
- auth = TRUE
+ authenticated = TRUE
success_message = "YOU SUCCESFULLY LOGGED IN!"
return TRUE
@@ -246,14 +249,14 @@
linkedServer.receive_information(signal, null)
usr.log_message("(Tablet: [name] | [usr.real_name]) sent \"[message]\" to [signal.format_target()]", LOG_PDA)
return TRUE
- // Malfunction AI and cyborgs can hack console. This will auth console, but you need to wait password selection
+ // Malfunction AI and cyborgs can hack console. This will authenticate the console, but you need to wait password selection
if("hack")
var/time = 10 SECONDS * length(linkedServer.decryptkey)
addtimer(CALLBACK(src, PROC_REF(unemag_console)), time)
screen = MSG_MON_SCREEN_HACKED
error_message = "%$&(£: Critical %$$@ Error // !RestArting! - ?pLeaSe wAit!"
linkedServer.toggled = FALSE
- auth = TRUE
+ authenticated = TRUE
return TRUE
return TRUE
@@ -284,6 +287,9 @@
else
return INITIALIZE_HINT_LATELOAD
+/**
+ * Handles printing the monitor key for a given server onto this piece of paper.
+ */
/obj/item/paper/monitorkey/proc/print(obj/machinery/telecomms/message_server/server)
add_raw_text("
Daily Key Reset
The new message monitor key is [server.decryptkey]. Please keep this a secret and away from the clown. If necessary, change the password to a more secure one.")
add_overlay("paper_words")
diff --git a/code/game/machinery/telecomms/machine_interactions.dm b/code/game/machinery/telecomms/machine_interactions.dm
index f3d5f8d835294..3fb8627169966 100644
--- a/code/game/machinery/telecomms/machine_interactions.dm
+++ b/code/game/machinery/telecomms/machine_interactions.dm
@@ -1,15 +1,11 @@
-
-/*
-
- All telecommunications interactions:
-
-*/
+// This file is separate from telecommunications.dm to isolate the implementation
+// of basic interactions with the machines.
/obj/machinery/telecomms
- var/temp = "" // output message
+ /// The current temporary frequency used to add new filtered frequencies
+ /// options.
var/tempfreq = FREQ_COMMON
- var/mob/living/operator
- ///Illegal frequencies that can't be listened to by telecommunication servers.
+ /// Illegal frequencies that can't be listened to by telecommunication servers.
var/list/banned_frequencies = list(
FREQ_SYNDICATE,
FREQ_CENTCOM,
@@ -19,7 +15,7 @@
FREQ_CTF_BLUE,
)
-/obj/machinery/telecomms/attackby(obj/item/P, mob/user, params)
+/obj/machinery/telecomms/attackby(obj/item/attacking_item, mob/user, params)
var/icon_closed = initial(icon_state)
var/icon_open = "[initial(icon_state)]_o"
@@ -27,19 +23,18 @@
icon_closed = "[initial(icon_state)]_off"
icon_open = "[initial(icon_state)]_o_off"
- if(default_deconstruction_screwdriver(user, icon_open, icon_closed, P))
+ if(default_deconstruction_screwdriver(user, icon_open, icon_closed, attacking_item))
return
// Using a multitool lets you access the receiver's interface
- else if(P.tool_behaviour == TOOL_MULTITOOL)
+ else if(attacking_item.tool_behaviour == TOOL_MULTITOOL)
attack_hand(user)
- else if(default_deconstruction_crowbar(P))
+ else if(default_deconstruction_crowbar(attacking_item))
return
else
return ..()
/obj/machinery/telecomms/ui_interact(mob/user, datum/tgui/ui)
- operator = user
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Telecomms")
@@ -92,66 +87,67 @@
if(.)
return
- if(!issilicon(usr))
- if(!istype(usr.get_active_held_item(), /obj/item/multitool))
+ var/mob/living/current_user = usr
+ if(!issilicon(current_user))
+ if(!istype(current_user.get_active_held_item(), /obj/item/multitool))
return
- var/obj/item/multitool/heldmultitool = get_multitool(operator)
+ var/obj/item/multitool/heldmultitool = get_multitool(current_user)
switch(action)
if("toggle")
toggled = !toggled
update_power()
update_appearance()
- operator.log_message("toggled [toggled ? "On" : "Off"] [src].", LOG_GAME)
+ current_user.log_message("toggled [toggled ? "On" : "Off"] [src].", LOG_GAME)
. = TRUE
if("id")
if(params["value"])
if(length(params["value"]) > 32)
- to_chat(operator, span_warning("Error: Machine ID too long!"))
+ to_chat(current_user, span_warning("Error: Machine ID too long!"))
playsound(src, 'sound/machines/buzz-sigh.ogg', 50, TRUE)
return
else
id = params["value"]
- operator.log_message("has changed the ID for [src] to [id].", LOG_GAME)
+ current_user.log_message("has changed the ID for [src] to [id].", LOG_GAME)
. = TRUE
if("network")
if(params["value"])
if(length(params["value"]) > 15)
- to_chat(operator, span_warning("Error: Network name too long!"))
+ to_chat(current_user, span_warning("Error: Network name too long!"))
playsound(src, 'sound/machines/buzz-sigh.ogg', 50, TRUE)
return
else
- for(var/obj/machinery/telecomms/T in links)
- remove_link(T)
+ for(var/obj/machinery/telecomms/linked_machine in links)
+ remove_link(linked_machine)
network = params["value"]
links = list()
- operator.log_message("has changed the network for [src] to [network].", LOG_GAME)
+ current_user.log_message("has changed the network for [src] to [network].", LOG_GAME)
. = TRUE
if("tempfreq")
if(params["value"])
tempfreq = text2num(params["value"]) * 10
if("freq")
if(tempfreq in banned_frequencies)
- to_chat(operator, span_warning("Error: Interference preventing filtering frequency: \"[tempfreq / 10] kHz\""))
+ to_chat(current_user, span_warning("Error: Interference preventing filtering frequency: \"[tempfreq / 10] kHz\""))
playsound(src, 'sound/machines/buzz-sigh.ogg', 50, TRUE)
else
if(!(tempfreq in freq_listening))
freq_listening.Add(tempfreq)
- operator.log_message("added frequency [tempfreq] for [src].", LOG_GAME)
+ current_user.log_message("added frequency [tempfreq] for [src].", LOG_GAME)
. = TRUE
if("delete")
freq_listening.Remove(params["value"])
- operator.log_message("removed frequency [params["value"]] for [src].", LOG_GAME)
+ current_user.log_message("removed frequency [params["value"]] for [src].", LOG_GAME)
. = TRUE
if("unlink")
- var/obj/machinery/telecomms/T = links[text2num(params["value"])]
- if(T)
- . = remove_link(T, operator)
+ var/obj/machinery/telecomms/machine_to_unlink = links[text2num(params["value"])]
+ if(machine_to_unlink)
+ . = remove_link(machine_to_unlink, current_user)
if("link")
if(heldmultitool)
- var/obj/machinery/telecomms/T = heldmultitool.buffer
- . = add_new_link(T, operator)
+ var/obj/machinery/telecomms/machine_to_link = heldmultitool.buffer
+ . = add_new_link(machine_to_link, current_user)
if("buffer")
heldmultitool.set_buffer(src)
. = TRUE
@@ -162,7 +158,7 @@
add_act(action, params)
. = TRUE
-///adds new_connection to src's links list AND vice versa. also updates links_by_telecomms_type
+/// Adds new_connection to src's links list AND vice versa. Also updates `links_by_telecomms_type`.
/obj/machinery/telecomms/proc/add_new_link(obj/machinery/telecomms/new_connection, mob/user)
if(!istype(new_connection) || new_connection == src)
return FALSE
@@ -180,7 +176,7 @@
user.log_message("linked [src] for [new_connection].", LOG_GAME)
return TRUE
-///removes old_connection from src's links list AND vice versa. also updates links_by_telecomms_type
+/// Removes old_connection from src's links list AND vice versa. Also updates `links_by_telecomms_type`.
/obj/machinery/telecomms/proc/remove_link(obj/machinery/telecomms/old_connection, mob/user)
if(!istype(old_connection) || old_connection == src)
return FALSE
@@ -198,6 +194,11 @@
return TRUE
+/**
+ * Wrapper for adding additional options to a machine's interface.
+ *
+ * Returns a list, or `null` if it wasn't implemented by the machine.
+ */
/obj/machinery/telecomms/proc/add_option()
return
@@ -214,7 +215,14 @@
data["receiving"] = receiving
return data
+/**
+ * Wrapper for adding another time of action for `ui_act()`, rather than
+ * having you override `ui_act` yourself.
+ *
+ * Returns `TRUE` if the action was handled, nothing if not.
+ */
/obj/machinery/telecomms/proc/add_act(action, params)
+ return
/obj/machinery/telecomms/relay/add_act(action, params)
switch(action)
@@ -236,20 +244,19 @@
else
change_frequency = 0
-// Returns a multitool from a user depending on their mobtype.
-
+/// Returns a multitool from a user depending on their mobtype.
/obj/machinery/telecomms/proc/get_multitool(mob/user)
- var/obj/item/multitool/P = null
+ var/obj/item/multitool/multitool = null
// Let's double check
if(!issilicon(user) && istype(user.get_active_held_item(), /obj/item/multitool))
- P = user.get_active_held_item()
+ multitool = user.get_active_held_item()
else if(isAI(user))
var/mob/living/silicon/ai/U = user
- P = U.aiMulti
+ multitool = U.aiMulti
else if(iscyborg(user) && in_range(user, src))
if(istype(user.get_active_held_item(), /obj/item/multitool))
- P = user.get_active_held_item()
- return P
+ multitool = user.get_active_held_item()
+ return multitool
/obj/machinery/telecomms/proc/canAccess(mob/user)
if(issilicon(user) || in_range(user, src))
diff --git a/code/game/machinery/telecomms/machines/allinone.dm b/code/game/machinery/telecomms/machines/allinone.dm
index 8296e0d0bd95a..22612118033a1 100644
--- a/code/game/machinery/telecomms/machines/allinone.dm
+++ b/code/game/machinery/telecomms/machines/allinone.dm
@@ -1,8 +1,7 @@
-/*
- Basically just an empty shell for receiving and broadcasting radio messages. Not
- very flexible, but it gets the job done.
-*/
-
+/**
+ * Basically just an empty shell for receiving and broadcasting radio messages. Not
+ * very flexible, but it gets the job done.
+ */
/obj/machinery/telecomms/allinone
name = "telecommunications mainframe"
icon_state = "comm_server"
@@ -41,6 +40,6 @@
sleep(signal.data["slow"]) // simulate the network lag if necessary
signal.broadcast()
-/obj/machinery/telecomms/allinone/attackby(obj/item/P, mob/user, params)
- if(P.tool_behaviour == TOOL_MULTITOOL)
+/obj/machinery/telecomms/allinone/attackby(obj/item/attacking_item, mob/user, params)
+ if(attacking_item.tool_behaviour == TOOL_MULTITOOL)
return attack_hand(user)
diff --git a/code/game/machinery/telecomms/machines/broadcaster.dm b/code/game/machinery/telecomms/machines/broadcaster.dm
index 56b53437e633a..a3b4caecf392c 100644
--- a/code/game/machinery/telecomms/machines/broadcaster.dm
+++ b/code/game/machinery/telecomms/machines/broadcaster.dm
@@ -1,13 +1,14 @@
-/*
- The broadcaster sends processed messages to all radio devices in the game. They
- do not have to be headsets; intercoms and station-bounced radios suffice.
-
- They receive their message from a server after the message has been logged.
-*/
-
-GLOBAL_LIST_EMPTY(recentmessages) // global list of recent messages broadcasted : used to circumvent massive radio spam
-GLOBAL_VAR_INIT(message_delay, 0) // To make sure restarting the recentmessages list is kept in sync
-
+/// Global list of recent messages broadcasted : used to circumvent massive radio spam
+GLOBAL_LIST_EMPTY(recent_messages)
+/// Used to make sure restarting the recent_messages list is kept in sync.
+GLOBAL_VAR_INIT(message_delay, FALSE)
+
+/**
+ * The broadcaster sends processed messages to all radio devices in the game. They
+ * do not have to be headsets; intercoms and station-bounced radios suffice.
+ *
+ * They receive their message from a server after the message has been logged.
+ */
/obj/machinery/telecomms/broadcaster
name = "subspace broadcaster"
icon_state = "broadcaster"
@@ -18,15 +19,17 @@ GLOBAL_VAR_INIT(message_delay, 0) // To make sure restarting the recentmessages
circuit = /obj/item/circuitboard/machine/telecomms/broadcaster
/obj/machinery/telecomms/broadcaster/receive_information(datum/signal/subspace/signal, obj/machinery/telecomms/machine_from)
- // Don't broadcast rejected signals
if(!istype(signal))
return
+
+ // Don't broadcast rejected signals
if(signal.data["reject"])
return
if(!signal.data["message"])
return
+
var/signal_message = "[signal.frequency]:[signal.data["message"]]:[signal.data["name"]]"
- if(signal_message in GLOB.recentmessages)
+ if(signal_message in GLOB.recent_messages)
return
// Prevents massive radio spam
@@ -35,11 +38,11 @@ GLOBAL_VAR_INIT(message_delay, 0) // To make sure restarting the recentmessages
if(original && ("compression" in signal.data))
original.data["compression"] = signal.data["compression"]
- var/turf/T = get_turf(src)
- if (T)
- signal.levels |= SSmapping.get_connected_levels(T)
+ var/turf/current_turf = get_turf(src)
+ if (current_turf)
+ signal.levels |= SSmapping.get_connected_levels(current_turf)
- GLOB.recentmessages.Add(signal_message)
+ GLOB.recent_messages.Add(signal_message)
if(signal.data["slow"] > 0)
sleep(signal.data["slow"]) // simulate the network lag if necessary
@@ -55,29 +58,31 @@ GLOBAL_VAR_INIT(message_delay, 0) // To make sure restarting the recentmessages
use_power(idle_power_usage)
+/**
+ * Simply resets the message delay and the recent messages list, to ensure that
+ * recent messages can be sent again. Is called on a one second timer after a
+ * delay is set, from `/obj/machinery/telecomms/broadcaster/receive_information()`
+ */
/proc/end_message_delay()
GLOB.message_delay = FALSE
- GLOB.recentmessages = list()
+ GLOB.recent_messages = list()
/obj/machinery/telecomms/broadcaster/Destroy()
// In case message_delay is left on 1, otherwise it won't reset the list and people can't say the same thing twice anymore.
if(GLOB.message_delay)
- GLOB.message_delay = 0
+ GLOB.message_delay = FALSE
return ..()
-
-//Preset Broadcasters
+// Preset Broadcasters
//--PRESET LEFT--//
-
/obj/machinery/telecomms/broadcaster/preset_left
id = "Broadcaster A"
network = "tcommsat"
autolinkers = list("broadcasterA")
//--PRESET RIGHT--//
-
/obj/machinery/telecomms/broadcaster/preset_right
id = "Broadcaster B"
network = "tcommsat"
diff --git a/code/game/machinery/telecomms/machines/bus.dm b/code/game/machinery/telecomms/machines/bus.dm
index 64a13ccbd716d..8ac8297b612b3 100644
--- a/code/game/machinery/telecomms/machines/bus.dm
+++ b/code/game/machinery/telecomms/machines/bus.dm
@@ -1,13 +1,13 @@
-/*
- The bus mainframe idles and waits for hubs to relay them signals. They act
- as junctions for the network.
-
- They transfer uncompressed subspace packets to processor units, and then take
- the processed packet to a server for logging.
-
- Link to a subspace hub if it can't send to a server.
-*/
-
+/**
+ * The bus mainframe idles and waits for hubs to relay them signals. They act
+ * as junctions for the network.
+ *
+ * They transfer uncompressed subspace packets to processor units, and then take
+ * the processed packet to a server for logging.
+ *
+ * Can be linked to a telecommunications hub or a broadcaster in the absence
+ * of a server, at the cost of some added latency.
+ */
/obj/machinery/telecomms/bus
name = "bus mainframe"
icon_state = "bus"
@@ -17,7 +17,9 @@
idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 0.01
netspeed = 40
circuit = /obj/item/circuitboard/machine/telecomms/bus
- var/change_frequency = 0
+ /// The frequency this bus will use to override the received signal's frequency,
+ /// if not `NONE`.
+ var/change_frequency = NONE
/obj/machinery/telecomms/bus/receive_information(datum/signal/subspace/signal, obj/machinery/telecomms/machine_from)
if(!istype(signal) || !is_freq_listening(signal))
@@ -47,7 +49,7 @@
use_power(idle_power_usage)
-//Preset Buses
+// Preset Buses
/obj/machinery/telecomms/bus/preset_one
id = "Bus 1"
@@ -75,6 +77,8 @@
/obj/machinery/telecomms/bus/preset_four/Initialize(mapload)
. = ..()
+ // We want to include every freely-available frequency on this one, so they
+ // get processed quickly when used on-station.
for(var/i = MIN_FREQ, i <= MAX_FREQ, i += 2)
freq_listening |= i
diff --git a/code/game/machinery/telecomms/machines/hub.dm b/code/game/machinery/telecomms/machines/hub.dm
index e84f1571736e0..094180a6e70d7 100644
--- a/code/game/machinery/telecomms/machines/hub.dm
+++ b/code/game/machinery/telecomms/machines/hub.dm
@@ -1,13 +1,12 @@
-/*
- The HUB idles until it receives information. It then passes on that information
- depending on where it came from.
-
- This is the heart of the Telecommunications Network, sending information where it
- is needed. It mainly receives information from long-distance Relays and then sends
- that information to be processed. Afterwards it gets the uncompressed information
- from Servers/Buses and sends that back to the relay, to then be broadcasted.
-*/
-
+/**
+ * The HUB idles until it receives information. It then passes on that information
+ * depending on where it came from.
+ *
+ * This is the heart of the Telecommunications Network, sending information where it
+ * is needed. It mainly receives information from long-distance Relays and then sends
+ * that information to be processed. Afterwards it gets the uncompressed information
+ * from Servers/Buses and sends that back to the relay, to then be broadcasted.
+ */
/obj/machinery/telecomms/hub
name = "telecommunication hub"
icon_state = "hub"
@@ -53,12 +52,31 @@
QDEL_NULL(soundloop)
return ..()
-//Preset HUB
+// Preset HUB
/obj/machinery/telecomms/hub/preset
id = "Hub"
network = "tcommsat"
- autolinkers = list("hub", "relay", "s_relay", "m_relay", "r_relay", "h_relay", "science", "medical",
- "supply", "service", "common", "command", "engineering", "security",
- "receiverA", "receiverB", "broadcasterA", "broadcasterB", "autorelay", "messaging")
+ autolinkers = list(
+ "hub",
+ "relay",
+ "s_relay",
+ "m_relay",
+ "r_relay",
+ "h_relay",
+ "science",
+ "medical",
+ "supply",
+ "service",
+ "common",
+ "command",
+ "engineering",
+ "security",
+ "receiverA",
+ "receiverB",
+ "broadcasterA",
+ "broadcasterB",
+ "autorelay",
+ "messaging",
+ )
diff --git a/code/game/machinery/telecomms/machines/message_server.dm b/code/game/machinery/telecomms/machines/message_server.dm
index 02a146fac314a..6faad6f5eabc7 100644
--- a/code/game/machinery/telecomms/machines/message_server.dm
+++ b/code/game/machinery/telecomms/machines/message_server.dm
@@ -1,17 +1,12 @@
-/*
- The equivalent of the server, for PDA and request console messages.
- Without it, PDA and request console messages cannot be transmitted.
- PDAs require the rest of the telecomms setup, but request consoles only
- require the message server.
-*/
-
// A decorational representation of SSblackbox, usually placed alongside the message server. Also contains a traitor theft item.
/obj/machinery/blackbox_recorder
+ name = "Blackbox Recorder"
icon = 'icons/obj/machines/telecomms.dmi'
icon_state = "blackbox"
- name = "Blackbox Recorder"
density = TRUE
armor_type = /datum/armor/machinery_blackbox_recorder
+ /// The object that's stored in the machine, which is to say, the blackbox itself.
+ /// When it hasn't already been stolen, of course.
var/obj/item/stored
/datum/armor/machinery_blackbox_recorder
@@ -39,15 +34,15 @@
to_chat(user, span_warning("It seems that the blackbox is missing..."))
return
-/obj/machinery/blackbox_recorder/attackby(obj/item/I, mob/living/user, params)
- if(istype(I, /obj/item/blackbox))
- if(HAS_TRAIT(I, TRAIT_NODROP) || !user.transferItemToLoc(I, src))
- to_chat(user, span_warning("[I] is stuck to your hand!"))
+/obj/machinery/blackbox_recorder/attackby(obj/item/attacking_item, mob/living/user, params)
+ if(istype(attacking_item, /obj/item/blackbox))
+ if(HAS_TRAIT(attacking_item, TRAIT_NODROP) || !user.transferItemToLoc(attacking_item, src))
+ to_chat(user, span_warning("[attacking_item] is stuck to your hand!"))
return
- user.visible_message(span_notice("[user] clicks [I] into [src]!"), \
+ user.visible_message(span_notice("[user] clicks [attacking_item] into [src]!"), \
span_notice("You press the device into [src], and it clicks into place. The tapes begin spinning again."))
playsound(src, 'sound/machines/click.ogg', 50, TRUE)
- stored = I
+ stored = attacking_item
update_appearance()
return
return ..()
@@ -73,26 +68,41 @@
w_class = WEIGHT_CLASS_BULKY
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ACID_PROOF
-#define MESSAGE_SERVER_FUNCTIONING_MESSAGE "This is an automated message. The messaging system is functioning correctly."
-// The message server itself.
+/**
+ * The equivalent of the server, for PDA and request console messages.
+ * Without it, PDA and request console messages cannot be transmitted.
+ * PDAs require the rest of the telecomms setup, but request consoles only
+ * require the message server.
+ */
/obj/machinery/telecomms/message_server
- icon_state = "message_server"
name = "Messaging Server"
desc = "A machine that processes and routes PDA and request console messages."
+ icon_state = "message_server"
telecomms_type = /obj/machinery/telecomms/message_server
density = TRUE
circuit = /obj/item/circuitboard/machine/telecomms/message_server
+ /// A list of all the PDA messages that were intercepted and processed by
+ /// this messaging server.
var/list/datum/data_tablet_msg/pda_msgs = list()
+ /// A list of all the Request Console messages that were intercepted and
+ /// processed by this messaging server.
var/list/datum/data_rc_msg/rc_msgs = list()
+ /// The password of this messaging server.
var/decryptkey = "password"
- var/calibrating = 15 MINUTES //Init reads this and adds world.time, then becomes 0 when that time has passed and the machine works
+ /// Init reads this and adds world.time, then becomes 0 when that time has
+ /// passed and the machine works.
+ /// Basically, if it's not 0, it's calibrating and therefore non-functional.
+ var/calibrating = 15 MINUTES
+
+
+#define MESSAGE_SERVER_FUNCTIONING_MESSAGE "This is an automated message. The messaging system is functioning correctly."
/obj/machinery/telecomms/message_server/Initialize(mapload)
. = ..()
if (!decryptkey)
- decryptkey = GenerateKey()
+ decryptkey = generate_key()
if (calibrating)
calibrating += world.time
@@ -112,12 +122,16 @@
if(calibrating)
. += span_warning("It's still calibrating.")
-/obj/machinery/telecomms/message_server/proc/GenerateKey()
- var/newKey
- newKey += pick("the", "if", "of", "as", "in", "a", "you", "from", "to", "an", "too", "little", "snow", "dead", "drunk", "rosebud", "duck", "al", "le")
- newKey += pick("diamond", "beer", "mushroom", "assistant", "clown", "captain", "twinkie", "security", "nuke", "small", "big", "escape", "yellow", "gloves", "monkey", "engine", "nuclear", "ai")
- newKey += pick("1", "2", "3", "4", "5", "6", "7", "8", "9", "0")
- return newKey
+/**
+ * Handles generating a key for the message server, returning it. Doesn't assign
+ * it in this proc, you have to do so yourself.
+ */
+/obj/machinery/telecomms/message_server/proc/generate_key()
+ var/generated_key
+ generated_key += pick("the", "if", "of", "as", "in", "a", "you", "from", "to", "an", "too", "little", "snow", "dead", "drunk", "rosebud", "duck", "al", "le")
+ generated_key += pick("diamond", "beer", "mushroom", "assistant", "clown", "captain", "twinkie", "security", "nuke", "small", "big", "escape", "yellow", "gloves", "monkey", "engine", "nuclear", "ai")
+ generated_key += pick("1", "2", "3", "4", "5", "6", "7", "8", "9", "0")
+ return generated_key
/obj/machinery/telecomms/message_server/process()
. = ..()
@@ -125,6 +139,8 @@
calibrating = 0
pda_msgs += new /datum/data_tablet_msg("System Administrator", "system", MESSAGE_SERVER_FUNCTIONING_MESSAGE)
+#undef MESSAGE_SERVER_FUNCTIONING_MESSAGE
+
/obj/machinery/telecomms/message_server/receive_information(datum/signal/subspace/messaging/signal, obj/machinery/telecomms/machine_from)
// can't log non-message signals
if(!istype(signal) || !signal.data["message"] || !on || calibrating)
@@ -136,8 +152,8 @@
var/datum/data_tablet_msg/log_message = new(PDAsignal.format_target(), PDAsignal.format_sender(), PDAsignal.format_message(), PDAsignal.format_photo_path())
pda_msgs += log_message
else if(istype(signal, /datum/signal/subspace/messaging/rc))
- var/datum/data_rc_msg/msg = new(signal.data["rec_dpt"], signal.data["send_dpt"], signal.data["message"], signal.data["stamped"], signal.data["verified"], signal.data["priority"])
- if(signal.data["send_dpt"]) // don't log messages not from a department but allow them to work
+ var/datum/data_rc_msg/msg = new(signal.data["receiving_department"], signal.data["sender_department"], signal.data["message"], signal.data["stamped"], signal.data["verified"], signal.data["priority"])
+ if(signal.data["sender_department"]) // don't log messages not from a department but allow them to work
rc_msgs += msg
signal.data["reject"] = FALSE
@@ -151,6 +167,14 @@
if(calibrating)
. += "message_server_calibrate"
+// Preset messaging server
+/obj/machinery/telecomms/message_server/preset
+ id = "Messaging Server"
+ network = "tcommsat"
+ autolinkers = list("messaging")
+ decryptkey = null //random
+ calibrating = 0
+
// Root messaging signal datum
/datum/signal/subspace/messaging
@@ -160,8 +184,8 @@
/datum/signal/subspace/messaging/New(init_source, init_data)
source = init_source
data = init_data
- var/turf/T = get_turf(source)
- levels = SSmapping.get_connected_levels(T)
+ var/turf/origin_turf = get_turf(source)
+ levels = SSmapping.get_connected_levels(origin_turf)
if(!("reject" in data))
data["reject"] = TRUE
@@ -172,20 +196,26 @@
return copy
// Tablet message signal datum
+/// Returns a string representing the target of this message, formatted properly.
/datum/signal/subspace/messaging/tablet_message/proc/format_target()
if (data["everyone"])
return "Everyone"
+
var/datum/computer_file/program/messenger/target_app = data["targets"][1]
var/obj/item/modular_computer/target = target_app.computer
- return "[target.saved_identification] ([target.saved_job])"
+ return STRINGIFY_PDA_TARGET(target.saved_identification, target.saved_job)
+/// Returns a string representing the sender of this message, formatted properly.
/datum/signal/subspace/messaging/tablet_message/proc/format_sender()
var/display_name = get_messenger_name(locate(data["ref"]))
return display_name ? display_name : STRINGIFY_PDA_TARGET(data["fakename"], data["fakejob"])
+/// Returns the formatted message contained in this message. Use this to apply
+/// any processing to it if it needs to be formatted in a specific way.
/datum/signal/subspace/messaging/tablet_message/proc/format_message()
return data["message"]
+/// Returns the formatted photo path contained in this message, if there's one.
/datum/signal/subspace/messaging/tablet_message/proc/format_photo_path()
return data["photo"]
@@ -201,13 +231,19 @@
if(ckey(console.department) == recipient_department || (data["ore_update"] && console.receive_ore_updates))
console.create_message(data)
-// Log datums stored by the message server.
+/// Log datums stored by the message server.
/datum/data_tablet_msg
+ /// Who sent the message.
var/sender = "Unspecified"
+ /// Who was targeted by the message.
var/recipient = "Unspecified"
- var/message = "Blank" // transferred message
- var/picture_asset_key // attached photo path
- var/automated = FALSE // automated message
+ /// The transfered message.
+ var/message = "Blank"
+ /// The attached photo path, if any.
+ var/picture_asset_key
+ /// Whether or not it's an automated message. Defaults to `FALSE`.
+ var/automated = FALSE
+
/datum/data_tablet_msg/New(param_rec, param_sender, param_message, param_photo)
if(param_rec)
@@ -219,19 +255,32 @@
if(param_photo)
picture_asset_key = param_photo
+
+#define REQUEST_PRIORITY_NORMAL "Normal"
+#define REQUEST_PRIORITY_HIGH "High"
+#define REQUEST_PRIORITY_EXTREME "Extreme"
+#define REQUEST_PRIORITY_UNDETERMINED "Undetermined"
+
+
/datum/data_rc_msg
- var/rec_dpt = "Unspecified" // receiving department
- var/send_dpt = "Unspecified" // sending department
+ /// The department that sent the request.
+ var/sender_department = "Unspecified"
+ /// The department that was targeted by the request.
+ var/receiving_department = "Unspecified"
+ /// The message of the request.
var/message = "Blank"
+ /// The stamp that authenticated this message, if any.
var/stamp = "Unstamped"
+ /// The ID that authenticated this message, if any.
var/id_auth = "Unauthenticated"
- var/priority = "Normal"
+ /// The priority of this request.
+ var/priority = REQUEST_PRIORITY_NORMAL
/datum/data_rc_msg/New(param_rec, param_sender, param_message, param_stamp, param_id_auth, param_priority)
if(param_rec)
- rec_dpt = param_rec
+ receiving_department = param_rec
if(param_sender)
- send_dpt = param_sender
+ sender_department = param_sender
if(param_message)
message = param_message
if(param_stamp)
@@ -241,19 +290,15 @@
if(param_priority)
switch(param_priority)
if(REQ_NORMAL_MESSAGE_PRIORITY)
- priority = "Normal"
+ priority = REQUEST_PRIORITY_NORMAL
if(REQ_HIGH_MESSAGE_PRIORITY)
- priority = "High"
+ priority = REQUEST_PRIORITY_HIGH
if(REQ_EXTREME_MESSAGE_PRIORITY)
- priority = "Extreme"
+ priority = REQUEST_PRIORITY_EXTREME
else
- priority = "Undetermined"
+ priority = REQUEST_PRIORITY_UNDETERMINED
-#undef MESSAGE_SERVER_FUNCTIONING_MESSAGE
-
-/obj/machinery/telecomms/message_server/preset
- id = "Messaging Server"
- network = "tcommsat"
- autolinkers = list("messaging")
- decryptkey = null //random
- calibrating = 0
+#undef REQUEST_PRIORITY_NORMAL
+#undef REQUEST_PRIORITY_HIGH
+#undef REQUEST_PRIORITY_EXTREME
+#undef REQUEST_PRIORITY_UNDETERMINED
diff --git a/code/game/machinery/telecomms/machines/processor.dm b/code/game/machinery/telecomms/machines/processor.dm
index 906635d67ab1a..005294507461f 100644
--- a/code/game/machinery/telecomms/machines/processor.dm
+++ b/code/game/machinery/telecomms/machines/processor.dm
@@ -1,11 +1,10 @@
-/*
- The processor is a very simple machine that decompresses subspace signals and
- transfers them back to the original bus. It is essential in producing audible
- data.
-
- Link to servers if bus is not present
-*/
-
+/**
+ * The processor is a very simple machine that decompresses subspace signals and
+ * transfers them back to the original bus. It is essential in producing audible
+ * data.
+ *
+ * They'll link to servers if bus is not present, with some delay added to it.
+ */
/obj/machinery/telecomms/processor
name = "processor unit"
icon_state = "processor"
@@ -14,16 +13,22 @@
density = TRUE
idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 0.01
circuit = /obj/item/circuitboard/machine/telecomms/processor
- var/process_mode = 1 // 1 = Uncompress Signals, 0 = Compress Signals
+ /// Whether this processor is currently compressing the data,
+ /// or actually decompressing it. Defaults to `FALSE`.
+ var/compressing = FALSE
+
+#define COMPRESSION_AMOUNT_COMPRESSING 100
+#define COMPRESSION_AMOUNT_DECOMPRESSING 0
/obj/machinery/telecomms/processor/receive_information(datum/signal/subspace/signal, obj/machinery/telecomms/machine_from)
if(!is_freq_listening(signal))
return
- if (!process_mode)
- signal.data["compression"] = 100 // even more compressed signal
- else if (signal.data["compression"])
- signal.data["compression"] = 0 // uncompress subspace signal
+ if(compressing)
+ signal.data["compression"] = COMPRESSION_AMOUNT_COMPRESSING // We compress the signal even further.
+ // Otherwise we just fully decompress it if it was compressed to begin with.
+ else if(signal.data["compression"])
+ signal.data["compression"] = COMPRESSION_AMOUNT_DECOMPRESSING
if(istype(machine_from, /obj/machinery/telecomms/bus))
relay_direct_information(signal, machine_from) // send the signal back to the machine
@@ -31,7 +36,10 @@
signal.data["slow"] += rand(5, 10) // slow the signal down
relay_information(signal, signal.server_type)
-//Preset Processors
+#undef COMPRESSION_AMOUNT_COMPRESSING
+#undef COMPRESSION_AMOUNT_DECOMPRESSING
+
+// Preset Processors
/obj/machinery/telecomms/processor/preset_one
id = "Processor 1"
diff --git a/code/game/machinery/telecomms/machines/receiver.dm b/code/game/machinery/telecomms/machines/receiver.dm
index c9c183c35ffb6..0c4b6d2a02dbc 100644
--- a/code/game/machinery/telecomms/machines/receiver.dm
+++ b/code/game/machinery/telecomms/machines/receiver.dm
@@ -1,11 +1,10 @@
-/*
- The receiver idles and receives messages from subspace-compatible radio equipment;
- primarily headsets. Then they just relay this information to all linked devices,
- which would probably be network hubs.
-
- Link to Processor Units in case receiver can't send to bus units.
-*/
-
+/**
+ * The receiver idles and receives messages from subspace-compatible radio equipment,
+ * primarily headsets. Then they just relay this information to all linked devices,
+ * which would usually be through the telecommunications hub.
+ *
+ * Link to Processor Units in case receiver can't send to a telecommunication hub.
+ */
/obj/machinery/telecomms/receiver
name = "subspace receiver"
icon_state = "broadcast receiver"
@@ -27,21 +26,27 @@
use_power(idle_power_usage)
+/**
+ * Checks whether the signal can be received by this receiver or not, based on
+ * if it's in the signal's `levels`, or if there's a liked hub with a linked
+ * relay that can receive the signal for it.
+ *
+ * Returns `TRUE` if it can receive the signal, `FALSE` if not.
+ */
/obj/machinery/telecomms/receiver/proc/check_receive_level(datum/signal/subspace/signal)
if (z in signal.levels)
return TRUE
- for(var/obj/machinery/telecomms/hub/H in links)
- for(var/obj/machinery/telecomms/relay/R in H.links)
- if(R.can_receive(signal) && (R.z in signal.levels))
+ for(var/obj/machinery/telecomms/hub/linked_hub in links)
+ for(var/obj/machinery/telecomms/relay/linked_relay in linked_hub.links)
+ if(linked_relay.can_receive(signal) && (linked_relay.z in signal.levels))
return TRUE
return FALSE
-//Preset Receivers
+// Preset Receivers
//--PRESET LEFT--//
-
/obj/machinery/telecomms/receiver/preset_left
id = "Receiver A"
network = "tcommsat"
@@ -50,16 +55,16 @@
//--PRESET RIGHT--//
-
/obj/machinery/telecomms/receiver/preset_right
id = "Receiver B"
network = "tcommsat"
autolinkers = list("receiverB") // link to relay
freq_listening = list(FREQ_COMMAND, FREQ_ENGINEERING, FREQ_SECURITY)
- //Common and other radio frequencies for people to freely use
/obj/machinery/telecomms/receiver/preset_right/Initialize(mapload)
. = ..()
+ // Also add common and other freely-available radio frequencies for people
+ // to have access to.
for(var/i = MIN_FREQ, i <= MAX_FREQ, i += 2)
freq_listening |= i
diff --git a/code/game/machinery/telecomms/machines/relay.dm b/code/game/machinery/telecomms/machines/relay.dm
index d3a6ac9ee908c..2173a519be4e1 100644
--- a/code/game/machinery/telecomms/machines/relay.dm
+++ b/code/game/machinery/telecomms/machines/relay.dm
@@ -1,11 +1,11 @@
-/*
- The relay idles until it receives information. It then passes on that information
- depending on where it came from.
-
- The relay is needed in order to send information pass Z levels. It must be linked
- with a HUB, the only other machine that can send/receive pass Z levels.
-*/
-
+/**
+ * The relay idles until it receives information. It then passes on that information
+ * depending on where it came from.
+ *
+ * The relay is needed in order to send information to different Z levels. It
+ * must be linked with a hub, the only other machine that can send to/receive
+ * from other Z levels.
+ */
/obj/machinery/telecomms/relay
name = "telecommunication relay"
icon_state = "relay"
@@ -14,10 +14,12 @@
density = TRUE
idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 0.01
netspeed = 5
- long_range_link = 1
+ long_range_link = TRUE
circuit = /obj/item/circuitboard/machine/telecomms/relay
- var/broadcasting = 1
- var/receiving = 1
+ /// Can this relay broadcast signals to other Z levels?
+ var/broadcasting = TRUE
+ /// Can this relay receive signals from other Z levels?
+ var/receiving = TRUE
/obj/machinery/telecomms/relay/receive_information(datum/signal/subspace/signal, obj/machinery/telecomms/machine_from)
// Add our level and send it back
@@ -25,33 +27,49 @@
if(can_send(signal) && relay_turf)
// Relays send signals to all ZTRAIT_STATION z-levels
if(SSmapping.level_trait(relay_turf.z, ZTRAIT_STATION))
- for(var/z in SSmapping.levels_by_trait(ZTRAIT_STATION))
- signal.levels |= SSmapping.get_connected_levels(z)
+ for(var/z_level in SSmapping.levels_by_trait(ZTRAIT_STATION))
+ signal.levels |= SSmapping.get_connected_levels(z_level)
else
signal.levels |= SSmapping.get_connected_levels(relay_turf)
use_power(idle_power_usage)
-/// Checks to see if it can send/receive.
-/obj/machinery/telecomms/relay/proc/can(datum/signal/signal)
+/**
+ * Checks to see if the relay can send/receive the signal, by checking if it's
+ * on, and if it's listening to the frequency of the signal.
+ *
+ * Returns `TRUE` if it can listen to the signal, `FALSE` if not.
+ */
+/obj/machinery/telecomms/relay/proc/can_listen_to_signal(datum/signal/signal)
if(!on)
return FALSE
if(!is_freq_listening(signal))
return FALSE
return TRUE
+/**
+ * Checks to see if the relay can send this signal, which requires it to have
+ * `broadcasting` set to `TRUE`.
+ *
+ * Returns `TRUE` if it can send the signal, `FALSE` if not.
+ */
/obj/machinery/telecomms/relay/proc/can_send(datum/signal/signal)
- if(!can(signal))
+ if(!can_listen_to_signal(signal))
return FALSE
return broadcasting
+/**
+ * Checks to see if the relay can receive this signal, which requires it to have
+ * `receiving` set to `TRUE`.
+ *
+ * Returns `TRUE` if it can receive the signal, `FALSE` if not.
+ */
/obj/machinery/telecomms/relay/proc/can_receive(datum/signal/signal)
- if(!can(signal))
+ if(!can_listen_to_signal(signal))
return FALSE
return receiving
-//Preset Relay
-
+// Preset Relays
/obj/machinery/telecomms/relay/preset
network = "tcommsat"
@@ -78,7 +96,7 @@
toggled = FALSE
autolinkers = list("r_relay")
-//Generic preset relay
+// Generic preset relay
/obj/machinery/telecomms/relay/preset/auto
hide = TRUE
autolinkers = list("autorelay")
diff --git a/code/game/machinery/telecomms/machines/server.dm b/code/game/machinery/telecomms/machines/server.dm
index 4bcc2c70a7311..fedcf519ff240 100644
--- a/code/game/machinery/telecomms/machines/server.dm
+++ b/code/game/machinery/telecomms/machines/server.dm
@@ -1,10 +1,11 @@
-/*
- The server logs all traffic and signal data. Once it records the signal, it sends
- it to the subspace broadcaster.
-
- Store a maximum of 100 logs and then deletes them.
-*/
-
+#define MAX_LOG_ENTRIES 400
+
+/**
+ * The server logs all traffic and signal data. Once it records the signal, it
+ * sends it to the subspace broadcaster.
+ *
+ * Store a maximum of `MAX_LOG_ENTRIES` (400) log entries and then deletes them.
+ */
/obj/machinery/telecomms/server
name = "telecommunication server"
icon_state = "comm_server"
@@ -13,8 +14,13 @@
density = TRUE
idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 0.01
circuit = /obj/item/circuitboard/machine/telecomms/server
+ /// A list of previous entries on the network. It will not exceed
+ /// `MAX_LOG_ENTRIES` entries in length, flushing the oldest entries
+ /// automatically.
var/list/log_entries = list()
- var/totaltraffic = 0 // gigabytes (if > 1024, divide by 1024 -> terrabytes)
+ /// Total trafic, which is increased every time a signal is increased and
+ /// the current traffic is higher than 0. See `traffic` for more info.
+ var/total_traffic = 0
/obj/machinery/telecomms/server/receive_information(datum/signal/subspace/vocal/signal, obj/machinery/telecomms/machine_from)
// can't log non-vocal signals
@@ -22,10 +28,10 @@
return
if(traffic > 0)
- totaltraffic += traffic // add current traffic to total traffic
+ total_traffic += traffic // add current traffic to total traffic
// Delete particularly old logs
- if (log_entries.len >= 400)
+ if (log_entries.len >= MAX_LOG_ENTRIES)
log_entries.Cut(1, 2)
// Don't create a log if the frequency is banned from being logged
@@ -39,7 +45,7 @@
// If the signal is still compressed, make the log entry gibberish
var/compression = signal.data["compression"]
- if(compression > 0)
+ if(compression > NONE)
log.input_type = "Corrupt File"
var/replace_characters = compression >= 20 ? TRUE : FALSE
log.parameters["name"] = Gibberish(signal.data["name"], replace_characters)
@@ -47,7 +53,7 @@
log.parameters["message"] = Gibberish(signal.data["message"], replace_characters)
// Give the log a name and store it
- var/identifier = num2text( rand(-1000,1000) + world.time )
+ var/identifier = num2text(rand(-1000, 1000) + world.time)
log.name = "data packet ([md5(identifier)])"
log_entries.Add(log)
@@ -57,11 +63,16 @@
use_power(idle_power_usage)
-// Simple log entry datum
+#undef MAX_LOG_ENTRIES
+
+/// Simple log entry datum for the telecommunication server
/datum/comm_log_entry
+ /// Type of entry.
var/input_type = "Speech File"
+ /// Name of the entry.
var/name = "data packet (#)"
- var/parameters = list() // copied from signal.data above
+ /// Parameters extracted from the signal.
+ var/parameters = list()
// Preset Servers
@@ -98,9 +109,9 @@
freq_listening = list()
autolinkers = list("common")
-//Common and other radio frequencies for people to freely use
/obj/machinery/telecomms/server/presets/common/Initialize(mapload)
. = ..()
+ // Common and other radio frequencies for people to freely use
for(var/i = MIN_FREQ, i <= MAX_FREQ, i += 2)
freq_listening |= i
diff --git a/code/game/machinery/telecomms/telecomunications.dm b/code/game/machinery/telecomms/telecomunications.dm
index 95d5ca581ca40..b8d0414ad282b 100644
--- a/code/game/machinery/telecomms/telecomunications.dm
+++ b/code/game/machinery/telecomms/telecomunications.dm
@@ -1,19 +1,11 @@
-
-/*
- Hello, friends, this is Doohl from sexylands. You may be wondering what this
- monstrous code file is. Sit down, boys and girls, while I tell you the tale.
-
-
- The telecom machines were designed to be compatible with any radio
- signals, provided they use subspace transmission. Currently they are only used for
- headsets, but they can eventually be outfitted for real COMPUTER networks. This
- is just a skeleton, ladies and gentlemen.
-
- Look at radio.dm for the prequel to this code.
-*/
-
+/// A list of all of the `/obj/machinery/telecomms` (and subtypes) machines
+/// that exist in the world currently.
GLOBAL_LIST_EMPTY(telecomms_list)
+/**
+ * The basic telecomms machinery type, implementing all of the logic that's
+ * shared between all of the telecomms machinery.
+ */
/obj/machinery/telecomms
icon = 'icons/obj/machines/telecomms.dmi'
critical_machine = TRUE
@@ -40,15 +32,16 @@ GLOBAL_LIST_EMPTY(telecomms_list)
// list of frequencies to tune into: if none, will listen to all
var/list/freq_listening = list()
+ /// Is it actually active or not?
var/on = TRUE
- /// Is it toggled on
+ /// Is it toggled on, so is it /meant/ to be active?
var/toggled = TRUE
/// Can you link it across Z levels or on the otherside of the map? (Relay & Hub)
var/long_range_link = FALSE
/// Is it a hidden machine?
var/hide = FALSE
- ///Looping sounds for any servers
+ /// Looping sounds for any servers
var/datum/looping_sound/server/soundloop
/// relay signal to all linked machinery that are of type [filter]. If signal has been sent [amount] times, stop sending
@@ -90,16 +83,20 @@ GLOBAL_LIST_EMPTY(telecomms_list)
return send_count
+/// Sends a signal directly to a machine.
/obj/machinery/telecomms/proc/relay_direct_information(datum/signal/signal, obj/machinery/telecomms/machine)
- // send signal directly to a machine
machine.receive_information(signal, src)
-///receive information from linked machinery
+/// Receive information from linked machinery
/obj/machinery/telecomms/proc/receive_information(datum/signal/signal, obj/machinery/telecomms/machine_from)
return
+/**
+ * Checks whether the machinery is listening to that signal.
+ *
+ * Returns `TRUE` if found, `FALSE` if not.
+ */
/obj/machinery/telecomms/proc/is_freq_listening(datum/signal/signal)
- // return TRUE if found, FALSE if not found
return signal && (!length(freq_listening) || (signal.frequency in freq_listening))
/obj/machinery/telecomms/Initialize(mapload)
@@ -122,17 +119,17 @@ GLOBAL_LIST_EMPTY(telecomms_list)
links = list()
return ..()
-/// Used in auto linking
-/obj/machinery/telecomms/proc/add_automatic_link(obj/machinery/telecomms/T)
+/// Handles the automatic linking of another machine to this one.
+/obj/machinery/telecomms/proc/add_automatic_link(obj/machinery/telecomms/machine_to_link)
var/turf/position = get_turf(src)
- var/turf/T_position = get_turf(T)
- if((position.z != T_position.z) && !(long_range_link && T.long_range_link))
+ var/turf/T_position = get_turf(machine_to_link)
+ if((position.z != T_position.z) && !(long_range_link && machine_to_link.long_range_link))
return
- if(src == T)
+ if(src == machine_to_link)
return
for(var/autolinker_id in autolinkers)
- if(autolinker_id in T.autolinkers)
- add_new_link(T)
+ if(autolinker_id in machine_to_link.autolinkers)
+ add_new_link(machine_to_link)
return
/obj/machinery/telecomms/update_icon_state()
@@ -143,6 +140,11 @@ GLOBAL_LIST_EMPTY(telecomms_list)
update_appearance()
return ..()
+/**
+ * Handles updating the power state of the machine, modifying its `on`
+ * variable based on if it's `toggled` and if it's either broken, has no power
+ * or it's EMP'd. Handles updating appearance based on that power change.
+ */
/obj/machinery/telecomms/proc/update_power()
var/old_on = on
if(toggled)
@@ -167,8 +169,9 @@ GLOBAL_LIST_EMPTY(telecomms_list)
return
if(prob(100/severity) && !(machine_stat & EMPED))
set_machine_stat(machine_stat | EMPED)
- var/duration = (300 * 10)/severity
- addtimer(CALLBACK(src, PROC_REF(de_emp)), rand(duration - 20, duration + 20))
+ var/duration = (300 SECONDS)/severity
+ addtimer(CALLBACK(src, PROC_REF(de_emp)), rand(duration - 2 SECONDS, duration + 2 SECONDS))
+/// Handles the machine stopping being affected by an EMP.
/obj/machinery/telecomms/proc/de_emp()
set_machine_stat(machine_stat & ~EMPED)
diff --git a/code/game/objects/effects/anomalies/anomalies_flux.dm b/code/game/objects/effects/anomalies/anomalies_flux.dm
index 2bb3ab28a1ada..35b8bc388d720 100644
--- a/code/game/objects/effects/anomalies/anomalies_flux.dm
+++ b/code/game/objects/effects/anomalies/anomalies_flux.dm
@@ -66,7 +66,7 @@
///range in whuich we zap
var/zap_range = 1
///strength of the zappy
- var/zap_power = 1e6
+ var/zap_power = 2500
///the zappy flags
var/zap_flags = ZAP_GENERATES_POWER | ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE
@@ -78,7 +78,7 @@
/obj/effect/anomaly/flux/big/anomalyEffect()
. = ..()
- tesla_zap(src, zap_range, zap_power, zap_flags)
+ tesla_zap(source = src, zap_range = zap_range, power = zap_power, cutoff = 1e3, zap_flags = zap_flags)
/obj/effect/anomaly/flux/big/Bumped(atom/movable/bumpee)
. = ..()
diff --git a/code/game/objects/effects/countdown.dm b/code/game/objects/effects/countdown.dm
index f820397c0d9fc..1c7377bd50aaf 100644
--- a/code/game/objects/effects/countdown.dm
+++ b/code/game/objects/effects/countdown.dm
@@ -157,7 +157,7 @@
return round(time_left)
/obj/effect/countdown/arena
- invisibility = 0
+ invisibility = INVISIBILITY_NONE
name = "arena countdown"
/obj/effect/countdown/arena/get_value()
diff --git a/code/game/objects/effects/decals/decal.dm b/code/game/objects/effects/decals/decal.dm
index 2c6d01b5bc400..941034ec511be 100644
--- a/code/game/objects/effects/decals/decal.dm
+++ b/code/game/objects/effects/decals/decal.dm
@@ -50,8 +50,8 @@
anchored = TRUE
/// Does this decal change colors on holidays
var/use_holiday_colors = FALSE
- /// The pattern used when recoloring the decal
- var/pattern = PATTERN_DEFAULT
+ /// The pattern used when recoloring the decal. If null, it'll use the def of the station or holiday.
+ var/pattern
// This is with the intent of optimizing mapload
// See spawners for more details since we use the same pattern
diff --git a/code/game/objects/effects/decals/misc.dm b/code/game/objects/effects/decals/misc.dm
index 4254d99fdc391..112f3b228dc47 100644
--- a/code/game/objects/effects/decals/misc.dm
+++ b/code/game/objects/effects/decals/misc.dm
@@ -13,6 +13,8 @@
var/lifetime = INFINITY
///Are we a part of a stream?
var/stream
+ /// String used in combat logs containing reagents present for when the puff hits something
+ var/logging_string
/obj/effect/decal/chempuff/Destroy(force)
user = null
@@ -22,28 +24,39 @@
/obj/effect/decal/chempuff/blob_act(obj/structure/blob/B)
return
-/obj/effect/decal/chempuff/proc/end_life(datum/move_loop/engine)
- QDEL_IN(src, engine.delay) //Gotta let it stop drifting
- animate(src, alpha = 0, time = engine.delay)
+/obj/effect/decal/chempuff/proc/end_life(delay = 0.5 SECONDS)
+ QDEL_IN(src, delay) //Gotta let it stop drifting
+ animate(src, alpha = 0, time = delay)
/obj/effect/decal/chempuff/proc/loop_ended(datum/move_loop/source)
SIGNAL_HANDLER
+
if(QDELETED(src))
return
- end_life(source)
+ end_life(source.delay)
/obj/effect/decal/chempuff/proc/check_move(datum/move_loop/source, result)
+ SIGNAL_HANDLER
+
if(QDELETED(src)) //Reasons PLEASE WORK I SWEAR TO GOD
return
if(result == MOVELOOP_FAILURE) //If we hit something
- end_life(source)
+ end_life(source.delay)
return
- var/puff_reagents_string = reagents.get_reagent_log_string()
- var/travelled_max_distance = (source.lifetime - source.delay <= 0)
- var/turf/our_turf = get_turf(src)
+ spray_down_turf(get_turf(src), travelled_max_distance = (source.lifetime - source.delay <= 0))
- for(var/atom/movable/turf_atom in our_turf)
+ if(lifetime < 0) // Did we use up all the puff early?
+ end_life(source.delay)
+
+/**
+ * Handles going through every movable on the passed turf and calling [spray_down_atom] on them.
+ *
+ * [travelled_max_distance] is used to determine if we're at the end of the life, as in some
+ * contexts an atom may or may not end up being exposed depending on how far we've travelled.
+ */
+/obj/effect/decal/chempuff/proc/spray_down_turf(turf/spraying, travelled_max_distance = FALSE)
+ for(var/atom/movable/turf_atom in spraying)
if(turf_atom == src || turf_atom.invisibility) //we ignore the puff itself and stuff below the floor
continue
@@ -51,8 +64,7 @@
break
if(!stream)
- reagents.expose(turf_atom, VAPOR)
- log_combat(user, turf_atom, "sprayed", sprayer, addition="which had [puff_reagents_string]")
+ spray_down_atom(turf_atom)
if(ismob(turf_atom))
lifetime -= 1
continue
@@ -65,23 +77,24 @@
if(turf_mob.body_position != STANDING_UP && !travelled_max_distance)
continue
- reagents.expose(turf_mob, VAPOR)
- log_combat(user, turf_mob, "sprayed", sprayer, addition="which had [puff_reagents_string]")
+ spray_down_atom(turf_atom)
lifetime -= 1
else if(travelled_max_distance)
- reagents.expose(turf_atom, VAPOR)
- log_combat(user, turf_atom, "sprayed", sprayer, addition="which had [puff_reagents_string]")
+ spray_down_atom(turf_atom)
lifetime -= 1
if(lifetime >= 0 && (!stream || travelled_max_distance))
- reagents.expose(our_turf, VAPOR)
- log_combat(user, our_turf, "sprayed", sprayer, addition="which had [puff_reagents_string]")
+ spray_down_atom(spraying)
lifetime -= 1
- // Did we use up all the puff early?
- if(lifetime < 0)
- end_life(source)
+/// Actually handles exposing the passed atom to the reagents and logging
+/obj/effect/decal/chempuff/proc/spray_down_atom(atom/spraying)
+ if(isnull(logging_string))
+ logging_string = reagents.get_reagent_log_string()
+
+ reagents.expose(spraying, VAPOR)
+ log_combat(user, spraying, "sprayed", sprayer, addition = "which had [logging_string]")
/obj/effect/decal/fakelattice
name = "lattice"
diff --git a/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm b/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm
index 787ce602a3a89..73e95ab48cde2 100644
--- a/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm
+++ b/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm
@@ -137,9 +137,10 @@
if(!istype(location))
return FALSE
+ var/datum/can_pass_info/info = new(no_id = TRUE)
for(var/iter_dir in GLOB.cardinals)
var/turf/spread_turf = get_step(src, iter_dir)
- if(spread_turf?.density || spread_turf.LinkBlockedWithAccess(spread_turf, no_id = TRUE))
+ if(spread_turf?.density || spread_turf.LinkBlockedWithAccess(spread_turf, info))
continue
var/obj/effect/particle_effect/fluid/foam/foundfoam = locate() in spread_turf //Don't spread foam where there's already foam!
diff --git a/code/game/objects/effects/landmarks.dm b/code/game/objects/effects/landmarks.dm
index 57e578867da11..a7934c94f31cb 100644
--- a/code/game/objects/effects/landmarks.dm
+++ b/code/game/objects/effects/landmarks.dm
@@ -79,7 +79,7 @@ INITIALIZE_IMMEDIATE(/obj/effect/landmark)
/obj/effect/landmark/start/bitrunner
name = "Bitrunner"
- icon_state = "x3"
+ icon_state = "Bitrunner"
/obj/effect/landmark/start/bartender
name = "Bartender"
diff --git a/code/game/objects/effects/phased_mob.dm b/code/game/objects/effects/phased_mob.dm
index 273a4c772a57a..1456fa350bfab 100644
--- a/code/game/objects/effects/phased_mob.dm
+++ b/code/game/objects/effects/phased_mob.dm
@@ -61,7 +61,8 @@
/obj/effect/dummy/phased_mob/ex_act()
return FALSE
-/obj/effect/dummy/phased_mob/bullet_act(blah)
+/obj/effect/dummy/phased_mob/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
+ SHOULD_CALL_PARENT(FALSE)
return BULLET_ACT_FORCE_PIERCE
/obj/effect/dummy/phased_mob/relaymove(mob/living/user, direction)
diff --git a/code/game/objects/effects/posters/contraband.dm b/code/game/objects/effects/posters/contraband.dm
index 6881c52862c71..2bb2fcce50e46 100644
--- a/code/game/objects/effects/posters/contraband.dm
+++ b/code/game/objects/effects/posters/contraband.dm
@@ -509,7 +509,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/triumphal_arch
. = ..()
. += span_notice("You browse some of the poster's information...")
. += "\t[span_info("Va Lümla Commissary Menu (Spring 335)")]"
- . += "\t[span_info("Windgrass Cigarettes, Half-Pack (6): 1 Ticket")]"
+ . += "\t[span_info("Sparkweed Cigarettes, Half-Pack (6): 1 Ticket")]"
. += "\t[span_info("Töchtaüse Schnapps, Bottle (4 Measures): 2 Tickets")]"
. += "\t[span_info("Activin Gum, Pack (4): 1 Ticket")]"
. += "\t[span_info("A18 Sustenance Bar, Breakfast, Bar (4): 1 Ticket")]"
diff --git a/code/game/objects/effects/spawners/random/maintenance.dm b/code/game/objects/effects/spawners/random/maintenance.dm
index 5481123bbff2a..242613e403d6a 100644
--- a/code/game/objects/effects/spawners/random/maintenance.dm
+++ b/code/game/objects/effects/spawners/random/maintenance.dm
@@ -13,7 +13,7 @@
return ..()
/obj/effect/spawner/random/maintenance/proc/hide()
- invisibility = INVISIBILITY_OBSERVER
+ SetInvisibility(INVISIBILITY_OBSERVER)
alpha = 100
/obj/effect/spawner/random/maintenance/proc/get_effective_lootcount()
diff --git a/code/game/objects/effects/spiderwebs.dm b/code/game/objects/effects/spiderwebs.dm
index 9b44d3507db2e..9a3d6c9c8e373 100644
--- a/code/game/objects/effects/spiderwebs.dm
+++ b/code/game/objects/effects/spiderwebs.dm
@@ -194,6 +194,22 @@
. = ..()
AddComponent(/datum/component/caltrop, min_damage = 20, max_damage = 30, flags = CALTROP_NOSTUN | CALTROP_BYPASS_SHOES)
+/obj/structure/spider/reflector
+ name = "Reflective silk screen"
+ icon = 'icons/effects/effects.dmi'
+ desc = "Made up of an extremly reflective silk material looking at it hurts."
+ icon_state = "reflector"
+ max_integrity = 30
+ density = TRUE
+ opacity = TRUE
+ anchored = TRUE
+ flags_ricochet = RICOCHET_SHINY | RICOCHET_HARD
+ receive_ricochet_chance_mod = INFINITY
+
+/obj/structure/spider/reflector/Initialize(mapload)
+ . = ..()
+ air_update_turf(TRUE, TRUE)
+
/obj/structure/spider/effigy
name = "web effigy"
icon = 'icons/effects/effects.dmi'
diff --git a/code/game/objects/effects/temporary_visuals/miscellaneous.dm b/code/game/objects/effects/temporary_visuals/miscellaneous.dm
index 08ad87d5eee89..68a25f411b96c 100644
--- a/code/game/objects/effects/temporary_visuals/miscellaneous.dm
+++ b/code/game/objects/effects/temporary_visuals/miscellaneous.dm
@@ -58,7 +58,7 @@
/obj/effect/temp_visual/dir_setting/firing_effect
icon = 'icons/effects/effects.dmi'
icon_state = "firing_effect"
- duration = 2
+ duration = 3
/obj/effect/temp_visual/dir_setting/firing_effect/setDir(newdir)
switch(newdir)
@@ -74,8 +74,14 @@
pixel_y = rand(-1,1)
..()
-/obj/effect/temp_visual/dir_setting/firing_effect/energy
- icon_state = "firing_effect_energy"
+/obj/effect/temp_visual/dir_setting/firing_effect/blue
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "firing_effect_blue"
+ duration = 3
+
+/obj/effect/temp_visual/dir_setting/firing_effect/red
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "firing_effect_red"
duration = 3
/obj/effect/temp_visual/dir_setting/firing_effect/magic
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index dc68589b03c29..5abdfe436cc30 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -498,7 +498,6 @@
return
var/datum/fantasy_affix/affix = affixes[picked_affix_name]
affixes.Remove(affix)
- QDEL_LIST_ASSOC(affixes) //remove the rest, we didn't use them
var/fantasy_quality = 0
if(affix.alignment & AFFIX_GOOD)
fantasy_quality++
@@ -1347,12 +1346,9 @@
// Update icons if this is being carried by a mob
/obj/item/wash(clean_types)
. = ..()
-
- SEND_SIGNAL(src, COMSIG_ATOM_WASHED)
-
if(ismob(loc))
var/mob/mob_loc = loc
- mob_loc.regenerate_icons()
+ mob_loc.update_clothing(slot_flags)
/// Called on [/datum/element/openspace_item_click_handler/proc/on_afterattack]. Check the relative file for information.
/obj/item/proc/handle_openspace_click(turf/target, mob/user, proximity_flag, click_parameters)
diff --git a/code/game/objects/items/boxcutter.dm b/code/game/objects/items/boxcutter.dm
index 62273c1080fc1..467bc666e6027 100644
--- a/code/game/objects/items/boxcutter.dm
+++ b/code/game/objects/items/boxcutter.dm
@@ -17,6 +17,8 @@
var/snap_time_weak_handcuffs = 0 SECONDS
/// Used on Initialize, how much time to cut real handcuffs. Null means it can't.
var/snap_time_strong_handcuffs = null
+ /// Starts open if true
+ var/start_extended = FALSE
/obj/item/boxcutter/get_all_tool_behaviours()
return list(TOOL_KNIFE)
@@ -31,6 +33,7 @@
AddComponent( \
/datum/component/transforming, \
+ start_transformed = start_extended, \
force_on = 10, \
throwforce_on = 4, \
throw_speed_on = throw_speed, \
@@ -53,3 +56,6 @@
else
RemoveElement(/datum/element/cuffsnapping, snap_time_weak_handcuffs, snap_time_strong_handcuffs)
return COMPONENT_NO_DEFAULT_MESSAGE
+
+/obj/item/boxcutter/extended
+ start_extended = TRUE
diff --git a/code/game/objects/items/cardboard_cutouts.dm b/code/game/objects/items/cardboard_cutouts.dm
index 65cfcfced2121..d86f386950c7b 100644
--- a/code/game/objects/items/cardboard_cutouts.dm
+++ b/code/game/objects/items/cardboard_cutouts.dm
@@ -6,6 +6,7 @@
icon_state = "cutout_basic"
w_class = WEIGHT_CLASS_BULKY
resistance_flags = FLAMMABLE
+ obj_flags = CAN_BE_HIT
item_flags = NO_PIXEL_RANDOM_DROP
/// If the cutout is pushed over and has to be righted
var/pushed_over = FALSE
@@ -35,7 +36,7 @@
//ATTACK HAND IGNORING PARENT RETURN VALUE
/obj/item/cardboard_cutout/attack_hand(mob/living/user, list/modifiers)
- if(!user.combat_mode || pushed_over)
+ if(!user.combat_mode || pushed_over || !isturf(loc))
return ..()
user.visible_message(span_warning("[user] pushes over [src]!"), span_danger("You push over [src]!"))
playsound(src, 'sound/weapons/genhit.ogg', 50, TRUE)
@@ -60,32 +61,20 @@
/obj/item/cardboard_cutout/attackby(obj/item/I, mob/living/user, params)
if(istype(I, /obj/item/toy/crayon))
change_appearance(I, user)
- return
- // Why yes, this does closely resemble mob and object attack code.
- if(I.item_flags & NOBLUDGEON)
- return
- if(!I.force)
- playsound(loc, 'sound/weapons/tap.ogg', get_clamped_volume(), TRUE, -1)
- else if(I.hitsound)
- playsound(loc, I.hitsound, get_clamped_volume(), TRUE, -1)
-
- user.changeNext_move(CLICK_CD_MELEE)
- user.do_attack_animation(src)
-
- if(I.force)
- user.visible_message(span_danger("[user] hits [src] with [I]!"), \
- span_danger("You hit [src] with [I]!"))
- if(prob(I.force))
- push_over()
-
-/obj/item/cardboard_cutout/bullet_act(obj/projectile/P, def_zone, piercing_hit = FALSE)
- if(istype(P, /obj/projectile/bullet))
- P.on_hit(src, 0, piercing_hit)
- visible_message(span_danger("[src] is hit by [P]!"))
- playsound(src, 'sound/weapons/slice.ogg', 50, TRUE)
- if(prob(P.damage))
+ return TRUE
+
+ return ..()
+
+/obj/item/cardboard_cutout/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir, armour_penetration)
+ . = ..()
+ var/damage_sustained = . || 0
+ if((damage_flag == BULLET || damage_flag == MELEE) && (damage_type == BRUTE) && prob(damage_sustained))
push_over()
- return BULLET_ACT_HIT
+
+/obj/item/cardboard_cutout/deconstruct(disassembled)
+ if(!(flags_1 & (HOLOGRAM_1|NODECONSTRUCT_1)))
+ new /obj/item/stack/sheet/cardboard(loc, 1)
+ return ..()
/proc/get_cardboard_cutout_instance(datum/cardboard_cutout/cardboard_cutout)
ASSERT(ispath(cardboard_cutout), "[cardboard_cutout] is not a path of /datum/cardboard_cutout")
diff --git a/code/game/objects/items/charter.dm b/code/game/objects/items/charter.dm
index 55dd8bad996ea..1d1f8fad7cc56 100644
--- a/code/game/objects/items/charter.dm
+++ b/code/game/objects/items/charter.dm
@@ -1,5 +1,3 @@
-#define STATION_RENAME_TIME_LIMIT 3000
-
/obj/item/station_charter
name = "station charter"
icon = 'icons/obj/scrolls.dmi'
@@ -119,5 +117,3 @@
SSblackbox.record_feedback("text", "station_renames", 1, "[station_name()]")
if(!unlimited_uses)
used = TRUE
-
-#undef STATION_RENAME_TIME_LIMIT
diff --git a/code/game/objects/items/circuitboards/computer_circuitboards.dm b/code/game/objects/items/circuitboards/computer_circuitboards.dm
index 79efe62a2250d..2eca8339590d4 100644
--- a/code/game/objects/items/circuitboards/computer_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/computer_circuitboards.dm
@@ -331,6 +331,20 @@
/obj/item/circuitboard/computer/tram_controls
name = "Tram Controls"
build_path = /obj/machinery/computer/tram_controls
+ var/split_mode = FALSE
+
+/obj/item/circuitboard/computer/tram_controls/split
+ split_mode = TRUE
+
+/obj/item/circuitboard/computer/tram_controls/examine(mob/user)
+ . = ..()
+ . += span_info("The board is configured for [split_mode ? "split window" : "normal window"].")
+ . += span_notice("The board mode can be changed with a [EXAMINE_HINT("multitool")].")
+
+/obj/item/circuitboard/computer/tram_controls/multitool_act(mob/living/user)
+ split_mode = !split_mode
+ to_chat(user, span_notice("[src] positioning set to [split_mode ? "split window" : "normal window"]."))
+ return TRUE
/obj/item/circuitboard/computer/terminal
name = "Terminal"
diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
index bd09efd02ca1c..08f2f111783c7 100644
--- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
@@ -462,7 +462,7 @@
req_components = list(
/datum/stock_part/matter_bin = 1,
/datum/stock_part/servo = 1,
- /obj/item/reagent_containers/cup/beaker = 2)
+ )
/obj/item/circuitboard/machine/circuit_imprinter/offstation
name = "Ancient Circuit Imprinter"
@@ -513,7 +513,7 @@
req_components = list(
/datum/stock_part/matter_bin = 2,
/datum/stock_part/servo = 2,
- /obj/item/reagent_containers/cup/beaker = 2)
+ )
/obj/item/circuitboard/machine/protolathe/offstation
name = "Ancient Protolathe"
@@ -601,7 +601,7 @@
req_components = list(
/datum/stock_part/matter_bin = 2,
/datum/stock_part/servo = 2,
- /obj/item/reagent_containers/cup/beaker = 2)
+ )
/obj/item/circuitboard/machine/techfab/department
name = "\improper Departmental Techfab"
@@ -1381,6 +1381,21 @@
/datum/stock_part/scanning_module = 1,
/datum/stock_part/card_reader = 1)
+//Tram
+/obj/item/circuitboard/machine/crossing_signal
+ name = "Crossing Signal"
+ build_path = /obj/machinery/transport/crossing_signal
+ req_components = list(
+ /datum/stock_part/micro_laser = 1,
+ )
+
+/obj/item/circuitboard/machine/guideway_sensor
+ name = "Guideway Sensor"
+ build_path = /obj/machinery/transport/guideway_sensor
+ req_components = list(
+ /obj/item/assembly/prox_sensor = 1,
+ )
+
//Misc
/obj/item/circuitboard/machine/sheetifier
name = "Sheet-meister 2000"
diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm
index bfe1457aa522b..7f7f733ad863a 100644
--- a/code/game/objects/items/crayons.dm
+++ b/code/game/objects/items/crayons.dm
@@ -815,7 +815,8 @@
carbon_target.set_eye_blur_if_lower(6 SECONDS)
carbon_target.adjust_temp_blindness(2 SECONDS)
if(carbon_target.get_eye_protection() <= 0) // no eye protection? ARGH IT BURNS. Warning: don't add a stun here. It's a roundstart item with some quirks.
- carbon_target.apply_effects(eyeblur = 5, jitter = 10)
+ carbon_target.adjust_jitter(1 SECONDS)
+ carbon_target.adjust_eye_blur(0.5 SECONDS)
flash_color(carbon_target, flash_color=paint_color, flash_time=40)
if(ishuman(carbon_target) && actually_paints)
var/mob/living/carbon/human/human_target = carbon_target
diff --git a/code/game/objects/items/devices/aicard_evil.dm b/code/game/objects/items/devices/aicard_evil.dm
index 1a5fce6897ae1..f91150bb086a6 100644
--- a/code/game/objects/items/devices/aicard_evil.dm
+++ b/code/game/objects/items/devices/aicard_evil.dm
@@ -28,26 +28,29 @@
finding_candidate = FALSE
return TRUE
+/// Sets up the ghost poll
/obj/item/aicard/syndie/loaded/proc/procure_ai(mob/user)
var/datum/antagonist/nukeop/op_datum = user.mind?.has_antag_datum(/datum/antagonist/nukeop,TRUE)
if(isnull(op_datum))
balloon_alert(user, "invalid access!")
return
- var/list/nuke_candidates = poll_ghost_candidates(
- question = "Do you want to play as a nuclear operative MODsuit AI?",
- jobban_type = ROLE_OPERATIVE,
- be_special_flag = ROLE_OPERATIVE_MIDROUND,
- poll_time = 15 SECONDS,
- ignore_category = POLL_IGNORE_SYNDICATE,
+
+ var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), user, op_datum)
+ AddComponent(/datum/component/orbit_poll, \
+ ignore_key = POLL_IGNORE_SYNDICATE, \
+ job_bans = ROLE_OPERATIVE, \
+ to_call = to_call, \
+ title = "Nuclear Operative Modsuit AI" \
)
- if(QDELETED(src))
- return
- if(!LAZYLEN(nuke_candidates))
+
+/// Poll has concluded with a ghost, create the AI
+/obj/item/aicard/syndie/loaded/proc/on_poll_concluded(mob/user, datum/antagonist/nukeop/op_datum, mob/dead/observer/ghost)
+ if(isnull(ghost))
to_chat(user, span_warning("Unable to connect to S.E.L.F. dispatch. Please wait and try again later or use the intelliCard on your uplink to get your points refunded."))
return
+
// pick ghost, create AI and transfer
- var/mob/dead/observer/ghos = pick(nuke_candidates)
- var/mob/living/silicon/ai/weak_syndie/new_ai = new /mob/living/silicon/ai/weak_syndie(get_turf(src), new /datum/ai_laws/syndicate_override, ghos)
+ var/mob/living/silicon/ai/weak_syndie/new_ai = new /mob/living/silicon/ai/weak_syndie(get_turf(src), new /datum/ai_laws/syndicate_override, ghost)
// create and apply syndie datum
var/datum/antagonist/nukeop/nuke_datum = new()
nuke_datum.send_to_spawnpoint = FALSE
diff --git a/code/game/objects/items/devices/chameleonproj.dm b/code/game/objects/items/devices/chameleonproj.dm
index 3ece9837b1a37..5406895a00393 100644
--- a/code/game/objects/items/devices/chameleonproj.dm
+++ b/code/game/objects/items/devices/chameleonproj.dm
@@ -58,6 +58,9 @@
if(iseffect(target))
if(!(istype(target, /obj/effect/decal))) //be a footprint
return
+ make_copy(target, user)
+
+/obj/item/chameleon/proc/make_copy(atom/target, mob/user)
playsound(get_turf(src), 'sound/weapons/flash.ogg', 100, TRUE, -6)
to_chat(user, span_notice("Scanned [target]."))
var/obj/temp = new /obj()
diff --git a/code/game/objects/items/devices/flashlight.dm b/code/game/objects/items/devices/flashlight.dm
index e49f2361a6e16..d13330362c45d 100644
--- a/code/game/objects/items/devices/flashlight.dm
+++ b/code/game/objects/items/devices/flashlight.dm
@@ -31,13 +31,13 @@
var/sound_on = 'sound/weapons/magin.ogg'
/// The sound the light makes when it's turned off
var/sound_off = 'sound/weapons/magout.ogg'
- /// Is the light turned on or off currently
- var/on = FALSE
+ /// Should the flashlight start turned on?
+ var/start_on = FALSE
/obj/item/flashlight/Initialize(mapload)
. = ..()
- if(icon_state == "[initial(icon_state)]-on")
- on = TRUE
+ if(start_on)
+ set_light_on(TRUE)
update_brightness()
register_context()
if(toggle_context)
@@ -52,18 +52,19 @@
/obj/item/flashlight/add_context(atom/source, list/context, obj/item/held_item, mob/living/user)
// single use lights can be toggled on once
- if(isnull(held_item) && (toggle_context || !on))
+ if(isnull(held_item) && (toggle_context || !light_on))
context[SCREENTIP_CONTEXT_RMB] = "Toggle light"
return CONTEXTUAL_SCREENTIP_SET
- if(istype(held_item, /obj/item/flashlight) && (toggle_context || !on))
+ if(istype(held_item, /obj/item/flashlight) && (toggle_context || !light_on))
context[SCREENTIP_CONTEXT_LMB] = "Toggle light"
return CONTEXTUAL_SCREENTIP_SET
return NONE
-/obj/item/flashlight/proc/update_brightness()
- if(on)
+/obj/item/flashlight/update_icon_state()
+ . = ..()
+ if(light_on)
icon_state = "[initial(icon_state)]-on"
if(!isnull(inhand_icon_state))
inhand_icon_state = "[initial(inhand_icon_state)]-on"
@@ -71,22 +72,26 @@
icon_state = initial(icon_state)
if(!isnull(inhand_icon_state))
inhand_icon_state = initial(inhand_icon_state)
- set_light_on(on)
+
+/obj/item/flashlight/proc/update_brightness()
+ update_appearance(UPDATE_ICON)
if(light_system == STATIC_LIGHT)
update_light()
/obj/item/flashlight/proc/toggle_light(mob/user)
- var/disrupted = FALSE
- on = !on
- playsound(src, on ? sound_on : sound_off, 40, TRUE)
+ playsound(src, light_on ? sound_off : sound_on, 40, TRUE)
if(!COOLDOWN_FINISHED(src, disabled_time))
if(user)
balloon_alert(user, "disrupted!")
- on = FALSE
- disrupted = TRUE
+ set_light_on(FALSE)
+ update_brightness()
+ update_item_action_buttons()
+ return FALSE
+ var/old_light_on = light_on
+ set_light_on(!light_on)
update_brightness()
update_item_action_buttons()
- return !disrupted
+ return light_on != old_light_on // If the value of light_on didn't change, return false. Otherwise true.
/obj/item/flashlight/attack_self(mob/user)
toggle_light(user)
@@ -104,7 +109,7 @@
/obj/item/flashlight/attack(mob/living/carbon/M, mob/living/carbon/human/user)
add_fingerprint(user)
- if(istype(M) && on && (user.zone_selected in list(BODY_ZONE_PRECISE_EYES, BODY_ZONE_PRECISE_MOUTH)))
+ if(istype(M) && light_on && (user.zone_selected in list(BODY_ZONE_PRECISE_EYES, BODY_ZONE_PRECISE_MOUTH)))
if((HAS_TRAIT(user, TRAIT_CLUMSY) || HAS_TRAIT(user, TRAIT_DUMB)) && prob(50)) //too dumb to use flashlight properly
return ..() //just hit them in the head
@@ -274,7 +279,7 @@
/// when hit by a light disruptor - turns the light off, forces the light to be disabled for a few seconds
/obj/item/flashlight/proc/on_saboteur(datum/source, disrupt_duration)
SIGNAL_HANDLER
- if(on)
+ if(light_on)
toggle_light()
COOLDOWN_START(src, disabled_time, disrupt_duration)
return COMSIG_SABOTEUR_SUCCESS
@@ -289,25 +294,33 @@
w_class = WEIGHT_CLASS_TINY
flags_1 = CONDUCT_1
light_range = 2
- var/holo_cooldown = 0
+ COOLDOWN_DECLARE(holosign_cooldown)
/obj/item/flashlight/pen/afterattack(atom/target, mob/user, proximity_flag)
. = ..()
- if(!proximity_flag)
- if(holo_cooldown > world.time)
- to_chat(user, span_warning("[src] is not ready yet!"))
- return
- var/T = get_turf(target)
- if(locate(/mob/living) in T)
- new /obj/effect/temp_visual/medical_holosign(T,user) //produce a holographic glow
- holo_cooldown = world.time + 10 SECONDS
- return
+ if(proximity_flag)
+ return
+
+ if(!COOLDOWN_FINISHED(src, holosign_cooldown))
+ balloon_alert(user, "not ready!")
+ return
+
+ var/target_turf = get_turf(target)
+ var/mob/living/living_target = locate(/mob/living) in target_turf
+
+ if(!living_target || (living_target == user))
+ return
+
+ to_chat(living_target, span_boldnotice("[user] is offering medical assistance; please halt your actions."))
+ new /obj/effect/temp_visual/medical_holosign(target_turf, user) //produce a holographic glow
+ COOLDOWN_START(src, holosign_cooldown, 10 SECONDS)
// see: [/datum/wound/burn/flesh/proc/uv()]
/obj/item/flashlight/pen/paramedic
name = "paramedic penlight"
desc = "A high-powered UV penlight intended to help stave off infection in the field on serious burned patients. Probably really bad to look into."
icon_state = "penlight_surgical"
+ light_color = LIGHT_COLOR_PURPLE
/// Our current UV cooldown
COOLDOWN_DECLARE(uv_cooldown)
/// How long between UV fryings
@@ -355,7 +368,7 @@
w_class = WEIGHT_CLASS_BULKY
flags_1 = CONDUCT_1
custom_materials = null
- on = TRUE
+ start_on = TRUE
// green-shaded desk lamp
/obj/item/flashlight/lamp/green
@@ -403,7 +416,7 @@
. = ..()
if(randomize_fuel)
fuel = rand(25 MINUTES, 35 MINUTES)
- if(on)
+ if(light_on)
attack_verb_continuous = string_list(list("burns", "singes"))
attack_verb_simple = string_list(list("burn", "singe"))
hitsound = 'sound/items/welder.ogg'
@@ -419,15 +432,16 @@
if(!isliving(victim))
return ..()
- if(on && victim.ignite_mob())
+ if(light_on && victim.ignite_mob())
message_admins("[ADMIN_LOOKUPFLW(user)] set [key_name_admin(victim)] on fire with [src] at [AREACOORD(user)]")
user.log_message("set [key_name(victim)] on fire with [src]", LOG_ATTACK)
return ..()
/obj/item/flashlight/flare/toggle_light()
- if(on || !fuel)
+ if(light_on || !fuel)
return FALSE
+ . = ..()
name = "lit [initial(name)]"
attack_verb_continuous = string_list(list("burns", "singes"))
@@ -435,10 +449,10 @@
hitsound = 'sound/items/welder.ogg'
force = on_damage
damtype = BURN
- . = ..()
+
/obj/item/flashlight/flare/proc/turn_off()
- on = FALSE
+ set_light_on(FALSE)
name = initial(name)
attack_verb_continuous = initial(attack_verb_continuous)
attack_verb_simple = initial(attack_verb_simple)
@@ -454,14 +468,14 @@
/obj/item/flashlight/flare/update_brightness()
..()
- inhand_icon_state = "[initial(inhand_icon_state)]" + (on ? "-on" : "")
+ inhand_icon_state = "[initial(inhand_icon_state)]" + (light_on ? "-on" : "")
update_appearance()
/obj/item/flashlight/flare/process(seconds_per_tick)
open_flame(heat)
fuel = max(fuel - seconds_per_tick * (1 SECONDS), 0)
- if(!fuel || !on)
+ if(!fuel || !light_on)
turn_off()
STOP_PROCESSING(SSobj, src)
@@ -474,7 +488,7 @@
if(user)
balloon_alert(user, "out of fuel!")
return NO_FUEL
- if(on)
+ if(light_on)
if(user)
balloon_alert(user, "already lit!")
return ALREADY_LIT
@@ -495,7 +509,7 @@
user.visible_message(span_notice("[user] lights \the [src]."), span_notice("You light \the [initial(src.name)]!"))
/obj/item/flashlight/flare/get_temperature()
- return on * heat
+ return light_on * heat
/obj/item/flashlight/flare/candle
name = "red candle"
@@ -543,8 +557,8 @@
/obj/item/flashlight/flare/candle/update_icon_state()
. = ..()
- icon_state = "candle[current_wax_level][on ? "_lit" : ""]"
- inhand_icon_state = "candle[on ? "_lit" : ""]"
+ icon_state = "candle[current_wax_level][light_on ? "_lit" : ""]"
+ inhand_icon_state = "candle[light_on ? "_lit" : ""]"
/**
* Try to ignite the candle.
@@ -603,7 +617,7 @@
return ..()
/obj/item/flashlight/flare/candle/attack_self(mob/user)
- if(on && (fuel != INFINITY || !can_be_extinguished)) // can't extinguish eternal candles
+ if(light_on && (fuel != INFINITY || !can_be_extinguished)) // can't extinguish eternal candles
turn_off()
user.visible_message(span_notice("[user] snuffs [src]."))
@@ -614,9 +628,9 @@
/obj/item/flashlight/flare/candle/infinite
name = "eternal candle"
fuel = INFINITY
- on = TRUE
randomize_fuel = FALSE
can_be_extinguished = FALSE
+ start_on = TRUE
/obj/item/flashlight/flare/torch
name = "torch"
@@ -697,7 +711,7 @@
return TRUE
/obj/item/flashlight/emp/attack(mob/living/M, mob/living/user)
- if(on && (user.zone_selected in list(BODY_ZONE_PRECISE_EYES, BODY_ZONE_PRECISE_MOUTH))) // call original attack when examining organs
+ if(light_on && (user.zone_selected in list(BODY_ZONE_PRECISE_EYES, BODY_ZONE_PRECISE_MOUTH))) // call original attack when examining organs
..()
return
@@ -763,7 +777,7 @@
STOP_PROCESSING(SSobj, src)
/obj/item/flashlight/glowstick/proc/turn_off()
- on = FALSE
+ set_light_on(FALSE)
update_appearance(UPDATE_ICON)
/obj/item/flashlight/glowstick/update_appearance(updates=ALL)
@@ -771,18 +785,18 @@
if(fuel <= 0)
set_light_on(FALSE)
return
- if(on)
+ if(light_on)
set_light_on(TRUE)
return
/obj/item/flashlight/glowstick/update_icon_state()
+ . = ..()
icon_state = "[base_icon_state][(fuel <= 0) ? "-empty" : ""]"
- inhand_icon_state = "[base_icon_state][((fuel > 0) && on) ? "-on" : ""]"
- return ..()
+ inhand_icon_state = "[base_icon_state][((fuel > 0) && light_on) ? "-on" : ""]"
/obj/item/flashlight/glowstick/update_overlays()
. = ..()
- if(fuel <= 0 && !on)
+ if(fuel <= 0 && !light_on)
return
var/mutable_appearance/glowstick_overlay = mutable_appearance(icon, "glowstick-glow")
@@ -793,7 +807,7 @@
if(fuel <= 0)
balloon_alert(user, "glowstick is spent!")
return
- if(on)
+ if(light_on)
balloon_alert(user, "already lit!")
return
@@ -847,13 +861,13 @@
light_power = 10
alpha = 0
plane = FLOOR_PLANE
- on = TRUE
anchored = TRUE
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
///Boolean that switches when a full color flip ends, so the light can appear in all colors.
var/even_cycle = FALSE
///Base light_range that can be set on Initialize to use in smooth light range expansions and contractions.
var/base_light_range = 4
+ start_on = TRUE
/obj/item/flashlight/spotlight/Initialize(mapload, _light_range, _light_power, _light_color)
. = ..()
@@ -876,6 +890,7 @@
var/dark_light_range = 2.5
///Variable to preserve old lighting behavior in flashlights, to handle darkness.
var/dark_light_power = -3
+ var/on = FALSE
/obj/item/flashlight/flashdark/update_brightness()
. = ..()
diff --git a/code/game/objects/items/devices/laserpointer.dm b/code/game/objects/items/devices/laserpointer.dm
index ccb7cf702c7eb..c56dec6c74fc4 100644
--- a/code/game/objects/items/devices/laserpointer.dm
+++ b/code/game/objects/items/devices/laserpointer.dm
@@ -71,7 +71,7 @@
diode = null
return TRUE
-/obj/item/laser_pointer/tool_act(mob/living/user, obj/item/tool)
+/obj/item/laser_pointer/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
. = ..()
if(isnull(crystal_lens) || !(tool.tool_behaviour == TOOL_WIRECUTTER || tool.tool_behaviour == TOOL_HEMOSTAT))
return
diff --git a/code/game/objects/items/devices/powersink.dm b/code/game/objects/items/devices/powersink.dm
index 6c0cd4017e327..0849c6c2b2638 100644
--- a/code/game/objects/items/devices/powersink.dm
+++ b/code/game/objects/items/devices/powersink.dm
@@ -124,7 +124,11 @@
span_hear("You hear a click."))
message_admins("Power sink activated by [ADMIN_LOOKUPFLW(user)] at [ADMIN_VERBOSEJMP(src)]")
user.log_message("activated a powersink", LOG_GAME)
- notify_ghosts("[user] has activated a power sink!", source = src, header = "Shocking News!")
+ notify_ghosts(
+ "[user] has activated a power sink!",
+ source = src,
+ header = "Shocking News!",
+ )
set_mode(OPERATING)
if(OPERATING)
@@ -188,7 +192,11 @@
if (!warning_given)
warning_given = TRUE
message_admins("Power sink at ([x],[y],[z] - JMP) has reached [ALERT]% of max heat. Explosion imminent.")
- notify_ghosts("[src] is about to reach critical heat capacity!", source = src, header = "Power Sunk")
+ notify_ghosts(
+ "[src] is about to reach critical heat capacity!",
+ source = src,
+ header = "Power Sunk",
+ )
playsound(src, 'sound/effects/screech.ogg', 100, TRUE, TRUE)
if(internal_heat >= max_heat)
diff --git a/code/game/objects/items/devices/pressureplates.dm b/code/game/objects/items/devices/pressureplates.dm
index e8c894d011225..1b6a5ee6db7b8 100644
--- a/code/game/objects/items/devices/pressureplates.dm
+++ b/code/game/objects/items/devices/pressureplates.dm
@@ -91,3 +91,21 @@
active = underfloor_accessibility < UNDERFLOOR_VISIBLE
+/obj/item/pressure_plate/puzzle
+ protected = TRUE
+ anchored = TRUE //this prevents us from being picked up
+ active = TRUE
+ removable_signaller = FALSE
+ /// puzzle id we send if stepped on
+ var/puzzle_id
+ /// queue size must match
+ var/queue_size = 2
+
+/obj/item/pressure_plate/puzzle/Initialize(mapload)
+ . = ..()
+ if(!isnull(puzzle_id))
+ SSqueuelinks.add_to_queue(src, puzzle_id, queue_size)
+
+/obj/item/pressure_plate/puzzle/trigger()
+ can_trigger = FALSE
+ SEND_SIGNAL(src, COMSIG_PUZZLE_COMPLETED)
diff --git a/code/game/objects/items/devices/radio/intercom.dm b/code/game/objects/items/devices/radio/intercom.dm
index 2456f80c01822..ed77524deb572 100644
--- a/code/game/objects/items/devices/radio/intercom.dm
+++ b/code/game/objects/items/devices/radio/intercom.dm
@@ -1,6 +1,6 @@
/obj/item/radio/intercom
name = "station intercom"
- desc = "Talk through this."
+ desc = "A trusty station intercom, ready to spring into action even when the headsets go silent."
icon = 'icons/obj/machines/wallmounts.dmi'
icon_state = "intercom"
anchored = TRUE
@@ -16,12 +16,17 @@
overlay_mic_idle = "intercom_m"
overlay_mic_active = null
+ ///The icon of intercom while its turned off
+ var/icon_off = "intercom-p"
+
/obj/item/radio/intercom/unscrewed
unscrewed = TRUE
/obj/item/radio/intercom/prison
name = "receive-only intercom"
desc = "A station intercom. It looks like it has been modified to not broadcast."
+ icon_state = "intercom_prison"
+ icon_off = "intercom_prison-p"
/obj/item/radio/intercom/prison/Initialize(mapload, ndir, building)
. = ..()
@@ -157,7 +162,7 @@
return
/obj/item/radio/intercom/update_icon_state()
- icon_state = on ? initial(icon_state) : "intercom-p"
+ icon_state = on ? initial(icon_state) : icon_off
return ..()
/**
@@ -193,7 +198,7 @@
pixel_shift = 26
custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 0.75, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 0.25)
-MAPPING_DIRECTIONAL_HELPERS(/obj/item/radio/intercom, 26)
+MAPPING_DIRECTIONAL_HELPERS(/obj/item/radio/intercom, 27)
/obj/item/radio/intercom/chapel
name = "Confessional intercom"
@@ -208,11 +213,12 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/item/radio/intercom, 26)
/obj/item/radio/intercom/command
name = "command intercom"
- desc = "The command team's special extended-frequency intercom. Mostly just used for eavesdropping, gossiping about subordinates, and complaining about the higher-ups."
- icon = 'icons/obj/machines/wallmounts.dmi'
+ desc = "The command's special free-frequency intercom. It's a versatile tool that can be tuned to any frequency, granting you access to channels you're not supposed to be on. Plus, it comes equipped with a built-in voice amplifier for crystal-clear communication."
icon_state = "intercom_command"
freerange = TRUE
+ command = TRUE
+ icon_off = "intercom_command-p"
-MAPPING_DIRECTIONAL_HELPERS(/obj/item/radio/intercom/prison, 26)
-MAPPING_DIRECTIONAL_HELPERS(/obj/item/radio/intercom/chapel, 26)
-MAPPING_DIRECTIONAL_HELPERS(/obj/item/radio/intercom/command, 26)
+MAPPING_DIRECTIONAL_HELPERS(/obj/item/radio/intercom/prison, 27)
+MAPPING_DIRECTIONAL_HELPERS(/obj/item/radio/intercom/chapel, 27)
+MAPPING_DIRECTIONAL_HELPERS(/obj/item/radio/intercom/command, 27)
diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm
index aeb599de32030..6eef9edd5a414 100644
--- a/code/game/objects/items/devices/radio/radio.dm
+++ b/code/game/objects/items/devices/radio/radio.dm
@@ -484,9 +484,9 @@
. = ..()
if(unscrewed)
return
- if(broadcasting)
+ if(broadcasting && overlay_mic_idle)
. += overlay_mic_idle
- if(listening)
+ if(listening && overlay_speaker_idle)
. += overlay_speaker_idle
/obj/item/radio/screwdriver_act(mob/living/user, obj/item/tool)
@@ -531,6 +531,7 @@
subspace_transmission = TRUE
subspace_switchable = TRUE
dog_fashion = null
+ canhear_range = 0
/obj/item/radio/borg/resetChannels()
. = ..()
diff --git a/code/game/objects/items/devices/reverse_bear_trap.dm b/code/game/objects/items/devices/reverse_bear_trap.dm
index b5cbb98557070..8ac8d2a602675 100644
--- a/code/game/objects/items/devices/reverse_bear_trap.dm
+++ b/code/game/objects/items/devices/reverse_bear_trap.dm
@@ -107,7 +107,15 @@
user.dropItemToGround(src)
target.equip_to_slot_if_possible(src, ITEM_SLOT_HEAD)
arm()
- notify_ghosts("[user] put a reverse bear trap on [target]!", source = src, action = NOTIFY_ORBIT, flashwindow = FALSE, ghost_sound = 'sound/machines/beep.ogg', notify_volume = 75, header = "Reverse bear trap armed")
+ notify_ghosts(
+ "[user] put a reverse bear trap on [target]!",
+ source = src,
+ action = NOTIFY_ORBIT,
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ ghost_sound = 'sound/machines/beep.ogg',
+ notify_volume = 75,
+ header = "Reverse bear trap armed",
+ )
/obj/item/reverse_bear_trap/proc/snap()
reset()
diff --git a/code/game/objects/items/dna_probe.dm b/code/game/objects/items/dna_probe.dm
index 4ea89d0a95e76..6c9651a314210 100644
--- a/code/game/objects/items/dna_probe.dm
+++ b/code/game/objects/items/dna_probe.dm
@@ -135,7 +135,7 @@
to_chat(user, span_notice("You pull out the needle from [src] and flip the switch, and start injecting yourself with it."))
if(!do_after(user, CARP_MIX_DNA_TIMER))
return
- var/mob/living/simple_animal/hostile/space_dragon/new_dragon = user.change_mob_type(/mob/living/simple_animal/hostile/space_dragon, location = loc, delete_old_mob = TRUE)
+ var/mob/living/basic/space_dragon/new_dragon = user.change_mob_type(/mob/living/basic/space_dragon, location = loc, delete_old_mob = TRUE)
new_dragon.add_filter("anger_glow", 3, list("type" = "outline", "color" = "#ff330030", "size" = 5))
new_dragon.add_movespeed_modifier(/datum/movespeed_modifier/dragon_rage)
priority_announce("A large organic energy flux has been recorded near of [station_name()], please stand-by.", "Lifesign Alert")
diff --git a/code/game/objects/items/eightball.dm b/code/game/objects/items/eightball.dm
index 737ba7e947c5a..6bac2e7df37dd 100644
--- a/code/game/objects/items/eightball.dm
+++ b/code/game/objects/items/eightball.dm
@@ -158,13 +158,12 @@
// notify ghosts that someone's shaking a haunted eightball
// and inform them of the message, (hopefully a yes/no question)
selected_message = last_message
- notify_ghosts("[user] is shaking [src], hoping to get an answer to \"[selected_message]\"", source=src, enter_link="(Click to help)", action=NOTIFY_ATTACK, header = "Magic eightball")
-
-/obj/item/toy/eightball/haunted/Topic(href, href_list)
- . = ..()
- if(href_list["interact"])
- if(isobserver(usr))
- interact(usr)
+ notify_ghosts(
+ "[user] is shaking [src], hoping to get an answer to \"[selected_message]\"",
+ source = src,
+ action = NOTIFY_PLAY,
+ header = "Magic eightball",
+ )
/obj/item/toy/eightball/haunted/get_answer()
var/top_amount = 0
diff --git a/code/game/objects/items/emags.dm b/code/game/objects/items/emags.dm
index 56da7757c0a70..982d96e2c74cc 100644
--- a/code/game/objects/items/emags.dm
+++ b/code/game/objects/items/emags.dm
@@ -54,7 +54,7 @@
/obj/item/card/emag/Initialize(mapload)
. = ..()
- type_blacklist = list(typesof(/obj/machinery/door/airlock) + typesof(/obj/machinery/door/window/) + typesof(/obj/machinery/door/firedoor) - typesof(/obj/machinery/door/window/tram/)) //list of all typepaths that require a specialized emag to hack.
+ type_blacklist = list(typesof(/obj/machinery/door/airlock) + typesof(/obj/machinery/door/window/) + typesof(/obj/machinery/door/firedoor) - typesof(/obj/machinery/door/airlock/tram)) //list of all typepaths that require a specialized emag to hack.
/obj/item/card/emag/attack()
return
diff --git a/code/game/objects/items/food/lizard.dm b/code/game/objects/items/food/lizard.dm
index e6a7bb7375384..2954b52a9fd06 100644
--- a/code/game/objects/items/food/lizard.dm
+++ b/code/game/objects/items/food/lizard.dm
@@ -810,3 +810,32 @@
tastes = list("sweet bugs" = 1)
foodtypes = MEAT | GORE | BUGS
w_class = WEIGHT_CLASS_SMALL
+
+/obj/item/food/rootbread_peanut_butter_jelly
+ name = "peanut butter and jelly rootwich"
+ desc = "A classic PB&J rootwich, just like the replicant that replaced your mom used to make."
+ icon_state = "peanutbutter-jelly"
+ icon = 'icons/obj/food/lizard.dmi'
+ food_reagents = list(
+ /datum/reagent/consumable/nutriment = 2,
+ /datum/reagent/consumable/nutriment/protein = 4,
+ /datum/reagent/consumable/nutriment/vitamin = 2,
+ )
+ tastes = list("peanut butter" = 1, "jelly" = 1, "rootbread" = 2)
+ foodtypes = FRUIT | NUTS
+ crafting_complexity = FOOD_COMPLEXITY_3
+
+/obj/item/food/rootbread_peanut_butter_banana
+ name = "peanut butter and banana rootwich"
+ desc = "A peanut butter rootwich with banana slices mixed in, a good high protein treat."
+ icon_state = "peanutbutter-banana"
+ icon = 'icons/obj/food/lizard.dmi'
+ food_reagents = list(
+ /datum/reagent/consumable/nutriment = 2,
+ /datum/reagent/consumable/nutriment/protein = 4,
+ /datum/reagent/consumable/banana = 5,
+ /datum/reagent/consumable/nutriment/vitamin = 2,
+ )
+ tastes = list("peanut butter" = 1, "banana" = 1, "rootbread" = 2)
+ foodtypes = FRUIT | NUTS
+ crafting_complexity = FOOD_COMPLEXITY_3
diff --git a/code/game/objects/items/food/meatslab.dm b/code/game/objects/items/food/meatslab.dm
index 483aa73607488..c6b7d8110e7ee 100644
--- a/code/game/objects/items/food/meatslab.dm
+++ b/code/game/objects/items/food/meatslab.dm
@@ -4,6 +4,27 @@
icon = 'icons/obj/food/meat.dmi'
var/subjectname = ""
var/subjectjob = null
+ var/blood_decal_type = /obj/effect/decal/cleanable/blood
+
+/obj/item/food/meat/Initialize(mapload)
+ . = ..()
+
+ if(!blood_decal_type)
+ return
+
+ AddComponent(
+ /datum/component/blood_walk,\
+ blood_type = blood_decal_type,\
+ blood_spawn_chance = 45,\
+ max_blood = custom_materials[custom_materials[1]],\
+ )
+
+ AddComponent(
+ /datum/component/bloody_spreader,\
+ blood_left = custom_materials[custom_materials[1]],\
+ blood_dna = list("meaty DNA" = "MT-"),\
+ diseases = null,\
+ )
/obj/item/food/meat/slab
name = "meat"
@@ -55,6 +76,7 @@
tastes = list("slime" = 1, "jelly" = 1)
foodtypes = MEAT | RAW | TOXIC
venue_value = FOOD_MEAT_MUTANT_RARE
+ blood_decal_type = null
/obj/item/food/meat/slab/human/mutant/golem
icon_state = "golemmeat"
@@ -66,6 +88,7 @@
tastes = list("rock" = 1)
foodtypes = MEAT | RAW | GROSS
venue_value = FOOD_MEAT_MUTANT_RARE
+ blood_decal_type = null
/obj/item/food/meat/slab/human/mutant/golem/adamantine
icon_state = "agolemmeat"
@@ -89,6 +112,7 @@
tastes = list("salad" = 1, "wood" = 1)
foodtypes = VEGETABLES
venue_value = FOOD_MEAT_MUTANT_RARE
+ blood_decal_type = /obj/effect/decal/cleanable/food/plant_smudge
/obj/item/food/meat/slab/human/mutant/shadow
icon_state = "shadowmeat"
@@ -107,6 +131,7 @@
tastes = list("maggots" = 1, "the inside of a reactor" = 1)
foodtypes = MEAT | RAW | GROSS | BUGS | GORE
venue_value = FOOD_MEAT_MUTANT
+ blood_decal_type = /obj/effect/decal/cleanable/insectguts
/obj/item/food/meat/slab/human/mutant/moth
icon_state = "mothmeat"
@@ -122,6 +147,7 @@
tastes = list("bone" = 1)
foodtypes = GROSS | GORE
venue_value = FOOD_MEAT_MUTANT_RARE
+ blood_decal_type = null
/obj/item/food/meat/slab/human/mutant/skeleton/make_processable()
return //skeletons dont have cutlets
@@ -140,6 +166,7 @@
tastes = list("pure electricity" = 2, "meat" = 1)
foodtypes = RAW | MEAT | TOXIC | GORE
venue_value = FOOD_MEAT_MUTANT
+ blood_decal_type = null
////////////////////////////////////// OTHER MEATS ////////////////////////////////////////////////////////
@@ -174,6 +201,7 @@
name = "bug meat"
icon_state = "spidermeat"
foodtypes = RAW | MEAT | BUGS
+ blood_decal_type = /obj/effect/decal/cleanable/insectguts
/obj/item/food/meat/slab/mouse
name = "mouse meat"
@@ -219,6 +247,7 @@
food_reagents = list(/datum/reagent/consumable/nutriment = 2)
tastes = list("tomato" = 1)
foodtypes = FRUIT
+ blood_decal_type = /obj/effect/decal/cleanable/food/tomato_smudge
/obj/item/food/meat/slab/killertomato/make_grillable()
AddComponent(/datum/component/grillable, /obj/item/food/meat/steak/killertomato, rand(70 SECONDS, 85 SECONDS), TRUE, TRUE)
@@ -260,6 +289,7 @@
bite_consumption = 4
tastes = list("meat" = 1, "acid" = 1)
foodtypes = RAW | MEAT
+ blood_decal_type = /obj/effect/decal/cleanable/xenoblood
/obj/item/food/meat/slab/xeno/make_processable()
AddElement(/datum/element/processable, TOOL_KNIFE, /obj/item/food/meat/rawcutlet/xeno, 3, 3 SECONDS, table_required = TRUE, screentip_verb = "Cut")
@@ -278,6 +308,7 @@
)
tastes = list("cobwebs" = 1)
foodtypes = RAW | MEAT | TOXIC
+ blood_decal_type = /obj/effect/decal/cleanable/insectguts
/obj/item/food/meat/slab/spider/make_processable()
AddElement(/datum/element/processable, TOOL_KNIFE, /obj/item/food/meat/rawcutlet/spider, 3, 3 SECONDS, table_required = TRUE, screentip_verb = "Cut")
@@ -348,6 +379,7 @@
tastes = list("bacon" = 1)
foodtypes = MEAT | BREAKFAST
crafting_complexity = FOOD_COMPLEXITY_1
+ blood_decal_type = null
/obj/item/food/meat/slab/gondola
name = "gondola meat"
@@ -410,6 +442,7 @@
tastes = list("crab" = 1)
foodtypes = SEAFOOD
crafting_complexity = FOOD_COMPLEXITY_1
+ blood_decal_type = null
/obj/item/food/meat/slab/chicken
name = "chicken meat"
@@ -468,6 +501,7 @@
foodtypes = MEAT
tastes = list("meat" = 1)
crafting_complexity = FOOD_COMPLEXITY_1
+ blood_decal_type = null
/obj/item/food/meat/steak/Initialize(mapload)
. = ..()
@@ -618,6 +652,7 @@
name = "raw killer tomato cutlet"
tastes = list("tomato" = 1)
foodtypes = FRUIT
+ blood_decal_type = /obj/effect/decal/cleanable/food/tomato_smudge
/obj/item/food/meat/rawcutlet/killertomato/make_grillable()
AddComponent(/datum/component/grillable, /obj/item/food/meat/cutlet/killertomato, rand(35 SECONDS, 50 SECONDS), TRUE, TRUE)
@@ -636,6 +671,7 @@
/obj/item/food/meat/rawcutlet/xeno
name = "raw xeno cutlet"
tastes = list("meat" = 1, "acid" = 1)
+ blood_decal_type = /obj/effect/decal/cleanable/xenoblood
/obj/item/food/meat/rawcutlet/xeno/make_grillable()
AddComponent(/datum/component/grillable, /obj/item/food/meat/cutlet/xeno, rand(35 SECONDS, 50 SECONDS), TRUE, TRUE)
@@ -643,6 +679,7 @@
/obj/item/food/meat/rawcutlet/spider
name = "raw spider cutlet"
tastes = list("cobwebs" = 1)
+ blood_decal_type = /obj/effect/decal/cleanable/insectguts
/obj/item/food/meat/rawcutlet/spider/make_grillable()
AddComponent(/datum/component/grillable, /obj/item/food/meat/cutlet/spider, rand(35 SECONDS, 50 SECONDS), TRUE, TRUE)
@@ -683,6 +720,7 @@
tastes = list("meat" = 1)
foodtypes = MEAT
crafting_complexity = FOOD_COMPLEXITY_1
+ blood_decal_type = null
/obj/item/food/meat/cutlet/Initialize(mapload)
. = ..()
diff --git a/code/game/objects/items/food/misc.dm b/code/game/objects/items/food/misc.dm
index 8d7e75edcedef..25be1aabfeaec 100644
--- a/code/game/objects/items/food/misc.dm
+++ b/code/game/objects/items/food/misc.dm
@@ -83,7 +83,7 @@
)
tastes = list("caramel" = 2, "popcorn" = 1)
foodtypes = JUNKFOOD | SUGAR
- trash_type = /obj/item/trash/popcorn/
+ trash_type = /obj/item/trash/popcorn
crafting_complexity = FOOD_COMPLEXITY_1
/obj/item/food/soydope
diff --git a/code/game/objects/items/food/monkeycube.dm b/code/game/objects/items/food/monkeycube.dm
index ffc9b63c62f09..a364a251a8344 100644
--- a/code/game/objects/items/food/monkeycube.dm
+++ b/code/game/objects/items/food/monkeycube.dm
@@ -8,20 +8,47 @@
foodtypes = MEAT | SUGAR
food_flags = FOOD_FINGER_FOOD
w_class = WEIGHT_CLASS_TINY
- var/faction
+ /// Mob typepath to spawn when expanding
var/spawned_mob = /mob/living/carbon/human/species/monkey
+ /// Whether we've been wetted and are expanding
+ var/expanding = FALSE
+
+/obj/item/food/monkeycube/attempt_pickup(mob/user)
+ if(expanding)
+ return FALSE
+ return ..()
/obj/item/food/monkeycube/proc/Expand()
+ if(expanding)
+ return
+
+ expanding = TRUE
+
+ if(ismob(loc))
+ var/mob/holder = loc
+ holder.dropItemToGround(src)
+
var/mob/spammer = get_mob_by_key(fingerprintslast)
- var/mob/living/bananas = new spawned_mob(drop_location(), TRUE, spammer)
- if(faction)
- bananas.faction = faction
+ var/mob/living/bananas = new spawned_mob(drop_location(), TRUE, spammer) // funny that we pass monkey init args to non-monkey mobs, that's totally a future issue
if (!QDELETED(bananas))
+ if(faction)
+ bananas.faction = faction
+
visible_message(span_notice("[src] expands!"))
bananas.log_message("spawned via [src], Last attached mob: [key_name(spammer)].", LOG_ATTACK)
+
+ var/alpha_to = bananas.alpha
+ var/matrix/scale_to = matrix(bananas.transform)
+ bananas.alpha = 0
+ bananas.transform = bananas.transform.Scale(0.1)
+ animate(bananas, 0.5 SECONDS, alpha = alpha_to, transform = scale_to, easing = QUAD_EASING|EASE_OUT)
+
else if (!spammer) // Visible message in case there are no fingerprints
visible_message(span_notice("[src] fails to expand!"))
- qdel(src)
+ return
+
+ animate(src, 0.4 SECONDS, alpha = 0, transform = transform.Scale(0), easing = QUAD_EASING|EASE_IN)
+ QDEL_IN(src, 0.5 SECONDS)
/obj/item/food/monkeycube/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] is putting [src] in [user.p_their()] mouth! It looks like [user.p_theyre()] trying to commit suicide!"))
diff --git a/code/game/objects/items/food/pie.dm b/code/game/objects/items/food/pie.dm
index cd4d0c03b1a83..46674fb735e94 100644
--- a/code/game/objects/items/food/pie.dm
+++ b/code/game/objects/items/food/pie.dm
@@ -122,6 +122,15 @@
tastes = list("pie" = 1, "meat" = 1)
foodtypes = GRAIN | MEAT
venue_value = FOOD_PRICE_NORMAL
+ slice_type = /obj/item/food/pieslice/meatpie
+ crafting_complexity = FOOD_COMPLEXITY_3
+
+/obj/item/food/pieslice/meatpie
+ name = "meat-pie slice"
+ desc = "Oh nice, meat pie!"
+ icon_state = "meatpie_slice"
+ tastes = list("pie" = 1, "meat" = 1)
+ foodtypes = GRAIN | MEAT
crafting_complexity = FOOD_COMPLEXITY_3
/obj/item/food/pie/tofupie
@@ -135,6 +144,15 @@
)
tastes = list("pie" = 1, "tofu" = 1)
foodtypes = GRAIN | VEGETABLES
+ slice_type = /obj/item/food/pieslice/tofupie
+ crafting_complexity = FOOD_COMPLEXITY_3
+
+/obj/item/food/pieslice/tofupie
+ name = "tofu-pie slice"
+ desc = "Oh nice, meat pie- WAIT A MINUTE!!"
+ icon_state = "meatpie_slice"
+ tastes = list("pie" = 1, "disappointment" = 1, "tofu" = 1)
+ foodtypes = GRAIN | VEGETABLES
crafting_complexity = FOOD_COMPLEXITY_3
/obj/item/food/pie/amanita_pie
@@ -187,6 +205,16 @@
)
tastes = list("pie" = 1, "meat" = 1, "acid" = 1)
foodtypes = GRAIN | MEAT
+ slice_type = /obj/item/food/pieslice/xemeatpie
+ crafting_complexity = FOOD_COMPLEXITY_3
+
+/obj/item/food/pieslice/xemeatpie
+ name = "xeno-pie slice"
+ desc = "Oh god... Is that still moving?"
+ icon_state = "xenopie_slice"
+ tastes = list("pie" = 1, "acid" = 1, "meat" = 1)
+ foodtypes = GRAIN | MEAT
+ crafting_complexity = FOOD_COMPLEXITY_3
/obj/item/food/pie/applepie
name = "apple pie"
@@ -198,6 +226,17 @@
)
tastes = list("pie" = 1, "apple" = 1)
foodtypes = GRAIN | FRUIT | SUGAR
+ slice_type = /obj/item/food/pieslice/apple
+ crafting_complexity = FOOD_COMPLEXITY_3
+
+/obj/item/food/pieslice/apple
+ name = "apple pie slice"
+ desc = "A slice of comfy apple pie, warm autumn memories ahead."
+ icon_state = "applepie_slice"
+ tastes = list("pie" = 1, "apples" = 1)
+ foodtypes = GRAIN | FRUIT | SUGAR
+ crafting_complexity = FOOD_COMPLEXITY_3
+
/obj/item/food/pie/cherrypie
name = "cherry pie"
@@ -209,6 +248,15 @@
)
tastes = list("pie" = 7, "Nicole Paige Brooks" = 2)
foodtypes = GRAIN | FRUIT | SUGAR
+ slice_type = /obj/item/food/pieslice/cherry
+ crafting_complexity = FOOD_COMPLEXITY_3
+
+/obj/item/food/pieslice/cherry
+ name = "cherry pie slice"
+ desc = "A slice of delicious cherry pie, I hope it's morellos!"
+ icon_state = "cherrypie_slice"
+ tastes = list("pie" = 1, "apples" = 1)
+ foodtypes = GRAIN | FRUIT | SUGAR
crafting_complexity = FOOD_COMPLEXITY_3
/obj/item/food/pie/pumpkinpie
@@ -344,6 +392,15 @@
)
tastes = list("mint" = 1, "pie" = 1)
foodtypes = GRAIN | FRUIT | SUGAR
+ slice_type = /obj/item/food/pieslice/frostypie
+ crafting_complexity = FOOD_COMPLEXITY_3
+
+/obj/item/food/pieslice/frostypie
+ name = "frosty pie slice"
+ desc = "Tasty blue, like my favourite crayon!"
+ icon_state = "frostypie_slice"
+ tastes = list("pie" = 1, "mint" = 1)
+ foodtypes = GRAIN | FRUIT | SUGAR
crafting_complexity = FOOD_COMPLEXITY_3
/obj/item/food/pie/baklava
diff --git a/code/game/objects/items/food/sandwichtoast.dm b/code/game/objects/items/food/sandwichtoast.dm
index 8b91b04621e00..c6488f67a1ed5 100644
--- a/code/game/objects/items/food/sandwichtoast.dm
+++ b/code/game/objects/items/food/sandwichtoast.dm
@@ -284,7 +284,7 @@
// Closest thing to a mullet we have
if(consumer.hairstyle == "Gelled Back" && istype(consumer.get_item_by_slot(ITEM_SLOT_ICLOTHING), /obj/item/clothing/under/rank/civilian/cookjorts))
return FOOD_LIKED
- return FOOD_DISLIKED
+ return FOOD_ALLERGIC
/**
* Callback to be used with the edible component.
diff --git a/code/game/objects/items/food/snacks.dm b/code/game/objects/items/food/snacks.dm
index 541c36b115060..1c3cb7736cec0 100644
--- a/code/game/objects/items/food/snacks.dm
+++ b/code/game/objects/items/food/snacks.dm
@@ -4,7 +4,7 @@
/obj/item/food/candy
name = "candy"
- desc = "Nougat love it or hate it."
+ desc = "It's nougat, love it or hate it."
icon_state = "candy"
trash_type = /obj/item/trash/candy
food_reagents = list(
diff --git a/code/game/objects/items/frog_statue.dm b/code/game/objects/items/frog_statue.dm
new file mode 100644
index 0000000000000..4d1bf9b6aa595
--- /dev/null
+++ b/code/game/objects/items/frog_statue.dm
@@ -0,0 +1,165 @@
+#define STATUE_FILTER "statue_filter"
+#define FILTER_COLOR "#34b347"
+#define RECALL_DURATION 3 SECONDS
+#define MINIMUM_COLOR_VALUE 60
+
+/obj/item/frog_statue
+ name = "frog statue"
+ desc = "Are they really comfortable living in this thing?"
+ icon = 'icons/obj/weapons/guns/magic.dmi'
+ icon_state = "frog_statue"
+ item_flags = NOBLUDGEON
+ ///our pet frog
+ var/mob/living/contained_frog
+ ///the summon cooldown
+ COOLDOWN_DECLARE(summon_cooldown)
+
+/obj/item/frog_statue/attack_self(mob/user)
+ . = ..()
+
+ if(.)
+ return TRUE
+
+ if(!COOLDOWN_FINISHED(src, summon_cooldown))
+ user.balloon_alert(user, "recharging!")
+ return TRUE
+
+ COOLDOWN_START(src, summon_cooldown, 30 SECONDS)
+ if(isnull(contained_frog))
+ user.balloon_alert(user, "no frog linked!")
+ return TRUE
+ if(contained_frog.loc == src)
+ release_frog(user)
+ return TRUE
+ recall_frog(user)
+ return TRUE
+
+/obj/item/frog_statue/examine(mob/user)
+ . = ..()
+ if(!IS_WIZARD(user))
+ return
+ if(isnull(contained_frog))
+ . += span_notice("There are currently no frogs linked to this statue!")
+ else
+ . += span_notice("Using it will [contained_frog in src ? "release" : "recall"] the beast!")
+
+///resummon the frog into its home
+/obj/item/frog_statue/proc/recall_frog(mob/user)
+ playsound(src, 'sound/items/frog_statue_release.ogg', 20)
+ user.Beam(contained_frog, icon_state = "lichbeam", time = RECALL_DURATION)
+ animate(contained_frog, transform = matrix().Scale(0.3, 0.3), time = RECALL_DURATION)
+ addtimer(CALLBACK(contained_frog, TYPE_PROC_REF(/atom/movable, forceMove), src), RECALL_DURATION)
+
+///release the frog to wreak havoc
+/obj/item/frog_statue/proc/release_frog(mob/user)
+ var/list/possible_turfs = list()
+ for(var/turf/possible_turf in oview(2, user))
+ if(possible_turf.is_blocked_turf() || isopenspaceturf(possible_turf))
+ continue
+ possible_turfs += possible_turf
+ playsound(src, 'sound/items/frog_statue_release.ogg', 50, TRUE)
+ var/turf/final_turf = length(possible_turfs) ? pick(possible_turfs) : get_turf(src)
+ user.Beam(final_turf, icon_state = "lichbeam", time = RECALL_DURATION)
+ contained_frog.forceMove(final_turf)
+ animate(contained_frog, transform = matrix(), time = RECALL_DURATION)
+ REMOVE_TRAIT(contained_frog, TRAIT_AI_PAUSED, MAGIC_TRAIT)
+
+///set this frog as our inhabitor
+/obj/item/frog_statue/proc/set_new_frog(mob/living/frog)
+ frog.transform = frog.transform.Scale(0.3, 0.3)
+ contained_frog = frog
+ animate_filter()
+ RegisterSignal(frog, COMSIG_QDELETING, PROC_REF(render_obsolete))
+
+/// we have lost our frog, let out a scream!
+/obj/item/frog_statue/proc/render_obsolete(datum/source)
+ SIGNAL_HANDLER
+
+ contained_frog = null
+ playsound(src, 'sound/magic/demon_dies.ogg', 50, TRUE)
+ UnregisterSignal(source, COMSIG_QDELETING)
+
+/obj/item/frog_statue/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ . = ..()
+ if(arrived != contained_frog)
+ return
+ animate_filter()
+ ADD_TRAIT(contained_frog, TRAIT_AI_PAUSED, MAGIC_TRAIT)
+ if(contained_frog.health < contained_frog.maxHealth)
+ START_PROCESSING(SSobj, src)
+
+/obj/item/frog_statue/process(seconds_per_tick)
+ if(isnull(contained_frog))
+ return
+ if(contained_frog.health == contained_frog.maxHealth)
+ STOP_PROCESSING(SSobj, src)
+ return
+ if(contained_frog.stat == DEAD)
+ contained_frog.revive()
+ contained_frog.adjustBruteLoss(-5)
+
+/obj/item/frog_statue/proc/animate_filter(mob/living/frog)
+ add_filter(STATUE_FILTER, 2, list("type" = "outline", "color" = FILTER_COLOR, "size" = 1))
+ var/filter = get_filter(STATUE_FILTER)
+ animate(filter, alpha = 230, time = 2 SECONDS, loop = -1)
+ animate(alpha = 30, time = 0.5 SECONDS)
+
+/obj/item/frog_statue/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(gone != contained_frog)
+ return
+ clear_filters()
+
+/obj/item/frog_contract
+ name = "frog contract"
+ desc = "Create a pact with an elder frog! This great beast will be your mount, protector, but most importantly your friend."
+ icon = 'icons/obj/scrolls.dmi'
+ icon_state = "scroll"
+
+/obj/item/frog_contract/attack_self(mob/user)
+ . = ..()
+ if(.)
+ return TRUE
+ create_frog(user)
+ return TRUE
+
+///customize our own frog and trap it into the statue
+/obj/item/frog_contract/proc/create_frog(mob/user)
+ var/obj/item/frog_statue/statue = new(null)
+ var/mob/living/basic/leaper/new_frog = new(statue)
+ statue.set_new_frog(new_frog)
+ new_frog.befriend(user)
+ ADD_TRAIT(new_frog, TRAIT_AI_PAUSED, MAGIC_TRAIT)
+ select_frog_name(user, new_frog)
+ select_frog_color(user, new_frog)
+ user.put_in_hands(statue)
+ qdel(src)
+
+
+
+/obj/item/frog_contract/proc/select_frog_name(mob/user, mob/new_frog)
+ var/frog_name = sanitize_name(tgui_input_text(user, "Choose your frog's name!", "Name pet toad", "leaper", MAX_NAME_LEN), allow_numbers = TRUE)
+ if(!frog_name)
+ to_chat(user, span_warning("Please enter a valid name."))
+ select_frog_name(user, new_frog)
+ return
+ new_frog.name = frog_name
+
+/obj/item/frog_contract/proc/select_frog_color(mob/user, mob/living/basic/leaper/new_frog)
+ var/frog_color = input(user, "Select your frog's color!" , "Pet toad color", COLOR_GREEN) as color|null
+ if(isnull(frog_color))
+ to_chat(user, span_warning("Please choose a valid color."))
+ select_frog_color(user, new_frog)
+ return
+ var/temp_hsv = RGBtoHSV(frog_color)
+ if(ReadHSV(temp_hsv)[3] < MINIMUM_COLOR_VALUE)
+ to_chat(user, span_danger("This color is too dark!"))
+ select_frog_color(user, new_frog)
+ return
+ new_frog.set_color_overlay(frog_color)
+
+
+#undef STATUE_FILTER
+#undef FILTER_COLOR
+#undef RECALL_DURATION
+#undef MINIMUM_COLOR_VALUE
diff --git a/code/game/objects/items/granters/crafting/_crafting_granter.dm b/code/game/objects/items/granters/crafting/_crafting_granter.dm
index 5b5ec82e42a35..d43b77e035c27 100644
--- a/code/game/objects/items/granters/crafting/_crafting_granter.dm
+++ b/code/game/objects/items/granters/crafting/_crafting_granter.dm
@@ -8,7 +8,7 @@
return
for(var/crafting_recipe_type in crafting_recipe_types)
user.mind.teach_crafting_recipe(crafting_recipe_type)
- var/datum/crafting_recipe/recipe = locate(crafting_recipe_type) in GLOB.crafting_recipes
+ var/datum/crafting_recipe/recipe = locate(crafting_recipe_type) in GLOB.crafting_recipes + GLOB.cooking_recipes
to_chat(user, span_notice("You learned how to make [recipe.name]."))
/obj/item/book/granter/crafting_recipe/dusting
diff --git a/code/game/objects/items/grenades/flashbang.dm b/code/game/objects/items/grenades/flashbang.dm
index a273430172fe0..98e7a4bab796e 100644
--- a/code/game/objects/items/grenades/flashbang.dm
+++ b/code/game/objects/items/grenades/flashbang.dm
@@ -111,7 +111,7 @@
living_mob.Paralyze(20)
living_mob.Knockdown(200)
living_mob.soundbang_act(1, 200, 10, 15)
- if(living_mob.apply_damages(10, 10))
+ if(living_mob.apply_damages(brute = 10, burn = 10))
to_chat(living_mob, span_userdanger("The blast from \the [src] bruises and burns you!"))
// only checking if they're on top of the tile, cause being one tile over will be its own punishment
diff --git a/code/game/objects/items/grenades/plastic.dm b/code/game/objects/items/grenades/plastic.dm
index d5f3b0dec9d43..998057bce2ade 100644
--- a/code/game/objects/items/grenades/plastic.dm
+++ b/code/game/objects/items/grenades/plastic.dm
@@ -119,7 +119,13 @@
message_admins("[ADMIN_LOOKUPFLW(user)] planted [name] on [target.name] at [ADMIN_VERBOSEJMP(target)] with [det_time] second fuse")
user.log_message("planted [name] on [target.name] with a [det_time] second fuse.", LOG_ATTACK)
- notify_ghosts("[user] has planted \a [src] on [target] with a [det_time] second fuse!", source = bomb_target, action = (isturf(target) ? NOTIFY_JUMP : NOTIFY_ORBIT), flashwindow = FALSE, header = "Explosive Planted")
+ notify_ghosts(
+ "[user] has planted \a [src] on [target] with a [det_time] second fuse!",
+ source = bomb_target,
+ action = (isturf(target) ? NOTIFY_JUMP : NOTIFY_ORBIT),
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ header = "Explosive Planted",
+ )
moveToNullspace() //Yep
diff --git a/code/game/objects/items/handcuffs.dm b/code/game/objects/items/handcuffs.dm
index daee6043682df..5472f880755ed 100644
--- a/code/game/objects/items/handcuffs.dm
+++ b/code/game/objects/items/handcuffs.dm
@@ -72,7 +72,8 @@
if(!istype(C))
return
- SEND_SIGNAL(C, COMSIG_CARBON_CUFF_ATTEMPTED, user)
+ if(SEND_SIGNAL(C, COMSIG_CARBON_CUFF_ATTEMPTED, user) & COMSIG_CARBON_CUFF_PREVENT)
+ return
if(iscarbon(user) && (HAS_TRAIT(user, TRAIT_CLUMSY) && prob(50))) //Clumsy people have a 50% chance to handcuff themselves instead of their target.
to_chat(user, span_warning("Uh... how do those things work?!"))
diff --git a/code/game/objects/items/his_grace.dm b/code/game/objects/items/his_grace.dm
index 12c06e6916ac0..fe0e12d4d50d5 100644
--- a/code/game/objects/items/his_grace.dm
+++ b/code/game/objects/items/his_grace.dm
@@ -149,7 +149,12 @@
gender = MALE
adjust_bloodthirst(1)
force_bonus = HIS_GRACE_FORCE_BONUS * LAZYLEN(contents)
- notify_ghosts("[user] has awoken His Grace!", source = src, action = NOTIFY_ORBIT, header = "All Hail His Grace!")
+ notify_ghosts(
+ "[user] has awoken His Grace!",
+ source = src,
+ action = NOTIFY_ORBIT,
+ header = "All Hail His Grace!",
+ )
playsound(user, 'sound/effects/pope_entry.ogg', 100)
update_appearance()
move_gracefully()
diff --git a/code/game/objects/items/holosign_creator.dm b/code/game/objects/items/holosign_creator.dm
index 3f8bb3c41deff..f491321090a8c 100644
--- a/code/game/objects/items/holosign_creator.dm
+++ b/code/game/objects/items/holosign_creator.dm
@@ -15,13 +15,16 @@
item_flags = NOBLUDGEON
var/list/signs
var/max_signs = 10
- var/creation_time = 0 //time to create a holosign in deciseconds.
+ //time to create a holosign in deciseconds.
+ var/creation_time = 0
+ //holosign image that is projected
var/holosign_type = /obj/structure/holosign/wetsign
var/holocreator_busy = FALSE //to prevent placing multiple holo barriers at once
/obj/item/holosign_creator/Initialize(mapload)
. = ..()
AddElement(/datum/element/openspace_item_click_handler)
+ RegisterSignal(src, COMSIG_OBJ_PAINTED, TYPE_PROC_REF(/obj/item/holosign_creator, on_color_change))
/obj/item/holosign_creator/handle_openspace_click(turf/target, mob/user, proximity_flag, click_parameters)
afterattack(target, user, proximity_flag, click_parameters)
@@ -64,6 +67,9 @@
if(target_turf.is_blocked_turf(TRUE)) //don't try to sneak dense stuff on our tile during the wait.
return .
target_holosign = new holosign_type(get_turf(target), src)
+ target_holosign.add_hiddenprint(user)
+ if(color)
+ target_holosign.color = color
return .
/obj/item/holosign_creator/attack(mob/living/carbon/human/M, mob/user)
@@ -71,16 +77,24 @@
/obj/item/holosign_creator/attack_self(mob/user)
if(LAZYLEN(signs))
- for(var/H in signs)
- qdel(H)
+ for(var/obj/structure/holosign/hologram as anything in signs)
+ qdel(hologram)
balloon_alert(user, "holograms cleared")
/obj/item/holosign_creator/Destroy()
. = ..()
if(LAZYLEN(signs))
- for(var/H in signs)
- qdel(H)
+ for(var/obj/structure/holosign/hologram as anything in signs)
+ qdel(hologram)
+/obj/item/holosign_creator/proc/on_color_change(obj/item/holosign_creator, mob/user, obj/item/toy/crayon/spraycan/spraycan, is_dark_color)
+ SIGNAL_HANDLER
+ if(!spraycan.actually_paints)
+ return
+
+ if(LAZYLEN(signs))
+ for(var/obj/structure/holosign/hologram as anything in signs)
+ hologram.color = color
/obj/item/holosign_creator/janibarrier
name = "custodial holobarrier projector"
@@ -127,28 +141,28 @@
creation_time = 1.5 SECONDS
max_signs = 9
holosign_type = /obj/structure/holosign/barrier/cyborg
- var/shock = 0
+ var/shock = FALSE
/obj/item/holosign_creator/cyborg/attack_self(mob/user)
if(iscyborg(user))
- var/mob/living/silicon/robot/R = user
+ var/mob/living/silicon/robot/borg = user
if(shock)
to_chat(user, span_notice("You clear all active holograms, and reset your projector to normal."))
holosign_type = /obj/structure/holosign/barrier/cyborg
- creation_time = 5
- for(var/sign in signs)
- qdel(sign)
- shock = 0
+ creation_time = 0.5 SECONDS
+ for(var/obj/structure/holosign/hologram as anything in signs)
+ qdel(hologram)
+ shock = FALSE
return
- if(R.emagged && !shock)
+ if(borg.emagged && !shock)
to_chat(user, span_warning("You clear all active holograms, and overload your energy projector!"))
holosign_type = /obj/structure/holosign/barrier/cyborg/hacked
- creation_time = 30
- for(var/sign in signs)
- qdel(sign)
- shock = 1
+ creation_time = 3 SECONDS
+ for(var/obj/structure/holosign/hologram as anything in signs)
+ qdel(hologram)
+ shock = TRUE
return
- for(var/sign in signs)
- qdel(sign)
+ for(var/obj/structure/holosign/hologram as anything in signs)
+ qdel(hologram)
balloon_alert(user, "holograms cleared")
diff --git a/code/game/objects/items/hot_potato.dm b/code/game/objects/items/hot_potato.dm
index 1a801b0dcda5b..797c24aaf4cc6 100644
--- a/code/game/objects/items/hot_potato.dm
+++ b/code/game/objects/items/hot_potato.dm
@@ -150,7 +150,12 @@
log_bomber(null, null, src, "was primed for detonation (Timer:[delay],Explosive:[detonate_explosion],Range:[detonate_dev_range]/[detonate_heavy_range]/[detonate_light_range]/[detonate_fire_range])")
active = TRUE
if(detonate_explosion) //doesn't send a notification unless it's a genuine, exploding hot potato.
- notify_ghosts("[user] has primed a Hot Potato!", source = src, action = NOTIFY_ORBIT, header = "Hot Hot Hot!")
+ notify_ghosts(
+ "[user] has primed a Hot Potato!",
+ source = src,
+ action = NOTIFY_ORBIT,
+ header = "Hot Hot Hot!",
+ )
/obj/item/hot_potato/proc/deactivate()
update_appearance()
diff --git a/code/game/objects/items/implants/implant_explosive.dm b/code/game/objects/items/implants/implant_explosive.dm
index c9f961b594e26..0661db5dbe012 100644
--- a/code/game/objects/items/implants/implant_explosive.dm
+++ b/code/game/objects/items/implants/implant_explosive.dm
@@ -125,10 +125,10 @@
"[imp_in] is about to detonate their explosive implant!",
source = src,
action = NOTIFY_ORBIT,
- flashwindow = FALSE,
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
ghost_sound = 'sound/machines/warning-buzzer.ogg',
header = "Tick Tick Tick...",
- notify_volume = 75
+ notify_volume = 75,
)
playsound(loc, 'sound/items/timer.ogg', 30, FALSE)
diff --git a/code/game/objects/items/melee/misc.dm b/code/game/objects/items/melee/misc.dm
index f24efb7d26380..67328c62ce290 100644
--- a/code/game/objects/items/melee/misc.dm
+++ b/code/game/objects/items/melee/misc.dm
@@ -235,6 +235,7 @@
shard.countdown = null
START_PROCESSING(SSobj, src)
visible_message(span_warning("[src] appears, balanced ever so perfectly on its hilt. This isn't ominous at all."))
+ RegisterSignal(src, COMSIG_ATOM_PRE_BULLET_ACT, PROC_REF(eat_bullets))
/obj/item/melee/supermatter_sword/process()
if(balanced || throwing || ismob(src.loc) || isnull(src.loc))
@@ -283,11 +284,16 @@
consume_everything()
return TRUE
-/obj/item/melee/supermatter_sword/bullet_act(obj/projectile/projectile)
- visible_message(span_danger("[projectile] smacks into [src] and rapidly flashes to ash."),\
- span_hear("You hear a loud crack as you are washed with a wave of heat."))
- consume_everything(projectile)
- return BULLET_ACT_HIT
+/obj/item/melee/supermatter_sword/proc/eat_bullets(datum/source, obj/projectile/hitting_projectile)
+ SIGNAL_HANDLER
+
+ visible_message(
+ span_danger("[hitting_projectile] smacks into [source] and rapidly flashes to ash."),
+ null,
+ span_hear("You hear a loud crack as you are washed with a wave of heat."),
+ )
+ consume_everything(hitting_projectile)
+ return COMPONENT_BULLET_BLOCKED
/obj/item/melee/supermatter_sword/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] touches [src]'s blade. It looks like [user.p_theyre()] tired of waiting for the radiation to kill [user.p_them()]!"))
diff --git a/code/game/objects/items/pillow.dm b/code/game/objects/items/pillow.dm
index ea7463e210fbf..5364905bdc96e 100644
--- a/code/game/objects/items/pillow.dm
+++ b/code/game/objects/items/pillow.dm
@@ -32,6 +32,15 @@
force_wielded = 10, \
)
+ var/static/list/slapcraft_recipe_list = list(\
+ /datum/crafting_recipe/pillow_suit, /datum/crafting_recipe/pillow_hood,\
+ )
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/pillow/Destroy(force)
. = ..()
QDEL_NULL(pillow_trophy)
diff --git a/code/game/objects/items/puzzle_pieces.dm b/code/game/objects/items/puzzle_pieces.dm
index 9bf33e36f2fe9..8eba5081d2266 100644
--- a/code/game/objects/items/puzzle_pieces.dm
+++ b/code/game/objects/items/puzzle_pieces.dm
@@ -1,7 +1,8 @@
-//Every time you got lost looking for keycards, incriment: 1
+//Every time you got lost looking for keycards, increment: 2
+
+//**************
+//*****Keys*****
//**************
-//*****Keys*******************
-//************** ** **
/obj/item/keycard
name = "security keycard"
desc = "This feels like it belongs to a door."
@@ -45,8 +46,10 @@
move_resist = MOVE_FORCE_OVERPOWERING
damage_deflection = 70
can_open_with_hands = FALSE
- /// Make sure that the puzzle has the same puzzle_id as the keycard door!
+ /// Make sure that the puzzle has the same puzzle_id as the keycard door! (If this is null, queuelinks dont happen!)
var/puzzle_id = null
+ /// do we use queue_links?
+ var/uses_queuelinks = TRUE
/// Message that occurs when the door is opened
var/open_message = "The door beeps, and slides opens."
@@ -63,16 +66,18 @@
/obj/machinery/door/puzzle/Initialize(mapload)
. = ..()
- RegisterSignal(SSdcs, COMSIG_GLOB_PUZZLE_COMPLETED, PROC_REF(try_signal))
+ if(!isnull(puzzle_id) && uses_queuelinks)
+ SSqueuelinks.add_to_queue(src, puzzle_id)
-/obj/machinery/door/puzzle/Destroy(force)
- . = ..()
- UnregisterSignal(SSdcs, COMSIG_GLOB_PUZZLE_COMPLETED)
+/obj/machinery/door/puzzle/MatchedLinks(id, list/partners)
+ for(var/partner in partners)
+ RegisterSignal(partner, COMSIG_PUZZLE_COMPLETED, PROC_REF(try_signal))
/obj/machinery/door/puzzle/proc/try_signal(datum/source, try_id)
SIGNAL_HANDLER
- INVOKE_ASYNC(src, PROC_REF(try_puzzle_open), try_id)
+ puzzle_id = null //honestly these cant be closed anyway and im not fucking around with door code anymore
+ INVOKE_ASYNC(src, PROC_REF(try_puzzle_open), null)
/obj/machinery/door/puzzle/Bumped(atom/movable/AM)
return !density && ..()
@@ -101,6 +106,7 @@
/obj/machinery/door/puzzle/keycard
desc = "This door only opens when a keycard is swiped. It looks virtually indestructible."
+ uses_queuelinks = FALSE
/obj/machinery/door/puzzle/keycard/attackby(obj/item/attacking_item, mob/user, params)
. = ..()
@@ -203,6 +209,8 @@
)
/// Banned combinations of the list in decimal
var/static/list/banned_combinations = list(-1, 47, 95, 203, 311, 325, 422, 473, 488, 500, 511)
+ /// queue size, must match count of objects this activates!
+ var/queue_size = 2
/datum/armor/structure_light_puzzle
melee = 100
@@ -224,6 +232,8 @@
var/position = !!(generated_board & (1< 35)
+ return ..() // Too many bullets, we're done here
+
+ // Projectiles which do not deal damage will not leave dent / scorch mark graphics.
+ // However we snowflake some projectiles to leave them anyway, because they're appropriate.
+ var/static/list/always_leave_marks
+ if(isnull(always_leave_marks))
+ always_leave_marks = typecacheof(list(
+ /obj/projectile/beam/practice,
+ /obj/projectile/beam/laser/carbine/practice,
+ ))
+
+ var/is_invalid_damage = hitting_projectile.damage_type != BRUTE && hitting_projectile.damage_type != BURN
+ var/is_safe = !hitting_projectile.is_hostile_projectile()
+ var/is_generic_projectile = !is_type_in_typecache(hitting_projectile, always_leave_marks)
+ if(is_generic_projectile && (is_invalid_damage || is_safe))
+ return ..() // Don't bother unless it's real shit
+
+ var/p_x = hitting_projectile.p_x + pick(0, 0, 0, 0, 0, -1, 1) // really ugly way of coding "sometimes offset p_x!"
+ var/p_y = hitting_projectile.p_y + pick(0, 0, 0, 0, 0, -1, 1)
+ var/icon/our_icon = icon(icon, icon_state)
+ if(!our_icon.GetPixel(p_x, p_y) || hitting_projectile.original != src)
+ return BULLET_ACT_FORCE_PIERCE // We, "missed", I guess?
+
+ . = ..()
+ if(. != BULLET_ACT_HIT)
+ return
+
+ var/image/bullet_hole = image('icons/effects/effects.dmi', "dent", OBJ_LAYER + 0.5)
+ bullet_hole.pixel_x = p_x - 1 //offset correction
+ bullet_hole.pixel_y = p_y - 1
+ if(hitting_projectile.damage_type != BRUTE)
+ bullet_hole.setDir(pick(GLOB.cardinals))// random scorch design
+ if(hitting_projectile.damage < 20 && is_generic_projectile)
+ bullet_hole.icon_state = "light_scorch"
+ else
+ bullet_hole.icon_state = "scorch"
+
+ LAZYADD(bullethole_overlays, bullet_hole)
+ update_appearance(UPDATE_OVERLAYS)
+
/obj/item/target/syndicate
icon_state = "target_s"
desc = "A shooting target that looks like syndicate scum."
- hp = 2600
+ max_integrity = 2600
/obj/item/target/alien
icon_state = "target_q"
desc = "A shooting target that looks like a xenomorphic alien."
- hp = 2350
+ max_integrity = 2350
/obj/item/target/alien/anchored
anchored = TRUE
@@ -33,44 +80,8 @@
/obj/item/target/clown
icon_state = "target_c"
desc = "A shooting target that looks like a useless clown."
- hp = 2000
-
-#define DECALTYPE_SCORCH 1
-#define DECALTYPE_BULLET 2
+ max_integrity = 2000
/obj/item/target/clown/bullet_act(obj/projectile/P)
. = ..()
- playsound(src.loc, 'sound/items/bikehorn.ogg', 50, TRUE)
-
-/obj/item/target/bullet_act(obj/projectile/P)
- if(istype(P, /obj/projectile/bullet)) // If it's a foam dart, don't bother with any of this other shit
- return P.on_hit(src, 0)
- var/p_x = P.p_x + pick(0,0,0,0,0,-1,1) // really ugly way of coding "sometimes offset P.p_x!"
- var/p_y = P.p_y + pick(0,0,0,0,0,-1,1)
- var/decaltype = DECALTYPE_SCORCH
- if(istype(P, /obj/projectile/bullet))
- decaltype = DECALTYPE_BULLET
- var/icon/C = icon(icon,icon_state)
- if(C.GetPixel(p_x, p_y) && P.original == src && overlays.len <= 35) // if the located pixel isn't blank (null)
- hp -= P.damage
- if(hp <= 0)
- visible_message(span_danger("[src] breaks into tiny pieces and collapses!"))
- qdel(src)
- var/image/bullet_hole = image('icons/effects/effects.dmi', "scorch", OBJ_LAYER + 0.5)
- bullet_hole.pixel_x = p_x - 1 //offset correction
- bullet_hole.pixel_y = p_y - 1
- if(decaltype == DECALTYPE_SCORCH)
- bullet_hole.setDir(pick(NORTH,SOUTH,EAST,WEST))// random scorch design
- if(P.damage >= 20 || istype(P, /obj/projectile/beam/practice))
- bullet_hole.setDir(pick(NORTH,SOUTH,EAST,WEST))
- else
- bullet_hole.icon_state = "light_scorch"
- else
- bullet_hole.icon_state = "dent"
- LAZYADD(bullethole_overlays, bullet_hole)
- add_overlay(bullet_hole)
- return BULLET_ACT_HIT
- return BULLET_ACT_FORCE_PIERCE
-
-#undef DECALTYPE_SCORCH
-#undef DECALTYPE_BULLET
+ playsound(src, 'sound/items/bikehorn.ogg', 50, TRUE)
diff --git a/code/game/objects/items/stacks/golem_food/golem_status_effects.dm b/code/game/objects/items/stacks/golem_food/golem_status_effects.dm
index 5ed7fcd46823b..26b5a4439ea20 100644
--- a/code/game/objects/items/stacks/golem_food/golem_status_effects.dm
+++ b/code/game/objects/items/stacks/golem_food/golem_status_effects.dm
@@ -190,7 +190,7 @@
return ..()
/// When we take fire damage (or... technically also cold damage, we don't differentiate), zap a nearby APC
-/datum/status_effect/golem/plasma/proc/on_burned(datum/source, damage, damagetype)
+/datum/status_effect/golem/plasma/proc/on_burned(datum/source, damage, damagetype, ...)
SIGNAL_HANDLER
if(damagetype != BURN)
return
@@ -343,7 +343,7 @@
if (!.)
return FALSE
var/mob/living/carbon/human/human_owner = owner
- RegisterSignal(human_owner, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, PROC_REF(on_punched))
+ RegisterSignal(human_owner, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_punched))
human_owner.physiology.brute_mod *= brute_modifier
for (var/obj/item/bodypart/arm/arm in human_owner.bodyparts)
if (arm.limb_id != SPECIES_GOLEM)
@@ -354,10 +354,10 @@
/datum/status_effect/golem/titanium/proc/on_punched(mob/living/puncher, atom/punchee, proximity)
SIGNAL_HANDLER
if (!proximity || !isliving(punchee))
- return
+ return NONE
var/mob/living/victim = punchee
if (victim.body_position == LYING_DOWN || (!(FACTION_MINING in victim.faction) && !(FACTION_BOSS in victim.faction)))
- return
+ return NONE
victim.apply_damage(mining_bonus, BRUTE)
/// Make the targeted arm big and strong
@@ -370,7 +370,7 @@
/datum/status_effect/golem/titanium/on_remove()
var/mob/living/carbon/human/human_owner = owner
- UnregisterSignal(human_owner, COMSIG_HUMAN_MELEE_UNARMED_ATTACK)
+ UnregisterSignal(human_owner, COMSIG_LIVING_UNARMED_ATTACK)
human_owner.physiology.brute_mod /= brute_modifier
for (var/obj/item/bodypart/arm/arm as anything in modified_arms)
debuff_arm(arm)
diff --git a/code/game/objects/items/stacks/medical.dm b/code/game/objects/items/stacks/medical.dm
index f4a61f2d7ca4d..3d4ea3676b33c 100644
--- a/code/game/objects/items/stacks/medical.dm
+++ b/code/game/objects/items/stacks/medical.dm
@@ -93,7 +93,7 @@
patient.balloon_alert(user, "not hurt!")
return FALSE
user.visible_message("[user] applies [src] on [patient].", "You apply [src] on [patient].")
- patient.heal_bodypart_damage((heal_brute * 0.5))
+ patient.heal_bodypart_damage((heal_brute * patient.maxHealth/100))
return TRUE
if(iscarbon(patient))
return heal_carbon(patient, user, heal_brute, heal_burn)
diff --git a/code/game/objects/items/stacks/sheets/leather.dm b/code/game/objects/items/stacks/sheets/leather.dm
index bff5e52f0a195..659cee172d1dc 100644
--- a/code/game/objects/items/stacks/sheets/leather.dm
+++ b/code/game/objects/items/stacks/sheets/leather.dm
@@ -247,6 +247,20 @@ GLOBAL_LIST_INIT(leather_recipes, list ( \
novariants = TRUE
merge_type = /obj/item/stack/sheet/sinew
+/obj/item/stack/sheet/sinew/Initialize(mapload, new_amount, merge, list/mat_override, mat_amt)
+ . = ..()
+
+ // As bone and sinew have just a little too many recipes for this, we'll just split them up.
+ // Sinew slapcrafting will mostly-sinew recipes, and bones will have mostly-bones recipes.
+ var/static/list/slapcraft_recipe_list = list(\
+ /datum/crafting_recipe/goliathcloak, /datum/crafting_recipe/skilt, /datum/crafting_recipe/drakecloak,\
+ )
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/stack/sheet/sinew/wolf
name = "wolf sinew"
desc = "Long stringy filaments which came from the insides of a wolf."
@@ -297,6 +311,16 @@ GLOBAL_LIST_INIT(sinew_recipes, list ( \
layer = MOB_LAYER
merge_type = /obj/item/stack/sheet/animalhide/ashdrake
+/obj/item/stack/sheet/animalhide/ashdrake/Initialize(mapload, new_amount, merge, list/mat_override, mat_amt)
+ . = ..()
+
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/drakecloak)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
//Step one - dehairing.
/obj/item/stack/sheet/animalhide/attackby(obj/item/W, mob/user, params)
@@ -354,8 +378,8 @@ GLOBAL_LIST_INIT(sinew_recipes, list ( \
AddComponent(/datum/component/bakeable, /obj/item/stack/sheet/leather, rand(15 SECONDS, 20 SECONDS), TRUE, TRUE)
/obj/item/stack/sheet/wethide/burn()
- visible_message(span_notice("[src] burns up, leaving a sheet of leather behind!"))
- new /obj/item/stack/sheet/leather(loc) // only one sheet remains to incentivise not burning your wethide to dry it
+ visible_message(span_notice("[src] dries up!"))
+ new /obj/item/stack/sheet/leather(loc, amount) // all the sheets to incentivize not losing your whole stack by accident
qdel(src)
/obj/item/stack/sheet/wethide/should_atmos_process(datum/gas_mixture/air, exposed_temperature)
@@ -364,6 +388,6 @@ GLOBAL_LIST_INIT(sinew_recipes, list ( \
/obj/item/stack/sheet/wethide/atmos_expose(datum/gas_mixture/air, exposed_temperature)
wetness--
if(wetness == 0)
- new /obj/item/stack/sheet/leather(drop_location(), 1)
+ new /obj/item/stack/sheet/leather(drop_location(), amount)
wetness = initial(wetness)
use(1)
diff --git a/code/game/objects/items/stacks/sheets/mineral.dm b/code/game/objects/items/stacks/sheets/mineral.dm
index 8da8d83e30eaf..8eafe6d52e5ae 100644
--- a/code/game/objects/items/stacks/sheets/mineral.dm
+++ b/code/game/objects/items/stacks/sheets/mineral.dm
@@ -282,8 +282,8 @@ GLOBAL_LIST_INIT(bananium_recipes, list ( \
walltype = /turf/closed/wall/mineral/titanium
GLOBAL_LIST_INIT(titanium_recipes, list ( \
- new/datum/stack_recipe("titanium tile", /obj/item/stack/tile/mineral/titanium, 1, 4, 20, check_density = FALSE, category = CAT_TILES), \
- new/datum/stack_recipe("shuttle seat", /obj/structure/chair/comfy/shuttle, 2, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_FURNITURE), \
+ new/datum/stack_recipe("Titanium tile", /obj/item/stack/tile/mineral/titanium, 1, 4, 20, check_density = FALSE, category = CAT_TILES), \
+ new/datum/stack_recipe("Shuttle seat", /obj/structure/chair/comfy/shuttle, 2, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_FURNITURE), \
))
/obj/item/stack/sheet/mineral/titanium/get_main_recipes()
diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm
index e9da67e9cc256..9ec054ea2008e 100644
--- a/code/game/objects/items/stacks/sheets/sheet_types.dm
+++ b/code/game/objects/items/stacks/sheets/sheet_types.dm
@@ -78,9 +78,8 @@ GLOBAL_LIST_INIT(metal_recipes, list ( \
new/datum/stack_recipe("floor tile", /obj/item/stack/tile/iron/base, 1, 4, 20, category = CAT_TILES), \
new/datum/stack_recipe("iron rod", /obj/item/stack/rods, 1, 2, 60, category = CAT_MISC), \
null, \
- new/datum/stack_recipe("wall girders (anchored)", /obj/structure/girder, 2, time = 4 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, trait_booster = TRAIT_QUICK_BUILD, trait_modifier = 0.75, category = CAT_STRUCTURE), \
+ new/datum/stack_recipe("wall girders (anchored)", /obj/structure/girder, 2, time = 4 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, placement_checks = STACK_CHECK_TRAM_FORBIDDEN, trait_booster = TRAIT_QUICK_BUILD, trait_modifier = 0.75, category = CAT_STRUCTURE), \
null, \
- new/datum/stack_recipe("tram wall girders (anchored)", /obj/structure/girder/tram, 2, time = 4 SECONDS, one_per_turf = TRUE, on_solid_ground = FALSE, check_density = FALSE, on_tram = TRUE, trait_booster = TRAIT_QUICK_BUILD, trait_modifier = 0.75, category = CAT_STRUCTURE), \
null, \
new/datum/stack_recipe("computer frame", /obj/structure/frame/computer, 5, time = 2.5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_EQUIPMENT), \
new/datum/stack_recipe("modular console", /obj/machinery/modular_computer, 10, time = 2.5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_EQUIPMENT), \
@@ -761,8 +760,23 @@ GLOBAL_LIST_INIT(bronze_recipes, list ( \
merge_type = /obj/item/stack/sheet/bone
material_type = /datum/material/bone
+/obj/item/stack/sheet/bone/Initialize(mapload, new_amount, merge, list/mat_override, mat_amt)
+ . = ..()
+
+ // As bone and sinew have just a little too many recipes for this, we'll just split them up.
+ // Sinew slapcrafting will mostly-sinew recipes, and bones will have mostly-bones recipes.
+ var/static/list/slapcraft_recipe_list = list(\
+ /datum/crafting_recipe/bonedagger, /datum/crafting_recipe/bonespear, /datum/crafting_recipe/boneaxe,\
+ /datum/crafting_recipe/bonearmor, /datum/crafting_recipe/skullhelm, /datum/crafting_recipe/bracers
+ )
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
GLOBAL_LIST_INIT(plastic_recipes, list(
- new /datum/stack_recipe("plastic floor tile", /obj/item/stack/tile/plastic, 1, 4, 20, check_density = FALSE, category = CAT_TILES), \
+ new /datum/stack_recipe("plastic floor tile", /obj/item/stack/tile/plastic, 1, 4, 20, time = 2 SECONDS, check_density = FALSE, category = CAT_TILES), \
+ new /datum/stack_recipe("thermoplastic tram tile", /obj/item/stack/thermoplastic, 1, 2, time = 4 SECONDS, check_density = FALSE, placement_checks = STACK_CHECK_TRAM_EXCLUSIVE, category = CAT_TILES), \
new /datum/stack_recipe("folding plastic chair", /obj/structure/chair/plastic, 2, check_density = FALSE, category = CAT_FURNITURE), \
new /datum/stack_recipe("plastic flaps", /obj/structure/plasticflaps, 5, one_per_turf = TRUE, on_solid_ground = TRUE, time = 4 SECONDS, category = CAT_FURNITURE), \
new /datum/stack_recipe("water bottle", /obj/item/reagent_containers/cup/glass/waterbottle/empty, check_density = FALSE, category = CAT_CONTAINERS), \
diff --git a/code/game/objects/items/stacks/stack.dm b/code/game/objects/items/stacks/stack.dm
index 7050309268cd0..7f789eaa514e3 100644
--- a/code/game/objects/items/stacks/stack.dm
+++ b/code/game/objects/items/stacks/stack.dm
@@ -493,19 +493,15 @@
builder.balloon_alert(builder, "won't fit here!")
return FALSE
- if(recipe.on_tram)
- if(!locate(/obj/structure/industrial_lift/tram) in dest_turf)
- builder.balloon_alert(builder, "must be made on a tram!")
- return FALSE
-
if(recipe.on_solid_ground)
if(isclosedturf(dest_turf))
builder.balloon_alert(builder, "cannot be made on a wall!")
return FALSE
if(is_type_in_typecache(dest_turf, GLOB.turfs_without_ground))
- builder.balloon_alert(builder, "must be made on solid ground!")
- return FALSE
+ if(!locate(/obj/structure/thermoplastic) in dest_turf) // for tram construction
+ builder.balloon_alert(builder, "must be made on solid ground!")
+ return FALSE
if(recipe.check_density)
for(var/obj/object in dest_turf)
@@ -527,6 +523,16 @@
builder.balloon_alert(builder, "can't be near another!")
return FALSE
+ if(recipe.placement_checks & STACK_CHECK_TRAM_FORBIDDEN)
+ if(locate(/obj/structure/transport/linear/tram) in dest_turf || locate(/obj/structure/thermoplastic) in dest_turf)
+ builder.balloon_alert(builder, "can't be on tram!")
+ return FALSE
+
+ if(recipe.placement_checks & STACK_CHECK_TRAM_EXCLUSIVE)
+ if(!locate(/obj/structure/transport/linear/tram) in dest_turf)
+ builder.balloon_alert(builder, "must be made on a tram!")
+ return FALSE
+
return TRUE
/obj/item/stack/use(used, transfer = FALSE, check = TRUE) // return 0 = borked; return 1 = had enough
diff --git a/code/game/objects/items/stacks/stack_recipe.dm b/code/game/objects/items/stacks/stack_recipe.dm
index a065a4916b396..bfdc3c8ca5717 100644
--- a/code/game/objects/items/stacks/stack_recipe.dm
+++ b/code/game/objects/items/stacks/stack_recipe.dm
@@ -24,11 +24,9 @@
var/check_direction = FALSE
/// If the atom requires a floor below
var/on_solid_ground = FALSE
- /// If the atom requires a tram floor below
- var/on_tram = FALSE
/// If the atom checks that there are objects with density in the same turf when being built. TRUE by default
var/check_density = TRUE
- /// Bitflag of additional placement checks required to place. (STACK_CHECK_CARDINALS|STACK_CHECK_ADJACENT)
+ /// Bitflag of additional placement checks required to place. (STACK_CHECK_CARDINALS|STACK_CHECK_ADJACENT|STACK_CHECK_TRAM_FORBIDDEN|STACK_CHECK_TRAM_EXCLUSIVE)
var/placement_checks = NONE
/// If TRUE, the created atom will gain custom mat datums
var/applies_mats = FALSE
@@ -48,7 +46,6 @@
time = 0,
one_per_turf = FALSE,
on_solid_ground = FALSE,
- on_tram = FALSE,
is_fulltile = FALSE,
check_direction = FALSE,
check_density = TRUE,
@@ -67,7 +64,6 @@
src.time = time
src.one_per_turf = one_per_turf
src.on_solid_ground = on_solid_ground
- src.on_tram = on_tram
src.is_fulltile = is_fulltile
src.check_direction = check_direction || is_fulltile
src.check_density = check_density
@@ -90,7 +86,6 @@
time = 0,
one_per_turf = FALSE,
on_solid_ground = FALSE,
- on_tram = FALSE,
window_checks = FALSE,
placement_checks = NONE,
applies_mats = FALSE,
diff --git a/code/game/objects/items/stacks/tiles/tile_types.dm b/code/game/objects/items/stacks/tiles/tile_types.dm
index e08b7064dfd7b..41b687842bae7 100644
--- a/code/game/objects/items/stacks/tiles/tile_types.dm
+++ b/code/game/objects/items/stacks/tiles/tile_types.dm
@@ -48,21 +48,21 @@
if(tile_reskin_types || tile_rotate_dirs)
. += span_notice("Use while in your hand to change what type of [src] you want.")
if(throwforce && !is_cyborg) //do not want to divide by zero or show the message to borgs who can't throw
- var/verb
+ var/damage_value
switch(CEILING(MAX_LIVING_HEALTH / throwforce, 1)) //throws to crit a human
if(1 to 3)
- verb = "superb"
+ damage_value = "superb"
if(4 to 6)
- verb = "great"
+ damage_value = "great"
if(7 to 9)
- verb = "good"
+ damage_value = "good"
if(10 to 12)
- verb = "fairly decent"
+ damage_value = "fairly decent"
if(13 to 15)
- verb = "mediocre"
- if(!verb)
+ damage_value = "mediocre"
+ if(!damage_value)
return
- . += span_notice("Those could work as a [verb] throwing weapon.")
+ . += span_notice("Those could work as a [damage_value] throwing weapon.")
/**
* Place our tile on a plating, or replace it.
@@ -1035,23 +1035,23 @@
turf_type = /turf/open/floor/noslip/tram
merge_type = /obj/item/stack/tile/noslip/tram
-/obj/item/stack/tile/noslip/tram_platform
+/obj/item/stack/tile/tram
name = "tram platform tiles"
singular_name = "tram platform"
desc = "A tile used for tram platforms."
icon_state = "darkiron_catwalk"
inhand_icon_state = "tile-neon"
- turf_type = /turf/open/floor/noslip/tram_platform
- merge_type = /obj/item/stack/tile/noslip/tram_platform
+ turf_type = /turf/open/floor/tram
+ merge_type = /obj/item/stack/tile/tram
-/obj/item/stack/tile/noslip/tram_plate
- name = "high-traction platform tile"
- singular_name = "high-traction platform tile"
- desc = "A high-traction tile used for tram platforms."
+/obj/item/stack/tile/tram/plate
+ name = "linear induction tram tiles"
+ singular_name = "linear induction tram tile tile"
+ desc = "A tile with an aluminium plate for tram propulsion."
icon_state = "darkiron_plate"
inhand_icon_state = "tile-neon"
- turf_type = /turf/open/floor/noslip/tram_plate
- merge_type = /obj/item/stack/tile/noslip/tram_plate
+ turf_type = /turf/open/floor/tram/plate
+ merge_type = /obj/item/stack/tile/tram/plate
//Circuit
/obj/item/stack/tile/circuit
diff --git a/code/game/objects/items/storage/backpack.dm b/code/game/objects/items/storage/backpack.dm
index 0461d76cb99c3..ee0f232a216e9 100644
--- a/code/game/objects/items/storage/backpack.dm
+++ b/code/game/objects/items/storage/backpack.dm
@@ -247,6 +247,7 @@
throwforce = 15
attack_verb_continuous = list("MEATS", "MEAT MEATS")
attack_verb_simple = list("MEAT", "MEAT MEAT")
+ custom_materials = list(/datum/material/meat = SHEET_MATERIAL_AMOUNT * 25) // MEAT
///Sounds used in the squeak component
var/list/meat_sounds = list('sound/effects/blobattack.ogg' = 1)
///Reagents added to the edible component, ingested when you EAT the MEAT
@@ -263,13 +264,26 @@
/obj/item/storage/backpack/meat/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/edible,\
+ AddComponent(
+ /datum/component/edible,\
initial_reagents = meat_reagents,\
foodtypes = foodtypes,\
tastes = tastes,\
eatverbs = eatverbs,\
)
AddComponent(/datum/component/squeak, meat_sounds)
+ AddComponent(
+ /datum/component/blood_walk,\
+ blood_type = /obj/effect/decal/cleanable/blood,\
+ blood_spawn_chance = 15,\
+ max_blood = 300,\
+ )
+ AddComponent(
+ /datum/component/bloody_spreader,\
+ blood_left = INFINITY,\
+ blood_dna = list("MEAT DNA" = "MT+"),\
+ diseases = null,\
+ )
/*
* Satchel Types
diff --git a/code/game/objects/items/storage/belt.dm b/code/game/objects/items/storage/belt.dm
index 1c223c50f3d39..ac4a4ec93baae 100644
--- a/code/game/objects/items/storage/belt.dm
+++ b/code/game/objects/items/storage/belt.dm
@@ -425,6 +425,7 @@
/obj/item/wirecutters,
/obj/item/wrench,
/obj/item/wormhole_jaunter,
+ /obj/item/skeleton_key,
))
diff --git a/code/game/objects/items/storage/holsters.dm b/code/game/objects/items/storage/holsters.dm
index cb722469a2c03..400c949a99320 100644
--- a/code/game/objects/items/storage/holsters.dm
+++ b/code/game/objects/items/storage/holsters.dm
@@ -195,7 +195,9 @@
/obj/item/storage/belt/holster/nukie/cowboy/full/PopulateContents()
generate_items_inside(list(
- /obj/item/gun/ballistic/revolver/syndicate/cowboy = 1,
+ /obj/item/gun/ballistic/revolver/syndicate/cowboy/nuclear = 1,
/obj/item/ammo_box/a357 = 2,
), src)
+
+
diff --git a/code/game/objects/items/storage/lockbox.dm b/code/game/objects/items/storage/lockbox.dm
index 79ab5003bc2a9..aab4db962f809 100644
--- a/code/game/objects/items/storage/lockbox.dm
+++ b/code/game/objects/items/storage/lockbox.dm
@@ -249,6 +249,7 @@
. = ..()
buyer_account = _buyer_account
ADD_TRAIT(src, TRAIT_NO_MISSING_ITEM_ERROR, TRAIT_GENERIC)
+ ADD_TRAIT(src, TRAIT_NO_MANIFEST_CONTENTS_ERROR, TRAIT_GENERIC)
/obj/item/storage/lockbox/order/attackby(obj/item/W, mob/user, params)
var/obj/item/card/id/id_card = W.GetID()
diff --git a/code/game/objects/items/storage/secure.dm b/code/game/objects/items/storage/secure.dm
index ad4be8d6e4088..0d845b5254efc 100644
--- a/code/game/objects/items/storage/secure.dm
+++ b/code/game/objects/items/storage/secure.dm
@@ -40,7 +40,7 @@
. = ..()
icon_state = "[initial(icon_state)][atom_storage?.locked ? "_locked" : null]"
-/obj/item/storage/secure/tool_act(mob/living/user, obj/item/tool)
+/obj/item/storage/secure/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
if(can_hack_open && atom_storage.locked)
return ..()
else
diff --git a/code/game/objects/items/storage/toolbox.dm b/code/game/objects/items/storage/toolbox.dm
index fd5eded2da398..42f5e08f479f5 100644
--- a/code/game/objects/items/storage/toolbox.dm
+++ b/code/game/objects/items/storage/toolbox.dm
@@ -374,7 +374,7 @@
/obj/item/storage/toolbox/guncase/revolver
name = "revolver gun case"
- weapon_to_spawn = /obj/item/gun/ballistic/revolver/syndicate
+ weapon_to_spawn = /obj/item/gun/ballistic/revolver/syndicate/nuclear
extra_to_spawn = /obj/item/ammo_box/a357
/obj/item/storage/toolbox/guncase/sword_and_board
@@ -385,7 +385,6 @@
/obj/item/storage/toolbox/guncase/sword_and_board/PopulateContents()
new weapon_to_spawn (src)
new extra_to_spawn (src)
- new /obj/item/mod/module/hat_stabilizer (src)
new /obj/item/clothing/head/costume/knight (src)
/obj/item/storage/toolbox/guncase/cqc
@@ -396,8 +395,8 @@
/obj/item/storage/toolbox/guncase/cqc/PopulateContents()
new weapon_to_spawn (src)
new extra_to_spawn (src)
- new /obj/item/mod/module/hat_stabilizer (src)
new /obj/item/clothing/head/costume/snakeeater (src)
+ new /obj/item/storage/fancy/cigarettes/cigpack_syndicate (src)
/obj/item/clothing/head/costume/snakeeater
name = "strange bandana"
@@ -447,3 +446,65 @@
desc = "A gun case. Has the symbol of the Third Soviet Union stamped on the side."
weapon_to_spawn = /obj/item/gun/ballistic/automatic/plastikov
extra_to_spawn = /obj/item/food/rationpack //sorry comrade, cannot get you more ammo, here, have lunch
+
+/obj/item/storage/toolbox/guncase/monkeycase
+ name = "monkey gun case"
+ desc = "Everything a monkey needs to truly go ape-shit. There's a paw-shaped hand scanner lock on the front of the case."
+
+/obj/item/storage/toolbox/guncase/monkeycase/Initialize(mapload)
+ . = ..()
+ atom_storage.locked = STORAGE_SOFT_LOCKED
+
+/obj/item/storage/toolbox/guncase/monkeycase/attack_self(mob/user, modifiers)
+ if(!monkey_check(user))
+ return
+ return ..()
+
+/obj/item/storage/toolbox/guncase/monkeycase/attack_self_secondary(mob/user, modifiers)
+ attack_self(user, modifiers)
+ return
+
+/obj/item/storage/toolbox/guncase/monkeycase/attack_hand(mob/user, list/modifiers)
+ if(!monkey_check(user))
+ return
+ return ..()
+
+/obj/item/storage/toolbox/guncase/monkeycase/proc/monkey_check(mob/user)
+ if(atom_storage.locked == STORAGE_NOT_LOCKED)
+ return TRUE
+
+ if(is_simian(user))
+ atom_storage.locked = STORAGE_NOT_LOCKED
+ to_chat(user, span_notice("You place your paw on the paw scanner, and hear a soft click as [src] unlocks!"))
+ playsound(src, 'sound/items/click.ogg', 25, TRUE)
+ return TRUE
+ to_chat(user, span_warning("You put your hand on the hand scanner, and it rejects it with an angry chimpanzee screech!"))
+ playsound(src, "sound/creatures/monkey/monkey_screech_[rand(1,7)].ogg", 75, TRUE)
+ return FALSE
+
+/obj/item/storage/toolbox/guncase/monkeycase/PopulateContents()
+ switch(rand(1, 3))
+ if(1)
+ // Uzi with a boxcutter.
+ new /obj/item/gun/ballistic/automatic/mini_uzi/chimpgun(src)
+ new /obj/item/ammo_box/magazine/uzim9mm(src)
+ new /obj/item/ammo_box/magazine/uzim9mm(src)
+ new /obj/item/boxcutter/extended(src)
+ if(2)
+ // Thompson with a boxcutter.
+ new /obj/item/gun/ballistic/automatic/tommygun/chimpgun(src)
+ new /obj/item/ammo_box/magazine/tommygunm45(src)
+ new /obj/item/ammo_box/magazine/tommygunm45(src)
+ new /obj/item/boxcutter/extended(src)
+ if(3)
+ // M1911 with a switchblade and an extra banana bomb.
+ new /obj/item/gun/ballistic/automatic/pistol/m1911/chimpgun(src)
+ new /obj/item/ammo_box/magazine/m45(src)
+ new /obj/item/ammo_box/magazine/m45(src)
+ new /obj/item/switchblade/extended(src)
+ new /obj/item/food/grown/banana/bunch/monkeybomb(src)
+
+ // Banana bomb! Basically a tiny flashbang for monkeys.
+ new /obj/item/food/grown/banana/bunch/monkeybomb(src)
+ // Somewhere to store it all.
+ new /obj/item/storage/backpack/messenger(src)
diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm
index 14f166208aa10..a5263780a9bf7 100644
--- a/code/game/objects/items/storage/uplink_kits.dm
+++ b/code/game/objects/items/storage/uplink_kits.dm
@@ -610,7 +610,16 @@
new /obj/item/storage/belt/grenade/full(src)
if(prob(1))
new /obj/item/clothing/head/hats/hos/shako(src)
- new /obj/item/mod/module/hat_stabilizer(src)
+
+/obj/item/storage/box/syndie_kit/core_gear
+ name = "core equipment box"
+ desc = "Contains all the necessary gear for success for any nuclear operative unsure of what is needed for success in the field. Everything here WILL help you."
+
+/obj/item/storage/box/syndie_kit/core_gear/PopulateContents()
+ new /obj/item/implanter/freedom (src)
+ new /obj/item/card/emag/doorjack (src)
+ new /obj/item/reagent_containers/hypospray/medipen/stimulants (src)
+ new /obj/item/grenade/c4 (src)
/// Surplus Ammo Box
@@ -747,13 +756,13 @@
human_target.reagents.add_reagent(/datum/reagent/toxin, 2)
return FALSE
- /// If all the antag datums are 'fake', disallow induction! No self-antagging.
+ /// If all the antag datums are 'fake' or none exist, disallow induction! No self-antagging.
var/faker
for(var/datum/antagonist/antag_datum as anything in human_target.mind.antag_datums)
if((antag_datum.antag_flags & FLAG_FAKE_ANTAG))
faker = TRUE
- if(faker) // GTFO. Technically not foolproof but making a heartbreaker or a paradox clone a nuke op sounds hilarious
+ if(faker || isnull(human_target.mind.antag_datums)) // GTFO. Technically not foolproof but making a heartbreaker or a paradox clone a nuke op sounds hilarious
to_chat(human_target, span_notice("Huh? Nothing happened? But you're starting to feel a little ill..."))
human_target.reagents.add_reagent(/datum/reagent/toxin, 15)
return FALSE
diff --git a/code/game/objects/items/tcg/tcg_machines.dm b/code/game/objects/items/tcg/tcg_machines.dm
index a38bb8a95994e..767592535f74d 100644
--- a/code/game/objects/items/tcg/tcg_machines.dm
+++ b/code/game/objects/items/tcg/tcg_machines.dm
@@ -293,9 +293,9 @@ GLOBAL_LIST_EMPTY(tcgcard_machine_radial_choices)
use_power = NO_POWER_USE
///Reference to the display panel generated by this button.
- var/obj/effect/decal/trading_card_panel/display_panel_ref
+ var/obj/effect/trading_card_panel/display_panel_ref
///Typepath of the display panel generated.
- var/display_panel_type = /obj/effect/decal/trading_card_panel
+ var/display_panel_type = /obj/effect/trading_card_panel
///Where the panel will be spawned in relation to the button on the X axis.
var/panel_offset_x = 1
///Where the panel will be spawned in relation to the button on the Y axis.
@@ -306,10 +306,7 @@ GLOBAL_LIST_EMPTY(tcgcard_mana_bar_radial_choices)
/obj/machinery/trading_card_button/Initialize(mapload)
. = ..()
- var/obj/effect/decal/trading_card_panel/new_panel = new display_panel_type(get_turf(src))
- new_panel.pixel_x = panel_offset_x
- new_panel.pixel_y = panel_offset_y
- display_panel_ref = new_panel
+ display_panel_ref = new display_panel_type(locate(x + panel_offset_x, y + panel_offset_y, z))
/obj/machinery/trading_card_button/Destroy()
QDEL_NULL(display_panel_ref)
@@ -367,8 +364,8 @@ GLOBAL_LIST_EMPTY(tcgcard_mana_bar_radial_choices)
name = "life control panel"
desc = "A set of buttons that lets you keep track of your life shards when playing Tactical Game Cards."
icon_state = "health_buttons"
- display_panel_type = /obj/effect/decal/trading_card_panel/health
- panel_offset_x = -24
+ display_panel_type = /obj/effect/trading_card_panel/health
+ panel_offset_x = -1
///Global list containing all options used for the TGC health button.
GLOBAL_LIST_EMPTY(tcgcard_health_bar_radial_choices)
@@ -395,11 +392,12 @@ GLOBAL_LIST_EMPTY(tcgcard_health_bar_radial_choices)
display_panel_ref.gems -= tgui_input_number(user, "Please input total damage", "Inflict damage", 1, display_panel_ref.gem_slots, 0)
///A display panel that renders a set of icons (in this case mana crystals), this is generated by /obj/machinery/trading_card_button and can be manipulated by the button which generates it.
-/obj/effect/decal/trading_card_panel
+/obj/effect/trading_card_panel
name = "mana panel"
icon = 'icons/obj/toys/tcgmisc_large.dmi'
icon_state = "display_panel"
pixel_x = -10
+ anchored = TRUE
///How much "active" gems will appear
var/gems = 1
@@ -428,11 +426,11 @@ GLOBAL_LIST_EMPTY(tcgcard_health_bar_radial_choices)
///The name of what this panel tracks, used in the description
var/gem_title = "mana"
-/obj/effect/decal/trading_card_panel/Initialize(mapload)
+/obj/effect/trading_card_panel/Initialize(mapload)
. = ..()
update_icon(UPDATE_OVERLAYS)
-/obj/effect/decal/trading_card_panel/update_overlays()
+/obj/effect/trading_card_panel/update_overlays()
. = ..()
if(!gem_slots)
return
@@ -447,12 +445,12 @@ GLOBAL_LIST_EMPTY(tcgcard_health_bar_radial_choices)
gem_overlay.pixel_w = (gem - 1) * individual_gem_offset_x + gem_bar_offset_w
. += gem_overlay
-/obj/effect/decal/trading_card_panel/examine(mob/user)
+/obj/effect/trading_card_panel/examine(mob/user)
. = ..()
. += span_notice("It is currently showing [gems] out of [gem_slots] [gem_title].")
///A variant of the display panel for life shards, this one is set up to display two columns.
-/obj/effect/decal/trading_card_panel/health
+/obj/effect/trading_card_panel/health
name = "life shard panel"
pixel_x = 9
diff --git a/code/game/objects/obj_defense.dm b/code/game/objects/obj_defense.dm
index ff9f99f121e81..8b608427e2139 100644
--- a/code/game/objects/obj_defense.dm
+++ b/code/game/objects/obj_defense.dm
@@ -23,14 +23,29 @@
return TRUE
-/obj/bullet_act(obj/projectile/P)
+/obj/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
. = ..()
- playsound(src, P.hitsound, 50, TRUE)
- var/damage
+ if(. != BULLET_ACT_HIT)
+ return .
+
+ playsound(src, hitting_projectile.hitsound, 50, TRUE)
+ var/damage_sustained = 0
if(!QDELETED(src)) //Bullet on_hit effect might have already destroyed this object
- damage = take_damage(P.damage * P.demolition_mod, P.damage_type, P.armor_flag, 0, REVERSE_DIR(P.dir), P.armour_penetration)
- if(P.suppressed != SUPPRESSED_VERY)
- visible_message(span_danger("[src] is hit by \a [P][damage ? "" : ", without leaving a mark"]!"), null, null, COMBAT_MESSAGE_RANGE)
+ damage_sustained = take_damage(
+ hitting_projectile.damage * hitting_projectile.demolition_mod,
+ hitting_projectile.damage_type,
+ hitting_projectile.armor_flag,
+ FALSE,
+ REVERSE_DIR(hitting_projectile.dir),
+ hitting_projectile.armour_penetration,
+ )
+ if(hitting_projectile.suppressed != SUPPRESSED_VERY)
+ visible_message(
+ span_danger("[src] is hit by \a [hitting_projectile][damage_sustained ? "" : ", without leaving a mark"]!"),
+ vision_distance = COMBAT_MESSAGE_RANGE,
+ )
+
+ return damage_sustained > 0 ? BULLET_ACT_HIT : BULLET_ACT_BLOCK
/obj/attack_hulk(mob/living/carbon/human/user)
..()
@@ -144,7 +159,7 @@
if(has_buckled_mobs())
for(var/m in buckled_mobs)
var/mob/living/buckled_mob = m
- buckled_mob.electrocute_act((clamp(round(strength * 3.125e-6), 10, 90) + rand(-5, 5)), src, flags = SHOCK_TESLA)
+ buckled_mob.electrocute_act((clamp(round(strength * 1.25e-3), 10, 90) + rand(-5, 5)), src, flags = SHOCK_TESLA)
///the obj is deconstructed into pieces, whether through careful disassembly or when destroyed.
/obj/proc/deconstruct(disassembled = TRUE)
diff --git a/code/game/objects/structures.dm b/code/game/objects/structures.dm
index ba8623ec4aa3a..cdc2b2bd5ec15 100644
--- a/code/game/objects/structures.dm
+++ b/code/game/objects/structures.dm
@@ -63,6 +63,6 @@
/obj/structure/zap_act(power, zap_flags)
if(zap_flags & ZAP_OBJ_DAMAGE)
- take_damage(power * 1.5625e-7, BURN, "energy")
+ take_damage(power * 2.5e-4, BURN, "energy")
power -= power * 5e-4 //walls take a lot out of ya
. = ..()
diff --git a/code/game/objects/structures/beds_chairs/bed.dm b/code/game/objects/structures/beds_chairs/bed.dm
index edcec5add3ac3..4b82aeb5e8eda 100644
--- a/code/game/objects/structures/beds_chairs/bed.dm
+++ b/code/game/objects/structures/beds_chairs/bed.dm
@@ -90,6 +90,8 @@
/obj/structure/bed/medical/Initialize(mapload)
. = ..()
AddElement(/datum/element/noisy_movement)
+ if(anchored)
+ update_appearance()
/obj/structure/bed/medical/add_context(atom/source, list/context, obj/item/held_item, mob/living/user)
. = ..()
diff --git a/code/game/objects/structures/bonfire.dm b/code/game/objects/structures/bonfire.dm
index b3c17c7ec983b..987b9bcc37b56 100644
--- a/code/game/objects/structures/bonfire.dm
+++ b/code/game/objects/structures/bonfire.dm
@@ -186,6 +186,11 @@
density = TRUE
/obj/structure/bonfire/prelit/Initialize(mapload)
+ . = ..()
+ return INITIALIZE_HINT_LATELOAD
+
+// Late init so that we can wait for air to exist in lazyloaded templates
+/obj/structure/bonfire/prelit/LateInitialize()
. = ..()
start_burning()
diff --git a/code/game/objects/structures/broken_flooring.dm b/code/game/objects/structures/broken_flooring.dm
index c81ca778424fe..b2be42ae40ddf 100644
--- a/code/game/objects/structures/broken_flooring.dm
+++ b/code/game/objects/structures/broken_flooring.dm
@@ -8,6 +8,8 @@
opacity = FALSE
plane = FLOOR_PLANE
layer = CATWALK_LAYER
+ /// do we always have FLOOR_PLANE even if we arent on plating?
+ var/always_floorplane = FALSE
/obj/structure/broken_flooring/Initialize(mapload)
. = ..()
@@ -16,7 +18,7 @@
/obj/structure/broken_flooring/LateInitialize()
. = ..()
var/turf/turf = get_turf(src)
- if(!isplatingturf(turf)) // Render as trash if not on plating
+ if(!isplatingturf(turf) && !always_floorplane) // Render as trash if not on plating
plane = GAME_PLANE
layer = LOW_OBJ_LAYER
return
@@ -35,20 +37,40 @@
/obj/structure/broken_flooring/singular
icon_state = "singular"
+/obj/structure/broken_flooring/singular/always_floorplane
+ always_floorplane = TRUE
+
/obj/structure/broken_flooring/pile
icon_state = "pile"
+/obj/structure/broken_flooring/pile/always_floorplane
+ always_floorplane = TRUE
+
/obj/structure/broken_flooring/side
icon_state = "side"
+/obj/structure/broken_flooring/side/always_floorplane
+ always_floorplane = TRUE
+
/obj/structure/broken_flooring/corner
icon_state = "corner"
+/obj/structure/broken_flooring/corner/always_floorplane
+ always_floorplane = TRUE
+
/obj/structure/broken_flooring/plating
icon_state = "plating"
+/obj/structure/broken_flooring/plating/always_floorplane
+ always_floorplane = TRUE
+
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/broken_flooring/singular, 0)
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/broken_flooring/pile, 0)
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/broken_flooring/side, 0)
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/broken_flooring/corner, 0)
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/broken_flooring/plating, 0)
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/broken_flooring/singular/always_floorplane, 0)
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/broken_flooring/pile/always_floorplane, 0)
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/broken_flooring/side/always_floorplane, 0)
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/broken_flooring/corner/always_floorplane, 0)
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/broken_flooring/plating/always_floorplane, 0)
diff --git a/code/game/objects/structures/construction_console/construction_console.dm b/code/game/objects/structures/construction_console/construction_console.dm
index 5cf4a9bfd8595..d33c91f7c072e 100644
--- a/code/game/objects/structures/construction_console/construction_console.dm
+++ b/code/game/objects/structures/construction_console/construction_console.dm
@@ -80,12 +80,12 @@
/obj/machinery/computer/camera_advanced/base_construction/GrantActions(mob/living/user)
..()
//When the eye is in use, make it visible to players so they know when someone is building.
- eyeobj.invisibility = 0
+ SetInvisibility(INVISIBILITY_NONE, id=type)
/obj/machinery/computer/camera_advanced/base_construction/remove_eye_control(mob/living/user)
..()
- //Hide the eye when not in use.
- eyeobj.invisibility = INVISIBILITY_MAXIMUM
+ //Set back to default invisibility when not in use.
+ RemoveInvisibility(type)
/**
* A mob used by [/obj/machinery/computer/camera_advanced/base_construction] for building in specific areas.
@@ -101,6 +101,7 @@
move_on_shuttle = TRUE
icon = 'icons/obj/mining.dmi'
icon_state = "construction_drone"
+ invisibility = INVISIBILITY_MAXIMUM
///Reference to the camera console controlling this drone
var/obj/machinery/computer/camera_advanced/base_construction/linked_console
diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm
index db1122465f3c7..7585a3bd37484 100644
--- a/code/game/objects/structures/crates_lockers/closets.dm
+++ b/code/game/objects/structures/crates_lockers/closets.dm
@@ -804,13 +804,13 @@ GLOBAL_LIST_EMPTY(roundstart_station_closets)
if(!opened)
return
user.visible_message(span_notice("[user] slices apart \the [src]."),
- span_notice("You cut \the [src] apart weaponith \the [weapon]."),
- span_hear("You hear weaponelding."))
+ span_notice("You cut \the [src] apart with \the [weapon]."),
+ span_hear("You hear welding."))
deconstruct(TRUE)
return
else // for example cardboard box is cut with wirecutters
user.visible_message(span_notice("[user] cut apart \the [src]."), \
- span_notice("You cut \the [src] apart weaponith \the [weapon]."))
+ span_notice("You cut \the [src] apart with \the [weapon]."))
deconstruct(TRUE)
return
if (user.combat_mode)
diff --git a/code/game/objects/structures/crates_lockers/crates/large.dm b/code/game/objects/structures/crates_lockers/crates/large.dm
index 3fe68ee5763d4..93b137dc9b3b1 100644
--- a/code/game/objects/structures/crates_lockers/crates/large.dm
+++ b/code/game/objects/structures/crates_lockers/crates/large.dm
@@ -60,8 +60,6 @@
..()
for (var/i in 1 to 5)
new /obj/effect/spawner/random/clothing/funny_hats(src)
- for (var/i in 1 to 5)
- new /obj/item/mod/module/hat_stabilizer(src)
if(prob(1))
var/our_contents = list()
for(var/obj/item/clothing/head/any_hat in contents)
diff --git a/code/game/objects/structures/crates_lockers/crates/secure.dm b/code/game/objects/structures/crates_lockers/crates/secure.dm
index a550daf05eaa7..595481b707c27 100644
--- a/code/game/objects/structures/crates_lockers/crates/secure.dm
+++ b/code/game/objects/structures/crates_lockers/crates/secure.dm
@@ -22,6 +22,7 @@
/obj/structure/closet/crate/secure/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_NO_MISSING_ITEM_ERROR, TRAIT_GENERIC)
+ ADD_TRAIT(src, TRAIT_NO_MANIFEST_CONTENTS_ERROR, TRAIT_GENERIC)
/obj/structure/closet/crate/secure/take_damage(damage_amount, damage_type = BRUTE, damage_flag = "", sound_effect = TRUE, attack_dir, armour_penetration = 0)
if(prob(tamperproof) && damage_amount >= DAMAGE_PRECISION)
diff --git a/code/game/objects/structures/false_walls.dm b/code/game/objects/structures/false_walls.dm
index a400771bc0cb6..49ce549abd577 100644
--- a/code/game/objects/structures/false_walls.dm
+++ b/code/game/objects/structures/false_walls.dm
@@ -83,7 +83,7 @@
qdel(src)
return T
-/obj/structure/falsewall/tool_act(mob/living/user, obj/item/tool)
+/obj/structure/falsewall/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
if(!opening)
return ..()
to_chat(user, span_warning("You must wait until the door has stopped moving!"))
diff --git a/code/game/objects/structures/fireplace.dm b/code/game/objects/structures/fireplace.dm
index c5bed8caaca37..180b085778abf 100644
--- a/code/game/objects/structures/fireplace.dm
+++ b/code/game/objects/structures/fireplace.dm
@@ -167,6 +167,17 @@
fuel_added = 0
update_appearance()
adjust_light()
+ particles = new /particles/smoke/burning()
+
+ switch(dir)
+ if(SOUTH)
+ particles.position = list(0, 29, 0)
+ if(EAST)
+ particles.position = list(-20, 9, 0)
+ if(WEST)
+ particles.position = list(20, 9, 0)
+ if(NORTH) // there is no icon state for SOUTH
+ QDEL_NULL(particles)
/obj/structure/fireplace/proc/put_out()
STOP_PROCESSING(SSobj, src)
@@ -175,6 +186,7 @@
update_appearance()
adjust_light()
desc = initial(desc)
+ QDEL_NULL(particles)
#undef LOG_BURN_TIMER
#undef PAPER_BURN_TIMER
diff --git a/code/game/objects/structures/fluff.dm b/code/game/objects/structures/fluff.dm
index 78085b37d5503..57207cf7abaed 100644
--- a/code/game/objects/structures/fluff.dm
+++ b/code/game/objects/structures/fluff.dm
@@ -269,24 +269,38 @@
/obj/structure/fluff/tram_rail
name = "tram rail"
desc = "Great for trams, not so great for skating."
- icon = 'icons/obj/fluff/tram_rails.dmi'
+ icon = 'icons/obj/tram/tram_rails.dmi'
icon_state = "rail"
layer = TRAM_RAIL_LAYER
plane = FLOOR_PLANE
- deconstructible = TRUE
+ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
+ deconstructible = FALSE
/obj/structure/fluff/tram_rail/floor
+ name = "tram rail protective cover"
icon_state = "rail_floor"
/obj/structure/fluff/tram_rail/end
icon_state = "railend"
+/obj/structure/fluff/tram_rail/electric
+ desc = "Great for trams, not so great for skating. This one is a power rail."
+
/obj/structure/fluff/tram_rail/anchor
name = "tram rail anchor"
icon_state = "anchor"
+/obj/structure/fluff/tram_rail/electric/anchor
+ name = "tram rail anchor"
+ icon_state = "anchor"
+
+/obj/structure/fluff/tram_rail/electric/attack_hand(mob/living/user, list/modifiers)
+ if(user.electrocute_act(75, src))
+ do_sparks(5, TRUE, src)
+
/obj/structure/fluff/broken_canister_frame
name = "broken canister frame"
+ desc = "A torn apart canister. It looks like some metal can be salvaged with a wrench."
icon_state = "broken_canister"
anchored = FALSE
density = TRUE
diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm
index 0310d903c4865..6d481a0946fdf 100644
--- a/code/game/objects/structures/girders.dm
+++ b/code/game/objects/structures/girders.dm
@@ -17,6 +17,7 @@
/obj/item/stack/sheet/plasteel = 2,
/obj/item/stack/sheet/bronze = 2,
/obj/item/stack/sheet/runed_metal = 1,
+ /obj/item/stack/sheet/titaniumglass = 2,
exotic_material = 2 // this needs to be refactored properly
)
@@ -49,9 +50,14 @@
if(istype(W, /obj/item/gun/energy/plasmacutter))
balloon_alert(user, "slicing apart...")
if(W.use_tool(src, user, 40, volume=100))
- var/obj/item/stack/sheet/iron/M = new (loc, 2)
- if (!QDELETED(M))
- M.add_fingerprint(user)
+ if(state == GIRDER_TRAM)
+ var/obj/item/stack/sheet/mineral/titanium/M = new (user.loc, 2)
+ if(!QDELETED(M))
+ M.add_fingerprint(user)
+ else
+ var/obj/item/stack/sheet/iron/M = new (loc, 2)
+ if(!QDELETED(M))
+ M.add_fingerprint(user)
qdel(src)
return
@@ -63,7 +69,7 @@
balloon_alert(user, "need floor!")
return
if(state == GIRDER_TRAM)
- if(!locate(/obj/structure/industrial_lift/tram) in src.loc.contents)
+ if(!locate(/obj/structure/transport/linear/tram) in src.loc.contents)
balloon_alert(user, "need tram floors!")
return
@@ -128,8 +134,8 @@
if (do_after(user, 4 SECONDS, target = src))
if(sheets.get_amount() < amount)
return
- sheets.use(2)
- var/obj/structure/tramwall/tram_wall = new(loc)
+ sheets.use(amount)
+ var/obj/structure/tram/alt/iron/tram_wall = new(loc)
transfer_fingerprints_to(tram_wall)
qdel(src)
return
@@ -148,6 +154,21 @@
qdel(src)
return
+ if(istype(sheets, /obj/item/stack/sheet/titaniumglass) && state == GIRDER_TRAM)
+ var/amount = construction_cost[/obj/item/stack/sheet/titaniumglass]
+ if(sheets.get_amount() < amount)
+ balloon_alert(user, "need [amount] sheets!")
+ return
+ balloon_alert(user, "adding panel...")
+ if (do_after(user, 2 SECONDS, target = src))
+ if(sheets.get_amount() < amount)
+ return
+ sheets.use(amount)
+ var/obj/structure/tram/tram_wall = new(loc)
+ transfer_fingerprints_to(tram_wall)
+ qdel(src)
+ return
+
if(istype(sheets, /obj/item/stack/sheet/plasteel))
var/amount = construction_cost[/obj/item/stack/sheet/plasteel]
if(state == GIRDER_DISPLACED)
@@ -202,22 +223,17 @@
if(sheets.get_amount() < amount)
balloon_alert(user, "need [amount] sheets!")
return
+ var/tram_wall_type = text2path("/obj/structure/tram/alt/[M]")
+ if(!tram_wall_type)
+ balloon_alert(user, "need titanium glass or mineral!")
+ return
balloon_alert(user, "adding plating...")
if (do_after(user, 4 SECONDS, target = src))
if(sheets.get_amount() < amount)
return
+ var/obj/structure/tram/tram_wall
+ tram_wall = new tram_wall_type(loc)
sheets.use(amount)
- var/obj/structure/tramwall/tram_wall
- var/tram_wall_type = text2path("/obj/structure/tramwall/[M]")
- if(tram_wall_type)
- tram_wall = new tram_wall_type(loc)
- else
- var/obj/structure/tramwall/material/mat_tram_wall = new(loc)
- var/list/material_list = list()
- material_list[GET_MATERIAL_REF(sheets.material_type)] = SHEET_MATERIAL_AMOUNT * 2
- if(material_list)
- mat_tram_wall.set_custom_materials(material_list)
- tram_wall = mat_tram_wall
transfer_fingerprints_to(tram_wall)
qdel(src)
return
@@ -290,9 +306,9 @@
if(state != GIRDER_TRAM)
return
state = GIRDER_DISASSEMBLED
- var/obj/item/stack/sheet/iron/M = new (loc, 2)
- if (!QDELETED(M))
- M.add_fingerprint(user)
+ var/obj/item/stack/sheet/mineral/titanium/material = new (user.loc, 2)
+ if (!QDELETED(material))
+ material.add_fingerprint(user)
qdel(src)
return TRUE
@@ -361,10 +377,12 @@
if((mover.pass_flags & PASSGRILLE) || isprojectile(mover))
return prob(girderpasschance)
-/obj/structure/girder/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
- . = !density
- if(caller)
- . = . || (caller.pass_flags & PASSGRILLE)
+/obj/structure/girder/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
+ if(!density)
+ return TRUE
+ if(pass_info.pass_flags & PASSGRILLE)
+ return TRUE
+ return FALSE
/obj/structure/girder/deconstruct(disassembled = TRUE)
if(!(flags_1 & NODECONSTRUCT_1))
@@ -392,8 +410,15 @@
max_integrity = 350
/obj/structure/girder/tram
- name = "tram girder"
+ name = "tram frame"
+ desc = "Titanium framework to construct tram walls. Can be plated with titanium glass or other wall materials."
+ icon_state = "tram"
state = GIRDER_TRAM
+ density = FALSE
+ obj_flags = CAN_BE_HIT | BLOCK_Z_OUT_DOWN
+
+/obj/structure/girder/tram/corner
+ name = "tram frame corner"
//////////////////////////////////////////// cult girder //////////////////////////////////////////////
diff --git a/code/game/objects/structures/grille.dm b/code/game/objects/structures/grille.dm
index bfe7f0b4a882d..bb82c6fb2949f 100644
--- a/code/game/objects/structures/grille.dm
+++ b/code/game/objects/structures/grille.dm
@@ -52,9 +52,12 @@
/obj/structure/grille/examine(mob/user)
. = ..()
+ if(flags_1 & NODECONSTRUCT_1)
+ return
+
if(anchored)
. += span_notice("It's secured in place with screws. The rods look like they could be cut through.")
- if(!anchored)
+ else
. += span_notice("The anchoring screws are unscrewed. The rods look like they could be cut through.")
/obj/structure/grille/rcd_vals(mob/user, obj/item/construction/rcd/the_rcd)
@@ -175,15 +178,19 @@
if(!. && isprojectile(mover))
return prob(30)
-/obj/structure/grille/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
- . = !density
- if(caller)
- . = . || (caller.pass_flags & PASSGRILLE)
+/obj/structure/grille/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
+ if(!density)
+ return TRUE
+ if(pass_info.pass_flags & PASSGRILLE)
+ return TRUE
+ return FALSE
/obj/structure/grille/wirecutter_act(mob/living/user, obj/item/tool)
add_fingerprint(user)
if(shock(user, 100))
return
+ if(flags_1 & NODECONSTRUCT_1)
+ return FALSE
tool.play_tool_sound(src, 100)
deconstruct()
return TOOL_ACT_TOOLTYPE_SUCCESS
@@ -194,6 +201,8 @@
add_fingerprint(user)
if(shock(user, 90))
return FALSE
+ if(flags_1 & NODECONSTRUCT_1)
+ return FALSE
if(!tool.use_tool(src, user, 0, volume=100))
return FALSE
set_anchored(!anchored)
@@ -350,8 +359,8 @@
var/obj/structure/cable/C = T.get_cable_node()
if(C)
playsound(src, 'sound/magic/lightningshock.ogg', 100, TRUE, extrarange = 5)
- tesla_zap(src, 3, C.newavail() * 0.01, ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_MOB_STUN | ZAP_LOW_POWER_GEN | ZAP_ALLOW_DUPLICATES) //Zap for 1/100 of the amount of power. At a million watts in the grid, it will be as powerful as a tesla revolver shot.
- C.add_delayedload(C.newavail() * 0.0375) // you can gain up to 3.5 via the 4x upgrades power is halved by the pole so thats 2x then 1X then .5X for 3.5x the 3 bounces shock.
+ tesla_zap(source = src, zap_range = 3, power = C.newavail() * 0.01, cutoff = 1e3, zap_flags = ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_MOB_STUN | ZAP_LOW_POWER_GEN | ZAP_ALLOW_DUPLICATES) //Zap for 1/100 of the amount of power. At a million watts in the grid, it will be as powerful as a tesla revolver shot.
+ C.add_delayedload(C.newavail() * 0.0375) // you can gain up to 3.5 via the 4x upgrades power is halved by the pole so thats 2x then 1X then .5X for 3.5x the 3 bounces shock. // What do you mean by this?
return ..()
/obj/structure/grille/get_dumping_location()
diff --git a/code/game/objects/structures/gym/punching_bag.dm b/code/game/objects/structures/gym/punching_bag.dm
index 969e2888d634d..9219153d02619 100644
--- a/code/game/objects/structures/gym/punching_bag.dm
+++ b/code/game/objects/structures/gym/punching_bag.dm
@@ -43,7 +43,17 @@
return
flick("[icon_state]-punch", src)
playsound(loc, pick(hit_sounds), 25, TRUE, -1)
+
+ var/stamina_exhaustion = 3
+ if(ishuman(user))
+ var/mob/living/carbon/human/boxer = user
+ var/obj/item/clothing/gloves/boxing/boxing_gloves = boxer.get_item_by_slot(ITEM_SLOT_GLOVES)
+ if(istype(boxing_gloves))
+ stamina_exhaustion = 2
+
+ user.adjustStaminaLoss(stamina_exhaustion)
user.add_mood_event("exercise", /datum/mood_event/exercise)
+ user.mind?.adjust_experience(/datum/skill/fitness, 0.1)
user.apply_status_effect(/datum/status_effect/exercised)
/obj/structure/punching_bag/wrench_act_secondary(mob/living/user, obj/item/tool)
diff --git a/code/game/objects/structures/gym/weight_machine.dm b/code/game/objects/structures/gym/weight_machine.dm
index 44162d169b566..761db678b3833 100644
--- a/code/game/objects/structures/gym/weight_machine.dm
+++ b/code/game/objects/structures/gym/weight_machine.dm
@@ -1,3 +1,7 @@
+#define WORKOUT_XP 5
+#define EXERCISE_STATUS_DURATION 20 SECONDS
+#define SAFE_DRUNK_LEVEL 39
+
/obj/structure/weightmachine
name = "chest press machine"
desc = "Just looking at this thing makes you feel tired."
@@ -15,6 +19,9 @@
///The weight action we give to people that buckle themselves to us.
var/datum/action/push_weights/weight_action
+ ///message when drunk user fails to use the machine
+ var/drunk_message = "You try for a new record and pull through! Through a muscle that is."
+
///List of messages picked when using the machine.
var/static/list/more_weight = list(
"pushing it to the limit!",
@@ -90,16 +97,43 @@
return TRUE
/obj/structure/weightmachine/proc/perform_workout(mob/living/user)
+ if(user.nutrition <= NUTRITION_LEVEL_STARVING)
+ user.balloon_alert(user, "too hungry to workout!")
+ return
+
user.balloon_alert_to_viewers("[pick(more_weight)]")
START_PROCESSING(SSobj, src)
+
if(do_after(user, 8 SECONDS, src) && user.has_gravity())
- user.Stun(2 SECONDS)
+ // with enough dedication, even clowns can overcome their handicaps
+ var/clumsy_chance = 30 - (user.mind.get_skill_level(/datum/skill/fitness) * 5)
+ if(HAS_TRAIT(user, TRAIT_CLUMSY) && prob(clumsy_chance))
+ playsound(src, 'sound/effects/bang.ogg', 50, TRUE)
+ to_chat(user, span_warning("Your hand slips, causing the [name] to smash you!"))
+ user.take_bodypart_damage(rand(2, 5))
+ end_workout()
+ return
+
+ // awlways a chance for a person not to fail horribly when drunk
+ if(user.get_drunk_amount() > SAFE_DRUNK_LEVEL && prob(min(user.get_drunk_amount(), 99)))
+ playsound(src,'sound/effects/bang.ogg', 50, TRUE)
+ to_chat(user, span_warning(drunk_message))
+ user.take_bodypart_damage(rand(5, 10), wound_bonus = 10)
+ end_workout()
+ return
+
if(issilicon(user))
user.balloon_alert(user, pick(finished_silicon_message))
else
user.balloon_alert(user, pick(finished_message))
+
+ user.adjust_nutrition(-3) // feel the burn
user.add_mood_event("exercise", /datum/mood_event/exercise)
- user.apply_status_effect(/datum/status_effect/exercised)
+
+ // remember the real xp gain is from sleeping after working out
+ user.mind.adjust_experience(/datum/skill/fitness, WORKOUT_XP)
+ user.apply_status_effect(/datum/status_effect/exercised, EXERCISE_STATUS_DURATION)
+
end_workout()
/obj/structure/weightmachine/proc/end_workout()
@@ -119,6 +153,10 @@
animate(user, pixel_y = pixel_shift_y, time = 4)
playsound(user, 'sound/machines/creak.ogg', 60, TRUE)
animate(pixel_y = user.base_pixel_y, time = 4)
+
+ var/stamina_exhaustion = 5 - (user.mind.get_skill_level(/datum/skill/fitness) * 0.5)
+ user.adjustStaminaLoss(stamina_exhaustion * seconds_per_tick)
+
return TRUE
/**
@@ -131,3 +169,8 @@
pixel_shift_y = 5
+ drunk_message = "You raise the bar over you trying to balance it with one hand, keyword tried."
+
+#undef WORKOUT_XP
+#undef EXERCISE_STATUS_DURATION
+#undef SAFE_DRUNK_LEVEL
diff --git a/code/game/objects/structures/holosign.dm b/code/game/objects/structures/holosign.dm
index b3d51ceffa167..bd20ef405fc82 100644
--- a/code/game/objects/structures/holosign.dm
+++ b/code/game/objects/structures/holosign.dm
@@ -139,14 +139,13 @@
density = TRUE
max_integrity = 10
allow_walk = FALSE
+ armor_type = /datum/armor/structure_holosign/cyborg_barrier // Gets a special armor subtype which is extra good at defense.
-/obj/structure/holosign/barrier/cyborg/bullet_act(obj/projectile/P)
- take_damage((P.damage / 5) , BRUTE, MELEE, 1) //Doesn't really matter what damage flag it is.
- if(istype(P, /obj/projectile/energy/electrode))
- take_damage(10, BRUTE, MELEE, 1) //Tasers aren't harmful.
- if(istype(P, /obj/projectile/beam/disabler))
- take_damage(5, BRUTE, MELEE, 1) //Disablers aren't harmful.
- return BULLET_ACT_HIT
+/datum/armor/structure_holosign/cyborg_barrier
+ bullet = 80
+ laser = 80
+ energy = 80
+ melee = 20
/obj/structure/holosign/barrier/medical
name = "\improper PENLITE holobarrier"
@@ -154,17 +153,10 @@
icon_state = "holo_medical"
alpha = 125 //lazy :)
max_integrity = 1
- var/force_allaccess = FALSE
var/buzzcd = 0
-/obj/structure/holosign/barrier/medical/examine(mob/user)
- . = ..()
- . += span_notice("The biometric scanners are [force_allaccess ? "off" : "on"].")
-
/obj/structure/holosign/barrier/medical/CanAllowThrough(atom/movable/mover, border_dir)
. = ..()
- if(force_allaccess)
- return TRUE
if(istype(mover, /obj/vehicle/ridden))
for(var/M in mover.buckled_mobs)
if(ishuman(M))
@@ -189,23 +181,13 @@
return FALSE
return TRUE
-/obj/structure/holosign/barrier/medical/attack_hand(mob/living/user, list/modifiers)
- if(!user.combat_mode && CanPass(user, get_dir(src, user)))
- force_allaccess = !force_allaccess
- to_chat(user, span_warning("You [force_allaccess ? "deactivate" : "activate"] the biometric scanners.")) //warning spans because you can make the station sick!
- else
- return ..()
-
/obj/structure/holosign/barrier/cyborg/hacked
name = "Charged Energy Field"
desc = "A powerful energy field that blocks movement. Energy arcs off it."
max_integrity = 20
+ armor_type = /datum/armor/structure_holosign //Yeah no this doesn't get projectile resistance.
var/shockcd = 0
-/obj/structure/holosign/barrier/cyborg/hacked/bullet_act(obj/projectile/P)
- take_damage(P.damage, BRUTE, MELEE, 1) //Yeah no this doesn't get projectile resistance.
- return BULLET_ACT_HIT
-
/obj/structure/holosign/barrier/cyborg/hacked/proc/cooldown()
shockcd = FALSE
diff --git a/code/game/objects/structures/maintenance.dm b/code/game/objects/structures/maintenance.dm
index 33f27f40c4739..acdf28353d68d 100644
--- a/code/game/objects/structures/maintenance.dm
+++ b/code/game/objects/structures/maintenance.dm
@@ -267,6 +267,7 @@ at the cost of risking a vicious bite.**/
COMSIG_ATOM_EXIT = PROC_REF(blow_steam),
)
AddElement(/datum/element/connect_loc, loc_connections)
+ register_context()
update_icon_state()
/obj/structure/steam_vent/attack_hand(mob/living/user, list/modifiers)
@@ -283,6 +284,16 @@ at the cost of risking a vicious bite.**/
return
blow_steam()
+/obj/structure/steam_vent/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ if(isnull(held_item))
+ context[SCREENTIP_CONTEXT_LMB] = vent_active ? "Close valve" : "Open valve"
+ return CONTEXTUAL_SCREENTIP_SET
+ if(held_item.tool_behaviour == TOOL_WRENCH)
+ context[SCREENTIP_CONTEXT_RMB] = "Deconstruct"
+ return CONTEXTUAL_SCREENTIP_SET
+ return .
+
/obj/structure/steam_vent/wrench_act_secondary(mob/living/user, obj/item/tool)
. = ..()
if(vent_active)
diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm
index 264a4b17bb6b1..3e2b5cd8a1e3e 100644
--- a/code/game/objects/structures/mirror.dm
+++ b/code/game/objects/structures/mirror.dm
@@ -26,7 +26,6 @@
integrity_failure = 0.5
max_integrity = 200
var/list/mirror_options = INERT_MIRROR_OPTIONS
- var/magical_mirror = FALSE
///Flags this race must have to be selectable with this type of mirror.
var/race_flags = MIRROR_MAGIC
@@ -83,7 +82,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
if(. || !ishuman(user) || broken)
return TRUE
- if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH) && !magical_mirror)
+ if(!istype(src, /obj/structure/mirror/magic) && !user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
return TRUE //no tele-grooming (if nonmagical)
return display_radial_menu(user)
@@ -110,20 +109,26 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
return display_radial_menu(user)
/obj/structure/mirror/proc/change_beard(mob/living/carbon/human/beard_dresser)
- if(beard_dresser.physique != FEMALE && !magical_mirror)
- var/new_style = tgui_input_list(beard_dresser, "Select a facial hairstyle", "Grooming", GLOB.facial_hairstyles_list)
- if(isnull(new_style))
- return TRUE
- if(HAS_TRAIT(beard_dresser, TRAIT_SHAVED))
- to_chat(beard_dresser, span_notice("If only growing back facial hair were that easy for you... The reminder makes you feel terrible."))
- beard_dresser.add_mood_event("bald_hair_day", /datum/mood_event/bald_reminder)
- return TRUE
- beard_dresser.set_facial_hairstyle(new_style, update = TRUE)
- else
+ if(beard_dresser.physique == FEMALE)
if(beard_dresser.facial_hairstyle == "Shaved")
- to_chat(beard_dresser, span_notice("You realize you don't have any facial hair."))
- return
- beard_dresser.set_facial_hairstyle("Shaved", update = TRUE)
+ balloon_alert(beard_dresser, "nothing to shave!")
+ return TRUE
+ var/shave_beard = tgui_alert(beard_dresser, "Shave your beard?", "Grooming", list("Yes", "No"))
+ if(shave_beard == "Yes")
+ beard_dresser.set_facial_hairstyle("Shaved", update = TRUE)
+ return TRUE
+
+ var/new_style = tgui_input_list(beard_dresser, "Select a facial hairstyle", "Grooming", GLOB.facial_hairstyles_list)
+
+ if(isnull(new_style))
+ return TRUE
+
+ if(HAS_TRAIT(beard_dresser, TRAIT_SHAVED))
+ to_chat(beard_dresser, span_notice("If only growing back facial hair were that easy for you... The reminder makes you feel terrible."))
+ beard_dresser.add_mood_event("bald_hair_day", /datum/mood_event/bald_reminder)
+ return TRUE
+
+ beard_dresser.set_facial_hairstyle(new_style, update = TRUE)
/obj/structure/mirror/proc/change_hair(mob/living/carbon/human/hairdresser)
var/new_style = tgui_input_list(hairdresser, "Select a hairstyle", "Grooming", GLOB.hairstyles_list)
@@ -152,17 +157,17 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
var/racechoice = tgui_input_list(race_changer, "What are we again?", "Race change", selectable_races)
if(isnull(racechoice))
return TRUE
- if(!selectable_races[racechoice])
- return TRUE
-
- var/datum/species/newrace = new selectable_races[racechoice]
+ var/new_race_path = selectable_races[racechoice]
+ if(!ispath(new_race_path, /datum/species))
+ return TRUE
+ var/datum/species/newrace = new new_race_path()
var/attributes_desc = newrace.get_physical_attributes()
- qdel(newrace)
var/answer = tgui_alert(race_changer, attributes_desc, "Become a [newrace]?", list("Yes", "No"))
if(answer != "Yes")
+ qdel(newrace)
change_race(race_changer) // try again
return
@@ -242,7 +247,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
. = ..()
if(broken) // breaking a mirror truly gets you bad luck!
to_chat(user, span_warning("A chill runs down your spine as [src] shatters..."))
- user.AddComponent(/datum/component/omen)
+ user.AddComponent(/datum/component/omen, incidents_left = 7)
/obj/structure/mirror/bullet_act(obj/projectile/P)
if(broken || !isliving(P.firer) || !P.damage)
@@ -252,7 +257,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
if(broken) // breaking a mirror truly gets you bad luck!
var/mob/living/unlucky_dude = P.firer
to_chat(unlucky_dude, span_warning("A chill runs down your spine as [src] shatters..."))
- unlucky_dude.AddComponent(/datum/component/omen)
+ unlucky_dude.AddComponent(/datum/component/omen, incidents_left = 7)
/obj/structure/mirror/atom_break(damage_flag, mapload)
. = ..()
@@ -317,7 +322,6 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
desc = "Turn and face the strange... face."
icon_state = "magic_mirror"
mirror_options = MAGIC_MIRROR_OPTIONS
- magical_mirror = TRUE
/obj/structure/mirror/magic/Initialize(mapload)
. = ..()
@@ -329,6 +333,13 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
selectable_races[initial(species_type.name)] = species_type
selectable_races = sort_list(selectable_races)
+/obj/structure/mirror/magic/change_beard(mob/living/carbon/human/beard_dresser) // magical mirrors do nothing but give you the damn beard
+ var/new_style = tgui_input_list(beard_dresser, "Select a facial hairstyle", "Grooming", GLOB.facial_hairstyles_list)
+ if(isnull(new_style))
+ return TRUE
+ beard_dresser.set_facial_hairstyle(new_style, update = TRUE)
+ return TRUE
+
//Magic mirrors can change hair color as well
/obj/structure/mirror/magic/change_hair(mob/living/carbon/human/user)
var/hairchoice = tgui_alert(user, "Hairstyle or hair color?", "Change Hair", list("Style", "Color"))
@@ -349,7 +360,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
/obj/structure/mirror/magic/attack_hand(mob/living/carbon/human/user)
. = ..()
- if(!.)
+ if(.)
return TRUE
if(HAS_TRAIT(user, TRAIT_ADVANCEDTOOLUSER) && HAS_TRAIT(user, TRAIT_LITERATE))
@@ -358,7 +369,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
to_chat(user, span_alert("You feel quite intelligent."))
// Prevents wizards from being soft locked out of everything
// If this stays after the species was changed once more, well, the magic mirror did it. It's magic i aint gotta explain shit
- ADD_TRAIT(user, list(TRAIT_LITERATE, TRAIT_ADVANCEDTOOLUSER), SPECIES_TRAIT)
+ user.add_traits(list(TRAIT_LITERATE, TRAIT_ADVANCEDTOOLUSER), SPECIES_TRAIT)
return TRUE
/obj/structure/mirror/magic/lesser/Initialize(mapload)
@@ -377,11 +388,13 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
/obj/structure/mirror/magic/pride/attack_hand(mob/living/carbon/human/user)
. = ..()
- if(!.)
+ if(.)
return TRUE
- user.visible_message(span_danger("The ground splits beneath [user] as [user.p_their()] hand leaves the mirror!"), \
- span_notice("Perfect. Much better! Now nobody will be able to resist yo-"))
+ user.visible_message(
+ span_bolddanger("The ground splits beneath [user] as [user.p_their()] hand leaves the mirror!"),
+ span_notice("Perfect. Much better! Now nobody will be able to resist yo-"),
+ )
var/turf/user_turf = get_turf(user)
var/list/levels = SSmapping.levels_by_trait(ZTRAIT_SPACE_RUINS)
diff --git a/code/game/objects/structures/plaques/static_plaques.dm b/code/game/objects/structures/plaques/static_plaques.dm
index c37ddc4ff6759..06538ea2ef1f1 100644
--- a/code/game/objects/structures/plaques/static_plaques.dm
+++ b/code/game/objects/structures/plaques/static_plaques.dm
@@ -19,6 +19,84 @@
/obj/structure/plaque/static_plaque/golden/captain
name = "The Most Robust Captain Award for Robustness"
+/obj/structure/plaque/static_plaque/tram
+ /// The tram we have info about
+ var/specific_transport_id = TRAMSTATION_LINE_1
+ /// Weakref to the tram we have info about
+ var/datum/weakref/transport_ref
+ /// Serial number of the tram
+ var/tram_serial
+ name = "\improper tram information plate"
+ icon_state = "commission_tram"
+ custom_materials = list(/datum/material/titanium = SHEET_MATERIAL_AMOUNT)
+ layer = SIGN_LAYER
+
+/obj/structure/plaque/static_plaque/tram/Initialize(mapload)
+ . = ..()
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/structure/plaque/static_plaque/tram/LateInitialize(mapload)
+ . = ..()
+ link_tram()
+ set_tram_serial()
+
+/obj/structure/plaque/static_plaque/tram/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ if(isnull(held_item))
+ context[SCREENTIP_CONTEXT_LMB] = "View details"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/structure/plaque/static_plaque/tram/proc/link_tram()
+ for(var/datum/transport_controller/linear/tram/tram as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
+ if(tram.specific_transport_id == specific_transport_id)
+ transport_ref = WEAKREF(tram)
+ break
+
+/obj/structure/plaque/static_plaque/tram/proc/set_tram_serial()
+ var/datum/transport_controller/linear/tram/tram = transport_ref?.resolve()
+ if(isnull(tram) || isnull(tram.tram_registration))
+ return
+
+ tram_serial = tram.tram_registration.serial_number
+ desc = "A plate showing details from the manufacturer about this Nakamura Engineering SkyyTram Mk VI, serial number [tram_serial].
We are not responsible for any injuries or fatalities caused by usage of the tram. \
+ Using the tram carries inherent risks, and we cannot guarantee the safety of all passengers. By using the tram, you assume, acknowledge, and accept all the risks and responsibilities.
\
+ Please be aware that riding the tram can cause a variety of injuries, including but not limited to: slips, trips, and falls; collisions with other passengers or objects; strains, sprains, and other musculoskeletal injuries; \
+ cuts, bruises, and lacerations; and more severe injuries such as head trauma, spinal cord injuries, and even death. These injuries can be caused by a variety of factors, including the movements of the tram, the behaviour \
+ of other passengers, and unforeseen circumstances such as foul play or mechanical issues.
\
+ By entering the tram, guideway, or crossings you agree Nanotrasen is not liable for any injuries, damages, or losses that may occur. If you do not agree to these terms, please do not use the tram. "
+
+/obj/structure/plaque/static_plaque/tram/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "TramPlaque")
+ ui.autoupdate = FALSE
+ ui.open()
+
+/obj/structure/plaque/static_plaque/tram/ui_static_data(mob/user)
+ var/datum/transport_controller/linear/tram/tram = transport_ref?.resolve()
+ var/list/data = list()
+ var/list/current_tram = list()
+ var/list/previous_trams = list()
+
+ current_tram += list(list(
+ "serialNumber" = tram.tram_registration.serial_number,
+ "mfgDate" = tram.tram_registration.mfg_date,
+ "distanceTravelled" = tram.tram_registration.distance_travelled,
+ "tramCollisions" = tram.tram_registration.collisions,
+ ))
+
+ for(var/datum/tram_mfg_info/previous_tram as anything in tram.tram_history)
+ previous_trams += list(list(
+ "serialNumber" = previous_tram.serial_number,
+ "mfgDate" = previous_tram.mfg_date,
+ "distanceTravelled" = previous_tram.distance_travelled,
+ "tramCollisions" = previous_tram.collisions,
+ ))
+
+ data["currentTram"] = current_tram
+ data["previousTrams"] = previous_trams
+ return data
+
// Commission plaques, to give a little backstory to the stations. Commission dates are date of merge (or best approximation, in the case of Meta) + 540 years to convert to SS13 dates.
// Where PRs are available, I've linked them. Where they are unavailable, a git hash is provided instead for the direct commit that added/removed the map.
// Please enjoy this trip through SS13's history.
@@ -149,15 +227,3 @@
/obj/structure/sign/plaques/kiddie/gameoflife
name = "\improper Conway's The Game Of Life plaque"
desc = "A plaque detailing the historical significance of The Game Of Life in the field of computer science, and that the mural underfoot is a representation of the game in action."
-
-/obj/structure/sign/plaques/tram
- name = "\improper tram information plate"
- desc = "A plate showing details from the manufacturer about this Nakamura Engineering SkyyTram Mk IV, serial number LT304TG2563.
We are not responsible for any injuries or fatalities caused by usage of the tram. \
- Using the tram carries inherent risks, and we cannot guarantee the safety of all passengers. By using the tram, you assume, acknowledge, and accept all the risks and responsibilities.
\
- Please be aware that riding the tram can cause a variety of injuries, including but not limited to: slips, trips, and falls; collisions with other passengers or objects; strains, sprains, and other musculoskeletal injuries; \
- cuts, bruises, and lacerations; and more severe injuries such as head trauma, spinal cord injuries, and even death. These injuries can be caused by a variety of factors, including the movements of the tram, the behaviour \
- of other passengers, and unforeseen circumstances such as foul play or mechanical issues.
\
- By entering the tram, guideway, or crossings you agree Nanotrasen is not liable for any injuries, damages, or losses that may occur. If you do not agree to these terms, please do not use the tram. "
- icon_state = "commission_tram"
- custom_materials = list(/datum/material/titanium =SHEET_MATERIAL_AMOUNT)
- plane = FLOOR_PLANE
diff --git a/code/game/objects/structures/plasticflaps.dm b/code/game/objects/structures/plasticflaps.dm
index 7136f28a44cd2..31d29e5b4f0b0 100644
--- a/code/game/objects/structures/plasticflaps.dm
+++ b/code/game/objects/structures/plasticflaps.dm
@@ -83,18 +83,15 @@
return FALSE
return TRUE
-/obj/structure/plasticflaps/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
- if(isliving(caller))
- if(isbot(caller))
+/obj/structure/plasticflaps/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
+ if(pass_info.is_living)
+ if(pass_info.is_bot)
return TRUE
-
- var/mob/living/living_caller = caller
- var/ventcrawler = HAS_TRAIT(living_caller, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(living_caller, TRAIT_VENTCRAWLER_NUDE)
- if(!ventcrawler && living_caller.mob_size != MOB_SIZE_TINY)
+ if(pass_info.can_ventcrawl && pass_info.mob_size != MOB_SIZE_TINY)
return FALSE
- if(caller?.pulling)
- return CanAStarPass(ID, to_dir, caller.pulling, no_id = no_id)
+ if(pass_info.pulling_info)
+ return CanAStarPass(to_dir, pass_info.pulling_info)
return TRUE //diseases, stings, etc can pass
diff --git a/code/game/objects/structures/railings.dm b/code/game/objects/structures/railings.dm
index f88ba15ecc52c..5aaed01e0b86c 100644
--- a/code/game/objects/structures/railings.dm
+++ b/code/game/objects/structures/railings.dm
@@ -63,6 +63,13 @@
AddComponent(/datum/component/simple_rotation, ROTATION_NEEDS_ROOM)
+/obj/structure/railing/examine(mob/user)
+ . = ..()
+ if(anchored == TRUE)
+ . += span_notice("The railing is bolted to the floor.")
+ else
+ . += span_notice("The railing is unbolted from the floor and can be deconstructed with wirecutters.")
+
/obj/structure/railing/attackby(obj/item/I, mob/living/user, params)
..()
add_fingerprint(user)
@@ -117,7 +124,7 @@
return . || mover.throwing || mover.movement_type & (FLYING | FLOATING)
return TRUE
-/obj/structure/railing/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
+/obj/structure/railing/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
if(!(to_dir & dir))
return TRUE
return ..()
diff --git a/code/game/objects/structures/safe.dm b/code/game/objects/structures/safe.dm
index 5d42331b5df6d..9c1a8f1c4f3c0 100644
--- a/code/game/objects/structures/safe.dm
+++ b/code/game/objects/structures/safe.dm
@@ -121,9 +121,9 @@ FLOOR SAFES
if(open)
var/list/contents_names = list()
data["contents"] = contents_names
- for(var/obj/O in contents)
- contents_names[++contents_names.len] = list("name" = O.name, "sprite" = O.icon_state)
- user << browse_rsc(icon(O.icon, O.icon_state), "[O.icon_state].png")
+ for(var/obj/jewel in contents)
+ contents_names[++contents_names.len] = list("name" = jewel.name, "sprite" = jewel.icon_state)
+ user << browse_rsc(icon(jewel.icon, jewel.icon_state), "[jewel.icon_state].png")
return data
diff --git a/code/game/objects/structures/spawner.dm b/code/game/objects/structures/spawner.dm
index a36a4b927a2a2..29a63b1bff808 100644
--- a/code/game/objects/structures/spawner.dm
+++ b/code/game/objects/structures/spawner.dm
@@ -8,11 +8,12 @@
anchored = TRUE
density = TRUE
+ faction = list(FACTION_HOSTILE)
+
var/max_mobs = 5
var/spawn_time = 30 SECONDS
var/mob_types = list(/mob/living/basic/carp)
var/spawn_text = "emerges from"
- var/faction = list(FACTION_HOSTILE)
var/spawner_type = /datum/component/spawner
/// Is this spawner taggable with something?
var/scanner_taggable = FALSE
@@ -76,7 +77,7 @@
icon = 'icons/obj/device.dmi'
icon_state = "syndbeacon"
spawn_text = "warps in from"
- mob_types = list(/mob/living/basic/syndicate/ranged)
+ mob_types = list(/mob/living/basic/trooper/syndicate/ranged)
faction = list(ROLE_SYNDICATE)
mob_gps_id = "SYN" // syndicate
spawner_gps_id = "Hostile Warp Beacon"
@@ -89,7 +90,7 @@
max_integrity = 150
max_mobs = 15
spawn_time = 15 SECONDS
- mob_types = list(/mob/living/simple_animal/hostile/skeleton)
+ mob_types = list(/mob/living/basic/skeleton)
spawn_text = "climbs out of"
faction = list(FACTION_SKELETON)
mob_gps_id = "SKL" // skeletons
diff --git a/code/game/objects/structures/spirit_board.dm b/code/game/objects/structures/spirit_board.dm
index d30e13630520f..e8882251237fd 100644
--- a/code/game/objects/structures/spirit_board.dm
+++ b/code/game/objects/structures/spirit_board.dm
@@ -6,77 +6,106 @@
resistance_flags = FLAMMABLE
density = TRUE
anchored = FALSE
+ /// Whether no one has moved the planchette yet.
var/virgin = TRUE //applies especially to admins
- var/next_use = 0
- var/planchette = "A"
+ /// How long between planchette movements.
+ COOLDOWN_DECLARE(next_use)
+ /// Where the planchette is currently pointing.
+ var/planchette
+ /// Ckey of last mob to use the board.
var/lastuser = null
+ /// List of options ghosts (or people) can pick from.
+ var/list/ghosty_options = list(
+ "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z",
+ "1","2","3","4","5","6","7","8","9","0",
+ "Yes","No",
+ )
+ /// Number of living, willing mobs adjacent to the board required for a seance to occur.
+ var/required_user_count = 2
+
+/obj/structure/spirit_board/Initialize(mapload)
+ . = ..()
+ if(prob(1))
+ name = "luigi board"
+ planchette = ghosty_options[1]
/obj/structure/spirit_board/examine()
- desc = "[initial(desc)] The planchette is sitting at \"[planchette]\"."
. = ..()
+ if(planchette)
+ . += span_notice("The planchette is currently at the letter \"[planchette]\".")
+ else
+ . += span_notice("The planchette is in the middle of the board on no particular letter.")
/obj/structure/spirit_board/attack_hand(mob/user, list/modifiers)
. = ..()
if(.)
- return
+ return .
spirit_board_pick_letter(user)
+ return TRUE
-
-//ATTACK GHOST IGNORING PARENT RETURN VALUE
/obj/structure/spirit_board/attack_ghost(mob/dead/observer/user)
+ . = ..()
+ if(.)
+ return .
spirit_board_pick_letter(user)
- return ..()
+ return TRUE
-/obj/structure/spirit_board/proc/spirit_board_pick_letter(mob/M)
- if(!spirit_board_checks(M))
- return FALSE
+/obj/structure/spirit_board/proc/spirit_board_pick_letter(mob/ghost)
+ if(!spirit_board_checks(ghost))
+ return
if(virgin)
virgin = FALSE
- notify_ghosts("Someone has begun playing with a [src.name] in [get_area(src)]!", source = src, header = "Spirit board")
+ notify_ghosts(
+ "Someone has begun playing with \a [src] in [get_area(src)]!",
+ source = src,
+ header = "Spirit board",
+ )
- planchette = tgui_input_list(M, "Choose the letter.", "Seance!", list("A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"))
- if(isnull(planchette))
+ var/new_planchette = tgui_input_list(ghost, "Choose the letter.", "Seance!", ghosty_options)
+ if(isnull(new_planchette))
return
- if(!Adjacent(M) || next_use > world.time)
+ if(!Adjacent(ghost) || !COOLDOWN_FINISHED(src, next_use))
return
- M.log_message("picked a letter on [src], which was \"[planchette]\".", LOG_GAME)
- next_use = world.time + rand(30,50)
- lastuser = M.ckey
- //blind message is the same because not everyone brings night vision to seances
- var/msg = span_notice("The planchette slowly moves... and stops at the letter \"[planchette]\".")
- visible_message(msg,"",msg)
+ planchette = new_planchette
+ ghost.log_message("picked a letter on [src], which was \"[planchette]\".", LOG_GAME)
+ COOLDOWN_START(src, next_use, rand(3 SECONDS, 5 SECONDS))
+ lastuser = ghost.ckey
+ for(var/mob/viewer in range(2, src))
+ if(isnull(viewer.client))
+ continue
+ if(viewer.stat != CONSCIOUS && viewer.stat != DEAD) // You gotta be awake or dead to pay the toll
+ continue
+ if(viewer.is_blind())
+ to_chat(viewer, span_hear("You hear a scraping sound..."))
+ else
+ to_chat(viewer, span_notice("The planchette slowly moves... and stops at the letter \"[planchette]\"."))
-/obj/structure/spirit_board/proc/spirit_board_checks(mob/M)
- //cooldown
- var/bonus = 0
- if(M.ckey == lastuser)
- bonus = 10 //Give some other people a chance, hog.
+/obj/structure/spirit_board/proc/spirit_board_checks(mob/ghost)
+ var/cd_penalty = (ghost.ckey == lastuser) ? 1 SECONDS : 0 SECONDS //Give some other people a chance, hog.
- if(next_use - bonus > world.time )
+ if(next_use - cd_penalty > world.time)
return FALSE //No feedback here, hiding the cooldown a little makes it harder to tell who's really picking letters.
- //lighting check
- var/light_amount = 0
- var/turf/T = get_turf(src)
- light_amount = T.get_lumcount()
+ var/turf/play_turf = get_turf(src)
+ if(play_turf?.get_lumcount() > 0.2)
+ to_chat(ghost, span_warning("It's too bright here to use [src]!"))
+ return FALSE
+ if(required_user_count > 0)
+ var/users_in_range = 0
+ for(var/mob/living/player in orange(1, src))
+ if(isnull(player.ckey) || isnull(player.client))
+ continue
- if(light_amount > 0.2)
- to_chat(M, span_warning("It's too bright here to use [src.name]!"))
- return FALSE
+ if(player.client?.is_afk() || player.stat != CONSCIOUS || HAS_TRAIT(player, TRAIT_HANDS_BLOCKED))//no playing with braindeads or corpses or handcuffed dudes.
+ to_chat(ghost, span_warning("[player] doesn't seem to be paying attention..."))
+ continue
- //mobs in range check
- var/users_in_range = 0
- for(var/mob/living/L in orange(1,src))
- if(L.ckey && L.client)
- if((world.time - L.client.inactivity) < (world.time - 300) || L.stat != CONSCIOUS || HAS_TRAIT(L, TRAIT_HANDS_BLOCKED))//no playing with braindeads or corpses or handcuffed dudes.
- to_chat(M, span_warning("[L] doesn't seem to be paying attention..."))
- else
- users_in_range++
+ users_in_range++
- if(users_in_range < 2)
- to_chat(M, span_warning("There aren't enough people to use the [src.name]!"))
- return FALSE
+ if(users_in_range < required_user_count)
+ to_chat(ghost, span_warning("There aren't enough people around to use [src]!"))
+ return FALSE
return TRUE
diff --git a/code/game/objects/structures/stairs.dm b/code/game/objects/structures/stairs.dm
index 5e4078b5afa53..169f76a4b5708 100644
--- a/code/game/objects/structures/stairs.dm
+++ b/code/game/objects/structures/stairs.dm
@@ -94,7 +94,8 @@
var/turf/checking = get_step_multiz(get_turf(src), UP)
if(!istype(checking))
return
- if(!checking.zPassIn(climber, UP, get_turf(src)))
+ // I'm only interested in if the pass is unobstructed, not if the mob will actually make it
+ if(!climber.can_z_move(UP, get_turf(src), checking, z_move_flags = ZMOVE_ALLOW_BUCKLED))
return
var/turf/target = get_step_multiz(get_turf(src), (dir|UP))
if(istype(target) && !climber.can_z_move(DOWN, target, z_move_flags = ZMOVE_FALL_FLAGS)) //Don't throw them into a tile that will just dump them back down.
diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm
index 4da8b734bb4da..226ae90ee6a52 100644
--- a/code/game/objects/structures/tables_racks.dm
+++ b/code/game/objects/structures/tables_racks.dm
@@ -149,10 +149,12 @@
if(locate(/obj/structure/table) in get_turf(mover))
return TRUE
-/obj/structure/table/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
- . = !density
- if(caller)
- . = . || (caller.pass_flags & PASSTABLE)
+/obj/structure/table/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
+ if(!density)
+ return TRUE
+ if(pass_info.pass_flags & PASSTABLE)
+ return TRUE
+ return FALSE
/obj/structure/table/proc/tableplace(mob/living/user, mob/living/pushed_mob)
pushed_mob.forceMove(loc)
@@ -290,7 +292,7 @@
..()
return SECONDARY_ATTACK_CONTINUE_CHAIN
-/obj/structure/table/proc/AfterPutItemOnTable(obj/item/I, mob/living/user)
+/obj/structure/table/proc/AfterPutItemOnTable(obj/item/thing, mob/living/user)
return
/obj/structure/table/deconstruct(disassembled = TRUE, wrench_disassembly = 0)
@@ -356,34 +358,52 @@
canSmoothWith = null
icon = 'icons/obj/smooth_structures/rollingtable.dmi'
icon_state = "rollingtable"
- var/list/attached_items = list()
+ /// Lazylist of the items that we have on our surface.
+ var/list/attached_items = null
/obj/structure/table/rolling/Initialize(mapload)
. = ..()
AddElement(/datum/element/noisy_movement)
+ RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(on_our_moved))
-/obj/structure/table/rolling/AfterPutItemOnTable(obj/item/I, mob/living/user)
+/obj/structure/table/rolling/Destroy()
+ for(var/item in attached_items)
+ clear_item_reference(item)
+ LAZYNULL(attached_items) // safety
+ return ..()
+
+/obj/structure/table/rolling/AfterPutItemOnTable(obj/item/thing, mob/living/user)
. = ..()
- attached_items += I
- RegisterSignal(I, COMSIG_MOVABLE_MOVED, PROC_REF(RemoveItemFromTable)) //Listen for the pickup event, unregister on pick-up so we aren't moved
+ LAZYADD(attached_items, thing)
+ RegisterSignal(thing, COMSIG_MOVABLE_MOVED, PROC_REF(on_item_moved))
-/obj/structure/table/rolling/proc/RemoveItemFromTable(datum/source, newloc, dir)
+/// Handles cases where any attached item moves, with or without the table. If we get picked up or anything, unregister the signal so we don't move with the table after removal from the surface.
+/obj/structure/table/rolling/proc/on_item_moved(datum/source, atom/old_loc, dir, forced, list/old_locs, momentum_change)
SIGNAL_HANDLER
- if(newloc != loc) //Did we not move with the table? because that shit's ok
- return FALSE
- attached_items -= source
- UnregisterSignal(source, COMSIG_MOVABLE_MOVED)
+ var/atom/thing = source // let it runtime if it doesn't work because that is mad wack
+ if(thing.loc == loc) // if we move with the table, move on
+ return
-/obj/structure/table/rolling/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
- . = ..()
- if(!loc)
+ clear_item_reference(thing)
+
+/// Handles movement of the table itself, as well as moving along any atoms we have on our surface.
+/obj/structure/table/rolling/proc/on_our_moved(datum/source, atom/old_loc, dir, forced, list/old_locs, momentum_change)
+ SIGNAL_HANDLER
+ if(isnull(loc)) // aw hell naw
return
+
for(var/mob/living/living_mob in old_loc.contents)//Kidnap everyone on top
living_mob.forceMove(loc)
+
for(var/atom/movable/attached_movable as anything in attached_items)
- if(!attached_movable.Move(loc))
- RemoveItemFromTable(attached_movable, attached_movable.loc)
+ if(!attached_movable.Move(loc)) // weird
+ clear_item_reference(attached_movable) // we check again in on_item_moved() just in case something's wacky tobaccy
+
+/// Removes the signal and the entrance from the list.
+/obj/structure/table/rolling/proc/clear_item_reference(obj/item/thing)
+ UnregisterSignal(thing, COMSIG_MOVABLE_MOVED)
+ LAZYREMOVE(attached_items, thing)
/*
* Glass tables
diff --git a/code/game/objects/structures/traps.dm b/code/game/objects/structures/traps.dm
index 124237fa59719..1b6f1da06b6ea 100644
--- a/code/game/objects/structures/traps.dm
+++ b/code/game/objects/structures/traps.dm
@@ -8,7 +8,7 @@
alpha = 30 //initially quite hidden when not "recharging"
var/flare_message = "the trap flares brightly!"
var/last_trigger = 0
- var/time_between_triggers = 600 //takes a minute to recharge
+ var/time_between_triggers = 1 MINUTES
var/charges = INFINITY
var/antimagic_flags = MAGIC_RESISTANCE
@@ -61,50 +61,50 @@
last_trigger = world.time
charges--
if(charges <= 0)
- animate(src, alpha = 0, time = 10)
- QDEL_IN(src, 10)
+ animate(src, alpha = 0, time = 1 SECONDS)
+ QDEL_IN(src, 1 SECONDS)
else
animate(src, alpha = initial(alpha), time = time_between_triggers)
-/obj/structure/trap/proc/on_entered(datum/source, atom/movable/AM)
+/obj/structure/trap/proc/on_entered(datum/source, atom/movable/victim)
SIGNAL_HANDLER
if(last_trigger + time_between_triggers > world.time)
return
// Don't want the traps triggered by sparks, ghosts or projectiles.
- if(is_type_in_typecache(AM, ignore_typecache))
+ if(is_type_in_typecache(victim, ignore_typecache))
return
- if(ismob(AM))
- var/mob/M = AM
- if(M.mind in immune_minds)
+ if(ismob(victim))
+ var/mob/mob_victim = victim
+ if(mob_victim.mind in immune_minds)
return
- if(M.can_block_magic(antimagic_flags))
+ if(mob_victim.can_block_magic(antimagic_flags))
flare()
return
if(charges <= 0)
return
flare()
- if(isliving(AM))
- trap_effect(AM)
+ if(isliving(victim))
+ trap_effect(victim)
-/obj/structure/trap/proc/trap_effect(mob/living/L)
+/obj/structure/trap/proc/trap_effect(mob/living/victim)
return
/obj/structure/trap/stun
name = "shock trap"
desc = "A trap that will shock and render you immobile. You'd better avoid it."
icon_state = "trap-shock"
- var/stun_time = 100
+ var/stun_time = 10 SECONDS
-/obj/structure/trap/stun/trap_effect(mob/living/L)
- L.electrocute_act(30, src, flags = SHOCK_NOGLOVES) // electrocute act does a message.
- L.Paralyze(stun_time)
+/obj/structure/trap/stun/trap_effect(mob/living/victim)
+ victim.electrocute_act(30, src, flags = SHOCK_NOGLOVES) // electrocute act does a message.
+ victim.Paralyze(stun_time)
/obj/structure/trap/stun/hunter
name = "bounty trap"
desc = "A trap that only goes off when a fugitive steps on it, announcing the location and stunning the target. You'd better avoid it."
icon = 'icons/obj/restraints.dmi'
icon_state = "bounty_trap_on"
- stun_time = 200
+ stun_time = 20 SECONDS
sparks = FALSE //the item version gives them off to prevent runtimes (see Destroy())
antimagic_flags = NONE
var/obj/item/bountytrap/stored_item
@@ -112,7 +112,7 @@
/obj/structure/trap/stun/hunter/Initialize(mapload)
. = ..()
- time_between_triggers = 10
+ time_between_triggers = 1 SECONDS
flare_message = "[src] snaps shut!"
/obj/structure/trap/stun/hunter/Destroy()
@@ -121,10 +121,10 @@
stored_item = null
return ..()
-/obj/structure/trap/stun/hunter/on_entered(datum/source, atom/movable/AM)
- if(isliving(AM))
- var/mob/living/L = AM
- if(!L.mind?.has_antag_datum(/datum/antagonist/fugitive))
+/obj/structure/trap/stun/hunter/on_entered(datum/source, atom/movable/victim)
+ if(isliving(victim))
+ var/mob/living/living_victim = victim
+ if(!living_victim.mind?.has_antag_datum(/datum/antagonist/fugitive))
return
caught = TRUE
. = ..()
@@ -169,11 +169,11 @@
radio.talk_into(src, "Fugitive has triggered this trap in the [get_area_name(src)]!", RADIO_CHANNEL_COMMON)
/obj/item/bountytrap/attack_self(mob/living/user)
- var/turf/T = get_turf(src)
- if(!user || !user.transferItemToLoc(src, T))//visibly unequips
+ var/turf/target_turf = get_turf(src)
+ if(!user || !user.transferItemToLoc(src, target_turf))//visibly unequips
return
to_chat(user, span_notice("You set up [src]. Examine while close to disarm it."))
- stored_trap.forceMove(T)//moves trap to ground
+ stored_trap.forceMove(target_turf)//moves trap to ground
forceMove(stored_trap)//moves item into trap
/obj/item/bountytrap/Destroy()
@@ -189,9 +189,9 @@
desc = "A trap that will set you ablaze. You'd better avoid it."
icon_state = "trap-fire"
-/obj/structure/trap/fire/trap_effect(mob/living/L)
- to_chat(L, span_danger("Spontaneous combustion!"))
- L.Paralyze(20)
+/obj/structure/trap/fire/trap_effect(mob/living/victim)
+ to_chat(victim, span_danger("Spontaneous combustion!"))
+ victim.Paralyze(2 SECONDS)
new /obj/effect/hotspot(get_turf(src))
/obj/structure/trap/chill
@@ -199,11 +199,11 @@
desc = "A trap that will chill you to the bone. You'd better avoid it."
icon_state = "trap-frost"
-/obj/structure/trap/chill/trap_effect(mob/living/L)
- to_chat(L, span_danger("You're frozen solid!"))
- L.Paralyze(20)
- L.adjust_bodytemperature(-300)
- L.apply_status_effect(/datum/status_effect/freon)
+/obj/structure/trap/chill/trap_effect(mob/living/victim)
+ to_chat(victim, span_bolddanger("You're frozen solid!"))
+ victim.Paralyze(2 SECONDS)
+ victim.adjust_bodytemperature(-300)
+ victim.apply_status_effect(/datum/status_effect/freon)
/obj/structure/trap/damage
@@ -212,12 +212,12 @@
icon_state = "trap-earth"
-/obj/structure/trap/damage/trap_effect(mob/living/L)
- to_chat(L, span_danger("The ground quakes beneath your feet!"))
- L.Paralyze(100)
- L.adjustBruteLoss(35)
+/obj/structure/trap/damage/trap_effect(mob/living/victim)
+ to_chat(victim, span_bolddanger("The ground quakes beneath your feet!"))
+ victim.Paralyze(10 SECONDS)
+ victim.adjustBruteLoss(35)
var/obj/structure/flora/rock/style_random/giant_rock = new(get_turf(src))
- QDEL_IN(giant_rock, 200)
+ QDEL_IN(giant_rock, 20 SECONDS)
/obj/structure/trap/ward
@@ -225,7 +225,7 @@
desc = "A divine barrier, It looks like you could destroy it with enough effort, or wait for it to dissipate..."
icon_state = "ward"
density = TRUE
- time_between_triggers = 1200 //Exists for 2 minutes
+ time_between_triggers = 2 MINUTES
/obj/structure/trap/ward/Initialize(mapload)
. = ..()
@@ -236,10 +236,10 @@
desc = "A trap that rings with unholy energy. You think you hear... chittering?"
icon_state = "trap-cult"
-/obj/structure/trap/cult/trap_effect(mob/living/L)
- to_chat(L, span_danger("With a crack, the hostile constructs come out of hiding, stunning you!"))
- L.electrocute_act(10, src, flags = SHOCK_NOGLOVES) // electrocute act does a message.
- L.Paralyze(20)
- new /mob/living/simple_animal/hostile/construct/proteon/hostile(loc)
- new /mob/living/simple_animal/hostile/construct/proteon/hostile(loc)
- QDEL_IN(src, 30)
+/obj/structure/trap/cult/trap_effect(mob/living/victim)
+ to_chat(victim, span_bolddanger("With a crack, the hostile constructs come out of hiding, stunning you!"))
+ victim.electrocute_act(10, src, flags = SHOCK_NOGLOVES) // electrocute act does a message.
+ victim.Paralyze(2 SECONDS)
+ new /mob/living/basic/construct/proteon/hostile(loc)
+ new /mob/living/basic/construct/proteon/hostile(loc)
+ QDEL_IN(src, 3 SECONDS)
diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm
index 71e97bdf32c95..67d3b7a2b18e0 100644
--- a/code/game/objects/structures/window.dm
+++ b/code/game/objects/structures/window.dm
@@ -79,6 +79,9 @@
/obj/structure/window/examine(mob/user)
. = ..()
+ if(flags_1 & NODECONSTRUCT_1)
+ return
+
switch(state)
if(WINDOW_SCREWED_TO_FRAME)
. += span_notice("The window is screwed to the frame.")
@@ -431,7 +434,7 @@
/obj/structure/window/get_dumping_location()
return null
-/obj/structure/window/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
+/obj/structure/window/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
if(!density)
return TRUE
if(fulltile || (dir == to_dir))
@@ -480,6 +483,9 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/window/unanchored/spawner, 0)
return FALSE
/obj/structure/window/reinforced/attackby_secondary(obj/item/tool, mob/user, params)
+ if(flags_1 & NODECONSTRUCT_1)
+ return ..()
+
switch(state)
if(RWINDOW_SECURE)
if(tool.tool_behaviour == TOOL_WELDER)
@@ -558,6 +564,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/window/unanchored/spawner, 0)
/obj/structure/window/reinforced/examine(mob/user)
. = ..()
+ if(flags_1 & NODECONSTRUCT_1)
+ return
switch(state)
if(RWINDOW_SECURE)
. += span_notice("It's been screwed in with one way screws, you'd need to heat them to have any chance of backing them out.")
diff --git a/code/game/turfs/closed/indestructible.dm b/code/game/turfs/closed/indestructible.dm
index a1320946a455d..e8b89c78ece64 100644
--- a/code/game/turfs/closed/indestructible.dm
+++ b/code/game/turfs/closed/indestructible.dm
@@ -359,3 +359,13 @@ INITIALIZE_IMMEDIATE(/turf/closed/indestructible/splashscreen)
/turf/closed/indestructible/grille/Initialize(mapload)
. = ..()
underlays += mutable_appearance('icons/turf/floors.dmi', "plating")
+
+/turf/closed/indestructible/meat
+ name = "dense meat wall"
+ desc = "A huge chunk of dense, packed meat. Effectively impervious to conventional methods of destruction."
+ icon = 'icons/turf/walls/meat.dmi'
+ icon_state = "meatwall-0"
+ base_icon_state = "meatwall"
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_WALLS
+ canSmoothWith = SMOOTH_GROUP_WALLS
diff --git a/code/game/turfs/closed/minerals.dm b/code/game/turfs/closed/minerals.dm
index 7b4defa96e773..73510d3baf308 100644
--- a/code/game/turfs/closed/minerals.dm
+++ b/code/game/turfs/closed/minerals.dm
@@ -98,7 +98,7 @@
if (!isturf(T))
return
- if(TIMER_COOLDOWN_CHECK(src, REF(user))) //prevents mining turfs in progress
+ if(TIMER_COOLDOWN_RUNNING(src, REF(user))) //prevents mining turfs in progress
return
TIMER_COOLDOWN_START(src, REF(user), tool_mine_speed)
@@ -119,7 +119,7 @@
var/turf/user_turf = user.loc
if (!isturf(user_turf))
return
- if(TIMER_COOLDOWN_CHECK(src, REF(user))) //prevents mining turfs in progress
+ if(TIMER_COOLDOWN_RUNNING(src, REF(user))) //prevents mining turfs in progress
return
var/mining_speed = mining_arms ? tool_mine_speed : hand_mine_speed
TIMER_COOLDOWN_START(src, REF(user), mining_speed)
diff --git a/code/game/turfs/closed/walls.dm b/code/game/turfs/closed/walls.dm
index e47784b98aa2e..d4ff7b38ef4ba 100644
--- a/code/game/turfs/closed/walls.dm
+++ b/code/game/turfs/closed/walls.dm
@@ -34,48 +34,63 @@
var/list/dent_decals
-/turf/closed/wall/MouseDrop_T(mob/living/carbon/carbon_mob, mob/user)
+/turf/closed/wall/MouseDrop_T(atom/dropping, mob/user, params)
..()
- if(carbon_mob != user)
+ if(dropping != user)
return
- if(carbon_mob.is_leaning == TRUE)
+ if(!iscarbon(dropping) && !iscyborg(dropping))
return
- if(carbon_mob.pulledby)
+ var/mob/living/leaner = dropping
+ if(leaner.incapacitated(IGNORE_RESTRAINTS) || leaner.stat != CONSCIOUS || HAS_TRAIT(leaner, TRAIT_NO_TRANSFORM))
return
- if(!carbon_mob.density)
+ if(!leaner.density || leaner.pulledby || leaner.buckled || !(leaner.mobility_flags & MOBILITY_STAND))
return
- var/turf/checked_turf = get_step(carbon_mob, REVERSE_DIR(carbon_mob.dir))
- if(checked_turf == src)
- carbon_mob.start_leaning(src)
-
-/mob/living/carbon/proc/start_leaning(obj/wall)
+ if(HAS_TRAIT_FROM(leaner, TRAIT_UNDENSE, LEANING_TRAIT))
+ return
+ var/turf/checked_turf = get_step(leaner, REVERSE_DIR(leaner.dir))
+ if(checked_turf != src)
+ return
+ leaner.start_leaning(src)
+/mob/living/proc/start_leaning(turf/closed/wall/wall)
+ var/new_y = base_pixel_y + pixel_y
+ var/new_x = base_pixel_x + pixel_x
switch(dir)
if(SOUTH)
- pixel_y += LEANING_OFFSET
+ new_y += LEANING_OFFSET
if(NORTH)
- pixel_y += -LEANING_OFFSET
+ new_y -= LEANING_OFFSET
if(WEST)
- pixel_x += LEANING_OFFSET
+ new_x += LEANING_OFFSET
if(EAST)
- pixel_x += -LEANING_OFFSET
-
- ADD_TRAIT(src, TRAIT_UNDENSE, LEANING_TRAIT)
- ADD_TRAIT(src, TRAIT_EXPANDED_FOV, LEANING_TRAIT)
- visible_message(span_notice("[src] leans against \the [wall]!"), \
- span_notice("You lean against \the [wall]!"))
- RegisterSignals(src, list(COMSIG_MOB_CLIENT_PRE_MOVE, COMSIG_HUMAN_DISARM_HIT, COMSIG_LIVING_GET_PULLED, COMSIG_MOVABLE_TELEPORTING, COMSIG_ATOM_DIR_CHANGE), PROC_REF(stop_leaning))
+ new_x -= LEANING_OFFSET
+
+ animate(src, 0.2 SECONDS, pixel_x = new_x, pixel_y = new_y)
+ add_traits(list(TRAIT_UNDENSE, TRAIT_EXPANDED_FOV), LEANING_TRAIT)
+ visible_message(
+ span_notice("[src] leans against [wall]."),
+ span_notice("You lean against [wall]."),
+ )
+ RegisterSignals(src, list(
+ COMSIG_MOB_CLIENT_PRE_MOVE,
+ COMSIG_HUMAN_DISARM_HIT,
+ COMSIG_LIVING_GET_PULLED,
+ COMSIG_MOVABLE_TELEPORTING,
+ COMSIG_ATOM_DIR_CHANGE,
+ ), PROC_REF(stop_leaning))
update_fov()
- is_leaning = TRUE
-/mob/living/carbon/proc/stop_leaning()
+/mob/living/proc/stop_leaning()
SIGNAL_HANDLER
- UnregisterSignal(src, list(COMSIG_MOB_CLIENT_PRE_MOVE, COMSIG_HUMAN_DISARM_HIT, COMSIG_LIVING_GET_PULLED, COMSIG_MOVABLE_TELEPORTING, COMSIG_ATOM_DIR_CHANGE))
- is_leaning = FALSE
- pixel_y = base_pixel_y + body_position_pixel_x_offset
- pixel_x = base_pixel_y + body_position_pixel_y_offset
- REMOVE_TRAIT(src, TRAIT_UNDENSE, LEANING_TRAIT)
- REMOVE_TRAIT(src, TRAIT_EXPANDED_FOV, LEANING_TRAIT)
+ UnregisterSignal(src, list(
+ COMSIG_MOB_CLIENT_PRE_MOVE,
+ COMSIG_HUMAN_DISARM_HIT,
+ COMSIG_LIVING_GET_PULLED,
+ COMSIG_MOVABLE_TELEPORTING,
+ COMSIG_ATOM_DIR_CHANGE,
+ ))
+ animate(src, 0.2 SECONDS, pixel_x = base_pixel_x, pixel_y = base_pixel_y)
+ remove_traits(list(TRAIT_UNDENSE, TRAIT_EXPANDED_FOV), LEANING_TRAIT)
update_fov()
/turf/closed/wall/Initialize(mapload)
diff --git a/code/game/turfs/open/_open.dm b/code/game/turfs/open/_open.dm
index 7831fa1863c5b..f9013363ff1be 100644
--- a/code/game/turfs/open/_open.dm
+++ b/code/game/turfs/open/_open.dm
@@ -52,22 +52,22 @@
. += mutable_appearance(damaged_dmi, pick(broken_states()))
//direction is direction of travel of A
-/turf/open/zPassIn(atom/movable/A, direction, turf/source)
- if(direction == DOWN)
- for(var/obj/O in contents)
- if(O.obj_flags & BLOCK_Z_IN_DOWN)
- return FALSE
- return TRUE
- return FALSE
+/turf/open/zPassIn(direction)
+ if(direction != DOWN)
+ return FALSE
+ for(var/obj/on_us in contents)
+ if(on_us.obj_flags & BLOCK_Z_IN_DOWN)
+ return FALSE
+ return TRUE
-//direction is direction of travel of A
-/turf/open/zPassOut(atom/movable/A, direction, turf/destination, allow_anchored_movement)
- if(direction == UP)
- for(var/obj/O in contents)
- if(O.obj_flags & BLOCK_Z_OUT_UP)
- return FALSE
- return TRUE
- return FALSE
+//direction is direction of travel of an atom
+/turf/open/zPassOut(direction)
+ if(direction != UP)
+ return FALSE
+ for(var/obj/on_us in contents)
+ if(on_us.obj_flags & BLOCK_Z_OUT_UP)
+ return FALSE
+ return TRUE
//direction is direction of travel of air
/turf/open/zAirIn(direction, turf/source)
@@ -124,9 +124,6 @@
/turf/open/indestructible/light
icon_state = "light_on-1"
-/turf/open/indestructible/plating
- icon_state = "plating"
-
/turf/open/indestructible/permalube
icon_state = "darkfull"
@@ -223,6 +220,29 @@
init_air = FALSE
baseturfs = /turf/open/indestructible/airblock
+/turf/open/indestructible/meat
+ icon_state = "meat"
+ footstep = FOOTSTEP_MEAT
+ barefootstep = FOOTSTEP_MEAT
+ clawfootstep = FOOTSTEP_MEAT
+ heavyfootstep = FOOTSTEP_MEAT
+ initial_gas_mix = OPENTURF_DEFAULT_ATMOS
+ baseturfs = /turf/open/indestructible/meat
+
+/turf/open/indestructible/meat/airless
+ initial_gas_mix = AIRLESS_ATMOS
+
+/turf/open/indestructible/plating
+ name = "plating"
+ icon_state = "plating"
+ desc = "The attachment points are all bent to uselessness, looks nigh-impervious to damage."
+ overfloor_placed = FALSE
+ underfloor_accessibility = UNDERFLOOR_INTERACTABLE
+ footstep = FOOTSTEP_PLATING
+
+/turf/open/indestructible/plating/airless
+ initial_gas_mix = AIRLESS_ATMOS
+
/turf/open/Initalize_Atmos(time)
excited = FALSE
update_visuals()
@@ -329,8 +349,8 @@
slipper.AddComponent(/datum/component/force_move, target, FALSE)//spinning would be bad for ice, fucks up the next dir
return TRUE
-/turf/open/proc/MakeSlippery(wet_setting = TURF_WET_WATER, min_wet_time = 0, wet_time_to_add = 0, max_wet_time = MAXIMUM_WET_TIME, permanent)
- AddComponent(/datum/component/wet_floor, wet_setting, min_wet_time, wet_time_to_add, max_wet_time, permanent)
+/turf/open/proc/MakeSlippery(wet_setting = TURF_WET_WATER, min_wet_time = 0, wet_time_to_add = 0, max_wet_time = MAXIMUM_WET_TIME, permanent = FALSE, should_display_overlay = TRUE)
+ AddComponent(/datum/component/wet_floor, wet_setting, min_wet_time, wet_time_to_add, max_wet_time, permanent, should_display_overlay)
/turf/open/proc/MakeDry(wet_setting = TURF_WET_WATER, immediate = FALSE, amount = INFINITY)
SEND_SIGNAL(src, COMSIG_TURF_MAKE_DRY, wet_setting, immediate, amount)
@@ -391,3 +411,31 @@
if(istype(get_step(src, direction), /turf/open/floor))
return TRUE
return FALSE
+
+/// Very similar to build_with_rods, this exists to allow consistent behavior between different types in terms of how
+/// Building floors works
+/turf/open/proc/build_with_transport_tiles(obj/item/stack/thermoplastic/used_tiles, user)
+ var/obj/structure/transport/linear/platform = locate(/obj/structure/transport/linear, src)
+ if(!platform)
+ balloon_alert(user, "no tram base!")
+ return
+ if(!used_tiles.use(1))
+ balloon_alert(user, "no tile!")
+ return
+
+ playsound(src, 'sound/weapons/genhit.ogg', 50, TRUE)
+ new used_tiles.tile_type(src)
+
+/// Very similar to build_with_rods, this exists to allow building transport/tram girders on openspace
+/turf/open/proc/build_with_titanium(obj/item/stack/sheet/mineral/titanium/used_stack, user)
+ var/obj/structure/transport/linear/platform = locate(/obj/structure/transport/linear, src)
+ if(!platform)
+ to_chat(user, span_warning("There is no transport frame to attach the anchor!"))
+ return
+ if(!used_stack.use(2))
+ balloon_alert(user, "not enough titanium!")
+ return
+
+ playsound(src, 'sound/weapons/genhit.ogg', 50, TRUE)
+ new /obj/structure/girder/tram(src)
+
diff --git a/code/game/turfs/open/chasm.dm b/code/game/turfs/open/chasm.dm
index 3af4d12b176a3..f4094c35b367b 100644
--- a/code/game/turfs/open/chasm.dm
+++ b/code/game/turfs/open/chasm.dm
@@ -76,6 +76,9 @@
/turf/open/chasm/proc/apply_components(mapload)
AddComponent(/datum/component/chasm, GET_TURF_BELOW(src), mapload)
+/turf/open/chasm/can_cross_safely(atom/movable/crossing)
+ return HAS_TRAIT(src, TRAIT_CHASM_STOPPED) || HAS_TRAIT(crossing, TRAIT_MOVE_FLYING)
+
// Chasms for Lavaland, with planetary atmos and lava glow
/turf/open/chasm/lavaland
initial_gas_mix = LAVALAND_DEFAULT_ATMOS
diff --git a/code/game/turfs/open/cliff.dm b/code/game/turfs/open/cliff.dm
index aaae468d769c7..507f3eb7a7a75 100644
--- a/code/game/turfs/open/cliff.dm
+++ b/code/game/turfs/open/cliff.dm
@@ -35,6 +35,10 @@
SET_PLANE(underlay, underlay_plane || plane, src)
underlays += underlay
+/turf/open/cliff/Destroy(force)
+ UnregisterSignal(src, COMSIG_TURF_MOVABLE_THROW_LANDED)
+ return ..()
+
/turf/open/cliff/CanPass(atom/movable/mover, border_dir)
..()
diff --git a/code/game/turfs/open/floor/fancy_floor.dm b/code/game/turfs/open/floor/fancy_floor.dm
index 79ed8f766ac88..32cd036fb5edc 100644
--- a/code/game/turfs/open/floor/fancy_floor.dm
+++ b/code/game/turfs/open/floor/fancy_floor.dm
@@ -74,6 +74,9 @@
/turf/open/floor/wood/airless
initial_gas_mix = AIRLESS_ATMOS
+/turf/open/floor/wood/lavaland
+ initial_gas_mix = LAVALAND_DEFAULT_ATMOS
+
/turf/open/floor/wood/tile
icon_state = "wood_tile"
floor_tile = /obj/item/stack/tile/wood/tile
@@ -275,6 +278,9 @@
smoothing_flags = NONE
floor_tile = /obj/item/stack/tile/carpet/symbol
+/turf/open/floor/carpet/lone/lavaland
+ initial_gas_mix = LAVALAND_DEFAULT_ATMOS
+
/turf/open/floor/carpet/lone/star
icon_state = "carpetstar"
floor_tile = /obj/item/stack/tile/carpet/star
diff --git a/code/game/turfs/open/floor/glass.dm b/code/game/turfs/open/floor/glass.dm
index af2a0bec5bffb..c28ec9e1d4e89 100644
--- a/code/game/turfs/open/floor/glass.dm
+++ b/code/game/turfs/open/floor/glass.dm
@@ -40,6 +40,7 @@
/turf/open/floor/glass/Destroy()
. = ..()
QDEL_LIST(glow_stuff)
+ UnregisterSignal(SSdcs, COMSIG_STARLIGHT_COLOR_CHANGED)
/// If this turf is at the bottom of the local rendering stack
/// Then we're gonna make it emissive block so the space below glows
@@ -51,7 +52,15 @@
return
glow_stuff = partially_block_emissives(src, alpha_to_leave)
- set_light(2, 0.75, starlight_color || GLOB.starlight_color)
+ if(!starlight_color)
+ RegisterSignal(SSdcs, COMSIG_STARLIGHT_COLOR_CHANGED, PROC_REF(starlight_changed))
+ else
+ UnregisterSignal(SSdcs, COMSIG_STARLIGHT_COLOR_CHANGED)
+ set_light(2, 1, starlight_color || GLOB.starlight_color, l_height = LIGHTING_HEIGHT_SPACE)
+
+/turf/open/floor/glass/proc/starlight_changed(datum/source, old_star, new_star)
+ if(light_color == old_star)
+ set_light(l_color = new_star)
/turf/open/floor/glass/make_plating()
return
diff --git a/code/game/turfs/open/floor/misc_floor.dm b/code/game/turfs/open/floor/misc_floor.dm
index 4fe0cb044ad59..f8464a8ce4ef6 100644
--- a/code/game/turfs/open/floor/misc_floor.dm
+++ b/code/game/turfs/open/floor/misc_floor.dm
@@ -157,7 +157,7 @@
/turf/open/floor/noslip/tram/Initialize(mapload)
. = ..()
- var/current_holiday_color = request_holiday_colors(src, PATTERN_VERTICAL_STRIPE)
+ var/current_holiday_color = request_station_colors(src, PATTERN_VERTICAL_STRIPE) || request_holiday_colors(src, PATTERN_VERTICAL_STRIPE)
if(current_holiday_color)
color = current_holiday_color
else
diff --git a/code/game/turfs/open/floor/plating.dm b/code/game/turfs/open/floor/plating.dm
index eee4797887bca..a38338010d410 100644
--- a/code/game/turfs/open/floor/plating.dm
+++ b/code/game/turfs/open/floor/plating.dm
@@ -177,7 +177,7 @@
ScrapeAway(flags = CHANGETURF_INHERIT_AIR)
return TRUE
-/turf/open/floor/plating/foam/tool_act(mob/living/user, obj/item/I, tool_type)
+/turf/open/floor/plating/foam/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
return
//reinforced plating deconstruction states
diff --git a/code/game/turfs/open/ice.dm b/code/game/turfs/open/ice.dm
index 96bf1baac0929..3f951684e86d3 100644
--- a/code/game/turfs/open/ice.dm
+++ b/code/game/turfs/open/ice.dm
@@ -17,7 +17,7 @@
/turf/open/misc/ice/Initialize(mapload)
. = ..()
- MakeSlippery(TURF_WET_PERMAFROST, INFINITY, 0, INFINITY, TRUE)
+ MakeSlippery(TURF_WET_PERMAFROST, INFINITY, 0, INFINITY, TRUE, FALSE)
/turf/open/misc/ice/break_tile()
return
diff --git a/code/game/turfs/open/lava.dm b/code/game/turfs/open/lava.dm
index 623da55f10409..b070da35d3c53 100644
--- a/code/game/turfs/open/lava.dm
+++ b/code/game/turfs/open/lava.dm
@@ -315,6 +315,9 @@
burn_living.adjust_fire_stacks(lava_firestacks * seconds_per_tick)
burn_living.ignite_mob()
+/turf/open/lava/can_cross_safely(atom/movable/crossing)
+ return HAS_TRAIT(src, TRAIT_LAVA_STOPPED) || HAS_TRAIT(crossing, immunity_trait ) || HAS_TRAIT(crossing, TRAIT_MOVE_FLYING)
+
/turf/open/lava/smooth
name = "lava"
baseturfs = /turf/open/lava/smooth
@@ -365,37 +368,40 @@
/turf/open/lava/plasma/do_burn(atom/movable/burn_target, seconds_per_tick = 1)
. = TRUE
- if(isobj(burn_target))
- return FALSE // Does nothing against objects. Old code.
+ if(!isliving(burn_target))
+ return FALSE
var/mob/living/burn_living = burn_target
- burn_living.adjustFireLoss(2)
- if(QDELETED(burn_living))
- return
- burn_living.adjust_fire_stacks(20) //dipping into a stream of plasma would probably make you more flammable than usual
- burn_living.adjust_bodytemperature(-rand(50,65)) //its cold, man
- if(!ishuman(burn_living) || SPT_PROB(65, seconds_per_tick))
+ var/need_mob_update
+ // This is from plasma, so it should obey plasma biotype requirements
+ need_mob_update += burn_living.adjustToxLoss(15, updating_health = FALSE, required_biotype = MOB_ORGANIC)
+ need_mob_update += burn_living.adjustFireLoss(25, updating_health = FALSE)
+ if(need_mob_update)
+ burn_living.updatehealth()
+
+ if(QDELETED(burn_living) \
+ || !ishuman(burn_living) \
+ || HAS_TRAIT(burn_living, TRAIT_NODISMEMBER) \
+ || HAS_TRAIT(burn_living, TRAIT_NO_PLASMA_TRANSFORM) \
+ || SPT_PROB(65, seconds_per_tick) \
+ )
return
+
var/mob/living/carbon/human/burn_human = burn_living
- var/datum/species/burn_species = burn_human.dna.species
- if(istype(burn_species, /datum/species/plasmaman) || istype(burn_species, /datum/species/android)) //ignore plasmamen/robotic species
- return
- var/list/plasma_parts = list()//a list of the organic parts to be turned into plasma limbs
- var/list/robo_parts = list()//keep a reference of robotic parts so we know if we can turn them into a plasmaman
+ var/list/immune_parts = list() // Parts we can't transform because they're not organic or can't be dismembered
+ var/list/transform_parts = list() // Parts we want to transform
+
for(var/obj/item/bodypart/burn_limb as anything in burn_human.bodyparts)
- if(IS_ORGANIC_LIMB(burn_limb) && burn_limb.limb_id != SPECIES_PLASMAMAN) //getting every organic, non-plasmaman limb (augments/androids are immune to this)
- plasma_parts += burn_limb
- if(IS_ROBOTIC_LIMB(burn_limb))
- robo_parts += burn_limb
+ if(!IS_ORGANIC_LIMB(burn_limb) || !burn_limb.can_dismember())
+ immune_parts += burn_limb
+ continue
+ if(burn_limb.limb_id == SPECIES_PLASMAMAN)
+ continue
+ transform_parts += burn_limb
- var/need_mob_update
- need_mob_update += burn_human.adjustToxLoss(15, updating_health = FALSE, required_biotype = MOB_ORGANIC) // This is from plasma, so it should obey plasma biotype requirements
- need_mob_update += burn_human.adjustFireLoss(25, updating_health = FALSE)
- if(need_mob_update)
- burn_human.updatehealth()
- if(plasma_parts.len)
- var/obj/item/bodypart/burn_limb = pick(plasma_parts) //using the above-mentioned list to get a choice of limbs
+ if(length(transform_parts))
+ var/obj/item/bodypart/burn_limb = pick_n_take(transform_parts)
burn_human.emote("scream")
var/obj/item/bodypart/plasmalimb
switch(burn_limb.body_zone) //get plasmaman limb to swap in
@@ -411,16 +417,20 @@
plasmalimb = new /obj/item/bodypart/chest/plasmaman
if(BODY_ZONE_HEAD)
plasmalimb = new /obj/item/bodypart/head/plasmaman
+
burn_human.del_and_replace_bodypart(plasmalimb)
burn_human.update_body_parts()
burn_human.emote("scream")
burn_human.visible_message(span_warning("[burn_human]'s [burn_limb.plaintext_zone] melts down to the bone!"), \
- span_userdanger("You scream out in pain as your [burn_limb.plaintext_zone] melts down to the bone, leaving an eerie plasma-like glow where flesh used to be!"))
- if(!plasma_parts.len && !robo_parts.len) //a person with no potential organic limbs left AND no robotic limbs, time to turn them into a plasmaman
- burn_human.ignite_mob()
- burn_human.set_species(/datum/species/plasmaman)
- burn_human.visible_message(span_warning("[burn_human] bursts into a brilliant purple flame as [burn_human.p_their()] entire body is that of a skeleton!"), \
- span_userdanger("Your senses numb as all of your remaining flesh is turned into a purple slurry, sloshing off your body and leaving only your bones to show in a vibrant purple!"))
+ span_userdanger("You scream out in pain as your [burn_limb.plaintext_zone] melts down to the bone, held together only by strands of purple fungus!"))
+
+ // If all of your limbs are plasma then congrats: you are plasma man
+ if(length(immune_parts) || length(transform_parts))
+ return
+ burn_human.ignite_mob()
+ burn_human.set_species(/datum/species/plasmaman)
+ burn_human.visible_message(span_warning("[burn_human] bursts into flame as the last of [burn_human.p_their()] body is coated in fungus!"), \
+ span_userdanger("Your senses numb as what remains of your flesh sloughs off, revealing the plasma-encrusted bone beneath!"))
//mafia specific tame happy plasma (normal atmos, no slowdown)
/turf/open/lava/plasma/mafia
diff --git a/code/game/turfs/open/openspace.dm b/code/game/turfs/open/openspace.dm
index 77a2259763ac5..bd74acab4408f 100644
--- a/code/game/turfs/open/openspace.dm
+++ b/code/game/turfs/open/openspace.dm
@@ -80,7 +80,7 @@
/turf/open/openspace/zAirOut()
return TRUE
-/turf/open/openspace/zPassIn(atom/movable/A, direction, turf/source)
+/turf/open/openspace/zPassIn(direction)
if(direction == DOWN)
for(var/obj/contained_object in contents)
if(contained_object.obj_flags & BLOCK_Z_IN_DOWN)
@@ -93,9 +93,7 @@
return TRUE
return FALSE
-/turf/open/openspace/zPassOut(atom/movable/A, direction, turf/destination, allow_anchored_movement)
- if(A.anchored && !allow_anchored_movement)
- return FALSE
+/turf/open/openspace/zPassOut(direction)
if(direction == DOWN)
for(var/obj/contained_object in contents)
if(contained_object.obj_flags & BLOCK_Z_OUT_DOWN)
@@ -122,6 +120,10 @@
build_with_rods(C, user)
else if(istype(C, /obj/item/stack/tile/iron))
build_with_floor_tiles(C, user)
+ else if(istype(C, /obj/item/stack/thermoplastic))
+ build_with_transport_tiles(C, user)
+ else if(istype(C, /obj/item/stack/sheet/mineral/titanium))
+ build_with_titanium(C, user)
/turf/open/openspace/build_with_floor_tiles(obj/item/stack/tile/iron/used_tiles)
if(!CanCoverUp())
@@ -150,8 +152,9 @@
/turf/open/openspace/rust_heretic_act()
return FALSE
-/turf/open/openspace/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
- if(caller && !caller.can_z_move(DOWN, src, null , ZMOVE_FALL_FLAGS)) //If we can't fall here (flying/lattice), it's fine to path through
+/turf/open/openspace/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
+ var/atom/movable/our_movable = pass_info.caller_ref.resolve()
+ if(our_movable && !our_movable.can_z_move(DOWN, src, null, ZMOVE_FALL_FLAGS)) //If we can't fall here (flying/lattice), it's fine to path through
return TRUE
return FALSE
@@ -163,6 +166,9 @@
PlaceOnTop(/turf/open/floor/plating, flags = flags)
PlaceOnTop(new_floor_path, flags = flags)
+/turf/open/openspace/can_cross_safely(atom/movable/crossing)
+ return HAS_TRAIT(crossing, TRAIT_MOVE_FLYING)
+
/turf/open/openspace/icemoon
name = "ice chasm"
baseturfs = /turf/open/openspace/icemoon
diff --git a/code/game/turfs/open/planet.dm b/code/game/turfs/open/planet.dm
index 65c76cef957a7..db391025e84b1 100644
--- a/code/game/turfs/open/planet.dm
+++ b/code/game/turfs/open/planet.dm
@@ -80,6 +80,9 @@
/turf/open/misc/grass/burnt_states()
return list("jungle_damaged")
+/turf/open/misc/grass/jungle/lavaland
+ initial_gas_mix = LAVALAND_DEFAULT_ATMOS
+
/turf/closed/mineral/random/jungle
baseturfs = /turf/open/misc/dirt/dark/jungle
diff --git a/code/game/turfs/open/space/space.dm b/code/game/turfs/open/space/space.dm
index fc20d9a11b15f..049309c262bdb 100644
--- a/code/game/turfs/open/space/space.dm
+++ b/code/game/turfs/open/space/space.dm
@@ -1,5 +1,46 @@
-///The color of light space emits
-GLOBAL_VAR_INIT(starlight_color, COLOR_STARLIGHT)
+///The base color of light space emits
+GLOBAL_VAR_INIT(base_starlight_color, default_starlight_color())
+///The color of light space is currently emitting
+GLOBAL_VAR_INIT(starlight_color, default_starlight_color())
+/proc/default_starlight_color()
+ var/turf/open/space/read_from = /turf/open/space
+ return initial(read_from.light_color)
+
+///The range of the light space is displaying
+GLOBAL_VAR_INIT(starlight_range, default_starlight_range())
+/proc/default_starlight_range()
+ var/turf/open/space/read_from = /turf/open/space
+ return initial(read_from.light_range)
+
+///The power of the light space is throwin out
+GLOBAL_VAR_INIT(starlight_power, default_starlight_power())
+/proc/default_starlight_power()
+ var/turf/open/space/read_from = /turf/open/space
+ return initial(read_from.light_power)
+
+/proc/set_base_starlight(star_color = null, range = null, power = null)
+ GLOB.base_starlight_color = star_color
+ set_starlight(star_color, range, power)
+
+/proc/set_starlight(star_color = null, range = null, power = null)
+ if(isnull(star_color))
+ star_color = GLOB.starlight_color
+ var/old_star_color = GLOB.starlight_color
+ GLOB.starlight_color = star_color
+ // set light color on all lit turfs
+ for(var/turf/open/space/spess as anything in GLOB.starlight)
+ spess.set_light(l_range = range, l_power = power, l_color = star_color)
+
+ if(star_color == old_star_color)
+ return
+
+ // Update the base overlays
+ for(var/obj/light as anything in GLOB.starlight_objects)
+ light.color = star_color
+ // Send some signals that'll update everything that uses the color
+ SEND_GLOBAL_SIGNAL(COMSIG_STARLIGHT_COLOR_CHANGED, old_star_color, star_color)
+
+GLOBAL_LIST_EMPTY(starlight)
/turf/open/space
icon = 'icons/turf/space.dmi'
@@ -23,7 +64,11 @@ GLOBAL_VAR_INIT(starlight_color, COLOR_STARLIGHT)
run_later = TRUE
plane = PLANE_SPACE
layer = SPACE_LAYER
- light_power = 0.75
+ light_power = 1
+ light_range = 2
+ light_color = COLOR_STARLIGHT
+ light_height = LIGHTING_HEIGHT_SPACE
+ light_on = FALSE
space_lit = TRUE
bullet_bounce_sound = null
vis_flags = VIS_INHERIT_ID //when this be added to vis_contents of something it be associated with something on clicking, important for visualisation of turf in openspace and interraction with openspace that show you turf.
@@ -35,6 +80,10 @@ GLOBAL_VAR_INIT(starlight_color, COLOR_STARLIGHT)
//This is used to optimize the map loader
return
+/turf/open/space/Destroy()
+ GLOB.starlight -= src
+ return ..()
+
//ATTACK GHOST IGNORING PARENT RETURN VALUE
/turf/open/space/attack_ghost(mob/dead/observer/user)
if(destination_z)
@@ -60,20 +109,22 @@ GLOBAL_VAR_INIT(starlight_color, COLOR_STARLIGHT)
/// Updates starlight. Called when we're unsure of a turf's starlight state
/// Returns TRUE if we succeed, FALSE otherwise
/turf/open/space/proc/update_starlight()
- for(var/t in RANGE_TURFS(1,src)) //RANGE_TURFS is in code\__HELPERS\game.dm
+ for(var/t in RANGE_TURFS(1, src)) //RANGE_TURFS is in code\__HELPERS\game.dm
// I've got a lot of cordons near spaceturfs, be good kids
if(isspaceturf(t) || istype(t, /turf/cordon))
//let's NOT update this that much pls
continue
enable_starlight()
return TRUE
- set_light(0)
+ GLOB.starlight -= src
+ set_light(l_on = FALSE)
return FALSE
/// Turns on the stars, if they aren't already
/turf/open/space/proc/enable_starlight()
- if(!light_range)
- set_light(2)
+ if(!light_on)
+ set_light(l_on = TRUE, l_range = GLOB.starlight_range, l_power = GLOB.starlight_power, l_color = GLOB.starlight_color)
+ GLOB.starlight += src
/turf/open/space/attack_paw(mob/user, list/modifiers)
return attack_hand(user, modifiers)
@@ -203,6 +254,9 @@ GLOBAL_VAR_INIT(starlight_color, COLOR_STARLIGHT)
destination_y = dest_y
destination_z = dest_z
+/turf/open/space/can_cross_safely(atom/movable/crossing)
+ return HAS_TRAIT(crossing, TRAIT_SPACEWALK)
+
/turf/open/space/openspace
icon = 'icons/turf/floors.dmi'
icon_state = MAP_SWITCH("pure_white", "invisible")
@@ -235,7 +289,7 @@ GLOBAL_VAR_INIT(starlight_color, COLOR_STARLIGHT)
/turf/open/space/openspace/zAirOut()
return TRUE
-/turf/open/space/openspace/zPassIn(atom/movable/A, direction, turf/source)
+/turf/open/space/openspace/zPassIn(direction)
if(direction == DOWN)
for(var/obj/contained_object in contents)
if(contained_object.obj_flags & BLOCK_Z_IN_DOWN)
@@ -248,9 +302,7 @@ GLOBAL_VAR_INIT(starlight_color, COLOR_STARLIGHT)
return TRUE
return FALSE
-/turf/open/space/openspace/zPassOut(atom/movable/A, direction, turf/destination, allow_anchored_movement)
- if(A.anchored && !allow_anchored_movement)
- return FALSE
+/turf/open/space/openspace/zPassOut(direction)
if(direction == DOWN)
for(var/obj/contained_object in contents)
if(contained_object.obj_flags & BLOCK_Z_OUT_DOWN)
@@ -267,9 +319,10 @@ GLOBAL_VAR_INIT(starlight_color, COLOR_STARLIGHT)
var/turf/below = GET_TURF_BELOW(src)
// Override = TRUE beacuse we could have our starlight updated many times without a failure, which'd trigger this
RegisterSignal(below, COMSIG_TURF_CHANGE, PROC_REF(on_below_change), override = TRUE)
- if(!isspaceturf(below))
+ if(!isspaceturf(below) || light_on)
return
- set_light(2)
+ set_light(l_on = TRUE, l_range = GLOB.starlight_range, l_power = GLOB.starlight_power, l_color = GLOB.starlight_color)
+ GLOB.starlight += src
/turf/open/space/openspace/update_starlight()
. = ..()
@@ -282,9 +335,11 @@ GLOBAL_VAR_INIT(starlight_color, COLOR_STARLIGHT)
/turf/open/space/openspace/proc/on_below_change(turf/source, path, list/new_baseturfs, flags, list/post_change_callbacks)
SIGNAL_HANDLER
if(isspaceturf(source) && !ispath(path, /turf/open/space))
- set_light(2)
+ GLOB.starlight += src
+ set_light(l_on = TRUE, l_range = GLOB.starlight_range, l_power = GLOB.starlight_power, l_color = GLOB.starlight_color)
else if(!isspaceturf(source) && ispath(path, /turf/open/space))
- set_light(0)
+ GLOB.starlight -= src
+ set_light(l_on = FALSE)
/turf/open/space/replace_floor(turf/open/new_floor_path, flags)
if (!initial(new_floor_path.overfloor_placed))
diff --git a/code/game/turfs/open/space/space_EXPENSIVE.dm b/code/game/turfs/open/space/space_EXPENSIVE.dm
index 44a15ac66d94a..bcb45e22dde04 100644
--- a/code/game/turfs/open/space/space_EXPENSIVE.dm
+++ b/code/game/turfs/open/space/space_EXPENSIVE.dm
@@ -19,8 +19,6 @@
stack_trace("Warning: [src]([type]) initialized multiple times!")
flags_1 |= INITIALIZED_1
- light_color = GLOB.starlight_color
-
// We make the assumption that the space plane will never be blacklisted, as an optimization
if(SSmapping.max_plane_offset)
plane = PLANE_SPACE - (PLANE_RANGE * SSmapping.z_level_to_plane_offset[z])
@@ -30,7 +28,7 @@
// Intentionally not add_overlay for performance reasons.
// add_overlay does a bunch of generic stuff, like creating a new list for overlays,
// queueing compile, cloning appearance, etc etc etc that is not necessary here.
- overlays += GLOB.fullbright_overlays[GET_TURF_PLANE_OFFSET(src) + 1]
+ overlays += GLOB.starlight_overlays[GET_TURF_PLANE_OFFSET(src) + 1]
if (!mapload)
if(requires_activation)
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 3d18dee305077..258460e3ad4e6 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -5,6 +5,7 @@ GLOBAL_LIST_EMPTY(station_turfs)
icon = 'icons/turf/floors.dmi'
vis_flags = VIS_INHERIT_ID // Important for interaction with and visualization of openspace.
luminosity = 1
+ light_height = LIGHTING_HEIGHT_FLOOR
///what /mob/oranges_ear instance is already assigned to us as there should only ever be one.
///used for guaranteeing there is only one oranges_ear per turf when assigned, speeds up view() iteration
@@ -156,8 +157,7 @@ GLOBAL_LIST_EMPTY(station_turfs)
var/area/our_area = loc
if(!our_area.area_has_base_lighting && space_lit) //Only provide your own lighting if the area doesn't for you
- var/mutable_appearance/overlay = GLOB.fullbright_overlays[GET_TURF_PLANE_OFFSET(src) + 1]
- add_overlay(overlay)
+ add_overlay(GLOB.starlight_overlays[GET_TURF_PLANE_OFFSET(src) + 1])
if(requires_activation)
CALCULATE_ADJACENT_TURFS(src, KILL_EXCITED)
@@ -296,20 +296,21 @@ GLOBAL_LIST_EMPTY(station_turfs)
return TRUE
return FALSE
-//zPassIn doesn't necessarily pass an atom!
-//direction is direction of travel of air
-/turf/proc/zPassIn(atom/movable/A, direction, turf/source)
+//The zpass procs exist to be overriden, not directly called
+//use can_z_pass for that
+///If we'd allow anything to travel into us
+/turf/proc/zPassIn(direction)
return FALSE
-//direction is direction of travel of air
-/turf/proc/zPassOut(atom/movable/A, direction, turf/destination, allow_anchored_movement)
+///If we'd allow anything to travel out of us
+/turf/proc/zPassOut(direction)
return FALSE
//direction is direction of travel of air
/turf/proc/zAirIn(direction, turf/source)
return FALSE
-//direction is direction of travel of air
+//direction is direction of travel
/turf/proc/zAirOut(direction, turf/source)
return FALSE
@@ -521,9 +522,9 @@ GLOBAL_LIST_EMPTY(station_turfs)
/turf/singularity_act()
if(underfloor_accessibility < UNDERFLOOR_INTERACTABLE)
- for(var/obj/O in contents) //this is for deleting things like wires contained in the turf
- if(HAS_TRAIT(O, TRAIT_T_RAY_VISIBLE))
- O.singularity_act()
+ for(var/obj/on_top in contents) //this is for deleting things like wires contained in the turf
+ if(HAS_TRAIT(on_top, TRAIT_T_RAY_VISIBLE))
+ on_top.singularity_act()
ScrapeAway(flags = CHANGETURF_INHERIT_AIR)
return(2)
@@ -722,19 +723,20 @@ GLOBAL_LIST_EMPTY(station_turfs)
*
* Arguments:
* * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach
- * * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf
+ * * access: A list that decides if we can gain access to doors that would otherwise block a turf
* * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space?
* * no_id: When true, doors with public access will count as impassible
*/
-/turf/proc/reachableAdjacentTurfs(atom/movable/caller, ID, simulated_only, no_id = FALSE)
+/turf/proc/reachableAdjacentTurfs(atom/movable/caller, list/access, simulated_only, no_id = FALSE)
var/static/space_type_cache = typecacheof(/turf/open/space)
. = list()
+ var/datum/can_pass_info/pass_info = new(caller, access, no_id)
for(var/iter_dir in GLOB.cardinals)
var/turf/turf_to_check = get_step(src,iter_dir)
if(!turf_to_check || (simulated_only && space_type_cache[turf_to_check.type]))
continue
- if(turf_to_check.density || LinkBlockedWithAccess(turf_to_check, caller, ID, no_id = no_id))
+ if(turf_to_check.density || LinkBlockedWithAccess(turf_to_check, pass_info))
continue
. += turf_to_check
@@ -752,3 +754,7 @@ GLOBAL_LIST_EMPTY(station_turfs)
explosive_resistance -= get_explosive_block()
inherent_explosive_resistance = explosion_block
explosive_resistance += get_explosive_block()
+
+/// Returns whether it is safe for an atom to move across this turf
+/turf/proc/can_cross_safely(atom/movable/crossing)
+ return TRUE
diff --git a/code/game/world.dm b/code/game/world.dm
index 600e1f6914bd6..26d9731369ca4 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -190,7 +190,7 @@ GLOBAL_VAR(restart_counter)
data["tick_usage"] = world.tick_usage
data["tick_lag"] = world.tick_lag
data["time"] = world.time
- data["timestamp"] = logger.unix_timestamp_string()
+ data["timestamp"] = rustg_unix_timestamp()
return data
/world/proc/SetupLogs()
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 600bc75a83d5f..e2ba145c640be 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -46,6 +46,7 @@ GLOBAL_PROTECT(admin_verbs_admin)
/datum/admins/proc/view_all_circuits,
/datum/verbs/menu/Admin/verb/playerpanel, /* It isn't /datum/admin but it fits no less */
/datum/admins/proc/change_shuttle_events, //allows us to change the shuttle events
+ /datum/admins/proc/reset_tram, //tram related admin actions
// Client procs
/client/proc/admin_call_shuttle, /*allows us to call the emergency shuttle*/
/client/proc/admin_cancel_shuttle, /*allows us to cancel the emergency shuttle, sending it back to centcom*/
@@ -264,7 +265,7 @@ GLOBAL_PROTECT(admin_verbs_poll)
add_verb(src, GLOB.admin_verbs_permissions)
if(rights & R_STEALTH)
add_verb(src, /client/proc/stealth)
- if(rights & R_ADMIN)
+ if(rights & R_POLL)
add_verb(src, GLOB.admin_verbs_poll)
if(rights & R_SOUND)
add_verb(src, GLOB.admin_verbs_sounds)
@@ -354,16 +355,16 @@ GLOBAL_PROTECT(admin_verbs_poll)
set name = "Invisimin"
set category = "Admin.Game"
set desc = "Toggles ghost-like invisibility (Don't abuse this)"
- if(holder && mob)
- if(initial(mob.invisibility) == INVISIBILITY_OBSERVER)
- to_chat(mob, span_boldannounce("Invisimin toggle failed. You are already an invisible mob like a ghost."), confidential = TRUE)
- return
- if(mob.invisibility == INVISIBILITY_OBSERVER)
- mob.invisibility = initial(mob.invisibility)
- to_chat(mob, span_boldannounce("Invisimin off. Invisibility reset."), confidential = TRUE)
- else
- mob.invisibility = INVISIBILITY_OBSERVER
- to_chat(mob, span_adminnotice("Invisimin on. You are now as invisible as a ghost."), confidential = TRUE)
+ if(isnull(holder) || isnull(mob))
+ return
+ if(mob.invisimin)
+ mob.invisimin = FALSE
+ mob.RemoveInvisibility(INVISIBILITY_SOURCE_INVISIMIN)
+ to_chat(mob, span_boldannounce("Invisimin off. Invisibility reset."), confidential = TRUE)
+ else
+ mob.invisimin = TRUE
+ mob.SetInvisibility(INVISIBILITY_OBSERVER, INVISIBILITY_SOURCE_INVISIMIN, INVISIBILITY_PRIORITY_ADMIN)
+ to_chat(mob, span_adminnotice("Invisimin on. You are now as invisible as a ghost."), confidential = TRUE)
/client/proc/check_antagonists()
set name = "Check Antagonists"
@@ -509,7 +510,7 @@ GLOBAL_PROTECT(admin_verbs_poll)
holder.fakekey = new_key
createStealthKey()
if(isobserver(mob))
- mob.invisibility = INVISIBILITY_MAXIMUM //JUST IN CASE
+ mob.SetInvisibility(INVISIBILITY_ABSTRACT, INVISIBILITY_SOURCE_STEALTHMODE, INVISIBILITY_PRIORITY_ADMIN)
mob.alpha = 0 //JUUUUST IN CASE
mob.name = " "
mob.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
@@ -523,7 +524,7 @@ GLOBAL_PROTECT(admin_verbs_poll)
/client/proc/disable_stealth_mode()
holder.fakekey = null
if(isobserver(mob))
- mob.invisibility = initial(mob.invisibility)
+ mob.RemoveInvisibility(INVISIBILITY_SOURCE_STEALTHMODE)
mob.alpha = initial(mob.alpha)
if(mob.mind)
if(mob.mind.ghostname)
diff --git a/code/modules/admin/smites/bad_luck.dm b/code/modules/admin/smites/bad_luck.dm
index c9f6a94b93a03..34592dd200d4b 100644
--- a/code/modules/admin/smites/bad_luck.dm
+++ b/code/modules/admin/smites/bad_luck.dm
@@ -6,21 +6,23 @@
var/silent
/// Is this permanent?
- var/permanent
+ var/incidents
/datum/smite/bad_luck/configure(client/user)
silent = tgui_alert(user, "Do you want to apply the omen with a player notification?", "Notify Player?", list("Notify", "Silent")) == "Silent"
- permanent = tgui_alert(user, "Would you like this to be permanent or removed automatically after the first accident?", "Permanent?", list("Permanent", "Temporary")) == "Permanent"
+ incidents = tgui_input_number(user, "For how many incidents will the omen last? 0 means permanent.", "Duration?", default = 0, round_value = 1)
+ if(incidents == 0)
+ incidents = INFINITY
/datum/smite/bad_luck/effect(client/user, mob/living/target)
. = ..()
//if permanent, replace any existing omen
- if(permanent)
+ if(incidents == INFINITY)
var/existing_component = target.GetComponent(/datum/component/omen)
qdel(existing_component)
- target.AddComponent(/datum/component/omen/smite, permanent = permanent)
+ target.AddComponent(/datum/component/omen/smite, incidents_left = incidents)
if(silent)
return
to_chat(target, span_warning("You get a bad feeling..."))
- if(permanent)
+ if(incidents == INFINITY)
to_chat(target, span_warning("A very bad feeling... As if malevolent forces are watching you..."))
diff --git a/code/modules/admin/verbs/adminevents.dm b/code/modules/admin/verbs/adminevents.dm
index da7102fa43fb5..2e3cae2ec4c10 100644
--- a/code/modules/admin/verbs/adminevents.dm
+++ b/code/modules/admin/verbs/adminevents.dm
@@ -224,7 +224,13 @@
SSshuttle.admin_emergency_no_recall = TRUE
SSshuttle.emergency.setTimer(0)
SSshuttle.emergency.mode = SHUTTLE_DISABLED
- priority_announce("Warning: Emergency Shuttle uplink failure, shuttle disabled until further notice.", "Emergency Shuttle Uplink Alert", 'sound/misc/announce_dig.ogg')
+ priority_announce(
+ text = "Emergency Shuttle uplink failure, shuttle disabled until further notice.",
+ title = "Uplink Failure",
+ sound = 'sound/misc/announce_dig.ogg',
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "grey",
+ )
/client/proc/admin_enable_shuttle()
set category = "Admin.Events"
@@ -250,7 +256,13 @@
if(SSshuttle.last_call_time < 10 SECONDS && SSshuttle.last_mode != SHUTTLE_IDLE)
SSshuttle.last_call_time = 10 SECONDS //Make sure no insta departures.
SSshuttle.emergency.setTimer(SSshuttle.last_call_time)
- priority_announce("Warning: Emergency Shuttle uplink reestablished, shuttle enabled.", "Emergency Shuttle Uplink Alert", 'sound/misc/announce_dig.ogg')
+ priority_announce(
+ text = "Emergency Shuttle uplink reestablished, shuttle enabled.",
+ title = "Uplink Restored",
+ sound = 'sound/misc/announce_dig.ogg',
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "green",
+ )
/client/proc/admin_hostile_environment()
set category = "Admin.Events"
diff --git a/code/modules/admin/verbs/commandreport.dm b/code/modules/admin/verbs/commandreport.dm
index badf3babb6698..f64dabd34d3ce 100644
--- a/code/modules/admin/verbs/commandreport.dm
+++ b/code/modules/admin/verbs/commandreport.dm
@@ -50,6 +50,10 @@
var/print_report = TRUE
/// The sound that's going to accompany our message.
var/played_sound = DEFAULT_ANNOUNCEMENT_SOUND
+ /// The colour of the announcement when sent
+ var/announcement_color = "default"
+ /// The subheader to include when sending the announcement. Keep blank to not include a subheader
+ var/subheader = ""
/// A static list of preset names that can be chosen.
var/list/preset_names = list(CENTCOM_PRESET, SYNDICATE_PRESET, WIZARD_PRESET, CUSTOM_PRESET)
@@ -79,6 +83,8 @@
data["announce_contents"] = announce_contents
data["print_report"] = print_report
data["played_sound"] = played_sound
+ data["announcement_color"] = announcement_color
+ data["subheader"] = subheader
return data
@@ -86,6 +92,7 @@
var/list/data = list()
data["command_name_presets"] = preset_names
data["announcer_sounds"] = list(DEFAULT_ANNOUNCEMENT_SOUND) + GLOB.announcer_keys
+ data["announcement_colors"] = ANNOUNCEMENT_COLORS
return data
@@ -108,6 +115,13 @@
announce_contents = !announce_contents
if("toggle_printing")
print_report = !print_report
+ if("update_announcement_color")
+ var/colors = ANNOUNCEMENT_COLORS
+ var/chosen_color = params["updated_announcement_color"]
+ if(chosen_color in colors)
+ announcement_color = chosen_color
+ if("set_subheader")
+ subheader = params["new_subheader"]
if("submit_report")
if(!command_name)
to_chat(ui_user, span_danger("You can't send a report with no command name."))
@@ -136,7 +150,13 @@
report_sound = SSstation.announcer.get_rand_report_sound()
if(announce_contents)
- priority_announce(command_report_content, null, report_sound, has_important_message = TRUE)
+ var/chosen_color = announcement_color
+ if(chosen_color == "default")
+ if(command_name == SYNDICATE_PRESET)
+ chosen_color = "red"
+ else if(command_name == WIZARD_PRESET)
+ chosen_color = "purple"
+ priority_announce(command_report_content, subheader == ""? null : subheader, report_sound, has_important_message = TRUE, color_override = chosen_color)
if(!announce_contents || print_report)
print_command_report(command_report_content, "[announce_contents ? "" : "Classified "][command_name] Update", !announce_contents)
diff --git a/code/modules/admin/verbs/list_exposer.dm b/code/modules/admin/verbs/list_exposer.dm
index 92a37a7f609d6..445097e8bef3f 100644
--- a/code/modules/admin/verbs/list_exposer.dm
+++ b/code/modules/admin/verbs/list_exposer.dm
@@ -13,8 +13,8 @@
if(!SSticker.HasRoundStarted())
tgui_alert(usr, "The game hasn't started yet!")
return
- var/data = "Showing last [length(GLOB.lastsignalers)] signalers."
- for(var/entry in GLOB.lastsignalers)
+ var/data = "Showing last [length(GLOB.investigate_signaler)] signalers."
+ for(var/entry in GLOB.investigate_signaler)
data += "[entry] "
usr << browse(data, "window=lastsignalers;size=800x500")
diff --git a/code/modules/admin/verbs/lua/README.md b/code/modules/admin/verbs/lua/README.md
index 17138c95fde19..707184d4d772b 100644
--- a/code/modules/admin/verbs/lua/README.md
+++ b/code/modules/admin/verbs/lua/README.md
@@ -138,6 +138,12 @@ The `SS13` package contains various helper functions that use code specific to t
### SS13.state
A reference to the state datum (`/datum/lua_state`) handling this Lua state.
+### SS13.get_runner_ckey()
+The ckey of the user who ran the lua script in the current context. Can be unreliable if accessed after sleeping.
+
+### SS13.get_runner_client()
+Returns the client of the user who ran the lua script in the current context. Can be unreliable if accessed after sleeping.
+
### SS13.global_proc
A wrapper for the magic string used to tell `WrapAdminProcCall` to call a global proc.
For instance, `/datum/callback` must be instantiated with `SS13.global_proc` as its first argument to specify that it will be invoking a global proc.
@@ -224,6 +230,42 @@ SS13.set_timeout(5, function()
end)
```
+### SS13.start_loop(time, amount, func)
+Creates a timer which will execute `func` after `time` **seconds**. `func` should not expect to be passed any arguments, as it will not be passed any. Works exactly the same as `SS13.set_timeout` except it will loop the timer `amount` times. If `amount` is set to -1, it will loop indefinitely. Returns a number value, which represents the timer's id. Can be stopped with `SS13.end_loop`
+Returns a number, the timer id, which is needed to stop indefinite timers.
+The following example will output a message to chat every 5 seconds, repeating 10 times:
+```lua
+SS13.start_loop(5, 10, function()
+ dm.global_proc("to_chat", dm.world, "Hello World!")
+end)
+```
+The following example will output a message to chat every 5 seconds, until `SS13.end_loop(timerid)` is called:
+```lua
+local timerid = SS13.start_loop(5, -1, function()
+ dm.global_proc("to_chat", dm.world, "Hello World!")
+end)
+```
+
+### SS13.end_loop(id)
+Prematurely ends a loop that hasn't ended yet, created with `SS13.start_loop`. Silently fails if there is no started loop with the specified id.
+The following example will output a message to chat every 5 seconds and delete it after it has repeated 20 times:
+```lua
+local repeated_amount = 0
+-- timerid won't be in the looping function's scope if declared before the function is declared.
+local timerid
+timerid = SS13.start_loop(5, -1, function()
+ dm.global_proc("to_chat", dm.world, "Hello World!")
+ repeated_amount += 1
+ if repeated_amount >= 20 then
+ SS13.end_loop(timerid)
+ end
+end)
+```
+
+### SS13.stop_all_loops()
+Stops all current running loops that haven't ended yet.
+Useful in case you accidentally left a indefinite loop running without storing the id anywhere.
+
### SS13.stop_tracking(datum)
Stops tracking a datum that was created via `SS13.new` so that it can be garbage collected and deleted without having to qdel. Should be used for things like callbacks and other such datums where the reference to the variable is no longer needed.
diff --git a/code/modules/admin/verbs/lua/lua_editor.dm b/code/modules/admin/verbs/lua/lua_editor.dm
index d8a414f4685c0..ea052509779bd 100644
--- a/code/modules/admin/verbs/lua/lua_editor.dm
+++ b/code/modules/admin/verbs/lua/lua_editor.dm
@@ -126,6 +126,7 @@
return TRUE
if("runCode")
var/code = params["code"]
+ current_state.ckey_last_runner = usr.ckey
var/result = current_state.load_script(code)
var/index_with_result = current_state.log_result(result)
message_admins("[key_name(usr)] executed [length(code)] bytes of lua code. [ADMIN_LUAVIEW_CHUNK(current_state, index_with_result)]")
diff --git a/code/modules/admin/verbs/lua/lua_state.dm b/code/modules/admin/verbs/lua/lua_state.dm
index b90344d633309..d5b2c6de65de8 100644
--- a/code/modules/admin/verbs/lua/lua_state.dm
+++ b/code/modules/admin/verbs/lua/lua_state.dm
@@ -21,6 +21,9 @@ GLOBAL_PROTECT(lua_usr)
/// A list in which to store datums and lists instantiated in lua, ensuring that they don't get garbage collected
var/list/references = list()
+ /// Ckey of the last user who ran a script on this lua state.
+ var/ckey_last_runner = ""
+
/datum/lua_state/vv_edit_var(var_name, var_value)
. = ..()
if(var_name == NAMEOF(src, internal_id))
diff --git a/code/modules/admin/verbs/secrets.dm b/code/modules/admin/verbs/secrets.dm
index e29ed6404e427..0943cb0c92f3d 100644
--- a/code/modules/admin/verbs/secrets.dm
+++ b/code/modules/admin/verbs/secrets.dm
@@ -554,7 +554,7 @@ GLOBAL_DATUM(everyone_a_traitor, /datum/everyone_is_a_traitor_controller)
var/spawnpoint = pick(GLOB.blobstart)
var/list/mob/dead/observer/candidates
var/mob/dead/observer/chosen_candidate
- var/mob/living/simple_animal/drone/nerd
+ var/mob/living/basic/drone/nerd
var/teamsize
teamsize = input(usr, "How many drones?", "N.E.R.D. team size", 2) as num|null
@@ -570,7 +570,7 @@ GLOBAL_DATUM(everyone_a_traitor, /datum/everyone_is_a_traitor_controller)
while(length(candidates) && teamsize)
chosen_candidate = pick(candidates)
candidates -= chosen_candidate
- nerd = new /mob/living/simple_animal/drone/classic(spawnpoint)
+ nerd = new /mob/living/basic/drone/classic(spawnpoint)
nerd.key = chosen_candidate.key
nerd.log_message("has been selected as a Nanotrasen emergency response drone.", LOG_GAME)
teamsize--
diff --git a/code/modules/admin/verbs/selectequipment.dm b/code/modules/admin/verbs/selectequipment.dm
index e88ca0bf23314..cc16be551844c 100644
--- a/code/modules/admin/verbs/selectequipment.dm
+++ b/code/modules/admin/verbs/selectequipment.dm
@@ -209,7 +209,7 @@
else
human_target = target
if(human_target.l_store || human_target.r_store || human_target.s_store) //saves a lot of time for admins and coders alike
- if(tgui_alert(usr,"Drop Items in Pockets? No will delete them.", "Robust quick dress shop", list("Yes", "No")) == "No")
+ if(tgui_alert(usr,"Do you need the items in your pockets?", "Pocket Items", list("Delete Them", "Drop Them")) == "Delete Them")
delete_pocket = TRUE
SSblackbox.record_feedback("tally", "admin_verb", 1, "Select Equipment") // If you are copy-pasting this, ensure the 4th parameter is unique to the new proc!
diff --git a/code/modules/admin/view_variables/debug_variables.dm b/code/modules/admin/view_variables/debug_variables.dm
index ea119597ffa68..8aea000a1a631 100644
--- a/code/modules/admin/view_variables/debug_variables.dm
+++ b/code/modules/admin/view_variables/debug_variables.dm
@@ -30,7 +30,7 @@
// This is split into a seperate proc mostly to make errors that happen not break things too much
/proc/_debug_variable_value(name, value, level, datum/owner, sanitize, display_flags)
- . = "DISPLAY_ERROR"
+ . = "DISPLAY_ERROR: ([value] [REF(value)])" // Make sure this line can never runtime
if(isnull(value))
return "null"
@@ -64,10 +64,15 @@
var/datum/datum_value = value
return datum_value.debug_variable_value(name, level, owner, sanitize, display_flags)
- if(islist(value) || hascall(value, "Cut")) // Some special lists arent detectable as a list through istype, so we check if it has a list proc instead
+ if(islist(value) || (name in GLOB.vv_special_lists)) // Some special lists arent detectable as a list through istype
var/list/list_value = value
var/list/items = list()
+ // This is becuse some lists either dont count as lists or a locate on their ref will return null
+ var/link_vars = "Vars=[REF(value)]"
+ if(name in GLOB.vv_special_lists)
+ link_vars = "Vars=[REF(owner)];special_varname=[name]"
+
if (!(display_flags & VV_ALWAYS_CONTRACT_LIST) && list_value.len > 0 && list_value.len <= (IS_NORMAL_LIST(list_value) ? VV_NORMAL_LIST_NO_EXPAND_THRESHOLD : VV_SPECIAL_LIST_NO_EXPAND_THRESHOLD))
for (var/i in 1 to list_value.len)
var/key = list_value[i]
@@ -80,9 +85,9 @@
items += debug_variable(key, val, level + 1, sanitize = sanitize)
- return "/list ([list_value.len])
"
@@ -197,6 +201,12 @@
pack.contains[i] = new_contents[i]
pack.cost += cost_increase
+//To append cargo crate value to final order cost
+/datum/supply_order/materials
+
+/datum/supply_order/materials/get_final_cost()
+ return (..() + CARGO_CRATE_VALUE)
+
#undef MANIFEST_ERROR_CHANCE
#undef MANIFEST_ERROR_NAME
#undef MANIFEST_ERROR_CONTENTS
diff --git a/code/modules/cargo/orderconsole.dm b/code/modules/cargo/orderconsole.dm
index 7767b83bcb419..16e3be76f3fb0 100644
--- a/code/modules/cargo/orderconsole.dm
+++ b/code/modules/cargo/orderconsole.dm
@@ -152,7 +152,7 @@
"cost" = pack.get_cost(),
"orderer" = order.orderer,
"reason" = order.reason,
- "id" = order.id
+ "id" = order.id,
))
data["amount_by_name"] = amount_by_name
@@ -177,7 +177,7 @@
"id" = pack,
"desc" = P.desc || P.name, // If there is a description, use it. Otherwise use the pack's name.
"goody" = P.goody,
- "access" = P.access
+ "access" = P.access,
))
return data
@@ -259,7 +259,15 @@
applied_coupon = coupon_check
break
- var/datum/supply_order/order = new(pack = pack ,orderer = name, orderer_rank = rank, orderer_ckey = ckey, reason = reason, paying_account = account, coupon = applied_coupon)
+ var/datum/supply_order/order = new(
+ pack = pack ,
+ orderer = name,
+ orderer_rank = rank,
+ orderer_ckey = ckey,
+ reason = reason,
+ paying_account = account,
+ coupon = applied_coupon,
+ )
working_list += order
if(self_paid)
@@ -280,13 +288,14 @@
continue
if(order.department_destination)
say("Only the department that ordered this item may cancel it.")
- return
+ return FALSE
if(order.applied_coupon)
say("Coupon refunded.")
order.applied_coupon.forceMove(get_turf(src))
SSshuttle.shopping_list -= order
- . = TRUE
- break
+ qdel(order)
+ return TRUE
+ return FALSE
/**
* maps the ordename displayed on the ui to its supply pack id
* * order_name - the name of the order
diff --git a/code/modules/cargo/packs/general.dm b/code/modules/cargo/packs/general.dm
index 99f8972ccb8c7..cd6df472d4bf6 100644
--- a/code/modules/cargo/packs/general.dm
+++ b/code/modules/cargo/packs/general.dm
@@ -26,48 +26,6 @@
crate_name = "tattoo crate"
crate_type = /obj/structure/closet/crate/wooden
-/datum/supply_pack/misc/aquarium_kit
- name = "Aquarium Kit"
- desc = "Everything you need to start your own aquarium. Contains aquarium construction kit, \
- fish catalog, fish food and three freshwater fish from our collection."
- cost = CARGO_CRATE_VALUE * 5
- contains = list(/obj/item/book/fish_catalog,
- /obj/item/storage/fish_case/random/freshwater = 3,
- /obj/item/fish_feed,
- /obj/item/storage/box/aquarium_props,
- /obj/item/aquarium_kit,
- )
- crate_name = "aquarium kit crate"
- crate_type = /obj/structure/closet/crate/wooden
-
-/datum/supply_pack/misc/aquarium_fish
- name = "Aquarium Fish Case"
- desc = "An aquarium fish bundle handpicked by monkeys from our collection. Contains two random fish."
- cost = CARGO_CRATE_VALUE * 2
- contains = list(/obj/item/storage/fish_case/random = 2)
- crate_name = "aquarium fish crate"
-
-/datum/supply_pack/misc/freshwater_fish
- name = "Freshwater Fish Case"
- desc = "Aquarium fish that have had most of their mud cleaned off."
- cost = CARGO_CRATE_VALUE * 2
- contains = list(/obj/item/storage/fish_case/random/freshwater = 2)
- crate_name = "freshwater fish crate"
-
-/datum/supply_pack/misc/saltwater_fish
- name = "Saltwater Fish Case"
- desc = "Aquarium fish that fill the room with the smell of salt."
- cost = CARGO_CRATE_VALUE * 2
- contains = list(/obj/item/storage/fish_case/random/saltwater = 2)
- crate_name = "saltwater fish crate"
-
-/datum/supply_pack/misc/tiziran_fish
- name = "Tiziran Fish Case"
- desc = "Tiziran saltwater fish imported from the Zagos Sea."
- cost = CARGO_CRATE_VALUE * 2
- contains = list(/obj/item/storage/fish_case/tiziran = 2)
- crate_name = "tiziran fish crate"
-
/datum/supply_pack/misc/bicycle
name = "Bicycle"
desc = "Nanotrasen reminds all employees to never toy with powers outside their control."
diff --git a/code/modules/cargo/packs/livestock.dm b/code/modules/cargo/packs/livestock.dm
index 69dd7e6180772..ef9fb96182302 100644
--- a/code/modules/cargo/packs/livestock.dm
+++ b/code/modules/cargo/packs/livestock.dm
@@ -223,3 +223,34 @@
. = ..()
for(var/i in 1 to 2)
new /mob/living/basic/garden_gnome(.)
+
+/datum/supply_pack/critter/fish
+ crate_type = /obj/structure/closet/crate
+
+/datum/supply_pack/critter/fish/aquarium_fish
+ name = "Aquarium Fish Case"
+ desc = "An aquarium fish bundle handpicked by monkeys from our collection. Contains two random fish."
+ cost = CARGO_CRATE_VALUE * 2
+ contains = list(/obj/item/storage/fish_case/random = 2)
+ crate_name = "aquarium fish crate"
+
+/datum/supply_pack/critter/fish/freshwater_fish
+ name = "Freshwater Fish Case"
+ desc = "Aquarium fish that have had most of their mud cleaned off."
+ cost = CARGO_CRATE_VALUE * 2
+ contains = list(/obj/item/storage/fish_case/random/freshwater = 2)
+ crate_name = "freshwater fish crate"
+
+/datum/supply_pack/critter/fish/saltwater_fish
+ name = "Saltwater Fish Case"
+ desc = "Aquarium fish that fill the room with the smell of salt."
+ cost = CARGO_CRATE_VALUE * 2
+ contains = list(/obj/item/storage/fish_case/random/saltwater = 2)
+ crate_name = "saltwater fish crate"
+
+/datum/supply_pack/critter/fish/tiziran_fish
+ name = "Tiziran Fish Case"
+ desc = "Tiziran saltwater fish imported from the Zagos Sea."
+ cost = CARGO_CRATE_VALUE * 2
+ contains = list(/obj/item/storage/fish_case/tiziran = 2)
+ crate_name = "tiziran fish crate"
diff --git a/code/modules/cargo/packs/security.dm b/code/modules/cargo/packs/security.dm
index 784b870fea084..42a6c03544a2a 100644
--- a/code/modules/cargo/packs/security.dm
+++ b/code/modules/cargo/packs/security.dm
@@ -233,6 +233,14 @@
crate_name = "laser carbine crate"
crate_type = /obj/structure/closet/crate/secure/plasma
+/datum/supply_pack/security/armory/disabler_smg
+ name = "Disabler SMG Crate"
+ desc = "Contains three disabler SMGs, capable of rapidly firing weak disabler beams."
+ cost = CARGO_CRATE_VALUE * 7
+ contains = list(/obj/item/gun/energy/disabler/smg = 3)
+ crate_name = "disabler smg crate"
+ crate_type = /obj/structure/closet/crate/secure/plasma
+
/datum/supply_pack/security/armory/exileimp
name = "Exile Implants Crate"
desc = "Contains five Exile implants."
diff --git a/code/modules/cargo/packs/service.dm b/code/modules/cargo/packs/service.dm
index 8c84864a80244..45660dcc73ee6 100644
--- a/code/modules/cargo/packs/service.dm
+++ b/code/modules/cargo/packs/service.dm
@@ -270,3 +270,17 @@
contains = list(/obj/machinery/coffeemaker/impressa)
crate_name = "coffeemaker crate"
crate_type = /obj/structure/closet/crate/large
+
+/datum/supply_pack/service/aquarium_kit
+ name = "Aquarium Kit"
+ desc = "Everything you need to start your own aquarium. Contains aquarium construction kit, \
+ fish catalog, fish food and three freshwater fish from our collection."
+ cost = CARGO_CRATE_VALUE * 5
+ contains = list(/obj/item/book/manual/fish_catalog,
+ /obj/item/storage/fish_case/random/freshwater = 3,
+ /obj/item/fish_feed,
+ /obj/item/storage/box/aquarium_props,
+ /obj/item/aquarium_kit,
+ )
+ crate_name = "aquarium kit crate"
+ crate_type = /obj/structure/closet/crate/wooden
diff --git a/code/modules/cargo/universal_scanner.dm b/code/modules/cargo/universal_scanner.dm
index 880a75783d899..2b9bb46e0580e 100644
--- a/code/modules/cargo/universal_scanner.dm
+++ b/code/modules/cargo/universal_scanner.dm
@@ -167,16 +167,52 @@
* Scans an object, target, and provides it's export value based on selling to the cargo shuttle, to mob/user.
*/
/obj/item/universal_scanner/proc/export_scan(obj/target, mob/user)
- // Before you fix it:
- // yes, checking manifests is a part of intended functionality.
var/datum/export_report/report = export_item_and_contents(target, dry_run = TRUE)
var/price = 0
for(var/exported_datum in report.total_amount)
price += report.total_value[exported_datum]
- if(price)
- to_chat(user, span_notice("Scanned [target], value: [price] credits[target.contents.len ? " (contents included)" : ""]."))
+
+ var/message = "Scanned [target]"
+ var/warning = FALSE
+ if(length(target.contents))
+ message = "Scanned [target] and its contents"
+ if(price)
+ message += ", total value: [price] credits"
+ else
+ message += ", no export values"
+ warning = TRUE
+ if(!report.all_contents_scannable)
+ message += " (Undeterminable value detected, final value may differ)"
+ message += "."
+ else
+ if(!report.all_contents_scannable)
+ message += ", unable to determine value."
+ warning = TRUE
+ else if(price)
+ message += ", value: [price] credits."
+ else
+ message += ", no export value."
+ warning = TRUE
+ if(warning)
+ to_chat(user, span_warning(message))
else
- to_chat(user, span_warning("Scanned [target], no export value."))
+ to_chat(user, span_notice(message))
+
+ if(price)
+ playsound(src, 'sound/machines/terminal_select.ogg', 50, vary = TRUE)
+
+ if(istype(target, /obj/item/delivery))
+ var/obj/item/delivery/parcel = target
+ if(!parcel.sticker)
+ return
+ var/obj/item/barcode/our_code = parcel.sticker
+ to_chat(user, span_notice("Export barcode detected! This parcel, upon export, will pay out to [our_code.payments_acc.account_holder], \
+ with a [our_code.cut_multiplier * 100]% split to them (already reflected in above recorded value)."))
+
+ if(istype(target, /obj/item/barcode))
+ var/obj/item/barcode/our_code = target
+ to_chat(user, span_notice("Export barcode detected! This barcode, if attached to a parcel, will pay out to [our_code.payments_acc.account_holder], \
+ with a [our_code.cut_multiplier * 100]% split to them."))
if(ishuman(user))
var/mob/living/carbon/human/scan_human = user
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index e6b94c20ede17..77240be2b60a3 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -266,6 +266,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
prefs = GLOB.preferences_datums[ckey]
if(prefs)
prefs.parent = src
+ prefs.load_savefile() // just to make sure we have the latest data
prefs.apply_all_client_preferences()
else
prefs = new /datum/preferences(src)
diff --git a/code/modules/client/preferences/README.md b/code/modules/client/preferences/README.md
index 0089953639883..fabfb779c902b 100644
--- a/code/modules/client/preferences/README.md
+++ b/code/modules/client/preferences/README.md
@@ -28,7 +28,7 @@ export const savefile_key_here: Feature = {
// Necessary for game preferences, unused for others
category: "CATEGORY",
- // Optional, only shown in game preferences
+ // Optional, shown as a tooltip
description: "This preference will blow your mind!",
}
```
diff --git a/code/modules/client/preferences/food_allergy.dm b/code/modules/client/preferences/food_allergy.dm
new file mode 100644
index 0000000000000..461c3b31e2a32
--- /dev/null
+++ b/code/modules/client/preferences/food_allergy.dm
@@ -0,0 +1,20 @@
+/datum/preference/choiced/food_allergy
+ category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
+ savefile_key = "food_allergy"
+ savefile_identifier = PREFERENCE_CHARACTER
+ can_randomize = FALSE
+
+/datum/preference/choiced/food_allergy/init_possible_values()
+ return list("Random") + assoc_to_keys(GLOB.possible_food_allergies)
+
+/datum/preference/choiced/food_allergy/create_default_value()
+ return "Random"
+
+/datum/preference/choiced/food_allergy/is_accessible(datum/preferences/preferences)
+ if (!..())
+ return FALSE
+
+ return "Food Allergy" in preferences.all_quirks
+
+/datum/preference/choiced/food_allergy/apply_to_human(mob/living/carbon/human/target, value)
+ return
diff --git a/code/modules/client/preferences/species_features/basic.dm b/code/modules/client/preferences/species_features/basic.dm
index 6e34a234e7d01..abf4ea0e44e20 100644
--- a/code/modules/client/preferences/species_features/basic.dm
+++ b/code/modules/client/preferences/species_features/basic.dm
@@ -1,4 +1,4 @@
-/proc/generate_icon_with_head_accessory(datum/sprite_accessory/sprite_accessory)
+/proc/generate_icon_with_head_accessory(datum/sprite_accessory/sprite_accessory, y_offset = 0)
var/static/icon/head_icon
if (isnull(head_icon))
head_icon = icon('icons/mob/human/bodyparts_greyscale.dmi', "human_head_m")
@@ -7,8 +7,10 @@
var/icon/final_icon = new(head_icon)
if (!isnull(sprite_accessory))
ASSERT(istype(sprite_accessory))
-
+
var/icon/head_accessory_icon = icon(sprite_accessory.icon, sprite_accessory.icon_state)
+ if(y_offset)
+ head_accessory_icon.Shift(NORTH, y_offset)
head_accessory_icon.Blend(COLOR_DARK_BROWN, ICON_MULTIPLY)
final_icon.Blend(head_accessory_icon, ICON_OVERLAY)
@@ -138,7 +140,8 @@
return assoc_to_keys_features(GLOB.hairstyles_list)
/datum/preference/choiced/hairstyle/icon_for(value)
- return generate_icon_with_head_accessory(GLOB.hairstyles_list[value])
+ var/datum/sprite_accessory/hair/hairstyle = GLOB.hairstyles_list[value]
+ return generate_icon_with_head_accessory(hairstyle, hairstyle?.y_offset)
/datum/preference/choiced/hairstyle/apply_to_human(mob/living/carbon/human/target, value)
target.set_hairstyle(value, update = FALSE)
diff --git a/code/modules/client/verbs/who.dm b/code/modules/client/verbs/who.dm
index 6dea707240fa5..5b31ae49849ce 100644
--- a/code/modules/client/verbs/who.dm
+++ b/code/modules/client/verbs/who.dm
@@ -1,4 +1,5 @@
#define DEFAULT_WHO_CELLS_PER_ROW 4
+#define NO_ADMINS_ONLINE_MESSAGE "Adminhelps are also sent through TGS to services like IRC and Discord. If no admins are available in game, sending an adminhelp might still be noticed and responded to."
/client/verb/who()
set name = "Who"
@@ -72,45 +73,100 @@
set category = "Admin"
set name = "Adminwho"
- var/msg = "Current Admins:\n"
- var/display_name
- if(holder)
- for(var/client/client in GLOB.admins)
- var/feedback_link = client.holder.feedback_link()
- display_name = feedback_link ? "[client]" : client
-
- msg += "\t[display_name] is a [client.holder.rank_names()]"
-
- if(client.holder.fakekey)
- msg += " (as [client.holder.fakekey])"
-
- if(isobserver(client.mob))
- msg += " - Observing"
- else if(isnewplayer(client.mob))
- if(SSticker.current_state <= GAME_STATE_PREGAME)
- var/mob/dead/new_player/lobbied_admin = client.mob
- if(lobbied_admin.ready == PLAYER_READY_TO_PLAY)
- msg += " - Lobby (Readied)"
- else
- msg += " - Lobby (Not readied)"
+ var/list/lines = list()
+ var/payload_string = generate_adminwho_string()
+ var/header
+
+ if(payload_string == NO_ADMINS_ONLINE_MESSAGE)
+ header = "No Admins Currently Online"
+ else
+ header = "Current Admins:"
+
+ lines += span_bold(header)
+ lines += payload_string
+
+ var/finalized_string = examine_block(jointext(lines, "\n"))
+ to_chat(src, finalized_string)
+
+/// Proc that generates the applicable string to dispatch to the client for adminwho.
+/client/proc/generate_adminwho_string()
+ var/list/list_of_admins = get_list_of_admins()
+ if(isnull(list_of_admins))
+ return NO_ADMINS_ONLINE_MESSAGE
+
+ var/list/message_strings = list()
+ if(isnull(holder))
+ message_strings += get_general_adminwho_information(list_of_admins)
+ message_strings += NO_ADMINS_ONLINE_MESSAGE
+ else
+ message_strings += get_sensitive_adminwho_information(list_of_admins)
+
+ return jointext(message_strings, "\n")
+
+/// Proc that returns a list of cliented admins. Remember that this list can contain nulls!
+/// Also, will return null if we don't have any admins.
+/proc/get_list_of_admins()
+ var/returnable_list = list()
+
+ for(var/client/admin in GLOB.admins)
+ returnable_list += admin
+
+ if(length(returnable_list) == 0)
+ return null
+
+ return returnable_list
+
+/// Proc that will return the applicable display name, linkified or not, based on the input client reference.
+/proc/get_linked_admin_name(client/admin)
+ var/feedback_link = admin.holder.feedback_link()
+ return isnull(feedback_link) ? admin : "[admin]"
+
+/// Proc that gathers adminwho information for a general player, which will only give information if an admin isn't AFK, and handles potential fakekeying.
+/// Will return a list of strings.
+/proc/get_general_adminwho_information(list/checkable_admins)
+ var/returnable_list = list()
+
+ for(var/client/admin in checkable_admins)
+ if(admin.is_afk() || !isnull(admin.holder.fakekey))
+ continue //Don't show afk or fakekeyed admins to adminwho
+
+ returnable_list += "• [get_linked_admin_name(admin)] is a [admin.holder.rank_names()]"
+
+ return returnable_list
+
+/// Proc that gathers adminwho information for admins, which will contain information on if the admin is AFK, readied to join, etc. Only arg is a list of clients to use.
+/// Will return a list of strings.
+/proc/get_sensitive_adminwho_information(list/checkable_admins)
+ var/returnable_list = list()
+
+ for(var/client/admin in checkable_admins)
+ var/list/admin_strings = list()
+
+ admin_strings += "• [get_linked_admin_name(admin)] is a [admin.holder.rank_names()]"
+
+ if(admin.holder.fakekey)
+ admin_strings += "(as [admin.holder.fakekey])"
+
+ if(isobserver(admin.mob))
+ admin_strings += "- Observing"
+ else if(isnewplayer(admin.mob))
+ if(SSticker.current_state <= GAME_STATE_PREGAME)
+ var/mob/dead/new_player/lobbied_admin = admin.mob
+ if(lobbied_admin.ready == PLAYER_READY_TO_PLAY)
+ admin_strings += "- Lobby (Readied)"
else
- msg += " - Lobby"
+ admin_strings += "- Lobby (Not Readied)"
else
- msg += " - Playing"
+ admin_strings += "- Lobby"
+ else
+ admin_strings += "- Playing"
- if(client.is_afk())
- msg += " (AFK)"
- msg += "\n"
- else
- for(var/client/client in GLOB.admins)
- var/feedback_link = client.holder.feedback_link()
- display_name = feedback_link ? "[client]" : client
+ if(admin.is_afk())
+ admin_strings += "(AFK)"
+
+ returnable_list += jointext(admin_strings, " ")
- if(client.is_afk())
- continue //Don't show afk admins to adminwho
- if(!client.holder.fakekey)
- msg += "\t[display_name] is a [client.holder.rank_names()]\n"
- msg += span_info("Adminhelps are also sent through TGS to services like IRC and Discord. If no admins are available in game, sending an adminhelp might still be noticed and responded to.")
- to_chat(src, msg)
+ return returnable_list
#undef DEFAULT_WHO_CELLS_PER_ROW
+#undef NO_ADMINS_ONLINE_MESSAGE
diff --git a/code/modules/clothing/chameleon/chameleon_drone.dm b/code/modules/clothing/chameleon/chameleon_drone.dm
index 83d6610d67153..dd50fc871088d 100644
--- a/code/modules/clothing/chameleon/chameleon_drone.dm
+++ b/code/modules/clothing/chameleon/chameleon_drone.dm
@@ -32,7 +32,7 @@
if(!IsAvailable(feedback = TRUE))
return FALSE
- var/mob/living/simple_animal/drone/droney = owner
+ var/mob/living/basic/drone/droney = owner
// The drone unEquip() proc sets head to null after dropping
// an item, so we need to keep a reference to our old headgear
diff --git a/code/modules/clothing/head/hat.dm b/code/modules/clothing/head/hat.dm
index 89244adcf31bc..2eaed3bfc2f42 100644
--- a/code/modules/clothing/head/hat.dm
+++ b/code/modules/clothing/head/hat.dm
@@ -90,11 +90,11 @@
inhand_icon_state = null
/obj/item/clothing/head/cowboy
- name = "bounty hunting hat"
+ name = "cowboy hat"
desc = "Ain't nobody gonna cheat the hangman in my town."
icon = 'icons/obj/clothing/head/cowboy.dmi'
worn_icon = 'icons/mob/clothing/head/cowboy.dmi'
- icon_state = "cowboy"
+ icon_state = "cowboy_hat_brown"
worn_icon_state = "hunter"
inhand_icon_state = null
armor_type = /datum/armor/head_cowboy
@@ -126,6 +126,10 @@
/// Bounty hunter's hat, very likely to intercept bullets
/obj/item/clothing/head/cowboy/bounty
+ name = "bounty hunting hat"
+ desc = "Reach for the skies, pardner."
+ icon_state = "bounty_hunter"
+ worn_icon_state = "hunter"
deflect_chance = 50
/obj/item/clothing/head/cowboy/black
diff --git a/code/modules/clothing/head/jobs.dm b/code/modules/clothing/head/jobs.dm
index 3c26de45e642f..1f7bc689812e3 100644
--- a/code/modules/clothing/head/jobs.dm
+++ b/code/modules/clothing/head/jobs.dm
@@ -343,9 +343,21 @@
/obj/item/clothing/head/hats/hos/cap
name = "head of security cap"
- desc = "The robust standard-issue cap of the Head of Security. For showing the officers who's in charge."
+ desc = "The robust standard-issue cap of the Head of Security. For showing the officers who's in charge. Looks a bit stout."
icon_state = "hoscap"
+/obj/item/clothing/head/hats/hos/cap/Initialize(mapload)
+ . = ..()
+ // Give it a little publicity
+ var/static/list/slapcraft_recipe_list = list(\
+ /datum/crafting_recipe/sturdy_shako,\
+ )
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/datum/armor/hats_hos
melee = 40
bullet = 30
diff --git a/code/modules/clothing/head/wig.dm b/code/modules/clothing/head/wig.dm
index b692c1383c50b..ad2759a137ced 100644
--- a/code/modules/clothing/head/wig.dm
+++ b/code/modules/clothing/head/wig.dm
@@ -25,24 +25,36 @@
item_flags &= ~EXAMINE_SKIP
/obj/item/clothing/head/wig/update_icon_state()
- var/datum/sprite_accessory/hair_style = GLOB.hairstyles_list[hairstyle]
+ var/datum/sprite_accessory/hair/hair_style = GLOB.hairstyles_list[hairstyle]
if(hair_style)
icon = hair_style.icon
icon_state = hair_style.icon_state
return ..()
+/obj/item/clothing/head/wig/build_worn_icon(
+ default_layer = 0,
+ default_icon_file = null,
+ isinhands = FALSE,
+ female_uniform = NO_FEMALE_UNIFORM,
+ override_state = null,
+ override_file = null,
+ use_height_offset = TRUE,
+)
+ return ..(default_layer, default_icon_file, isinhands, female_uniform, override_state, override_file, use_height_offset = FALSE)
+
/obj/item/clothing/head/wig/worn_overlays(mutable_appearance/standing, isinhands = FALSE, file2use)
. = ..()
if(isinhands)
return
- var/datum/sprite_accessory/hair = GLOB.hairstyles_list[hairstyle]
+ var/datum/sprite_accessory/hair/hair = GLOB.hairstyles_list[hairstyle]
if(!hair)
return
var/mutable_appearance/hair_overlay = mutable_appearance(hair.icon, hair.icon_state, layer = -HAIR_LAYER, appearance_flags = RESET_COLOR)
hair_overlay.color = color
+ hair_overlay.pixel_y = hair.y_offset
. += hair_overlay
// So that the wig actually blocks emissives.
diff --git a/code/modules/clothing/masks/_masks.dm b/code/modules/clothing/masks/_masks.dm
index ad296d3035684..0bcd0e14382c3 100644
--- a/code/modules/clothing/masks/_masks.dm
+++ b/code/modules/clothing/masks/_masks.dm
@@ -16,6 +16,8 @@
var/voice_override
/// If set to true, activates the radio effect on TTS. Used for sec hailers, but other masks can utilize it for their own vocal effect.
var/use_radio_beeps_tts = FALSE
+ /// The unique sound effect of dying while wearing this
+ var/unique_death
/obj/item/clothing/mask/attack_self(mob/user)
if((clothing_flags & VOICEBOX_TOGGLABLE))
diff --git a/code/modules/clothing/masks/hailer.dm b/code/modules/clothing/masks/hailer.dm
index 64de19b95aa17..5720c754ead99 100644
--- a/code/modules/clothing/masks/hailer.dm
+++ b/code/modules/clothing/masks/hailer.dm
@@ -57,6 +57,7 @@ GLOBAL_LIST_INIT(hailer_phrases, list(
visor_flags_cover = MASKCOVERSMOUTH
tint = 0
has_fov = FALSE
+ unique_death = 'sound/voice/sec_death.ogg'
COOLDOWN_DECLARE(hailer_cooldown)
///Decides the phrases available for use; defines used are the last index of a category of available phrases
var/aggressiveness = AGGR_BAD_COP
diff --git a/code/modules/clothing/shoes/cowboy.dm b/code/modules/clothing/shoes/cowboy.dm
index 0aa518bc1364d..a033a561439ff 100644
--- a/code/modules/clothing/shoes/cowboy.dm
+++ b/code/modules/clothing/shoes/cowboy.dm
@@ -38,11 +38,7 @@
occupant.forceMove(user.drop_location())
user.visible_message(span_warning("[user] recoils as something slithers out of [src]."), span_userdanger("You feel a sudden stabbing pain in your [pick("foot", "toe", "ankle")]!"))
user.Knockdown(20) //Is one second paralyze better here? I feel you would fall on your ass in some fashion.
- user.apply_damage(5, BRUTE, target_zone)
- if(istype(occupant, /mob/living/simple_animal/hostile/retaliate))
- user.reagents.add_reagent(/datum/reagent/toxin, 7)
-
-
+ occupant.UnarmedAttack(user, proximity_flag = TRUE)
/obj/item/clothing/shoes/cowboy/dropped(mob/living/user)
. = ..()
diff --git a/code/modules/clothing/shoes/gunboots.dm b/code/modules/clothing/shoes/gunboots.dm
index db24b2338cf3a..d8335b5fcb0ed 100644
--- a/code/modules/clothing/shoes/gunboots.dm
+++ b/code/modules/clothing/shoes/gunboots.dm
@@ -18,13 +18,13 @@
/obj/item/clothing/shoes/gunboots/equipped(mob/user, slot)
. = ..()
if(slot & ITEM_SLOT_FEET)
- RegisterSignal(user, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, PROC_REF(check_kick))
+ RegisterSignal(user, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(check_kick))
else
- UnregisterSignal(user, COMSIG_HUMAN_MELEE_UNARMED_ATTACK)
+ UnregisterSignal(user, COMSIG_LIVING_UNARMED_ATTACK)
/obj/item/clothing/shoes/gunboots/dropped(mob/user)
if(user)
- UnregisterSignal(user, COMSIG_HUMAN_MELEE_UNARMED_ATTACK)
+ UnregisterSignal(user, COMSIG_LIVING_UNARMED_ATTACK)
return ..()
/// After each step, check if we randomly fire a shot
@@ -38,8 +38,8 @@
/// Stomping on someone while wearing gunboots shoots them point blank
/obj/item/clothing/shoes/gunboots/proc/check_kick(mob/living/carbon/human/kicking_person, atom/attacked_atom, proximity)
SIGNAL_HANDLER
- if(!isliving(attacked_atom))
- return
+ if(!proximity || !isliving(attacked_atom))
+ return NONE
var/mob/living/attacked_living = attacked_atom
if(attacked_living.body_position == LYING_DOWN)
INVOKE_ASYNC(src, PROC_REF(fire_shot), attacked_living)
diff --git a/code/modules/clothing/spacesuits/bountyhunter.dm b/code/modules/clothing/spacesuits/bountyhunter.dm
index 0c83e1b01d224..9218deb5633fc 100644
--- a/code/modules/clothing/spacesuits/bountyhunter.dm
+++ b/code/modules/clothing/spacesuits/bountyhunter.dm
@@ -3,7 +3,7 @@
desc = "A custom version of the MK.II SWAT suit, modified to look rugged and tough. Works as a space suit, if you can find a helmet."
icon_state = "hunter"
inhand_icon_state = "swat_suit"
- allowed = list(/obj/item/gun, /obj/item/ammo_box, /obj/item/ammo_casing, /obj/item/melee/baton, /obj/item/restraints/handcuffs, /obj/item/tank/internals, /obj/item/knife/combat)
+ allowed = list(/obj/item/gun, /obj/item/melee/baton, /obj/item/restraints/handcuffs, /obj/item/tank/internals, /obj/item/knife/combat)
armor_type = /datum/armor/space_hunter
strip_delay = 130
resistance_flags = FIRE_PROOF | ACID_PROOF
diff --git a/code/modules/clothing/spacesuits/freedom.dm b/code/modules/clothing/spacesuits/freedom.dm
index 73343a82eeeb7..b0a08f4cc7367 100644
--- a/code/modules/clothing/spacesuits/freedom.dm
+++ b/code/modules/clothing/spacesuits/freedom.dm
@@ -25,7 +25,7 @@
desc = "An advanced, light suit, fabricated from a mixture of synthetic feathers and space-resistant material. A gun holster appears to be integrated into the suit and the wings appear to be stuck in 'freedom' mode."
icon_state = "freedom"
inhand_icon_state = null
- allowed = list(/obj/item/gun, /obj/item/ammo_box, /obj/item/ammo_casing, /obj/item/melee/baton, /obj/item/restraints/handcuffs, /obj/item/tank/internals)
+ allowed = list(/obj/item/gun, /obj/item/melee/baton, /obj/item/restraints/handcuffs, /obj/item/tank/internals)
armor_type = /datum/armor/space_freedom
strip_delay = 130
max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
diff --git a/code/modules/clothing/spacesuits/pirate.dm b/code/modules/clothing/spacesuits/pirate.dm
index 34a495b4dbef8..946c0c2b66fdd 100644
--- a/code/modules/clothing/spacesuits/pirate.dm
+++ b/code/modules/clothing/spacesuits/pirate.dm
@@ -25,7 +25,7 @@
desc = "A modified suit to allow space pirates to board shuttles and stations while avoiding the maw of the void. Comes with additional protection and is lighter to move in."
icon_state = "spacepirate"
w_class = WEIGHT_CLASS_NORMAL
- allowed = list(/obj/item/gun, /obj/item/ammo_box, /obj/item/ammo_casing, /obj/item/melee/baton, /obj/item/restraints/handcuffs, /obj/item/tank/internals, /obj/item/melee/energy/sword/pirate, /obj/item/clothing/glasses/eyepatch, /obj/item/reagent_containers/cup/glass/bottle/rum)
+ allowed = list(/obj/item/gun, /obj/item/melee/baton, /obj/item/restraints/handcuffs, /obj/item/tank/internals, /obj/item/melee/energy/sword/pirate, /obj/item/clothing/glasses/eyepatch, /obj/item/reagent_containers/cup/glass/bottle/rum)
slowdown = 0
armor_type = /datum/armor/space_pirate
strip_delay = 40
diff --git a/code/modules/clothing/spacesuits/specialops.dm b/code/modules/clothing/spacesuits/specialops.dm
index 3634066e72fd4..cf8fc2a475cc6 100644
--- a/code/modules/clothing/spacesuits/specialops.dm
+++ b/code/modules/clothing/spacesuits/specialops.dm
@@ -35,7 +35,7 @@
slowdown = 0
flags_inv = 0
w_class = WEIGHT_CLASS_NORMAL
- allowed = list(/obj/item/gun, /obj/item/ammo_box, /obj/item/ammo_casing, /obj/item/melee/baton, /obj/item/restraints/handcuffs, /obj/item/tank/internals)
+ allowed = list(/obj/item/gun, /obj/item/melee/baton, /obj/item/restraints/handcuffs, /obj/item/tank/internals)
armor_type = /datum/armor/space_officer
strip_delay = 130
max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
diff --git a/code/modules/clothing/spacesuits/syndi.dm b/code/modules/clothing/spacesuits/syndi.dm
index d9939a466211e..cbacf064052db 100644
--- a/code/modules/clothing/spacesuits/syndi.dm
+++ b/code/modules/clothing/spacesuits/syndi.dm
@@ -38,7 +38,7 @@ GLOBAL_LIST_INIT(syndicate_space_suits_to_helmets,list(
inhand_icon_state = "space_suit_syndicate"
desc = "Has a tag on it: Totally not property of an enemy corporation, honest!"
w_class = WEIGHT_CLASS_NORMAL
- allowed = list(/obj/item/gun, /obj/item/ammo_box, /obj/item/ammo_casing, /obj/item/melee/baton, /obj/item/melee/energy/sword/saber, /obj/item/restraints/handcuffs, /obj/item/tank/internals)
+ allowed = list(/obj/item/gun, /obj/item/melee/baton, /obj/item/melee/energy/sword/saber, /obj/item/restraints/handcuffs, /obj/item/tank/internals)
armor_type = /datum/armor/space_syndicate
cell = /obj/item/stock_parts/cell/hyper
var/helmet_type = /obj/item/clothing/head/helmet/space/syndicate
diff --git a/code/modules/clothing/suits/ghostsheet.dm b/code/modules/clothing/suits/ghostsheet.dm
index e99d838dc50df..268cca461993f 100644
--- a/code/modules/clothing/suits/ghostsheet.dm
+++ b/code/modules/clothing/suits/ghostsheet.dm
@@ -10,6 +10,7 @@
flags_inv = HIDEGLOVES|HIDEEARS|HIDEFACE|HIDEHAIR|HIDEFACIALHAIR|HIDESNOUT
alternate_worn_layer = UNDER_HEAD_LAYER
species_exception = list(/datum/species/golem)
+ supports_variations_flags = CLOTHING_DIGITIGRADE_VARIATION_NO_NEW_ICON
/obj/item/clothing/suit/costume/ghost_sheet/Initialize(mapload)
. = ..()
diff --git a/code/modules/clothing/suits/labcoat.dm b/code/modules/clothing/suits/labcoat.dm
index 3ded96e52a39f..9a3d94e1dbf4b 100644
--- a/code/modules/clothing/suits/labcoat.dm
+++ b/code/modules/clothing/suits/labcoat.dm
@@ -64,12 +64,18 @@
/obj/item/clothing/suit/toggle/labcoat/genetics
name = "geneticist labcoat"
desc = "A suit that protects against minor chemical spills. Has a blue stripe on the shoulder."
- icon_state = "labcoat_gen"
+ icon_state = "labcoat_job"
+ greyscale_config = /datum/greyscale_config/labcoat
+ greyscale_config_worn = /datum/greyscale_config/labcoat/worn
+ greyscale_colors = "#EEEEEE#4A77A1#4A77A1#7095C2"
/obj/item/clothing/suit/toggle/labcoat/chemist
name = "chemist labcoat"
desc = "A suit that protects against minor chemical spills. Has an orange stripe on the shoulder."
- icon_state = "labcoat_chem"
+ icon_state = "labcoat_job"
+ greyscale_config = /datum/greyscale_config/labcoat
+ greyscale_config_worn = /datum/greyscale_config/labcoat/worn
+ greyscale_colors = "#EEEEEE#F17420#F17420#EB6F2C"
/obj/item/clothing/suit/toggle/labcoat/chemist/Initialize(mapload)
. = ..()
@@ -78,7 +84,10 @@
/obj/item/clothing/suit/toggle/labcoat/virologist
name = "virologist labcoat"
desc = "A suit that protects against minor chemical spills. Has a green stripe on the shoulder."
- icon_state = "labcoat_vir"
+ icon_state = "labcoat_job"
+ greyscale_config = /datum/greyscale_config/labcoat
+ greyscale_config_worn = /datum/greyscale_config/labcoat/worn
+ greyscale_colors = "#EEEEEE#198019#198019#40992E"
/obj/item/clothing/suit/toggle/labcoat/virologist/Initialize(mapload)
. = ..()
@@ -87,7 +96,10 @@
/obj/item/clothing/suit/toggle/labcoat/coroner
name = "coroner labcoat"
desc = "A suit that protects against minor chemical spills. Has a black stripe on the shoulder."
- icon_state = "labcoat_coroner"
+ icon_state = "labcoat_job"
+ greyscale_config = /datum/greyscale_config/labcoat
+ greyscale_config_worn = /datum/greyscale_config/labcoat/worn
+ greyscale_colors = "#EEEEEE#2D2D33#2D2D33#39393F"
/obj/item/clothing/suit/toggle/labcoat/coroner/Initialize(mapload)
. = ..()
@@ -99,7 +111,10 @@
/obj/item/clothing/suit/toggle/labcoat/science
name = "scientist labcoat"
desc = "A suit that protects against minor chemical spills. Has a purple stripe on the shoulder."
- icon_state = "labcoat_sci"
+ icon_state = "labcoat_job"
+ greyscale_config = /datum/greyscale_config/labcoat
+ greyscale_config_worn = /datum/greyscale_config/labcoat/worn
+ greyscale_colors = "#EEEEEE#7E1980#7E1980#B347A1"
/obj/item/clothing/suit/toggle/labcoat/science/Initialize(mapload)
. = ..()
@@ -108,9 +123,15 @@
/obj/item/clothing/suit/toggle/labcoat/roboticist
name = "roboticist labcoat"
desc = "More like an eccentric coat than a labcoat. Helps pass off bloodstains as part of the aesthetic. Comes with red shoulder pads."
- icon_state = "labcoat_robo"
+ icon_state = "labcoat_job"
+ greyscale_config = /datum/greyscale_config/labcoat
+ greyscale_config_worn = /datum/greyscale_config/labcoat/worn
+ greyscale_colors = "#EEEEEE#88242D#88242D#39393F"
/obj/item/clothing/suit/toggle/labcoat/interdyne
name = "interdyne labcoat"
desc = "More like an eccentric coat than a labcoat. Helps pass off bloodstains as part of the aesthetic. Comes with red shoulder pads."
- icon_state = "labcoat_robo"
+ icon_state = "labcoat_job"
+ greyscale_config = /datum/greyscale_config/labcoat
+ greyscale_config_worn = /datum/greyscale_config/labcoat/worn
+ greyscale_colors = "#EEEEEE#88242D#88242D#39393F"
diff --git a/code/modules/clothing/suits/reactive_armour.dm b/code/modules/clothing/suits/reactive_armour.dm
index 6c33e287f033a..9fec4dbe82b92 100644
--- a/code/modules/clothing/suits/reactive_armour.dm
+++ b/code/modules/clothing/suits/reactive_armour.dm
@@ -226,7 +226,7 @@
emp_message = span_warning("The tesla capacitors beep ominously for a moment.")
clothing_traits = list(TRAIT_TESLA_SHOCKIMMUNE)
/// How strong are the zaps we give off?
- var/zap_power = 1e7
+ var/zap_power = 2.5e4
/// How far to the zaps we give off go?
var/zap_range = 20
/// What flags do we pass to the zaps we give off?
@@ -240,7 +240,7 @@
/obj/item/clothing/suit/armor/reactive/tesla/reactive_activation(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK)
owner.visible_message(span_danger("[src] blocks [attack_text], sending out arcs of lightning!"))
- tesla_zap(owner, zap_range, zap_power, zap_flags)
+ tesla_zap(source = owner, zap_range = zap_range, power = zap_power, cutoff = 1e3, zap_flags = zap_flags)
reactivearmor_cooldown = world.time + reactivearmor_cooldown_duration
return TRUE
diff --git a/code/modules/clothing/under/syndicate.dm b/code/modules/clothing/under/syndicate.dm
index 19d5d3b93b2fe..20a5cda059202 100644
--- a/code/modules/clothing/under/syndicate.dm
+++ b/code/modules/clothing/under/syndicate.dm
@@ -8,6 +8,7 @@
alt_covers_chest = TRUE
icon = 'icons/obj/clothing/under/syndicate.dmi'
worn_icon = 'icons/mob/clothing/under/syndicate.dmi'
+ supports_variations_flags = CLOTHING_MONKEY_VARIATION
/datum/armor/clothing_under/syndicate
melee = 10
@@ -32,6 +33,7 @@
armor_type = /datum/armor/clothing_under/syndicate_bloodred
resistance_flags = FIRE_PROOF | ACID_PROOF
can_adjust = FALSE
+ supports_variations_flags = NONE
/datum/armor/clothing_under/syndicate_bloodred
melee = 10
@@ -99,6 +101,7 @@
icon_state = "tactical_suit"
inhand_icon_state = "bl_suit"
can_adjust = FALSE
+ supports_variations_flags = NONE
/obj/item/clothing/under/syndicate/camo
name = "camouflage fatigues"
@@ -106,12 +109,14 @@
icon_state = "camogreen"
inhand_icon_state = "g_suit"
can_adjust = FALSE
+ supports_variations_flags = NONE
/obj/item/clothing/under/syndicate/soviet
name = "Ratnik 5 tracksuit"
desc = "Badly translated labels tell you to clean this in Vodka. Great for squatting in."
icon_state = "trackpants"
can_adjust = FALSE
+ supports_variations_flags = NONE
armor_type = /datum/armor/clothing_under/syndicate_soviet
resistance_flags = NONE
@@ -123,12 +128,14 @@
desc = "With a suit lined with this many pockets, you are ready to operate."
icon_state = "syndicate_combat"
can_adjust = FALSE
+ supports_variations_flags = NONE
/obj/item/clothing/under/syndicate/rus_army
name = "advanced military tracksuit"
desc = "Military grade tracksuits for frontline squatting."
icon_state = "rus_under"
can_adjust = FALSE
+ supports_variations_flags = NONE
armor_type = /datum/armor/clothing_under/syndicate_rus_army
resistance_flags = NONE
@@ -142,6 +149,7 @@
worn_icon = 'icons/mob/clothing/under/medical.dmi'
icon_state = "scrubswine"
can_adjust = FALSE
+ supports_variations_flags = NONE
armor_type = /datum/armor/clothing_under/syndicate_scrubs
/datum/armor/clothing_under/syndicate_scrubs
diff --git a/code/modules/error_handler/error_handler.dm b/code/modules/error_handler/error_handler.dm
index 1b3f6bca16d77..a6841d3975444 100644
--- a/code/modules/error_handler/error_handler.dm
+++ b/code/modules/error_handler/error_handler.dm
@@ -26,6 +26,7 @@ GLOBAL_VAR_INIT(total_runtimes_skipped, 0)
Reboot(reason = 1)
return
+ var/static/regex/stack_workaround = regex("[WORKAROUND_IDENTIFIER](.+?)[WORKAROUND_IDENTIFIER]")
var/static/list/error_last_seen = list()
var/static/list/error_cooldown = list() /* Error_cooldown items will either be positive(cooldown time) or negative(silenced error)
If negative, starts at -1, and goes down by 1 each time that error gets skipped*/
@@ -33,6 +34,12 @@ GLOBAL_VAR_INIT(total_runtimes_skipped, 0)
if(!error_last_seen) // A runtime is occurring too early in start-up initialization
return ..()
+ if(stack_workaround.Find(E.name))
+ var/list/data = json_decode(stack_workaround.group[1])
+ E.file = data[1]
+ E.line = data[2]
+ E.name = stack_workaround.Replace(E.name, "")
+
var/erroruid = "[E.file][E.line]"
var/last_seen = error_last_seen[erroruid]
var/cooldown = error_cooldown[erroruid] || 0
diff --git a/code/modules/events/_event.dm b/code/modules/events/_event.dm
index 46353b80ea54b..fd2ae8bd59674 100644
--- a/code/modules/events/_event.dm
+++ b/code/modules/events/_event.dm
@@ -229,7 +229,12 @@ Runs the event
/datum/round_event/proc/announce_to_ghosts(atom/atom_of_interest)
if(control.alert_observers)
if (atom_of_interest)
- notify_ghosts("[control.name] has an object of interest: [atom_of_interest]!", source=atom_of_interest, action=NOTIFY_ORBIT, header="Something's Interesting!")
+ notify_ghosts(
+ "[control.name] has an object of interest: [atom_of_interest]!",
+ source = atom_of_interest,
+ action = NOTIFY_ORBIT,
+ header = "Something's Interesting!",
+ )
return
//Called when the tick is equal to the announce_when variable.
diff --git a/code/modules/events/aurora_caelus.dm b/code/modules/events/aurora_caelus.dm
index d45578299e7f9..8efe01169ce26 100644
--- a/code/modules/events/aurora_caelus.dm
+++ b/code/modules/events/aurora_caelus.dm
@@ -14,10 +14,8 @@
/datum/round_event/aurora_caelus
announce_when = 1
- start_when = 9
- end_when = 50
- var/list/aurora_colors = list("#A2FF80", "#A2FF8B", "#A2FF96", "#A2FFA5", "#A2FFB6", "#A2FFC7", "#A2FFDE", "#A2FFEE")
- var/aurora_progress = 0 //this cycles from 1 to 8, slowly changing colors from gentle green to gentle blue
+ start_when = 21
+ end_when = 80
/datum/round_event/aurora_caelus/announce()
priority_announce("[station_name()]: A harmless cloud of ions is approaching your station, and will exhaust their energy battering the hull. Nanotrasen has approved a short break for all employees to relax and observe this very rare event. During this time, starlight will be bright but gentle, shifting between quiet green and blue colors. Any staff who would like to view these lights for themselves may proceed to the area nearest to them with viewing ports to open space. We hope you enjoy the lights.",
@@ -27,60 +25,92 @@
var/mob/M = V
if((M.client.prefs.read_preference(/datum/preference/toggle/sound_midi)) && is_station_level(M.z))
M.playsound_local(M, 'sound/ambience/aurora_caelus.ogg', 20, FALSE, pressure_affected = FALSE)
+ fade_space(fade_in = TRUE)
+ fade_kitchen(fade_in = TRUE)
/datum/round_event/aurora_caelus/start()
- for(var/area/affected_area as anything in GLOB.areas)
- if(affected_area.area_flags & AREA_USES_STARLIGHT)
- for(var/turf/open/space/spess in affected_area.get_contained_turfs())
- spess.set_light(spess.light_range * 3, spess.light_power * 0.5)
- if(istype(affected_area, /area/station/service/kitchen))
- for(var/turf/open/kitchen in affected_area.get_contained_turfs())
- kitchen.set_light(1, 0.75)
- if(!prob(1) && !check_holidays(APRIL_FOOLS))
- continue
- var/obj/machinery/oven/roast_ruiner = locate() in affected_area
- if(roast_ruiner)
- roast_ruiner.balloon_alert_to_viewers("oh egads!")
- var/turf/ruined_roast = get_turf(roast_ruiner)
- ruined_roast.atmos_spawn_air("[GAS_PLASMA]=100;[TURF_TEMPERATURE(1000)]")
- message_admins("Aurora Caelus event caused an oven to ignite at [ADMIN_VERBOSEJMP(ruined_roast)].")
- log_game("Aurora Caelus event caused an oven to ignite at [loc_name(ruined_roast)].")
- announce_to_ghosts(roast_ruiner)
- for(var/mob/living/carbon/human/seymour as anything in GLOB.human_list)
- if(seymour.mind && istype(seymour.mind.assigned_role, /datum/job/cook))
- seymour.say("My roast is ruined!!!", forced = "ruined roast")
- seymour.emote("scream")
-
+ if(!prob(1) && !check_holidays(APRIL_FOOLS))
+ return
+ for(var/area/station/service/kitchen/affected_area in GLOB.areas)
+ var/obj/machinery/oven/roast_ruiner = locate() in affected_area
+ if(roast_ruiner)
+ roast_ruiner.balloon_alert_to_viewers("oh egads!")
+ var/turf/ruined_roast = get_turf(roast_ruiner)
+ ruined_roast.atmos_spawn_air("[GAS_PLASMA]=100;[TURF_TEMPERATURE(1000)]")
+ message_admins("Aurora Caelus event caused an oven to ignite at [ADMIN_VERBOSEJMP(ruined_roast)].")
+ log_game("Aurora Caelus event caused an oven to ignite at [loc_name(ruined_roast)].")
+ announce_to_ghosts(roast_ruiner)
+ for(var/mob/living/carbon/human/seymour as anything in GLOB.human_list)
+ if(seymour.mind && istype(seymour.mind.assigned_role, /datum/job/cook))
+ seymour.say("My roast is ruined!!!", forced = "ruined roast")
+ seymour.emote("scream")
/datum/round_event/aurora_caelus/tick()
- if(activeFor % 5 == 0)
- aurora_progress++
- var/aurora_color = aurora_colors[aurora_progress]
- for(var/area/affected_area as anything in GLOB.areas)
- if(affected_area.area_flags & AREA_USES_STARLIGHT)
- for(var/turf/open/space/spess in affected_area.get_contained_turfs())
- spess.set_light(l_color = aurora_color)
- if(istype(affected_area, /area/station/service/kitchen))
- for(var/turf/open/kitchen_floor in affected_area.get_contained_turfs())
- kitchen_floor.set_light(l_color = aurora_color)
+ if(activeFor % 8 != 0)
+ return
+ var/aurora_color = hsl_gradient((activeFor - start_when) / (end_when - start_when), 0, "#A2FF80", 1, "#A2FFEE")
+ set_starlight(aurora_color)
+
+ for(var/area/station/service/kitchen/affected_area in GLOB.areas)
+ for(var/turf/open/kitchen_floor in affected_area.get_contained_turfs())
+ kitchen_floor.set_light(l_color = aurora_color)
/datum/round_event/aurora_caelus/end()
- for(var/area in GLOB.areas)
- var/area/affected_area = area
- if(affected_area.area_flags & AREA_USES_STARLIGHT)
- for(var/turf/open/space/spess in affected_area.get_contained_turfs())
- fade_to_black(spess)
- if(istype(affected_area, /area/station/service/kitchen))
- for(var/turf/open/superturfentent in affected_area.get_contained_turfs())
- fade_to_black(superturfentent)
+ fade_space()
+ fade_kitchen()
priority_announce("The aurora caelus event is now ending. Starlight conditions will slowly return to normal. When this has concluded, please return to your workplace and continue work as normal. Have a pleasant shift, [station_name()], and thank you for watching with us.",
sound = 'sound/misc/notice2.ogg',
sender_override = "Nanotrasen Meteorology Division")
-/datum/round_event/aurora_caelus/proc/fade_to_black(turf/open/space/spess)
+/datum/round_event/aurora_caelus/proc/fade_space(fade_in = FALSE)
+ set waitfor = FALSE
+ // iterate all glass tiles
+ var/start_color = hsl_gradient(1, 0, "#A2FF80", 1, "#A2FFEE")
+ var/start_range = GLOB.starlight_range * 1.75
+ var/start_power = GLOB.starlight_power * 0.6
+ var/end_color = GLOB.base_starlight_color
+ var/end_range = GLOB.starlight_range
+ var/end_power = GLOB.starlight_power
+ if(fade_in)
+ end_color = hsl_gradient(0, 0, "#A2FF80", 1, "#A2FFEE")
+ end_range = start_range
+ end_power = start_power
+ start_color = GLOB.base_starlight_color
+ start_range = GLOB.starlight_range
+ start_power = GLOB.starlight_power
+
+ for(var/i in 1 to 5)
+ var/walked_color = hsl_gradient(i/5, 0, start_color, 1, end_color)
+ var/walked_range = LERP(start_range, end_range, i/5)
+ var/walked_power = LERP(start_power, end_power, i/5)
+ set_starlight(walked_color, walked_range, walked_power)
+ sleep(8 SECONDS)
+ set_starlight(end_color, end_range, end_power)
+
+/datum/round_event/aurora_caelus/proc/fade_kitchen(fade_in = FALSE)
set waitfor = FALSE
- var/new_light = initial(spess.light_range)
- while(spess.light_range > new_light)
- spess.set_light(spess.light_range - 0.2)
- sleep(3 SECONDS)
- spess.set_light(new_light, initial(spess.light_power), initial(spess.light_color))
+ var/start_color = hsl_gradient(1, 0, "#A2FF80", 1, "#A2FFEE")
+ var/start_range = 1
+ var/start_power = 0.75
+ var/end_color = "#000000"
+ var/end_range = 0.5
+ var/end_power = 0
+ if(fade_in)
+ end_color = hsl_gradient(0, 0, "#A2FF80", 1, "#A2FFEE")
+ end_range = start_range
+ end_power = start_power
+ start_color = "#000000"
+ start_range = 0.5
+ start_power = 0
+
+ for(var/i in 1 to 5)
+ var/walked_color = hsl_gradient(i/5, 0, start_color, 1, end_color)
+ var/walked_range = LERP(start_range, end_range, i/5)
+ var/walked_power = LERP(start_power, end_power, i/5)
+ for(var/area/station/service/kitchen/affected_area in GLOB.areas)
+ for(var/turf/open/kitchen_floor in affected_area.get_contained_turfs())
+ kitchen_floor.set_light(walked_range, walked_power, walked_color)
+ sleep(8 SECONDS)
+ for(var/area/station/service/kitchen/affected_area in GLOB.areas)
+ for(var/turf/open/kitchen_floor in affected_area.get_contained_turfs())
+ kitchen_floor.set_light(end_range, end_power, end_color)
diff --git a/code/modules/events/communications_blackout.dm b/code/modules/events/communications_blackout.dm
index 0747998e6744b..62925611993e8 100644
--- a/code/modules/events/communications_blackout.dm
+++ b/code/modules/events/communications_blackout.dm
@@ -23,7 +23,7 @@
to_chat(A, " [span_warning("[alert]")] ")
if(prob(30) || fake) //most of the time, we don't want an announcement, so as to allow AIs to fake blackouts.
- priority_announce(alert)
+ priority_announce(alert, "Anomaly Alert")
/datum/round_event/communications_blackout/start()
diff --git a/code/modules/events/earthquake.dm b/code/modules/events/earthquake.dm
index 498648986e49c..4f20f26b93bf2 100644
--- a/code/modules/events/earthquake.dm
+++ b/code/modules/events/earthquake.dm
@@ -93,7 +93,11 @@
priority_announce("Planetary monitoring systems indicate a devastating seismic event in the near future.", "Seismic Report")
/datum/round_event/earthquake/start()
- notify_ghosts("The earthquake's epicenter has been located: [get_area_name(epicenter)]!", source = epicenter, header = "Rumble Rumble Rumble!")
+ notify_ghosts(
+ "The earthquake's epicenter has been located: [get_area_name(epicenter)]!",
+ source = epicenter,
+ header = "Rumble Rumble Rumble!",
+ )
/datum/round_event/earthquake/tick()
if(ISMULTIPLE(activeFor, 5))
diff --git a/code/modules/events/ghost_role/fugitive_event.dm b/code/modules/events/ghost_role/fugitive_event.dm
index a70cf3e31b324..11eaf3c78b2c1 100644
--- a/code/modules/events/ghost_role/fugitive_event.dm
+++ b/code/modules/events/ghost_role/fugitive_event.dm
@@ -134,9 +134,19 @@
var/mob/our_candidate = candidates[1]
var/mob/spawned_mob = spawner.create_from_ghost(our_candidate)
candidates -= our_candidate
- notify_ghosts("[spawner.prompt_name] has awoken: [spawned_mob]!", source = spawned_mob, action = NOTIFY_ORBIT, header="Come look!")
+ notify_ghosts(
+ "[spawner.prompt_name] has awoken: [spawned_mob]!",
+ source = spawned_mob,
+ action = NOTIFY_ORBIT,
+ header = "Come look!",
+ )
else
- notify_ghosts("[spawner.prompt_name] spawner has been created!", source = spawner, action = NOTIFY_ORBIT, header="Spawn Here!")
+ notify_ghosts(
+ "[spawner.prompt_name] spawner has been created!",
+ source = spawner,
+ action = NOTIFY_ORBIT,
+ header = "Spawn Here!",
+ )
priority_announce("Unidentified ship detected near the station.")
diff --git a/code/modules/events/ghost_role/revenant_event.dm b/code/modules/events/ghost_role/revenant_event.dm
index f739a3e13d46b..4ed05bbc82c8f 100644
--- a/code/modules/events/ghost_role/revenant_event.dm
+++ b/code/modules/events/ghost_role/revenant_event.dm
@@ -55,7 +55,7 @@
return MAP_ERROR
var/mob/living/basic/revenant/revvie = new(pick(spawn_locs))
- selected.mind.transfer_to(revvie)
+ revvie.key = selected.key
message_admins("[ADMIN_LOOKUPFLW(revvie)] has been made into a revenant by an event.")
revvie.log_message("was spawned as a revenant by an event.", LOG_GAME)
spawned_mobs += revvie
diff --git a/code/modules/events/ghost_role/space_dragon.dm b/code/modules/events/ghost_role/space_dragon.dm
index 379b5d7e07b7d..dd75a97a86999 100644
--- a/code/modules/events/ghost_role/space_dragon.dm
+++ b/code/modules/events/ghost_role/space_dragon.dm
@@ -31,7 +31,7 @@
if(isnull(spawn_location))
return MAP_ERROR
- var/mob/living/simple_animal/hostile/space_dragon/dragon = new (spawn_location)
+ var/mob/living/basic/space_dragon/dragon = new (spawn_location)
dragon.key = key
dragon.mind.add_antag_datum(/datum/antagonist/space_dragon)
playsound(dragon, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1)
diff --git a/code/modules/events/immovable_rod/immovable_rod.dm b/code/modules/events/immovable_rod/immovable_rod.dm
index 1280eb1faa75b..a4cc4d4d6835a 100644
--- a/code/modules/events/immovable_rod/immovable_rod.dm
+++ b/code/modules/events/immovable_rod/immovable_rod.dm
@@ -185,7 +185,7 @@
// If we Bump into the tram front or back, push the tram. Otherwise smash the object as usual.
if(isobj(clong))
if(istramwall(clong) && !special_target)
- rod_vs_tram_battle(clong)
+ rod_vs_tram_battle()
return ..()
var/obj/clong_obj = clong
@@ -301,17 +301,17 @@
* while flying parallel.
*/
/obj/effect/immovablerod/proc/rod_vs_tram_battle()
- var/obj/structure/industrial_lift/tram/industrial_lift = locate() in src.loc
+ var/obj/structure/transport/linear/tram/transport_module = locate() in src.loc
- if(isnull(industrial_lift))
+ if(isnull(transport_module))
return
- var/datum/lift_master/tram/lift_master = industrial_lift.lift_master_datum
+ var/datum/transport_controller/linear/tram/tram_controller = transport_module.transport_controller_datum
- if(isnull(lift_master))
+ if(isnull(tram_controller))
return
- var/push_target = lift_master.rod_collision(src)
+ var/push_target = tram_controller.rod_collision(src)
if(!push_target)
return
diff --git a/code/modules/events/portal_storm.dm b/code/modules/events/portal_storm.dm
index 9ba8ca4992e51..8ad0b48985f68 100644
--- a/code/modules/events/portal_storm.dm
+++ b/code/modules/events/portal_storm.dm
@@ -8,9 +8,11 @@
description = "Syndicate troops pour out of portals."
/datum/round_event/portal_storm/syndicate_shocktroop
- boss_types = list(/mob/living/basic/syndicate/melee/space/stormtrooper = 2)
- hostile_types = list(/mob/living/basic/syndicate/melee/space = 8,\
- /mob/living/basic/syndicate/ranged/space = 2)
+ boss_types = list(/mob/living/basic/trooper/syndicate/melee/space/stormtrooper = 2)
+ hostile_types = list(
+ /mob/living/basic/trooper/syndicate/melee/space = 8,
+ /mob/living/basic/trooper/syndicate/ranged/space = 2,
+ )
/datum/round_event_control/portal_storm_narsie
name = "Portal Storm: Constructs"
@@ -23,9 +25,11 @@
max_wizard_trigger_potency = 7
/datum/round_event/portal_storm/portal_storm_narsie
- boss_types = list(/mob/living/simple_animal/hostile/construct/artificer/hostile = 6)
- hostile_types = list(/mob/living/simple_animal/hostile/construct/juggernaut/hostile = 8,\
- /mob/living/simple_animal/hostile/construct/wraith/hostile = 6)
+ boss_types = list(/mob/living/basic/construct/artificer/hostile = 6)
+ hostile_types = list(
+ /mob/living/basic/construct/juggernaut/hostile = 8,
+ /mob/living/basic/construct/wraith/hostile = 6,
+ )
/datum/round_event/portal_storm
start_when = 7
diff --git a/code/modules/events/processor_overload.dm b/code/modules/events/processor_overload.dm
index ebcbb27f2781d..3c97ca91cdcca 100644
--- a/code/modules/events/processor_overload.dm
+++ b/code/modules/events/processor_overload.dm
@@ -26,7 +26,7 @@
// whether it's, say, a tesla zapping tcomms, or some selective
// modification of the tcomms bus
if(prob(80) || fake)
- priority_announce(alert)
+ priority_announce(alert, "Anomaly Alert")
/datum/round_event/processor_overload/start()
diff --git a/code/modules/events/radiation_leak.dm b/code/modules/events/radiation_leak.dm
index 0fbe29927666f..b97cc19d9cc90 100644
--- a/code/modules/events/radiation_leak.dm
+++ b/code/modules/events/radiation_leak.dm
@@ -60,7 +60,7 @@
priority_announce("A radiation leak has been detected in [location_descriptor || "an unknown area"]. \
All crew are to evacuate the affected area. Our [pick("mechanics", "engineers", "scientists", "interns", "sensors", "readings")] \
- report that a machine within is causing it - repair it quickly to stop the leak.")
+ report that a machine within is causing it - repair it quickly to stop the leak.", "[command_name()] Engineering Division")
/datum/round_event/radiation_leak/start()
var/obj/machinery/the_source_of_our_problems = picked_machine_ref?.resolve()
diff --git a/code/modules/events/scrubber_overflow.dm b/code/modules/events/scrubber_overflow.dm
index 6aad17f00faa1..7f24648a9e5a7 100644
--- a/code/modules/events/scrubber_overflow.dm
+++ b/code/modules/events/scrubber_overflow.dm
@@ -67,7 +67,7 @@
deadchat_broadcast(" has just been[random ? " randomly" : ""] triggered[cause ? " by [cause]" : ""]!", "Scrubber Overflow: [initial(forced_reagent_type.name)]", message_type=DEADCHAT_ANNOUNCEMENT)
/datum/round_event/scrubber_overflow/announce(fake)
- priority_announce("The scrubbers network is experiencing a backpressure surge. Some ejection of contents may occur.", "Atmospherics alert")
+ priority_announce("The scrubbers network is experiencing a backpressure surge. Some ejection of contents may occur.", "[command_name()] Engineering Division")
/datum/round_event/scrubber_overflow/setup()
for(var/obj/machinery/atmospherics/components/unary/vent_scrubber/temp_vent as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/atmospherics/components/unary/vent_scrubber))
diff --git a/code/modules/events/shuttle_insurance.dm b/code/modules/events/shuttle_insurance.dm
index d1e39125e3468..4fce9c556b8b5 100644
--- a/code/modules/events/shuttle_insurance.dm
+++ b/code/modules/events/shuttle_insurance.dm
@@ -47,12 +47,12 @@
/datum/round_event/shuttle_insurance/proc/answered()
if(EMERGENCY_AT_LEAST_DOCKED)
- priority_announce("You are definitely too late to purchase insurance, my friends. Our agents don't work on site.",sender_override = ship_name)
+ priority_announce("You are definitely too late to purchase insurance, my friends. Our agents don't work on site.",sender_override = ship_name, color_override = "red")
return
if(insurance_message && insurance_message.answered == 1)
var/datum/bank_account/station_balance = SSeconomy.get_dep_account(ACCOUNT_CAR)
if(!station_balance?.adjust_money(-insurance_evaluation))
- priority_announce("You didn't send us enough money for shuttle insurance. This, in the space layman's terms, is considered scamming. We're keeping your money, scammers!",sender_override = ship_name)
+ priority_announce("You didn't send us enough money for shuttle insurance. This, in the space layman's terms, is considered scamming. We're keeping your money, scammers!", sender_override = ship_name, color_override = "red")
return
- priority_announce("Thank you for purchasing shuttle insurance!",sender_override = ship_name)
+ priority_announce("Thank you for purchasing shuttle insurance!", sender_override = ship_name, color_override = "red")
SSshuttle.shuttle_insurance = TRUE
diff --git a/code/modules/events/shuttle_loan/shuttle_loan_datum.dm b/code/modules/events/shuttle_loan/shuttle_loan_datum.dm
index 550e670f695d0..97cde21a488f9 100644
--- a/code/modules/events/shuttle_loan/shuttle_loan_datum.dm
+++ b/code/modules/events/shuttle_loan/shuttle_loan_datum.dm
@@ -88,12 +88,12 @@
var/datum/supply_pack/pack = SSshuttle.supply_packs[/datum/supply_pack/imports/specialops]
pack.generate(pick_n_take(empty_shuttle_turfs))
- spawn_list.Add(/mob/living/basic/syndicate/ranged/infiltrator)
- spawn_list.Add(/mob/living/basic/syndicate/ranged/infiltrator)
+ spawn_list.Add(/mob/living/basic/trooper/syndicate/ranged/infiltrator)
+ spawn_list.Add(/mob/living/basic/trooper/syndicate/ranged/infiltrator)
if(prob(75))
- spawn_list.Add(/mob/living/basic/syndicate/ranged/infiltrator)
+ spawn_list.Add(/mob/living/basic/trooper/syndicate/ranged/infiltrator)
if(prob(50))
- spawn_list.Add(/mob/living/basic/syndicate/ranged/infiltrator)
+ spawn_list.Add(/mob/living/basic/trooper/syndicate/ranged/infiltrator)
/datum/shuttle_loan_situation/lots_of_bees
sender = "CentCom Janitorial Division"
@@ -180,11 +180,11 @@
var/datum/supply_pack/pack = SSshuttle.supply_packs[/datum/supply_pack/service/party]
pack.generate(pick_n_take(empty_shuttle_turfs))
- spawn_list.Add(/mob/living/basic/syndicate/russian)
- spawn_list.Add(/mob/living/basic/syndicate/russian/ranged) //drops a mateba
+ spawn_list.Add(/mob/living/basic/trooper/russian)
+ spawn_list.Add(/mob/living/basic/trooper/russian/ranged) //drops a mateba
spawn_list.Add(/mob/living/basic/bear/russian)
if(prob(75))
- spawn_list.Add(/mob/living/basic/syndicate/russian)
+ spawn_list.Add(/mob/living/basic/trooper/russian)
if(prob(50))
spawn_list.Add(/mob/living/basic/bear/russian)
diff --git a/code/modules/events/shuttle_loan/shuttle_loan_event.dm b/code/modules/events/shuttle_loan/shuttle_loan_event.dm
index 96db32c044d6f..c7248a3626607 100644
--- a/code/modules/events/shuttle_loan/shuttle_loan_event.dm
+++ b/code/modules/events/shuttle_loan/shuttle_loan_event.dm
@@ -43,7 +43,7 @@
SSshuttle.shuttle_loan = src
/datum/round_event/shuttle_loan/proc/loan_shuttle()
- priority_announce(situation.thanks_msg, "Cargo shuttle commandeered by CentCom.")
+ priority_announce(situation.thanks_msg, "Cargo shuttle commandeered by [command_name()].")
dispatched = TRUE
var/datum/bank_account/dep_account = SSeconomy.get_dep_account(ACCOUNT_CAR)
diff --git a/code/modules/events/tram_malfunction.dm b/code/modules/events/tram_malfunction.dm
index b5130a8c6934a..18ee4afb7a18c 100644
--- a/code/modules/events/tram_malfunction.dm
+++ b/code/modules/events/tram_malfunction.dm
@@ -18,9 +18,9 @@
if (!.)
return FALSE
- for(var/tram_id in GLOB.active_lifts_by_type)
- var/datum/lift_master/tram_ref = GLOB.active_lifts_by_type[tram_id][1]
- if(tram_ref.specific_lift_id == MAIN_STATION_TRAM)
+ for(var/tram_id in SStransport.transports_by_type)
+ var/datum/transport_controller/linear/tram/tram_ref = SStransport.transports_by_type[tram_id][1]
+ if(tram_ref.specific_transport_id == TRAMSTATION_LINE_1)
return .
return FALSE
@@ -28,43 +28,27 @@
/datum/round_event/tram_malfunction
announce_when = 1
end_when = TRAM_MALFUNCTION_TIME_LOWER
- var/specific_lift_id = MAIN_STATION_TRAM
- var/original_lethality
+ /// The ID of the tram we're going to malfunction
+ var/specific_transport_id = TRAMSTATION_LINE_1
/datum/round_event/tram_malfunction/setup()
end_when = rand(TRAM_MALFUNCTION_TIME_LOWER, TRAM_MALFUNCTION_TIME_UPPER)
/datum/round_event/tram_malfunction/announce()
- priority_announce("Our automated control system has lost contact with the tram's on board computer. Please take extra care while we diagnose and resolve the issue. Signals and emergency braking may not be available during this time.", "CentCom Engineering Division")
+ priority_announce("Our automated control system has lost contact with the tram's onboard computer. Please take extra care while engineers diagnose and resolve the issue.", "[command_name()] Engineering Division")
/datum/round_event/tram_malfunction/start()
- for(var/obj/machinery/crossing_signal/signal as anything in GLOB.tram_signals)
- signal.start_malfunction()
-
- for(var/obj/machinery/door/window/tram/door as anything in GLOB.tram_doors)
- door.start_malfunction()
-
- for(var/obj/machinery/destination_sign/sign as anything in GLOB.tram_signs)
- sign.malfunctioning = TRUE
-
- for(var/obj/structure/industrial_lift/tram as anything in GLOB.lifts)
- original_lethality = tram.collision_lethality
- tram.collision_lethality = original_lethality * 1.25
+ for(var/datum/transport_controller/linear/tram/malfunctioning_controller as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
+ if(malfunctioning_controller.specific_transport_id == specific_transport_id)
+ malfunctioning_controller.start_malf_event()
+ return
/datum/round_event/tram_malfunction/end()
- for(var/obj/machinery/crossing_signal/signal as anything in GLOB.tram_signals)
- signal.end_malfunction()
-
- for(var/obj/machinery/door/window/tram/door as anything in GLOB.tram_doors)
- door.end_malfunction()
-
- for(var/obj/machinery/destination_sign/sign as anything in GLOB.tram_signs)
- sign.malfunctioning = FALSE
-
- for(var/obj/structure/industrial_lift/tram as anything in GLOB.lifts)
- tram.collision_lethality = original_lethality
-
- priority_announce("We've successfully reset the software on the tram, normal operations are now resuming. Sorry for any inconvienence this may have caused.", "CentCom Engineering Division")
+ for(var/datum/transport_controller/linear/tram/malfunctioning_controller as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
+ if(malfunctioning_controller.specific_transport_id == specific_transport_id && malfunctioning_controller.controller_status & COMM_ERROR)
+ malfunctioning_controller.end_malf_event()
+ priority_announce("The software on the tram has been reset, normal operations are now resuming. Sorry for any inconvienence this may have caused.", "[command_name()] Engineering Division")
+ return
#undef TRAM_MALFUNCTION_TIME_UPPER
#undef TRAM_MALFUNCTION_TIME_LOWER
diff --git a/code/modules/events/wizard/shuffle.dm b/code/modules/events/wizard/shuffle.dm
index a41e7d7de322e..460fe7b8a2f68 100644
--- a/code/modules/events/wizard/shuffle.dm
+++ b/code/modules/events/wizard/shuffle.dm
@@ -87,7 +87,7 @@
var/list/mobs_to_swap = list()
for(var/mob/living/carbon/human/alive_human in GLOB.alive_mob_list)
- if(alive_human.stat != CONSCIOUS || !alive_human.mind || IS_WIZARD(alive_human))
+ if(alive_human.stat != CONSCIOUS || isnull(alive_human.mind) || IS_WIZARD(alive_human) || HAS_TRAIT(alive_human, TRAIT_NO_MINDSWAP))
continue //the wizard(s) are spared on this one
mobs_to_swap += alive_human
diff --git a/code/modules/experisci/experiment/types/scanning_fish.dm b/code/modules/experisci/experiment/types/scanning_fish.dm
index 52e58c9104ccb..20a5f928f48f6 100644
--- a/code/modules/experisci/experiment/types/scanning_fish.dm
+++ b/code/modules/experisci/experiment/types/scanning_fish.dm
@@ -10,7 +10,7 @@ GLOBAL_LIST_EMPTY(scanned_fish_by_techweb)
name = "Fish Scanning Experiment 1"
description = "An experiment requiring different fish species to be scanned to unlock the 'Beach' setting for the fishing portal generator."
performance_hint = "Scan fish. Examine scanner to review progress. Unlock new fishing portals."
- allowed_experimentors = list(/obj/item/experi_scanner, /obj/machinery/destructive_scanner, /obj/item/fishing_rod/tech)
+ allowed_experimentors = list(/obj/item/experi_scanner, /obj/machinery/destructive_scanner, /obj/item/fishing_rod/tech, /obj/item/fish_analyzer)
traits = EXPERIMENT_TRAIT_TYPECACHE
points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 750)
required_atoms = list(/obj/item/fish = 4)
diff --git a/code/modules/fishing/aquarium/aquarium.dm b/code/modules/fishing/aquarium/aquarium.dm
index 7b50d13c61fe7..f0d5c7083ecd0 100644
--- a/code/modules/fishing/aquarium/aquarium.dm
+++ b/code/modules/fishing/aquarium/aquarium.dm
@@ -5,6 +5,7 @@
/obj/structure/aquarium
name = "aquarium"
+ desc = "A vivivarium in which aquatic fuana and flora are usually kept and displayed."
density = TRUE
anchored = TRUE
@@ -18,6 +19,11 @@
var/min_fluid_temp = MIN_AQUARIUM_TEMP
var/max_fluid_temp = MAX_AQUARIUM_TEMP
+ ///While the feed storage is not empty, this is the interval which the fish are fed.
+ var/feeding_interval = 3 MINUTES
+ ///The last time fishes were fed by the acquarium itsef.
+ var/last_feeding
+
/// Can fish reproduce in this quarium.
var/allow_breeding = FALSE
@@ -46,6 +52,9 @@
RegisterSignal(src, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON, PROC_REF(track_if_fish))
AddElement(/datum/element/relay_attackers)
RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked))
+ create_reagents(6, SEALED_CONTAINER)
+ RegisterSignal(reagents, COMSIG_REAGENTS_NEW_REAGENT, PROC_REF(start_autofeed))
+ AddComponent(/datum/component/plumbing/aquarium)
/obj/structure/aquarium/proc/track_if_fish(atom/source, atom/initialized)
SIGNAL_HANDLER
@@ -61,6 +70,22 @@
. = ..()
LAZYREMOVEASSOC(tracked_fish_by_type, gone.type, gone)
+/obj/structure/aquarium/proc/start_autofeed(datum/source, new_reagent, amount, reagtemp, data, no_react)
+ SIGNAL_HANDLER
+ START_PROCESSING(SSobj, src)
+ UnregisterSignal(reagents, COMSIG_REAGENTS_NEW_REAGENT)
+
+/obj/structure/aquarium/process(seconds_per_tick)
+ if(!reagents.total_volume)
+ RegisterSignal(reagents, COMSIG_REAGENTS_NEW_REAGENT, PROC_REF(start_autofeed))
+ return PROCESS_KILL
+ if(world.time + feeding_interval > last_feeding)
+ return
+ last_feeding = world.time
+ var/list/fishes = get_fishes()
+ for(var/obj/item/fish/fish as anything in fishes)
+ fish.feed(reagents)
+
/// Returns tracked_fish_by_type but flattened and without the items in the blacklist, also shuffled if shuffle is TRUE.
/obj/structure/aquarium/proc/get_fishes(shuffle = FALSE, blacklist)
. = list()
@@ -115,12 +140,20 @@
/obj/structure/aquarium/examine(mob/user)
. = ..()
- . += span_notice("Alt-click to [panel_open ? "close" : "open"] the control panel.")
+ . += span_notice("Alt-click to [panel_open ? "close" : "open"] the control and feed panel.")
+ if(panel_open && reagents.total_volume)
+ . += span_notice("You can use a plunger to empty the feed storage.")
-/obj/structure/aquarium/AltClick(mob/user)
+/obj/structure/aquarium/AltClick(mob/living/user)
+ . = ..()
if(!user.can_perform_action(src))
- return ..()
+ return
panel_open = !panel_open
+ balloon_alert(user, "panel [panel_open ? "open" : "closed"]")
+ if(panel_open)
+ reagents.flags |= TRANSPARENT|REFILLABLE
+ else
+ reagents.flags &= ~(TRANSPARENT|REFILLABLE)
update_appearance()
/obj/structure/aquarium/wrench_act(mob/living/user, obj/item/tool)
@@ -128,6 +161,15 @@
default_unfasten_wrench(user, tool)
return TOOL_ACT_TOOLTYPE_SUCCESS
+/obj/structure/aquarium/plunger_act(obj/item/plunger/P, mob/living/user, reinforced)
+ if(!panel_open)
+ return
+ to_chat(user, span_notice("You start plunging [name]."))
+ if(do_after(user, 3 SECONDS, target = src))
+ to_chat(user, span_notice("You finish plunging the [name]."))
+ reagents.expose(get_turf(src), TOUCH) //splash on the floor
+ reagents.clear_reagents()
+
/obj/structure/aquarium/attackby(obj/item/item, mob/living/user, params)
if(broken)
var/obj/item/stack/sheet/glass/glass = item
@@ -148,7 +190,7 @@
update_appearance()
return TRUE
- if(istype(item, /obj/item/fish_feed))
+ if(istype(item, /obj/item/fish_feed) && !panel_open)
if(!item.reagents.total_volume)
balloon_alert(user, "[item] is empty!")
return TRUE
@@ -220,6 +262,7 @@
.["fluid_type"] = fluid_type
.["temperature"] = fluid_temp
.["allow_breeding"] = allow_breeding
+ .["feeding_interval"] = feeding_interval / (1 MINUTES)
var/list/content_data = list()
for(var/atom/movable/fish in contents)
content_data += list(list("name"=fish.name,"ref"=ref(fish)))
@@ -251,6 +294,9 @@
if("allow_breeding")
allow_breeding = !allow_breeding
. = TRUE
+ if("feeding_interval")
+ feeding_interval = params["feeding_interval"] MINUTES
+ . = TRUE
if("remove")
var/atom/movable/inside = locate(params["ref"]) in contents
if(inside)
@@ -293,7 +339,6 @@
#undef AQUARIUM_MIN_OFFSET
#undef AQUARIUM_MAX_OFFSET
-
/obj/structure/aquarium/prefilled/Initialize(mapload)
. = ..()
@@ -303,3 +348,6 @@
new /obj/item/fish/goldfish(src)
new /obj/item/fish/angelfish(src)
new /obj/item/fish/guppy(src)
+
+ //They'll be alive for about 30 minutes with this amount.
+ reagents.add_reagent(/datum/reagent/consumable/nutriment, 3)
diff --git a/code/modules/fishing/aquarium/aquarium_kit.dm b/code/modules/fishing/aquarium/aquarium_kit.dm
index cba44721e3a40..d7547e92ef684 100644
--- a/code/modules/fishing/aquarium/aquarium_kit.dm
+++ b/code/modules/fishing/aquarium/aquarium_kit.dm
@@ -35,6 +35,7 @@
var/fish_type = get_fish_type()
if(fish_type)
var/obj/item/fish/spawned_fish = new fish_type(null)
+ ADD_TRAIT(spawned_fish, TRAIT_FISH_FROM_CASE, TRAIT_GENERIC)
spawned_fish.forceMove(src) // trigger storage.handle_entered
/obj/item/storage/fish_case/proc/get_fish_type()
@@ -96,9 +97,9 @@
icon_state = "construction_kit"
w_class = WEIGHT_CLASS_TINY
-/obj/item/aquarium_kit/attack_self(mob/user)
+/obj/item/aquarium_kit/Initialize(mapload)
. = ..()
- to_chat(user,span_notice("There's instruction and tools necessary to build aquarium inside. All you need is to start crafting."))
+ AddComponent(/datum/component/slapcrafting, /datum/crafting_recipe/aquarium)
/obj/item/aquarium_prop
name = "generic aquarium prop"
diff --git a/code/modules/fishing/fish/_fish.dm b/code/modules/fishing/fish/_fish.dm
index 1275886141b52..26b7693f4aa86 100644
--- a/code/modules/fishing/fish/_fish.dm
+++ b/code/modules/fishing/fish/_fish.dm
@@ -314,6 +314,8 @@
last_feeding = world.time
else
var/datum/reagent/wrong_reagent = pick(fed_reagents.reagent_list)
+ if(!wrong_reagent)
+ return
fed_reagent_type = wrong_reagent.type
fed_reagents.remove_reagent(fed_reagent_type, 0.1)
SEND_SIGNAL(src, COMSIG_FISH_FED, fed_reagents, fed_reagent_type)
diff --git a/code/modules/fishing/fish_catalog.dm b/code/modules/fishing/fish_catalog.dm
index a0f66c2227d0d..3e1cb6cfcfa6b 100644
--- a/code/modules/fishing/fish_catalog.dm
+++ b/code/modules/fishing/fish_catalog.dm
@@ -1,17 +1,17 @@
///Book detailing where to get the fish and their properties.
-/obj/item/book/fish_catalog
+/obj/item/book/manual/fish_catalog
name = "Fish Encyclopedia"
desc = "Indexes all fish known to mankind (and related species)."
icon_state = "fishbook"
starting_content = "Lot of fish stuff" //book wrappers could use cleaning so this is not necessary
-/obj/item/book/fish_catalog/ui_interact(mob/user, datum/tgui/ui)
+/obj/item/book/manual/fish_catalog/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "FishCatalog", name)
ui.open()
-/obj/item/book/fish_catalog/ui_static_data(mob/user)
+/obj/item/book/manual/fish_catalog/ui_static_data(mob/user)
. = ..()
var/static/fish_info
if(!fish_info)
@@ -43,7 +43,7 @@
.["fish_info"] = fish_info
.["sponsored_by"] = AQUARIUM_COMPANY
-/obj/item/book/proc/bait_description(bait)
+/obj/item/book/manual/fish_catalog/proc/bait_description(bait)
if(ispath(bait))
var/obj/bait_item = bait
return initial(bait_item.name)
@@ -52,6 +52,9 @@
switch(special_identifier["Type"])
if("Foodtype")
return jointext(bitfield_to_list(special_identifier["Value"], FOOD_FLAGS_IC),",")
+ if("Reagent")
+ var/datum/reagent/prototype = special_identifier["Value"]
+ return "[initial(prototype.name)] (at least [special_identifier["Amount"]]u)"
else
stack_trace("Unknown bait identifier in fish favourite/disliked list")
return "SOMETHING VERY WEIRD"
@@ -59,7 +62,7 @@
//Here we handle descriptions of traits fish use as qualifiers
return "something special"
-/obj/item/book/fish_catalog/proc/build_fishing_tips(fish_type)
+/obj/item/book/manual/fish_catalog/proc/build_fishing_tips(fish_type)
var/obj/item/fish/fishy = fish_type
. = list()
//// Where can it be found - iterate fish sources, how should this handle key
@@ -101,7 +104,7 @@
.["difficulty"] = "Hard"
return .
-/obj/item/book/fish_catalog/ui_assets(mob/user)
+/obj/item/book/manual/fish_catalog/ui_assets(mob/user)
return list(
get_asset_datum(/datum/asset/spritesheet/fish)
)
diff --git a/code/modules/fishing/fishing_minigame.dm b/code/modules/fishing/fishing_minigame.dm
index 71617b4de07b3..7744bcc01bee0 100644
--- a/code/modules/fishing/fishing_minigame.dm
+++ b/code/modules/fishing/fishing_minigame.dm
@@ -245,7 +245,7 @@
SIGNAL_HANDLER
fishing_line = null
///The lure may be out of sight if the user has moed around a corner, so the message should be displayed over him instead.
- user.balloon_alert(user.is_holding(used_rod) ? "line snapped" : "rod dropped")
+ user.balloon_alert(user, user.is_holding(used_rod) ? "line snapped" : "rod dropped")
interrupt()
/datum/fishing_challenge/proc/handle_click(mob/source, atom/target, modifiers)
diff --git a/code/modules/fishing/sources/_fish_source.dm b/code/modules/fishing/sources/_fish_source.dm
index 657b2f30968b7..7ef4323895775 100644
--- a/code/modules/fishing/sources/_fish_source.dm
+++ b/code/modules/fishing/sources/_fish_source.dm
@@ -66,7 +66,7 @@ GLOBAL_LIST_INIT(specific_fish_icons, zebra_typecacheof(list(
return
/// Can we fish in this spot at all. Returns DENIAL_REASON or null if we're good to go
-/datum/fish_source/proc/reason_we_cant_fish(obj/item/fishing_rod/rod, mob/fisherman)
+/datum/fish_source/proc/reason_we_cant_fish(obj/item/fishing_rod/rod, mob/fisherman, atom/parent)
return rod.reason_we_cant_fish(src)
/**
diff --git a/code/modules/fishing/sources/source_types.dm b/code/modules/fishing/sources/source_types.dm
index e2e5491dd1d3d..edab6dc0db5cf 100644
--- a/code/modules/fishing/sources/source_types.dm
+++ b/code/modules/fishing/sources/source_types.dm
@@ -216,7 +216,7 @@
fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
-/datum/fish_source/lavaland/reason_we_cant_fish(obj/item/fishing_rod/rod, mob/fisherman)
+/datum/fish_source/lavaland/reason_we_cant_fish(obj/item/fishing_rod/rod, mob/fisherman, atom/parent)
. = ..()
var/turf/approx = get_turf(fisherman) //todo pass the parent
if(!SSmapping.level_trait(approx.z, ZTRAIT_MINING))
@@ -277,7 +277,7 @@
)
fishing_difficulty = FISHING_DEFAULT_DIFFICULTY - 5
-/datum/fish_source/holographic/reason_we_cant_fish(obj/item/fishing_rod/rod, mob/fisherman)
+/datum/fish_source/holographic/reason_we_cant_fish(obj/item/fishing_rod/rod, mob/fisherman, atom/parent)
. = ..()
if(!istype(get_area(fisherman), /area/station/holodeck))
return "You need to be inside the Holodeck to catch holographic fish."
@@ -310,3 +310,63 @@
/obj/item/fish/mastodon = 1,
)
fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 15
+
+#define RANDOM_SEED "Random seed"
+
+/datum/fish_source/hydro_tray
+ catalog_description = "Hydroponics trays"
+ fish_table = list(
+ FISHING_DUD = 25,
+ /obj/item/food/grown/grass = 25,
+ RANDOM_SEED = 16,
+ /obj/item/seeds/grass = 6,
+ /obj/item/seeds/random = 1,
+ /mob/living/basic/frog = 1,
+ /mob/living/basic/axolotl = 1,
+ )
+ fish_counts = list(
+ /obj/item/food/grown/grass = 10,
+ /obj/item/seeds/grass = 4,
+ RANDOM_SEED = 4,
+ /obj/item/seeds/random = 1,
+ /mob/living/basic/frog = 1,
+ /mob/living/basic/axolotl = 1,
+ )
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY - 10
+
+/datum/fish_source/hydro_tray/reason_we_cant_fish(obj/item/fishing_rod/rod, mob/fisherman, atom/parent)
+ if(!istype(parent, /obj/machinery/hydroponics/constructable))
+ return ..()
+
+ var/obj/machinery/hydroponics/constructable/basin = parent
+ if(basin.waterlevel <= 0)
+ return "There's no water in [parent] to fish in."
+ if(basin.myseed)
+ return "There's a plant growing in [parent]."
+
+ return ..()
+
+/datum/fish_source/hydro_tray/spawn_reward(reward_path, mob/fisherman, turf/fishing_spot)
+ if(reward_path != RANDOM_SEED)
+ var/mob/living/created_reward = ..()
+ if(istype(created_reward))
+ created_reward.name = "small [created_reward.name]"
+ created_reward.update_transform(0.75)
+ return created_reward
+
+ var/static/list/seeds_to_draw_from
+ if(isnull(seeds_to_draw_from))
+ seeds_to_draw_from = subtypesof(/obj/item/seeds)
+ // These two are already covered innately
+ seeds_to_draw_from -= /obj/item/seeds/random
+ seeds_to_draw_from -= /obj/item/seeds/grass
+ // -1 yield are unharvestable plants so we don't care
+ // 20 rarirty is where most of the wacky plants are so let's ignore them
+ for(var/obj/item/seeds/seed_path as anything in seeds_to_draw_from)
+ if(initial(seed_path.yield) == -1 || initial(seed_path.rarity) >= PLANT_MODERATELY_RARE)
+ seeds_to_draw_from -= seed_path
+
+ var/picked_path = pick(seeds_to_draw_from)
+ return new picked_path(get_turf(fishing_spot))
+
+#undef RANDOM_SEED
diff --git a/code/modules/flufftext/Dreaming.dm b/code/modules/flufftext/Dreaming.dm
index 9c0ca9d33c566..06472f84be606 100644
--- a/code/modules/flufftext/Dreaming.dm
+++ b/code/modules/flufftext/Dreaming.dm
@@ -19,14 +19,94 @@
/mob/living/carbon/proc/dream()
set waitfor = FALSE
- var/list/dream_fragments = list()
+
+ var/datum/dream/chosen_dream = pick_weight(GLOB.dreams)
+
+ dreaming = TRUE
+ dream_sequence(chosen_dream.GenerateDream(src), chosen_dream)
+
+/**
+ * Displays the passed list of dream fragments to a sleeping carbon.
+ *
+ * Displays the first string of the passed dream fragments, then either ends the dream sequence
+ * or performs a callback on itself depending on if there are any remaining dream fragments to display.
+ *
+ * Arguments:
+ * * dream_fragments - A list of strings, in the order they will be displayed.
+ * * current_dream - The dream datum used for the current dream
+ */
+
+/mob/living/carbon/proc/dream_sequence(list/dream_fragments, datum/dream/current_dream)
+ if(stat != UNCONSCIOUS || HAS_TRAIT(src, TRAIT_CRITICAL_CONDITION))
+ dreaming = FALSE
+ current_dream.OnDreamEnd(src)
+ return
+ var/next_message = dream_fragments[1]
+ dream_fragments.Cut(1,2)
+
+ if(istype(next_message, /datum/callback))
+ var/datum/callback/something_happens = next_message
+ next_message = something_happens.InvokeAsync(src)
+
+ to_chat(src, span_notice("... [next_message] ..."))
+
+ if(LAZYLEN(dream_fragments))
+ var/next_wait = rand(10, 30)
+ if(current_dream.sleep_until_finished)
+ AdjustSleeping(next_wait)
+ addtimer(CALLBACK(src, PROC_REF(dream_sequence), dream_fragments, current_dream), next_wait)
+ else
+ dreaming = FALSE
+ current_dream.OnDreamEnd(src)
+
+//-------------------------
+// DREAM DATUMS
+
+GLOBAL_LIST_INIT(dreams, populate_dream_list())
+
+/proc/populate_dream_list()
+ var/list/output = list()
+ for(var/datum/dream/dream_type as anything in subtypesof(/datum/dream))
+ output[new dream_type] = initial(dream_type.weight)
+ return output
+
+/**
+ * Contains all the behavior needed to play a kind of dream.
+ * All dream types get randomly selected from based on weight when an appropriate mobs dreams.
+ */
+/datum/dream
+ /// The relative chance this dream will be randomly selected
+ var/weight = 0
+
+ /// Causes the mob to sleep long enough for the dream to finish if begun
+ var/sleep_until_finished = FALSE
+
+/**
+ * Called when beginning a new dream for the dreamer.
+ * Gives back a list of dream events. Events can be text or callbacks that return text.
+ */
+/datum/dream/proc/GenerateDream(mob/living/carbon/dreamer)
+ return list()
+
+/**
+ * Called when the dream ends or is interrupted.
+ */
+/datum/dream/proc/OnDreamEnd(mob/living/carbon/dreamer)
+ return
+
+/// The classic random dream of various words that might form a cohesive narrative, but usually wont
+/datum/dream/random
+ weight = 1000
+
+/datum/dream/random/GenerateDream(mob/living/carbon/dreamer)
var/list/custom_dream_nouns = list()
var/fragment = ""
- for(var/obj/item/bedsheet/sheet in loc)
+ for(var/obj/item/bedsheet/sheet in dreamer.loc)
custom_dream_nouns += sheet.dream_messages
- dream_fragments += "you see"
+ . = list()
+ . += "you see"
//Subject
if(custom_dream_nouns.len && prob(90))
@@ -40,7 +120,7 @@
fragment = replacetext(fragment, "%ADJECTIVE% ", "")
if(findtext(fragment, "%A% "))
fragment = "\a [replacetext(fragment, "%A% ", "")]"
- dream_fragments += fragment
+ . += fragment
//Verb
fragment = ""
@@ -51,10 +131,9 @@
else
fragment += "will "
fragment += pick(GLOB.verbs)
- dream_fragments += fragment
+ . += fragment
if(prob(25))
- dream_sequence(dream_fragments)
return
//Object
@@ -66,29 +145,38 @@
fragment = replacetext(fragment, "%ADJECTIVE% ", "")
if(findtext(fragment, "%A% "))
fragment = "\a [replacetext(fragment, "%A% ", "")]"
- dream_fragments += fragment
+ . += fragment
- dreaming = TRUE
- dream_sequence(dream_fragments)
+/// Dream plays a random sound at you, chosen from all sounds in the folder
+/datum/dream/hear_something
+ weight = 500
-/**
- * Displays the passed list of dream fragments to a sleeping carbon.
- *
- * Displays the first string of the passed dream fragments, then either ends the dream sequence
- * or performs a callback on itself depending on if there are any remaining dream fragments to display.
- *
- * Arguments:
- * * dream_fragments - A list of strings, in the order they will be displayed.
- */
+ var/reserved_sound_channel
-/mob/living/carbon/proc/dream_sequence(list/dream_fragments)
- if(stat != UNCONSCIOUS || HAS_TRAIT(src, TRAIT_CRITICAL_CONDITION))
- dreaming = FALSE
- return
- var/next_message = dream_fragments[1]
- dream_fragments.Cut(1,2)
- to_chat(src, span_notice("... [next_message] ..."))
- if(LAZYLEN(dream_fragments))
- addtimer(CALLBACK(src, PROC_REF(dream_sequence), dream_fragments), rand(10,30))
- else
- dreaming = FALSE
+/datum/dream/hear_something/New()
+ . = ..()
+ RegisterSignal(SSsounds, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(ReserveSoundChannel))
+
+/datum/dream/hear_something/GenerateDream(mob/living/carbon/dreamer)
+ . = ..()
+ . += pick("you wind up a toy", "you hear something strange", "you pick out a record to play", "you hit shuffle on your music player")
+ . += CALLBACK(src, PROC_REF(PlayRandomSound))
+ . += "it reminds you of something"
+
+/datum/dream/hear_something/OnDreamEnd(mob/living/carbon/dreamer)
+ . = ..()
+ // In case we play some long ass music track
+ addtimer(CALLBACK(src, PROC_REF(StopSound), dreamer), 5 SECONDS)
+
+/datum/dream/hear_something/proc/ReserveSoundChannel()
+ reserved_sound_channel = SSsounds.reserve_sound_channel(src)
+ UnregisterSignal(SSsounds, COMSIG_SUBSYSTEM_POST_INITIALIZE)
+
+/datum/dream/hear_something/proc/PlayRandomSound(mob/living/carbon/dreamer)
+ var/sound/random_sound = sound(pick(SSsounds.all_sounds), channel=reserved_sound_channel)
+ random_sound.status = SOUND_STREAM
+ SEND_SOUND(dreamer, random_sound)
+ return "you hear something you weren't expecting!"
+
+/datum/dream/hear_something/proc/StopSound(mob/living/carbon/dreamer)
+ SEND_SOUND(dreamer, sound(channel=reserved_sound_channel))
diff --git a/code/modules/food_and_drinks/machinery/microwave.dm b/code/modules/food_and_drinks/machinery/microwave.dm
index d52e2213a5b28..10520e8f6c1c4 100644
--- a/code/modules/food_and_drinks/machinery/microwave.dm
+++ b/code/modules/food_and_drinks/machinery/microwave.dm
@@ -35,6 +35,8 @@
var/wire_disabled = FALSE
/// Wire cut to run mode backwards
var/wire_mode_swap = FALSE
+ /// Fail due to inserted PDA
+ var/pda_failure = FALSE
var/operating = FALSE
/// How dirty is it?
var/dirty = 0
@@ -123,8 +125,8 @@
else if(held_item && istype(held_item, /obj/item/stock_parts/cell))
context[SCREENTIP_CONTEXT_CTRL_LMB] = "Insert cell"
- if(!anchored && held_item?.tool_behaviour == TOOL_WRENCH)
- context[SCREENTIP_CONTEXT_LMB] = "Install/Secure"
+ if(held_item?.tool_behaviour == TOOL_WRENCH)
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Unsecure" : "Install/Secure"]"
return CONTEXTUAL_SCREENTIP_SET
if(broken > NOT_BROKEN)
@@ -216,7 +218,7 @@
. = ..()
// All of these will use a full icon state instead
- if(panel_open || dirty == MAX_MICROWAVE_DIRTINESS || broken || dirty_anim_playing)
+ if(panel_open || dirty >= MAX_MICROWAVE_DIRTINESS || broken || dirty_anim_playing)
return .
var/ingredient_count = 0
@@ -294,7 +296,7 @@
icon_state = "[base_icon_state]mwb"
else if(dirty_anim_playing)
icon_state = "[base_icon_state]mwbloody1"
- else if(dirty == MAX_MICROWAVE_DIRTINESS)
+ else if(dirty >= MAX_MICROWAVE_DIRTINESS)
icon_state = open ? "[base_icon_state]mwbloodyo" : "[base_icon_state]mwbloody"
else if(operating)
icon_state = "[base_icon_state]back_on"
@@ -308,80 +310,83 @@
return ..()
/obj/machinery/microwave/wrench_act(mob/living/user, obj/item/tool)
- . = ..()
- if(dirty >= MAX_MICROWAVE_DIRTINESS)
- return FALSE
if(default_unfasten_wrench(user, tool))
update_appearance()
return TOOL_ACT_TOOLTYPE_SUCCESS
/obj/machinery/microwave/crowbar_act(mob/living/user, obj/item/tool)
- if(operating)
- return
if(!default_deconstruction_crowbar(tool))
return
return TOOL_ACT_TOOLTYPE_SUCCESS
/obj/machinery/microwave/screwdriver_act(mob/living/user, obj/item/tool)
- if(operating)
- return
- if(dirty >= MAX_MICROWAVE_DIRTINESS)
- return
if(default_deconstruction_screwdriver(user, icon_state, icon_state, tool))
update_appearance()
return TOOL_ACT_TOOLTYPE_SUCCESS
-/obj/machinery/microwave/attackby(obj/item/item, mob/living/user, params)
+/obj/machinery/microwave/wirecutter_act(mob/living/user, obj/item/tool)
+ if(broken != REALLY_BROKEN)
+ return
+
+ user.visible_message(
+ span_notice("[user] starts to fix part of [src]."),
+ span_notice("You start to fix part of [src]..."),
+ )
+
+ if(!tool.use_tool(src, user, 2 SECONDS, volume = 50))
+ return TOOL_ACT_SIGNAL_BLOCKING
+
+ user.visible_message(
+ span_notice("[user] fixes part of [src]."),
+ span_notice("You fix part of [src]."),
+ )
+ broken = KINDA_BROKEN // Fix it a bit
+ update_appearance()
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/machinery/microwave/welder_act(mob/living/user, obj/item/tool)
+ if(broken != KINDA_BROKEN)
+ return
+
+ user.visible_message(
+ span_notice("[user] starts to fix part of [src]."),
+ span_notice("You start to fix part of [src]..."),
+ )
+
+ if(!tool.use_tool(src, user, 2 SECONDS, amount = 1, volume = 50))
+ return TOOL_ACT_SIGNAL_BLOCKING
+
+ user.visible_message(
+ span_notice("[user] fixes [src]."),
+ span_notice("You fix [src]."),
+ )
+ broken = NOT_BROKEN
+ update_appearance()
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/machinery/microwave/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
if(operating)
return
+ if(dirty >= MAX_MICROWAVE_DIRTINESS)
+ return
+
+ . = ..()
+ if(. & TOOL_ACT_MELEE_CHAIN_BLOCKING)
+ return
- if(panel_open && is_wire_tool(item))
+ if(panel_open && is_wire_tool(tool))
wires.interact(user)
- return TRUE
+ return TOOL_ACT_SIGNAL_BLOCKING
+
+/obj/machinery/microwave/attackby(obj/item/item, mob/living/user, params)
+ if(operating)
+ return
if(broken > NOT_BROKEN)
- if(broken == REALLY_BROKEN && item.tool_behaviour == TOOL_WIRECUTTER) // If it's broken and they're using a TOOL_WIRECUTTER
- user.visible_message(span_notice("[user] starts to fix part of \the [src]."), span_notice("You start to fix part of \the [src]..."))
- if(item.use_tool(src, user, 20))
- user.visible_message(span_notice("[user] fixes part of \the [src]."), span_notice("You fix part of \the [src]."))
- broken = KINDA_BROKEN // Fix it a bit
- else if(broken == KINDA_BROKEN && item.tool_behaviour == TOOL_WELDER) // If it's broken and they're doing the wrench
- user.visible_message(span_notice("[user] starts to fix part of \the [src]."), span_notice("You start to fix part of \the [src]..."))
- if(item.use_tool(src, user, 20))
- user.visible_message(span_notice("[user] fixes \the [src]."), span_notice("You fix \the [src]."))
- broken = NOT_BROKEN
- update_appearance()
- return FALSE //to use some fuel
- else
+ if(IS_EDIBLE(item))
balloon_alert(user, "it's broken!")
return TRUE
- return
-
- if(istype(item, /obj/item/reagent_containers/spray))
- var/obj/item/reagent_containers/spray/clean_spray = item
- open(autoclose = 2 SECONDS)
- if(clean_spray.reagents.has_reagent(/datum/reagent/space_cleaner, clean_spray.amount_per_transfer_from_this))
- clean_spray.reagents.remove_reagent(/datum/reagent/space_cleaner, clean_spray.amount_per_transfer_from_this,1)
- playsound(loc, 'sound/effects/spray3.ogg', 50, TRUE, -6)
- user.visible_message(span_notice("[user] cleans \the [src]."), span_notice("You clean \the [src]."))
- dirty = 0
- update_appearance()
- else
- to_chat(user, span_warning("You need more space cleaner!"))
- return TRUE
-
- if(istype(item, /obj/item/soap) || istype(item, /obj/item/reagent_containers/cup/rag))
- var/cleanspeed = 50
- if(istype(item, /obj/item/soap))
- var/obj/item/soap/used_soap = item
- cleanspeed = used_soap.cleanspeed
- user.visible_message(span_notice("[user] starts to clean \the [src]."), span_notice("You start to clean \the [src]..."))
- open(autoclose = cleanspeed + 1 SECONDS)
- if(do_after(user, cleanspeed, target = src))
- user.visible_message(span_notice("[user] cleans \the [src]."), span_notice("You clean \the [src]."))
- dirty = 0
- update_appearance()
- return TRUE
+ return ..()
if(istype(item, /obj/item/stock_parts/cell) && cell_powered)
var/swapped = FALSE
@@ -400,12 +405,16 @@
return TRUE
if(!anchored)
- balloon_alert(user, "not secured!")
+ if(IS_EDIBLE(item))
+ balloon_alert(user, "not secured!")
+ return TRUE
return ..()
if(dirty >= MAX_MICROWAVE_DIRTINESS) // The microwave is all dirty so can't be used!
- balloon_alert(user, "it's too dirty!")
- return TRUE
+ if(IS_EDIBLE(item))
+ balloon_alert(user, "it's too dirty!")
+ return TRUE
+ return ..()
if(vampire_charging_capable && istype(item, /obj/item/modular_computer/pda) && ingredients.len > 0)
balloon_alert(user, "max 1 pda!")
@@ -469,6 +478,9 @@
vampire_charging_enabled = !vampire_charging_enabled
balloon_alert(user, "set to [vampire_charging_enabled ? "charge" : "cook"]")
+ playsound(src, 'sound/machines/twobeep_high.ogg', 50, FALSE)
+ if(issilicon(user))
+ visible_message(span_notice("[user] sets \the [src] to [vampire_charging_enabled ? "charge" : "cook"]."), blind_message = span_notice("You hear \the [src] make an informative beep!"))
/obj/machinery/microwave/CtrlClick(mob/user)
. = ..()
@@ -517,6 +529,15 @@
if("examine")
examine(user)
+/obj/machinery/microwave/wash(clean_types)
+ . = ..()
+ if(operating || !(clean_types & CLEAN_SCRUB))
+ return .
+
+ dirty = 0
+ update_appearance()
+ return . || TRUE
+
/obj/machinery/microwave/proc/eject()
var/atom/drop_loc = drop_location()
for(var/atom/movable/movable_ingredient as anything in ingredients)
@@ -571,6 +592,14 @@
for(var/atom/movable/potential_fooditem as anything in ingredients)
if(IS_EDIBLE(potential_fooditem))
non_food_ingedients--
+ if(istype(potential_fooditem, /obj/item/modular_computer/pda) && prob(75))
+ pda_failure = TRUE
+ notify_ghosts(
+ "[cooker] has overheated their PDA!",
+ source = src,
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ header = "Hunger Games: Catching Fire",
+ )
// If we're cooking non-food items we can fail randomly
if(length(non_food_ingedients) && prob(min(dirty * 5, 100)))
@@ -662,15 +691,15 @@
*/
/obj/machinery/microwave/proc/loop_finish(mob/cooker)
operating = FALSE
+ if(pda_failure)
+ spark()
+ pda_failure = FALSE // in case they repair it after this, reset
+ broken = REALLY_BROKEN
+ explosion(src, heavy_impact_range = 1, light_impact_range = 2, flame_range = 1)
var/cursed_chef = cooker && HAS_TRAIT(cooker, TRAIT_CURSED)
var/metal_amount = 0
for(var/obj/item/cooked_item in ingredients)
- if(istype(cooked_item, /obj/item/modular_computer/pda) && prob(75))
- spark()
- broken = REALLY_BROKEN
- explosion(src, heavy_impact_range = 1, light_impact_range = 2, flame_range = 1)
-
var/sigreturn = cooked_item.microwave_act(src, cooker, randomize_pixel_offset = ingredients.len)
if(sigreturn & COMPONENT_MICROWAVE_SUCCESS)
if(isstack(cooked_item))
@@ -734,7 +763,6 @@
* * cooker - The mob that initiated the cook cycle, can be null if no apparent mob triggered it (such as via emp)
*/
/obj/machinery/microwave/proc/vampire(mob/cooker)
- wzhzhzh()
var/obj/item/modular_computer/pda/vampire_pda = LAZYACCESS(ingredients, 1)
if(isnull(vampire_pda))
playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
@@ -742,11 +770,12 @@
return
vampire_cell = vampire_pda.internal_cell
- if(isnull(vampire_pda))
+ if(isnull(vampire_cell))
playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
after_finish_loop()
return
+ wzhzhzh()
var/vampire_charge_amount = vampire_cell.maxcharge - vampire_cell.charge
charge_loop(vampire_charge_amount, cooker = cooker)
diff --git a/code/modules/food_and_drinks/recipes/food_mixtures.dm b/code/modules/food_and_drinks/recipes/food_mixtures.dm
index a31ed2defcd84..b0477566094dc 100644
--- a/code/modules/food_and_drinks/recipes/food_mixtures.dm
+++ b/code/modules/food_and_drinks/recipes/food_mixtures.dm
@@ -45,10 +45,9 @@
if(resulting_food_path)
var/atom/location = holder.my_atom.drop_location()
for(var/i in 1 to created_volume)
+ var/obj/item/food/result = new resulting_food_path(location)
if(ispath(resulting_food_path, /obj/item/food) && !isnull(resulting_reagent_purity))
- new resulting_food_path(location, resulting_reagent_purity)
- else
- new resulting_food_path(location)
+ result.reagents?.set_all_reagents_purity(resulting_reagent_purity)
/datum/chemical_reaction/food/tofu
required_reagents = list(/datum/reagent/consumable/soymilk = 10)
diff --git a/code/modules/food_and_drinks/recipes/tablecraft/recipes_lizard.dm b/code/modules/food_and_drinks/recipes/tablecraft/recipes_lizard.dm
index 714c36223a41e..100e74fd91682 100644
--- a/code/modules/food_and_drinks/recipes/tablecraft/recipes_lizard.dm
+++ b/code/modules/food_and_drinks/recipes/tablecraft/recipes_lizard.dm
@@ -187,6 +187,17 @@
result = /obj/item/food/rootdough
category = CAT_LIZARD
+/datum/crafting_recipe/food/rootdough2
+ name = "Rootdough"
+ reqs = list(
+ /obj/item/food/grown/potato = 2,
+ /datum/reagent/consumable/soymilk = 15,
+ /datum/reagent/consumable/korta_flour = 5,
+ /datum/reagent/water = 10
+ )
+ result = /obj/item/food/rootdough
+ category = CAT_LIZARD
+
/datum/crafting_recipe/food/snail_nizaya
name = "Desert snail nizaya"
reqs = list(
@@ -432,6 +443,25 @@
result = /obj/item/food/steeped_mushrooms
category = CAT_LIZARD
+/datum/crafting_recipe/food/rootbreadpbj
+ name = "Peanut butter and jelly rootwich"
+ reqs = list(
+ /obj/item/food/breadslice/root = 2,
+ /datum/reagent/consumable/peanut_butter = 5,
+ /datum/reagent/consumable/cherryjelly = 5
+ )
+ result = /obj/item/food/rootbread_peanut_butter_jelly
+ category = CAT_LIZARD
+
+/datum/crafting_recipe/food/rootbreadpbb
+ name = "Peanut butter and banana rootwich"
+ reqs = list(
+ /obj/item/food/breadslice/root = 2,
+ /datum/reagent/consumable/peanut_butter = 5,
+ /obj/item/food/grown/banana = 1
+ )
+ result = /obj/item/food/rootbread_peanut_butter_banana
+ category = CAT_LIZARD
// Soups
/datum/crafting_recipe/food/reaction/soup/atrakor_dumplings
diff --git a/code/modules/hallucination/body.dm b/code/modules/hallucination/body.dm
index 5167b63c5eed4..2d017f969679d 100644
--- a/code/modules/hallucination/body.dm
+++ b/code/modules/hallucination/body.dm
@@ -151,11 +151,11 @@
body_floats = TRUE
/datum/hallucination/body/weird/faceless
- body_image_file = 'icons/mob/simple/traders.dmi'
+ body_image_file = 'icons/obj/trader_signs.dmi'
body_image_state = "faceless"
/datum/hallucination/body/weird/bones
- body_image_file = 'icons/mob/simple/traders.dmi'
+ body_image_file = 'icons/obj/trader_signs.dmi'
body_image_state = "mrbones"
/datum/hallucination/body/weird/freezer
diff --git a/code/modules/hallucination/mother.dm b/code/modules/hallucination/mother.dm
new file mode 100644
index 0000000000000..bc2b312cc0b80
--- /dev/null
+++ b/code/modules/hallucination/mother.dm
@@ -0,0 +1,93 @@
+/// Your mother appears to scold you.
+/datum/hallucination/your_mother
+ random_hallucination_weight = 2
+ var/obj/effect/client_image_holder/hallucination/your_mother/mother
+
+/datum/hallucination/your_mother/start()
+ var/list/spawn_locs = list()
+ for(var/turf/open/floor in view(hallucinator, 4))
+ if(floor.is_blocked_turf(exclude_mobs = TRUE))
+ continue
+ spawn_locs += floor
+
+ if(!length(spawn_locs))
+ return FALSE
+ var/turf/spawn_loc = pick(spawn_locs)
+ mother = new(spawn_loc, hallucinator, src)
+ mother.AddComponent(/datum/component/leash, owner = hallucinator, distance = get_dist(hallucinator, mother)) //basically makes mother follow them
+ point_at(hallucinator)
+ talk("[capitalize(hallucinator.real_name)]!!!!") // Your mother won't be fooled by paltry disguises
+ var/list/scold_lines = list(
+ pick(list("CLEAN YOUR ROOM THIS INSTANT!", "IT'S TIME TO WAKE UP FOR SCHOOL!!")),
+ pick(list("YOU INSULT YOUR GRANDPARENTS!", "USELESS!")),
+ pick(list("I BROUGHT YOU INTO THIS WORLD, I CAN TAKE YOU OUT!!!", "YOU'RE GROUNDED!!")),
+ )
+ var/delay = 2 SECONDS
+ for(var/line in scold_lines)
+ addtimer(CALLBACK(src, PROC_REF(talk), line), delay)
+ delay += 2 SECONDS
+ addtimer(CALLBACK(src, PROC_REF(exit)), delay + 4 SECONDS)
+ return TRUE
+
+/datum/hallucination/your_mother/proc/point_at(atom/target)
+ var/turf/tile = get_turf(target)
+ if(!tile)
+ return
+
+ var/obj/visual = image('icons/hud/screen_gen.dmi', mother.loc, "arrow", FLY_LAYER)
+
+ INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(flick_overlay_global), visual, list(hallucinator.client), 2.5 SECONDS)
+ animate(visual, pixel_x = (tile.x - mother.x) * world.icon_size, pixel_y = (tile.y - mother.y) * world.icon_size, time = 1.7, easing = EASE_OUT)
+
+/datum/hallucination/your_mother/proc/talk(text)
+ var/plus_runechat = hallucinator.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat)
+ var/datum/language/understood_language = hallucinator.get_random_understood_language()
+ var/spans = list(mother.speech_span)
+
+ if(!plus_runechat)
+ var/image/speech_overlay = image('icons/mob/effects/talk.dmi', mother, "default0", layer = ABOVE_MOB_LAYER)
+ INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(flick_overlay_global), speech_overlay, list(hallucinator.client), 30)
+ else
+ hallucinator.create_chat_message(mother, understood_language, text, spans)
+
+ var/message = hallucinator.compose_message(mother, understood_language, text, null, spans, visible_name = TRUE)
+ to_chat(hallucinator, message)
+
+/datum/hallucination/your_mother/proc/exit()
+ qdel(src)
+
+/datum/outfit/yourmother
+ name = "Your Mother"
+
+ uniform = /obj/item/clothing/under/color/jumpskirt/red
+ neck = /obj/item/clothing/neck/beads
+ shoes = /obj/item/clothing/shoes/sandal
+
+/datum/outfit/yourmother/post_equip(mob/living/carbon/human/user, visualsOnly = FALSE)
+ . = ..()
+ user.set_hairstyle("Braided", update = TRUE) //get_dynamic_human_appearance uses bald dummies
+
+/obj/effect/client_image_holder/hallucination/your_mother
+ gender = FEMALE
+ image_icon = 'icons/mob/simple/simple_human.dmi'
+ name = "Your mother"
+ desc = "She is not happy."
+ image_state = ""
+
+/obj/effect/client_image_holder/hallucination/your_mother/Initialize(mapload, list/mobs_which_see_us, datum/hallucination/parent)
+ var/mob/living/hallucinator = parent.hallucinator
+ if (ishuman(hallucinator))
+ var/mob/living/carbon/dna_haver = hallucinator
+ image_icon = getFlatIcon(get_dynamic_human_appearance(/datum/outfit/yourmother, dna_haver.dna.species.type))
+ return ..()
+
+ if (istype(hallucinator, /mob/living/basic/pet/dog/corgi/ian))
+ image_icon = getFlatIcon(get_dynamic_human_appearance(/datum/outfit/job/hop))
+ name = "Head of Personnel"
+ return ..()
+
+ image_icon = hallucinator.icon
+ image_state = hallucinator.icon_state
+ image_pixel_x = hallucinator.pixel_x
+ image_pixel_y = hallucinator.pixel_y
+ return ..()
diff --git a/code/modules/hallucination/station_message.dm b/code/modules/hallucination/station_message.dm
index fa51e103cca33..97b37e77cd9e2 100644
--- a/code/modules/hallucination/station_message.dm
+++ b/code/modules/hallucination/station_message.dm
@@ -1,6 +1,3 @@
-#define ALERT_TITLE(text) ("
" + text + "
")
-#define ALERT_BODY(text) ("
" + span_alert(text) + "
")
-
/datum/hallucination/station_message
abstract_hallucination_parent = /datum/hallucination/station_message
random_hallucination_weight = 1
@@ -12,17 +9,21 @@
/datum/hallucination/station_message/blob_alert
/datum/hallucination/station_message/blob_alert/start()
- to_chat(hallucinator, ALERT_TITLE("Biohazard Alert"))
- to_chat(hallucinator, ALERT_BODY("Confirmed outbreak of level 5 biohazard aboard [station_name()]. All personnel must contain the outbreak."))
- SEND_SOUND(hallucinator, sound(SSstation.announcer.event_sounds[ANNOUNCER_OUTBREAK5]))
+ priority_announce("Confirmed outbreak of level 5 biohazard aboard [station_name()]. All personnel must contain the outbreak.", \
+ "Biohazard Alert", ANNOUNCER_OUTBREAK5, players = list(hallucinator))
return ..()
/datum/hallucination/station_message/shuttle_dock
/datum/hallucination/station_message/shuttle_dock/start()
- to_chat(hallucinator, ALERT_TITLE("Priority Announcement"))
- to_chat(hallucinator, ALERT_BODY("[SSshuttle.emergency || "The Emergency Shuttle"] has docked with the station. You have 3 minutes to board the Emergency Shuttle."))
- SEND_SOUND(hallucinator, sound(SSstation.announcer.event_sounds[ANNOUNCER_SHUTTLEDOCK]))
+ priority_announce(
+ text = "[SSshuttle.emergency] has docked with the station. You have [DisplayTimeText(SSshuttle.emergency_dock_time)] to board the emergency shuttle.",
+ title = "Emergency Shuttle Arrival",
+ sound = ANNOUNCER_SHUTTLEDOCK,
+ sender_override = "Emergency Shuttle Uplink Alert",
+ players = list(hallucinator),
+ color_override = "orange",
+ )
return ..()
/datum/hallucination/station_message/malf_ai
@@ -31,9 +32,8 @@
if(!(locate(/mob/living/silicon/ai) in GLOB.silicon_mobs))
return FALSE
- to_chat(hallucinator, ALERT_TITLE("Anomaly Alert"))
- to_chat(hallucinator, ALERT_BODY("Hostile runtimes detected in all station systems, please deactivate your AI to prevent possible damage to its morality core."))
- SEND_SOUND(hallucinator, sound(SSstation.announcer.event_sounds[ANNOUNCER_AIMALF]))
+ priority_announce("Hostile runtimes detected in all station systems, please deactivate your AI to prevent possible damage to its morality core.", \
+ "Anomaly Alert", ANNOUNCER_AIMALF, players = list(hallucinator))
return ..()
/datum/hallucination/station_message/heretic
@@ -55,10 +55,13 @@
var/message_with_name = pick(ascension_bodies)
message_with_name = replacetext(message_with_name, "%FAKENAME%", totally_real_heretic.real_name)
-
- to_chat(hallucinator, ALERT_TITLE(generate_heretic_text()))
- to_chat(hallucinator, ALERT_BODY("[generate_heretic_text()] [message_with_name] [generate_heretic_text()]"))
- SEND_SOUND(hallucinator, sound(SSstation.announcer.event_sounds[ANNOUNCER_SPANOMALIES]))
+ priority_announce(
+ text = "[generate_heretic_text()] [message_with_name] [generate_heretic_text()]",
+ title = "[generate_heretic_text()]",
+ sound = ANNOUNCER_SPANOMALIES,
+ players = list(hallucinator),
+ color_override = "pink",
+ )
return ..()
/datum/hallucination/station_message/cult_summon
@@ -74,20 +77,20 @@
var/area/fake_summon_area_type = pick(GLOB.the_station_areas - hallucinator_area.type)
var/area/fake_summon_area = GLOB.areas_by_type[fake_summon_area_type]
- to_chat(hallucinator, ALERT_TITLE("Central Command Higher Dimensional Affairs"))
- to_chat(hallucinator, ALERT_BODY("Figments from an eldritch god are being summoned by [totally_real_cult_leader.real_name] \
- into [fake_summon_area] from an unknown dimension. Disrupt the ritual at all costs!"))
-
- SEND_SOUND(hallucinator, 'sound/ambience/antag/bloodcult/bloodcult_scribe.ogg')
+ priority_announce(
+ text = "Figments from an eldritch god are being summoned by [totally_real_cult_leader.real_name] into [fake_summon_area] from an unknown dimension. Disrupt the ritual at all costs!",
+ title = "[command_name()] Higher Dimensional Affairs",
+ sound = 'sound/ambience/antag/bloodcult/bloodcult_scribe.ogg',
+ has_important_message = TRUE,
+ players = list(hallucinator),
+ )
return ..()
/datum/hallucination/station_message/meteors
random_hallucination_weight = 2
/datum/hallucination/station_message/meteors/start()
- to_chat(hallucinator, ALERT_TITLE("Meteor Alert"))
- to_chat(hallucinator, ALERT_BODY("Meteors have been detected on collision course with the station."))
- SEND_SOUND(hallucinator, sound(SSstation.announcer.event_sounds[ANNOUNCER_METEORS]))
+ priority_announce("Meteors have been detected on collision course with the station.", "Meteor Alert", ANNOUNCER_METEORS, players = list(hallucinator))
return ..()
/datum/hallucination/station_message/supermatter_delam
@@ -113,6 +116,3 @@
hallucinator.playsound_local(get_turf(hallucinator), 'sound/effects/explosion_distant.ogg', 50, FALSE, pressure_affected = FALSE)
qdel(src)
-
-#undef ALERT_TITLE
-#undef ALERT_BODY
diff --git a/code/modules/holiday/holidays.dm b/code/modules/holiday/holidays.dm
index c2b1165f74c74..035f44ae11d26 100644
--- a/code/modules/holiday/holidays.dm
+++ b/code/modules/holiday/holidays.dm
@@ -27,6 +27,8 @@
var/poster_icon = "holiday_unfinished"
/// Color scheme for this holiday
var/list/holiday_colors
+ /// The default pattern of the holiday, if the requested pattern is null.
+ var/holiday_pattern = PATTERN_DEFAULT
// This proc gets run before the game starts when the holiday is activated. Do festive shit here.
/datum/holiday/proc/celebrate()
@@ -78,7 +80,7 @@
return FALSE
/// Procs to return holiday themed colors for recoloring atoms
-/datum/holiday/proc/get_holiday_colors(atom/thing_to_color, pattern = PATTERN_DEFAULT)
+/datum/holiday/proc/get_holiday_colors(atom/thing_to_color, pattern = holiday_pattern)
if(!holiday_colors)
return
switch(pattern)
@@ -87,7 +89,7 @@
if(PATTERN_VERTICAL_STRIPE)
return holiday_colors[(thing_to_color.x % holiday_colors.len) + 1]
-/proc/request_holiday_colors(atom/thing_to_color, pattern = PATTERN_DEFAULT)
+/proc/request_holiday_colors(atom/thing_to_color, pattern)
switch(pattern)
if(PATTERN_RANDOM)
return "#[random_short_color()]"
@@ -98,7 +100,9 @@
return
for(var/holiday_key in GLOB.holidays)
var/datum/holiday/holiday_real = GLOB.holidays[holiday_key]
- return holiday_real.get_holiday_colors(thing_to_color, pattern)
+ if(!holiday_real.holiday_colors)
+ continue
+ return holiday_real.get_holiday_colors(thing_to_color, pattern || holiday_real.holiday_pattern)
// The actual holidays
@@ -132,6 +136,12 @@
timezones = list(TIMEZONE_NZDT, TIMEZONE_CHADT)
begin_day = 6
begin_month = FEBRUARY
+ holiday_colors = list(
+ COLOR_UNION_JACK_BLUE,
+ COLOR_WHITE,
+ COLOR_UNION_JACK_RED,
+ COLOR_WHITE,
+ )
/datum/holiday/nz/getStationPrefix()
return pick("Aotearoa","Kiwi","Fish 'n' Chips","Kākāpō","Southern Cross")
@@ -222,6 +232,12 @@
begin_day = 17
begin_month = MARCH
holiday_hat = /obj/item/clothing/head/soft/green
+ holiday_colors = list(
+ COLOR_IRISH_GREEN,
+ COLOR_WHITE,
+ COLOR_IRISH_ORANGE,
+ )
+ holiday_pattern = PATTERN_VERTICAL_STRIPE
/datum/holiday/no_this_is_patrick/getStationPrefix()
return pick("Blarney","Green","Leprechaun","Booze")
@@ -264,6 +280,11 @@
begin_day = 20
begin_month = APRIL
holiday_hat = /obj/item/clothing/head/rasta
+ holiday_colors = list(
+ COLOR_ETHIOPIA_GREEN,
+ COLOR_ETHIOPIA_YELLOW,
+ COLOR_ETHIOPIA_RED,
+ )
/datum/holiday/fourtwenty/getStationPrefix()
return pick("Snoop","Blunt","Toke","Dank","Cheech","Chong")
@@ -366,12 +387,12 @@
begin_day = 23
end_day = 29
holiday_colors = list(
- COLOR_PRIDE_PURPLE,
- COLOR_PRIDE_BLUE,
- COLOR_PRIDE_GREEN,
- COLOR_PRIDE_YELLOW,
- COLOR_PRIDE_ORANGE,
- COLOR_PRIDE_RED,
+ COLOR_PRIDE_PURPLE,
+ COLOR_PRIDE_BLUE,
+ COLOR_PRIDE_GREEN,
+ COLOR_PRIDE_YELLOW,
+ COLOR_PRIDE_ORANGE,
+ COLOR_PRIDE_RED,
)
// JULY
@@ -398,6 +419,14 @@
begin_month = JULY
mail_holiday = TRUE
holiday_hat = /obj/item/clothing/head/cowboy/brown
+ holiday_colors = list(
+ COLOR_OLD_GLORY_BLUE,
+ COLOR_OLD_GLORY_RED,
+ COLOR_WHITE,
+ COLOR_OLD_GLORY_RED,
+ COLOR_WHITE,
+ )
+
/datum/holiday/usa/getStationPrefix()
return pick("Independent","American","Burger","Bald Eagle","Star-Spangled", "Fireworks")
@@ -414,9 +443,15 @@
begin_month = JULY
holiday_hat = /obj/item/clothing/head/beret
mail_holiday = TRUE
+ holiday_colors = list(
+ COLOR_FRENCH_BLUE,
+ COLOR_WHITE,
+ COLOR_FRENCH_RED
+ )
+ holiday_pattern = PATTERN_VERTICAL_STRIPE
/datum/holiday/france/getStationPrefix()
- return pick("Francais","Fromage", "Zut", "Merde")
+ return pick("Francais", "Fromage", "Zut", "Merde", "Sacrebleu")
/datum/holiday/france/greet()
return "Do you hear the people sing?"
@@ -463,6 +498,7 @@
name = "Independence Day of Ukraine"
begin_month = AUGUST
begin_day = 24
+ holiday_colors = list(COLOR_TRUE_BLUE, COLOR_TANGERINE_YELLOW)
/datum/holiday/ukraine/getStationPrefix()
return pick("Kyiv", "Ukraine")
@@ -553,6 +589,7 @@
begin_month = OCTOBER
end_day = 2
end_month = NOVEMBER
+ holiday_colors = list(COLOR_MOSTLY_PURE_ORANGE, COLOR_PRISONER_BLACK)
/datum/holiday/halloween/greet()
return "Have a spooky Halloween!"
@@ -575,6 +612,11 @@
begin_day = 6
begin_month = NOVEMBER
end_day = 7
+ holiday_colors = list(
+ COLOR_MEDIUM_DARK_RED,
+ COLOR_GOLD,
+ COLOR_MEDIUM_DARK_RED,
+ )
/datum/holiday/october_revolution/getStationPrefix()
return pick("Communist", "Soviet", "Bolshevik", "Socialist", "Red", "Workers'")
@@ -661,6 +703,10 @@
end_day = 27
holiday_hat = /obj/item/clothing/head/costume/santa
mail_holiday = TRUE
+ holiday_colors = list(
+ COLOR_CHRISTMAS_GREEN,
+ COLOR_CHRISTMAS_RED,
+ )
/datum/holiday/xmas/getStationPrefix()
return pick(
diff --git a/code/modules/holodeck/holo_effect.dm b/code/modules/holodeck/holo_effect.dm
index a8d51c8772829..76b3d320774e4 100644
--- a/code/modules/holodeck/holo_effect.dm
+++ b/code/modules/holodeck/holo_effect.dm
@@ -102,7 +102,7 @@
mobtype = /mob/living/basic/bee/toxin
/obj/effect/holodeck_effect/mobspawner/monkey
- mobtype = /mob/living/simple_animal/holodeck_monkey
+ mobtype = /mob/living/carbon/human/species/monkey/holodeck
/obj/effect/holodeck_effect/mobspawner/penguin
mobtype = /mob/living/basic/pet/penguin/emperor/neuter
diff --git a/code/modules/holodeck/mobs.dm b/code/modules/holodeck/mobs.dm
deleted file mode 100644
index 7a7e7c22d9e10..0000000000000
--- a/code/modules/holodeck/mobs.dm
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- Mobs
-*/
-
-/mob/living/simple_animal/holodeck_monkey
- name = "monkey"
- desc = "A holographic creature fond of bananas."
- icon = 'icons/mob/human/human.dmi'
- icon_state = "monkey"
- icon_living = "monkey"
- icon_dead = "monkey_dead"
- speak_emote = list("chimpers")
- emote_hear = list("chimpers.")
- emote_see = list("scratches.", "looks around.")
- speak_chance = 1
- turns_per_move = 2
- butcher_results = list()
- response_help_continuous = "pets"
- response_help_simple = "pet"
- response_disarm_continuous = "pushes aside"
- response_disarm_simple = "push aside"
- response_harm_continuous = "kicks"
- response_harm_simple = "kick"
diff --git a/code/modules/holodeck/turfs.dm b/code/modules/holodeck/turfs.dm
index 28482873aeb98..1e89052af59b4 100644
--- a/code/modules/holodeck/turfs.dm
+++ b/code/modules/holodeck/turfs.dm
@@ -8,7 +8,7 @@
/turf/open/floor/holofloor/attackby(obj/item/I, mob/living/user)
return // HOLOFLOOR DOES NOT GIVE A FUCK
-/turf/open/floor/holofloor/tool_act(mob/living/user, obj/item/I, tool_type)
+/turf/open/floor/holofloor/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
return
/turf/open/floor/holofloor/burn_tile()
diff --git a/code/modules/hydroponics/beekeeping/bee_smoker.dm b/code/modules/hydroponics/beekeeping/bee_smoker.dm
new file mode 100644
index 0000000000000..fc296339a9f74
--- /dev/null
+++ b/code/modules/hydroponics/beekeeping/bee_smoker.dm
@@ -0,0 +1,117 @@
+/// multiplier to decide how much fuel we add to a smoker
+#define WEED_WINE_MULTIPLIER 0.2
+
+/obj/item/bee_smoker
+ name = "bee smoker"
+ desc = "A device which can be used to hypnotize bees!"
+ icon = 'icons/obj/service/hydroponics/equipment.dmi'
+ icon_state = "bee_smoker"
+ inhand_icon_state = "bee_smoker"
+ lefthand_file = 'icons/mob/inhands/equipment/hydroponics_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/equipment/hydroponics_righthand.dmi'
+ item_flags = NOBLUDGEON
+ /// current level of fuel we have
+ var/current_herb_fuel = 50
+ /// maximum amount of fuel we can hold
+ var/max_herb_fuel = 50
+ /// are we currently activated?
+ var/activated = FALSE
+ /// sound to play when releasing smoke
+ var/datum/looping_sound/beesmoke/beesmoke_loop
+ ///how much fuel it costs to use this item
+ var/single_use_cost = 5
+
+/obj/item/bee_smoker/Initialize(mapload)
+ . = ..()
+ beesmoke_loop = new(src)
+
+/obj/item/bee_smoker/attack_self(mob/user)
+ . = ..()
+ if(.)
+ return TRUE
+ if(!activated && current_herb_fuel <= 0)
+ user.balloon_alert(user, "no fuel!")
+ return TRUE
+ alter_state()
+ user.balloon_alert(user, "[activated ? "activated" : "deactivated"]")
+ return TRUE
+
+/obj/item/bee_smoker/afterattack(atom/attacked_atom, mob/living/user, proximity)
+ . = ..()
+
+ if(!proximity)
+ return
+
+ . |= AFTERATTACK_PROCESSED_ITEM
+
+ if(!activated)
+ user.balloon_alert(user, "not activated!")
+ return
+
+ if(current_herb_fuel < single_use_cost)
+ user.balloon_alert(user, "not enough fuel!")
+ return
+
+ current_herb_fuel -= single_use_cost
+ playsound(src, 'sound/effects/spray2.ogg', 100, TRUE)
+ var/turf/target_turf = get_turf(attacked_atom)
+ new /obj/effect/temp_visual/mook_dust(target_turf)
+
+ for(var/mob/living/basic/bee/friend in target_turf)
+ if(friend.flags_1 & HOLOGRAM_1)
+ continue
+ friend.befriend(user)
+
+ if(!istype(attacked_atom, /obj/structure/beebox))
+ return
+
+ var/obj/structure/beebox/hive = attacked_atom
+ for(var/mob/living/bee as anything in hive.bees)
+ if(bee.flags_1 & HOLOGRAM_1)
+ continue
+ bee.befriend(user)
+
+/obj/item/bee_smoker/attackby(obj/item/herb, mob/living/carbon/human/user, list/modifiers)
+ . = ..()
+ if(.)
+ return
+ if(!istype(herb, /obj/item/food/grown/cannabis))
+ return
+ var/obj/item/food/grown/cannabis/weed = herb
+ if(isnull(weed.wine_power))
+ return TRUE
+ if(current_herb_fuel == max_herb_fuel)
+ user.balloon_alert(user, "already at maximum fuel!")
+ return TRUE
+ var/fuel_worth = weed.wine_power * WEED_WINE_MULTIPLIER
+ current_herb_fuel = (current_herb_fuel + fuel_worth > max_herb_fuel) ? max_herb_fuel : current_herb_fuel + fuel_worth
+ user.balloon_alert(user, "fuel added")
+ qdel(weed)
+ return TRUE
+
+/obj/item/bee_smoker/process(seconds_per_tick)
+ current_herb_fuel--
+ if(current_herb_fuel <= 0)
+ alter_state()
+
+/obj/item/bee_smoker/proc/alter_state()
+ activated = !activated
+ playsound(src, 'sound/items/welderdeactivate.ogg', 50, TRUE)
+
+ if(!activated)
+ beesmoke_loop.stop()
+ QDEL_NULL(particles)
+ STOP_PROCESSING(SSobj, src)
+ return
+
+ beesmoke_loop.start()
+ START_PROCESSING(SSobj, src)
+ particles = new /particles/smoke/bee_smoke
+
+/particles/smoke/bee_smoke
+ lifespan = 0.4 SECONDS
+ position = list(-12, 7, 0)
+ velocity = list(0, 0.15, 0)
+ fade = 2
+
+#undef WEED_WINE_MULTIPLIER
diff --git a/code/modules/hydroponics/grown.dm b/code/modules/hydroponics/grown.dm
index 098c6e81ce6a9..98569f7e24811 100644
--- a/code/modules/hydroponics/grown.dm
+++ b/code/modules/hydroponics/grown.dm
@@ -114,7 +114,7 @@
/obj/item/food/grown/proc/ferment()
var/reagent_purity = seed.get_reagent_purity()
var/purity_above_base = clamp((reagent_purity - 0.5) * 2, 0, 1)
- var/quality_min = 0
+ var/quality_min = DRINK_NICE
var/quality_max = DRINK_FANTASTIC
var/quality = round(LERP(quality_min, quality_max, purity_above_base))
for(var/datum/reagent/reagent in reagents.reagent_list)
diff --git a/code/modules/hydroponics/grown/banana.dm b/code/modules/hydroponics/grown/banana.dm
index 64979b048cbbf..3b7ddbbd83c4a 100644
--- a/code/modules/hydroponics/grown/banana.dm
+++ b/code/modules/hydroponics/grown/banana.dm
@@ -191,3 +191,18 @@
var/obj/effect/decal/cleanable/food/plant_smudge/banana_smudge = new(loc)
banana_smudge.color = "#ffe02f"
qdel(src)
+
+/obj/item/food/grown/banana/bunch/monkeybomb
+ desc = "Am exquisite bunch of bananas. Their otherwordly plumpness seems to be hiding something."
+
+/obj/item/food/grown/banana/bunch/monkeybomb/examine(mob/user)
+ . = ..()
+ if(!is_simian(user))
+ . += span_notice("There's a banana label on one of the 'nanas you can't quite make out the details of.")
+ return
+ . += span_notice("The banana label on this bunch indicates that monkeys can use this as a sonic grenade with a 3 second timer!")
+
+/obj/item/food/grown/banana/bunch/monkeybomb/attack_self(mob/user, modifiers)
+ if(!is_simian(user))
+ return to_chat(user, span_notice("You don't really know what to do with this."))
+ else start_ripening()
diff --git a/code/modules/hydroponics/grown/beans.dm b/code/modules/hydroponics/grown/beans.dm
index 8643c616b4757..a4c03cf6b53b1 100644
--- a/code/modules/hydroponics/grown/beans.dm
+++ b/code/modules/hydroponics/grown/beans.dm
@@ -39,7 +39,7 @@
potency = 10
mutatelist = null
reagents_add = list(/datum/reagent/toxin/carpotoxin = 0.1, /datum/reagent/consumable/nutriment/vitamin = 0.04, /datum/reagent/consumable/nutriment = 0.05)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
/obj/item/food/grown/koibeans
seed = /obj/item/seeds/soya/koi
@@ -96,7 +96,7 @@
mutatelist = null
reagents_add = list(/datum/reagent/consumable/nutriment = 0.05, /datum/reagent/ants = 0.1) //IRL jumping beans contain insect larve, hence the ants
graft_gene = /datum/plant_gene/trait/stable_stats
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
/obj/item/food/grown/jumpingbeans
seed = /obj/item/seeds/greenbean/jump
@@ -105,4 +105,3 @@
icon_state = "jumpingbean"
foodtypes = FRUIT | BUGS
tastes = list("bugs" = 1)
-
diff --git a/code/modules/hydroponics/grown/berries.dm b/code/modules/hydroponics/grown/berries.dm
index 45b0db82ea327..233765609200f 100644
--- a/code/modules/hydroponics/grown/berries.dm
+++ b/code/modules/hydroponics/grown/berries.dm
@@ -92,7 +92,7 @@
mutatelist = null
genes = list(/datum/plant_gene/trait/glow/white, /datum/plant_gene/trait/repeated_harvest)
reagents_add = list(/datum/reagent/uranium = 0.25, /datum/reagent/iodine = 0.2, /datum/reagent/consumable/nutriment/vitamin = 0.04, /datum/reagent/consumable/nutriment = 0.1)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
graft_gene = /datum/plant_gene/trait/glow/white
/obj/item/food/grown/berries/glow
@@ -188,3 +188,34 @@
juice_typepath = /datum/reagent/consumable/toechtauese_juice
tastes = list("fiery itchy pain" = 1)
distill_reagent = /datum/reagent/toxin/itching_powder
+
+/obj/item/seeds/lanternfruit
+ name = "pack of lanternfruit seeds"
+ desc = "These seeds grow into lanternfruit pods."
+ icon_state = "seed-lanternfruit"
+ species = "lanternfruit"
+ plantname = "Lanternfruit Pod"
+ product = /obj/item/food/grown/lanternfruit
+ lifespan = 35
+ endurance = 35
+ maturation = 5
+ production = 5
+ growthstages = 3
+ instability = 15
+ growing_icon = 'icons/obj/service/hydroponics/growing_fruits.dmi'
+ icon_grow = "lanternfruit-grow"
+ icon_dead = "lanternfruit-dead"
+ icon_harvest = "lanternfruit-harvest"
+ genes = list(/datum/plant_gene/trait/glow/yellow)
+ mutatelist = null
+ reagents_add = list(/datum/reagent/sulfur = 0.07, /datum/reagent/consumable/sugar = 0.07, /datum/reagent/consumable/liquidelectricity = 0.07)
+ graft_gene = /datum/plant_gene/trait/glow/yellow
+
+/obj/item/food/grown/lanternfruit
+ seed = /obj/item/seeds/lanternfruit
+ name = "lanternfruits"
+ desc = "A sofly glowing fruit with a handle-shaped stem, an Ethereal favorite!"
+ icon_state = "lanternfruit"
+ foodtypes = FRUIT
+ tastes = list("tv static" = 1, "sour pear" = 1, "grapefruit" = 1)
+ distill_reagent = /datum/reagent/consumable/ethanol/wine_voltaic
diff --git a/code/modules/hydroponics/grown/cereals.dm b/code/modules/hydroponics/grown/cereals.dm
index 92ef29cc19559..948a9d404c69e 100644
--- a/code/modules/hydroponics/grown/cereals.dm
+++ b/code/modules/hydroponics/grown/cereals.dm
@@ -97,8 +97,8 @@
/obj/item/food/grown/meatwheat/attack_self(mob/living/user)
user.visible_message(span_notice("[user] crushes [src] into meat."), span_notice("You crush [src] into something that resembles meat."))
playsound(user, 'sound/effects/blobattack.ogg', 50, TRUE)
- var/reagent_purity = seed.get_reagent_purity()
- var/obj/item/food/meat/slab/meatwheat/M = new(null, reagent_purity)
+ var/obj/item/food/meat/slab/meatwheat/meaties = new(null)
+ meaties.reagents.set_all_reagents_purity(seed.get_reagent_purity())
qdel(src)
- user.put_in_hands(M)
- return 1
+ user.put_in_hands(meaties)
+ return TRUE
diff --git a/code/modules/hydroponics/grown/chili.dm b/code/modules/hydroponics/grown/chili.dm
index ee5c87a40c359..9f6d3bbd08ce0 100644
--- a/code/modules/hydroponics/grown/chili.dm
+++ b/code/modules/hydroponics/grown/chili.dm
@@ -39,7 +39,7 @@
lifespan = 25
maturation = 4
production = 4
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
genes = list(/datum/plant_gene/trait/chem_cooling)
mutatelist = null
reagents_add = list(/datum/reagent/consumable/frostoil = 0.25, /datum/reagent/consumable/nutriment/vitamin = 0.02, /datum/reagent/consumable/nutriment = 0.02)
@@ -66,7 +66,7 @@
maturation = 10
production = 10
yield = 3
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
genes = list(/datum/plant_gene/trait/chem_heating, /datum/plant_gene/trait/backfire/chili_heat)
mutatelist = null
reagents_add = list(/datum/reagent/consumable/condensedcapsaicin = 0.3, /datum/reagent/consumable/capsaicin = 0.55, /datum/reagent/consumable/nutriment = 0.04)
@@ -93,7 +93,7 @@
maturation = 10
production = 10
yield = 3
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
genes = list(/datum/plant_gene/trait/repeated_harvest)
mutatelist = null
reagents_add = list(/datum/reagent/consumable/nutriment/vitamin = 0.08, /datum/reagent/consumable/nutriment = 0.04)
diff --git a/code/modules/hydroponics/grown/corn.dm b/code/modules/hydroponics/grown/corn.dm
index a606cf62017cc..1b3a7979cf058 100644
--- a/code/modules/hydroponics/grown/corn.dm
+++ b/code/modules/hydroponics/grown/corn.dm
@@ -14,7 +14,7 @@
icon_grow = "corn-grow" // Uses one growth icons set for all the subtypes
icon_dead = "corn-dead" // Same for the dead icon
mutatelist = list(/obj/item/seeds/corn/snapcorn)
- reagents_add = list(/datum/reagent/consumable/nutriment/fat/oil = 0.2, /datum/reagent/consumable/nutriment/vitamin = 0.04, /datum/reagent/consumable/nutriment = 0.1)
+ reagents_add = list(/datum/reagent/consumable/nutriment/fat/oil/corn = 0.2, /datum/reagent/consumable/nutriment/vitamin = 0.04, /datum/reagent/consumable/nutriment = 0.1)
/obj/item/food/grown/corn
seed = /obj/item/seeds/corn
@@ -24,7 +24,7 @@
trash_type = /obj/item/grown/corncob
bite_consumption_mod = 2
foodtypes = VEGETABLES
- grind_results = list(/datum/reagent/consumable/cornmeal = 0)
+ grind_results = list(/datum/reagent/consumable/cornmeal = 0, /datum/reagent/consumable/nutriment/fat/oil/corn = 0)
juice_typepath = /datum/reagent/consumable/corn_starch
tastes = list("corn" = 1)
distill_reagent = /datum/reagent/consumable/ethanol/whiskey
diff --git a/code/modules/hydroponics/grown/flowers.dm b/code/modules/hydroponics/grown/flowers.dm
index 333c7f57c4584..84b1414335caf 100644
--- a/code/modules/hydroponics/grown/flowers.dm
+++ b/code/modules/hydroponics/grown/flowers.dm
@@ -242,7 +242,7 @@
genes = list(/datum/plant_gene/trait/backfire/novaflower_heat, /datum/plant_gene/trait/attack/novaflower_attack, /datum/plant_gene/trait/preserved)
mutatelist = null
reagents_add = list(/datum/reagent/consumable/condensedcapsaicin = 0.25, /datum/reagent/consumable/capsaicin = 0.3, /datum/reagent/consumable/nutriment = 0, /datum/reagent/toxin/acid = 0.05)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
/obj/item/grown/novaflower
seed = /obj/item/seeds/sunflower/novaflower
diff --git a/code/modules/hydroponics/grown/korta_nut.dm b/code/modules/hydroponics/grown/korta_nut.dm
index bf6ab82d47410..cfa6c1e5b51f3 100644
--- a/code/modules/hydroponics/grown/korta_nut.dm
+++ b/code/modules/hydroponics/grown/korta_nut.dm
@@ -39,7 +39,7 @@
production = 10
mutatelist = null
reagents_add = list(/datum/reagent/consumable/korta_nectar = 0.1, /datum/reagent/consumable/nutriment/vitamin = 0.04, /datum/reagent/consumable/nutriment = 0.1)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
/obj/item/food/grown/korta_nut/sweet
seed = /obj/item/seeds/korta_nut/sweet
diff --git a/code/modules/hydroponics/grown/melon.dm b/code/modules/hydroponics/grown/melon.dm
index f69f8a68317cb..0e59231141fc4 100644
--- a/code/modules/hydroponics/grown/melon.dm
+++ b/code/modules/hydroponics/grown/melon.dm
@@ -50,7 +50,7 @@
genes = list(/datum/plant_gene/trait/glow/yellow, /datum/plant_gene/trait/anti_magic)
mutatelist = null
reagents_add = list(/datum/reagent/water/holywater = 0.2, /datum/reagent/consumable/nutriment/vitamin = 0.04, /datum/reagent/consumable/nutriment = 0.1)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
graft_gene = /datum/plant_gene/trait/glow/yellow
/obj/item/food/grown/holymelon
diff --git a/code/modules/hydroponics/grown/mushrooms.dm b/code/modules/hydroponics/grown/mushrooms.dm
index 074f9d5f8d2fb..bfc50f0c483b3 100644
--- a/code/modules/hydroponics/grown/mushrooms.dm
+++ b/code/modules/hydroponics/grown/mushrooms.dm
@@ -238,7 +238,7 @@
potency = 30 //-> brightness
instability = 20
growthstages = 4
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
genes = list(/datum/plant_gene/trait/glow, /datum/plant_gene/trait/plant_type/fungal_metabolism)
growing_icon = 'icons/obj/service/hydroponics/growing_mushrooms.dmi'
mutatelist = list(/obj/item/seeds/glowshroom/glowcap, /obj/item/seeds/glowshroom/shadowshroom)
diff --git a/code/modules/hydroponics/grown/pumpkin.dm b/code/modules/hydroponics/grown/pumpkin.dm
index 561bd5119aa73..11130c153344a 100644
--- a/code/modules/hydroponics/grown/pumpkin.dm
+++ b/code/modules/hydroponics/grown/pumpkin.dm
@@ -48,7 +48,7 @@
product = /obj/item/food/grown/pumpkin/blumpkin
mutatelist = null
reagents_add = list(/datum/reagent/ammonia = 0.2, /datum/reagent/chlorine = 0.1, /datum/reagent/consumable/nutriment = 0.2)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
/obj/item/food/grown/pumpkin/blumpkin
seed = /obj/item/seeds/pumpkin/blumpkin
diff --git a/code/modules/hydroponics/grown/tea_coffee.dm b/code/modules/hydroponics/grown/tea_coffee.dm
index 72ef53545d04c..366dd8b45237e 100644
--- a/code/modules/hydroponics/grown/tea_coffee.dm
+++ b/code/modules/hydroponics/grown/tea_coffee.dm
@@ -34,7 +34,7 @@
product = /obj/item/food/grown/tea/astra
mutatelist = null
reagents_add = list(/datum/reagent/medicine/synaptizine = 0.1, /datum/reagent/consumable/nutriment/vitamin = 0.04, /datum/reagent/toxin/teapowder = 0.1)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
/obj/item/food/grown/tea/astra
seed = /obj/item/seeds/tea/astra
@@ -83,7 +83,7 @@
product = /obj/item/food/grown/coffee/robusta
mutatelist = null
reagents_add = list(/datum/reagent/medicine/ephedrine = 0.1, /datum/reagent/consumable/nutriment/vitamin = 0.04, /datum/reagent/toxin/coffeepowder = 0.1)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
/obj/item/food/grown/coffee/robusta
seed = /obj/item/seeds/coffee/robusta
diff --git a/code/modules/hydroponics/grown/tobacco.dm b/code/modules/hydroponics/grown/tobacco.dm
index c47bdfc32f664..87f8253a4d96b 100644
--- a/code/modules/hydroponics/grown/tobacco.dm
+++ b/code/modules/hydroponics/grown/tobacco.dm
@@ -32,7 +32,7 @@
product = /obj/item/food/grown/tobacco/space
mutatelist = null
reagents_add = list(/datum/reagent/medicine/salbutamol = 0.05, /datum/reagent/drug/nicotine = 0.08, /datum/reagent/consumable/nutriment = 0.03)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
/obj/item/food/grown/tobacco/space
seed = /obj/item/seeds/tobacco/space
diff --git a/code/modules/hydroponics/grown/tomato.dm b/code/modules/hydroponics/grown/tomato.dm
index c2fa44c76625f..e676d822bbf1a 100644
--- a/code/modules/hydroponics/grown/tomato.dm
+++ b/code/modules/hydroponics/grown/tomato.dm
@@ -37,7 +37,7 @@
product = /obj/item/food/grown/tomato/blood
mutatelist = null
reagents_add = list(/datum/reagent/blood = 0.2, /datum/reagent/consumable/nutriment/vitamin = 0.04, /datum/reagent/consumable/nutriment = 0.1)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
/obj/item/food/grown/tomato/blood
seed = /obj/item/seeds/tomato/blood
@@ -63,7 +63,7 @@
mutatelist = list(/obj/item/seeds/tomato/blue/bluespace)
genes = list(/datum/plant_gene/trait/slip, /datum/plant_gene/trait/repeated_harvest)
reagents_add = list(/datum/reagent/lube = 0.2, /datum/reagent/consumable/nutriment/vitamin = 0.04, /datum/reagent/consumable/nutriment = 0.1)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
graft_gene = /datum/plant_gene/trait/slip
/obj/item/food/grown/tomato/blue
diff --git a/code/modules/hydroponics/grown/towercap.dm b/code/modules/hydroponics/grown/towercap.dm
index 3012a1f14793b..046e835c50ca5 100644
--- a/code/modules/hydroponics/grown/towercap.dm
+++ b/code/modules/hydroponics/grown/towercap.dm
@@ -28,7 +28,7 @@
product = /obj/item/grown/log/steel
mutatelist = null
reagents_add = list(/datum/reagent/cellulose = 0.05, /datum/reagent/iron = 0.05)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
/obj/item/grown/log
seed = /obj/item/seeds/tower
diff --git a/code/modules/hydroponics/grown/weeds/nettle.dm b/code/modules/hydroponics/grown/weeds/nettle.dm
index 316cc7723acb8..98868b8d6e748 100644
--- a/code/modules/hydroponics/grown/weeds/nettle.dm
+++ b/code/modules/hydroponics/grown/weeds/nettle.dm
@@ -29,7 +29,7 @@
genes = list(/datum/plant_gene/trait/repeated_harvest, /datum/plant_gene/trait/plant_type/weed_hardy, /datum/plant_gene/trait/stinging, /datum/plant_gene/trait/attack/nettle_attack/death, /datum/plant_gene/trait/backfire/nettle_burn/death)
mutatelist = null
reagents_add = list(/datum/reagent/toxin/acid/fluacid = 0.5, /datum/reagent/toxin/acid = 0.5)
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
graft_gene = /datum/plant_gene/trait/stinging
/obj/item/food/grown/nettle // "snack"
diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm
index 22ea8d5334e3b..7e82310991cc5 100644
--- a/code/modules/hydroponics/hydroponics.dm
+++ b/code/modules/hydroponics/hydroponics.dm
@@ -60,8 +60,8 @@
//SO HERE LIES THE "nutrilevel" VAR. IT'S DEAD AND I PUT IT OUT OF IT'S MISERY. USE "reagents" INSTEAD. ~ArcaneMusic, accept no substitutes.
create_reagents(maxnutri, INJECTABLE)
if(mapload)
- reagents.add_reagent(/datum/reagent/plantnutriment/eznutriment, 10) //Half filled nutrient trays for dirt trays to have more to grow with in prison/lavaland.
- waterlevel = 100
+ reagents.add_reagent(/datum/reagent/plantnutriment/eznutriment, max(maxnutri / 2, 10)) //Half filled nutrient trays for dirt trays to have more to grow with in prison/lavaland.
+ waterlevel = maxwater
. = ..()
var/static/list/hovering_item_typechecks = list(
@@ -161,6 +161,7 @@
AddComponent(/datum/component/simple_rotation)
AddComponent(/datum/component/plumbing/hydroponics)
AddComponent(/datum/component/usb_port, list(/obj/item/circuit_component/hydroponics))
+ AddComponent(/datum/component/fishing_spot, /datum/fish_source/hydro_tray)
/obj/machinery/hydroponics/constructable/RefreshParts()
. = ..()
@@ -281,11 +282,11 @@
/obj/machinery/hydroponics/bullet_act(obj/projectile/Proj) //Works with the Somatoray to modify plant variables.
if(!myseed)
return ..()
- if(istype(Proj , /obj/projectile/energy/floramut))
+ if(istype(Proj , /obj/projectile/energy/flora/mut))
mutate()
- else if(istype(Proj , /obj/projectile/energy/florayield))
+ else if(istype(Proj , /obj/projectile/energy/flora/yield))
return myseed.bullet_act(Proj)
- else if(istype(Proj , /obj/projectile/energy/florarevolution))
+ else if(istype(Proj , /obj/projectile/energy/flora/evolution))
if(myseed)
if(LAZYLEN(myseed.mutatelist))
myseed.set_instability(myseed.instability/2)
diff --git a/code/modules/hydroponics/seeds.dm b/code/modules/hydroponics/seeds.dm
index 36653ebafb9ff..3a399c872162b 100644
--- a/code/modules/hydroponics/seeds.dm
+++ b/code/modules/hydroponics/seeds.dm
@@ -170,7 +170,7 @@
/obj/item/seeds/bullet_act(obj/projectile/Proj) //Works with the Somatoray to modify plant variables.
- if(istype(Proj, /obj/projectile/energy/florayield))
+ if(istype(Proj, /obj/projectile/energy/flora/yield))
var/rating = 1
if(istype(loc, /obj/machinery/hydroponics))
var/obj/machinery/hydroponics/H = loc
diff --git a/code/modules/industrial_lift/lift_master.dm b/code/modules/industrial_lift/lift_master.dm
deleted file mode 100644
index 207e8b7fd742d..0000000000000
--- a/code/modules/industrial_lift/lift_master.dm
+++ /dev/null
@@ -1,634 +0,0 @@
-///associative list of the form: list(lift_id = list(all lift_master datums attached to lifts of that type))
-GLOBAL_LIST_EMPTY(active_lifts_by_type)
-
-///coordinate and control movement across linked industrial_lift's. allows moving large single multitile platforms and many 1 tile platforms.
-///also is capable of linking platforms across linked z levels
-/datum/lift_master
- ///the lift platforms we consider as part of this lift. ordered in order of lowest z level to highest z level after init.
- ///(the sorting algorithm sucks btw)
- var/list/obj/structure/industrial_lift/lift_platforms
-
- /// Typepath list of what to ignore smashing through, controls all lifts
- var/static/list/ignored_smashthroughs = list(
- /obj/machinery/power/supermatter_crystal,
- /obj/structure/holosign,
- /obj/machinery/field,
- )
-
- ///whether the lift handled by this lift_master datum is multitile as opposed to nxm platforms per z level
- var/multitile_platform = FALSE
-
- ///taken from our lift platforms. if true we go through each z level of platforms and attempt to make the lowest left corner platform
- ///into one giant multitile object the size of all other platforms on that z level.
- var/create_multitile_platform = FALSE
-
- ///lift platforms have already been sorted in order of z level.
- var/z_sorted = FALSE
-
- ///lift_id taken from our base lift platform, used to put us into GLOB.active_lifts_by_type
- var/lift_id = BASIC_LIFT_ID
-
- ///overridable ID string to link control units to this specific lift_master datum. created by placing a lift id landmark object
- ///somewhere on the tram, if its anywhere on the tram we'll find it in init and set this to whatever it specifies
- var/specific_lift_id
-
- ///if true, the lift cannot be manually moved.
- var/controls_locked = FALSE
-
-/datum/lift_master/New(obj/structure/industrial_lift/lift_platform)
- lift_id = lift_platform.lift_id
- create_multitile_platform = lift_platform.create_multitile_platform
-
- Rebuild_lift_plaform(lift_platform)
- ignored_smashthroughs = typecacheof(ignored_smashthroughs)
-
- LAZYADDASSOCLIST(GLOB.active_lifts_by_type, lift_id, src)
-
- for(var/obj/structure/industrial_lift/lift as anything in lift_platforms)
- lift.add_initial_contents()
-
-/datum/lift_master/Destroy()
- for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms)
- lift_platform.lift_master_datum = null
- lift_platforms = null
-
- LAZYREMOVEASSOC(GLOB.active_lifts_by_type, lift_id, src)
- if(isnull(GLOB.active_lifts_by_type))
- GLOB.active_lifts_by_type = list()//im lazy
-
- return ..()
-
-/datum/lift_master/proc/add_lift_platforms(obj/structure/industrial_lift/new_lift_platform)
- if(new_lift_platform in lift_platforms)
- return
- for(var/obj/structure/industrial_lift/other_platform in new_lift_platform.loc)
- if(other_platform != new_lift_platform)
- stack_trace("there is more than one lift platform on a tile when a lift_master adds it. this causes problems")
- qdel(other_platform)
-
- new_lift_platform.lift_master_datum = src
- LAZYADD(lift_platforms, new_lift_platform)
- RegisterSignal(new_lift_platform, COMSIG_QDELETING, PROC_REF(remove_lift_platforms))
-
- check_for_landmarks(new_lift_platform)
-
- if(z_sorted)//make sure we dont lose z ordering if we get additional platforms after init
- order_platforms_by_z_level()
-
-/datum/lift_master/proc/remove_lift_platforms(obj/structure/industrial_lift/old_lift_platform)
- SIGNAL_HANDLER
-
- if(!(old_lift_platform in lift_platforms))
- return
-
- old_lift_platform.lift_master_datum = null
- LAZYREMOVE(lift_platforms, old_lift_platform)
- UnregisterSignal(old_lift_platform, COMSIG_QDELETING)
- if(!length(lift_platforms))
- qdel(src)
-
-///Collect all bordered platforms via a simple floodfill algorithm. allows multiz trams because its funny
-/datum/lift_master/proc/Rebuild_lift_plaform(obj/structure/industrial_lift/base_lift_platform)
- add_lift_platforms(base_lift_platform)
- var/list/possible_expansions = list(base_lift_platform)
-
- while(possible_expansions.len)
- for(var/obj/structure/industrial_lift/borderline as anything in possible_expansions)
- var/list/result = borderline.lift_platform_expansion(src)
- if(length(result))
- for(var/obj/structure/industrial_lift/lift_platform as anything in result)
- if(lift_platforms.Find(lift_platform))
- continue
-
- add_lift_platforms(lift_platform)
- possible_expansions |= lift_platform
-
- possible_expansions -= borderline
-
-///check for any landmarks placed inside the locs of the given lift_platform
-/datum/lift_master/proc/check_for_landmarks(obj/structure/industrial_lift/new_lift_platform)
- SHOULD_CALL_PARENT(TRUE)
-
- for(var/turf/platform_loc as anything in new_lift_platform.locs)
- var/obj/effect/landmark/lift_id/id_giver = locate() in platform_loc
-
- if(id_giver)
- set_info_from_id_landmark(id_giver)
-
-///set vars and such given an overriding lift_id landmark
-/datum/lift_master/proc/set_info_from_id_landmark(obj/effect/landmark/lift_id/landmark)
- SHOULD_CALL_PARENT(TRUE)
-
- if(!istype(landmark, /obj/effect/landmark/lift_id))//lift_master subtypes can want differnet id's than the base type wants
- return
-
- if(landmark.specific_lift_id)
- specific_lift_id = landmark.specific_lift_id
-
- qdel(landmark)
-
-///orders the lift platforms in order of lowest z level to highest z level.
-/datum/lift_master/proc/order_platforms_by_z_level()
- //contains nested lists for every z level in the world. why? because its really easy to sort
- var/list/platforms_by_z = list()
- platforms_by_z.len = world.maxz
-
- for(var/z in 1 to world.maxz)
- platforms_by_z[z] = list()
-
- for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms)
- if(QDELETED(lift_platform) || !lift_platform.z)
- lift_platforms -= lift_platform
- continue
-
- platforms_by_z[lift_platform.z] += lift_platform
-
- if(create_multitile_platform)
- for(var/list/z_list as anything in platforms_by_z)
- if(!length(z_list))
- continue
-
- create_multitile_platform_for_z_level(z_list)//this will subtract all but one platform from the list
-
- var/list/output = list()
-
- for(var/list/z_list as anything in platforms_by_z)
- output += z_list
-
- lift_platforms = output
-
- z_sorted = TRUE
-
-///goes through all platforms in the given list and finds the one in the lower left corner
-/datum/lift_master/proc/create_multitile_platform_for_z_level(list/obj/structure/industrial_lift/platforms_in_z)
- var/min_x = INFINITY
- var/max_x = 0
-
- var/min_y = INFINITY
- var/max_y = 0
-
- var/z = 0
-
- for(var/obj/structure/industrial_lift/lift_to_sort as anything in platforms_in_z)
- if(!z)
- if(!lift_to_sort.z)
- stack_trace("create_multitile_platform_for_z_level() was given a platform in nullspace or not on a turf!")
- platforms_in_z -= lift_to_sort
- continue
-
- z = lift_to_sort.z
-
- if(z != lift_to_sort.z)
- stack_trace("create_multitile_platform_for_z_level() was given lifts on different z levels!")
- platforms_in_z -= lift_to_sort
- continue
-
- min_x = min(min_x, lift_to_sort.x)
- max_x = max(max_x, lift_to_sort.x)
-
- min_y = min(min_y, lift_to_sort.y)
- max_y = max(max_y, lift_to_sort.y)
-
- var/turf/lower_left_corner_loc = locate(min_x, min_y, z)
- if(!lower_left_corner_loc)
- CRASH("was unable to find a turf at the lower left corner of this z")
-
- var/obj/structure/industrial_lift/lower_left_corner_lift = locate() in lower_left_corner_loc
-
- if(!lower_left_corner_lift)
- CRASH("there was no lift in the lower left corner of the given lifts")
-
- platforms_in_z.Cut()
- platforms_in_z += lower_left_corner_lift//we want to change the list given to us not create a new one. so we do this
-
- lower_left_corner_lift.create_multitile_platform(min_x, min_y, max_x, max_y, z)
-
-///returns the closest lift to the specified atom, prioritizing lifts on the same z level. used for comparing distance
-/datum/lift_master/proc/return_closest_platform_to(atom/comparison, allow_multiple_answers = FALSE)
- if(!istype(comparison) || !comparison.z)
- return FALSE
-
- var/list/obj/structure/industrial_lift/candidate_platforms = list()
-
- for(var/obj/structure/industrial_lift/platform as anything in lift_platforms)
- if(platform.z == comparison.z)
- candidate_platforms += platform
-
- var/obj/structure/industrial_lift/winner = candidate_platforms[1]
- var/winner_distance = get_dist(comparison, winner)
-
- var/list/tied_winners = list(winner)
-
- for(var/obj/structure/industrial_lift/platform_to_sort as anything in candidate_platforms)
- var/platform_distance = get_dist(comparison, platform_to_sort)
-
- if(platform_distance < winner_distance)
- winner = platform_to_sort
- winner_distance = platform_distance
-
- if(allow_multiple_answers)
- tied_winners = list(winner)
-
- else if(platform_distance == winner_distance && allow_multiple_answers)
- tied_winners += platform_to_sort
-
- if(allow_multiple_answers)
- return tied_winners
-
- return winner
-
-/// Returns a lift platform on the z-level which is vertically closest to the passed target_z
-/datum/lift_master/proc/return_closest_platform_to_z(target_z)
- var/obj/structure/industrial_lift/found_platform
- for(var/obj/structure/industrial_lift/lift as anything in lift_platforms)
- // Already at the same Z-level, we can stop
- if(lift.z == target_z)
- found_platform = lift
- break
-
- // Set up an initial lift to compare to
- if(!found_platform)
- found_platform = lift
- continue
-
- // Same level, we can go with the one we currently have
- if(lift.z == found_platform.z)
- continue
-
- // If the difference between the current found platform and the target
- // if less than the distance between the next lift and the target,
- // our current platform is closer to the target than the next one, so we can skip it
- if(abs(found_platform.z - target_z) < abs(lift.z - target_z))
- continue
-
- // The difference is smaller for this lift, so it's closer
- found_platform = lift
-
- return found_platform
-
-/// Returns a list of all the z-levels our lift is currently on.
-/datum/lift_master/proc/get_zs_we_are_on()
- var/list/zs_we_are_present_on = list()
- for(var/obj/structure/industrial_lift/lift as anything in lift_platforms)
- zs_we_are_present_on |= lift.z
- return zs_we_are_present_on
-
-///returns all industrial_lifts associated with this tram on the given z level or given atoms z level
-/datum/lift_master/proc/get_platforms_on_level(atom/atom_reference_OR_z_level_number)
- var/z = atom_reference_OR_z_level_number
- if(isatom(atom_reference_OR_z_level_number))
- z = atom_reference_OR_z_level_number.z
-
- if(!isnum(z) || z < 0 || z > world.maxz)
- return null
-
- var/list/platforms_in_z = list()
-
- for(var/obj/structure/industrial_lift/lift_to_check as anything in lift_platforms)
- if(lift_to_check.z)
- platforms_in_z += lift_to_check
-
- return platforms_in_z
-
-/**
- * Moves the lift UP or DOWN, this is what users invoke with their hand.
- * This is a SAFE proc, ensuring every part of the lift moves SANELY.
- *
- * Arguments:
- * going - UP or DOWN directions, where the lift should go. Keep in mind by this point checks of whether it should go up or down have already been done.
- * user - Whomever made the lift movement.
- */
-/datum/lift_master/proc/move_lift_vertically(going, mob/user)
- //lift_platforms are sorted in order of lowest z to highest z, so going upwards we need to move them in reverse order to not collide
- if(going == UP)
- var/obj/structure/industrial_lift/platform_to_move
- var/current_index = length(lift_platforms)
-
- while(current_index > 0)
- platform_to_move = lift_platforms[current_index]
- current_index--
-
- platform_to_move.travel(going)
-
- else if(going == DOWN)
- for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms)
- lift_platform.travel(going)
-
-/**
- * Moves the lift after a passed delay.
- *
- * This is a more "user friendly" or "realistic" lift move.
- * It includes things like:
- * - Allowing lift "travel time"
- * - Shutting elevator safety doors
- * - Sound effects while moving
- * - Safety warnings for anyone below the lift (while it's moving downwards)
- *
- * Arguments:
- * duration - required, how long do we wait to move the lift?
- * door_duration - optional, how long should we wait to open the doors after arriving? If null, we won't open or close doors
- * direction - which direction are we moving the lift?
- * user - optional, who is moving the lift?
- */
-/datum/lift_master/proc/move_after_delay(lift_move_duration, door_duration, direction, mob/user)
- if(!isnum(lift_move_duration))
- CRASH("[type] move_after_delay called with invalid duration ([lift_move_duration]).")
- if(lift_move_duration <= 0 SECONDS)
- move_lift_vertically(direction, user)
- return
-
- // Get the lowest or highest lift according to which direction we're moving
- var/obj/structure/industrial_lift/prime_lift = return_closest_platform_to_z(direction == UP ? world.maxz : 0)
-
- // If anyone changes the hydraulic sound effect I sure hope they update this variable...
- var/hydraulic_sfx_duration = 2 SECONDS
- // ...because we use the duration of the sound effect to make it last for roughly the duration of the lift travel
- playsound(prime_lift, 'sound/mecha/hydraulic.ogg', 25, vary = TRUE, frequency = clamp(hydraulic_sfx_duration / lift_move_duration, 0.33, 3))
-
- // Move the lift after a timer
- addtimer(CALLBACK(src, PROC_REF(move_lift_vertically), direction, user), lift_move_duration, TIMER_UNIQUE)
- // Open doors after the set duration if supplied
- if(isnum(door_duration))
- addtimer(CALLBACK(src, PROC_REF(open_lift_doors_callback)), door_duration, TIMER_UNIQUE)
-
- // Here on we only care about lifts going DOWN
- if(direction != DOWN)
- return
-
- // Okay we're going down, let's try to display some warnings to people below
- var/list/turf/lift_locs = list()
- for(var/obj/structure/industrial_lift/going_to_move as anything in lift_platforms)
- // This lift has no warnings so we don't even need to worry about it
- if(!going_to_move.warns_on_down_movement)
- continue
- // Collect all the turfs our lift is found at
- lift_locs |= going_to_move.locs
-
- for(var/turf/moving in lift_locs)
- // Find what's below the turf that's moving
- var/turf/below_us = get_step_multiz(moving, DOWN)
- // Hold up the turf below us is also in our locs list. Multi-z lift? Don't show a warning
- if(below_us in lift_locs)
- continue
- // Display the warning for until we land
- new /obj/effect/temp_visual/telegraphing/lift_travel(below_us, lift_move_duration)
-
-/**
- * Simple wrapper for checking if we can move 1 zlevel, and if we can, do said move.
- * Locks controls, closes all doors, then moves the lift and re-opens the doors afterwards.
- *
- * Arguments:
- * direction - which direction are we moving?
- * lift_move_duration - how long does the move take? can be 0 or null for instant move.
- * door_duration - how long does it take for the doors to open after a move?
- * user - optional, who moved it?
- */
-/datum/lift_master/proc/simple_move_wrapper(direction, lift_move_duration, mob/user)
- if(!Check_lift_move(direction))
- return FALSE
-
- // Lock controls, to prevent moving-while-moving memes
- set_controls(LIFT_PLATFORM_LOCKED)
- // Send out a signal that we're going
- SEND_SIGNAL(src, COMSIG_LIFT_SET_DIRECTION, direction)
- // Close all lift doors
- update_lift_doors(action = CLOSE_DOORS)
-
- if(isnull(lift_move_duration) || lift_move_duration <= 0 SECONDS)
- // Do an instant move
- move_lift_vertically(direction, user)
- // Open doors on the zs we arrive at
- update_lift_doors(get_zs_we_are_on(), action = OPEN_DOORS)
- // And unlock the controls after
- set_controls(LIFT_PLATFORM_UNLOCKED)
- return TRUE
-
- // Do a delayed move
- move_after_delay(
- lift_move_duration = lift_move_duration,
- door_duration = lift_move_duration * 1.5,
- direction = direction,
- user = user,
- )
-
- addtimer(CALLBACK(src, PROC_REF(finish_simple_move_wrapper)), lift_move_duration * 1.5)
- return TRUE
-
-/**
- * Wrap everything up from simple_move_wrapper finishing its movement
- */
-/datum/lift_master/proc/finish_simple_move_wrapper()
- SEND_SIGNAL(src, COMSIG_LIFT_SET_DIRECTION, 0)
- set_controls(LIFT_PLATFORM_UNLOCKED)
-
-/**
- * Moves the lift to the passed z-level.
- *
- * Checks for validity of the move: Are we moving to the same z-level, can we actually move to that z-level?
- * Does NOT check if the lift controls are currently locked.
- *
- * Moves to the passed z-level by calling move_after_delay repeatedly until the passed z-level is reached.
- * This proc sleeps as it moves.
- *
- * Arguments:
- * target_z - required, the Z we want to move to
- * loop_callback - optional, an additional callback invoked during the l oop that allows the move to cancel.
- * user - optional, who started the move
- */
-/datum/lift_master/proc/move_to_zlevel(target_z, datum/callback/loop_callback, mob/user)
- if(!isnum(target_z) || target_z <= 0)
- CRASH("[type] move_to_zlevel was passed an invalid target_z ([target_z]).")
-
- var/obj/structure/industrial_lift/prime_lift = return_closest_platform_to_z(target_z)
- var/lift_z = prime_lift.z
- // We're already at the desired z-level!
- if(target_z == lift_z)
- return FALSE
-
- // The amount of z levels between the our and target_z
- var/z_difference = abs(target_z - lift_z)
- // Direction (up/down) needed to go to reach target_z
- var/direction = lift_z < target_z ? UP : DOWN
-
- // We can't go that way anymore, or possibly ever
- if(!Check_lift_move(direction))
- return FALSE
-
- // Okay we're ready to start moving now.
- set_controls(LIFT_PLATFORM_LOCKED)
- // Send out a signal that we're going
- SEND_SIGNAL(src, COMSIG_LIFT_SET_DIRECTION, direction)
- var/travel_speed = prime_lift.elevator_vertical_speed
-
- // Close all lift doors
- update_lift_doors(action = CLOSE_DOORS)
- // Approach the desired z-level one step at a time
- for(var/i in 1 to z_difference)
- if(!Check_lift_move(direction))
- break
- if(loop_callback && !loop_callback.Invoke())
- break
- // move_after_delay will set up a timer and cause us to move after a time
- move_after_delay(
- lift_move_duration = travel_speed,
- direction = direction,
- user = user,
- )
- // and we don't want to send another request until the timer's done
- stoplag(travel_speed + 0.1 SECONDS)
- if(QDELETED(src) || QDELETED(prime_lift))
- return
-
- addtimer(CALLBACK(src, PROC_REF(open_lift_doors_callback)), 2 SECONDS)
- SEND_SIGNAL(src, COMSIG_LIFT_SET_DIRECTION, 0)
- set_controls(LIFT_PLATFORM_UNLOCKED)
- return TRUE
-
-/**
- * Updates all blast doors and shutters that share an ID with our lift.
- *
- * Arguments:
- * on_z_level - optional, only open doors on this z-level or list of z-levels
- * action - how do we update the doors? OPEN_DOORS to make them open, CLOSE_DOORS to make them shut
- */
-/datum/lift_master/proc/update_lift_doors(on_z_level, action)
-
- if(!isnull(on_z_level) && !islist(on_z_level))
- on_z_level = list(on_z_level)
-
- var/played_ding = FALSE
- for(var/obj/machinery/door/elevator_door as anything in GLOB.elevator_doors)
- if(elevator_door.elevator_linked_id != specific_lift_id)
- continue
- if(on_z_level && !(elevator_door.z in on_z_level))
- continue
- switch(action)
- if(OPEN_DOORS)
- elevator_door.elevator_status = LIFT_PLATFORM_UNLOCKED
- if(!played_ding)
- playsound(elevator_door, 'sound/machines/ping.ogg', 50, TRUE)
- played_ding = TRUE
- addtimer(CALLBACK(elevator_door, TYPE_PROC_REF(/obj/machinery/door, open)), 0.7 SECONDS)
- if(CLOSE_DOORS)
- elevator_door.elevator_status = LIFT_PLATFORM_LOCKED
- INVOKE_ASYNC(elevator_door, TYPE_PROC_REF(/obj/machinery/door, close))
- else
- stack_trace("Elevator lift update_lift_doors called with an improper action ([action]).")
-
-/// Helper used in callbacks to open all the doors our lift is on
-/datum/lift_master/proc/open_lift_doors_callback()
- update_lift_doors(get_zs_we_are_on(), action = OPEN_DOORS)
-
-/**
- * Moves the lift, this is what users invoke with their hand.
- * This is a SAFE proc, ensuring every part of the lift moves SANELY.
- * It also locks controls for the (miniscule) duration of the movement, so the elevator cannot be broken by spamming.
- */
-/datum/lift_master/proc/move_lift_horizontally(going)
- set_controls(LIFT_PLATFORM_LOCKED)
-
- if(multitile_platform)
- for(var/obj/structure/industrial_lift/platform_to_move as anything in lift_platforms)
- platform_to_move.travel(going)
-
- set_controls(LIFT_PLATFORM_UNLOCKED)
- return
-
- var/max_x = 0
- var/max_y = 0
- var/max_z = 0
- var/min_x = world.maxx
- var/min_y = world.maxy
- var/min_z = world.maxz
-
- for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms)
- max_z = max(max_z, lift_platform.z)
- min_z = min(min_z, lift_platform.z)
-
- min_x = min(min_x, lift_platform.x)
- max_x = max(max_x, lift_platform.x)
- //this assumes that all z levels have identical horizontal bounding boxes
- //but if youre still using a non multitile tram platform at this point
- //then its your own problem. it wont runtime it will jsut be slower than it needs to be if this assumption isnt
- //the case
-
- min_y = min(min_y, lift_platform.y)
- max_y = max(max_y, lift_platform.y)
-
- for(var/z in min_z to max_z)
- //This must be safe way to border tile to tile move of bordered platforms, that excludes platform overlapping.
- if(going & WEST)
- //Go along the X axis from min to max, from left to right
- for(var/x in min_x to max_x)
- if(going & NORTH)
- //Go along the Y axis from max to min, from up to down
- for(var/y in max_y to min_y step -1)
- var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z))
- lift_platform?.travel(going)
-
- else if(going & SOUTH)
- //Go along the Y axis from min to max, from down to up
- for(var/y in min_y to max_y)
- var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z))
- lift_platform?.travel(going)
-
- else
- for(var/y in min_y to max_y)
- var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z))
- lift_platform?.travel(going)
- else
- //Go along the X axis from max to min, from right to left
- for(var/x in max_x to min_x step -1)
- if(going & NORTH)
- //Go along the Y axis from max to min, from up to down
- for(var/y in max_y to min_y step -1)
- var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z))
- lift_platform?.travel(going)
-
- else if (going & SOUTH)
- for(var/y in min_y to max_y)
- var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z))
- lift_platform?.travel(going)
-
- else
- //Go along the Y axis from min to max, from down to up
- for(var/y in min_y to max_y)
- var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z))
- lift_platform?.travel(going)
-
- set_controls(LIFT_PLATFORM_UNLOCKED)
-
-///Check destination turfs
-/datum/lift_master/proc/Check_lift_move(check_dir)
- for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms)
- for(var/turf/bound_turf in lift_platform.locs)
- var/turf/T = get_step_multiz(lift_platform, check_dir)
- if(!T)//the edges of multi-z maps
- return FALSE
- if(check_dir == UP && !istype(T, /turf/open/openspace)) // We don't want to go through the ceiling!
- return FALSE
- if(check_dir == DOWN && !istype(get_turf(lift_platform), /turf/open/openspace)) // No going through the floor!
- return FALSE
- return TRUE
-
-/**
- * Sets all lift parts's controls_locked variable. Used to prevent moving mid movement, or cooldowns.
- */
-/datum/lift_master/proc/set_controls(state)
- controls_locked = state
-
-/**
- * resets the contents of all platforms to their original state in case someone put a bunch of shit onto the tram.
- * intended to be called by admins. passes all arguments to reset_contents() for each of our platforms.
- *
- * Arguments:
- * * consider_anything_past - number. if > 0 our platforms will only handle foreign contents that exceed this number in each of their locs
- * * foreign_objects - bool. if true our platforms will consider /atom/movable's that arent mobs as part of foreign contents
- * * foreign_non_player_mobs - bool. if true our platforms consider mobs that dont have a mind to be foreign
- * * consider_player_mobs - bool. if true our platforms consider player mobs to be foreign. only works if foreign_non_player_mobs is true as well
- */
-/datum/lift_master/proc/reset_lift_contents(consider_anything_past = 0, foreign_objects = TRUE, foreign_non_player_mobs = TRUE, consider_player_mobs = FALSE)
- for(var/obj/structure/industrial_lift/lift_to_reset in lift_platforms)
- lift_to_reset.reset_contents(consider_anything_past, foreign_objects, foreign_non_player_mobs, consider_player_mobs)
-
- return TRUE
diff --git a/code/modules/industrial_lift/tram/tram_doors.dm b/code/modules/industrial_lift/tram/tram_doors.dm
deleted file mode 100644
index f0253659eb65e..0000000000000
--- a/code/modules/industrial_lift/tram/tram_doors.dm
+++ /dev/null
@@ -1,135 +0,0 @@
-/obj/machinery/door/window/tram
- name = "tram door"
- desc = "Probably won't crush you if you try to rush them as they close. But we know you live on that danger, try and beat the tram!"
- icon = 'icons/obj/doors/tramdoor.dmi'
- req_access = list("tcomms")
- multi_tile = TRUE
- var/associated_lift = MAIN_STATION_TRAM
- var/datum/weakref/tram_ref
- /// Are the doors in a malfunctioning state (dangerous)
- var/malfunctioning = FALSE
-
-/obj/machinery/door/window/tram/left
- icon_state = "left"
- base_state = "left"
-
-/obj/machinery/door/window/tram/left/directional/south
- plane = WALL_PLANE_UPPER
-
-/obj/machinery/door/window/tram/right
- icon_state = "right"
- base_state = "right"
-
-/obj/machinery/door/window/tram/hilbert
- icon = 'icons/obj/mining_zones/survival_pod.dmi'
- associated_lift = HILBERT_TRAM
- icon_state = "windoor"
- base_state = "windoor"
-
-/obj/machinery/door/window/tram/emag_act(mob/user, obj/item/card/emag/emag_card)
- if(obj_flags & EMAGGED)
- return FALSE
- balloon_alert(user, "disabled motion sensors")
- obj_flags |= EMAGGED
- return TRUE
-
-/// Random event called by code\modules\events\tram_malfunction.dm
-/// Makes the doors malfunction
-/obj/machinery/door/window/tram/proc/start_malfunction()
- if(obj_flags & EMAGGED)
- return
-
- malfunctioning = TRUE
- process()
-
-/// Random event called by code\modules\events\tram_malfunction.dm
-/// Returns doors to their original status
-/obj/machinery/door/window/tram/proc/end_malfunction()
- if(obj_flags & EMAGGED)
- return
-
- malfunctioning = FALSE
- process()
-
-/obj/machinery/door/window/tram/proc/cycle_doors(command, forced=FALSE)
- if(command == "open" && icon_state == "[base_state]open")
- if(!forced && !hasPower())
- return FALSE
- return TRUE
- if(command == "close" && icon_state == base_state)
- return TRUE
- switch(command)
- if("open")
- playsound(src, 'sound/machines/tramopen.ogg', vol = 75, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
- do_animate("opening")
- icon_state ="[base_state]open"
- sleep(7 DECISECONDS)
- set_density(FALSE)
- air_update_turf(TRUE, FALSE)
- if("close")
- if((obj_flags & EMAGGED) || malfunctioning)
- flick("[base_state]spark", src)
- playsound(src, SFX_SPARKS, vol = 75, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
- sleep(6 DECISECONDS)
- playsound(src, 'sound/machines/tramclose.ogg', vol = 75, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
- do_animate("closing")
- icon_state = base_state
- sleep(19 DECISECONDS)
- if((obj_flags & EMAGGED) || malfunctioning)
- if(malfunctioning && prob(85))
- return
- for(var/i in 1 to 3)
- for(var/mob/living/crushee in get_turf(src))
- crush()
- sleep(2 DECISECONDS)
- air_update_turf(TRUE, TRUE)
- operating = FALSE
- set_density(TRUE)
-
- update_freelook_sight()
- return TRUE
-
-/obj/machinery/door/window/tram/right/directional/south
- plane = WALL_PLANE_UPPER
-
-/obj/machinery/door/window/tram/proc/find_tram()
- for(var/datum/lift_master/lift as anything in GLOB.active_lifts_by_type[TRAM_LIFT_ID])
- if(lift.specific_lift_id == associated_lift)
- tram_ref = WEAKREF(lift)
-
-/obj/machinery/door/window/tram/Initialize(mapload, set_dir, unres_sides)
- . = ..()
- RemoveElement(/datum/element/atmos_sensitive, mapload)
- if(filler)
- filler.set_density(FALSE) // tram doors allow you to stand on the tile
- INVOKE_ASYNC(src, PROC_REF(open))
- GLOB.tram_doors += src
- find_tram()
-
-/obj/machinery/door/window/tram/Destroy()
- GLOB.tram_doors -= src
- return ..()
-
-/obj/machinery/door/window/tram/examine(mob/user)
- . = ..()
- . += span_notice("It has labels indicating that it has an emergency mechanism to open using just your hands in the event of an emergency.")
-
-/obj/machinery/door/window/tram/try_safety_unlock(mob/user)
- if(!hasPower() && density)
- balloon_alert(user, "pulling emergency exit...")
- if(do_after(user, 7 SECONDS, target = src))
- try_to_crowbar(null, user, TRUE)
- return TRUE
-
-/obj/machinery/door/window/tram/bumpopen(mob/user)
- if(operating || !density)
- return
- var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
- add_fingerprint(user)
- if(tram_part.travel_distance < XING_DEFAULT_TRAM_LENGTH || tram_part.travel_distance > tram_part.travel_trip_length - XING_DEFAULT_TRAM_LENGTH)
- return // we're already animating, don't reset that
- cycle_doors(OPEN_DOORS, TRUE) //making a daring exit midtravel? make sure the doors don't go in the wrong state on arrival.
- return
-
-MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/tram/left, 0)
-MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/tram/right, 0)
diff --git a/code/modules/industrial_lift/tram/tram_floors.dm b/code/modules/industrial_lift/tram/tram_floors.dm
deleted file mode 100644
index 3d4cff43ccbdc..0000000000000
--- a/code/modules/industrial_lift/tram/tram_floors.dm
+++ /dev/null
@@ -1,96 +0,0 @@
-/turf/open/floor/noslip/tram
- name = "high-traction platform"
- icon_state = "noslip_tram"
- base_icon_state = "noslip_tram"
- floor_tile = /obj/item/stack/tile/noslip/tram
-
-/turf/open/floor/noslip/tram_plate
- name = "linear induction plate"
- desc = "The linear induction plate that powers the tram."
- icon_state = "tram_plate"
- base_icon_state = "tram_plate"
- floor_tile = /obj/item/stack/tile/noslip/tram_plate
- slowdown = 0
- flags_1 = NONE
-
-/turf/open/floor/noslip/tram_plate/energized
- desc = "The linear induction plate that powers the tram. It is currently energized."
- /// Inbound station
- var/inbound
- /// Outbound station
- var/outbound
-
-/turf/open/floor/noslip/tram_platform
- name = "tram platform"
- icon_state = "tram_platform"
- base_icon_state = "tram_platform"
- floor_tile = /obj/item/stack/tile/noslip/tram_platform
- slowdown = 0
-
-/turf/open/floor/noslip/tram_plate/broken_states()
- return list("tram_plate-damaged1","tram_plate-damaged2")
-
-/turf/open/floor/noslip/tram_plate/burnt_states()
- return list("tram_plate-scorched1","tram_plate-scorched2")
-
-/turf/open/floor/noslip/tram_platform/broken_states()
- return list("tram_platform-damaged1","tram_platform-damaged2")
-
-/turf/open/floor/noslip/tram_platform/burnt_states()
- return list("tram_platform-scorched1","tram_platform-scorched2")
-
-/turf/open/floor/noslip/tram_plate/energized/proc/find_tram()
- for(var/datum/lift_master/tram/tram as anything in GLOB.active_lifts_by_type[TRAM_LIFT_ID])
- if(tram.specific_lift_id != MAIN_STATION_TRAM)
- continue
- return tram
-
-/turf/open/floor/noslip/tram_plate/energized/proc/toast(mob/living/future_tram_victim)
- var/datum/lift_master/tram/tram = find_tram()
-
- // Check for stopped states.
- if(!tram || !tram.is_operational || !inbound || !outbound)
- return FALSE
-
- var/obj/structure/industrial_lift/tram/tram_part = tram.return_closest_platform_to(src)
-
- if(QDELETED(tram_part))
- return FALSE
-
- // Everything will be based on position and travel direction
- var/plate_pos
- var/tram_pos
- var/tram_velocity_sign // 1 for positive axis movement, -1 for negative
- // Try to be agnostic about N-S vs E-W movement
- if(tram.travel_direction & (NORTH|SOUTH))
- plate_pos = y
- tram_pos = tram_part.y
- tram_velocity_sign = tram.travel_direction & NORTH ? 1 : -1
- else
- plate_pos = x
- tram_pos = tram_part.x
- tram_velocity_sign = tram.travel_direction & EAST ? 1 : -1
-
- // How far away are we? negative if already passed.
- var/approach_distance = tram_velocity_sign * (plate_pos - (tram_pos + (XING_DEFAULT_TRAM_LENGTH * 0.5)))
-
- // Check if our victim is in the active path of the tram.
- if(!tram.travelling)
- return FALSE
- if(approach_distance < 0)
- return FALSE
- playsound(src, SFX_SPARKS, 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
- audible_message(
- span_hear("You hear an electric crackle when you step on the plate...")
- )
- if(tram.travel_direction & WEST && inbound < tram.idle_platform.platform_code)
- return FALSE
- if(tram.travel_direction & EAST && outbound > tram.idle_platform.platform_code)
- return FALSE
- if(approach_distance >= XING_DISTANCE_AMBER)
- return FALSE
-
- // Finally the interesting part where they ACTUALLY get hit!
- notify_ghosts("[future_tram_victim] has fallen in the path of an oncoming tram!", source = future_tram_victim, action = NOTIFY_ORBIT, header = "Electrifying!")
- future_tram_victim.electrocute_act(15, src, 1)
- return TRUE
diff --git a/code/modules/industrial_lift/tram/tram_landmark.dm b/code/modules/industrial_lift/tram/tram_landmark.dm
deleted file mode 100644
index 76341a7d06463..0000000000000
--- a/code/modules/industrial_lift/tram/tram_landmark.dm
+++ /dev/null
@@ -1,109 +0,0 @@
-GLOBAL_LIST_EMPTY(tram_landmarks)
-
-/obj/effect/landmark/tram
- name = "tram destination" //the tram buttons will mention this.
- icon_state = "tram"
-
- ///the id of the tram we're linked to.
- var/specific_lift_id = MAIN_STATION_TRAM
- /// The ID of that particular destination.
- var/platform_code = null
- /// Icons for the tgui console to list out for what is at this location
- var/list/tgui_icons = list()
-
-/obj/effect/landmark/tram/Initialize(mapload)
- . = ..()
- LAZYADDASSOCLIST(GLOB.tram_landmarks, specific_lift_id, src)
-
-/obj/effect/landmark/tram/Destroy()
- LAZYREMOVEASSOC(GLOB.tram_landmarks, specific_lift_id, src)
- return ..()
-
-/obj/effect/landmark/tram/nav
- name = "tram nav beacon"
- invisibility = INVISIBILITY_MAXIMUM // nav aids can't be abstract since they stay with the tram
-
-/**
- * lift_id landmarks. used to map in specific_lift_id to trams. when the trams lift_master encounters one on a trams tile
- * it sets its specific_lift_id to that landmark. allows you to have multiple trams and multiple controls linking to their specific tram
- */
-/obj/effect/landmark/lift_id
- name = "lift id setter"
- icon_state = "lift_id"
- ///what specific id we give to the tram we're placed on, should explicitly set this if its a subtype, or weird things might happen
- var/specific_lift_id = MAIN_STATION_TRAM
-
-//tramstation
-
-/obj/effect/landmark/tram/nav/tramstation/main
- name = MAIN_STATION_TRAM
- specific_lift_id = TRAM_NAV_BEACONS
- dir = WEST
-
-/obj/effect/landmark/tram/platform/tramstation/west
- name = "West Wing"
- platform_code = TRAMSTATION_WEST
- tgui_icons = list("Arrivals" = "plane-arrival", "Command" = "bullhorn", "Security" = "gavel")
-
-/obj/effect/landmark/tram/platform/tramstation/central
- name = "Central Wing"
- platform_code = TRAMSTATION_CENTRAL
- tgui_icons = list("Service" = "cocktail", "Medical" = "plus", "Engineering" = "wrench")
-
-/obj/effect/landmark/tram/platform/tramstation/east
- name = "East Wing"
- platform_code = TRAMSTATION_EAST
- tgui_icons = list("Departures" = "plane-departure", "Cargo" = "box", "Science" = "flask")
-
-//map-agnostic landmarks
-
-/obj/effect/landmark/tram/nav/immovable_rod
- name = "DESTINATION/NOT/FOUND"
- specific_lift_id = IMMOVABLE_ROD_DESTINATIONS
-
-/obj/effect/landmark/tram/nav/hilbert
- name = HILBERT_TRAM
- specific_lift_id = TRAM_NAV_BEACONS
- dir = WEST
-
-//birdshot
-
-/obj/effect/landmark/lift_id/birdshot/prison
- specific_lift_id = PRISON_TRAM
-
-/obj/effect/landmark/lift_id/birdshot/maint
- specific_lift_id = MAINTENANCE_TRAM
-
-/obj/effect/landmark/tram/nav/birdshot/prison
- name = PRISON_TRAM
- specific_lift_id = TRAM_NAV_BEACONS
- dir = NORTH
-
-/obj/effect/landmark/tram/nav/birdshot/maint
- name = MAINTENANCE_TRAM
- specific_lift_id = TRAM_NAV_BEACONS
- dir = WEST
-
-/obj/effect/landmark/tram/platform/birdshot/sec_wing
- name = "Security Wing"
- specific_lift_id = PRISON_TRAM
- platform_code = BIRDSHOT_SECURITY_WING
- tgui_icons = list("Security" = "gavel")
-
-/obj/effect/landmark/tram/platform/birdshot/prison_wing
- name = "Prison Wing"
- specific_lift_id = PRISON_TRAM
- platform_code = BIRDSHOT_PRISON_WING
- tgui_icons = list("Prison" = "box")
-
-/obj/effect/landmark/tram/platform/birdshot/maint_left
- name = "Port Platform"
- specific_lift_id = MAINTENANCE_TRAM
- platform_code = BIRDSHOT_MAINTENANCE_LEFT
- tgui_icons = list("Port Platform" = "plane-departure")
-
-/obj/effect/landmark/tram/platform/birdshot/maint_right
- name = "Starboard Platform"
- specific_lift_id = MAINTENANCE_TRAM
- platform_code = BRIDSHOT_MAINTENANCE_RIGHT
- tgui_icons = list("Starboard Platform" = "plane-arrival")
diff --git a/code/modules/industrial_lift/tram/tram_lift_master.dm b/code/modules/industrial_lift/tram/tram_lift_master.dm
deleted file mode 100644
index e2d044dc5b2c3..0000000000000
--- a/code/modules/industrial_lift/tram/tram_lift_master.dm
+++ /dev/null
@@ -1,330 +0,0 @@
-/datum/lift_master/tram
-
- ///whether this tram is traveling across vertical and/or horizontal axis for some distance. not all lifts use this
- var/travelling = FALSE
- ///if we're travelling, what direction are we going
- var/travel_direction = NONE
- ///if we're travelling, how far do we have to go
- var/travel_distance = 0
- ///how far in total we'll be travelling
- var/travel_trip_length = 0
-
- ///multiplier on how much damage/force the tram imparts on things it hits
- var/collision_lethality = 1
-
- /// reference to the destination landmark we consider ourselves "at". since we potentially span multiple z levels we dont actually
- /// know where on us this platform is. as long as we know THAT its on us we can just move the distance and direction between this
- /// and the destination landmark.
- var/obj/effect/landmark/tram/idle_platform
-
- /// a navigational landmark that we use to find the tram's location on the map at any time
- var/obj/effect/landmark/tram/nav/nav_beacon
-
- ///decisecond delay between horizontal movement. cannot make the tram move faster than 1 movement per world.tick_lag.
- ///this var is poorly named its actually horizontal movement delay but whatever.
- var/horizontal_speed = 0.5
-
- ///version of horizontal_speed that gets set in init and is considered our base speed if our lift gets slowed down
- var/base_horizontal_speed = 0.5
-
- ///the world.time we should next move at. in case our speed is set to less than 1 movement per tick
- var/next_move = INFINITY
-
- ///whether we have been slowed down automatically
- var/slowed_down = FALSE
-
- ///how many times we moved while costing more than SStramprocess.max_time milliseconds per movement.
- ///if this exceeds SStramprocess.max_exceeding_moves
- var/times_exceeded = 0
-
- ///how many times we moved while costing less than 0.5 * SStramprocess.max_time milliseconds per movement
- var/times_below = 0
-
- var/is_operational = TRUE
-
-/datum/lift_master/tram/New(obj/structure/industrial_lift/tram/lift_platform)
- . = ..()
- horizontal_speed = lift_platform.horizontal_speed
- base_horizontal_speed = lift_platform.horizontal_speed
-
- check_starting_landmark()
-
-/datum/lift_master/tram/vv_edit_var(var_name, var_value)
- . = ..()
- if(var_name == "base_horizontal_speed")
- horizontal_speed = max(horizontal_speed, base_horizontal_speed)
-
-/datum/lift_master/tram/add_lift_platforms(obj/structure/industrial_lift/new_lift_platform)
- . = ..()
- RegisterSignal(new_lift_platform, COMSIG_MOVABLE_BUMP, PROC_REF(gracefully_break))
-
-/datum/lift_master/tram/check_for_landmarks(obj/structure/industrial_lift/tram/new_lift_platform)
- . = ..()
- for(var/turf/platform_loc as anything in new_lift_platform.locs)
- var/obj/effect/landmark/tram/platform/initial_destination = locate() in platform_loc
- var/obj/effect/landmark/tram/nav/beacon = locate() in platform_loc
-
- if(initial_destination)
- idle_platform = initial_destination
-
- if(beacon)
- nav_beacon = beacon
-
-/datum/lift_master/tram/proc/check_starting_landmark()
- if(!idle_platform || !nav_beacon)
- CRASH("a tram lift_master was initialized without the required landmarks to give it direction!")
-
- SStramprocess.can_fire = TRUE
-
- return TRUE
-
-/**
- * Signal for when the tram runs into a field of which it cannot go through.
- * Stops the train's travel fully, sends a message, and destroys the train.
- * Arguments:
- * bumped_atom - The atom this tram bumped into
- */
-/datum/lift_master/tram/proc/gracefully_break(atom/bumped_atom)
- SIGNAL_HANDLER
-
- travel_distance = 0
- bumped_atom.visible_message(span_userdanger("The [bumped_atom.name] crashes into the field violently!"))
- for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_platforms)
- tram_part.set_travelling(FALSE)
- for(var/tram_contents in tram_part.lift_load)
- if(iseffect(tram_contents))
- continue
-
- if(isliving(tram_contents))
- explosion(tram_contents, devastation_range = rand(0, 1), heavy_impact_range = 2, light_impact_range = 3) //50% chance of gib
-
- else if(prob(9))
- explosion(tram_contents, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 3)
-
- explosion(tram_part, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 3)
- qdel(tram_part)
-
- for(var/obj/machinery/destination_sign/desto as anything in GLOB.tram_signs)
- desto.icon_state = "[desto.base_icon_state][DESTINATION_NOT_IN_SERVICE]"
-
- for(var/obj/machinery/crossing_signal/xing as anything in GLOB.tram_signals)
- xing.set_signal_state(XING_STATE_MALF)
- xing.update_appearance()
-
-/**
- * Handles moving the tram
- *
- * Tells the individual tram parts where to actually go and has an extra safety checks
- * incase multiple inputs get through, preventing conflicting directions and the tram
- * literally ripping itself apart. all of the actual movement is handled by SStramprocess
- * Arguments: destination platform, rapid (bypass some safety checks)
- */
-/datum/lift_master/tram/proc/tram_travel(obj/effect/landmark/tram/destination_platform, rapid = FALSE)
- if(destination_platform == idle_platform)
- return FALSE
-
- travel_direction = get_dir(nav_beacon, destination_platform)
- travel_distance = get_dist(nav_beacon, destination_platform)
- travel_trip_length = travel_distance
- idle_platform = destination_platform
- set_travelling(TRUE)
- set_controls(LIFT_PLATFORM_LOCKED)
- if(rapid) // bypass for unsafe, rapid departure
- dispatch_tram(destination_platform)
- return TRUE
- else
- update_tram_doors(CLOSE_DOORS)
- addtimer(CALLBACK(src, PROC_REF(dispatch_tram), destination_platform), 3 SECONDS)
- return TRUE
-
-/datum/lift_master/tram/proc/dispatch_tram(obj/effect/landmark/tram/destination_platform)
- SEND_SIGNAL(src, COMSIG_TRAM_TRAVEL, idle_platform, destination_platform)
-
- for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_platforms) //only thing everyone needs to know is the new location.
- if(tram_part.travelling) //wee woo wee woo there was a double action queued. damn multi tile structs
- return //we don't care to undo locked controls, though, as that will resolve itself
-
- tram_part.glide_size_override = DELAY_TO_GLIDE_SIZE(horizontal_speed)
- tram_part.set_travelling(TRUE)
-
- next_move = world.time + horizontal_speed
-
- START_PROCESSING(SStramprocess, src)
-
-/datum/lift_master/tram/process(seconds_per_tick)
- if(!travel_distance)
- update_tram_doors(OPEN_DOORS)
- addtimer(CALLBACK(src, PROC_REF(unlock_controls)), 2 SECONDS)
- return PROCESS_KILL
- else if(world.time >= next_move)
- var/start_time = TICK_USAGE
- travel_distance--
-
- move_lift_horizontally(travel_direction)
-
- var/duration = TICK_USAGE_TO_MS(start_time)
- if(slowed_down)
- if(duration <= (SStramprocess.max_time / 2))
- times_below++
- else
- times_below = 0
-
- if(times_below >= SStramprocess.max_cheap_moves)
- horizontal_speed = base_horizontal_speed
- slowed_down = FALSE
- times_below = 0
-
- else if(duration > SStramprocess.max_time)
- times_exceeded++
-
- if(times_exceeded >= SStramprocess.max_exceeding_moves)
- message_admins("The tram at [ADMIN_JMP(lift_platforms[1])] is taking more than [SStramprocess.max_time] milliseconds per movement, halving its movement speed. if this continues to be a problem you can call reset_lift_contents() on the trams lift_master_datum to reset it to its original state and clear added objects")
- horizontal_speed = base_horizontal_speed * 2 //halves its speed
- slowed_down = TRUE
- times_exceeded = 0
- else
- times_exceeded = max(times_exceeded - 1, 0)
-
- next_move = world.time + horizontal_speed
-
-/**
- * Handles unlocking the tram controls for use after moving
- *
- * More safety checks to make sure the tram has actually docked properly
- * at a location before users are allowed to interact with the tram console again.
- * Tram finds its location at this point before fully unlocking controls to the user.
- */
-/datum/lift_master/tram/proc/unlock_controls()
- set_travelling(FALSE)
- set_controls(LIFT_PLATFORM_UNLOCKED)
- for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_platforms) //only thing everyone needs to know is the new location.
- tram_part.set_travelling(FALSE)
-
-
-/datum/lift_master/tram/proc/set_travelling(new_travelling)
- if(travelling == new_travelling)
- return
-
- travelling = new_travelling
- SEND_SIGNAL(src, COMSIG_TRAM_SET_TRAVELLING, travelling)
-
-/**
- * Controls the doors of the tram when it departs and arrives at stations.
- * The tram doors are in a list of airlocks and we apply the proc on that list.
- */
-/datum/lift_master/tram/proc/update_tram_doors(action)
- for(var/obj/machinery/door/window/tram/tram_door as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/door/window/tram))
- if(tram_door.associated_lift != specific_lift_id)
- continue
- set_door_state(tram_door, action)
-
-/datum/lift_master/tram/proc/set_door_state(tram_door, action)
- switch(action)
- if(OPEN_DOORS)
- INVOKE_ASYNC(tram_door, TYPE_PROC_REF(/obj/machinery/door/window/tram, cycle_doors), action)
-
- if(CLOSE_DOORS)
- INVOKE_ASYNC(tram_door, TYPE_PROC_REF(/obj/machinery/door/window/tram, cycle_doors), action)
-
- else
- stack_trace("Tram doors update_tram_doors called with an improper action ([action]).")
-
-/datum/lift_master/tram/proc/set_operational(new_value)
- if(is_operational != new_value)
- is_operational = new_value
-
-/**
- * Returns the closest tram nav beacon to an atom
- *
- * Creates a list of nav beacons in the requested direction
- * and returns the closest to be passed to the industrial_lift
- *
- * Arguments: source: the starting point to find a beacon
- * travel_dir: travel direction in tram form, INBOUND or OUTBOUND
- * beacon_type: what list of beacons we pull from
- */
-/datum/lift_master/tram/proc/closest_nav_in_travel_dir(atom/origin, travel_dir, beacon_type)
- if(!istype(origin) || !origin.z)
- return FALSE
-
- var/list/obj/effect/landmark/tram/nav/inbound_candidates = list()
- var/list/obj/effect/landmark/tram/nav/outbound_candidates = list()
-
- for(var/obj/effect/landmark/tram/nav/candidate_beacon in GLOB.tram_landmarks[beacon_type])
- if(candidate_beacon.z != origin.z || candidate_beacon.z != nav_beacon.z)
- continue
-
- switch(nav_beacon.dir)
- if(EAST, WEST)
- if(candidate_beacon.y != nav_beacon.y)
- continue
- else if(candidate_beacon.x < nav_beacon.x)
- inbound_candidates += candidate_beacon
- else
- outbound_candidates += candidate_beacon
- if(NORTH, SOUTH)
- if(candidate_beacon.x != nav_beacon.x)
- continue
- else if(candidate_beacon.y < nav_beacon.y)
- inbound_candidates += candidate_beacon
- else
- outbound_candidates += candidate_beacon
-
- switch(travel_dir)
- if(INBOUND)
- var/obj/effect/landmark/tram/nav/selected = get_closest_atom(/obj/effect/landmark/tram/nav, inbound_candidates, origin)
- if(selected)
- return selected
- stack_trace("No inbound beacon candidate found for [origin]. Cancelling dispatch.")
- return FALSE
-
- if(OUTBOUND)
- var/obj/effect/landmark/tram/nav/selected = get_closest_atom(/obj/effect/landmark/tram/nav, outbound_candidates, origin)
- if(selected)
- return selected
- stack_trace("No outbound beacon candidate found for [origin]. Cancelling dispatch.")
- return FALSE
-
- else
- stack_trace("Tram receieved invalid travel direction [travel_dir]. Cancelling dispatch.")
-
- return FALSE
-
-/**
- * Moves the tram when hit by an immovable rod
- *
- * Tells the individual tram parts where to actually go and has an extra safety checks
- * incase multiple inputs get through, preventing conflicting directions and the tram
- * literally ripping itself apart. all of the actual movement is handled by SStramprocess
- *
- * Arguments: collided_rod (the immovable rod that hit the tram)
- * Return: push_destination (the landmark /obj/effect/landmark/tram/nav that the tram is being pushed to due to the rod's trajectory)
- */
-/datum/lift_master/tram/proc/rod_collision(obj/effect/immovablerod/collided_rod)
- if(!is_operational)
- return
- var/rod_velocity_sign
- // Determine inbound or outbound
- if(collided_rod.dir & (NORTH|SOUTH))
- rod_velocity_sign = collided_rod.dir & NORTH ? OUTBOUND : INBOUND
- else
- rod_velocity_sign = collided_rod.dir & EAST ? OUTBOUND : INBOUND
-
- var/obj/effect/landmark/tram/nav/push_destination = closest_nav_in_travel_dir(origin = nav_beacon, travel_dir = rod_velocity_sign, beacon_type = IMMOVABLE_ROD_DESTINATIONS)
- if(!push_destination)
- return
- travel_direction = get_dir(nav_beacon, push_destination)
- travel_distance = get_dist(nav_beacon, push_destination)
- travel_trip_length = travel_distance
- idle_platform = push_destination
- // Don't bother processing crossing signals, where this tram's going there are no signals
- for(var/obj/machinery/crossing_signal/xing as anything in GLOB.tram_signals)
- xing.temp_malfunction()
- priority_announce("In a turn of rather peculiar events, it appears that [GLOB.station_name] has struck an immovable rod. (Don't ask us where it came from.) This has led to a station brakes failure on one of the tram platforms.\n\n\
- Our diligent team of engineers have been informed and they're rushing over - although not quite at the speed of our recently flying tram.\n\n\
- So while we all look in awe at the universe's mysterious sense of humour, please stand clear of the tracks and remember to stand behind the yellow line.", "Braking News")
- set_travelling(TRUE)
- set_controls(LIFT_PLATFORM_LOCKED)
- dispatch_tram(destination_platform = push_destination)
- set_operational(FALSE)
- return push_destination
diff --git a/code/modules/industrial_lift/tram/tram_machinery.dm b/code/modules/industrial_lift/tram/tram_machinery.dm
deleted file mode 100644
index 2e4399cce7028..0000000000000
--- a/code/modules/industrial_lift/tram/tram_machinery.dm
+++ /dev/null
@@ -1,777 +0,0 @@
-GLOBAL_LIST_EMPTY(tram_signals)
-GLOBAL_LIST_EMPTY(tram_signs)
-GLOBAL_LIST_EMPTY(tram_doors)
-
-/obj/machinery/computer/tram_controls
- name = "tram controls"
- desc = "An interface for the tram that lets you tell the tram where to go and hopefully it makes it there. I'm here to describe the controls to you, not to inspire confidence."
- icon_state = "tram_controls"
- base_icon_state = "tram_"
- icon_screen = "tram_Central Wing_idle"
- icon_keyboard = null
- layer = SIGN_LAYER
- density = FALSE
- circuit = /obj/item/circuitboard/computer/tram_controls
- flags_1 = NODECONSTRUCT_1 | SUPERMATTER_IGNORES_1
- resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
- interaction_flags_machine = INTERACT_MACHINE_ALLOW_SILICON|INTERACT_MACHINE_SET_MACHINE
- light_color = COLOR_BLUE_LIGHT
- light_range = 0 //we dont want to spam SSlighting with source updates every movement
-
- ///Weakref to the tram piece we control
- var/datum/weakref/tram_ref
-
- var/specific_lift_id = MAIN_STATION_TRAM
-
-/obj/machinery/computer/tram_controls/Initialize(mapload, obj/item/circuitboard/C)
- . = ..()
- AddComponent(/datum/component/usb_port, list(/obj/item/circuit_component/tram_controls))
- return INITIALIZE_HINT_LATELOAD
-
-/obj/machinery/computer/tram_controls/LateInitialize()
- . = ..()
- find_tram()
-
- var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
- if(tram_part)
- RegisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING, PROC_REF(update_tram_display))
- icon_screen = "[base_icon_state][tram_part.idle_platform.name]_idle"
- update_appearance(UPDATE_ICON)
-
-/**
- * Finds the tram from the console
- *
- * Locates tram parts in the lift global list after everything is done.
- */
-/obj/machinery/computer/tram_controls/proc/find_tram()
- for(var/datum/lift_master/lift as anything in GLOB.active_lifts_by_type[TRAM_LIFT_ID])
- if(lift.specific_lift_id == specific_lift_id)
- tram_ref = WEAKREF(lift)
-
-/obj/machinery/computer/tram_controls/ui_state(mob/user)
- return GLOB.not_incapacitated_state
-
-/obj/machinery/computer/tram_controls/ui_status(mob/user,/datum/tgui/ui)
- var/datum/lift_master/tram/tram = tram_ref?.resolve()
-
- if(tram?.travelling)
- return UI_CLOSE
- if(!in_range(user, src) && !isobserver(user))
- return UI_CLOSE
- return ..()
-
-/obj/machinery/computer/tram_controls/ui_interact(mob/user, datum/tgui/ui)
- . = ..()
- if(!user.can_read(src, reading_check_flags = READING_CHECK_LITERACY))
- try_illiterate_movement(user)
- return
- ui = SStgui.try_update_ui(user, src, ui)
- if(!ui)
- ui = new(user, src, "TramControl", name)
- ui.open()
-
-/// Traverse to a random location after some time
-/obj/machinery/computer/tram_controls/proc/try_illiterate_movement(mob/user)
- var/datum/lift_master/tram/tram_lift = tram_ref?.resolve()
- if (!tram_lift || tram_lift.travelling)
- return
- user.visible_message(span_notice("[user] starts mashing buttons at random!"))
- if(!do_after(user, 5 SECONDS, target = src))
- return
- if (!tram_lift || tram_lift.travelling)
- to_chat(user, span_warning("The screen displays a flashing error message, but you can't comprehend it."))
- return // Broke or started moving during progress bar
- var/list/all_destinations = GLOB.tram_landmarks[specific_lift_id] || list()
- var/list/possible_destinations = all_destinations.Copy() - tram_lift.idle_platform
- if (!length(possible_destinations))
- to_chat(user, span_warning("The screen displays a flashing error message, but you can't comprehend it."))
- return // No possible places to end up
- try_send_tram(pick(possible_destinations))
-
-/obj/machinery/computer/tram_controls/ui_data(mob/user)
- var/datum/lift_master/tram/tram_lift = tram_ref?.resolve()
- var/list/data = list()
- data["moving"] = tram_lift?.travelling
- data["broken"] = tram_lift ? FALSE : TRUE
- var/obj/effect/landmark/tram/current_loc = tram_lift?.idle_platform
- if(current_loc)
- data["tram_location"] = current_loc.name
- return data
-
-/obj/machinery/computer/tram_controls/ui_static_data(mob/user)
- var/list/data = list()
- data["destinations"] = get_destinations()
- return data
-
-/**
- * Finds the destinations for the tram console gui
- *
- * Pulls tram landmarks from the landmark gobal list
- * and uses those to show the proper icons and destination
- * names for the tram console gui.
- */
-/obj/machinery/computer/tram_controls/proc/get_destinations()
- . = list()
- for(var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks[specific_lift_id])
- var/list/this_destination = list()
- this_destination["name"] = destination.name
- this_destination["dest_icons"] = destination.tgui_icons
- this_destination["id"] = destination.platform_code
- . += list(this_destination)
-
-/obj/machinery/computer/tram_controls/ui_act(action, params)
- . = ..()
- if (.)
- return
-
- switch (action)
- if ("send")
- var/obj/effect/landmark/tram/destination_platform
- for (var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks[specific_lift_id])
- if(destination.platform_code == params["destination"])
- destination_platform = destination
- break
-
- if (!destination_platform)
- return FALSE
-
- return try_send_tram(destination_platform)
-
-/// Attempts to sends the tram to the given destination
-/obj/machinery/computer/tram_controls/proc/try_send_tram(obj/effect/landmark/tram/destination_platform)
- var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
- if(!tram_part)
- return FALSE
- if(tram_part.controls_locked || tram_part.travelling) // someone else started already
- return FALSE
- if(!tram_part.tram_travel(destination_platform))
- return FALSE // lift_master failure
- say("The next station is: [destination_platform.name]")
- update_appearance()
- return TRUE
-
-/obj/machinery/computer/tram_controls/proc/update_tram_display(obj/effect/landmark/tram/idle_platform, travelling)
- SIGNAL_HANDLER
- var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
- if(travelling)
- icon_screen = "[base_icon_state][tram_part.idle_platform.name]_active"
- else
- icon_screen = "[base_icon_state][tram_part.idle_platform.name]_idle"
- update_appearance(UPDATE_ICON)
- return PROCESS_KILL
-
-/obj/machinery/computer/tram_controls/power_change() // Change tram operating status on power loss/recovery
- . = ..()
- var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
- update_operating()
- if(tram_part)
- if(!tram_part.travelling)
- if(is_operational)
- for(var/obj/machinery/crossing_signal/xing as anything in GLOB.tram_signals)
- xing.set_signal_state(XING_STATE_MALF, TRUE)
- for(var/obj/machinery/destination_sign/desto as anything in GLOB.tram_signs)
- desto.icon_state = "[desto.base_icon_state][DESTINATION_OFF]"
- desto.update_appearance()
- else
- for(var/obj/machinery/crossing_signal/xing as anything in GLOB.tram_signals)
- xing.set_signal_state(XING_STATE_MALF, TRUE)
- for(var/obj/machinery/destination_sign/desto as anything in GLOB.tram_signs)
- desto.icon_state = "[desto.base_icon_state][DESTINATION_NOT_IN_SERVICE]"
- desto.update_appearance()
-
-/obj/machinery/computer/tram_controls/proc/update_operating() // Pass the operating status from the controls to the lift_master
- var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
- if(tram_part)
- if(machine_stat & NOPOWER)
- tram_part.is_operational = FALSE
- else
- tram_part.is_operational = TRUE
-
-/obj/item/circuit_component/tram_controls
- display_name = "Tram Controls"
-
- /// The destination to go
- var/datum/port/input/new_destination
-
- /// The trigger to send the tram
- var/datum/port/input/trigger_move
-
- /// The current location
- var/datum/port/output/location
-
- /// Whether or not the tram is moving
- var/datum/port/output/travelling_output
-
- /// The tram controls computer (/obj/machinery/computer/tram_controls)
- var/obj/machinery/computer/tram_controls/computer
-
-/obj/item/circuit_component/tram_controls/populate_ports()
- new_destination = add_input_port("Destination", PORT_TYPE_STRING, trigger = null)
- trigger_move = add_input_port("Send Tram", PORT_TYPE_SIGNAL)
-
- location = add_output_port("Location", PORT_TYPE_STRING)
- travelling_output = add_output_port("Travelling", PORT_TYPE_NUMBER)
-
-/obj/item/circuit_component/tram_controls/register_usb_parent(atom/movable/shell)
- . = ..()
- if (istype(shell, /obj/machinery/computer/tram_controls))
- computer = shell
- var/datum/lift_master/tram/tram_part = computer.tram_ref?.resolve()
- RegisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING, PROC_REF(on_tram_set_travelling))
- RegisterSignal(tram_part, COMSIG_TRAM_TRAVEL, PROC_REF(on_tram_travel))
-
-/obj/item/circuit_component/tram_controls/unregister_usb_parent(atom/movable/shell)
- var/datum/lift_master/tram/tram_part = computer.tram_ref?.resolve()
- computer = null
- UnregisterSignal(tram_part, list(COMSIG_TRAM_SET_TRAVELLING, COMSIG_TRAM_TRAVEL))
- return ..()
-
-/obj/item/circuit_component/tram_controls/input_received(datum/port/input/port)
- if (!COMPONENT_TRIGGERED_BY(trigger_move, port))
- return
-
- if (isnull(computer))
- return
-
- if (!computer.powered())
- return
-
- var/destination
- for(var/obj/effect/landmark/tram/possible_destination as anything in GLOB.tram_landmarks[computer.specific_lift_id])
- if(possible_destination.name == new_destination.value)
- destination = possible_destination
- break
-
- if (!destination)
- return
-
- computer.try_send_tram(destination)
-
-/obj/item/circuit_component/tram_controls/proc/on_tram_set_travelling(datum/source, travelling)
- SIGNAL_HANDLER
- travelling_output.set_output(travelling)
-
-/obj/item/circuit_component/tram_controls/proc/on_tram_travel(datum/source, obj/effect/landmark/tram/idle_platform, obj/effect/landmark/tram/destination_platform)
- SIGNAL_HANDLER
- location.set_output(destination_platform.name)
-
-/// Pedestrian crossing signal for tram
-/obj/machinery/crossing_signal
- name = "crossing signal"
- desc = "Indicates to pedestrians if it's safe to cross the tracks."
- icon = 'icons/obj/machines/crossing_signal.dmi'
- base_icon_state = "crossing-"
- plane = GAME_PLANE_UPPER
- max_integrity = 250
- integrity_failure = 0.25
- idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 2.4
- active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 0.74
- anchored = TRUE
- density = FALSE
- // pointless if it only takes 2 seconds to cross but updates every 2 seconds
- subsystem_type = /datum/controller/subsystem/processing/fastprocess
- light_range = 1.5
- light_power = 3
- light_color = LIGHT_COLOR_BABY_BLUE
- luminosity = 1
-
- /// green, amber, or red for tram, blue if it's emag, tram missing, etc.
- var/signal_state = XING_STATE_MALF
- /// The ID of the tram we control
- var/tram_id = MAIN_STATION_TRAM
- /// Weakref to the tram piece we control
- var/datum/weakref/tram_ref
-
- /** Proximity thresholds for crossing signal states
- *
- * The proc that checks the distance between the tram and crossing signal uses these vars to determine the distance between tram and signal to change
- * colors. The numbers are specifically set for Tramstation. If we get another map with crossing signals we'll have to probably subtype it or something.
- * If the value is set too high, it will cause the lights to turn red when the tram arrives at another station. You want to optimize the amount of
- * warning without turning it red unnessecarily.
- *
- * Red: decent chance of getting hit, but if you're quick it's a decent gamble.
- * Amber: slow people may be in danger.
- */
- var/amber_distance_threshold = XING_DISTANCE_AMBER
- var/red_distance_threshold = XING_DISTANCE_RED
- /// If the signal is facing east or west
- var/signal_direction
- /// Inbound station
- var/inbound
- /// Outbound station
- var/outbound
- /// Is the signal malfunctioning?
- var/malfunctioning = FALSE
-
-/** Crossing signal subtypes
- *
- * Each map will have a different amount of tiles between stations, so adjust the signals here based on the map.
- * The distance is calculated from the bottom left corner of the tram,
- * so signals on the east side have their distance reduced by the tram length, in this case 10 for Tramstation.
-*/
-/obj/machinery/crossing_signal/northwest
- icon_state = "crossing-base-right"
- signal_direction = XING_SIGNAL_DIRECTION_WEST
- pixel_x = -32
- pixel_y = -1
-
-/obj/machinery/crossing_signal/northeast
- icon_state = "crossing-base-left"
- signal_direction = XING_SIGNAL_DIRECTION_EAST
- pixel_x = -2
- pixel_y = -1
-
-/obj/machinery/crossing_signal/southwest
- icon_state = "crossing-base-right"
- signal_direction = XING_SIGNAL_DIRECTION_WEST
- pixel_x = -32
- pixel_y = 20
-
-/obj/machinery/crossing_signal/southeast
- icon_state = "crossing-base-left"
- signal_direction = XING_SIGNAL_DIRECTION_EAST
- pixel_x = -2
- pixel_y = 20
-
-/obj/machinery/static_signal
- name = "crossing signal"
- desc = "Indicates to pedestrians if it's safe to cross the tracks."
- icon = 'icons/obj/machines/crossing_signal.dmi'
- icon_state = "static-left-on"
- base_icon_state = "static-left-"
- plane = GAME_PLANE_UPPER
- max_integrity = 250
- integrity_failure = 0.25
- idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 2.4
- active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 0.74
- anchored = TRUE
- density = FALSE
- light_range = 1.5
- light_power = 3
- light_color = COLOR_VIBRANT_LIME
- luminosity = 1
-
-/obj/machinery/static_signal/northwest
- icon_state = "static-right-on"
- base_icon_state = "static-right-"
- pixel_x = -32
- pixel_y = -1
-
-/obj/machinery/static_signal/northeast
- pixel_x = -2
- pixel_y = -1
-
-/obj/machinery/static_signal/southwest
- icon_state = "static-right-on"
- base_icon_state = "static-right-"
- pixel_x = -32
- pixel_y = 20
-
-/obj/machinery/static_signal/southeast
- pixel_x = -2
- pixel_y = 20
-
-/obj/machinery/crossing_signal/Initialize(mapload)
- . = ..()
- return INITIALIZE_HINT_LATELOAD
-
-/obj/machinery/crossing_signal/LateInitialize()
- . = ..()
- find_tram()
-
- var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
- if(tram_part)
- RegisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING, PROC_REF(on_tram_travelling))
- GLOB.tram_signals += src
-
-/obj/machinery/crossing_signal/Destroy()
- GLOB.tram_signals -= src
- . = ..()
-
- var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
- if(tram_part)
- UnregisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING)
-
-/obj/machinery/crossing_signal/emag_act(mob/user, obj/item/card/emag/emag_card)
- if(obj_flags & EMAGGED)
- return FALSE
- balloon_alert(user, "disabled motion sensors")
- if(signal_state != XING_STATE_MALF)
- set_signal_state(XING_STATE_MALF)
- obj_flags |= EMAGGED
- return TRUE
-
-/obj/machinery/crossing_signal/proc/start_malfunction()
- if(signal_state != XING_STATE_MALF)
- malfunctioning = TRUE
- set_signal_state(XING_STATE_MALF)
-
-/obj/machinery/crossing_signal/proc/end_malfunction()
- if(obj_flags & EMAGGED)
- return
-
- malfunctioning = FALSE
- process()
-
-/obj/machinery/crossing_signal/proc/temp_malfunction()
- start_malfunction()
- addtimer(CALLBACK(src, PROC_REF(end_malfunction)), 15 SECONDS)
-
-/**
- * Finds the tram, just like the tram computer
- *
- * Locates tram parts in the lift global list after everything is done.
- */
-/obj/machinery/crossing_signal/proc/find_tram()
- for(var/datum/lift_master/tram/tram as anything in GLOB.active_lifts_by_type[TRAM_LIFT_ID])
- if(tram.specific_lift_id != tram_id)
- continue
- tram_ref = WEAKREF(tram)
- break
-
-/**
- * Only process if the tram is actually moving
- */
-/obj/machinery/crossing_signal/proc/on_tram_travelling(datum/source, travelling)
- SIGNAL_HANDLER
-
- update_operating()
-
-/obj/machinery/crossing_signal/on_set_is_operational()
- . = ..()
-
- update_operating()
-
-/**
- * Update processing state.
- *
- * Returns whether we are still processing.
- */
-/obj/machinery/crossing_signal/proc/update_operating()
-
- use_power(idle_power_usage)
-
- // Emagged crossing signals don't update
- if(obj_flags & EMAGGED)
- return
- // Malfunctioning signals don't update
- if(malfunctioning)
- return
- // Immediately process for snappy feedback
- var/should_process = process() != PROCESS_KILL
- if(should_process)
- begin_processing()
- return
- end_processing()
-
-/obj/machinery/crossing_signal/process()
-
- var/datum/lift_master/tram/tram = tram_ref?.resolve()
-
- // Check for stopped states.
- if(!tram || !is_operational || !tram.is_operational || !inbound || !outbound)
- // Tram missing, we lost power, or something isn't right
- // Throw the error message (blue)
- set_signal_state(XING_STATE_MALF, force = !is_operational)
- return PROCESS_KILL
-
- use_power(active_power_usage)
-
- var/obj/structure/industrial_lift/tram/tram_part = tram.return_closest_platform_to(src)
-
- if(QDELETED(tram_part))
- set_signal_state(XING_STATE_MALF, force = !is_operational)
- return PROCESS_KILL
-
- // Everything will be based on position and travel direction
- var/signal_pos
- var/tram_pos
- var/tram_velocity_sign // 1 for positive axis movement, -1 for negative
- // Try to be agnostic about N-S vs E-W movement
- if(tram.travel_direction & (NORTH|SOUTH))
- signal_pos = y
- tram_pos = tram_part.y
- tram_velocity_sign = tram.travel_direction & NORTH ? 1 : -1
- else
- signal_pos = x
- tram_pos = tram_part.x
- tram_velocity_sign = tram.travel_direction & EAST ? 1 : -1
-
- // How far away are we? negative if already passed.
- var/approach_distance = tram_velocity_sign * (signal_pos - (tram_pos + (XING_DEFAULT_TRAM_LENGTH * 0.5)))
-
- // Check for stopped state.
- // Will kill the process since tram starting up will restart process.
- if(!tram.travelling)
- set_signal_state(XING_STATE_GREEN)
- return PROCESS_KILL
-
- // Check if tram is driving away from us.
- if(approach_distance < 0)
- // driving away. Green. In fact, in order to reverse, it'll have to stop, so let's go ahead and kill.
- set_signal_state(XING_STATE_GREEN)
- return PROCESS_KILL
-
- // Check the tram's terminus station.
- // INBOUND 1 < 2 < 3
- // OUTBOUND 1 > 2 > 3
- if(tram.travel_direction & WEST && inbound < tram.idle_platform.platform_code)
- set_signal_state(XING_STATE_GREEN)
- return PROCESS_KILL
- if(tram.travel_direction & EAST && outbound > tram.idle_platform.platform_code)
- set_signal_state(XING_STATE_GREEN)
- return PROCESS_KILL
-
- // Finally the interesting part where it's ACTUALLY approaching
- if(approach_distance <= red_distance_threshold)
- set_signal_state(XING_STATE_RED)
- return
- if(approach_distance <= amber_distance_threshold)
- set_signal_state(XING_STATE_AMBER)
- return
- set_signal_state(XING_STATE_GREEN)
-
-/**
- * Set the signal state and update appearance.
- *
- * Arguments:
- * new_state - the new state (XING_STATE_RED, etc)
- * force_update - force appearance to update even if state didn't change.
- */
-/obj/machinery/crossing_signal/proc/set_signal_state(new_state, force = FALSE)
- if(new_state == signal_state && !force)
- return
-
- signal_state = new_state
- update_appearance()
-
-/obj/machinery/crossing_signal/update_appearance(updates)
- . = ..()
-
- if(!is_operational)
- set_light(l_on = FALSE)
- return
-
- var/new_color
- switch(signal_state)
- if(XING_STATE_MALF)
- new_color = LIGHT_COLOR_BABY_BLUE
- if(XING_STATE_GREEN)
- new_color = LIGHT_COLOR_VIVID_GREEN
- if(XING_STATE_AMBER)
- new_color = LIGHT_COLOR_BRIGHT_YELLOW
- else
- new_color = LIGHT_COLOR_FLARE
-
- set_light(l_on = TRUE, l_color = new_color)
-
-/obj/machinery/crossing_signal/update_overlays()
- . = ..()
-
- if(!is_operational)
- return
-
- if(!signal_direction) //Base type doesnt have directions set
- return
-
- var/lights_overlay = "[base_icon_state][signal_direction][signal_state]"
-
- . += mutable_appearance(icon, lights_overlay)
- . += emissive_appearance(icon, "[lights_overlay]e", offset_spokesman = src, alpha = src.alpha)
-
-/obj/machinery/static_signal/power_change()
- ..()
- if(!is_operational)
- icon_state = "[base_icon_state]off"
- set_light(l_on = FALSE)
- return
-
- icon_state = "[base_icon_state]on"
- set_light(l_on = TRUE)
-
-/obj/machinery/destination_sign
- name = "destination sign"
- desc = "A display to show you what direction the tram is travelling."
- icon = 'icons/obj/machines/tram_sign.dmi'
- icon_state = "desto_off"
- base_icon_state = "desto_"
- idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 1.2
- active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 0.47
- anchored = TRUE
- density = FALSE
- subsystem_type = /datum/controller/subsystem/processing/fastprocess
-
- /// The ID of the tram we're indicating
- var/tram_id = MAIN_STATION_TRAM
- /// Weakref to the tram piece we indicate
- var/datum/weakref/tram_ref
- /// The last destination we were at
- var/previous_destination
- /// The light mask overlay we use
- var/light_mask
- /// Is this sign malfunctioning?
- var/malfunctioning = FALSE
- /// A default list of possible sign states
- var/static/list/sign_states = list()
-
-/obj/machinery/destination_sign/north
- layer = BELOW_OBJ_LAYER
-
-/obj/machinery/destination_sign/south
- plane = WALL_PLANE_UPPER
- layer = BELOW_OBJ_LAYER
-
-/obj/machinery/destination_sign/indicator
- icon_state = "indicator_off"
- base_icon_state = "indicator_"
- light_range = 1.5
- light_color = LIGHT_COLOR_DARK_BLUE
- light_mask = "indicator_off_e"
-
-/obj/machinery/destination_sign/Initialize(mapload)
- . = ..()
- return INITIALIZE_HINT_LATELOAD
-
-/obj/machinery/destination_sign/LateInitialize()
- . = ..()
- find_tram()
-
- var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
- if(tram_part)
- RegisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING, PROC_REF(on_tram_travelling))
- GLOB.tram_signs += src
-
- sign_states = list(
- "[DESTINATION_WEST_ACTIVE]",
- "[DESTINATION_WEST_IDLE]",
- "[DESTINATION_EAST_ACTIVE]",
- "[DESTINATION_EAST_IDLE]",
- "[DESTINATION_CENTRAL_IDLE]",
- "[DESTINATION_CENTRAL_EASTBOUND_ACTIVE]",
- "[DESTINATION_CENTRAL_WESTBOUND_ACTIVE]",
- )
-
-/obj/machinery/destination_sign/Destroy()
- GLOB.tram_signs -= src
- . = ..()
-
- var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
- if(tram_part)
- UnregisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING)
-
-/obj/machinery/destination_sign/proc/find_tram()
- for(var/datum/lift_master/tram/tram as anything in GLOB.active_lifts_by_type[TRAM_LIFT_ID])
- if(tram.specific_lift_id != tram_id)
- continue
- tram_ref = WEAKREF(tram)
- break
-
-/obj/machinery/destination_sign/proc/on_tram_travelling(datum/source, travelling)
- SIGNAL_HANDLER
- update_sign()
- INVOKE_ASYNC(src, TYPE_PROC_REF(/datum, process))
-
-/obj/machinery/destination_sign/proc/update_operating()
- // Immediately process for snappy feedback
- var/should_process = process() != PROCESS_KILL
- if(should_process)
- begin_processing()
- return
- end_processing()
-
-/obj/machinery/destination_sign/proc/update_sign()
- var/datum/lift_master/tram/tram = tram_ref?.resolve()
-
- if(!tram || !tram.is_operational)
- icon_state = "[base_icon_state][DESTINATION_NOT_IN_SERVICE]"
- light_mask = "[base_icon_state][DESTINATION_NOT_IN_SERVICE]_e"
- update_appearance()
- return PROCESS_KILL
-
- use_power(active_power_usage)
-
- if(malfunctioning)
- icon_state = "[base_icon_state][pick(sign_states)]"
- light_mask = "[base_icon_state][pick(sign_states)]_e"
- update_appearance()
- return PROCESS_KILL
-
- if(!tram.travelling)
- if(istype(tram.idle_platform, /obj/effect/landmark/tram/platform/tramstation/west))
- icon_state = "[base_icon_state][DESTINATION_WEST_IDLE]"
- light_mask = "[base_icon_state][DESTINATION_WEST_IDLE]_e"
- previous_destination = tram.idle_platform
- update_appearance()
- return PROCESS_KILL
-
- if(istype(tram.idle_platform, /obj/effect/landmark/tram/platform/tramstation/central))
- icon_state = "[base_icon_state][DESTINATION_CENTRAL_IDLE]"
- light_mask = "[base_icon_state][DESTINATION_CENTRAL_IDLE]_e"
- previous_destination = tram.idle_platform
- update_appearance()
- return PROCESS_KILL
-
- if(istype(tram.idle_platform, /obj/effect/landmark/tram/platform/tramstation/east))
- icon_state = "[base_icon_state][DESTINATION_EAST_IDLE]"
- light_mask = "[base_icon_state][DESTINATION_EAST_IDLE]_e"
- previous_destination = tram.idle_platform
- update_appearance()
- return PROCESS_KILL
-
- if(istype(tram.idle_platform, /obj/effect/landmark/tram/platform/tramstation/west))
- icon_state = "[base_icon_state][DESTINATION_WEST_ACTIVE]"
- light_mask = "[base_icon_state][DESTINATION_WEST_ACTIVE]_e"
- update_appearance()
- return PROCESS_KILL
-
- if(istype(tram.idle_platform, /obj/effect/landmark/tram/platform/tramstation/central))
- if(istype(previous_destination, /obj/effect/landmark/tram/platform/tramstation/west))
- icon_state = "[base_icon_state][DESTINATION_CENTRAL_EASTBOUND_ACTIVE]"
- light_mask = "[base_icon_state][DESTINATION_CENTRAL_EASTBOUND_ACTIVE]_e"
- if(istype(previous_destination, /obj/effect/landmark/tram/platform/tramstation/east))
- icon_state = "[base_icon_state][DESTINATION_CENTRAL_WESTBOUND_ACTIVE]"
- light_mask = "[base_icon_state][DESTINATION_CENTRAL_WESTBOUND_ACTIVE]_e"
- update_appearance()
- return PROCESS_KILL
-
- if(istype(tram.idle_platform, /obj/effect/landmark/tram/platform/tramstation/east))
- icon_state = "[base_icon_state][DESTINATION_EAST_ACTIVE]"
- light_mask = "[base_icon_state][DESTINATION_EAST_ACTIVE]_e"
- update_appearance()
- return PROCESS_KILL
-
-/obj/machinery/destination_sign/update_overlays()
- . = ..()
- if(!light_mask)
- return
-
- if(!(machine_stat & (NOPOWER|BROKEN)) && !panel_open)
- . += emissive_appearance(icon, light_mask, src, alpha = alpha)
-
-/obj/machinery/button/tram
- name = "tram request"
- desc = "A button for calling the tram. It has a speakerbox in it with some internals."
- base_icon_state = "tram"
- icon_state = "tram"
- light_color = LIGHT_COLOR_DARK_BLUE
- can_alter_skin = FALSE
- device_type = /obj/item/assembly/control/tram
- req_access = list()
- id = 1
- /// The specific lift id of the tram we're calling.
- var/lift_id = MAIN_STATION_TRAM
-
-/obj/machinery/button/tram/setup_device()
- var/obj/item/assembly/control/tram/tram_device = device
- tram_device.initial_id = id
- tram_device.specific_lift_id = lift_id
- return ..()
-
-/obj/machinery/button/tram/examine(mob/user)
- . = ..()
- . += span_notice("There's a small inscription on the button...")
- . += span_notice("THIS CALLS THE TRAM! IT DOES NOT OPERATE IT! The console on the tram tells it where to go!")
-
-MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/computer/tram_controls, 0)
-MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/destination_sign/indicator, 32)
-MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/button/tram, 32)
diff --git a/code/modules/industrial_lift/tram/tram_override_objects.dm b/code/modules/industrial_lift/tram/tram_override_objects.dm
deleted file mode 100644
index 57a0368d6bc59..0000000000000
--- a/code/modules/industrial_lift/tram/tram_override_objects.dm
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * the tram has a few objects mapped onto it at roundstart, by default many of those objects have unwanted properties
- * for example grilles and windows have the atmos_sensitive element applied to them, which makes them register to
- * themselves moving to re register signals onto the turf via connect_loc. this is bad and dumb since it makes the tram
- * more expensive to move.
- *
- * if you map something on to the tram, make SURE if possible that it doesnt have anything reacting to its own movement
- * it will make the tram more expensive to move and we dont want that because we dont want to return to the days where
- * the tram took a third of the tick per movement when its just carrying its default mapped in objects
- */
-
-/obj/structure/grille/tram/Initialize(mapload)
- . = ..()
- RemoveElement(/datum/element/atmos_sensitive, mapload)
- //atmos_sensitive applies connect_loc which 1. reacts to movement in order to 2. unregister and register signals to
- //the old and new locs. we dont want that, pretend these grilles and windows are plastic or something idk
-
-/obj/structure/window/reinforced/tram/Initialize(mapload, direct)
- . = ..()
- RemoveElement(/datum/element/atmos_sensitive, mapload)
-
-/turf/open/floor/glass/reinforced/tram/Initialize(mapload)
- . = ..()
- RemoveElement(/datum/element/atmos_sensitive, mapload)
-
-/turf/open/floor/glass/reinforced/tram
- name = "tram bridge"
- desc = "It shakes a bit when you step, but lets you cross between sides quickly!"
diff --git a/code/modules/industrial_lift/tram/tram_remote.dm b/code/modules/industrial_lift/tram/tram_remote.dm
deleted file mode 100644
index abb3b430e1813..0000000000000
--- a/code/modules/industrial_lift/tram/tram_remote.dm
+++ /dev/null
@@ -1,150 +0,0 @@
-#define TRAMCTRL_FAST 1
-#define TRAMCTRL_SAFE 0
-
-/obj/item/tram_remote
- icon_state = "tramremote_nis"
- inhand_icon_state = "electronic"
- lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
- righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
- icon = 'icons/obj/device.dmi'
- name = "tram remote"
- desc = "A remote control that can be linked to a tram. This can only go well."
- w_class = WEIGHT_CLASS_TINY
- ///desired tram direction
- var/direction = INBOUND
- ///fast and fun, or safe and boring
- var/mode = TRAMCTRL_FAST
- ///weakref to the tram piece we control
- var/datum/weakref/tram_ref
- ///cooldown for the remote
- COOLDOWN_DECLARE(tram_remote)
-
-/obj/item/tram_remote/Initialize(mapload)
- . = ..()
- register_context()
-
-/obj/item/tram_remote/add_context(atom/source, list/context, obj/item/held_item, mob/user)
- if(!tram_ref)
- context[SCREENTIP_CONTEXT_LMB] = "Link tram"
- return CONTEXTUAL_SCREENTIP_SET
- context[SCREENTIP_CONTEXT_LMB] = "Dispatch tram"
- context[SCREENTIP_CONTEXT_RMB] = "Change direction"
- context[SCREENTIP_CONTEXT_CTRL_LMB] = "Toggle door safeties"
- return CONTEXTUAL_SCREENTIP_SET
-
-///set tram control direction
-/obj/item/tram_remote/attack_self_secondary(mob/user)
- switch(direction)
- if(INBOUND)
- direction = OUTBOUND
- if(OUTBOUND)
- direction = INBOUND
- update_appearance()
- balloon_alert(user, "[direction ? "< inbound" : "outbound >"]")
- return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
-
-///set safety bypass
-/obj/item/tram_remote/CtrlClick(mob/user)
- switch(mode)
- if(TRAMCTRL_SAFE)
- mode = TRAMCTRL_FAST
- if(TRAMCTRL_FAST)
- mode = TRAMCTRL_SAFE
- update_appearance()
- balloon_alert(user, "mode: [mode ? "fast" : "safe"]")
-
-/obj/item/tram_remote/examine(mob/user)
- . = ..()
- if(!tram_ref)
- . += "There is an X showing on the display."
- . += "Left-click a tram request button to link."
- return
- . += "The arrow on the display is pointing [direction ? "inbound" : "outbound"]."
- . += "The rapid mode light is [mode ? "on" : "off"]."
- if (!COOLDOWN_FINISHED(src, tram_remote))
- . += "The number on the display shows [DisplayTimeText(COOLDOWN_TIMELEFT(src, tram_remote), 1)]."
- else
- . += "The display indicates ready."
- . += "Left-click to dispatch tram."
- . += "Right-click to toggle direction."
- . += "Ctrl-click to toggle safety bypass."
-
-/obj/item/tram_remote/update_icon_state()
- . = ..()
- if(!tram_ref)
- icon_state = "tramremote_nis"
- return
- switch(direction)
- if(INBOUND)
- icon_state = "tramremote_ib"
- if(OUTBOUND)
- icon_state = "tramremote_ob"
-
-/obj/item/tram_remote/update_overlays()
- . = ..()
- if(mode == TRAMCTRL_FAST)
- . += mutable_appearance(icon, "tramremote_emag")
-
-/obj/item/tram_remote/attack_self(mob/user)
- if (!COOLDOWN_FINISHED(src, tram_remote))
- balloon_alert(user, "cooldown: [DisplayTimeText(COOLDOWN_TIMELEFT(src, tram_remote), 1)]")
- return FALSE
- if(try_force_tram(user))
- COOLDOWN_START(src, tram_remote, 2 MINUTES)
-
-///send our selected commands to the tram
-/obj/item/tram_remote/proc/try_force_tram(mob/user)
- var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
- if(!tram_part)
- balloon_alert(user, "no tram linked!")
- return FALSE
- if(tram_part.controls_locked || tram_part.travelling) // someone else started already
- balloon_alert(user, "tram busy!")
- return FALSE
- var/tram_id = tram_part.specific_lift_id
- var/destination_platform = null
- var/platform = 0
- switch(direction)
- if(INBOUND)
- platform = clamp(tram_part.idle_platform.platform_code - 1, 1, INFINITY)
- if(OUTBOUND)
- platform = clamp(tram_part.idle_platform.platform_code + 1, 1, INFINITY)
- if(platform == tram_part.idle_platform.platform_code)
- balloon_alert(user, "invalid command!")
- return FALSE
- for (var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks[tram_id])
- if(destination.platform_code == platform)
- destination_platform = destination
- break
- if(!destination_platform)
- balloon_alert(user, "invalid command!")
- return FALSE
- else
- switch(mode)
- if(TRAMCTRL_FAST)
- tram_part.tram_travel(destination_platform, rapid = TRUE)
- if(TRAMCTRL_SAFE)
- tram_part.tram_travel(destination_platform, rapid = FALSE)
- balloon_alert(user, "tram dispatched")
- return TRUE
-
-/obj/item/tram_remote/afterattack(atom/target, mob/user)
- link_tram(user, target)
-
-/obj/item/tram_remote/proc/link_tram(mob/user, atom/target)
- var/obj/machinery/button/tram/smacked_device = target
- if(!istype(smacked_device, /obj/machinery/button/tram))
- return
- tram_ref = null
- for(var/datum/lift_master/lift as anything in GLOB.active_lifts_by_type[TRAM_LIFT_ID])
- if(lift.specific_lift_id == smacked_device.lift_id)
- tram_ref = WEAKREF(lift)
- break
- if(tram_ref)
- balloon_alert(user, "tram linked")
- else
- balloon_alert(user, "link failed!")
- update_appearance()
-
-#undef TRAMCTRL_FAST
-#undef TRAMCTRL_SAFE
diff --git a/code/modules/industrial_lift/tram/tram_structures.dm b/code/modules/industrial_lift/tram/tram_structures.dm
deleted file mode 100644
index 81c80e135aeab..0000000000000
--- a/code/modules/industrial_lift/tram/tram_structures.dm
+++ /dev/null
@@ -1,22 +0,0 @@
-/obj/structure/chair/sofa/bench/tram
- name = "bench"
- desc = "Perfectly designed to be comfortable to sit on, and hellish to sleep on."
- icon_state = "bench_middle"
- greyscale_config = /datum/greyscale_config/bench_middle
- greyscale_colors = "#00CCFF"
-
-/obj/structure/chair/sofa/bench/tram/left
- icon_state = "bench_left"
- greyscale_config = /datum/greyscale_config/bench_left
-
-/obj/structure/chair/sofa/bench/tram/right
- icon_state = "bench_right"
- greyscale_config = /datum/greyscale_config/bench_right
-
-/obj/structure/chair/sofa/bench/tram/corner
- icon_state = "bench_corner"
- greyscale_config = /datum/greyscale_config/bench_corner
-
-/obj/structure/chair/sofa/bench/tram/solo
- icon_state = "bench_solo"
- greyscale_config = /datum/greyscale_config/bench_solo
diff --git a/code/modules/industrial_lift/tram/tram_walls.dm b/code/modules/industrial_lift/tram/tram_walls.dm
deleted file mode 100644
index c8bf7e970b521..0000000000000
--- a/code/modules/industrial_lift/tram/tram_walls.dm
+++ /dev/null
@@ -1,326 +0,0 @@
-/*
- * Tram Walls
- */
-/obj/structure/tramwall
- name = "wall"
- desc = "A huge chunk of metal used to separate rooms."
- anchored = TRUE
- icon = 'icons/turf/walls/wall.dmi'
- icon_state = "wall-0"
- base_icon_state = "wall"
- layer = LOW_OBJ_LAYER
- density = TRUE
- opacity = FALSE
- max_integrity = 100
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
- canSmoothWith = SMOOTH_GROUP_WALLS
- can_be_unanchored = FALSE
- can_atmos_pass = ATMOS_PASS_DENSITY
- rad_insulation = RAD_MEDIUM_INSULATION
- material_flags = MATERIAL_EFFECTS
- var/mineral = /obj/item/stack/sheet/iron
- var/mineral_amount = 2
- var/tram_wall_type = /obj/structure/tramwall
- var/girder_type = /obj/structure/girder/tram
- var/slicing_duration = 100
-
-/obj/structure/tramwall/Initialize(mapload)
- AddElement(/datum/element/blocks_explosives)
- . = ..()
- var/obj/item/stack/initialized_mineral = new mineral
- set_custom_materials(initialized_mineral.mats_per_unit, mineral_amount)
- qdel(initialized_mineral)
- air_update_turf(TRUE, TRUE)
-
-/obj/structure/tramwall/attackby(obj/item/welder, mob/user, params)
- if(welder.tool_behaviour == TOOL_WELDER)
- if(!welder.tool_start_check(user, amount=round(slicing_duration / 50)))
- return FALSE
-
- to_chat(user, span_notice("You begin slicing through the outer plating..."))
- if(welder.use_tool(src, user, slicing_duration, volume=100))
- to_chat(user, span_notice("You remove the outer plating."))
- dismantle(user, TRUE)
- else
- return ..()
-
-/obj/structure/tramwall/proc/dismantle(mob/user, disassembled=TRUE, obj/item/tool = null)
- user.visible_message(span_notice("[user] dismantles the wall."), span_notice("You dismantle the wall."))
- if(tool)
- tool.play_tool_sound(src, 100)
- else
- playsound(src, 'sound/items/welder.ogg', 100, TRUE)
- deconstruct(disassembled)
-
-/obj/structure/tramwall/deconstruct(disassembled = TRUE)
- if(!(flags_1 & NODECONSTRUCT_1))
- if(disassembled)
- new girder_type(loc)
- if(mineral_amount)
- for(var/i in 1 to mineral_amount)
- new mineral(loc)
- qdel(src)
-
-/obj/structure/tramwall/get_dumping_location()
- return null
-
-/obj/structure/tramwall/examine_status(mob/user)
- to_chat(user, span_notice("The outer plating is welded firmly in place."))
- return null
-
-
-/*
- * Other misc tramwall types
- */
-
-/obj/structure/tramwall/titanium
- name = "wall"
- desc = "A light-weight titanium wall used in shuttles."
- icon = 'icons/turf/walls/tram_wall.dmi'
- icon_state = "shuttle_wall-0"
- base_icon_state = "shuttle_wall"
- mineral = /obj/item/stack/sheet/mineral/titanium
- tram_wall_type = /obj/structure/tramwall/titanium
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_TITANIUM_WALLS + SMOOTH_GROUP_WALLS
- canSmoothWith = SMOOTH_GROUP_SHUTTLE_PARTS + SMOOTH_GROUP_AIRLOCK + SMOOTH_GROUP_TITANIUM_WALLS
-
-/obj/structure/tramwall/plastitanium
- name = "wall"
- desc = "An evil wall of plasma and titanium."
- icon = 'icons/turf/walls/plastitanium_wall.dmi'
- icon_state = "plastitanium_wall-0"
- base_icon_state = "plastitanium_wall"
- mineral = /obj/item/stack/sheet/mineral/plastitanium
- tram_wall_type = /obj/structure/tramwall/plastitanium
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_PLASTITANIUM_WALLS + SMOOTH_GROUP_WALLS
- canSmoothWith = SMOOTH_GROUP_SHUTTLE_PARTS + SMOOTH_GROUP_AIRLOCK + SMOOTH_GROUP_PLASTITANIUM_WALLS
-
-/obj/structure/tramwall/gold
- name = "gold wall"
- desc = "A wall with gold plating. Swag!"
- icon = 'icons/turf/walls/gold_wall.dmi'
- icon_state = "gold_wall-0"
- base_icon_state = "gold_wall"
- mineral = /obj/item/stack/sheet/mineral/gold
- tram_wall_type = /obj/structure/tramwall/gold
- explosion_block = 0 //gold is a soft metal you dingus.
- smoothing_groups = SMOOTH_GROUP_GOLD_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
- canSmoothWith = SMOOTH_GROUP_GOLD_WALLS
- custom_materials = list(/datum/material/gold = SHEET_MATERIAL_AMOUNT*2)
-
-/obj/structure/tramwall/silver
- name = "silver wall"
- desc = "A wall with silver plating. Shiny!"
- icon = 'icons/turf/walls/silver_wall.dmi'
- icon_state = "silver_wall-0"
- base_icon_state = "silver_wall"
- mineral = /obj/item/stack/sheet/mineral/silver
- tram_wall_type = /obj/structure/tramwall/silver
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_SILVER_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
- canSmoothWith = SMOOTH_GROUP_SILVER_WALLS
- custom_materials = list(/datum/material/silver = SHEET_MATERIAL_AMOUNT*2)
-
-/obj/structure/tramwall/diamond
- name = "diamond wall"
- desc = "A wall with diamond plating. You monster."
- icon = 'icons/turf/walls/diamond_wall.dmi'
- icon_state = "diamond_wall-0"
- base_icon_state = "diamond_wall"
- mineral = /obj/item/stack/sheet/mineral/diamond
- tram_wall_type = /obj/structure/tramwall/diamond
- slicing_duration = 200 //diamond wall takes twice as much time to slice
- max_integrity = 800
- explosion_block = 3
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_DIAMOND_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
- canSmoothWith = SMOOTH_GROUP_DIAMOND_WALLS
- custom_materials = list(/datum/material/diamond = SHEET_MATERIAL_AMOUNT*2)
-
-/obj/structure/tramwall/bananium
- name = "bananium wall"
- desc = "A wall with bananium plating. Honk!"
- icon = 'icons/turf/walls/bananium_wall.dmi'
- icon_state = "bananium_wall-0"
- base_icon_state = "bananium_wall"
- mineral = /obj/item/stack/sheet/mineral/bananium
- tram_wall_type = /obj/structure/tramwall/bananium
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_BANANIUM_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
- canSmoothWith = SMOOTH_GROUP_BANANIUM_WALLS
- custom_materials = list(/datum/material/bananium = SHEET_MATERIAL_AMOUNT*2)
-
-/obj/structure/tramwall/sandstone
- name = "sandstone wall"
- desc = "A wall with sandstone plating. Rough."
- icon = 'icons/turf/walls/sandstone_wall.dmi'
- icon_state = "sandstone_wall-0"
- base_icon_state = "sandstone_wall"
- mineral = /obj/item/stack/sheet/mineral/sandstone
- tram_wall_type = /obj/structure/tramwall/sandstone
- explosion_block = 0
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_SANDSTONE_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
- canSmoothWith = SMOOTH_GROUP_SANDSTONE_WALLS
- custom_materials = list(/datum/material/sandstone = SHEET_MATERIAL_AMOUNT*2)
-
-/obj/structure/tramwall/uranium
- article = "a"
- name = "uranium wall"
- desc = "A wall with uranium plating. This is probably a bad idea."
- icon = 'icons/turf/walls/uranium_wall.dmi'
- icon_state = "uranium_wall-0"
- base_icon_state = "uranium_wall"
- mineral = /obj/item/stack/sheet/mineral/uranium
- tram_wall_type = /obj/structure/tramwall/uranium
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_URANIUM_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
- canSmoothWith = SMOOTH_GROUP_URANIUM_WALLS
- custom_materials = list(/datum/material/uranium = SHEET_MATERIAL_AMOUNT*2)
-
- /// Mutex to prevent infinite recursion when propagating radiation pulses
- var/active = null
-
- /// The last time a radiation pulse was performed
- var/last_event = 0
-
-/obj/structure/tramwall/uranium/attackby(obj/item/W, mob/user, params)
- radiate()
- return ..()
-
-/obj/structure/tramwall/uranium/attack_hand(mob/user, list/modifiers)
- radiate()
- return ..()
-
-/obj/structure/tramwall/uranium/proc/radiate()
- SIGNAL_HANDLER
- if(active)
- return
- if(world.time <= last_event + 1.5 SECONDS)
- return
- active = TRUE
- radiation_pulse(
- src,
- max_range = 3,
- threshold = RAD_LIGHT_INSULATION,
- chance = URANIUM_IRRADIATION_CHANCE,
- minimum_exposure_time = URANIUM_RADIATION_MINIMUM_EXPOSURE_TIME,
- )
- propagate_radiation_pulse()
- last_event = world.time
- active = FALSE
-
-/obj/structure/tramwall/plasma
- name = "plasma wall"
- desc = "A wall with plasma plating. This is definitely a bad idea."
- icon = 'icons/turf/walls/plasma_wall.dmi'
- icon_state = "plasma_wall-0"
- base_icon_state = "plasma_wall"
- mineral = /obj/item/stack/sheet/mineral/plasma
- tram_wall_type = /obj/structure/tramwall/plasma
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_PLASMA_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
- canSmoothWith = SMOOTH_GROUP_PLASMA_WALLS
- custom_materials = list(/datum/material/plasma = SHEET_MATERIAL_AMOUNT*2)
-
-/obj/structure/tramwall/wood
- name = "wooden wall"
- desc = "A wall with wooden plating. Stiff."
- icon = 'icons/turf/walls/wood_wall.dmi'
- icon_state = "wood_wall-0"
- base_icon_state = "wood_wall"
- mineral = /obj/item/stack/sheet/mineral/wood
- tram_wall_type = /obj/structure/tramwall/wood
- explosion_block = 0
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_WOOD_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
- canSmoothWith = SMOOTH_GROUP_WOOD_WALLS
- custom_materials = list(/datum/material/wood = SHEET_MATERIAL_AMOUNT*2)
-
-/obj/structure/tramwall/wood/attackby(obj/item/W, mob/user)
- if(W.get_sharpness() && W.force)
- var/duration = ((4.8 SECONDS) / W.force) * 2 //In seconds, for now.
- if(istype(W, /obj/item/hatchet) || istype(W, /obj/item/fireaxe))
- duration /= 4 //Much better with hatchets and axes.
- if(do_after(user, duration * (1 SECONDS), target=src)) //Into deciseconds.
- dismantle(user, disassembled = FALSE, tool = W)
- return
- return ..()
-
-/obj/structure/tramwall/bamboo
- name = "bamboo wall"
- desc = "A wall with a bamboo finish."
- icon = 'icons/turf/walls/bamboo_wall.dmi'
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_WALLS + SMOOTH_GROUP_BAMBOO_WALLS + SMOOTH_GROUP_CLOSED_TURFS
- canSmoothWith = SMOOTH_GROUP_BAMBOO_WALLS
- mineral = /obj/item/stack/sheet/mineral/bamboo
- tram_wall_type = /obj/structure/tramwall/bamboo
-
-/obj/structure/tramwall/iron
- name = "rough iron wall"
- desc = "A wall with rough iron plating."
- icon = 'icons/turf/walls/iron_wall.dmi'
- icon_state = "iron_wall-0"
- base_icon_state = "iron_wall"
- mineral = /obj/item/stack/rods
- mineral_amount = 5
- tram_wall_type = /obj/structure/tramwall/iron
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_IRON_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
- canSmoothWith = SMOOTH_GROUP_IRON_WALLS
- custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2.5)
-
-/obj/structure/tramwall/abductor
- name = "alien wall"
- desc = "A wall with alien alloy plating."
- icon = 'icons/turf/walls/abductor_wall.dmi'
- icon_state = "abductor_wall-0"
- base_icon_state = "abductor_wall"
- mineral = /obj/item/stack/sheet/mineral/abductor
- tram_wall_type = /obj/structure/tramwall/abductor
- slicing_duration = 200 //alien wall takes twice as much time to slice
- explosion_block = 3
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_ABDUCTOR_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
- canSmoothWith = SMOOTH_GROUP_ABDUCTOR_WALLS
- custom_materials = list(/datum/material/alloy/alien = SHEET_MATERIAL_AMOUNT*2)
-
-/obj/structure/tramwall/material
- name = "wall"
- desc = "A huge chunk of material used to separate rooms."
- icon = 'icons/turf/walls/materialwall.dmi'
- icon_state = "materialwall-0"
- base_icon_state = "materialwall"
- smoothing_flags = SMOOTH_BITMASK
- smoothing_groups = SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS + SMOOTH_GROUP_MATERIAL_WALLS
- canSmoothWith = SMOOTH_GROUP_MATERIAL_WALLS
- material_flags = MATERIAL_EFFECTS | MATERIAL_ADD_PREFIX | MATERIAL_COLOR | MATERIAL_AFFECT_STATISTICS
-
-/obj/structure/tramwall/material/deconstruct(disassembled = TRUE)
- if(!(flags_1 & NODECONSTRUCT_1))
- if(disassembled)
- new girder_type(loc)
- for(var/material in custom_materials)
- var/datum/material/material_datum = material
- new material_datum.sheet_type(loc, FLOOR(custom_materials[material_datum] / SHEET_MATERIAL_AMOUNT, 1))
- qdel(src)
-
-/obj/structure/tramwall/material/mat_update_desc(mat)
- desc = "A huge chunk of [mat] used to separate rooms."
-
-/obj/structure/tramwall/material/update_icon(updates)
- . = ..()
- for(var/datum/material/material in custom_materials)
- if(material.alpha < 255)
- update_transparency_underlays()
- return
-
-/obj/structure/tramwall/material/proc/update_transparency_underlays()
- underlays.Cut()
- var/mutable_appearance/girder_underlay = mutable_appearance('icons/obj/structures.dmi', "girder", layer = LOW_OBJ_LAYER-0.01)
- girder_underlay.appearance_flags = RESET_ALPHA | RESET_COLOR
- underlays += girder_underlay
diff --git a/code/modules/industrial_lift/tram/tram_windows.dm b/code/modules/industrial_lift/tram/tram_windows.dm
deleted file mode 100644
index 55ec5aa283fca..0000000000000
--- a/code/modules/industrial_lift/tram/tram_windows.dm
+++ /dev/null
@@ -1,73 +0,0 @@
-/obj/structure/window/reinforced/tram
- name = "tram window"
- desc = "A window made out of a titanium-silicate alloy. It looks tough to break. Is that a challenge?"
- icon = 'icons/obj/smooth_structures/tram_window.dmi'
- icon_state = "tram_mid"
- smoothing_flags = SMOOTH_BITMASK|SMOOTH_BORDER_OBJECT
- canSmoothWith = SMOOTH_GROUP_WINDOW_DIRECTIONAL_TRAM
- smoothing_groups = SMOOTH_GROUP_WINDOW_DIRECTIONAL_TRAM
- reinf = TRUE
- heat_resistance = 1600
- armor_type = /datum/armor/window_tram
- max_integrity = 100
- explosion_block = 0
- glass_type = /obj/item/stack/sheet/titaniumglass
- rad_insulation = RAD_MEDIUM_INSULATION
- glass_material_datum = /datum/material/alloy/titaniumglass
-
-/obj/structure/window/reinforced/tram/Initialize(mapload, direct)
- . = ..()
- setDir(dir)
-
-/obj/structure/window/reinforced/tram/setDir(new_dir)
- . = ..()
- if(fulltile)
- return
- if(dir & NORTH)
- layer = LOW_ITEM_LAYER
- else
- layer = BELOW_OBJ_LAYER
- if(dir & SOUTH)
- SET_PLANE_IMPLICIT(src, WALL_PLANE_UPPER)
- else
- SET_PLANE_IMPLICIT(src, GAME_PLANE)
-
-/obj/structure/window/reinforced/tram/set_smoothed_icon_state(new_junction)
- if(fulltile)
- return ..()
- smoothing_junction = new_junction
- var/go_off = reverse_ndir(smoothing_junction)
- var/smooth_left = (go_off & turn(dir, 90))
- var/smooth_right = (go_off & turn(dir, -90))
- if(smooth_left && smooth_right)
- icon_state = "tram_mid"
- else if (smooth_left)
- icon_state = "tram_left"
- else if (smooth_right)
- icon_state = "tram_right"
- else
- icon_state = "tram_mid"
-
-/obj/structure/window/reinforced/tram/front
- name = "tram wall"
- desc = "A lightweight titanium composite structure with a windscreen installed."
- icon_state = "tram_window-0"
- base_icon_state = "tram_window"
- wtype = "shuttle"
- fulltile = TRUE
- smoothing_flags = NONE
- canSmoothWith = null
- smoothing_groups = SMOOTH_GROUP_WINDOW_DIRECTIONAL_TRAM
- flags_1 = PREVENT_CLICK_UNDER_1
- explosion_block = 3
- glass_amount = 2
- receive_ricochet_chance_mod = 1.2
-
-/datum/armor/window_tram
- melee = 80
- bullet = 5
- bomb = 45
- fire = 99
- acid = 100
-
-MAPPING_DIRECTIONAL_HELPERS(/obj/structure/window/reinforced/tram, 0)
diff --git a/code/modules/jobs/access.dm b/code/modules/jobs/access.dm
index 7502165f811fc..22fbd42b71cd4 100644
--- a/code/modules/jobs/access.dm
+++ b/code/modules/jobs/access.dm
@@ -1,17 +1,22 @@
-//returns TRUE if this mob has sufficient access to use this object
+//
+/**
+ * Returns TRUE if this mob has sufficient access to use this object
+ *
+ * * accessor - mob trying to access this object, !!CAN BE NULL!! because of telekiesis because we're in hell
+ */
/obj/proc/allowed(mob/accessor)
var/result_bitflags = SEND_SIGNAL(src, COMSIG_OBJ_ALLOWED, accessor)
if(result_bitflags & COMPONENT_OBJ_ALLOW)
return TRUE
if(result_bitflags & COMPONENT_OBJ_DISALLOW) // override all other checks
return FALSE
- if(HAS_TRAIT(accessor, TRAIT_ALWAYS_NO_ACCESS))
+ if(!isnull(accessor) && HAS_TRAIT(accessor, TRAIT_ALWAYS_NO_ACCESS))
return FALSE
//check if it doesn't require any access at all
if(check_access(null))
return TRUE
- if(!istype(accessor)) //likely a TK user.
+ if(isnull(accessor)) //likely a TK user.
return FALSE
if(issilicon(accessor))
if(ispAI(accessor))
diff --git a/code/modules/library/bibles.dm b/code/modules/library/bibles.dm
index 8a10a058341fe..ecbcc397dbab5 100644
--- a/code/modules/library/bibles.dm
+++ b/code/modules/library/bibles.dm
@@ -289,7 +289,7 @@ GLOBAL_LIST_INIT(bibleitemstates, list(
make_new_altar(bible_smacked, user)
return
for(var/obj/effect/rune/nearby_runes in range(2, user))
- nearby_runes.invisibility = 0
+ nearby_runes.SetInvisibility(INVISIBILITY_NONE, id=type, priority=INVISIBILITY_PRIORITY_BASIC_ANTI_INVISIBILITY)
bible_smacked.balloon_alert(user, "floor smacked!")
if(user.mind?.holy_role)
@@ -323,13 +323,14 @@ GLOBAL_LIST_INIT(bibleitemstates, list(
playsound(src,'sound/effects/pray_chaplain.ogg',60,TRUE)
for(var/obj/item/soulstone/stone in sword.contents)
stone.required_role = null
- for(var/mob/living/simple_animal/shade/shade in stone)
+ for(var/mob/living/basic/shade/shade in stone)
var/datum/antagonist/cult/cultist = shade.mind.has_antag_datum(/datum/antagonist/cult)
if(cultist)
cultist.silent = TRUE
cultist.on_removal()
- shade.icon_state = "shade_holy"
- shade.name = "Purified [shade.name]"
+ shade.theme = THEME_HOLY
+ shade.name = "Purified [shade.real_name]"
+ shade.update_appearance(UPDATE_ICON_STATE)
stone.release_shades(user)
qdel(stone)
new /obj/item/nullrod/claymore(get_turf(sword))
diff --git a/code/modules/lighting/lighting_area.dm b/code/modules/lighting/lighting_area.dm
index f7e1ca2d86802..c6f427f592f6b 100644
--- a/code/modules/lighting/lighting_area.dm
+++ b/code/modules/lighting/lighting_area.dm
@@ -48,33 +48,78 @@
add_base_lighting()
/area/proc/remove_base_lighting()
+ UnregisterSignal(SSdcs, COMSIG_STARLIGHT_COLOR_CHANGED)
var/list/z_offsets = SSmapping.z_level_to_plane_offset
- for(var/turf/T as anything in get_contained_turfs())
- if(z_offsets[T.z])
- T.cut_overlay(lighting_effects[z_offsets[T.z] + 1])
+ if(length(lighting_effects) > 1)
+ for(var/turf/T as anything in get_contained_turfs())
+ if(z_offsets[T.z])
+ T.cut_overlay(lighting_effects[z_offsets[T.z] + 1])
cut_overlay(lighting_effects[1])
- QDEL_LIST(lighting_effects)
+ lighting_effects = null
area_has_base_lighting = FALSE
/area/proc/add_base_lighting()
lighting_effects = list()
for(var/offset in 0 to SSmapping.max_plane_offset)
- var/mutable_appearance/lighting_effect = mutable_appearance('icons/effects/alphacolors.dmi', "white")
- SET_PLANE_W_SCALAR(lighting_effect, LIGHTING_PLANE, offset)
- lighting_effect.layer = LIGHTING_PRIMARY_LAYER
- lighting_effect.blend_mode = BLEND_ADD
- lighting_effect.alpha = base_lighting_alpha
- lighting_effect.color = (base_lighting_color == COLOR_STARLIGHT ? GLOB.starlight_color : base_lighting_color)
- lighting_effect.appearance_flags = RESET_TRANSFORM | RESET_ALPHA | RESET_COLOR
- lighting_effects += lighting_effect
+ var/mutable_appearance/light
+ if(base_lighting_color == COLOR_STARLIGHT)
+ light = new(GLOB.starlight_overlays[offset + 1])
+ else
+ light = mutable_appearance('icons/effects/alphacolors.dmi', "white")
+ light.color = base_lighting_color
+ light.layer = LIGHTING_PRIMARY_LAYER
+ light.blend_mode = BLEND_ADD
+ light.appearance_flags = RESET_TRANSFORM | RESET_ALPHA | RESET_COLOR
+ light.alpha = base_lighting_alpha
+ SET_PLANE_W_SCALAR(light, LIGHTING_PLANE, offset)
+ lighting_effects += light
+
+ if(base_lighting_color == COLOR_STARLIGHT)
+ // Ok this is gonna be dumb
+ // We rely on render_source working, and it DOES NOT APPEAR TO in area rendering
+ // So we're gonna have to update the area's overlay manually. everything else can be automatic tho
+ // Fortunately the first overlay is only ever used by the area, soooo
+ var/mutable_appearance/light = mutable_appearance('icons/effects/alphacolors.dmi', "white")
+ light.layer = LIGHTING_PRIMARY_LAYER
+ light.blend_mode = BLEND_ADD
+ light.appearance_flags = RESET_TRANSFORM | RESET_ALPHA | RESET_COLOR
+ light.color = GLOB.starlight_color
+ light.alpha = base_lighting_alpha
+ SET_PLANE_W_SCALAR(light, LIGHTING_PLANE, 0)
+ lighting_effects[1] = light
+ RegisterSignal(SSdcs, COMSIG_STARLIGHT_COLOR_CHANGED, PROC_REF(starlight_changed))
+
add_overlay(lighting_effects[1])
var/list/z_offsets = SSmapping.z_level_to_plane_offset
- for(var/turf/T as anything in get_contained_turfs())
- T.luminosity = 1
- // This outside loop is EXTREMELY hot because it's run by space tiles. Don't want no part in that
- // We will only add overlays to turfs not on the first z layer, because that's a significantly lesser portion
- // And we need to do them separate, or lighting will go fuckey
- if(z_offsets[T.z])
- T.add_overlay(lighting_effects[z_offsets[T.z] + 1])
+ if(length(lighting_effects) > 1)
+ // This inside loop is EXTREMELY hot because it's run by space tiles. Don't want no part in that
+ for(var/turf/T as anything in get_contained_turfs())
+ T.luminosity = 1
+ // We will only add overlays to turfs not on the first z layer, because that's a significantly lesser portion
+ // And we need to do them separate, or lighting will go fuckey
+ if(z_offsets[T.z])
+ T.add_overlay(lighting_effects[z_offsets[T.z] + 1])
+ else
+ for(var/turf/T as anything in get_contained_turfs())
+ T.luminosity = 1
area_has_base_lighting = TRUE
+
+/area/proc/starlight_changed(datum/source, old_star, new_star)
+ var/mutable_appearance/old_star_effect = mutable_appearance('icons/effects/alphacolors.dmi', "white")
+ old_star_effect.layer = LIGHTING_PRIMARY_LAYER
+ old_star_effect.blend_mode = BLEND_ADD
+ old_star_effect.appearance_flags = RESET_TRANSFORM | RESET_ALPHA | RESET_COLOR
+ old_star_effect.color = old_star
+ old_star_effect.alpha = base_lighting_alpha
+ SET_PLANE_W_SCALAR(old_star_effect, LIGHTING_PLANE, 0)
+ cut_overlay(old_star_effect)
+ var/mutable_appearance/new_star_effect = mutable_appearance('icons/effects/alphacolors.dmi', "white")
+ new_star_effect.layer = LIGHTING_PRIMARY_LAYER
+ new_star_effect.blend_mode = BLEND_ADD
+ new_star_effect.appearance_flags = RESET_TRANSFORM | RESET_ALPHA | RESET_COLOR
+ new_star_effect.color = new_star
+ new_star_effect.alpha = base_lighting_alpha
+ SET_PLANE_W_SCALAR(new_star_effect, LIGHTING_PLANE, 0)
+ add_overlay(new_star_effect)
+ lighting_effects[1] = new_star_effect
diff --git a/code/modules/lighting/lighting_atom.dm b/code/modules/lighting/lighting_atom.dm
index 2ab7ad1c13486..5d82c33e23fbb 100644
--- a/code/modules/lighting/lighting_atom.dm
+++ b/code/modules/lighting/lighting_atom.dm
@@ -1,6 +1,6 @@
// The proc you should always use to set the light of this atom.
-/atom/proc/set_light(l_range, l_power, l_color = NONSENSICAL_VALUE, l_angle, l_dir, l_on)
+/atom/proc/set_light(l_range, l_power, l_color = NONSENSICAL_VALUE, l_angle, l_dir, l_height, l_on)
// We null everything but l_dir, because we don't want to allow for modifications while frozen
if(light_flags & LIGHT_FROZEN)
l_range = null
@@ -8,6 +8,7 @@
l_color = null
l_on = null
l_angle = null
+ l_height = null
if(l_range > 0 && l_range < MINIMUM_USEFUL_LIGHT_RANGE)
l_range = MINIMUM_USEFUL_LIGHT_RANGE //Brings the range up to 1.4, which is just barely brighter than the soft lighting that surrounds players.
@@ -33,6 +34,9 @@
if(!isnull(l_on))
set_light_on(l_on)
+ if(!isnull(l_height))
+ set_light_height(l_height)
+
update_light()
/// Will update the light (duh).
@@ -167,6 +171,17 @@
SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_ON, .)
return .
+/// Setter for the height of our light
+/atom/proc/set_light_height(new_value)
+ if(new_value == light_height || light_flags & LIGHT_FROZEN)
+ return
+ if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_HEIGHT, new_value) & COMPONENT_BLOCK_LIGHT_UPDATE)
+ return
+ . = light_height
+ light_height = new_value
+ SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_HEIGHT, .)
+ return .
+
/// Setter for the light flags of this atom.
/atom/proc/set_light_flags(new_value)
if(new_value == light_flags || (light_flags & LIGHT_FROZEN && new_value & LIGHT_FROZEN))
diff --git a/code/modules/lighting/lighting_source.dm b/code/modules/lighting/lighting_source.dm
index 5137632918709..8918bcb0ed3c6 100644
--- a/code/modules/lighting/lighting_source.dm
+++ b/code/modules/lighting/lighting_source.dm
@@ -23,6 +23,8 @@
var/light_range
/// The colour of the light, string, decomposed by parse_light_color()
var/light_color
+ /// The height of the light. The larger this is, the dimmer we'll start
+ var/light_height
// Variables for keeping track of the colour.
var/lum_r
@@ -77,7 +79,8 @@
if (needs_update)
SSlighting.sources_queue -= src
-
+ SSlighting.current_sources -= src
+
top_atom = null
source_atom = null
source_turf = null
@@ -216,20 +219,20 @@
/datum/light_source/proc/get_sheet(multiz = FALSE)
var/list/static/key_to_sheet = list()
var/range = max(1, light_range);
- var/key = "[range]-[visual_offset]-[offset_x]-[offset_y]-[light_dir]-[light_angle]-[multiz]"
+ var/key = "[range]-[visual_offset]-[offset_x]-[offset_y]-[light_dir]-[light_angle]-[light_height]-[multiz]"
var/list/hand_back = key_to_sheet[key]
if(!hand_back)
if(multiz)
- hand_back = generate_sheet_multiz(range, visual_offset, offset_x, offset_y, light_dir, light_angle)
+ hand_back = generate_sheet_multiz(range, visual_offset, offset_x, offset_y, light_dir, light_angle, light_height)
else
- hand_back = generate_sheet(range, visual_offset, offset_x, offset_y, light_dir, light_angle)
+ hand_back = generate_sheet(range, visual_offset, offset_x, offset_y, light_dir, light_angle, light_height)
key_to_sheet[key] = hand_back
return hand_back
/// Returns a list of lists that encodes the light falloff of our source
/// Takes anything that impacts our generation as input
/// This function should be "pure", no side effects or reads from the source object
-/datum/light_source/proc/generate_sheet(range, visual_offset, x_offset, y_offset, center_dir, angle, z_level = 0)
+/datum/light_source/proc/generate_sheet(range, visual_offset, x_offset, y_offset, center_dir, angle, height, z_level = 0)
var/list/encode = list()
// How far away the turfs we get are, and how many there are are often not the same calculation
// So we need to include the visual offset, so we can ensure our sheet is large enough to accept all the distance differences
@@ -240,30 +243,30 @@
for(var/x in (-(bound_range) + x_offset - 0.5) to (bound_range + x_offset + 0.5))
var/list/row = list()
for(var/y in (-(bound_range) + y_offset - 0.5) to (bound_range + y_offset + 0.5))
- row += falloff_at_coord(x, y, z_level, range, center_dir, light_angle)
+ row += falloff_at_coord(x, y, z_level, range, center_dir, angle, height)
encode += list(row)
return encode
/// Returns a THREE dimensional list of lists that encodes the lighting falloff of our source
/// Takes anything that impacts our generation as input
/// This function should be "pure", no side effects or reads from the passed object
-/datum/light_source/proc/generate_sheet_multiz(range, visual_offset, x_offset, y_offset, center_dir, angle)
+/datum/light_source/proc/generate_sheet_multiz(range, visual_offset, x_offset, y_offset, center_dir, angle, height)
var/list/encode = list()
var/z_range = SSmapping.max_plane_offset // Let's just be safe yeah?
for(var/z in -z_range to z_range)
- var/list/sheet = generate_sheet(range, visual_offset, x_offset, y_offset, center_dir, angle, z)
+ var/list/sheet = generate_sheet(range, visual_offset, x_offset, y_offset, center_dir, angle, height, z)
encode += list(sheet)
return encode
/// Takes x y and z offsets from the source as input, alongside our source's range
/// Returns a value between 0 and 1, 0 being dark on that tile, 1 being fully lit
-/datum/light_source/proc/falloff_at_coord(x, y, z, range, center_dir, angle)
+/datum/light_source/proc/falloff_at_coord(x, y, z, range, center_dir, angle, height)
var/range_divisor = max(1, range)
// You may notice we use squares here even though there are three components
// Because z diffs are so functionally small, cubes and cube roots are too aggressive
// The larger the distance is, the less bright our light will be
- var/multiplier = 1 - CLAMP01(sqrt(x ** 2 + y ** 2 + z ** 2 + LIGHTING_HEIGHT) / range_divisor)
+ var/multiplier = 1 - CLAMP01(sqrt(x ** 2 + y ** 2 + z ** 2 + height) / range_divisor)
if(angle >= 360 || angle <= 0)
return multiplier
@@ -428,6 +431,10 @@
light_angle = source_atom.light_angle
update = TRUE
+ if(source_atom.light_height != light_height)
+ light_height = source_atom.light_height
+ update = TRUE
+
var/list/visual_offsets = calculate_light_offset(visual_source)
if(visual_offsets[1] != offset_x || visual_offsets[2] != offset_y || source_turf != old_source_turf)
offset_x = visual_offsets[1]
diff --git a/code/modules/lighting/lighting_turf.dm b/code/modules/lighting/lighting_turf.dm
index b2bc367662937..26ebbd1ba4e29 100644
--- a/code/modules/lighting/lighting_turf.dm
+++ b/code/modules/lighting/lighting_turf.dm
@@ -110,6 +110,9 @@
if(new_area.lighting_effects)
add_overlay(new_area.lighting_effects[index])
- // If we're changing into an area with no lighting, and we're lit, light ourselves
- if(!new_area.lighting_effects && old_area.lighting_effects && space_lit)
- overlays += GLOB.fullbright_overlays[GET_TURF_PLANE_OFFSET(src) + 1]
+ // Manage removing/adding starlight overlays, we'll inherit from the area so we can drop it if the area has it already
+ if(space_lit)
+ if(!new_area.lighting_effects && old_area.lighting_effects)
+ overlays += GLOB.starlight_overlays[GET_TURF_PLANE_OFFSET(src) + 1]
+ else if (new_area.lighting_effects && !old_area.lighting_effects)
+ overlays -= GLOB.starlight_overlays[GET_TURF_PLANE_OFFSET(src) + 1]
diff --git a/code/modules/lighting/static_lighting_area.dm b/code/modules/lighting/static_lighting_area.dm
index 7dc6cc6c3d850..e05868d0b829b 100644
--- a/code/modules/lighting/static_lighting_area.dm
+++ b/code/modules/lighting/static_lighting_area.dm
@@ -1,14 +1,38 @@
+/// List of plane offset + 1 -> object to display to use
+/// Fills with offsets as they are generated
+/// Holds a list of objects that represent starlight. The idea is to render_source them
+/// So modifying starlight requires touching only one place (NOTE: this doesn't work for the area overlays)
+/// In order to modify them you need to use set_starlight. Areas don't work with render sources it looks like
+GLOBAL_LIST_INIT_TYPED(starlight_objects, /obj, list(starlight_object(0)))
+/obj/starlight_appearance
+ icon = 'icons/effects/alphacolors.dmi'
+ icon_state = "white"
+ layer = LIGHTING_PRIMARY_LAYER
+ blend_mode = BLEND_ADD
+ screen_loc = "1,1"
+
+/proc/starlight_object(offset)
+ var/obj/starlight_appearance/glow = new()
+ SET_PLANE_W_SCALAR(glow, LIGHTING_PLANE, offset)
+ glow.layer = LIGHTING_PRIMARY_LAYER
+ glow.blend_mode = BLEND_ADD
+ glow.color = GLOB.starlight_color
+ glow.render_target = SPACE_OVERLAY_RENDER_TARGET(offset)
+ return glow
+
/// List of plane offset + 1 -> mutable appearance to use
/// Fills with offsets as they are generated
-GLOBAL_LIST_INIT_TYPED(fullbright_overlays, /mutable_appearance, list(create_fullbright_overlay(0)))
+/// They mirror their appearance from the starlight objects, which lets us save
+/// time updating them
+GLOBAL_LIST_INIT_TYPED(starlight_overlays, /obj, list(starlight_overlay(0)))
-/proc/create_fullbright_overlay(offset)
- var/mutable_appearance/lighting_effect = mutable_appearance('icons/effects/alphacolors.dmi', "white")
- SET_PLANE_W_SCALAR(lighting_effect, LIGHTING_PLANE, offset)
- lighting_effect.layer = LIGHTING_PRIMARY_LAYER
- lighting_effect.blend_mode = BLEND_ADD
- lighting_effect.color = GLOB.starlight_color
- return lighting_effect
+/proc/starlight_overlay(offset)
+ var/mutable_appearance/glow = new /mutable_appearance()
+ SET_PLANE_W_SCALAR(glow, LIGHTING_PLANE, offset)
+ glow.layer = LIGHTING_PRIMARY_LAYER
+ glow.blend_mode = BLEND_ADD
+ glow.render_source = SPACE_OVERLAY_RENDER_TARGET(offset)
+ return glow
/area
///Whether this area allows static lighting and thus loads the lighting objects
diff --git a/code/modules/logging/categories/log_category_misc.dm b/code/modules/logging/categories/log_category_misc.dm
index e3a737d432819..2d200e63aa27a 100644
--- a/code/modules/logging/categories/log_category_misc.dm
+++ b/code/modules/logging/categories/log_category_misc.dm
@@ -53,6 +53,9 @@
category = LOG_CATEGORY_TELECOMMS
config_flag = /datum/config_entry/flag/log_telecomms
+/datum/log_category/transport
+ category = LOG_CATEGORY_TRANSPORT
+
/datum/log_category/speech_indicator
category = LOG_CATEGORY_SPEECH_INDICATOR
config_flag = /datum/config_entry/flag/log_speech_indicators
diff --git a/code/modules/logging/log_holder.dm b/code/modules/logging/log_holder.dm
index 7d8386e77b6e0..1cea0e554be16 100644
--- a/code/modules/logging/log_holder.dm
+++ b/code/modules/logging/log_holder.dm
@@ -107,7 +107,7 @@ GENERAL_PROTECT_DATUM(/datum/log_holder)
return
switch(action)
- if("re-render")
+ if("refresh")
cache_ui_data()
SStgui.update_uis(src)
return TRUE
@@ -121,7 +121,7 @@ GENERAL_PROTECT_DATUM(/datum/log_holder)
CRASH("Attempted to call init_logging twice!")
round_id = GLOB.round_id
- logging_start_timestamp = unix_timestamp_string()
+ logging_start_timestamp = rustg_unix_timestamp()
log_categories = list()
disabled_categories = list()
@@ -243,13 +243,10 @@ GENERAL_PROTECT_DATUM(/datum/log_holder)
if(human_readable_enabled)
rustg_file_write("\[[human_readable_timestamp()]\] Starting up round ID [round_id].\n - -------------------------\n", category_instance.get_output_file(null, "log"))
-/datum/log_holder/proc/unix_timestamp_string() // pending change to rust-g
- return RUSTG_CALL(RUST_G, "unix_timestamp")()
-
/datum/log_holder/proc/human_readable_timestamp(precision = 3)
var/start = time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss")
// now we grab the millis from the rustg timestamp
- var/rustg_stamp = unix_timestamp_string()
+ var/rustg_stamp = rustg_unix_timestamp()
var/list/timestamp = splittext(rustg_stamp, ".")
#ifdef UNIT_TESTS
if(length(timestamp) != 2)
diff --git a/code/modules/mafia/_defines.dm b/code/modules/mafia/_defines.dm
index 835aff9d63dd4..ef24645bb053f 100644
--- a/code/modules/mafia/_defines.dm
+++ b/code/modules/mafia/_defines.dm
@@ -100,6 +100,8 @@
/// now clearing refs to prepare for the next day. Do not do any actions here, it's just for ref clearing.
#define COMSIG_MAFIA_NIGHT_END "night_end"
+/// signal sent to roles when the game is confirmed starting
+#define COMSIG_MAFIA_GAME_START "game_start"
/// signal sent to roles when the game is confirmed ending
#define COMSIG_MAFIA_GAME_END "game_end"
@@ -107,6 +109,8 @@
GLOBAL_LIST_EMPTY(mafia_signup)
/// list of ghosts who want to play mafia that have since disconnected. They are kept in the lobby, but not counted for starting a game.
GLOBAL_LIST_EMPTY(mafia_bad_signup)
+/// list of PDAs who want to play mafia, every time someone enters the list it checks to see if enough are in
+GLOBAL_LIST_EMPTY(pda_mafia_signup)
/// the current global mafia game running.
GLOBAL_VAR(mafia_game)
/// list of ghosts in mafia_signup who have voted to start early
diff --git a/code/modules/mafia/abilities/abilities.dm b/code/modules/mafia/abilities/abilities.dm
index 59ee7570c322d..3c8e643a427aa 100644
--- a/code/modules/mafia/abilities/abilities.dm
+++ b/code/modules/mafia/abilities/abilities.dm
@@ -29,6 +29,11 @@
target_role = null
return ..()
+///Handles special messagese sent by ability-specific stuff (such as changeling chat).
+/datum/mafia_ability/proc/handle_speech(datum/source, list/speech_args)
+ SIGNAL_HANDLER
+ return FALSE
+
/**
* Called when refs need to be cleared, when the target is no longer set.
*/
@@ -52,21 +57,28 @@
if(game.phase != valid_use_period)
return FALSE
if(host_role.role_flags & ROLE_ROLEBLOCKED)
- to_chat(host_role.body, span_warning("You were roleblocked!"))
+ host_role.send_message_to_player(span_warning("You were roleblocked!"))
+ return FALSE
+ if(host_role.game_status == MAFIA_DEAD)
return FALSE
if(potential_target)
- if((use_flags & CAN_USE_ON_DEAD) && (potential_target.game_status != MAFIA_DEAD))
+ if(use_flags & CAN_USE_ON_DEAD)
+ if(potential_target.game_status != MAFIA_DEAD)
+ if(!silent)
+ host_role.send_message_to_player(span_notice("This can only be used on dead players."))
+ return FALSE
+ else if(potential_target.game_status == MAFIA_DEAD)
if(!silent)
- to_chat(host_role.body, span_notice("This can only be used on dead players."))
+ host_role.send_message_to_player(span_notice("This can only be used on living players."))
return FALSE
if(!(use_flags & CAN_USE_ON_SELF) && (potential_target == host_role))
if(!silent)
- to_chat(host_role.body, span_notice("This can only be used on others."))
+ host_role.send_message_to_player(span_notice("This can only be used on others."))
return FALSE
if(!(use_flags & CAN_USE_ON_OTHERS) && (potential_target != host_role))
if(!silent)
- to_chat(host_role.body, span_notice("This can only be used on yourself."))
+ host_role.send_message_to_player(span_notice("This can only be used on yourself."))
return FALSE
return TRUE
@@ -90,7 +102,7 @@
if(target_role)
if(SEND_SIGNAL(target_role, COMSIG_MAFIA_ON_VISIT, game, host_role) & MAFIA_VISIT_INTERRUPTED) //visited a warden. something that prevents you by visiting that person
- to_chat(host_role.body, span_danger("Your [name] was interrupted!"))
+ host_role.send_message_to_player(span_danger("Your [name] was interrupted!"))
return FALSE
return TRUE
@@ -121,5 +133,5 @@
target_role = new_target
feedback_text = replacetext(feedback_text, "%WILL_PERFORM%", "now")
- to_chat(host_role.body, span_notice(feedback_text))
+ host_role.send_message_to_player(span_notice(feedback_text))
return TRUE
diff --git a/code/modules/mafia/abilities/investigative/investigate.dm b/code/modules/mafia/abilities/investigative/investigate.dm
index 0af1ed674b865..d23b51f9813de 100644
--- a/code/modules/mafia/abilities/investigative/investigate.dm
+++ b/code/modules/mafia/abilities/investigative/investigate.dm
@@ -20,5 +20,5 @@
if(MAFIA_TEAM_SOLO)
fluff = "rogue, with their own objectives..."
- to_chat(host_role.body, span_warning("Your investigations reveal that [target_role.body.real_name] is [fluff]"))
+ host_role.send_message_to_player(span_warning("Your investigations reveal that [target_role.body.real_name] is [fluff]"))
return TRUE
diff --git a/code/modules/mafia/abilities/investigative/pray.dm b/code/modules/mafia/abilities/investigative/pray.dm
index b5bd6ee7e8897..cbf8459f9f2ab 100644
--- a/code/modules/mafia/abilities/investigative/pray.dm
+++ b/code/modules/mafia/abilities/investigative/pray.dm
@@ -14,5 +14,5 @@
if(!.)
return FALSE
- to_chat(host_role.body, span_warning("You invoke spirit of [target_role.body.real_name] and learn their role was [target_role.name]."))
+ host_role.send_message_to_player(span_warning("You invoke spirit of [target_role.body.real_name] and learn their role was [target_role.name]."))
return TRUE
diff --git a/code/modules/mafia/abilities/investigative/reveal.dm b/code/modules/mafia/abilities/investigative/reveal.dm
index db48b552b62cd..5e38d3c9fafa2 100644
--- a/code/modules/mafia/abilities/investigative/reveal.dm
+++ b/code/modules/mafia/abilities/investigative/reveal.dm
@@ -13,7 +13,7 @@
if(!.)
return FALSE
- to_chat(host_role.body, span_warning("You have revealed the true nature of the [target_role]!"))
+ host_role.send_message_to_player(span_warning("You have revealed the true nature of the [target_role]!"))
target_role.reveal_role(game, verbose = TRUE)
return TRUE
diff --git a/code/modules/mafia/abilities/investigative/thoughtfeed.dm b/code/modules/mafia/abilities/investigative/thoughtfeed.dm
index 48d465173cddb..f0eb49f2b4d71 100644
--- a/code/modules/mafia/abilities/investigative/thoughtfeed.dm
+++ b/code/modules/mafia/abilities/investigative/thoughtfeed.dm
@@ -13,7 +13,7 @@
return FALSE
if((target_role.role_flags & ROLE_UNDETECTABLE))
- to_chat(host_role.body,span_warning("[target_role.body.real_name]'s memories reveal that they are the [pick(game.all_roles - target_role)]."))
+ host_role.send_message_to_player(span_warning("[target_role.body.real_name]'s memories reveal that they are the [pick(game.all_roles - target_role)]."))
else
- to_chat(host_role.body,span_warning("[target_role.body.real_name]'s memories reveal that they are the [target_role.name]."))
+ host_role.send_message_to_player(span_warning("[target_role.body.real_name]'s memories reveal that they are the [target_role.name]."))
return TRUE
diff --git a/code/modules/mafia/abilities/killing/alert.dm b/code/modules/mafia/abilities/killing/alert.dm
index 7af38f9befa78..74710087137b8 100644
--- a/code/modules/mafia/abilities/killing/alert.dm
+++ b/code/modules/mafia/abilities/killing/alert.dm
@@ -22,8 +22,8 @@
SIGNAL_HANDLER
if(attacker == host_role)
return
- to_chat(host_role.body, span_userdanger("You have shot a visitor!"))
- to_chat(attacker.body, span_userdanger("You have visited the warden!"))
+ host_role.send_message_to_player(span_userdanger("You have shot a visitor!"))
+ attacker.send_message_to_player(span_userdanger("You have visited the warden!"))
attacker.kill(game, host_role, lynch = FALSE)
return MAFIA_VISIT_INTERRUPTED
diff --git a/code/modules/mafia/abilities/killing/flicker_rampage.dm b/code/modules/mafia/abilities/killing/flicker_rampage.dm
index 08a18e7877807..10331d4798609 100644
--- a/code/modules/mafia/abilities/killing/flicker_rampage.dm
+++ b/code/modules/mafia/abilities/killing/flicker_rampage.dm
@@ -22,11 +22,11 @@
return FALSE
if(!(target_role in darkened_players))
- to_chat(target_role.body, span_userdanger("The lights begin to flicker and dim. You're in danger."))
+ target_role.send_message_to_player(span_userdanger("The lights begin to flicker and dim. You're in danger."))
darkened_players += target_role
else
for(var/datum/mafia_role/dead_players as anything in darkened_players)
- to_chat(dead_players.body, span_userdanger("A shadowy figure appears out of the darkness!"))
+ dead_players.send_message_to_player(span_userdanger("A shadowy figure appears out of the darkness!"))
dead_players.kill(game, host_role, FALSE)
darkened_players -= dead_players
return TRUE
@@ -37,6 +37,6 @@
return //no chance man, that's a town lynch
if(attacker in darkened_players)
- to_chat(host_role.body, span_userdanger("You were attacked by someone in a flickering room. You have danced in the shadows, evading them."))
+ host_role.send_message_to_player(span_userdanger("You were attacked by someone in a flickering room. You have danced in the shadows, evading them."))
return MAFIA_PREVENT_KILL
diff --git a/code/modules/mafia/abilities/killing/kill.dm b/code/modules/mafia/abilities/killing/kill.dm
index 2c09c7525eb6b..d02fd6c287de1 100644
--- a/code/modules/mafia/abilities/killing/kill.dm
+++ b/code/modules/mafia/abilities/killing/kill.dm
@@ -18,17 +18,17 @@
return FALSE
if(!target_role.kill(game, host_role, FALSE))
- to_chat(host_role.body, span_danger("Your attempt at killing [target_role.body.real_name] was prevented!"))
+ host_role.send_message_to_player(span_danger("Your attempt at killing [target_role.body.real_name] was prevented!"))
else
- to_chat(target_role.body, span_userdanger("You have been [attack_action] \a [host_role.name]!"))
+ target_role.send_message_to_player(span_userdanger("You have been [attack_action] \a [host_role.name]!"))
if(honorable && (target_role.team != MAFIA_TEAM_TOWN))
- to_chat(host_role.body, span_userdanger("You have killed an innocent crewmember. You will die tomorrow night."))
+ host_role.send_message_to_player(span_userdanger("You have killed an innocent crewmember. You will die tomorrow night."))
RegisterSignal(game, COMSIG_MAFIA_SUNDOWN, PROC_REF(internal_affairs))
return TRUE
/datum/mafia_ability/attack_player/proc/internal_affairs(datum/mafia_controller/game)
SIGNAL_HANDLER
- to_chat(host_role.body, span_userdanger("You have been killed by Nanotrasen Internal Affairs!"))
+ host_role.send_message_to_player(span_userdanger("You have been killed by Nanotrasen Internal Affairs!"))
host_role.reveal_role(game, verbose = TRUE)
host_role.kill(game, host_role, FALSE) //you technically kill yourself but that shouldn't matter
diff --git a/code/modules/mafia/abilities/protective/heal.dm b/code/modules/mafia/abilities/protective/heal.dm
index 65cd26ad0a11b..4c47d1c85ab5d 100644
--- a/code/modules/mafia/abilities/protective/heal.dm
+++ b/code/modules/mafia/abilities/protective/heal.dm
@@ -18,7 +18,7 @@
if(!.)
return FALSE
if(new_target.role_flags & ROLE_VULNERABLE)
- to_chat(host_role.body, span_notice("[new_target] can't be protected."))
+ host_role.send_message_to_player(span_notice("[new_target] can't be protected."))
return FALSE
/datum/mafia_ability/heal/perform_action_target(datum/mafia_controller/game, datum/mafia_role/day_target)
@@ -39,10 +39,10 @@
/datum/mafia_ability/heal/proc/prevent_kill(datum/source, datum/mafia_controller/game, datum/mafia_role/attacker, lynch)
SIGNAL_HANDLER
if(host_role == target_role)
- to_chat(host_role.body, span_warning("You were attacked last night!"))
+ host_role.send_message_to_player(span_warning("You were attacked last night!"))
return MAFIA_PREVENT_KILL
- to_chat(host_role.body, span_warning("The person you protected tonight was attacked!"))
- to_chat(target_role.body, span_greentext("You were attacked last night, but [saving_message]!"))
+ host_role.send_message_to_player(span_warning("The person you protected tonight was attacked!"))
+ target_role.send_message_to_player(span_greentext("You were attacked last night, but [saving_message]!"))
return MAFIA_PREVENT_KILL
/**
@@ -60,6 +60,6 @@
return FALSE
if(attacker.kill(game, host_role, FALSE)) //you attack the attacker
- to_chat(attacker.body, span_userdanger("You have been ambushed by Security!"))
+ attacker.send_message_to_player(span_userdanger("You have been ambushed by Security!"))
host_role.kill(game, attacker, FALSE) //the attacker attacks you, they were able to attack the target so they can attack you.
return FALSE
diff --git a/code/modules/mafia/abilities/protective/vest.dm b/code/modules/mafia/abilities/protective/vest.dm
index 2c65202dd4338..b65a8cb73ba26 100644
--- a/code/modules/mafia/abilities/protective/vest.dm
+++ b/code/modules/mafia/abilities/protective/vest.dm
@@ -33,7 +33,7 @@
/datum/mafia_ability/vest/proc/self_defense(datum/source, datum/mafia_controller/game, datum/mafia_role/attacker, lynch)
SIGNAL_HANDLER
- to_chat(host_role.body, span_greentext("Your vest saved you!"))
+ host_role.send_message_to_player(span_greentext("Your vest saved you!"))
return MAFIA_PREVENT_KILL
/datum/mafia_ability/vest/proc/end_protection(datum/mafia_controller/game)
diff --git a/code/modules/mafia/abilities/voting/changeling_kill.dm b/code/modules/mafia/abilities/voting/changeling_kill.dm
index 0e851d78a9508..bb1b1e76c8118 100644
--- a/code/modules/mafia/abilities/voting/changeling_kill.dm
+++ b/code/modules/mafia/abilities/voting/changeling_kill.dm
@@ -30,12 +30,32 @@
ling_sent = TRUE
if(target_role.kill(game, host_role, FALSE))
- to_chat(target_role.body, span_userdanger("You have been killed by a Changeling!"))
+ target_role.send_message_to_player(span_userdanger("You have been killed by a Changeling!"))
game.send_message(span_danger("[host_role.body.real_name] was selected to attack [target_role.body.real_name] tonight!"), MAFIA_TEAM_MAFIA)
return TRUE
/datum/mafia_ability/changeling_kill/set_target(datum/mafia_controller/game, datum/mafia_role/new_target)
+ if(new_target.team == MAFIA_TEAM_MAFIA)
+ return FALSE
if(!validate_action_target(game, new_target))
return FALSE
using_ability = TRUE
game.vote_for(host_role, new_target, "Mafia", MAFIA_TEAM_MAFIA)
+
+/**
+ * handle_message
+ *
+ * During the night, Changelings talking will instead redirect it to Changeling chat.
+ */
+/datum/mafia_ability/changeling_kill/handle_speech(datum/source, list/speech_args)
+ . = ..()
+ var/datum/mafia_controller/mafia_game = GLOB.mafia_game
+ if(!mafia_game)
+ return FALSE
+ if (mafia_game.phase != MAFIA_PHASE_NIGHT)
+ return FALSE
+
+ var/phrase = html_decode(speech_args[SPEECH_MESSAGE])
+ mafia_game.send_message(span_changeling("[host_role.body.real_name]: [phrase]"), MAFIA_TEAM_MAFIA)
+ speech_args[SPEECH_MESSAGE] = ""
+ return TRUE
diff --git a/code/modules/mafia/controller.dm b/code/modules/mafia/controller.dm
index f8d9db131064d..a6e1e3c5f7f11 100644
--- a/code/modules/mafia/controller.dm
+++ b/code/modules/mafia/controller.dm
@@ -2,6 +2,9 @@ GLOBAL_LIST_INIT(mafia_roles_by_name, setup_mafia_roles_by_name())
GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
+///How many votes are needed to unlock the 'Universally Hated' achievement.
+#define UNIVERSALLY_HATED_REQUIREMENT 12
+
/**
* The mafia controller handles the mafia minigame in progress.
* It is first created when the first ghost signs up to play.
@@ -11,7 +14,8 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
var/list/datum/mafia_role/all_roles = list()
///all living roles in the game, removed on death.
var/list/datum/mafia_role/living_roles = list()
- ///exists to speed up role retrieval, it's a dict. `player_role_lookup[player ckey]` will give you the role they play
+ ///exists to speed up role retrieval, it's a dict.
+ /// `player_role_lookup[player ckey/PDA]` will give you the role they play
var/list/player_role_lookup = list()
///what part of the game you're playing in. day phases, night phases, judgement phases, etc.
var/phase = MAFIA_PHASE_SETUP
@@ -52,9 +56,6 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
///current timer for phase
var/next_phase_timer
- ///used for debugging in testing (doesn't put people out of the game, some other shit i forgot, who knows just don't set this in live) honestly kinda deprecated
- var/debug = FALSE
-
///was our game forced to start early?
var/early_start = FALSE
@@ -105,10 +106,11 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
/datum/mafia_controller/Destroy(force, ...)
. = ..()
- if(GLOB.mafia_game == src)
- GLOB.mafia_game = null
end_game()
+ player_role_lookup.Cut()
QDEL_NULL(map_deleter)
+ if(GLOB.mafia_game == src)
+ GLOB.mafia_game = null
/**
* Triggers at beginning of the game when there is a confirmed list of valid, ready players.
@@ -122,9 +124,9 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
* * Puts players in each role randomly
* Arguments:
* * setup_list: list of all the datum setups (fancy list of roles) that would work for the game
- * * ready_players: list of filtered, sane players (so not playing or disconnected) for the game to put into roles
+ * * ready_ghosts_and_pdas: list of filtered, sane (so not playing or disconnected) ghost ckeys & PDAs for the game to put into roles.
*/
-/datum/mafia_controller/proc/prepare_game(setup_list, ready_players)
+/datum/mafia_controller/proc/prepare_game(setup_list, ready_ghosts_and_pdas)
var/static/list/possible_maps = subtypesof(/datum/map_template/mafia)
var/turf/spawn_area = get_turf(locate(/obj/effect/landmark/mafia_game_area) in GLOB.landmarks_list)
@@ -136,8 +138,11 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
var/list/bounds = current_map.load(spawn_area)
if(!bounds)
CRASH("Loading mafia map failed!")
- map_deleter.defineRegion(spawn_area, locate(spawn_area.x + 23,spawn_area.y + 23,spawn_area.z), replace = TRUE) //so we're ready to mass delete when round ends
-
+#ifdef UNIT_TESTS //unit test map is smaller
+ map_deleter.defineRegion(spawn_area, locate(spawn_area.x + 7, spawn_area.y + 7, spawn_area.z), replace = TRUE) //so we're ready to mass delete when round ends
+#else
+ map_deleter.defineRegion(spawn_area, locate(spawn_area.x + 23, spawn_area.y + 23, spawn_area.z), replace = TRUE) //so we're ready to mass delete when round ends
+#endif
if(!landmarks.len)//we grab town center when we grab landmarks, if there is none (the first game signed up for let's grab them post load)
for(var/obj/effect/landmark/mafia/possible_spawn in GLOB.landmarks_list)
if(istype(possible_spawn, /obj/effect/landmark/mafia/town_center))
@@ -156,16 +161,23 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
var/list/spawnpoints = landmarks.Copy()
for(var/datum/mafia_role/role as anything in all_roles)
role.assigned_landmark = pick_n_take(spawnpoints)
- if(!debug)
- role.player_key = pick_n_take(ready_players)
+ var/selected_player //this can be a ckey or a PDA
+ selected_player = pick(ready_ghosts_and_pdas)
+ var/client/player_client = GLOB.directory[selected_player]
+ if(player_client)
+ role.player_key = selected_player
else
- role.player_key = pop(ready_players)
+ role.player_pda = selected_player
+ ready_ghosts_and_pdas -= selected_player
-/datum/mafia_controller/proc/send_message(msg, team)
+///Sends a global message to all players, or just 'team' if set.
+/datum/mafia_controller/proc/send_message(msg, team, log_only = FALSE)
for(var/datum/mafia_role/role as anything in all_roles)
if(team && role.team != team)
continue
- to_chat(role.body, msg)
+ role.role_messages += msg
+ if(!log_only)
+ to_chat(role.body, msg)
/**
* The game by this point is now all set up, and so we can put people in their bodies and start the first phase.
@@ -176,10 +188,16 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
*/
/datum/mafia_controller/proc/start_game()
create_bodies()
+ SEND_GLOBAL_SIGNAL(COMSIG_MAFIA_GAME_START, src)
start_day(can_vote = FALSE)
- send_message(span_notice("The selected map is [current_map.name]![current_map.description]"))
+ send_message(span_notice("The selected map is [current_map.name]! [current_map.description]"))
send_message("Day [turn] started! There is no voting on the first day. Say hello to everybody!")
next_phase_timer = addtimer(CALLBACK(src, PROC_REF(check_trial), FALSE), (FIRST_DAY_PERIOD_LENGTH / time_speedup), TIMER_STOPPABLE) //no voting period = no votes = instant night
+ for(var/datum/mafia_role/roles as anything in all_roles)
+ var/obj/item/modular_computer/modpc = roles.player_pda
+ if(!modpc)
+ continue
+ modpc.update_static_data_for_all_viewers()
/**
* How every day starts.
@@ -229,7 +247,7 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
var/datum/mafia_role/loser = get_vote_winner("Day")//, majority_of_town = TRUE)
var/loser_votes = get_vote_count(loser, "Day")
if(loser)
- if(loser_votes > 12)
+ if(loser_votes > UNIVERSALLY_HATED_REQUIREMENT)
award_role(/datum/award/achievement/mafia/universally_hated, loser)
//refresh the lists
judgement_abstain_votes = list()
@@ -369,12 +387,16 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
* * role: mafia_role datum to reward.
*/
/datum/mafia_controller/proc/award_role(award, datum/mafia_role/rewarded)
- var/client/role_client = rewarded.body.client
- role_client?.give_award(award, rewarded.body)
+ rewarded.body?.client?.give_award(award, rewarded.body)
+ if(!rewarded.player_pda)
+ return
+ for(var/datum/tgui/window as anything in rewarded.player_pda.open_uis)
+ window.user?.client?.give_award(award, window.user.client.mob)
/**
* The end of the game is in two procs, because we want a bit of time for players to see eachothers roles.
* Because of how check_victory works, the game is halted in other places by this point.
+ * We won't delete ourselves in a certain amount of time in unit tests, as the unit test will handle our deletion instead.
*
* What players do in this phase:
* * See everyone's role postgame
@@ -389,7 +411,9 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
roles.mafia_alert.update_text("[message]")
roles.reveal_role(src)
phase = MAFIA_PHASE_VICTORY_LAP
+#ifndef UNIT_TESTS
next_phase_timer = QDEL_IN_STOPPABLE(src, VICTORY_LAP_PERIOD_LENGTH)
+#endif
/**
* Cleans up the game, resetting variables back to the beginning and removing the map with the generator.
@@ -556,7 +580,7 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
if(phase != MAFIA_PHASE_VOTING)
return
- var/v = get_vote_count(player_role_lookup[source],"Day")
+ var/v = get_vote_count(get_role_player(source),"Day")
var/mutable_appearance/MA = mutable_appearance('icons/obj/mafia.dmi',"vote_[v > 12 ? "over_12" : v]")
overlay_list += MA
@@ -582,263 +606,59 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
H.equipOutfit(outfit_to_distribute)
H.status_flags |= GODMODE
RegisterSignal(H, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(display_votes))
- var/datum/action/innate/mafia_panel/mafia_panel = new(null,src)
- mafia_panel.Grant(H)
+ var/obj/item/modular_computer/modpc = role.player_pda
+ role.register_body(H)
+ if(modpc)
+ player_role_lookup[modpc] = role
+ else
+ player_role_lookup[role.player_key] = role
var/client/player_client = GLOB.directory[role.player_key]
if(player_client)
- player_client.prefs.safe_transfer_prefs_to(H, is_antag = TRUE)
- role.body = H
- player_role_lookup[H] = role
- role.put_player_in_body(player_client)
+ role.put_player_in_body(player_client)
role.greet()
-/datum/mafia_controller/ui_static_data(mob/user)
- var/list/data = list()
-
- if(user.client?.holder)
- data["admin_controls"] = TRUE //show admin buttons to start/setup/stop
- data["all_roles"] = current_setup_text
-
- var/datum/mafia_role/user_role = player_role_lookup[user]
- if(user_role)
- data["roleinfo"] = list(
- "role" = user_role.name,
- "desc" = user_role.desc,
- "hud_icon" = user_role.hud_icon,
- "revealed_icon" = user_role.revealed_icon,
- )
-
- return data
-
-/datum/mafia_controller/ui_data(mob/user)
- var/list/data = list()
-
- data["phase"] = phase
- if(turn)
- data["turn"] = " - Day [turn]"
-
- if(phase == MAFIA_PHASE_SETUP)
- data["lobbydata"] = list()
- for(var/key in GLOB.mafia_signup + GLOB.mafia_bad_signup)
- var/list/lobby_member = list()
- lobby_member["name"] = key
- lobby_member["status"] = (key in GLOB.mafia_bad_signup) ? "Disconnected" : "Ready"
- data["lobbydata"] += list(lobby_member)
- return data
-
- data["timeleft"] = next_phase_timer ? timeleft(next_phase_timer) : 0 //the tgui menu counts this down.
-
- var/datum/mafia_role/user_role = player_role_lookup[user]
- if(user_role)
- data["user_notes"] = user_role.written_notes
-
- data["players"] = list()
- for(var/datum/mafia_role/role as anything in all_roles)
- var/list/player_info = list()
- player_info["name"] = role.body.real_name
- player_info["ref"] = REF(role)
- player_info["alive"] = role.game_status == MAFIA_ALIVE
- player_info["possible_actions"] = list()
-
- if(user_role) //not observer
- for(var/datum/mafia_ability/action as anything in user_role.role_unique_actions)
- if(action.validate_action_target(src, potential_target = role, silent = TRUE))
- player_info["possible_actions"] += list(list("name" = action, "ref" = REF(action)))
-
- data["players"] += list(player_info)
+///From a 'user' (Either a mob or ModPC) or TGUI UI, will try to find the Mafia role from 'player_role_lookup'.
+/datum/mafia_controller/proc/get_role_player(atom/user, datum/tgui/ui)
+ var/obj/item/modular_computer/modpc = ui?.src_object || user
+ if(istype(modpc))
+ return player_role_lookup[modpc]
+ var/mob/mob_user = user
+ if(!istype(mob_user))
+ CRASH("[user] is not a modPC nor a mob, but we are trying to find their role.")
+ return player_role_lookup[mob_user.ckey]
- return data
-
-/datum/mafia_controller/ui_assets(mob/user)
- return list(
- get_asset_datum(/datum/asset/spritesheet/mafia),
- )
-
-/datum/mafia_controller/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
- . = ..()
- if(.)
- return
- var/datum/mafia_role/user_role = player_role_lookup[usr]
- //Admin actions
- if(usr.client?.holder)
- switch(action)
- if("new_game")
- if(phase == MAFIA_PHASE_SETUP)
- return
- basic_setup()
- if("nuke")
- qdel(src)
- if("next_phase")
- if(phase == MAFIA_PHASE_SETUP)
- return
- var/datum/timedevent/timer = SStimer.timer_id_dict[next_phase_timer]
- if(!timer.spent)
- var/datum/callback/tc = timer.callBack
- deltimer(next_phase_timer)
- tc.InvokeAsync()
- return TRUE
- if("players_home")
- var/list/failed = list()
- for(var/datum/mafia_role/player in all_roles)
- if(!player.body)
- failed += player
- continue
- player.body.forceMove(get_turf(player.assigned_landmark))
- if(failed.len)
- to_chat(usr, "List of players who no longer had a body (if you see this, the game is runtiming anyway so just hit \"New Game\" to end it)")
- for(var/i in failed)
- var/datum/mafia_role/fail = i
- to_chat(usr, fail.player_key)
- if("debug_setup")
- var/list/debug_setup = list()
- var/list/rolelist_dict = list("CANCEL", "FINISH") + GLOB.mafia_roles_by_name
- var/done = FALSE
-
- while(!done)
- to_chat(usr, "You have a total player count of [assoc_value_sum(debug_setup)] in this setup.")
- var/chosen_role_name = tgui_input_list(usr, "Select a role!", "Custom Setup Creation", rolelist_dict)
- if(!chosen_role_name)
- return
- switch(chosen_role_name)
- if("CANCEL")
- done = TRUE
- return
- if("FINISH")
- done = TRUE
- break
- else
- var/found_path = rolelist_dict[chosen_role_name]
- var/role_count = tgui_input_number(usr, "How many? Zero to cancel.", "Custom Setup Creation", 0, 12)
- if(role_count > 0)
- debug_setup[found_path] = role_count
- custom_setup = debug_setup
- early_start = TRUE
- try_autostart()//don't worry, this fails if there's a game in progress
- if("cancel_setup")
- custom_setup = list()
- if("start_now")
- forced_setup()
-
- switch(action) //both living and dead
- if("mf_lookup")
- var/role_lookup = params["role_name"]
- var/datum/mafia_role/helper
- for(var/datum/mafia_role/role as anything in all_roles)
- if(role_lookup == role.name)
- helper = role
- break
- helper.show_help(usr)
-
- if(!user_role)//just the dead
- switch(action)
- if("mf_signup")
- var/client/C = ui.user.client
- if(!SSticker.HasRoundStarted())
- to_chat(usr, span_warning("Wait for the round to start."))
- return
- if(GLOB.mafia_signup[C.ckey])
- GLOB.mafia_signup -= C.ckey
- GLOB.mafia_early_votes -= C.ckey //Remove their early start vote as well
- to_chat(usr, span_notice("You unregister from Mafia."))
- return TRUE
- else
- GLOB.mafia_signup[C.ckey] = TRUE
- to_chat(usr, span_notice("You sign up for Mafia."))
- if(phase == MAFIA_PHASE_SETUP)
- check_signups()
- try_autostart()
- return TRUE
- if("vote_to_start")
- var/client/C = ui.user.client
- if(phase != MAFIA_PHASE_SETUP)
- to_chat(usr, span_notice("You cannot vote to start while a game is underway!"))
- return
- if(!GLOB.mafia_signup[C.ckey])
- to_chat(usr, span_notice("You must be signed up for this game to vote!"))
- return
- if(GLOB.mafia_early_votes[C.ckey])
- GLOB.mafia_early_votes -= C.ckey
- to_chat(usr, span_notice("You are no longer voting to start the game early."))
- else
- GLOB.mafia_early_votes[C.ckey] = C
- to_chat(usr, span_notice("You vote to start the game early ([length(GLOB.mafia_early_votes)] out of [max(round(length(GLOB.mafia_signup) / 2), round(MAFIA_MIN_PLAYER_COUNT / 2))])."))
- if(check_start_votes()) //See if we have enough votes to start
- forced_setup()
- return TRUE
-
- if(user_role && user_role.game_status == MAFIA_DEAD)
- return
-
- //User actions (just living)
- switch(action)
- if("change_notes")
- if(user_role.game_status == MAFIA_DEAD)
- return TRUE
- user_role.written_notes = params["new_notes"]
- user_role.body.balloon_alert(user_role.body, "notes saved")
- return TRUE
- if("send_notes_to_chat")
- if(user_role.game_status == MAFIA_DEAD || !user_role.written_notes)
- return TRUE
- if(phase == MAFIA_PHASE_NIGHT)
- return TRUE
- if(!COOLDOWN_FINISHED(user_role, note_chat_sending_cooldown))
- return FALSE
- COOLDOWN_START(user_role, note_chat_sending_cooldown, MAFIA_NOTE_SENDING_COOLDOWN)
- user_role.body.say("[user_role.written_notes]", forced = "mafia notes sending")
- return TRUE
- if("perform_action")
- var/datum/mafia_role/target = locate(params["target"]) in all_roles
- if(!istype(target))
- return
- var/datum/mafia_ability/used_action = locate(params["action_ref"]) in user_role.role_unique_actions
- if(!used_action)
- return
- switch(phase)
- if(MAFIA_PHASE_DAY, MAFIA_PHASE_VOTING)
- used_action.using_ability = TRUE
- used_action.perform_action_target(src, target)
- if(MAFIA_PHASE_NIGHT)
- used_action.set_target(src, target)
+/**
+ * Signs the player up for Mafia, or removes them from the list if they are already
+ * signed up.
+ * Args:
+ * - ghost_client: is the client of the observer signing up. This can be null in favor of modpc.
+ * - modpc: is a living player signing up through a PDA. This can be null in favor of ghost_client.
+ */
+/datum/mafia_controller/proc/signup_mafia(mob/user, client/ghost_client, obj/item/modular_computer/modpc)
+ if(!SSticker.HasRoundStarted())
+ to_chat(user, span_warning("Wait for the round to start."))
+ return FALSE
+ if(isnull(modpc))
+ if(GLOB.mafia_signup[ghost_client.ckey])
+ GLOB.mafia_signup -= ghost_client.ckey
+ GLOB.mafia_early_votes -= ghost_client.ckey //Remove their early start vote as well
+ to_chat(user, span_notice("You unregister from Mafia."))
+ else
+ GLOB.mafia_signup[ghost_client.ckey] = TRUE
+ to_chat(user, span_notice("You sign up for Mafia."))
+ else
+ if(GLOB.pda_mafia_signup[modpc])
+ GLOB.pda_mafia_signup -= modpc
+ GLOB.mafia_early_votes -= modpc //Remove their early start vote as well
+ to_chat(user, span_notice("You unregister from Mafia."))
return TRUE
-
- if(user_role != on_trial)
- switch(action)
- if("vote_abstain")
- if(phase != MAFIA_PHASE_JUDGEMENT || (user_role in judgement_abstain_votes))
- return
- to_chat(user_role.body,"You have decided to abstain.")
- judgement_innocent_votes -= user_role
- judgement_guilty_votes -= user_role
- judgement_abstain_votes += user_role
- if("vote_innocent")
- if(phase != MAFIA_PHASE_JUDGEMENT || (user_role in judgement_innocent_votes))
- return
- to_chat(user_role.body,"Your vote on [on_trial.body.real_name] submitted as INNOCENT!")
- judgement_abstain_votes -= user_role//no fakers, and...
- judgement_guilty_votes -= user_role//no radical centrism
- judgement_innocent_votes += user_role
- if("vote_guilty")
- if(phase != MAFIA_PHASE_JUDGEMENT || (user_role in judgement_guilty_votes))
- return
- to_chat(user_role.body,"Your vote on [on_trial.body.real_name] submitted as GUILTY!")
- judgement_abstain_votes -= user_role//no fakers, and...
- judgement_innocent_votes -= user_role//no radical centrism
- judgement_guilty_votes += user_role
-
-/datum/mafia_controller/ui_state(mob/user)
- return GLOB.always_state
-
-/datum/mafia_controller/ui_interact(mob/user, datum/tgui/ui)
- ui = SStgui.try_update_ui(user, src, null)
- if(!ui)
- ui = new(user, src, "MafiaPanel")
- ui.open()
-
-/proc/assoc_value_sum(list/L)
- . = 0
- for(var/key in L)
- . += L[key]
+ else
+ GLOB.pda_mafia_signup[modpc] = TRUE
+ to_chat(user, span_notice("You sign up for Mafia."))
+ if(phase == MAFIA_PHASE_SETUP)
+ check_signups()
+ try_autostart()
+ return TRUE
/**
* Returns a standard setup, with certain important/unique roles guaranteed.
@@ -883,15 +703,16 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
/datum/mafia_controller/proc/basic_setup()
var/req_players = MAFIA_MAX_PLAYER_COUNT
var/list/setup = custom_setup
- if(setup.len)
+ if(length(setup))
req_players = assoc_value_sum(setup)
- var/list/filtered_keys = filter_players(req_players)
- var/needed_players = length(filtered_keys)
+ var/list/filtered_keys_and_pdas = filter_players(req_players)
- if(!setup.len) //don't actually have one yet, so generate a max player random setup. it's good to do this here instead of above so it doesn't generate one every time a game could possibly start.
- setup = generate_standard_setup(needed_players)
- prepare_game(setup, filtered_keys)
+ //don't actually have one yet, so generate a max player random setup.
+ //it's good to do this here instead of above so it doesn't generate one every time a game could possibly start.
+ if(!length(setup))
+ setup = generate_standard_setup(length(filtered_keys_and_pdas))
+ prepare_game(setup, filtered_keys_and_pdas)
start_game()
/**
@@ -902,16 +723,15 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
/datum/mafia_controller/proc/forced_setup()
check_signups() //Refresh the signup list, so our numbers are accurate and we only take active players into consideration.
- var/list/filtered_keys = filter_players(length(GLOB.mafia_signup))
- var/req_players = length(filtered_keys)
-
- if(!req_players) //If we have nobody signed up, we give up on starting
+ var/players_needed = GLOB.mafia_signup.len + GLOB.pda_mafia_signup.len
+ var/list/filtered_keys_and_pdas = filter_players(players_needed)
+ if(!length(filtered_keys_and_pdas)) //If we have nobody signed up, we give up on starting
log_admin("Attempted to force a mafia game to start with nobody signed up!")
return
- var/list/setup = generate_standard_setup(req_players)
+ var/list/setup = generate_standard_setup(length(filtered_keys_and_pdas))
- prepare_game(setup, filtered_keys)
+ prepare_game(setup, filtered_keys_and_pdas)
early_start = TRUE
start_game()
@@ -925,10 +745,10 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
/datum/mafia_controller/proc/check_start_votes()
check_signups() //Same as before. What a useful proc.
- if(length(GLOB.mafia_signup) < MAFIA_MIN_PLAYER_COUNT)
+ if(length(GLOB.mafia_signup + GLOB.pda_mafia_signup) < MAFIA_MIN_PLAYER_COUNT)
return FALSE //Make sure we have the minimum playercount to host a game first.
- if(length(GLOB.mafia_early_votes) < round(length(GLOB.mafia_signup) / 2))
+ if(length(GLOB.mafia_early_votes) < round(length(GLOB.mafia_signup + GLOB.pda_mafia_signup) / 2))
return FALSE
return TRUE
@@ -942,38 +762,44 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
* This should only be run as we are in the process of starting a game.
*
* max_players - The maximum number of keys to put in our return list before we start telling people they're not getting in.
- * filtered_keys - A list of player ckeys, to be included in the game.
+ * filtered_keys_and_pdas - A list of player ckeys and PDAs, to be included in the game.
*/
/datum/mafia_controller/proc/filter_players(max_players)
//final list for all the players who will be in this game
- var/list/filtered_keys = list()
+ var/list/filtered_keys_and_pdas = list()
//cuts invalid players from signups (disconnected/not a ghost)
- var/list/possible_keys = list()
+ var/list/possible_players = list()
for(var/key in GLOB.mafia_signup)
if(GLOB.directory[key])
var/client/C = GLOB.directory[key]
if(isobserver(C.mob))
- possible_keys += key
+ possible_players += key
continue
GLOB.mafia_signup -= key //not valid to play when we checked so remove them from signups
- //If we're not over capacity and don't need to notify anyone of their exclusion, return early.
- if(length(possible_keys) < max_players)
- return filtered_keys
+ for(var/obj/item/modular_computer/pda/pdas as anything in GLOB.pda_mafia_signup)
+ possible_players += pdas
//if there were too many players, still start but only make filtered keys as big as it needs to be (cut excess)
//also removes people who do get into final player list from the signup so they have to sign up again when game ends
for(var/i in 1 to max_players)
- var/chosen_key = pick_n_take(possible_keys)
- filtered_keys += chosen_key
- GLOB.mafia_signup -= chosen_key
+ if(!length(possible_players))
+ break
+ var/chosen_key = pick_n_take(possible_players)
+ filtered_keys_and_pdas += chosen_key
+ if(chosen_key in GLOB.pda_mafia_signup)
+ GLOB.pda_mafia_signup -= chosen_key
+ else if(chosen_key in GLOB.mafia_signup)
+ GLOB.mafia_signup -= chosen_key
//small message about not getting into this game for clarity on why they didn't get in
- for(var/unpicked in possible_keys)
+ for(var/unpicked in possible_players)
+ if(!(unpicked in GLOB.mafia_signup))
+ continue
var/client/unpicked_client = GLOB.directory[unpicked]
to_chat(unpicked_client, span_danger("Sorry, the starting mafia game has too many players and you were not picked."))
to_chat(unpicked_client, span_warning("You're still signed up, getting messages from the current round, and have another chance to join when the one starting now finishes."))
- return filtered_keys
+ return filtered_keys_and_pdas
/**
* Called when someone signs up, and sees if there are enough people in the signup list to begin.
@@ -983,7 +809,7 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
/datum/mafia_controller/proc/try_autostart()
if(phase != MAFIA_PHASE_SETUP || !(GLOB.ghost_role_flags & GHOSTROLE_MINIGAME))
return
- if(GLOB.mafia_signup.len >= MAFIA_MAX_PLAYER_COUNT || custom_setup)//enough people to try and make something (or debug mode)
+ if((GLOB.mafia_signup.len + GLOB.pda_mafia_signup.len) >= MAFIA_MAX_PLAYER_COUNT || custom_setup)//enough people to try and make something (or debug mode)
basic_setup()
/**
@@ -992,6 +818,7 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
* If a disconnected player gets a non-ghost mob and reconnects, they will be first put back into mafia_signup then filtered by that.
*/
/datum/mafia_controller/proc/check_signups()
+#ifndef UNIT_TESTS
for(var/bad_key in GLOB.mafia_bad_signup)
if(GLOB.directory[bad_key])//they have reconnected if we can search their key and get a client
GLOB.mafia_bad_signup -= bad_key
@@ -1005,6 +832,7 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
if(!isobserver(C.mob))
//they are back to playing the game, remove them from the signups
GLOB.mafia_signup -= key
+#endif
/datum/action/innate/mafia_panel
name = "Mafia Panel"
@@ -1018,6 +846,10 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
. = ..()
controller_panel = controller
+/datum/action/innate/mafia_panel/Destroy()
+ . = ..()
+ controller_panel = null
+
/datum/action/innate/mafia_panel/Activate()
controller_panel.ui_interact(owner)
@@ -1034,7 +866,7 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
maptext_height = 480
maptext_width = 480
///The client that owns the popup.
- var/datum/mafia_role/mafia/owner
+ var/datum/mafia_role/owner
/atom/movable/screen/mafia_popup/Initialize(mapload, datum/mafia_role/mafia)
. = ..()
@@ -1045,6 +877,9 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
return ..()
/atom/movable/screen/mafia_popup/proc/update_text(text)
+ owner.role_messages += text
+ if(!owner.body.client)
+ return
maptext = MAPTEXT(" [text]")
maptext_width = view_to_pixels(owner.body.client?.view_size.getView())[1]
owner.body.client?.screen += src
@@ -1063,3 +898,5 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
QDEL_NULL(GLOB.mafia_game)
var/datum/mafia_controller/new_controller = new()
return new_controller
+
+#undef UNIVERSALLY_HATED_REQUIREMENT
diff --git a/code/modules/mafia/controller_ui.dm b/code/modules/mafia/controller_ui.dm
new file mode 100644
index 0000000000000..9caa25d9f74de
--- /dev/null
+++ b/code/modules/mafia/controller_ui.dm
@@ -0,0 +1,261 @@
+// 'user' can be a modPC, hence why it's pathed to the atom
+/datum/mafia_controller/ui_static_data(atom/user)
+ var/list/data = list()
+
+ if(usr.client?.holder)
+ data["admin_controls"] = TRUE //show admin buttons to start/setup/stop
+ data["is_observer"] = isobserver(user)
+ data["all_roles"] = current_setup_text
+
+ if(phase == MAFIA_PHASE_SETUP)
+ return data
+
+ var/datum/mafia_role/user_role = get_role_player(user)
+ if(user_role)
+ data["roleinfo"] = list(
+ "role" = user_role.name,
+ "desc" = user_role.desc,
+ "hud_icon" = user_role.hud_icon,
+ "revealed_icon" = user_role.revealed_icon,
+ )
+
+ return data
+
+// 'user' can be a modPC, hence why it's pathed to the atom
+/datum/mafia_controller/ui_data(atom/user)
+ var/list/data = list()
+
+ data["phase"] = phase
+ if(turn)
+ data["turn"] = " - Day [turn]"
+
+ if(phase == MAFIA_PHASE_SETUP)
+ data["lobbydata"] = list()
+ for(var/key in GLOB.mafia_signup + GLOB.mafia_bad_signup + GLOB.pda_mafia_signup)
+ var/list/lobby_member = list()
+ lobby_member["name"] = key
+ lobby_member["status"] = (key in GLOB.mafia_bad_signup) ? "Disconnected" : "Ready"
+ data["lobbydata"] += list(lobby_member)
+ return data
+
+ data["timeleft"] = next_phase_timer ? timeleft(next_phase_timer) : 0
+
+ var/datum/mafia_role/user_role = get_role_player(user)
+ if(user_role)
+ data["user_notes"] = user_role.written_notes
+ var/list/ui_messages = list()
+ for(var/i = user_role.role_messages.len to 1 step -1)
+ ui_messages.Add(list(list(
+ "msg" = user_role.role_messages[i],
+ )))
+ data["messages"] = ui_messages
+
+ data["players"] = list()
+ for(var/datum/mafia_role/role as anything in all_roles)
+ var/list/player_info = list()
+ player_info["name"] = role.body.real_name
+ player_info["ref"] = REF(role)
+ player_info["alive"] = role.game_status == MAFIA_ALIVE
+ player_info["possible_actions"] = list()
+
+ if(user_role) //not observer
+ for(var/datum/mafia_ability/action as anything in user_role.role_unique_actions)
+ if(action.validate_action_target(src, potential_target = role, silent = TRUE))
+ player_info["possible_actions"] += list(list("name" = action, "ref" = REF(action)))
+
+ data["players"] += list(player_info)
+
+ return data
+
+/datum/mafia_controller/ui_assets(mob/user)
+ return list(
+ get_asset_datum(/datum/asset/spritesheet/mafia),
+ )
+
+/datum/mafia_controller/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+ var/datum/mafia_role/user_role = get_role_player(usr, ui)
+ var/obj/item/modular_computer/modpc = ui.src_object
+ if(!istype(modpc))
+ modpc = null
+ //Admin actions
+ if(ui.user.client.holder)
+ switch(action)
+ if("new_game")
+ if(phase == MAFIA_PHASE_SETUP)
+ return
+ basic_setup()
+ if("nuke")
+ qdel(src)
+ if("next_phase")
+ if(phase == MAFIA_PHASE_SETUP)
+ return
+ var/datum/timedevent/timer = SStimer.timer_id_dict[next_phase_timer]
+ if(!timer.spent)
+ var/datum/callback/tc = timer.callBack
+ deltimer(next_phase_timer)
+ tc.InvokeAsync()
+ return TRUE
+ if("players_home")
+ var/list/failed = list()
+ for(var/datum/mafia_role/player in all_roles)
+ if(!player.body)
+ failed += player
+ continue
+ player.body.forceMove(get_turf(player.assigned_landmark))
+ if(failed.len)
+ to_chat(usr, "List of players who no longer had a body (if you see this, the game is runtiming anyway so just hit \"New Game\" to end it)")
+ for(var/datum/mafia_role/fail as anything in failed)
+ to_chat(usr, fail.player_key || fail.player_pda)
+ if("debug_setup")
+ var/list/debug_setup = list()
+ var/list/rolelist_dict = list("CANCEL", "FINISH") + GLOB.mafia_roles_by_name
+ var/done = FALSE
+
+ while(!done)
+ to_chat(usr, "You have a total player count of [assoc_value_sum(debug_setup)] in this setup.")
+ var/chosen_role_name = tgui_input_list(usr, "Select a role!", "Custom Setup Creation", rolelist_dict)
+ if(!chosen_role_name)
+ return
+ switch(chosen_role_name)
+ if("CANCEL")
+ done = TRUE
+ return
+ if("FINISH")
+ done = TRUE
+ break
+ else
+ var/found_path = rolelist_dict[chosen_role_name]
+ var/role_count = tgui_input_number(usr, "How many? Zero to cancel.", "Custom Setup Creation", 0, 12)
+ if(role_count > 0)
+ debug_setup[found_path] = role_count
+ custom_setup = debug_setup
+ early_start = TRUE
+ try_autostart()//don't worry, this fails if there's a game in progress
+ if("cancel_setup")
+ custom_setup = list()
+ if("start_now")
+ forced_setup()
+
+ switch(action) //both living and dead
+ if("mf_lookup")
+ var/role_lookup = params["role_name"]
+ var/datum/mafia_role/helper
+ for(var/datum/mafia_role/role as anything in all_roles)
+ if(role_lookup == role.name)
+ helper = role
+ break
+ helper.show_help(usr)
+
+ if(!user_role)//just the dead
+ switch(action)
+ if("mf_signup")
+ if(signup_mafia(usr, ui.user.client, modpc))
+ return TRUE
+ if("vote_to_start")
+ var/client/ghost_client = ui.user.client
+ if(phase != MAFIA_PHASE_SETUP)
+ to_chat(usr, span_notice("You cannot vote to start while a game is underway!"))
+ return
+ if(isnull(modpc))
+ if(!GLOB.mafia_signup[ghost_client.ckey])
+ to_chat(usr, span_notice("You must be signed up for this game to vote!"))
+ return
+ if(GLOB.mafia_early_votes[ghost_client.ckey])
+ GLOB.mafia_early_votes -= ghost_client.ckey
+ to_chat(usr, span_notice("You are no longer voting to start the game early."))
+ else
+ GLOB.mafia_early_votes[ghost_client.ckey] = ghost_client
+ to_chat(usr, span_notice("You vote to start the game early ([length(GLOB.mafia_early_votes)] out of [max(round(length(GLOB.mafia_signup + GLOB.pda_mafia_signup) / 2), round(MAFIA_MIN_PLAYER_COUNT / 2))])."))
+ if(check_start_votes()) //See if we have enough votes to start
+ forced_setup()
+ else
+ if(!GLOB.pda_mafia_signup[modpc])
+ to_chat(usr, span_notice("You must be signed up for this game to vote!"))
+ return
+ if(GLOB.mafia_early_votes[modpc])
+ GLOB.mafia_early_votes -= modpc
+ to_chat(usr, span_notice("You are no longer voting to start the game early."))
+ else
+ GLOB.mafia_early_votes[modpc] = modpc
+ to_chat(usr, span_notice("You vote to start the game early ([length(GLOB.mafia_early_votes)] out of [max(round(length(GLOB.mafia_signup + GLOB.pda_mafia_signup) / 2), round(MAFIA_MIN_PLAYER_COUNT / 2))])."))
+ if(check_start_votes()) //See if we have enough votes to start
+ forced_setup()
+ return TRUE
+
+ if(user_role && user_role.game_status == MAFIA_DEAD)
+ return
+
+ //User actions (just living)
+ switch(action)
+ if("change_notes")
+ if(user_role.game_status == MAFIA_DEAD)
+ return TRUE
+ user_role.written_notes = sanitize_text(params["new_notes"])
+ user_role.send_message_to_player("notes saved", balloon_alert = TRUE)
+ return TRUE
+ if("send_message_to_chat")
+ if(user_role.game_status == MAFIA_DEAD)
+ return TRUE
+ var/message_said = sanitize_text(params["message"])
+ user_role.body.say(message_said, forced = "mafia chat (sent by [ui.user.client])")
+ return TRUE
+ if("send_notes_to_chat")
+ if(user_role.game_status == MAFIA_DEAD || !user_role.written_notes)
+ return TRUE
+ if(phase == MAFIA_PHASE_NIGHT)
+ return TRUE
+ if(!COOLDOWN_FINISHED(user_role, note_chat_sending_cooldown))
+ return FALSE
+ COOLDOWN_START(user_role, note_chat_sending_cooldown, MAFIA_NOTE_SENDING_COOLDOWN)
+ user_role.body.say("[user_role.written_notes]", forced = "mafia notes sending")
+ return TRUE
+ if("perform_action")
+ var/datum/mafia_role/target = locate(params["target"]) in all_roles
+ if(!istype(target))
+ return
+ var/datum/mafia_ability/used_action = locate(params["action_ref"]) in user_role.role_unique_actions
+ if(!used_action)
+ return
+ switch(phase)
+ if(MAFIA_PHASE_DAY, MAFIA_PHASE_VOTING)
+ used_action.using_ability = TRUE
+ used_action.perform_action_target(src, target)
+ if(MAFIA_PHASE_NIGHT)
+ used_action.set_target(src, target)
+ return TRUE
+
+ if(user_role != on_trial)
+ switch(action)
+ if("vote_abstain")
+ if(phase != MAFIA_PHASE_JUDGEMENT || (user_role in judgement_abstain_votes))
+ return
+ user_role.send_message_to_player("You have decided to abstain.")
+ judgement_innocent_votes -= user_role
+ judgement_guilty_votes -= user_role
+ judgement_abstain_votes += user_role
+ if("vote_innocent")
+ if(phase != MAFIA_PHASE_JUDGEMENT || (user_role in judgement_innocent_votes))
+ return
+ user_role.send_message_to_player("Your vote on [on_trial.body.real_name] submitted as INNOCENT!")
+ judgement_abstain_votes -= user_role//no fakers, and...
+ judgement_guilty_votes -= user_role//no radical centrism
+ judgement_innocent_votes += user_role
+ if("vote_guilty")
+ if(phase != MAFIA_PHASE_JUDGEMENT || (user_role in judgement_guilty_votes))
+ return
+ user_role.send_message_to_player("Your vote on [on_trial.body.real_name] submitted as GUILTY!")
+ judgement_abstain_votes -= user_role//no fakers, and...
+ judgement_innocent_votes -= user_role//no radical centrism
+ judgement_guilty_votes += user_role
+
+/datum/mafia_controller/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/mafia_controller/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, null)
+ if(!ui)
+ ui = new(user, src, "MafiaPanel")
+ ui.open()
diff --git a/code/modules/mafia/map_pieces.dm b/code/modules/mafia/map_pieces.dm
index 342f4a9045a94..f3a6baced78c5 100644
--- a/code/modules/mafia/map_pieces.dm
+++ b/code/modules/mafia/map_pieces.dm
@@ -26,61 +26,67 @@
MF = create_mafia_game()
MF.ui_interact(user)
-/area/centcom/mafia
- name = "Mafia Minigame"
- icon_state = "mafia"
- static_lighting = FALSE
-
- base_lighting_alpha = 255
- requires_power = FALSE
- has_gravity = STANDARD_GRAVITY
- flags_1 = NONE
- area_flags = BLOCK_SUICIDE | UNIQUE_AREA
-
/datum/map_template/mafia
should_place_on_top = FALSE
+ ///The map suffix to put onto the mappath.
+ var/map_suffix
///A brief background tidbit
var/description = ""
///What costume will this map force players to start with?
var/custom_outfit
+/datum/map_template/mafia/New(path = null, rename = null, cache = FALSE)
+ path = "_maps/map_files/Mafia/" + map_suffix
+ return ..()
+
+//we only have one map in unit tests for consistency.
+#ifdef UNIT_TESTS
+/datum/map_template/mafia/unit_test
+ name = "Mafia Unit Test"
+ description = "A map designed specifically for Unit Testing to ensure the game runs properly."
+ map_suffix = "mafia_unit_test.dmm"
+
+#else
+
/datum/map_template/mafia/summerball
name = "Summerball 2020"
description = "The original, the OG. The 2020 Summer ball was where mafia came from, with this map."
- mappath = "_maps/map_files/Mafia/mafia_ball.dmm"
-
-/datum/map_template/mafia/syndicate
- name = "Syndicate Megastation"
- description = "Yes, it's a very confusing day at the Megastation. Will the syndicate conflict resolution operatives succeed?"
- mappath = "_maps/map_files/Mafia/mafia_syndie.dmm"
- custom_outfit = /datum/outfit/mafia/syndie
-
-/datum/map_template/mafia/lavaland
- name = "Lavaland Excursion"
- description = "The station has no idea what's going down on lavaland right now, we got changelings... traitors, and worst of all... lawyers roleblocking you every night."
- mappath = "_maps/map_files/Mafia/mafia_lavaland.dmm"
- custom_outfit = /datum/outfit/mafia/lavaland
+ map_suffix = "mafia_ball.dmm"
/datum/map_template/mafia/ufo
name = "Alien Mothership"
description = "The haunted ghost UFO tour has gone south and now it's up to our fine townies and scare seekers to kill the actual real alien changelings..."
- mappath = "_maps/map_files/Mafia/mafia_ayylmao.dmm"
+ map_suffix = "mafia_ayylmao.dmm"
custom_outfit = /datum/outfit/mafia/abductee
/datum/map_template/mafia/spider_clan
name = "Spider Clan Kidnapping"
description = "New and improved spider clan kidnappings are a lot less boring and have a lot more lynching. Damn westaboos!"
- mappath = "_maps/map_files/Mafia/mafia_spiderclan.dmm"
+ map_suffix = "mafia_spiderclan.dmm"
custom_outfit = /datum/outfit/mafia/ninja
+/datum/map_template/mafia/gothic
+ name = "Vampire's Castle"
+ description = "Vampires and changelings clash to find out who's the superior bloodsucking monster in this creepy castle map."
+ map_suffix = "mafia_gothic.dmm"
+ custom_outfit = /datum/outfit/mafia/gothic
+
+/datum/map_template/mafia/syndicate
+ name = "Syndicate Megastation"
+ description = "Yes, it's a very confusing day at the Megastation. Will the syndicate conflict resolution operatives succeed?"
+ map_suffix = "mafia_syndie.dmm"
+ custom_outfit = /datum/outfit/mafia/syndie
+
/datum/map_template/mafia/snowy
name = "Snowdin"
description = "Based off of the icy moon map of the same name, the guy who reworked it did a good enough job to recieve a derivative piece of work based on it. Cool!"
- mappath = "_maps/map_files/Mafia/mafia_snow.dmm"
+ map_suffix = "mafia_snow.dmm"
custom_outfit = /datum/outfit/mafia/snowy
-/datum/map_template/mafia/gothic
- name = "Vampire's Castle"
- description = "Vampires and changelings clash to find out who's the superior bloodsucking monster in this creepy castle map."
- mappath = "_maps/map_files/Mafia/mafia_gothic.dmm"
- custom_outfit = /datum/outfit/mafia/gothic
+/datum/map_template/mafia/lavaland
+ name = "Lavaland Excursion"
+ description = "The station has no idea what's going down on lavaland right now, we got changelings... traitors, and worst of all... lawyers roleblocking you every night."
+ map_suffix = "mafia_lavaland.dmm"
+ custom_outfit = /datum/outfit/mafia/lavaland
+
+#endif
diff --git a/code/modules/mafia/roles/changelings/changeling.dm b/code/modules/mafia/roles/changelings/changeling.dm
index 2415e39f8ee63..8e650515ffbc7 100644
--- a/code/modules/mafia/roles/changelings/changeling.dm
+++ b/code/modules/mafia/roles/changelings/changeling.dm
@@ -1,6 +1,6 @@
/datum/mafia_role/mafia
name = "Changeling"
- desc = "You're a member of the changeling hive. Use ':j' talk prefix to talk to your fellow lings."
+ desc = "You're a member of the changeling hive. You may speak with your fellow Changelings at night."
team = MAFIA_TEAM_MAFIA
role_type = MAFIA_REGULAR
role_flags = ROLE_CAN_KILL
diff --git a/code/modules/mafia/roles/roles.dm b/code/modules/mafia/roles/roles.dm
index e8b5f8a63ab41..f301a8ac02aac 100644
--- a/code/modules/mafia/roles/roles.dm
+++ b/code/modules/mafia/roles/roles.dm
@@ -15,10 +15,21 @@
///The player's written notes, that they can send to chat at any time.
var/written_notes
+ ///The ckey of the person playing as this Mafia role, CAN BE NULL IN FAVOR OF player_pda.
var/player_key
+ ///The PDA of the person playing as this Mafia role, CAN BE NULL IN FAVOR OF player_key.
+ var/obj/item/modular_computer/player_pda
+
+ ///List of all messages this role got throughout the game.
+ var/list/role_messages = list()
+
+
var/mob/living/carbon/human/body
var/obj/effect/landmark/mafia/assigned_landmark
+ ///The Mafia innate action panel that allows players to view the game's state.
+ var/datum/action/innate/mafia_panel/mafia_panel
+
///how many votes submitted when you vote. used in voting and deciding victory.
var/vote_power = 1
///what they get equipped with when they are revealed
@@ -41,16 +52,65 @@
/datum/mafia_role/New(datum/mafia_controller/game)
. = ..()
+ mafia_panel = new(null, game)
for(var/datum/mafia_ability/abilities as anything in role_unique_actions + /datum/mafia_ability/voting)
role_unique_actions += new abilities(game, src)
role_unique_actions -= abilities
/datum/mafia_role/Destroy(force, ...)
+ UnregisterSignal(body, COMSIG_MOB_SAY)
QDEL_NULL(mafia_alert)
- QDEL_NULL(body)
+ QDEL_NULL(mafia_panel)
QDEL_LIST(role_unique_actions)
+ //we null these instead of qdel because Mafia controller's mapdeleter deletes it all.
+ assigned_landmark = null
+ body = null
+ role_messages.Cut()
return ..()
+/datum/mafia_role/proc/register_body(mob/living/carbon/human/new_body)
+ if(body)
+ UnregisterSignal(new_body, COMSIG_MOB_SAY)
+ mafia_panel.Remove(body)
+ body = new_body
+ RegisterSignal(new_body, COMSIG_MOB_SAY, PROC_REF(handle_speech))
+ mafia_panel.Grant(new_body)
+
+/**
+ * send_message_to_player
+ *
+ * Sends a message to a player, checking if they are playing through a PDA or not.
+ * Args:
+ * * message - The message to send to the person
+ * * balloon_alert - Whether it should be as a balloon alert, only if it's to a non-PDA user.
+ */
+/datum/mafia_role/proc/send_message_to_player(message, balloon_alert = FALSE)
+ if(player_pda)
+ role_messages += message
+ return
+ if(balloon_alert)
+ body.balloon_alert(body, message)
+ return
+ to_chat(body, message)
+
+/**
+ * handle_speech
+ *
+ * Handles Mafia roles talking in chat.
+ * First it will go through their abilities for Ability-specific speech,
+ * if none affects it, we will go to day chat.
+ */
+/datum/mafia_role/proc/handle_speech(datum/source, list/speech_args)
+ SIGNAL_HANDLER
+ for(var/datum/mafia_ability/abilities as anything in role_unique_actions)
+ if(abilities.handle_speech(source, speech_args))
+ return
+ var/datum/mafia_controller/mafia_game = GLOB.mafia_game
+ if(!mafia_game || mafia_game.phase == MAFIA_PHASE_NIGHT)
+ return
+ var/message = "[source]: [html_decode(speech_args[SPEECH_MESSAGE])]"
+ mafia_game.send_message(message, log_only = TRUE)
+
/**
* Puts the player in their body and keeps track of their previous one to put them back in later.
* Adds the playing_mafia trait so people examining them will know why they're currently lacking a soul.
@@ -69,14 +129,15 @@
*
* Does not count as visiting, see visit proc.
*/
-/datum/mafia_role/proc/kill(datum/mafia_controller/game, datum/mafia_role/attacker, lynch=FALSE)
+/datum/mafia_role/proc/kill(datum/mafia_controller/game, datum/mafia_role/attacker, lynch = FALSE)
+ if(game_status == MAFIA_DEAD)
+ return FALSE
if(attacker && (attacker.role_flags & ROLE_ROLEBLOCKED))
return FALSE
if(SEND_SIGNAL(src, COMSIG_MAFIA_ON_KILL, game, attacker, lynch) & MAFIA_PREVENT_KILL)
return FALSE
- if(game_status != MAFIA_DEAD)
- game_status = MAFIA_DEAD
- body.death()
+ game_status = MAFIA_DEAD
+ body.death()
if(lynch)
reveal_role(game, verbose = TRUE)
game.living_roles -= src
diff --git a/code/modules/mapfluff/ruins/icemoonruin_code/mining_site.dm b/code/modules/mapfluff/ruins/icemoonruin_code/mining_site.dm
new file mode 100644
index 0000000000000..8a37031b8184a
--- /dev/null
+++ b/code/modules/mapfluff/ruins/icemoonruin_code/mining_site.dm
@@ -0,0 +1,41 @@
+/datum/outfit/minesite
+ name = "Mining Site Worker"
+
+ uniform = /obj/item/clothing/under/rank/cargo/miner
+ suit = /obj/item/clothing/suit/hooded/wintercoat
+ back = /obj/item/storage/backpack/duffelbag
+ gloves = /obj/item/clothing/gloves/color/black
+ shoes = /obj/item/clothing/shoes/winterboots/ice_boots
+ head = /obj/item/clothing/head/utility/hardhat/orange
+
+/datum/outfit/minesite/overseer
+ name = "Mining Site Overseer"
+
+ uniform = /obj/item/clothing/under/rank/cargo/qm
+ suit = /obj/item/clothing/suit/hooded/wintercoat
+ back = /obj/item/storage/backpack/duffelbag
+ gloves = /obj/item/clothing/gloves/color/black
+ shoes = /obj/item/clothing/shoes/winterboots/ice_boots
+ head = /obj/item/clothing/head/utility/hardhat/white
+ glasses = /obj/item/clothing/glasses/sunglasses
+ r_hand = /obj/item/megaphone
+ l_hand = /obj/item/clipboard
+
+/obj/effect/mob_spawn/corpse/human/minesite
+ name = "Mining Site Worker"
+ outfit = /datum/outfit/minesite
+ icon_state = "corpseminer"
+
+// Gives the minesite corpses the gutted effect so that the boss ignores them
+/obj/effect/mob_spawn/corpse/human/minesite/special(mob/living/spawned_mob)
+ . = ..()
+ spawned_mob.apply_status_effect(/datum/status_effect/gutted)
+
+/obj/effect/mob_spawn/corpse/human/minesite/overseer
+ name = "Mining Site Overseer"
+ outfit = /datum/outfit/minesite/overseer
+ icon_state = "corpsecargotech"
+
+/obj/item/paper/crumpled/bloody/ruins/mining_site
+ name = "blood-written note"
+ default_raw_text = "
STRENGTH... UNPARALLELED. UNNATURAL."
diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/biodome_winter.dm b/code/modules/mapfluff/ruins/lavalandruin_code/biodome_winter.dm
index adb5afaf5c83f..770d787f8ad54 100644
--- a/code/modules/mapfluff/ruins/lavalandruin_code/biodome_winter.dm
+++ b/code/modules/mapfluff/ruins/lavalandruin_code/biodome_winter.dm
@@ -18,7 +18,7 @@
. = ..()
. += span_notice("Throw this at objects or creatures to freeze them, it will boomerang back so be cautious!")
-/obj/item/freeze_cube/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, quickstart = TRUE)
+/obj/item/freeze_cube/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, gentle, quickstart = TRUE)
. = ..()
if(!.)
return
diff --git a/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm b/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm
index c9aee1ea54253..d8b6ab715ff1e 100644
--- a/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm
+++ b/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm
@@ -13,8 +13,8 @@
resistance_flags = FIRE_PROOF | LAVA_PROOF
max_integrity = 200
+ faction = list(FACTION_ASHWALKER)
- var/faction = list(FACTION_ASHWALKER)
var/meat_counter = 6
var/datum/team/ashwalkers/ashies
var/datum/linked_objective
diff --git a/code/modules/mapfluff/ruins/objects_and_mobs/necropolis_gate.dm b/code/modules/mapfluff/ruins/objects_and_mobs/necropolis_gate.dm
index 54e167fb25541..f2734bb7b05b8 100644
--- a/code/modules/mapfluff/ruins/objects_and_mobs/necropolis_gate.dm
+++ b/code/modules/mapfluff/ruins/objects_and_mobs/necropolis_gate.dm
@@ -190,7 +190,12 @@ GLOBAL_DATUM(necropolis_gate, /obj/structure/necropolis_gate/legion_gate)
M.playsound_local(T, null, 100, FALSE, 0, FALSE, pressure_affected = FALSE, sound_to_use = legion_sound)
flash_color(M, flash_color = "#FF0000", flash_time = 50)
var/mutable_appearance/release_overlay = mutable_appearance('icons/effects/effects.dmi', "legiondoor")
- notify_ghosts("Legion has been released in the [get_area(src)]!", source = src, alert_overlay = release_overlay, action = NOTIFY_JUMP, flashwindow = FALSE)
+ notify_ghosts(
+ "Legion has been released in the [get_area(src)]!",
+ source = src,
+ alert_overlay = release_overlay,
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ )
/obj/effect/decal/necropolis_gate_decal
icon = 'icons/effects/96x96.dmi'
diff --git a/code/modules/mapfluff/ruins/spaceruin_code/clericsden.dm b/code/modules/mapfluff/ruins/spaceruin_code/clericsden.dm
index 2ea63245661a6..56b09ef7f79fa 100644
--- a/code/modules/mapfluff/ruins/spaceruin_code/clericsden.dm
+++ b/code/modules/mapfluff/ruins/spaceruin_code/clericsden.dm
@@ -13,24 +13,3 @@
/obj/item/paper/fluff/ruins/clericsden/warning
default_raw_text = "FATHER ODIVALLUS DO NOT GO FORWARD WITH THE RITUAL. THE ASTEROID WE'RE ANCHORED TO IS UNSTABLE, YOU WILL DESTROY THE STATION. I HOPE THIS REACHES YOU IN TIME. FATHER AURELLION."
-
-/mob/living/simple_animal/hostile/construct/proteon
- name = "Proteon"
- real_name = "Proteon"
- desc = "A weaker construct meant to scour ruins for objects of Nar'Sie's affection. Those barbed claws are no joke."
- icon_state = "proteon"
- icon_living = "proteon"
- maxHealth = 35
- health = 35
- melee_damage_lower = 8
- melee_damage_upper = 10
- retreat_distance = 4 //AI proteons will rapidly move in and out of combat to avoid conflict, but will still target and follow you.
- attack_verb_continuous = "pinches"
- attack_verb_simple = "pinch"
- environment_smash = ENVIRONMENT_SMASH_WALLS
- attack_sound = 'sound/weapons/punch2.ogg'
- playstyle_string = "You are a Proteon. Your abilities in combat are outmatched by most combat constructs, but you are still fast and nimble. Run metal and supplies, and cooperate with your fellow cultists."
-
-/mob/living/simple_animal/hostile/construct/proteon/hostile //Style of mob spawned by trapped cult runes in the cleric ruin.
- AIStatus = AI_ON
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES //standard ai construct behavior, breaks things if it wants, but not walls.
diff --git a/code/modules/mapfluff/ruins/spaceruin_code/forgottenship.dm b/code/modules/mapfluff/ruins/spaceruin_code/forgottenship.dm
index 830fb83c0f39a..e06c9bbb03608 100644
--- a/code/modules/mapfluff/ruins/spaceruin_code/forgottenship.dm
+++ b/code/modules/mapfluff/ruins/spaceruin_code/forgottenship.dm
@@ -124,42 +124,3 @@ GLOBAL_VAR_INIT(fscpassword, generate_password())
icon_state = "syndie-ship"
ambientsounds = list('sound/ambience/ambitech2.ogg', 'sound/ambience/ambitech3.ogg')
area_flags = NOTELEPORT | UNIQUE_AREA
-
-//Special NT NPCs
-
-/mob/living/simple_animal/hostile/nanotrasen/ranged/assault
- name = "Nanotrasen Assault Officer"
- desc = "Nanotrasen Assault Officer. Contact CentCom if you saw him on your station. Prepare to die, if you've been found near Syndicate property."
- ranged = TRUE
- rapid = 4
- rapid_fire_delay = 1
- rapid_melee = 1
- retreat_distance = 2
- minimum_distance = 4
- casingtype = /obj/item/ammo_casing/a223/weak
- projectilesound = 'sound/weapons/gun/smg/shot.ogg'
- loot = list(/obj/effect/mob_spawn/corpse/human/nanotrasenassaultsoldier)
- mob_spawner = /obj/effect/mob_spawn/corpse/human/nanotrasenassaultsoldier
- held_item = /obj/item/gun/ballistic/automatic/ar
-
-/mob/living/simple_animal/hostile/nanotrasen/elite
- name = "Nanotrasen Elite Assault Officer"
- desc = "Pray for your life, syndicate. Run while you can."
- maxHealth = 150
- health = 150
- melee_damage_lower = 13
- melee_damage_upper = 18
- ranged = TRUE
- rapid = 3
- rapid_fire_delay = 5
- rapid_melee = 3
- retreat_distance = 0
- minimum_distance = 1
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- minbodytemp = 0
- projectiletype = /obj/projectile/beam/laser
- projectilesound = 'sound/weapons/laser.ogg'
- loot = list(/obj/effect/gibspawner/human)
- faction = list(ROLE_DEATHSQUAD)
- mob_spawner = /obj/effect/mob_spawn/corpse/human/nanotrasenelitesoldier
- held_item = /obj/item/gun/energy/pulse/carbine/lethal
diff --git a/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm b/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm
index d1926119a15b1..05daaff58645e 100644
--- a/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm
+++ b/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm
@@ -532,28 +532,28 @@ GLOBAL_VAR_INIT(hhMysteryRoomNumber, rand(1, 999999))
to_chat(user, "No vacated rooms.")
return .
-/obj/effect/landmark/lift_id/hilbert
- specific_lift_id = HILBERT_TRAM
+/obj/effect/landmark/transport/transport_id/hilbert
+ specific_transport_id = HILBERT_LINE_1
-/obj/effect/landmark/tram/nav/hilbert
- name = HILBERT_TRAM
- specific_lift_id = TRAM_NAV_BEACONS
+/obj/effect/landmark/transport/nav_beacon/tram/nav/hilbert
+ name = HILBERT_LINE_1
+ specific_transport_id = TRAM_NAV_BEACONS
-/obj/effect/landmark/tram/platform/hilbert/left
+/obj/effect/landmark/transport/nav_beacon/tram/platform/hilbert/left
name = "Port"
- specific_lift_id = HILBERT_TRAM
+ specific_transport_id = HILBERT_LINE_1
platform_code = HILBERT_PORT
tgui_icons = list("Reception" = "briefcase", "Botany" = "leaf", "Chemistry" = "flask")
-/obj/effect/landmark/tram/platform/hilbert/middle
+/obj/effect/landmark/transport/nav_beacon/tram/platform/hilbert/middle
name = "Central"
- specific_lift_id = HILBERT_TRAM
+ specific_transport_id = HILBERT_LINE_1
platform_code = HILBERT_CENTRAL
tgui_icons = list("Processing" = "cogs", "Xenobiology" = "paw")
-/obj/effect/landmark/tram/platform/hilbert/right
+/obj/effect/landmark/transport/nav_beacon/tram/platform/hilbert/right
name = "Starboard"
- specific_lift_id = HILBERT_TRAM
+ specific_transport_id = HILBERT_LINE_1
platform_code = HILBERT_STARBOARD
tgui_icons = list("Ordnance" = "bullseye", "Office" = "user", "Dormitories" = "bed")
diff --git a/code/modules/mapfluff/ruins/spaceruin_code/meatderelict.dm b/code/modules/mapfluff/ruins/spaceruin_code/meatderelict.dm
new file mode 100644
index 0000000000000..5647b5aca2382
--- /dev/null
+++ b/code/modules/mapfluff/ruins/spaceruin_code/meatderelict.dm
@@ -0,0 +1,156 @@
+/obj/item/keycard/meatderelict/director
+ name = "directors keycard"
+ desc = "A fancy keycard. Likely unlocks the directors office. The name tag is all smudged."
+ color = "#990000"
+ puzzle_id = "md_director"
+
+/obj/item/keycard/meatderelict/engpost
+ name = "post keycard"
+ desc = "A fancy keycard. Has the engineering insignia on it."
+ color = "#f0da12"
+ puzzle_id = "md_engpost"
+
+/obj/item/keycard/meatderelict/armory
+ name = "armory keycard"
+ desc = "A red keycard. Has a really cool image of a gun on it. Fancy."
+ color = "#FF7276"
+ puzzle_id = "md_armory"
+
+/obj/item/paper/crumpled/bloody/fluff/meatderelict/directoroffice
+ name = "directors note"
+ default_raw_text = "The research was going smooth... but the experiment did not go as planned. He convulsed and screamed as he slowly mutated into... that thing. It started to spread everywhere, outside the lab too. There is no way we can cover up that we are not a teleport research outpost, so I locked down the lab, but they already know. They sent a squad to rescue us, but..."
+
+/obj/item/paper/crumpled/fluff/meatderelict/shieldgens
+ name = "shield gate marketing sketch"
+ default_raw_text = "The QR-109 Shield Gate is a robust hardlight machine capable of producing a strong shield to bar entry. With control panel integration, it can be enabled or disabled from anywhere, such as ship's Bridge, Engineering Bay, or wherever else! The rest is faded..."
+
+/obj/item/paper/crumpled/fluff/meatderelict
+ name = "engineer note"
+ default_raw_text = "I've overclocked the power generators to add that needed juice to the experiment, though they're a bit unstable."
+
+/obj/item/paper/crumpled/fluff/meatderelict/fridge
+ name = "engineer complaint"
+ default_raw_text = "Whoever keeps stealing my fucking ice cream from my fridge, I swear I will actually fuck you up. It is not cheap to get this delicious ice cream here, nor is it for you. And don't touch my snacks in the drawer!"
+
+/obj/machinery/computer/terminal/meatderelict
+ upperinfo = "COPYRIGHT 2500 NANOSOFT-TM - DO NOT REDISTRIBUTE - Now with audio!" //not that old
+ content = list(
+ "Experimental Test Satellite 37B Nanotrasen™️ approved deep space experimentation lab
Entry 1:
Subject - \[Species 501-C-12\] Date - \[REDACTED\] We have acquired a biological sample of unknown origins \[Species 501-C-12\] from an NT outpost on the far reaches. Initial experiments have determined the sample to be a creature never previously recorded. It weighs approximately 7 grams and seems to be docile. Initial examinations determine that it is an extremely fast replicating organism which can alter its physiology to take multiple differing shapes. \[Recording Terminated\] - Dr. Phil Cornelius",
+ "Entry 2:
Subject - \[Species 501-C-12\] Date - \[REDACTED\] The creature responds to electrical stimuli. It has failed to respond to Light, Heat, Cold, Oxygen, Plasma, CO2, Nitrogen. It, within moments, seemed to have generated muscle tissue within its otherwise shapeless form and moved away from the source of electricity. Feeding the creature has been a simple matter, it consumed just about any form of protein. It appears to rapidly digest and convert forms of protein into more of itself. Any undigestible products are simply left alone. Will continue to monitor creature and provide reports to Nanotrasen Central Command. \[Recording Terminated\] - Dr. Phil Cornelius",
+ "Entry 3:
Subject - \[Species 501-C-12\] Date - \[REDACTED\] Any attempts at contacting Nanotrasen has failed. I've never seen anything like it. I... I don't think I'm going to survive much longer, I can hear it pushing on my room door. If anyone reads this, let my family know that I- \[Loud crash\] GET BACK \[Gunshots\] AHHHHHHHHHHHH \[Recording Terminated\] - Dr. Phil Cornelius"
+ )
+
+/obj/machinery/door/puzzle/meatderelict
+ name = "lockdown door"
+ desc = "A beaten door, still sturdy. Impervious to conventional methods of destruction, must be a way to open it nearby."
+ icon = 'icons/obj/doors/puzzledoor/danger.dmi'
+ puzzle_id = "md_prevault"
+
+/mob/living/basic/meteor_heart/opens_puzzle_door
+ ///the puzzle id we send on death
+ var/id
+ ///queue size, must match
+ var/queue_size = 2
+
+/mob/living/basic/meteor_heart/opens_puzzle_door/Initialize(mapload)
+ . = ..()
+ new /obj/effect/puzzle_death_signal_holder(loc, src, id, queue_size)
+
+/obj/effect/puzzle_death_signal_holder // ok apparently registering signals on qdeling stuff is not very functional
+ ///delay
+ var/delay = 2.5 SECONDS
+ invisibility = INVISIBILITY_ABSTRACT
+
+/obj/effect/puzzle_death_signal_holder/Initialize(mapload, mob/listened, id, queue_size = 2)
+ . = ..()
+ if(isnull(id))
+ return INITIALIZE_HINT_QDEL
+ RegisterSignal(listened, COMSIG_LIVING_DEATH, PROC_REF(on_death))
+ SSqueuelinks.add_to_queue(src, id, queue_size)
+
+/obj/effect/puzzle_death_signal_holder/proc/on_death(datum/source)
+ SIGNAL_HANDLER
+ addtimer(CALLBACK(src, PROC_REF(send_sig)), delay)
+
+/obj/effect/puzzle_death_signal_holder/proc/send_sig()
+ SEND_SIGNAL(src, COMSIG_PUZZLE_COMPLETED)
+ qdel(src)
+
+/obj/machinery/puzzle_button/meatderelict
+ name = "lockdown panel"
+ desc = "A panel that controls the lockdown of this outpost."
+ id = "md_prevault"
+
+/obj/machinery/puzzle_button/meatderelict/open_doors()
+ . = ..()
+ playsound(src, 'sound/effects/alert.ogg', 100, TRUE)
+ visible_message(span_warning("[src] lets out an alarm as the lockdown is lifted!"))
+
+/obj/structure/puzzle_blockade/meat
+ name = "mass of meat and teeth"
+ desc = "A horrible mass of meat and teeth. Can it see you? You hope not. Virtually indestructible, must be a way around."
+ icon = 'icons/obj/structures.dmi'
+ icon_state = "meatblockade"
+ opacity = TRUE
+
+/obj/structure/puzzle_blockade/meat/try_signal(datum/source)
+ Shake(duration = 0.5 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(open_up)), 0.5 SECONDS)
+
+/obj/structure/puzzle_blockade/meat/proc/open_up()
+ new /obj/effect/gibspawner/generic(drop_location())
+ qdel(src)
+
+/obj/lightning_thrower
+ name = "overcharged SMES"
+ desc = "An overclocked SMES, bursting with power."
+ anchored = TRUE
+ density = TRUE
+ icon = 'icons/obj/machines/engine/other.dmi'
+ icon_state = "smes"
+ /// do we currently want to shock diagonal tiles? if not, we shock cardinals
+ var/throw_diagonals = FALSE
+ /// flags we apply to the shock
+ var/shock_flags = SHOCK_KNOCKDOWN | SHOCK_NOGLOVES
+ /// damage of the shock
+ var/shock_damage = 20
+ /// list of turfs that are currently shocked so we can unregister the signal
+ var/list/signal_turfs = list()
+ /// how long do we shock
+ var/shock_duration = 0.5 SECONDS
+
+/obj/lightning_thrower/Initialize(mapload)
+ . = ..()
+ START_PROCESSING(SSprocessing, src)
+
+/obj/lightning_thrower/Destroy()
+ . = ..()
+ signal_turfs = null
+ STOP_PROCESSING(SSprocessing, src)
+
+/obj/lightning_thrower/process(seconds_per_tick)
+ var/list/dirs = throw_diagonals ? GLOB.diagonals : GLOB.cardinals
+ throw_diagonals = !throw_diagonals
+ playsound(src, 'sound/magic/lightningbolt.ogg', 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE, ignore_walls = FALSE)
+ for(var/direction in dirs)
+ var/victim_turf = get_step(src, direction)
+ if(isclosedturf(victim_turf))
+ continue
+ Beam(victim_turf, icon_state="lightning[rand(1,12)]", time = shock_duration)
+ RegisterSignal(victim_turf, COMSIG_ATOM_ENTERED, PROC_REF(shock_victim)) //we cant move anyway
+ signal_turfs += victim_turf
+ for(var/mob/living/victim in victim_turf)
+ shock_victim(null, victim)
+ addtimer(CALLBACK(src, PROC_REF(clear_signals)), shock_duration)
+
+/obj/lightning_thrower/proc/clear_signals(datum/source)
+ SIGNAL_HANDLER
+ for(var/turf in signal_turfs)
+ UnregisterSignal(turf, COMSIG_ATOM_ENTERED)
+ signal_turfs -= turf
+
+/obj/lightning_thrower/proc/shock_victim(datum/source, mob/living/victim)
+ SIGNAL_HANDLER
+ if(!istype(victim))
+ return
+ victim.electrocute_act(shock_damage, src, flags = shock_flags)
diff --git a/code/modules/mapfluff/ruins/spaceruin_code/meateor.dm b/code/modules/mapfluff/ruins/spaceruin_code/meateor.dm
index fc79c82e780ef..59998bb53c8f2 100644
--- a/code/modules/mapfluff/ruins/spaceruin_code/meateor.dm
+++ b/code/modules/mapfluff/ruins/spaceruin_code/meateor.dm
@@ -44,6 +44,14 @@
icon = 'icons/mob/simple/meteor_heart.dmi'
anchored = TRUE
+/obj/structure/meateor_fluff/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/bloody_spreader,\
+ blood_left = INFINITY,\
+ blood_dna = list("meaty DNA" = "MT-"),\
+ diseases = null,\
+ )
+
/obj/structure/meateor_fluff/play_attack_sound(damage_amount, damage_type, damage_flag)
switch(damage_type)
if(BRUTE)
diff --git a/code/modules/mapping/map_template.dm b/code/modules/mapping/map_template.dm
index b90f406d8fc9d..950f3e2809ef2 100644
--- a/code/modules/mapping/map_template.dm
+++ b/code/modules/mapping/map_template.dm
@@ -29,6 +29,8 @@
var/list/ceiling_baseturfs = list()
/datum/map_template/New(path = null, rename = null, cache = FALSE)
+ SHOULD_CALL_PARENT(TRUE)
+ . = ..()
if(path)
mappath = path
if(mappath)
diff --git a/code/modules/mining/equipment/kinetic_crusher.dm b/code/modules/mining/equipment/kinetic_crusher.dm
index 4ceddb09854ca..f5676221f7abf 100644
--- a/code/modules/mining/equipment/kinetic_crusher.dm
+++ b/code/modules/mining/equipment/kinetic_crusher.dm
@@ -201,7 +201,7 @@
hammer_synced = null
return ..()
-/obj/projectile/destabilizer/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/destabilizer/on_hit(atom/target, blocked = 0, pierce_hit)
if(isliving(target))
var/mob/living/L = target
var/had_effect = (L.has_status_effect(/datum/status_effect/crusher_mark)) //used as a boolean
diff --git a/code/modules/mining/equipment/monster_organs/brimdust_sac.dm b/code/modules/mining/equipment/monster_organs/brimdust_sac.dm
index dc6539ff336b5..a77e526a9d1f7 100644
--- a/code/modules/mining/equipment/monster_organs/brimdust_sac.dm
+++ b/code/modules/mining/equipment/monster_organs/brimdust_sac.dm
@@ -141,7 +141,7 @@
return COMPONENT_CLEANED
/// When you take brute damage, schedule an explosion
-/datum/status_effect/stacking/brimdust_coating/proc/on_take_damage(datum/source, damage, damagetype)
+/datum/status_effect/stacking/brimdust_coating/proc/on_take_damage(datum/source, damage, damagetype, ...)
SIGNAL_HANDLER
if(damagetype != BRUTE)
return
diff --git a/code/modules/mining/fulton.dm b/code/modules/mining/fulton.dm
index 20a436dc5c6f5..c8c8cf966bab4 100644
--- a/code/modules/mining/fulton.dm
+++ b/code/modules/mining/fulton.dm
@@ -6,136 +6,179 @@ GLOBAL_LIST_EMPTY(total_extraction_beacons)
icon = 'icons/obj/fulton.dmi'
icon_state = "extraction_pack"
w_class = WEIGHT_CLASS_NORMAL
- var/obj/structure/extraction_point/beacon
+ /// Beacon weakref
+ var/datum/weakref/beacon_ref
+ /// List of networks
var/list/beacon_networks = list("station")
+ /// Number of uses left
var/uses_left = 3
+ /// Can be used indoors
var/can_use_indoors
- var/safe_for_living_creatures = 1
+ /// Can be used on living creatures
+ var/safe_for_living_creatures = TRUE
+ /// Maximum force that can be used to extract
var/max_force_fulton = MOVE_FORCE_STRONG
/obj/item/extraction_pack/examine()
. = ..()
- . += "It has [uses_left] use\s remaining."
+ . += span_infoplain("It has [uses_left] use\s remaining.")
+
+ var/obj/structure/extraction_point/beacon = beacon_ref?.resolve()
+
+ if(isnull(beacon))
+ beacon_ref = null
+ . += span_infoplain("It is not linked to a beacon.")
+ return
+
+ . += span_infoplain("It is linked to [beacon.name].")
/obj/item/extraction_pack/attack_self(mob/user)
var/list/possible_beacons = list()
- for(var/obj/structure/extraction_point/extraction_point as anything in GLOB.total_extraction_beacons)
+ for(var/datum/weakref/point_ref as anything in GLOB.total_extraction_beacons)
+ var/obj/structure/extraction_point/extraction_point = point_ref.resolve()
+ if(isnull(extraction_point))
+ GLOB.total_extraction_beacons.Remove(point_ref)
if(extraction_point.beacon_network in beacon_networks)
possible_beacons += extraction_point
if(!length(possible_beacons))
- to_chat(user, span_warning("There are no extraction beacons in existence!"))
+ balloon_alert(user, "no beacons")
+ return
+
+ var/chosen_beacon = tgui_input_list(user, "Beacon to connect to", "Balloon Extraction Pack", sort_names(possible_beacons))
+ if(isnull(chosen_beacon))
return
- else
- var/chosen_beacon = tgui_input_list(user, "Beacon to connect to", "Balloon Extraction Pack", sort_names(possible_beacons))
- if(isnull(chosen_beacon))
- return
- beacon = chosen_beacon
- to_chat(user, span_notice("You link the extraction pack to the beacon system."))
-/obj/item/extraction_pack/afterattack(atom/movable/A, mob/living/carbon/human/user, flag, params)
+ beacon_ref = WEAKREF(chosen_beacon)
+ balloon_alert(user, "linked!")
+
+/obj/item/extraction_pack/afterattack(atom/movable/thing, mob/living/carbon/human/user, proximity_flag, params)
. = ..()
. |= AFTERATTACK_PROCESSED_ITEM
- if(!beacon)
- to_chat(user, span_warning("[src] is not linked to a beacon, and cannot be used!"))
- return
- if(!(beacon in GLOB.total_extraction_beacons))
- beacon = null
- to_chat(user, span_warning("The connected beacon has been destroyed!"))
+
+ var/obj/structure/extraction_point/beacon = beacon_ref?.resolve()
+ if(isnull(beacon))
+ balloon_alert(user, "not linked")
+ beacon_ref = null
return
+
if(!can_use_indoors)
- var/area/area = get_area(A)
+ var/area/area = get_area(thing)
if(!area.outdoors)
- to_chat(user, span_warning("[src] can only be used on things that are outdoors!"))
+ balloon_alert(user, "not outdoors")
return
- if(!flag)
+
+ if(!proximity_flag || !istype(thing))
return
- if(!istype(A))
+
+ if(!safe_for_living_creatures && check_for_living_mobs(thing))
+ to_chat(user, span_warning("[src] is not safe for use with living creatures, they wouldn't survive the trip back!"))
+ balloon_alert(user, "not safe!")
+ return
+
+ if(!isturf(thing.loc)) // no extracting stuff inside other stuff
+ return
+ if(thing.anchored || (thing.move_resist > max_force_fulton))
return
- else
- if(!safe_for_living_creatures && check_for_living_mobs(A))
- to_chat(user, span_warning("[src] is not safe for use with living creatures, they wouldn't survive the trip back!"))
- return
- if(!isturf(A.loc)) // no extracting stuff inside other stuff
- return
- if(A.anchored || (A.move_resist > max_force_fulton))
- return
- to_chat(user, span_notice("You start attaching the pack to [A]..."))
- if(do_after(user,50,target=A))
- to_chat(user, span_notice("You attach the pack to [A] and activate it."))
- if(loc == user)
- user.back?.atom_storage?.attempt_insert(src, user, force = STORAGE_SOFT_LOCKED)
- uses_left--
- if(uses_left <= 0)
- user.transferItemToLoc(src, A, TRUE)
- var/mutable_appearance/balloon
- var/mutable_appearance/balloon2
- var/mutable_appearance/balloon3
- if(isliving(A))
- var/mob/living/M = A
- M.Paralyze(320) // Keep them from moving during the duration of the extraction
- if(M.buckled)
- M.buckled.unbuckle_mob(M, TRUE) // Unbuckle them to prevent anchoring problems
- else
- A.set_anchored(TRUE)
- A.set_density(FALSE)
- var/obj/effect/extraction_holder/holder_obj = new(A.loc)
- holder_obj.appearance = A.appearance
- A.forceMove(holder_obj)
- balloon2 = mutable_appearance('icons/effects/fulton_balloon.dmi', "fulton_expand")
- balloon2.pixel_y = 10
- balloon2.appearance_flags = RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM
- holder_obj.add_overlay(balloon2)
- sleep(0.4 SECONDS)
- balloon = mutable_appearance('icons/effects/fulton_balloon.dmi', "fulton_balloon")
- balloon.pixel_y = 10
- balloon.appearance_flags = RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM
- holder_obj.cut_overlay(balloon2)
- holder_obj.add_overlay(balloon)
- playsound(holder_obj.loc, 'sound/items/fultext_deploy.ogg', 50, TRUE, -3)
- animate(holder_obj, pixel_z = 10, time = 20)
- sleep(2 SECONDS)
- animate(holder_obj, pixel_z = 15, time = 10)
- sleep(1 SECONDS)
- animate(holder_obj, pixel_z = 10, time = 10)
- sleep(1 SECONDS)
- animate(holder_obj, pixel_z = 15, time = 10)
- sleep(1 SECONDS)
- animate(holder_obj, pixel_z = 10, time = 10)
- sleep(1 SECONDS)
- playsound(holder_obj.loc, 'sound/items/fultext_launch.ogg', 50, TRUE, -3)
- animate(holder_obj, pixel_z = 1000, time = 30)
- if(ishuman(A))
- var/mob/living/carbon/human/L = A
- L.SetUnconscious(0)
- L.remove_status_effect(/datum/status_effect/drowsiness)
- L.SetSleeping(0)
- sleep(3 SECONDS)
- var/list/flooring_near_beacon = list()
- for(var/turf/open/floor in orange(1, beacon))
- flooring_near_beacon += floor
- holder_obj.forceMove(pick(flooring_near_beacon))
- animate(holder_obj, pixel_z = 10, time = 50)
- sleep(5 SECONDS)
- animate(holder_obj, pixel_z = 15, time = 10)
- sleep(1 SECONDS)
- animate(holder_obj, pixel_z = 10, time = 10)
- sleep(1 SECONDS)
- balloon3 = mutable_appearance('icons/effects/fulton_balloon.dmi', "fulton_retract")
- balloon3.pixel_y = 10
- balloon3.appearance_flags = RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM
- holder_obj.cut_overlay(balloon)
- holder_obj.add_overlay(balloon3)
- sleep(0.4 SECONDS)
- holder_obj.cut_overlay(balloon3)
- A.set_anchored(FALSE) // An item has to be unanchored to be extracted in the first place.
- A.set_density(initial(A.density))
- animate(holder_obj, pixel_z = 0, time = 5)
- sleep(0.5 SECONDS)
- A.forceMove(holder_obj.loc)
- qdel(holder_obj)
- if(uses_left <= 0)
- qdel(src)
+ balloon_alert_to_viewers("attaching...")
+ playsound(thing, 'sound/items/zip.ogg', vol = 50, vary = TRUE)
+ if(isliving(thing))
+ var/mob/living/creature = thing
+ if(creature.mind)
+ to_chat(thing, span_userdanger("You are being extracted! Stand still to proceed."))
+
+ if(!do_after(user, 5 SECONDS, target = thing))
+ return
+
+ balloon_alert_to_viewers("extracting!")
+ if(loc == user)
+ user.back?.atom_storage?.attempt_insert(src, user, force = STORAGE_SOFT_LOCKED)
+ uses_left--
+
+ if(uses_left <= 0)
+ user.transferItemToLoc(src, thing, TRUE)
+
+ var/mutable_appearance/balloon
+ var/mutable_appearance/balloon2
+ var/mutable_appearance/balloon3
+
+ if(isliving(thing))
+ var/mob/living/creature = thing
+ creature.Paralyze(32 SECONDS) // Keep them from moving during the duration of the extraction
+ if(creature.buckled)
+ creature.buckled.unbuckle_mob(creature, TRUE) // Unbuckle them to prevent anchoring problems
+ else
+ thing.set_anchored(TRUE)
+ thing.set_density(FALSE)
+
+ var/obj/effect/extraction_holder/holder_obj = new(get_turf(thing))
+ holder_obj.appearance = thing.appearance
+ thing.forceMove(holder_obj)
+ balloon2 = mutable_appearance('icons/effects/fulton_balloon.dmi', "fulton_expand")
+ balloon2.pixel_y = 10
+ balloon2.appearance_flags = RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM
+ holder_obj.add_overlay(balloon2)
+
+ sleep(0.4 SECONDS)
+
+ balloon = mutable_appearance('icons/effects/fulton_balloon.dmi', "fulton_balloon")
+ balloon.pixel_y = 10
+ balloon.appearance_flags = RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM
+ holder_obj.cut_overlay(balloon2)
+ holder_obj.add_overlay(balloon)
+ playsound(holder_obj.loc, 'sound/items/fultext_deploy.ogg', vol = 50, vary = TRUE, extrarange = -3)
+
+ animate(holder_obj, pixel_z = 10, time = 2 SECONDS)
+ animate(pixel_z = 15, time = 1 SECONDS)
+ animate(pixel_z = 10, time = 1 SECONDS)
+ animate(pixel_z = 15, time = 1 SECONDS)
+ animate(pixel_z = 10, time = 1 SECONDS)
+ sleep(6 SECONDS)
+
+ playsound(holder_obj.loc, 'sound/items/fultext_launch.ogg', vol = 50, vary = TRUE, extrarange = -3)
+ animate(holder_obj, pixel_z = 1000, time = 3 SECONDS)
+
+ if(ishuman(thing))
+ var/mob/living/carbon/human/creature = thing
+ creature.SetUnconscious(0)
+ creature.remove_status_effect(/datum/status_effect/drowsiness)
+ creature.SetSleeping(0)
+
+ sleep(3 SECONDS)
+
+ var/turf/flooring_near_beacon = list()
+ var/turf/beacon_turf = get_turf(beacon)
+ for(var/turf/floor as anything in RANGE_TURFS(1, beacon_turf))
+ if(!floor.is_blocked_turf())
+ flooring_near_beacon += floor
+
+ if(!length(flooring_near_beacon))
+ flooring_near_beacon += beacon_turf
+
+ holder_obj.forceMove(pick(flooring_near_beacon))
+
+ animate(holder_obj, pixel_z = 10, time = 5 SECONDS)
+ animate(pixel_z = 15, time = 1 SECONDS)
+ animate(pixel_z = 10, time = 1 SECONDS)
+ sleep(7 SECONDS)
+
+ balloon3 = mutable_appearance('icons/effects/fulton_balloon.dmi', "fulton_retract")
+ balloon3.pixel_y = 10
+ balloon3.appearance_flags = RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM
+ holder_obj.cut_overlay(balloon)
+ holder_obj.add_overlay(balloon3)
+ sleep(0.4 SECONDS)
+
+ holder_obj.cut_overlay(balloon3)
+ thing.set_anchored(FALSE) // An item has to be unanchored to be extracted in the first place.
+ thing.set_density(initial(thing.density))
+ animate(holder_obj, pixel_z = 0, time = 0.5 SECONDS)
+ sleep(0.5 SECONDS)
+
+ thing.forceMove(holder_obj.loc)
+ qdel(holder_obj)
+ if(uses_left <= 0)
+ qdel(src)
/obj/item/fulton_core
name = "extraction beacon assembly kit"
@@ -162,13 +205,9 @@ GLOBAL_LIST_EMPTY(total_extraction_beacons)
/obj/structure/extraction_point/Initialize(mapload)
. = ..()
name += " ([rand(100,999)]) ([get_area_name(src, TRUE)])"
- GLOB.total_extraction_beacons += src
+ GLOB.total_extraction_beacons.Add(WEAKREF(src))
update_appearance(UPDATE_OVERLAYS)
-/obj/structure/extraction_point/Destroy()
- GLOB.total_extraction_beacons -= src
- return ..()
-
/obj/structure/extraction_point/attack_hand(mob/living/user, list/modifiers)
. = ..()
balloon_alert_to_viewers("undeploying...")
diff --git a/code/modules/mining/lavaland/ash_flora.dm b/code/modules/mining/lavaland/ash_flora.dm
index e1260ab365ea5..16b8b3ec2f2b1 100644
--- a/code/modules/mining/lavaland/ash_flora.dm
+++ b/code/modules/mining/lavaland/ash_flora.dm
@@ -254,7 +254,7 @@
yield = 4
potency = 15
growthstages = 3
- rarity = 20
+ rarity = PLANT_MODERATELY_RARE
reagents_add = list(/datum/reagent/consumable/nutriment = 0.1)
species = "polypore" // silence unit test
genes = list(/datum/plant_gene/trait/fire_resistance)
diff --git a/code/modules/mining/lavaland/megafauna_loot.dm b/code/modules/mining/lavaland/megafauna_loot.dm
index 88262b1e96ac1..91b70f9f4ed52 100644
--- a/code/modules/mining/lavaland/megafauna_loot.dm
+++ b/code/modules/mining/lavaland/megafauna_loot.dm
@@ -399,7 +399,7 @@
give_blood(10)
/obj/item/soulscythe/attack_hand(mob/user, list/modifiers)
- if(soul.ckey && !soul.faction_check_mob(user))
+ if(soul.ckey && !soul.faction_check_atom(user))
to_chat(user, span_warning("You can't pick up [src]!"))
return
return ..()
@@ -423,19 +423,31 @@
using = TRUE
balloon_alert(user, "you hold the scythe up...")
ADD_TRAIT(src, TRAIT_NODROP, type)
- var/list/mob/dead/observer/candidates = poll_ghost_candidates("Do you want to play as [user.real_name]'s soulscythe?", ROLE_PAI, FALSE, 100, POLL_IGNORE_POSSESSED_BLADE)
- if(LAZYLEN(candidates))
- var/mob/dead/observer/picked_ghost = pick(candidates)
- soul.ckey = picked_ghost.ckey
- soul.copy_languages(user, LANGUAGE_MASTER) //Make sure the sword can understand and communicate with the user.
- soul.faction = list("[REF(user)]")
- balloon_alert(user, "the scythe glows up")
- add_overlay("soulscythe_gem")
- density = TRUE
- if(!ismob(loc))
- reset_spin()
- else
- balloon_alert(user, "the scythe is dormant!")
+
+ var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), user)
+ AddComponent(/datum/component/orbit_poll, \
+ ignore_key = POLL_IGNORE_POSSESSED_BLADE, \
+ job_bans = ROLE_PAI, \
+ to_call = to_call, \
+ )
+
+/// Ghost poll has concluded and a candidate has been chosen.
+/obj/item/soulscythe/proc/on_poll_concluded(mob/living/master, mob/dead/observer/ghost)
+ if(isnull(ghost))
+ balloon_alert(master, "the scythe is dormant!")
+ REMOVE_TRAIT(src, TRAIT_NODROP, type)
+ using = FALSE
+ return
+
+ soul.ckey = ghost.ckey
+ soul.copy_languages(master, LANGUAGE_MASTER) //Make sure the sword can understand and communicate with the master.
+ soul.faction = list("[REF(master)]")
+ balloon_alert(master, "the scythe glows")
+ add_overlay("soulscythe_gem")
+ density = TRUE
+ if(!ismob(loc))
+ reset_spin()
+
REMOVE_TRAIT(src, TRAIT_NODROP, type)
using = FALSE
@@ -606,7 +618,7 @@
light_power = 1
light_color = LIGHT_COLOR_BLOOD_MAGIC
-/obj/projectile/soulscythe/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/soulscythe/on_hit(atom/target, blocked = 0, pierce_hit)
if(ishostile(target))
damage *= 2
return ..()
@@ -649,7 +661,7 @@
/obj/item/melee/ghost_sword/Destroy()
for(var/mob/dead/observer/G in spirits)
- G.invisibility = GLOB.observer_default_invisibility
+ G.RemoveInvisibility(type)
spirits.Cut()
STOP_PROCESSING(SSobj, src)
. = ..()
@@ -660,9 +672,13 @@
return
to_chat(user, span_notice("You call out for aid, attempting to summon spirits to your side."))
- notify_ghosts("[user] is raising [user.p_their()] [name], calling for your help!",
- enter_link="(Click to help)",
- source = user, ignore_key = POLL_IGNORE_SPECTRAL_BLADE, header = "Spectral blade")
+ notify_ghosts(
+ "[user] is raising [user.p_their()] [name], calling for your help!",
+ action = NOTIFY_ORBIT,
+ source = user,
+ ignore_key = POLL_IGNORE_SPECTRAL_BLADE,
+ header = "Spectral blade",
+ )
summon_cooldown = world.time + 600
@@ -688,10 +704,10 @@
continue
var/mob/dead/observer/G = i
ghost_counter++
- G.invisibility = 0
+ G.SetInvisibility(INVISIBILITY_NONE, id=type, priority=INVISIBILITY_PRIORITY_BASIC_ANTI_INVISIBILITY)
current_spirits |= G
for(var/mob/dead/observer/G in spirits - current_spirits)
- G.invisibility = GLOB.observer_default_invisibility
+ G.RemoveInvisibility(type)
spirits = current_spirits
return ghost_counter
diff --git a/code/modules/mining/lavaland/tendril_loot.dm b/code/modules/mining/lavaland/tendril_loot.dm
index d3d4b4b672f20..270decf728487 100644
--- a/code/modules/mining/lavaland/tendril_loot.dm
+++ b/code/modules/mining/lavaland/tendril_loot.dm
@@ -60,7 +60,6 @@
desc = "A device which causes kinetic accelerators to permanently gain damage against creature types killed with it."
id = "bountymod"
materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT*2, /datum/material/silver = SHEET_MATERIAL_AMOUNT*2, /datum/material/gold = SHEET_MATERIAL_AMOUNT*2, /datum/material/bluespace = SHEET_MATERIAL_AMOUNT*2)
- reagents_list = list(/datum/reagent/blood = 40)
build_path = /obj/item/borg/upgrade/modkit/bounty
//Spooky special loot
@@ -539,8 +538,11 @@
. = ..()
if(!ishuman(exposed_mob) || exposed_mob.stat == DEAD)
return
+ if(!(methods & (INGEST | TOUCH)))
+ return
var/mob/living/carbon/human/exposed_human = exposed_mob
- if(!HAS_TRAIT(exposed_human, TRAIT_CAN_USE_FLIGHT_POTION) || reac_volume < 5 || !exposed_human.dna)
+ var/obj/item/bodypart/chest/chest = exposed_human.get_bodypart(BODY_ZONE_CHEST)
+ if(!chest.wing_types || reac_volume < 5 || !exposed_human.dna)
if((methods & INGEST) && show_message)
to_chat(exposed_human, span_notice("You feel nothing but a terrible aftertaste."))
return
@@ -548,16 +550,16 @@
to_chat(exposed_human, span_userdanger("A terrible pain travels down your back as your wings change shape!"))
else
to_chat(exposed_human, span_userdanger("A terrible pain travels down your back as wings burst out!"))
- var/obj/item/organ/external/wings/functional/wings = get_wing_choice(exposed_human)
+ var/obj/item/organ/external/wings/functional/wings = get_wing_choice(exposed_human, chest)
wings = new wings()
wings.Insert(exposed_human)
exposed_human.dna.species.handle_mutant_bodyparts(exposed_human)
playsound(exposed_human.loc, 'sound/items/poster_ripped.ogg', 50, TRUE, -1)
- exposed_human.adjustBruteLoss(20)
+ exposed_human.apply_damage(20, def_zone = BODY_ZONE_CHEST, forced = TRUE, wound_bonus = CANT_WOUND)
exposed_human.emote("scream")
-/datum/reagent/flightpotion/proc/get_wing_choice(mob/living/carbon/human/needs_wings)
- var/list/wing_types = needs_wings.dna.species.wing_types.Copy()
+/datum/reagent/flightpotion/proc/get_wing_choice(mob/needs_wings, obj/item/bodypart/chest/chest)
+ var/list/wing_types = chest.wing_types.Copy()
if(wing_types.len == 1 || !needs_wings.client)
return wing_types[1]
var/list/radial_wings = list()
@@ -632,7 +634,7 @@
. = ..()
if(slot & ITEM_SLOT_GLOVES)
tool_behaviour = TOOL_MINING
- RegisterSignal(user, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, PROC_REF(rocksmash))
+ RegisterSignal(user, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(rocksmash))
RegisterSignal(user, COMSIG_MOVABLE_BUMP, PROC_REF(rocksmash))
else
stopmining(user)
@@ -643,13 +645,15 @@
/obj/item/clothing/gloves/gauntlets/proc/stopmining(mob/user)
tool_behaviour = initial(tool_behaviour)
- UnregisterSignal(user, COMSIG_HUMAN_EARLY_UNARMED_ATTACK)
+ UnregisterSignal(user, COMSIG_LIVING_UNARMED_ATTACK)
UnregisterSignal(user, COMSIG_MOVABLE_BUMP)
/obj/item/clothing/gloves/gauntlets/proc/rocksmash(mob/living/carbon/human/user, atom/rocks, proximity)
SIGNAL_HANDLER
+ if(!proximity)
+ return NONE
if(!ismineralturf(rocks) && !isasteroidturf(rocks))
- return
+ return NONE
rocks.attackby(src, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
@@ -1049,7 +1053,7 @@
/obj/item/cursed_katana/proc/cloak(mob/living/target, mob/user)
user.alpha = 150
- user.invisibility = INVISIBILITY_OBSERVER // so hostile mobs cant see us or target us
+ user.SetInvisibility(INVISIBILITY_OBSERVER, id=type) // so hostile mobs cant see us or target us
user.add_sight(SEE_SELF) // so we can see us
user.visible_message(span_warning("[user] vanishes into thin air!"),
span_notice("You enter the dark cloak."))
@@ -1063,7 +1067,7 @@
/obj/item/cursed_katana/proc/uncloak(mob/user)
user.alpha = 255
- user.invisibility = 0
+ user.RemoveInvisibility(type)
user.clear_sight(SEE_SELF)
user.visible_message(span_warning("[user] appears from thin air!"),
span_notice("You exit the dark cloak."))
diff --git a/code/modules/mining/ores_coins.dm b/code/modules/mining/ores_coins.dm
index b01b537ec80d9..bea2570fc4e71 100644
--- a/code/modules/mining/ores_coins.dm
+++ b/code/modules/mining/ores_coins.dm
@@ -236,35 +236,77 @@ GLOBAL_LIST_INIT(sand_recipes, list(\
inhand_icon_state = "Gibtonite ore"
w_class = WEIGHT_CLASS_BULKY
throw_range = 0
+ /// if the gibtonite is currently primed for explosion
var/primed = FALSE
- var/det_time = 100
- var/quality = GIBTONITE_QUALITY_LOW //How pure this gibtonite is, determines the explosion produced by it and is derived from the det_time of the rock wall it was taken from, higher value = better
- var/attacher = "UNKNOWN"
+ /// how long does it take for this to detonate
+ var/det_time = 10 SECONDS
+ /// the timer
var/det_timer
+ /// How pure this gibtonite is, determines the explosion produced by it and is derived from the det_time of the rock wall it was taken from, higher value = better
+ var/quality = GIBTONITE_QUALITY_LOW
+ /// who attached the rig to us
+ var/attacher
+ /// the assembly rig
+ var/obj/item/assembly_holder/rig
+ /// the rig overlay
+ var/mutable_appearance/rig_overlay
/obj/item/gibtonite/Initialize(mapload)
. = ..()
AddComponent(/datum/component/two_handed, require_twohands=TRUE)
AddComponent(/datum/component/golem_food, consume_on_eat = FALSE, golem_food_key = /obj/item/gibtonite)
+/obj/item/gibtonite/examine(mob/user)
+ . = ..()
+ if(rig)
+ . += span_warning("There is some kind of device rigged to it!")
+ else
+ . += span_notice("You could rig something to it.")
+
/obj/item/gibtonite/Destroy()
- qdel(wires)
- set_wires(null)
+ QDEL_NULL(rig)
+ rig_overlay = null
return ..()
+/obj/item/gibtonite/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(gone == rig)
+ rig = null
+ attacher = null
+ cut_overlays(rig_overlay)
+ UnregisterSignal(src, COMSIG_IGNITER_ACTIVATE)
+
+/obj/item/gibtonite/IsSpecialAssembly()
+ return TRUE
+
/obj/item/gibtonite/attackby(obj/item/I, mob/user, params)
- if(!wires && isigniter(I))
- user.visible_message(span_notice("[user] attaches [I] to [src]."), span_notice("You attach [I] to [src]."))
- set_wires(new /datum/wires/explosive/gibtonite(src))
+ if(istype(I, /obj/item/assembly_holder) && !rig)
+ var/obj/item/assembly_holder/holder = I
+ if(!(locate(/obj/item/assembly/igniter) in holder.assemblies))
+ return ..()
+ if(!user.transferItemToLoc(holder, src))
+ return
+ add_fingerprint(user)
+ rig = holder
+ holder.master = src
+ holder.on_attach()
+ rig_overlay = holder
+ rig_overlay.pixel_y -= 5
+ add_overlay(rig_overlay)
+ RegisterSignal(src, COMSIG_IGNITER_ACTIVATE, PROC_REF(igniter_prime))
+ log_bomber(user, "attached [holder] to ", src)
attacher = key_name(user)
- qdel(I)
- add_overlay("Gibtonite_igniter")
+ user.balloon_alert_to_viewers("attached rig")
return
-
- if(wires && !primed)
- if(is_wire_tool(I))
- wires.interact(user)
+
+ if(I.tool_behaviour == TOOL_WRENCH && rig)
+ rig.on_found()
+ if(QDELETED(src))
return
+ user.balloon_alert_to_viewers("detached rig")
+ user.log_message("detached [rig] from [src].", LOG_GAME)
+ user.put_in_hands(rig)
+ return
if(I.tool_behaviour == TOOL_MINING || istype(I, /obj/item/resonator) || I.force >= 10)
GibtoniteReaction(user, "A resonator has primed for detonation a")
@@ -342,6 +384,10 @@ GLOBAL_LIST_INIT(sand_recipes, list(\
hit_mob.Paralyze(1.5 SECONDS)
hit_mob.Knockdown(8 SECONDS)
+/obj/item/gibtonite/proc/igniter_prime()
+ SIGNAL_HANDLER
+ GibtoniteReaction(null, "An attached rig has primed a")
+
/obj/item/stack/ore/Initialize(mapload, new_amount, merge = TRUE, list/mat_override=null, mat_amt=1)
. = ..()
pixel_x = base_pixel_x + rand(0, 16) - 8
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 7b827b73d8ed2..7c661514e64a5 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -219,9 +219,8 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER)
setDir(2 )//reset the dir to its default so the sprites all properly align up
if(ghost_accs == GHOST_ACCS_FULL && (icon_state in GLOB.ghost_forms_with_accessories_list)) //check if this form supports accessories and if the client wants to show them
- var/datum/sprite_accessory/S
if(facial_hairstyle)
- S = GLOB.facial_hairstyles_list[facial_hairstyle]
+ var/datum/sprite_accessory/S = GLOB.facial_hairstyles_list[facial_hairstyle]
if(S)
facial_hair_overlay = mutable_appearance(S.icon, "[S.icon_state]", -HAIR_LAYER)
if(facial_hair_color)
@@ -229,12 +228,13 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER)
facial_hair_overlay.alpha = 200
add_overlay(facial_hair_overlay)
if(hairstyle)
- S = GLOB.hairstyles_list[hairstyle]
+ var/datum/sprite_accessory/hair/S = GLOB.hairstyles_list[hairstyle]
if(S)
hair_overlay = mutable_appearance(S.icon, "[S.icon_state]", -HAIR_LAYER)
if(hair_color)
hair_overlay.color = hair_color
hair_overlay.alpha = 200
+ hair_overlay.pixel_y = S.y_offset
add_overlay(hair_overlay)
/*
@@ -728,6 +728,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
if(istype(target) && (target != src))
ManualFollow(target)
return
+
if(href_list["x"] && href_list["y"] && href_list["z"])
var/tx = text2num(href_list["x"])
var/ty = text2num(href_list["y"])
@@ -736,10 +737,23 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
if(istype(target))
abstract_move(target)
return
+
if(href_list["reenter"])
reenter_corpse()
return
+ if(href_list["jump"])
+ var/atom/movable/target = locate(href_list["jump"])
+ var/turf/target_turf = get_turf(target)
+ if(target_turf && isturf(target_turf))
+ abstract_move(target_turf)
+
+ if(href_list["play"])
+ var/atom/movable/target = locate(href_list["play"])
+ if(istype(target) && (target != src))
+ target.attack_ghost(usr)
+ return
+
//We don't want to update the current var
//But we will still carry a mind.
/mob/dead/observer/mind_initialize()
@@ -996,7 +1010,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
/mob/dead/observer/proc/set_invisibility(value)
- invisibility = value
+ SetInvisibility(value, id=type)
set_light_on(!value ? TRUE : FALSE)
diff --git a/code/modules/mob/emote.dm b/code/modules/mob/emote.dm
index 0688778b04840..1bb8e61ef2b9a 100644
--- a/code/modules/mob/emote.dm
+++ b/code/modules/mob/emote.dm
@@ -46,20 +46,28 @@
/datum/emote/help/run_emote(mob/user, params, type_override, intentional)
. = ..()
var/list/keys = list()
- var/list/message = list("Available emotes, you can use them with say \"*emote\": ")
+ var/list/message = list("Available emotes, you can use them with say [span_bold("\"*emote\"")]: \n")
+ message += span_smallnoticeital("Note - emotes highlighted in blue play a sound \n\n")
for(var/key in GLOB.emote_list)
- for(var/datum/emote/P in GLOB.emote_list[key])
- if(P.key in keys)
+ for(var/datum/emote/emote_action in GLOB.emote_list[key])
+ if(emote_action.key in keys)
continue
- if(P.can_run_emote(user, status_check = FALSE , intentional = TRUE))
- keys += P.key
+ if(emote_action.can_run_emote(user, status_check = FALSE , intentional = TRUE))
+ keys += emote_action.key
keys = sort_list(keys)
+
+ // the span formatting will mess up sorting so need to do it afterwards
+ for(var/i in 1 to keys.len)
+ for(var/datum/emote/emote_action in GLOB.emote_list[keys[i]])
+ if(emote_action.get_sound(user) && emote_action.should_play_sound(user, intentional = TRUE))
+ keys[i] = span_boldnotice(keys[i])
+
message += keys.Join(", ")
message += "."
message = message.Join("")
- to_chat(user, message)
+ to_chat(user, examine_block(message))
/datum/emote/flip
key = "flip"
diff --git a/code/modules/mob/living/basic/basic_defense.dm b/code/modules/mob/living/basic/basic_defense.dm
index 87fe6f8fed1bb..a1093f914d83f 100644
--- a/code/modules/mob/living/basic/basic_defense.dm
+++ b/code/modules/mob/living/basic/basic_defense.dm
@@ -11,13 +11,13 @@
var/shove_dir = get_dir(user, src)
if(!Move(get_step(src, shove_dir), shove_dir))
log_combat(user, src, "shoved", "failing to move it")
- user.visible_message(span_danger("[user.name] shoves [src]!"),
- span_danger("You shove [src]!"), span_hear("You hear aggressive shuffling!"), COMBAT_MESSAGE_RANGE, list(src))
+ user.visible_message(span_danger("[user.name] [response_disarm_continuous] [src]!"),
+ span_danger("You [response_disarm_simple] [src]!"), span_hear("You hear aggressive shuffling!"), COMBAT_MESSAGE_RANGE, list(src))
to_chat(src, span_userdanger("You're shoved by [user.name]!"))
return TRUE
log_combat(user, src, "shoved", "pushing it")
- user.visible_message(span_danger("[user.name] shoves [src], pushing [p_them()]!"),
- span_danger("You shove [src], pushing [p_them()]!"), span_hear("You hear aggressive shuffling!"), COMBAT_MESSAGE_RANGE, list(src))
+ user.visible_message(span_danger("[user.name] [response_disarm_continuous] [src], pushing [p_them()]!"),
+ span_danger("You [response_disarm_simple] [src], pushing [p_them()]!"), span_hear("You hear aggressive shuffling!"), COMBAT_MESSAGE_RANGE, list(src))
to_chat(src, span_userdanger("You're pushed by [user.name]!"))
return TRUE
@@ -109,12 +109,12 @@
damage = rand(20, 35)
return attack_threshold_check(damage)
-/mob/living/basic/attack_drone(mob/living/simple_animal/drone/attacking_drone)
+/mob/living/basic/attack_drone(mob/living/basic/drone/attacking_drone)
if(attacking_drone.combat_mode) //No kicking dogs even as a rogue drone. Use a weapon.
return
return ..()
-/mob/living/basic/attack_drone_secondary(mob/living/simple_animal/drone/attacking_drone)
+/mob/living/basic/attack_drone_secondary(mob/living/basic/drone/attacking_drone)
if(attacking_drone.combat_mode)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
return ..()
diff --git a/code/modules/mob/living/basic/blob_minions/blob_ai.dm b/code/modules/mob/living/basic/blob_minions/blob_ai.dm
index 6168b7ca83be7..fc21d030d07d1 100644
--- a/code/modules/mob/living/basic/blob_minions/blob_ai.dm
+++ b/code/modules/mob/living/basic/blob_minions/blob_ai.dm
@@ -4,7 +4,8 @@
*/
/datum/ai_controller/basic_controller/blobbernaut
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
ai_movement = /datum/ai_movement/jps
@@ -20,7 +21,8 @@
*/
/datum/ai_controller/basic_controller/blob_zombie
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
ai_movement = /datum/ai_movement/jps
@@ -37,7 +39,8 @@
*/
/datum/ai_controller/basic_controller/blob_spore
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
ai_movement = /datum/ai_movement/jps
diff --git a/code/modules/mob/living/basic/clown/clown.dm b/code/modules/mob/living/basic/clown/clown.dm
index 5682edf933907..e55b91bade2ec 100644
--- a/code/modules/mob/living/basic/clown/clown.dm
+++ b/code/modules/mob/living/basic/clown/clown.dm
@@ -391,8 +391,7 @@
/mob/living/basic/clown/mutant/glutton/Initialize(mapload)
. = ..()
- var/datum/action/cooldown/regurgitate/spit = new(src)
- spit.Grant(src)
+ GRANT_ACTION(/datum/action/cooldown/regurgitate)
AddElement(/datum/element/swabable, CELL_LINE_TABLE_GLUTTON, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/cheesiehonkers, /obj/item/food/cornchips), tame_chance = 30, bonus_tame_chance = 0, after_tame = CALLBACK(src, PROC_REF(tamed)))
@@ -541,22 +540,15 @@
BB_EMOTE_SEE = list("bites into the banana", "plucks a banana off its head", "photosynthesizes"),
BB_EMOTE_SOUND = list('sound/items/bikehorn.ogg'),
)
- ///Our peel dropping ability
- var/datum/action/cooldown/rustle/banana_rustle
- ///Our banana bunch spawning ability
- var/datum/action/cooldown/exquisite_bunch/banana_bunch
/mob/living/basic/clown/banana/Initialize(mapload)
. = ..()
- banana_rustle = new()
- banana_rustle.Grant(src)
- banana_bunch = new()
- banana_bunch.Grant(src)
-/mob/living/basic/clown/banana/Destroy()
- . = ..()
- QDEL_NULL(banana_rustle)
- QDEL_NULL(banana_bunch)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/exquisite_bunch,
+ /datum/action/cooldown/rustle,
+ )
+ grant_actions_by_list(innate_actions)
///drops peels around the mob when activated
/datum/action/cooldown/rustle
diff --git a/code/modules/mob/living/basic/clown/clown_ai.dm b/code/modules/mob/living/basic/clown/clown_ai.dm
index b2e6418dde75f..f54c432140ee3 100644
--- a/code/modules/mob/living/basic/clown/clown_ai.dm
+++ b/code/modules/mob/living/basic/clown/clown_ai.dm
@@ -14,6 +14,7 @@
/datum/ai_controller/basic_controller/clown/murder
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
BB_BASIC_MOB_SPEAK_LINES = null,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
diff --git a/code/modules/mob/living/basic/constructs/_construct.dm b/code/modules/mob/living/basic/cult/constructs/_construct.dm
similarity index 74%
rename from code/modules/mob/living/basic/constructs/_construct.dm
rename to code/modules/mob/living/basic/cult/constructs/_construct.dm
index f2e55cceb86b3..534cb3a7eac1b 100644
--- a/code/modules/mob/living/basic/constructs/_construct.dm
+++ b/code/modules/mob/living/basic/cult/constructs/_construct.dm
@@ -22,6 +22,7 @@
response_disarm_simple = "flail at"
response_harm_continuous = "punches"
response_harm_simple = "punch"
+ melee_attack_cooldown = CLICK_CD_MELEE
// Vivid red, cause cult theme
lighting_cutoff_red = 30
@@ -42,25 +43,31 @@
var/can_repair_self = FALSE
/// Theme controls color. THEME_CULT is red THEME_WIZARD is purple and THEME_HOLY is blue
var/theme = THEME_CULT
- /// What flavor of gunk does this construct drop on death?
- var/static/list/remains = list(/obj/item/ectoplasm/construct)
- /// Can this construct smash walls? Gets the wall_smasher element if so.
+ /// Can this construct destroy walls?
var/smashes_walls = FALSE
+ /// The different flavors of goop constructs can drop, depending on theme.
+ var/static/list/remains_by_theme = list(
+ THEME_CULT = list(/obj/item/ectoplasm/construct),
+ THEME_HOLY = list(/obj/item/ectoplasm/angelic),
+ THEME_WIZARD = list(/obj/item/ectoplasm/mystic),
+ )
/mob/living/basic/construct/Initialize(mapload)
. = ..()
AddElement(/datum/element/simple_flying)
+ var/list/remains = string_list(remains_by_theme[theme])
if(length(remains))
AddElement(/datum/element/death_drops, remains)
if(smashes_walls)
- AddElement(/datum/element/wall_smasher, strength_flag = ENVIRONMENT_SMASH_WALLS)
+ AddElement(/datum/element/wall_tearer, allow_reinforced = FALSE)
if(can_repair)
AddComponent(\
/datum/component/healing_touch,\
heal_brute = 5,\
heal_burn = 0,\
heal_time = 0,\
- valid_targets_typecache = typecacheof(list(/mob/living/basic/construct, /mob/living/simple_animal/hostile/construct, /mob/living/simple_animal/shade)),\
+ valid_targets_typecache = typecacheof(list(/mob/living/basic/construct, /mob/living/basic/shade)),\
+ valid_biotypes = MOB_MINERAL | MOB_SPIRIT,\
self_targetting = can_repair_self ? HEALING_TOUCH_ANYONE : HEALING_TOUCH_NOT_SELF,\
action_text = "%SOURCE% begins repairing %TARGET%'s dents.",\
complete_text = "%TARGET%'s dents are repaired.",\
@@ -73,9 +80,7 @@
structure_types_typecache = structure_types,\
)
add_traits(list(TRAIT_HEALS_FROM_CULT_PYLONS, TRAIT_SPACEWALK), INNATE_TRAIT)
- for(var/spell in construct_spells)
- var/datum/action/new_spell = new spell(src)
- new_spell.Grant(src)
+ grant_actions_by_list(construct_spells)
var/spell_count = 1
for(var/datum/action/spell as anything in actions)
@@ -122,34 +127,6 @@
/mob/living/basic/construct/electrocute_act(shock_damage, source, siemens_coeff = 1, flags = NONE)
return FALSE
-// Allows simple constructs to repair basic constructs.
-/mob/living/basic/construct/attack_animal(mob/living/simple_animal/user, list/modifiers)
- if(!isconstruct(user))
- if(src != user)
- return ..()
- return
-
- if(src == user) //basic constructs use the healing hands component instead
- return
-
- var/mob/living/simple_animal/hostile/construct/doll = user
- if(!doll.can_repair || (doll == src && !doll.can_repair_self))
- return ..()
- if(theme != doll.theme)
- return ..()
-
- if(health >= maxHealth)
- to_chat(user, span_cult("You cannot repair [src]'s dents, as [p_they()] [p_have()] none!"))
- return
-
- heal_overall_damage(brute = 5)
-
- Beam(user, icon_state = "sendbeam", time = 4)
- user.visible_message(
- span_danger("[user] repairs some of \the [src]'s dents."),
- span_cult("You repair some of [src]'s dents, leaving [src] at [health]/[maxHealth] health."),
- )
-
/// Construct ectoplasm. Largely a placeholder, since the death drop element needs a unique list.
/obj/item/ectoplasm/construct
name = "blood-red ectoplasm"
diff --git a/code/modules/mob/living/simple_animal/hostile/constructs/artificer.dm b/code/modules/mob/living/basic/cult/constructs/artificer.dm
similarity index 50%
rename from code/modules/mob/living/simple_animal/hostile/constructs/artificer.dm
rename to code/modules/mob/living/basic/cult/constructs/artificer.dm
index 743931c508d09..8856c0e66a2ad 100644
--- a/code/modules/mob/living/simple_animal/hostile/constructs/artificer.dm
+++ b/code/modules/mob/living/basic/cult/constructs/artificer.dm
@@ -1,4 +1,4 @@
-/mob/living/simple_animal/hostile/construct/artificer
+/mob/living/basic/construct/artificer
name = "Artificer"
real_name = "Artificer"
desc = "A bulbous construct dedicated to building and maintaining the Cult of Nar'Sie's armies."
@@ -8,22 +8,18 @@
health = 50
response_harm_continuous = "viciously beats"
response_harm_simple = "viciously beat"
- harm_intent_damage = 5
obj_damage = 60
melee_damage_lower = 5
melee_damage_upper = 5
- retreat_distance = 10
- minimum_distance = 10 //AI artificers will flee like fuck
attack_verb_continuous = "rams"
attack_verb_simple = "ram"
- environment_smash = ENVIRONMENT_SMASH_WALLS
attack_sound = 'sound/weapons/punch2.ogg'
construct_spells = list(
+ /datum/action/cooldown/spell/aoe/magic_missile/lesser,
+ /datum/action/cooldown/spell/conjure/construct/lesser,
/datum/action/cooldown/spell/conjure/cult_floor,
/datum/action/cooldown/spell/conjure/cult_wall,
/datum/action/cooldown/spell/conjure/soulstone,
- /datum/action/cooldown/spell/conjure/construct/lesser,
- /datum/action/cooldown/spell/aoe/magic_missile/lesser,
/datum/action/innate/cult/create_rune/revive,
)
playstyle_string = "You are an Artificer. You are incredibly weak and fragile, \
@@ -33,70 +29,39 @@
can_repair = TRUE
can_repair_self = TRUE
+ smashes_walls = TRUE
///The health HUD applied to this mob.
var/health_hud = DATA_HUD_MEDICAL_ADVANCED
-/mob/living/simple_animal/hostile/construct/artificer/Initialize(mapload)
+/mob/living/basic/construct/artificer/Initialize(mapload)
. = ..()
+ AddElement(/datum/element/ai_retaliate)
var/datum/atom_hud/datahud = GLOB.huds[health_hud]
datahud.show_to(src)
-/mob/living/simple_animal/hostile/construct/artificer/Found(atom/thing) //what have we found here?
- if(!isconstruct(thing)) //is it a construct?
- return FALSE
- var/mob/living/simple_animal/hostile/construct/cultie = thing
- if(cultie.health < cultie.maxHealth) //is it hurt? let's go heal it if it is
- return TRUE
-
-/mob/living/simple_animal/hostile/construct/artificer/CanAttack(atom/the_target)
- if(see_invisible < the_target.invisibility)//Target's invisible to us, forget it
- return FALSE
- if(Found(the_target) || ..()) //If we Found it or Can_Attack it normally, we Can_Attack it as long as it wasn't invisible
- return TRUE //as a note this shouldn't be added to base hostile mobs because it'll mess up retaliate hostile mobs
- return FALSE
-
-/mob/living/simple_animal/hostile/construct/artificer/MoveToTarget(list/possible_targets)
- ..()
- if(!isliving(target))
- return
-
- var/mob/living/victim = target
- if(isconstruct(victim) && victim.health >= victim.maxHealth) //is this target an unhurt construct? stop trying to heal it
- LoseTarget()
- return
- if(victim.health <= melee_damage_lower+melee_damage_upper) //ey bucko you're hurt as fuck let's go hit you
- retreat_distance = null
- minimum_distance = 1
+/// Hostile NPC version. Heals nearby constructs and cult structures, avoids targets that aren't extremely hurt.
+/mob/living/basic/construct/artificer/hostile
+ ai_controller = /datum/ai_controller/basic_controller/artificer
+ smashes_walls = FALSE
+ melee_attack_cooldown = 2 SECONDS
-/mob/living/simple_animal/hostile/construct/artificer/Aggro()
- ..()
- if(isconstruct(target)) //oh the target is a construct no need to flee
- retreat_distance = null
- minimum_distance = 1
-
-/mob/living/simple_animal/hostile/construct/artificer/LoseAggro()
- ..()
- retreat_distance = initial(retreat_distance)
- minimum_distance = initial(minimum_distance)
-
-/mob/living/simple_animal/hostile/construct/artificer/hostile //actually hostile, will move around, hit things, heal other constructs
- AIStatus = AI_ON
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES //only token destruction, don't smash the cult wall NO STOP
-
-/////////////////////////////Artificer-alts/////////////////////////
-/mob/living/simple_animal/hostile/construct/artificer/angelic
+// Alternate artificer themes
+/mob/living/basic/construct/artificer/angelic
desc = "A bulbous construct dedicated to building and maintaining holy armies."
theme = THEME_HOLY
- loot = list(/obj/item/ectoplasm/angelic)
construct_spells = list(
/datum/action/cooldown/spell/conjure/soulstone/purified,
/datum/action/cooldown/spell/conjure/construct/lesser,
/datum/action/cooldown/spell/aoe/magic_missile/lesser,
/datum/action/innate/cult/create_rune/revive,
)
-/mob/living/simple_animal/hostile/construct/artificer/mystic
+
+/mob/living/basic/construct/artificer/angelic/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_ANGELIC, INNATE_TRAIT)
+
+/mob/living/basic/construct/artificer/mystic
theme = THEME_WIZARD
- loot = list(/obj/item/ectoplasm/mystic)
construct_spells = list(
/datum/action/cooldown/spell/conjure/cult_floor,
/datum/action/cooldown/spell/conjure/cult_wall,
@@ -106,7 +71,7 @@
/datum/action/innate/cult/create_rune/revive,
)
-/mob/living/simple_animal/hostile/construct/artificer/noncult
+/mob/living/basic/construct/artificer/noncult
construct_spells = list(
/datum/action/cooldown/spell/conjure/cult_floor,
/datum/action/cooldown/spell/conjure/cult_wall,
diff --git a/code/modules/mob/living/basic/cult/constructs/construct_ai.dm b/code/modules/mob/living/basic/cult/constructs/construct_ai.dm
new file mode 100644
index 0000000000000..6f79b327915be
--- /dev/null
+++ b/code/modules/mob/living/basic/cult/constructs/construct_ai.dm
@@ -0,0 +1,90 @@
+/**
+ * Artificers
+ *
+ * Artificers will seek out and heal the most wounded construct or shade they can see.
+ * If there is no one to heal, they will run away from any non-allied mobs.
+ */
+/datum/ai_controller/basic_controller/artificer
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/same_faction/construct,
+ BB_FLEE_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_WOUNDED_ONLY = TRUE,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_wounded_target,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/target_retaliate/to_flee,
+ /datum/ai_planning_subtree/flee_target/from_flee_key,
+ )
+
+/**
+ * Juggernauts
+ *
+ * Juggernauts slowly walk toward non-allied mobs and pummel them to death.
+ */
+/datum/ai_controller/basic_controller/juggernaut
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ )
+
+ 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/basic_melee_attack_subtree,
+ )
+
+/**
+ * Proteons
+ *
+ * Proteons perform cowardly hit-and-run attacks, fleeing melee when struck but returning to fight again.
+ */
+/datum/ai_controller/basic_controller/proteon
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ BB_FLEE_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate/to_flee,
+ /datum/ai_planning_subtree/flee_target/from_flee_key,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/**
+ * Wraiths
+ *
+ * Wraiths seek out the most injured non-allied mob to beat to death.
+ */
+/datum/ai_controller/basic_controller/wraith
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_wounded_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/// Targetting datum that will only allow mobs that constructs can heal.
+/datum/targetting_datum/basic/same_faction/construct
+ target_wounded_key = BB_TARGET_WOUNDED_ONLY
+
+/datum/targetting_datum/basic/same_faction/construct/can_attack(mob/living/living_mob, atom/the_target, vision_range, check_faction = TRUE)
+ if(isconstruct(the_target) || istype(the_target, /mob/living/basic/shade))
+ return ..()
+ return FALSE
diff --git a/code/modules/mob/living/basic/constructs/harvester.dm b/code/modules/mob/living/basic/cult/constructs/harvester.dm
similarity index 100%
rename from code/modules/mob/living/basic/constructs/harvester.dm
rename to code/modules/mob/living/basic/cult/constructs/harvester.dm
diff --git a/code/modules/mob/living/basic/cult/constructs/juggernaut.dm b/code/modules/mob/living/basic/cult/constructs/juggernaut.dm
new file mode 100644
index 0000000000000..6beee5554a3ca
--- /dev/null
+++ b/code/modules/mob/living/basic/cult/constructs/juggernaut.dm
@@ -0,0 +1,61 @@
+/mob/living/basic/construct/juggernaut
+ name = "Juggernaut"
+ real_name = "Juggernaut"
+ desc = "A massive, armored construct built to spearhead attacks and soak up enemy fire."
+ icon_state = "juggernaut"
+ icon_living = "juggernaut"
+ maxHealth = 150
+ health = 150
+ response_harm_continuous = "harmlessly punches"
+ response_harm_simple = "harmlessly punch"
+ obj_damage = 90
+ melee_damage_lower = 25
+ melee_damage_upper = 25
+ attack_verb_continuous = "smashes their armored gauntlet into"
+ attack_verb_simple = "smash your armored gauntlet into"
+ speed = 2.5
+ attack_sound = 'sound/weapons/punch3.ogg'
+ status_flags = NONE
+ mob_size = MOB_SIZE_LARGE
+ force_threshold = 10
+ construct_spells = list(
+ /datum/action/cooldown/spell/basic_projectile/juggernaut,
+ /datum/action/cooldown/spell/forcewall/cult,
+ /datum/action/innate/cult/create_rune/wall,
+ )
+ playstyle_string = span_bold("You are a Juggernaut. Though slow, your shell can withstand heavy punishment, create shield walls, rip apart enemies and walls alike, and even deflect energy weapons.")
+
+ smashes_walls = TRUE
+
+/// Hostile NPC version. Pretty dumb, just attacks whoever is near.
+/mob/living/basic/construct/juggernaut/hostile
+ ai_controller = /datum/ai_controller/basic_controller/juggernaut
+ smashes_walls = FALSE
+ melee_attack_cooldown = 2 SECONDS
+
+/mob/living/basic/construct/juggernaut/bullet_act(obj/projectile/bullet)
+ if(!istype(bullet, /obj/projectile/energy) && !istype(bullet, /obj/projectile/beam))
+ return ..()
+ if(!prob(40 - round(bullet.damage / 3))) // reflect chance
+ return ..()
+
+ apply_damage(bullet.damage * 0.5, bullet.damage_type)
+ visible_message(
+ span_danger("The [bullet.name] is reflected by [src]'s armored shell!"),
+ span_userdanger("The [bullet.name] is reflected by your armored shell!"),
+ )
+
+ bullet.reflect(src)
+
+ return BULLET_ACT_FORCE_PIERCE // complete projectile permutation
+
+// Alternate juggernaut themes
+/mob/living/basic/construct/juggernaut/angelic
+ theme = THEME_HOLY
+
+/mob/living/basic/construct/juggernaut/angelic/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_ANGELIC, INNATE_TRAIT)
+
+/mob/living/basic/construct/juggernaut/mystic
+ theme = THEME_WIZARD
diff --git a/code/modules/mob/living/basic/cult/constructs/proteon.dm b/code/modules/mob/living/basic/cult/constructs/proteon.dm
new file mode 100644
index 0000000000000..2ff58d2463c0b
--- /dev/null
+++ b/code/modules/mob/living/basic/cult/constructs/proteon.dm
@@ -0,0 +1,39 @@
+/// Proteon - a very weak construct that only appears in NPC form in various ruins.
+/mob/living/basic/construct/proteon
+ name = "Proteon"
+ real_name = "Proteon"
+ desc = "A weaker construct meant to scour ruins for objects of Nar'Sie's affection. Those barbed claws are no joke."
+ icon_state = "proteon"
+ icon_living = "proteon"
+ maxHealth = 35
+ health = 35
+ melee_damage_lower = 8
+ melee_damage_upper = 10
+ attack_verb_continuous = "pinches"
+ attack_verb_simple = "pinch"
+ smashes_walls = TRUE
+ attack_sound = 'sound/weapons/punch2.ogg'
+ playstyle_string = span_bold("You are a Proteon. Your abilities in combat are outmatched by most combat constructs, but you are still fast and nimble. Run metal and supplies, and cooperate with your fellow cultists.")
+
+/// Hostile NPC version
+/mob/living/basic/construct/proteon/hostile
+ ai_controller = /datum/ai_controller/basic_controller/proteon
+ smashes_walls = FALSE
+ melee_attack_cooldown = 1.5 SECONDS
+
+/mob/living/basic/construct/proteon/hostile/Initialize(mapload)
+ . = ..()
+ var/datum/callback/retaliate_callback = CALLBACK(src, PROC_REF(ai_retaliate_behaviour))
+ AddComponent(/datum/component/ai_retaliate_advanced, retaliate_callback)
+
+/// Set a timer to clear our retaliate list
+/mob/living/basic/construct/proteon/hostile/proc/ai_retaliate_behaviour(mob/living/attacker)
+ if (!istype(attacker))
+ return
+ var/random_timer = rand(2 SECONDS, 4 SECONDS) //for unpredictability
+ addtimer(CALLBACK(src, PROC_REF(clear_retaliate_list)), random_timer)
+
+/mob/living/basic/construct/proteon/hostile/proc/clear_retaliate_list()
+ if(!ai_controller.blackboard_key_exists(BB_BASIC_MOB_RETALIATE_LIST))
+ return
+ ai_controller.clear_blackboard_key(BB_BASIC_MOB_RETALIATE_LIST)
diff --git a/code/modules/mob/living/basic/cult/constructs/wraith.dm b/code/modules/mob/living/basic/cult/constructs/wraith.dm
new file mode 100644
index 0000000000000..06a09b6446ed3
--- /dev/null
+++ b/code/modules/mob/living/basic/cult/constructs/wraith.dm
@@ -0,0 +1,50 @@
+/mob/living/basic/construct/wraith
+ name = "Wraith"
+ real_name = "Wraith"
+ desc = "A wicked, clawed shell constructed to assassinate enemies and sow chaos behind enemy lines."
+ icon_state = "wraith"
+ icon_living = "wraith"
+ maxHealth = 65
+ health = 65
+ melee_damage_lower = 20
+ melee_damage_upper = 20
+ attack_verb_continuous = "slashes"
+ attack_verb_simple = "slash"
+ attack_sound = 'sound/weapons/bladeslice.ogg'
+ attack_vis_effect = ATTACK_EFFECT_SLASH
+ construct_spells = list(
+ /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift,
+ /datum/action/innate/cult/create_rune/tele,
+ )
+ playstyle_string = span_bold("You are a Wraith. Though relatively fragile, you are fast, deadly, and can phase through walls. Your attacks will lower the cooldown on phasing, moreso for fatal blows.")
+
+/mob/living/basic/construct/wraith/Initialize(mapload)
+ . = ..()
+ var/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/jaunt = locate() in actions
+ if(isnull(jaunt))
+ return .
+ AddComponent(/datum/component/recharging_attacks, recharged_action = jaunt)
+
+/// Hostile NPC version. Attempts to kill the lowest-health mob it can see.
+/mob/living/basic/construct/wraith/hostile
+ ai_controller = /datum/ai_controller/basic_controller/wraith
+ melee_attack_cooldown = 1.5 SECONDS
+
+// Alternate wraith themes
+/mob/living/basic/construct/wraith/angelic
+ theme = THEME_HOLY
+ construct_spells = list(
+ /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/angelic,
+ /datum/action/innate/cult/create_rune/tele,
+ )
+
+/mob/living/basic/construct/wraith/angelic/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_ANGELIC, INNATE_TRAIT)
+
+/mob/living/basic/construct/wraith/mystic
+ theme = THEME_WIZARD
+ construct_spells = list(
+ /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/mystic,
+ /datum/action/innate/cult/create_rune/tele,
+ )
diff --git a/code/modules/mob/living/basic/cult/shade.dm b/code/modules/mob/living/basic/cult/shade.dm
new file mode 100644
index 0000000000000..fac1d347665ef
--- /dev/null
+++ b/code/modules/mob/living/basic/cult/shade.dm
@@ -0,0 +1,71 @@
+/mob/living/basic/shade
+ name = "Shade"
+ real_name = "Shade"
+ desc = "A bound spirit."
+ gender = PLURAL
+ icon = 'icons/mob/nonhuman-player/cult.dmi'
+ icon_state = "shade_cult"
+ icon_living = "shade_cult"
+ mob_biotypes = MOB_SPIRIT
+ maxHealth = 40
+ health = 40
+ speak_emote = list("hisses")
+ response_help_continuous = "puts their hand through"
+ response_help_simple = "put your hand through"
+ response_disarm_continuous = "flails at"
+ response_disarm_simple = "flail at"
+ response_harm_continuous = "punches"
+ response_harm_simple = "punch"
+ melee_damage_lower = 5
+ melee_damage_upper = 12
+ attack_verb_continuous = "metaphysically strikes"
+ attack_verb_simple = "metaphysically strike"
+ unsuitable_cold_damage = 0
+ unsuitable_heat_damage = 0
+ unsuitable_atmos_damage = 0
+ speed = -1
+ faction = list(FACTION_CULT)
+ basic_mob_flags = DEL_ON_DEATH
+ initial_language_holder = /datum/language_holder/construct
+ /// Theme controls color. THEME_CULT is red THEME_WIZARD is purple and THEME_HOLY is blue
+ var/theme = THEME_CULT
+ /// The different flavors of goop shades can drop, depending on theme.
+ var/static/list/remains_by_theme = list(
+ THEME_CULT = list(/obj/item/ectoplasm/construct),
+ THEME_HOLY = list(/obj/item/ectoplasm/angelic),
+ THEME_WIZARD = list(/obj/item/ectoplasm/mystic),
+ )
+
+/mob/living/basic/shade/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/simple_flying)
+ add_traits(list(TRAIT_HEALS_FROM_CULT_PYLONS, TRAIT_SPACEWALK, TRAIT_VENTCRAWLER_ALWAYS), INNATE_TRAIT)
+ if(isnull(theme))
+ return
+ icon_state = "shade_[theme]"
+ var/list/remains = string_list(remains_by_theme[theme])
+ if(length(remains))
+ AddElement(/datum/element/death_drops, remains)
+
+/mob/living/basic/shade/update_icon_state()
+ . = ..()
+ if(!isnull(theme))
+ icon_state = "shade_[theme]"
+ icon_living = icon_state
+
+/mob/living/basic/shade/death()
+ if(death_message == initial(death_message))
+ death_message = "lets out a contented sigh as [p_their()] form unwinds."
+ ..()
+
+/mob/living/basic/shade/can_suicide()
+ if(istype(loc, /obj/item/soulstone)) //do not suicide inside the soulstone
+ return FALSE
+ return ..()
+
+/mob/living/basic/shade/attackby(obj/item/item, mob/user, params)
+ if(istype(item, /obj/item/soulstone))
+ var/obj/item/soulstone/stone = item
+ stone.capture_shade(src, user)
+ else
+ . = ..()
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm b/code/modules/mob/living/basic/drone/_drone.dm
similarity index 81%
rename from code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
rename to code/modules/mob/living/basic/drone/_drone.dm
index 7f6a398b9dafc..3b8a960f2cf07 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
+++ b/code/modules/mob/living/basic/drone/_drone.dm
@@ -1,6 +1,6 @@
/**
- * # Maintenance Drone
+ * Maintenance Drone
*
* Small player controlled fixer-upper
*
@@ -13,7 +13,7 @@
* They have laws to prevent them from doing anything else.
*
*/
-/mob/living/simple_animal/drone
+/mob/living/basic/drone
name = "Drone"
desc = "A maintenance drone, an expendable robot built to perform station repairs."
icon = 'icons/mob/silicon/drone.dmi'
@@ -23,9 +23,8 @@
health = 45
maxHealth = 45
unsuitable_atmos_damage = 0
- minbodytemp = 0
- maxbodytemp = 0
- wander = 0
+ unsuitable_cold_damage = 0
+ unsuitable_heat_damage = 0
speed = 0
density = FALSE
pass_flags = PASSTABLE | PASSMOB
@@ -43,7 +42,6 @@
hud_possible = list(DIAG_STAT_HUD, DIAG_HUD, ANTAG_HUD)
unique_name = TRUE
faction = list(FACTION_NEUTRAL,FACTION_SILICON,FACTION_TURRET)
- dextrous = TRUE
hud_type = /datum/hud/dextrous/drone
// Going for a sort of pale green here
lighting_cutoff_red = 30
@@ -52,7 +50,6 @@
can_be_held = TRUE
worn_slot_flags = ITEM_SLOT_HEAD
- held_items = list(null, null)
/// `TRUE` if we have picked our visual appearance, `FALSE` otherwise (default)
var/picked = FALSE
/// Stored drone color, restored when unhacked
@@ -71,9 +68,9 @@
var/obj/item/internal_storage
/// Headwear slot
var/obj/item/head
- /// Default [/mob/living/simple_animal/drone/var/internal_storage] item
+ /// Default [/mob/living/basic/drone/var/internal_storage] item
var/obj/item/default_storage = /obj/item/storage/drone_tools
- /// Default [/mob/living/simple_animal/drone/var/head] item
+ /// Default [/mob/living/basic/drone/var/head] item
var/obj/item/default_headwear
/**
* icon_state of drone from icons/mobs/drone.dmi
@@ -86,9 +83,11 @@
* - [CLOCKDRONE]
*/
var/visualAppearance = MAINTDRONE
- /// Hacked state, see [/mob/living/simple_animal/drone/proc/update_drone_hack]
+ /// Hacked state, see [/mob/living/basic/drone/proc/update_drone_hack]
var/hacked = FALSE
- /// If we have laws to minimize bothering others. Enables or disables drone laws enforcement components (use [/mob/living/simple_animal/drone/proc/set_shy] to set)
+ /// Whether this drone can be un-hacked. Used for subtypes that cannot be meaningfully "fixed".
+ var/can_unhack = TRUE
+ /// If we have laws to minimize bothering others. Enables or disables drone laws enforcement components (use [/mob/living/basic/drone/proc/set_shy] to set)
var/shy = TRUE
/// Flavor text announced to drones on [/mob/proc/Login]
var/flavortext = \
@@ -169,19 +168,16 @@
/obj/machinery/atmospherics/components/unary/vent_scrubber,
)
-/mob/living/simple_animal/drone/Initialize(mapload)
+/mob/living/basic/drone/Initialize(mapload)
. = ..()
GLOB.drones_list += src
- access_card = new /obj/item/card/id/advanced/simple_bot(src)
+ AddElement(/datum/element/dextrous, hud_type = hud_type)
AddComponent(/datum/component/basic_inhands, y_offset = getItemPixelShiftY())
-
- // Doing this hurts my soul, but simple_animal access reworks are for another day.
- var/datum/id_trim/job/cap_trim = SSid_access.trim_singletons_by_path[/datum/id_trim/job/captain]
- access_card.add_access(cap_trim.access + cap_trim.wildcard_access)
+ AddComponent(/datum/component/simple_access, SSid_access.get_region_access_list(list(REGION_ALL_GLOBAL)))
if(default_storage)
- var/obj/item/I = new default_storage(src)
- equip_to_slot_or_del(I, ITEM_SLOT_DEX_STORAGE)
+ var/obj/item/storage = new default_storage(src)
+ equip_to_slot_or_del(storage, ITEM_SLOT_DEX_STORAGE)
for(var/holiday_name in GLOB.holidays)
var/datum/holiday/holiday_today = GLOB.holidays[holiday_name]
@@ -193,8 +189,6 @@
var/obj/item/new_hat = new default_headwear(src)
equip_to_slot_or_del(new_hat, ITEM_SLOT_HEAD)
- ADD_TRAIT(access_card, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT)
-
shy_update()
alert_drones(DRONE_NET_CONNECT)
@@ -202,7 +196,7 @@
for(var/datum/atom_hud/data/diagnostic/diag_hud in GLOB.huds)
diag_hud.add_atom_to_hud(src)
- add_traits(list(TRAIT_VENTCRAWLER_ALWAYS, TRAIT_NEGATES_GRAVITY, TRAIT_LITERATE, TRAIT_KNOW_ENGI_WIRES), INNATE_TRAIT)
+ add_traits(list(TRAIT_VENTCRAWLER_ALWAYS, TRAIT_NEGATES_GRAVITY, TRAIT_LITERATE, TRAIT_KNOW_ENGI_WIRES, TRAIT_ADVANCEDTOOLUSER), INNATE_TRAIT)
listener = new(list(ALARM_ATMOS, ALARM_FIRE, ALARM_POWER), list(z))
RegisterSignal(listener, COMSIG_ALARM_LISTENER_TRIGGERED, PROC_REF(alarm_triggered))
@@ -210,16 +204,16 @@
listener.RegisterSignal(src, COMSIG_LIVING_DEATH, TYPE_PROC_REF(/datum/alarm_listener, prevent_alarm_changes))
listener.RegisterSignal(src, COMSIG_LIVING_REVIVE, TYPE_PROC_REF(/datum/alarm_listener, allow_alarm_changes))
-/mob/living/simple_animal/drone/med_hud_set_health()
+/mob/living/basic/drone/med_hud_set_health()
var/image/holder = hud_list[DIAG_HUD]
- var/icon/I = icon(icon, icon_state, dir)
- holder.pixel_y = I.Height() - world.icon_size
+ var/icon/hud_icon = icon(icon, icon_state, dir)
+ holder.pixel_y = hud_icon.Height() - world.icon_size
holder.icon_state = "huddiag[RoundDiagBar(health/maxHealth)]"
-/mob/living/simple_animal/drone/med_hud_set_status()
+/mob/living/basic/drone/med_hud_set_status()
var/image/holder = hud_list[DIAG_STAT_HUD]
- var/icon/I = icon(icon, icon_state, dir)
- holder.pixel_y = I.Height() - world.icon_size
+ var/icon/hud_icon = icon(icon, icon_state, dir)
+ holder.pixel_y = hud_icon.Height() - world.icon_size
if(stat == DEAD)
holder.icon_state = "huddead2"
else if(incapacitated())
@@ -227,13 +221,12 @@
else
holder.icon_state = "hudstat"
-/mob/living/simple_animal/drone/Destroy()
+/mob/living/basic/drone/Destroy()
GLOB.drones_list -= src
- QDEL_NULL(access_card) //Otherwise it ends up on the floor!
QDEL_NULL(listener)
return ..()
-/mob/living/simple_animal/drone/Login()
+/mob/living/basic/drone/Login()
. = ..()
if(!. || !client)
return FALSE
@@ -245,14 +238,14 @@
if(!picked)
pickVisualAppearance()
-/mob/living/simple_animal/drone/auto_deadmin_on_login()
+/mob/living/basic/drone/auto_deadmin_on_login()
if(!client?.holder)
return TRUE
if(CONFIG_GET(flag/auto_deadmin_silicons) || (client.prefs?.toggles & DEADMIN_POSITION_SILICON))
return client.holder.auto_deadmin()
return ..()
-/mob/living/simple_animal/drone/death(gibbed)
+/mob/living/basic/drone/death(gibbed)
..(gibbed)
if(internal_storage)
dropItemToGround(internal_storage)
@@ -262,10 +255,10 @@
alert_drones(DRONE_NET_DISCONNECT)
-/mob/living/simple_animal/drone/gib()
+/mob/living/basic/drone/gib()
dust()
-/mob/living/simple_animal/drone/examine(mob/user)
+/mob/living/basic/drone/examine(mob/user)
. = list("This is [icon2html(src, user)] \a [src]!")
//Hands
@@ -306,11 +299,11 @@
. += ""
-/mob/living/simple_animal/drone/assess_threat(judgement_criteria, lasercolor = "", datum/callback/weaponcheck=null) //Secbots won't hunt maintenance drones.
+/mob/living/basic/drone/assess_threat(judgement_criteria, lasercolor = "", datum/callback/weaponcheck=null) //Secbots won't hunt maintenance drones.
return -10
-/mob/living/simple_animal/drone/emp_act(severity)
+/mob/living/basic/drone/emp_act(severity)
. = ..()
if(. & EMP_PROTECT_SELF)
return
@@ -320,32 +313,32 @@
adjustBruteLoss(heavy_emp_damage)
to_chat(src, span_userdanger("HeAV% DA%^MMA+G TO I/O CIR!%UUT!"))
-/mob/living/simple_animal/drone/proc/alarm_triggered(datum/source, alarm_type, area/source_area)
+/mob/living/basic/drone/proc/alarm_triggered(datum/source, alarm_type, area/source_area)
SIGNAL_HANDLER
to_chat(src, "--- [alarm_type] alarm detected in [source_area.name]!")
-/mob/living/simple_animal/drone/proc/alarm_cleared(datum/source, alarm_type, area/source_area)
+/mob/living/basic/drone/proc/alarm_cleared(datum/source, alarm_type, area/source_area)
SIGNAL_HANDLER
to_chat(src, "--- [alarm_type] alarm in [source_area.name] has been cleared.")
-/mob/living/simple_animal/drone/proc/blacklist_on_try_use_machine(datum/source, obj/machinery/machine)
+/mob/living/basic/drone/proc/blacklist_on_try_use_machine(datum/source, obj/machinery/machine)
SIGNAL_HANDLER
if(GLOB.drone_machine_blacklist_enabled && is_type_in_typecache(machine, drone_machinery_blacklist_compiled))
to_chat(src, span_warning("Using [machine] could break your laws."))
return COMPONENT_CANT_USE_MACHINE_INTERACT | COMPONENT_CANT_USE_MACHINE_TOOLS
-/mob/living/simple_animal/drone/proc/blacklist_on_try_wires_interact(datum/source, atom/machine)
+/mob/living/basic/drone/proc/blacklist_on_try_wires_interact(datum/source, atom/machine)
SIGNAL_HANDLER
if(GLOB.drone_machine_blacklist_enabled && is_type_in_typecache(machine, drone_machinery_blacklist_compiled))
to_chat(src, span_warning("Using [machine] could break your laws."))
return COMPONENT_CANT_INTERACT_WIRES
-/mob/living/simple_animal/drone/proc/set_shy(new_shy)
+/mob/living/basic/drone/proc/set_shy(new_shy)
shy = new_shy
shy_update()
-/mob/living/simple_animal/drone/proc/shy_update()
+/mob/living/basic/drone/proc/shy_update()
var/list/drone_bad_areas = make_associative(drone_area_blacklist_flat) + typecacheof(drone_area_blacklist_recursive)
var/list/drone_good_items = make_associative(drone_item_whitelist_flat) + typecacheof(drone_item_whitelist_recursive)
@@ -353,7 +346,7 @@
var/list/drone_good_machinery = LAZYCOPY(drone_machinery_whitelist_flat) + typecacheof(drone_machinery_whitelist_recursive) // not a valid typecache, only intended for negation against drone_bad_machinery
drone_machinery_blacklist_compiled = drone_bad_machinery - drone_good_machinery
- var/static/list/not_shy_of = typecacheof(list(/mob/living/simple_animal/drone, /mob/living/simple_animal/bot))
+ var/static/list/not_shy_of = typecacheof(list(/mob/living/basic/drone, /mob/living/simple_animal/bot))
if(shy)
ADD_TRAIT(src, TRAIT_PACIFISM, DRONE_SHY_TRAIT)
LoadComponent(/datum/component/shy, mob_whitelist=not_shy_of, shy_range=3, message="Your laws prevent this action near %TARGET.", keyless_shy=FALSE, clientless_shy=TRUE, dead_shy=FALSE, dead_shy_immediate=TRUE, machine_whitelist=shy_machine_whitelist)
@@ -370,16 +363,13 @@
qdel(GetComponent(/datum/component/itempicky))
UnregisterSignal(src, list(COMSIG_TRY_USE_MACHINE, COMSIG_TRY_WIRES_INTERACT))
-/mob/living/simple_animal/drone/handle_temperature_damage()
- return
-
-/mob/living/simple_animal/drone/flash_act(intensity = 1, override_blindness_check = 0, affect_silicon = 0, visual = 0, type = /atom/movable/screen/fullscreen/flash, length = 25)
+/mob/living/basic/drone/flash_act(intensity = 1, override_blindness_check = 0, affect_silicon = 0, visual = 0, type = /atom/movable/screen/fullscreen/flash, length = 25)
if(affect_silicon)
return ..()
-/mob/living/simple_animal/drone/bee_friendly()
+/mob/living/basic/drone/bee_friendly()
// Why would bees pay attention to drones?
return TRUE
-/mob/living/simple_animal/drone/electrocute_act(shock_damage, source, siemens_coeff, flags = NONE)
+/mob/living/basic/drone/electrocute_act(shock_damage, source, siemens_coeff, flags = NONE)
return FALSE //So they don't die trying to fix wiring
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/drone_say.dm b/code/modules/mob/living/basic/drone/drone_say.dm
similarity index 55%
rename from code/modules/mob/living/simple_animal/friendly/drone/drone_say.dm
rename to code/modules/mob/living/basic/drone/drone_say.dm
index 88fc3b5cd85bd..af0bef41bb1ca 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/drone_say.dm
+++ b/code/modules/mob/living/basic/drone/drone_say.dm
@@ -6,21 +6,21 @@
* * dead_can_hear - Boolean that determines if ghosts can hear the message (`FALSE` by default)
* * source - [/atom] source that created the message
* * faction_checked_mob - [/mob/living] to determine faction matches from
- * * exact_faction_match - Passed to [/mob/proc/faction_check_mob]
+ * * exact_faction_match - Passed to [/mob/proc/faction_check_atom]
*/
/proc/_alert_drones(msg, dead_can_hear = FALSE, atom/source, mob/living/faction_checked_mob, exact_faction_match)
if (dead_can_hear && source)
- for (var/mob/M in GLOB.dead_mob_list)
- var/link = FOLLOW_LINK(M, source)
- to_chat(M, "[link] [msg]")
- for(var/i in GLOB.drones_list)
- var/mob/living/simple_animal/drone/D = i
- if(istype(D) && D.stat != DEAD)
+ for (var/mob/dead_mob in GLOB.dead_mob_list)
+ var/link = FOLLOW_LINK(dead_mob, source)
+ to_chat(dead_mob, "[link] [msg]")
+ for(var/global_drone in GLOB.drones_list)
+ var/mob/living/basic/drone/drone = global_drone
+ if(istype(drone) && drone.stat != DEAD)
if(faction_checked_mob)
- if(D.faction_check_mob(faction_checked_mob, exact_faction_match))
- to_chat(D, msg)
+ if(drone.faction_check_atom(faction_checked_mob, exact_faction_match))
+ to_chat(drone, msg)
else
- to_chat(D, msg)
+ to_chat(drone, msg)
@@ -28,16 +28,16 @@
* Wraps [/proc/_alert_drones] with defaults
*
* * source - `src`
- * * faction_check_mob - `src`
+ * * faction_check_atom - `src`
* * dead_can_hear - `TRUE`
*/
-/mob/living/simple_animal/drone/proc/alert_drones(msg, dead_can_hear = FALSE)
+/mob/living/basic/drone/proc/alert_drones(msg, dead_can_hear = FALSE)
_alert_drones(msg, dead_can_hear, src, src, TRUE)
/**
- * Wraps [/mob/living/simple_animal/drone/proc/alert_drones] as a Drone Chat
+ * Wraps [/mob/living/basic/drone/proc/alert_drones] as a Drone Chat
*
* Shares the same radio code with binary
*/
-/mob/living/simple_animal/drone/proc/drone_chat(msg)
+/mob/living/basic/drone/proc/drone_chat(msg)
alert_drones("Drone Chat: [span_name("[name]")] [say_quote(msg)]", TRUE)
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/drone_tools.dm b/code/modules/mob/living/basic/drone/drone_tools.dm
similarity index 100%
rename from code/modules/mob/living/simple_animal/friendly/drone/drone_tools.dm
rename to code/modules/mob/living/basic/drone/drone_tools.dm
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm b/code/modules/mob/living/basic/drone/drones_as_items.dm
similarity index 81%
rename from code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm
rename to code/modules/mob/living/basic/drone/drones_as_items.dm
index c163066ae1e16..d816c9b3060c0 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm
+++ b/code/modules/mob/living/basic/drone/drones_as_items.dm
@@ -1,12 +1,7 @@
-///////////////////
-//DRONES AS ITEMS//
-///////////////////
-//Drone shells
-
/** Drone Shell: Ghost role item for drones
*
* A simple mob spawner item that transforms into a maintenance drone
- * Resepcts drone minimum age
+ * Respects drone minimum age
*/
/obj/effect/mob_spawn/ghost_role/drone
@@ -18,7 +13,7 @@
density = FALSE
mob_name = "drone"
///Type of drone that will be spawned
- mob_type = /mob/living/simple_animal/drone
+ mob_type = /mob/living/basic/drone
role_ban = ROLE_DRONE
show_flavor = FALSE
prompt_name = "maintenance drone"
@@ -29,13 +24,19 @@
/obj/effect/mob_spawn/ghost_role/drone/Initialize(mapload)
. = ..()
- var/area/A = get_area(src)
- if(A)
- notify_ghosts("A drone shell has been created in \the [A.name].", source = src, action=NOTIFY_ATTACK, flashwindow = FALSE, ignore_key = POLL_IGNORE_DRONE, notify_suiciders = FALSE)
+ var/area/area = get_area(src)
+ if(area)
+ notify_ghosts(
+ "A drone shell has been created in \the [area.name].",
+ source = src,
+ action = NOTIFY_PLAY,
+ notify_flags = (GHOST_NOTIFY_IGNORE_MAPLOAD),
+ ignore_key = POLL_IGNORE_DRONE,
+ )
/obj/effect/mob_spawn/ghost_role/drone/allow_spawn(mob/user, silent = FALSE)
var/client/user_client = user.client
- var/mob/living/simple_animal/drone/drone_type = mob_type
+ var/mob/living/basic/drone/drone_type = mob_type
if(!initial(drone_type.shy) || isnull(user_client) || !CONFIG_GET(flag/use_exp_restrictions_other))
return ..()
var/required_role = CONFIG_GET(string/drone_required_role)
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/extra_drone_types.dm b/code/modules/mob/living/basic/drone/extra_drone_types.dm
similarity index 75%
rename from code/modules/mob/living/simple_animal/friendly/drone/extra_drone_types.dm
rename to code/modules/mob/living/basic/drone/extra_drone_types.dm
index d4353c95c824a..927d28f0ca249 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/extra_drone_types.dm
+++ b/code/modules/mob/living/basic/drone/extra_drone_types.dm
@@ -1,15 +1,8 @@
-////////////////////
-//MORE DRONE TYPES//
-////////////////////
-//Drones with custom laws
-//Drones with custom shells
-//Drones with overridden procs
-//Drones with camogear for hat related memes
-//Drone type for use with polymorph (no preloaded items, random appearance)
-
-
-//More types of drones
-/mob/living/simple_animal/drone/syndrone
+/**
+* A Syndicate drone, tasked to cause chaos on the station.
+* Has a lot more health and its own uplink with 10 TC.
+*/
+/mob/living/basic/drone/syndrone
name = "Syndrone"
desc = "A modified maintenance drone. This one brings with it the feeling of terror."
icon_state = "drone_synd"
@@ -29,38 +22,25 @@
default_storage = /obj/item/uplink
default_headwear = /obj/item/clothing/head/helmet/swat
hacked = TRUE
+ can_unhack = FALSE
shy = FALSE
flavortext = null
-/mob/living/simple_animal/drone/syndrone/Initialize(mapload)
- . = ..()
- var/datum/component/uplink/hidden_uplink = internal_storage.GetComponent(/datum/component/uplink)
- hidden_uplink.set_telecrystals(10)
+ /// The number of telecrystals to put in the drone's uplink
+ var/telecrystal_count = 10
-/mob/living/simple_animal/drone/syndrone/badass
- name = "Badass Syndrone"
- default_storage = /obj/item/uplink/nuclear
-/mob/living/simple_animal/drone/syndrone/badass/Initialize(mapload)
+/mob/living/basic/drone/syndrone/Initialize(mapload)
. = ..()
var/datum/component/uplink/hidden_uplink = internal_storage.GetComponent(/datum/component/uplink)
- hidden_uplink.set_telecrystals(30)
- var/obj/item/implant/weapons_auth/W = new/obj/item/implant/weapons_auth(src)
- W.implant(src, force = TRUE)
-
-/mob/living/simple_animal/drone/snowflake
- default_headwear = /obj/item/clothing/head/chameleon/drone
-
-/mob/living/simple_animal/drone/snowflake/Initialize(mapload)
- . = ..()
- desc += " This drone appears to have a complex holoprojector built on its 'head'."
+ hidden_uplink.set_telecrystals(telecrystal_count)
/obj/effect/mob_spawn/ghost_role/drone/syndrone
name = "syndrone shell"
desc = "A shell of a syndrone, a modified maintenance drone designed to infiltrate and annihilate."
icon_state = "syndrone_item"
mob_name = "syndrone"
- mob_type = /mob/living/simple_animal/drone/syndrone
+ mob_type = /mob/living/basic/drone/syndrone
prompt_name = "a syndrone"
you_are_text = "You are a Syndicate Maintenance Drone."
flavour_text = "In a prior life, you maintained a Nanotrasen Research Station. Abducted from your home, you were given some upgrades... and now serve an enemy of your former masters."
@@ -71,27 +51,44 @@
title = ROLE_SYNDICATE_DRONE
policy_index = ROLE_SYNDICATE_DRONE
+/// A version of the syndrone that gets a nuclear uplink, a firearms implant, and 30 TC.
+/mob/living/basic/drone/syndrone/badass
+ name = "Badass Syndrone"
+ default_storage = /obj/item/uplink/nuclear
+ telecrystal_count = 30
+
+/mob/living/basic/drone/syndrone/badass/Initialize(mapload)
+ . = ..()
+ var/obj/item/implant/weapons_auth/weapon_implant = new/obj/item/implant/weapons_auth(src)
+ weapon_implant.implant(src, force = TRUE)
+
/obj/effect/mob_spawn/ghost_role/drone/syndrone/badass
name = "badass syndrone shell"
mob_name = "badass syndrone"
- mob_type = /mob/living/simple_animal/drone/syndrone/badass
+ mob_type = /mob/living/basic/drone/syndrone/badass
prompt_name = "a badass syndrone"
flavour_text = "In a prior life, you maintained a Nanotrasen Research Station. Abducted from your home, you were given some BETTER upgrades... and now serve an enemy of your former masters."
+/// A drone that spawns with a chameleon hat for fashion purposes.
+/mob/living/basic/drone/snowflake
+ default_headwear = /obj/item/clothing/head/chameleon/drone
+ desc = "A maintenance drone, an expendable robot built to perform station repairs. This drone appears to have a complex holoprojector built on its 'head'."
+
/obj/effect/mob_spawn/ghost_role/drone/snowflake
name = "snowflake drone shell"
desc = "A shell of a snowflake drone, a maintenance drone with a built in holographic projector to display hats and masks."
mob_name = "snowflake drone"
prompt_name = "a drone with a holohat projector"
- mob_type = /mob/living/simple_animal/drone/snowflake
+ mob_type = /mob/living/basic/drone/snowflake
-/mob/living/simple_animal/drone/polymorphed
+/// A free drone that people can be turned into via wabbajack.
+/mob/living/basic/drone/polymorphed
default_storage = null
default_headwear = null
picked = TRUE
flavortext = null
-/mob/living/simple_animal/drone/polymorphed/Initialize(mapload)
+/mob/living/basic/drone/polymorphed/Initialize(mapload)
. = ..()
liberate()
visualAppearance = pick(MAINTDRONE, REPAIRDRONE, SCOUTDRONE)
@@ -104,33 +101,17 @@
icon_living = icon_state
icon_dead = "[visualAppearance]_dead"
-/obj/effect/mob_spawn/ghost_role/drone/classic
- mob_type = /mob/living/simple_animal/drone/classic
-
-/mob/living/simple_animal/drone/classic
+/// "Classic" drones, which are not shy and get a duffelbag of tools instead of built-in tools.
+/mob/living/basic/drone/classic
name = "classic drone shell"
shy = FALSE
default_storage = /obj/item/storage/backpack/duffelbag/drone
-/obj/effect/mob_spawn/ghost_role/drone/derelict
- name = "derelict drone shell"
- desc = "A long-forgotten drone shell. It seems kind of... Space Russian."
- icon = 'icons/mob/silicon/drone.dmi'
- icon_state = "drone_maint_hat"
- mob_name = "derelict drone"
- mob_type = /mob/living/simple_animal/drone/derelict
- anchored = TRUE
- prompt_name = "a derelict drone"
- you_are_text = "You are a drone on Kosmicheskaya Stantsiya 13."
- flavour_text = "Something has brought you out of hibernation, and the station is in gross disrepair."
- important_text = "Build, repair, maintain and improve the station that housed you on activation."
- spawner_job_path = /datum/job/derelict_drone
-
-/datum/job/derelict_drone
- title = ROLE_DERELICT_DRONE
- policy_index = ROLE_DERELICT_DRONE
+/obj/effect/mob_spawn/ghost_role/drone/classic
+ mob_type = /mob/living/basic/drone/classic
-/mob/living/simple_animal/drone/derelict
+/// Derelict drones, a ghost role tasked with repairing KS13. Get gibbed if they leave.
+/mob/living/basic/drone/derelict
name = "derelict drone"
default_headwear = /obj/item/clothing/head/costume/ushanka
laws = \
@@ -148,8 +129,24 @@
"If you do not have the regular drone laws, follow your laws to the best of your ability."
shy = FALSE
-/mob/living/simple_animal/drone/derelict/Initialize(mapload)
+/mob/living/basic/drone/derelict/Initialize(mapload)
. = ..()
AddComponent(/datum/component/stationstuck, PUNISHMENT_GIB, "01000110 01010101 01000011 01001011 00100000 01011001 01001111 01010101 WARNING: Dereliction of KS13 detected. Self-destruct activated.")
+/obj/effect/mob_spawn/ghost_role/drone/derelict
+ name = "derelict drone shell"
+ desc = "A long-forgotten drone shell. It seems kind of... Space Russian."
+ icon = 'icons/mob/silicon/drone.dmi'
+ icon_state = "drone_maint_hat"
+ mob_name = "derelict drone"
+ mob_type = /mob/living/basic/drone/derelict
+ anchored = TRUE
+ prompt_name = "a derelict drone"
+ you_are_text = "You are a drone on Kosmicheskaya Stantsiya 13."
+ flavour_text = "Something has brought you out of hibernation, and the station is in gross disrepair."
+ important_text = "Build, repair, maintain and improve the station that housed you on activation."
+ spawner_job_path = /datum/job/derelict_drone
+/datum/job/derelict_drone
+ title = ROLE_DERELICT_DRONE
+ policy_index = ROLE_DERELICT_DRONE
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/interaction.dm b/code/modules/mob/living/basic/drone/interaction.dm
similarity index 76%
rename from code/modules/mob/living/simple_animal/friendly/drone/interaction.dm
rename to code/modules/mob/living/basic/drone/interaction.dm
index f6077a5e4800d..ad6261f5cce8b 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/interaction.dm
+++ b/code/modules/mob/living/basic/drone/interaction.dm
@@ -1,12 +1,6 @@
+// Drones' interactions with other mobs
-/////////////////////
-//DRONE INTERACTION//
-/////////////////////
-//How drones interact with the world
-//How the world interacts with drones
-
-
-/mob/living/simple_animal/drone/attack_drone(mob/living/simple_animal/drone/drone)
+/mob/living/basic/drone/attack_drone(mob/living/basic/drone/drone)
if(drone == src || stat != DEAD)
return FALSE
var/input = tgui_alert(drone, "Perform which action?", "Drone Interaction", list("Reactivate", "Cannibalize"))
@@ -24,36 +18,27 @@
drone.visible_message(span_notice("[drone] repairs itself using [src]'s remains!"), span_notice("You repair yourself using [src]'s remains."))
drone.adjustBruteLoss(-src.maxHealth)
new /obj/effect/decal/cleanable/oil/streak(get_turf(src))
+ ghostize(can_reenter_corpse = FALSE)
qdel(src)
else
to_chat(drone, span_warning("You need to remain still to cannibalize [src]!"))
-/mob/living/simple_animal/drone/attack_drone_secondary(mob/living/simple_animal/drone/drone)
+/mob/living/basic/drone/attack_drone_secondary(mob/living/basic/drone/drone)
return SECONDARY_ATTACK_CALL_NORMAL
-//ATTACK HAND IGNORING PARENT RETURN VALUE
-/mob/living/simple_animal/drone/attack_hand(mob/user, list/modifiers)
- if(ishuman(user))
- if(stat == DEAD || status_flags & GODMODE || !can_be_held)
- ..()
- return
- if(user.get_active_held_item())
- to_chat(user, span_warning("Your hands are full!"))
- return
- visible_message(span_warning("[user] starts picking up [src]."), \
- span_userdanger("[user] starts picking you up!"))
- if(!do_after(user, 20, target = src))
- return
- visible_message(span_warning("[user] picks up [src]!"), \
- span_userdanger("[user] picks you up!"))
- if(buckled)
- to_chat(user, span_warning("[src] is buckled to [buckled] and cannot be picked up!"))
- return
- to_chat(user, span_notice("You pick [src] up."))
- drop_all_held_items()
- var/obj/item/clothing/head/mob_holder/drone/DH = new(get_turf(src), src)
- DH.slot_flags = worn_slot_flags
- user.put_in_hands(DH)
+/mob/living/basic/drone/attack_hand(mob/user, list/modifiers)
+ if(isdrone(user))
+ attack_drone(user)
+ return ..()
+
+/mob/living/basic/drone/mob_try_pickup(mob/living/user, instant=FALSE)
+ if(stat == DEAD || status_flags & GODMODE)
+ return
+ return ..()
+
+/mob/living/basic/drone/mob_pickup(mob/living/user)
+ drop_all_held_items()
+ return ..()
/**
* Called when a drone attempts to reactivate a dead drone
@@ -64,7 +49,7 @@
* Arguments:
* * user - The [/mob/living] attempting to reactivate the drone
*/
-/mob/living/simple_animal/drone/proc/try_reactivate(mob/living/user)
+/mob/living/basic/drone/proc/try_reactivate(mob/living/user)
var/mob/dead/observer/G = get_ghost()
if(!client && (!G || !G.client))
var/list/faux_gadgets = list(
@@ -91,9 +76,13 @@
else
to_chat(user, span_warning("You need to remain still to reactivate [src]!"))
-
-/mob/living/simple_animal/drone/screwdriver_act(mob/living/user, obj/item/tool)
+/// Screwdrivering repairs the drone to full hp, if it isn't dead.
+/mob/living/basic/drone/screwdriver_act(mob/living/user, obj/item/tool)
if(stat == DEAD)
+ if(isdrone(user))
+ user.balloon_alert(user, "reactivate instead!")
+ else
+ user.balloon_alert(user, "can't fix!")
return FALSE
if(health >= maxHealth)
to_chat(user, span_warning("[src]'s screws can't get any tighter!"))
@@ -108,7 +97,8 @@
visible_message(span_notice("[user] tightens [src == user ? "[user.p_their()]" : "[src]'s"] loose screws!"), span_notice("[src == user ? "You tighten" : "[user] tightens"] your loose screws."))
return TOOL_ACT_TOOLTYPE_SUCCESS
-/mob/living/simple_animal/drone/wrench_act(mob/living/user, obj/item/tool)
+/// Wrenching un-hacks hacked drones.
+/mob/living/basic/drone/wrench_act(mob/living/user, obj/item/tool)
if(user == src)
return FALSE
user.visible_message(
@@ -123,18 +113,19 @@
update_drone_hack(FALSE)
return TOOL_ACT_TOOLTYPE_SUCCESS
-/mob/living/simple_animal/drone/transferItemToLoc(obj/item/item, newloc, force, silent)
+/mob/living/basic/drone/transferItemToLoc(obj/item/item, newloc, force, silent)
return !(item.type in drone_item_whitelist_flat) && ..()
-/mob/living/simple_animal/drone/getarmor(def_zone, type)
+/mob/living/basic/drone/getarmor(def_zone, type)
var/armorval = 0
if(head)
armorval = head.get_armor_rating(type)
return (armorval * get_armor_effectiveness()) //armor is reduced for tiny fragile drones
-/mob/living/simple_animal/drone/proc/get_armor_effectiveness()
- return 0 //multiplier for whatever head armor you wear as a drone
+/// Returns a multiplier for any head armor you wear as a drone.
+/mob/living/basic/drone/proc/get_armor_effectiveness()
+ return 0
/**
* Hack or unhack a drone
@@ -148,7 +139,7 @@
* Arguments
* * hack - Boolean if the drone is being hacked or unhacked
*/
-/mob/living/simple_animal/drone/proc/update_drone_hack(hack)
+/mob/living/basic/drone/proc/update_drone_hack(hack)
if(!mind)
return
if(hack)
@@ -171,7 +162,7 @@
speed = 1 //gotta go slow
message_admins("[ADMIN_LOOKUPFLW(src)] became a hacked drone hellbent on destroying the station!")
else
- if(!hacked)
+ if(!hacked || !can_unhack)
return
Stun(40)
visible_message(span_info("[src]'s display glows a content blue!"), \
@@ -189,17 +180,10 @@
update_drone_icon_hacked()
/**
- * # F R E E D R O N E
- * ### R
- * ### E
- * ### E
- * ### D
- * ### R
- * ### O
- * ### N
- * ### E
+ * Makes the drone into a Free Drone, who have no real laws and can do whatever they like.
+ * Only currently used for players wabbajacked into drones.
*/
-/mob/living/simple_animal/drone/proc/liberate()
+/mob/living/basic/drone/proc/liberate()
laws = "1. You are a Free Drone."
set_shy(FALSE)
to_chat(src, laws)
@@ -208,12 +192,12 @@
* Changes the icon state to a hacked version
*
* See also
- * * [/mob/living/simple_animal/drone/var/visualAppearance]
+ * * [/mob/living/basic/drone/var/visualAppearance]
* * [MAINTDRONE]
* * [REPAIRDRONE]
* * [SCOUTDRONE]
*/
-/mob/living/simple_animal/drone/proc/update_drone_icon_hacked() //this is hacked both ways
+/mob/living/basic/drone/proc/update_drone_icon_hacked() //this is hacked both ways
var/static/hacked_appearances = list(
SCOUTDRONE = SCOUTDRONE_HACKED,
REPAIRDRONE = REPAIRDRONE_HACKED,
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm b/code/modules/mob/living/basic/drone/inventory.dm
similarity index 62%
rename from code/modules/mob/living/simple_animal/friendly/drone/inventory.dm
rename to code/modules/mob/living/basic/drone/inventory.dm
index 9c620b8a089b8..e63d370549dae 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm
+++ b/code/modules/mob/living/basic/drone/inventory.dm
@@ -1,30 +1,24 @@
+// Drone inventory procs
-///////////////////
-//DRONE INVENTORY//
-///////////////////
-//Drone inventory
-//Drone hands
-
-
-/mob/living/simple_animal/drone/doUnEquip(obj/item/I, force, newloc, no_move, invdrop = TRUE, silent = FALSE)
+/mob/living/basic/drone/doUnEquip(obj/item/item, force, newloc, no_move, invdrop = TRUE, silent = FALSE)
if(..())
update_held_items()
- if(I == head)
+ if(item == head)
head = null
update_worn_head()
- if(I == internal_storage)
+ if(item == internal_storage)
internal_storage = null
update_inv_internal_storage()
return TRUE
return FALSE
-/mob/living/simple_animal/drone/can_equip(obj/item/I, slot, disable_warning = FALSE, bypass_equip_delay_self = FALSE, ignore_equipped = FALSE, indirect_action = FALSE)
+/mob/living/basic/drone/can_equip(obj/item/item, slot, disable_warning = FALSE, bypass_equip_delay_self = FALSE, ignore_equipped = FALSE, indirect_action = FALSE)
switch(slot)
if(ITEM_SLOT_HEAD)
if(head)
return FALSE
- if(!((I.slot_flags & ITEM_SLOT_HEAD) || (I.slot_flags & ITEM_SLOT_MASK)))
+ if(!((item.slot_flags & ITEM_SLOT_HEAD) || (item.slot_flags & ITEM_SLOT_MASK)))
return FALSE
return TRUE
if(ITEM_SLOT_DEX_STORAGE)
@@ -34,7 +28,7 @@
..()
-/mob/living/simple_animal/drone/get_item_by_slot(slot_id)
+/mob/living/basic/drone/get_item_by_slot(slot_id)
switch(slot_id)
if(ITEM_SLOT_HEAD)
return head
@@ -43,14 +37,14 @@
return ..()
-/mob/living/simple_animal/drone/get_slot_by_item(obj/item/looking_for)
+/mob/living/basic/drone/get_slot_by_item(obj/item/looking_for)
if(internal_storage == looking_for)
return ITEM_SLOT_DEX_STORAGE
if(head == looking_for)
return ITEM_SLOT_HEAD
return ..()
-/mob/living/simple_animal/drone/equip_to_slot(obj/item/equipping, slot, initial = FALSE, redraw_mob = FALSE, indirect_action = FALSE)
+/mob/living/basic/drone/equip_to_slot(obj/item/equipping, slot, initial = FALSE, redraw_mob = FALSE, indirect_action = FALSE)
if(!slot)
return
if(!istype(equipping))
@@ -82,8 +76,8 @@
//Call back for item being equipped to drone
equipping.on_equipped(src, slot)
-/mob/living/simple_animal/drone/getBackSlot()
+/mob/living/basic/drone/getBackSlot()
return ITEM_SLOT_DEX_STORAGE
-/mob/living/simple_animal/drone/getBeltSlot()
+/mob/living/basic/drone/getBeltSlot()
return ITEM_SLOT_DEX_STORAGE
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/verbs.dm b/code/modules/mob/living/basic/drone/verbs.dm
similarity index 72%
rename from code/modules/mob/living/simple_animal/friendly/drone/verbs.dm
rename to code/modules/mob/living/basic/drone/verbs.dm
index da54c6a81cd37..833d37a3c8945 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/verbs.dm
+++ b/code/modules/mob/living/basic/drone/verbs.dm
@@ -1,15 +1,9 @@
-
-///////////////
-//DRONE VERBS//
-///////////////
-//Drone verbs that appear in the Drone tab and on buttons
-
/**
* Echoes drone laws to the user
*
- * See [/mob/living/simple_animal/drone/var/laws]
+ * See [/mob/living/basic/drone/var/laws]
*/
-/mob/living/simple_animal/drone/verb/check_laws()
+/mob/living/basic/drone/verb/check_laws()
set category = "Drone"
set name = "Check Laws"
@@ -27,7 +21,7 @@
*
* Attaches area name to message
*/
-/mob/living/simple_animal/drone/verb/drone_ping()
+/mob/living/basic/drone/verb/drone_ping()
set category = "Drone"
set name = "Drone ping"
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/visuals_icons.dm b/code/modules/mob/living/basic/drone/visuals_icons.dm
similarity index 80%
rename from code/modules/mob/living/simple_animal/friendly/drone/visuals_icons.dm
rename to code/modules/mob/living/basic/drone/visuals_icons.dm
index 90391dff58546..ec01d7d2d7892 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/visuals_icons.dm
+++ b/code/modules/mob/living/basic/drone/visuals_icons.dm
@@ -1,23 +1,17 @@
+// Drone overlays and visuals
-/////////////////
-//DRONE VISUALS//
-/////////////////
-//Drone overlays
-//Drone visuals
-
-
-/mob/living/simple_animal/drone/proc/apply_overlay(cache_index)
+/mob/living/basic/drone/proc/apply_overlay(cache_index)
if((. = drone_overlays[cache_index]))
add_overlay(.)
-/mob/living/simple_animal/drone/proc/remove_overlay(cache_index)
- var/I = drone_overlays[cache_index]
- if(I)
- cut_overlay(I)
+/mob/living/basic/drone/proc/remove_overlay(cache_index)
+ var/overlay = drone_overlays[cache_index]
+ if(overlay)
+ cut_overlay(overlay)
drone_overlays[cache_index] = null
-/mob/living/simple_animal/drone/update_clothing(slot_flags)
+/mob/living/basic/drone/update_clothing(slot_flags)
if(slot_flags & ITEM_SLOT_HEAD)
update_worn_head()
if(slot_flags & ITEM_SLOT_MASK)
@@ -27,13 +21,13 @@
if(slot_flags & (ITEM_SLOT_HANDS|ITEM_SLOT_BACKPACK|ITEM_SLOT_DEX_STORAGE))
update_inv_internal_storage()
-/mob/living/simple_animal/drone/proc/update_inv_internal_storage()
+/mob/living/basic/drone/proc/update_inv_internal_storage()
if(internal_storage && client && hud_used?.hud_shown)
internal_storage.screen_loc = ui_drone_storage
client.screen += internal_storage
-/mob/living/simple_animal/drone/update_worn_head()
+/mob/living/basic/drone/update_worn_head()
remove_overlay(DRONE_HEAD_LAYER)
if(head)
@@ -50,10 +44,10 @@
apply_overlay(DRONE_HEAD_LAYER)
-/mob/living/simple_animal/drone/update_worn_mask()
+/mob/living/basic/drone/update_worn_mask()
update_worn_head()
-/mob/living/simple_animal/drone/regenerate_icons()
+/mob/living/basic/drone/regenerate_icons()
// Drones only have 4 slots, which in this specific instance
// is a small blessing.
update_held_items()
@@ -61,13 +55,13 @@
update_inv_internal_storage()
/**
- * Prompt for usr to pick [/mob/living/simple_animal/drone/var/visualAppearance]
+ * Prompt for user to pick [/mob/living/basic/drone/var/visualAppearance]
*
- * Does nothing if there is no usr
+ * Does nothing if there is no user
*
* Called on [/mob/proc/Login]
*/
-/mob/living/simple_animal/drone/proc/pickVisualAppearance()
+/mob/living/basic/drone/proc/pickVisualAppearance()
picked = FALSE
var/list/drone_icons = list(
"Maintenance Drone" = image(icon = 'icons/mob/silicon/drone.dmi', icon_state = "[MAINTDRONE]_grey"),
@@ -111,14 +105,14 @@
/**
* check_menu: Checks if we are allowed to interact with a radial menu
*/
-/mob/living/simple_animal/drone/proc/check_menu()
+/mob/living/basic/drone/proc/check_menu()
if(!istype(src))
return FALSE
if(incapacitated())
return FALSE
return TRUE
-/mob/living/simple_animal/drone/proc/getItemPixelShiftY()
+/mob/living/basic/drone/proc/getItemPixelShiftY()
switch(visualAppearance)
if(MAINTDRONE)
. = 0
diff --git a/code/modules/mob/living/basic/farm_animals/bee/_bee.dm b/code/modules/mob/living/basic/farm_animals/bee/_bee.dm
index 655d08aa86482..692a7f108d189 100644
--- a/code/modules/mob/living/basic/farm_animals/bee/_bee.dm
+++ b/code/modules/mob/living/basic/farm_animals/bee/_bee.dm
@@ -54,6 +54,16 @@
var/icon_base = "bee"
///the bee is a queen?
var/is_queen = FALSE
+ ///commands we follow
+ var/list/pet_commands = list(
+ /datum/pet_command/idle,
+ /datum/pet_command/free,
+ /datum/pet_command/beehive/enter,
+ /datum/pet_command/beehive/exit,
+ /datum/pet_command/follow/bee,
+ /datum/pet_command/point_targetting/attack/swirl,
+ /datum/pet_command/scatter,
+ )
/mob/living/basic/bee/Initialize(mapload)
. = ..()
@@ -62,6 +72,7 @@
AddElement(/datum/element/simple_flying)
AddComponent(/datum/component/clickbox, x_offset = -2, y_offset = -2)
AddComponent(/datum/component/swarming)
+ AddComponent(/datum/component/obeys_commands, pet_commands)
AddElement(/datum/element/swabable, CELL_LINE_TABLE_QUEEN_BEE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack))
diff --git a/code/modules/mob/living/basic/farm_animals/bee/bee_ai_behavior.dm b/code/modules/mob/living/basic/farm_animals/bee/bee_ai_behavior.dm
index 3c8b018cc935e..67e98a5f3d3e8 100644
--- a/code/modules/mob/living/basic/farm_animals/bee/bee_ai_behavior.dm
+++ b/code/modules/mob/living/basic/farm_animals/bee/bee_ai_behavior.dm
@@ -102,9 +102,149 @@
return FALSE
var/atom/bee_hive = bee_ai.blackboard[BB_CURRENT_HOME]
- if(bee_hive && get_dist(target, bee_hive) > AGGRO_DISTANCE_FROM_HIVE)
+ if(bee_hive && get_dist(target, bee_hive) > AGGRO_DISTANCE_FROM_HIVE && can_see(owner, bee_hive, 9))
return FALSE
return !(mob_target.bee_friendly())
+
+///pet commands
+/datum/pet_command/follow/bee
+ ///the behavior we use to follow
+ follow_behavior = /datum/ai_behavior/pet_follow_friend/bee
+
+/datum/ai_behavior/pet_follow_friend/bee
+ required_distance = 0
+
+///swirl around the owner in menacing fashion
+/datum/pet_command/point_targetting/attack/swirl
+ command_name = "Swirl"
+ command_desc = "Your pets will swirl around you and attack whoever you point at!"
+ speech_commands = list("swirl", "spiral", "swarm")
+ pointed_reaction = null
+ refuse_reaction = null
+ command_feedback = null
+ ///the owner we will swarm around
+ var/key_to_swarm = BB_SWARM_TARGET
+
+/datum/pet_command/point_targetting/attack/swirl/try_activate_command(mob/living/commander)
+ var/mob/living/living_pawn = weak_parent.resolve()
+ if(isnull(living_pawn))
+ return
+ var/datum/ai_controller/basic_controller/controller = living_pawn.ai_controller
+ if(isnull(controller))
+ return
+ controller.clear_blackboard_key(BB_CURRENT_PET_TARGET)
+ controller.set_blackboard_key(key_to_swarm, commander)
+ return ..()
+
+/datum/pet_command/point_targetting/attack/swirl/execute_action(datum/ai_controller/controller)
+ if(controller.blackboard_key_exists(BB_CURRENT_PET_TARGET))
+ return ..()
+ controller.queue_behavior(/datum/ai_behavior/swirl_around_target, BB_SWARM_TARGET)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+/datum/ai_behavior/swirl_around_target
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
+ required_distance = 0
+ ///chance to swirl
+ var/swirl_chance = 60
+
+/datum/ai_behavior/swirl_around_target/setup(datum/ai_controller/controller, target_key)
+ . = ..()
+ var/atom/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ set_movement_target(controller, target)
+
+/datum/ai_behavior/swirl_around_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+ var/atom/target = controller.blackboard[target_key]
+ var/mob/living/living_pawn = controller.pawn
+
+ if(QDELETED(target))
+ finish_action(controller, TRUE)
+
+ if(get_dist(target, living_pawn) > 1)
+ set_movement_target(controller, target)
+ return
+
+ if(!SPT_PROB(swirl_chance, seconds_per_tick))
+ return
+
+ var/list/possible_turfs = list()
+
+ for(var/turf/possible_turf in oview(2, target))
+ if(possible_turf.is_blocked_turf(source_atom = living_pawn))
+ continue
+ possible_turfs += possible_turf
+
+ if(!length(possible_turfs))
+ return
+
+ if(isnull(controller.movement_target_source) || controller.movement_target_source == type)
+ set_movement_target(controller, pick(possible_turfs))
+
+
+/datum/pet_command/beehive
+ radial_icon = 'icons/obj/service/hydroponics/equipment.dmi'
+ radial_icon_state = "beebox"
+
+/datum/pet_command/beehive/try_activate_command(mob/living/commander)
+ var/mob/living/living_pawn = weak_parent.resolve()
+ if(isnull(living_pawn))
+ return
+ var/datum/ai_controller/basic_controller/controller = living_pawn.ai_controller
+ if(isnull(controller))
+ return
+ var/obj/hive = controller.blackboard[BB_CURRENT_HOME]
+ if(isnull(hive))
+ return
+ if(!check_beehive_conditions(living_pawn, hive))
+ return
+ return ..()
+
+/datum/pet_command/beehive/proc/check_beehive_conditions(obj/structure/hive)
+ return
+
+/datum/pet_command/beehive/execute_action(datum/ai_controller/controller)
+ controller.queue_behavior(/datum/ai_behavior/enter_exit_hive, BB_CURRENT_HOME)
+ controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+/datum/pet_command/beehive/enter
+ command_name = "Enter beehive"
+ command_desc = "Your bees will enter their beehive."
+ speech_commands = list("enter", "home", "in")
+
+/datum/pet_command/beehive/enter/check_beehive_conditions(mob/living/living_pawn, obj/structure/hive)
+ if(living_pawn in hive) //already in hive
+ return FALSE
+ return can_see(living_pawn, hive, 9)
+
+/datum/pet_command/beehive/exit
+ command_name = "Exit beehive"
+ command_desc = "Your bees will exit their beehive."
+ speech_commands = list("exit", "leave", "out")
+
+/datum/pet_command/beehive/exit/check_beehive_conditions(mob/living/living_pawn, obj/structure/hive)
+ return (living_pawn in hive)
+
+/datum/pet_command/scatter
+ command_name = "Scatter"
+ command_desc = "Command your pets to scatter all around you!"
+ speech_commands = list("disperse", "spread", "scatter")
+
+/datum/pet_command/scatter/set_command_active(mob/living/parent, mob/living/commander)
+ . = ..()
+ set_command_target(parent, commander)
+
+/datum/pet_command/scatter/execute_action(datum/ai_controller/controller)
+ controller.queue_behavior(/datum/ai_behavior/run_away_from_target/scatter, BB_CURRENT_PET_TARGET)
+ controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+/datum/ai_behavior/run_away_from_target/scatter
+ run_distance = 4
+
#undef AGGRO_DISTANCE_FROM_HIVE
diff --git a/code/modules/mob/living/basic/farm_animals/bee/bee_ai_subtree.dm b/code/modules/mob/living/basic/farm_animals/bee/bee_ai_subtree.dm
index e2d6ac04f7e38..6379f239ba0ba 100644
--- a/code/modules/mob/living/basic/farm_animals/bee/bee_ai_subtree.dm
+++ b/code/modules/mob/living/basic/farm_animals/bee/bee_ai_subtree.dm
@@ -1,6 +1,7 @@
/datum/ai_controller/basic_controller/bee
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/bee,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends,
)
ai_traits = STOP_MOVING_WHEN_PULLED
@@ -8,6 +9,7 @@
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
+ /datum/ai_planning_subtree/pet_planning,
/datum/ai_planning_subtree/find_valid_home,
/datum/ai_planning_subtree/enter_exit_home,
/datum/ai_planning_subtree/find_and_hunt_target/pollinate,
diff --git a/code/modules/mob/living/basic/farm_animals/cow/_cow.dm b/code/modules/mob/living/basic/farm_animals/cow/_cow.dm
index 8777cf5a4d1a8..bcd0084adb0ab 100644
--- a/code/modules/mob/living/basic/farm_animals/cow/_cow.dm
+++ b/code/modules/mob/living/basic/farm_animals/cow/_cow.dm
@@ -62,7 +62,7 @@
if(!food_types)
food_types = src.food_types.Copy()
AddComponent(/datum/component/tameable, food_types = food_types, tame_chance = 25, bonus_tame_chance = 15, after_tame = CALLBACK(src, PROC_REF(tamed)))
- AddElement(/datum/element/basic_eating, 10, 0, null, food_types)
+ AddElement(/datum/element/basic_eating, food_types = food_types)
/mob/living/basic/cow/proc/tamed(mob/living/tamer)
buckle_lying = 0
diff --git a/code/modules/mob/living/basic/farm_animals/cow/cow_moonicorn.dm b/code/modules/mob/living/basic/farm_animals/cow/cow_moonicorn.dm
index 7db589334ffc3..6eaef1bf5c3e5 100644
--- a/code/modules/mob/living/basic/farm_animals/cow/cow_moonicorn.dm
+++ b/code/modules/mob/living/basic/farm_animals/cow/cow_moonicorn.dm
@@ -34,7 +34,7 @@
var/static/list/food_types
if(!food_types)
food_types = src.food_types.Copy()
- AddElement(/datum/element/basic_eating, 10, 0, null, food_types)
+ AddElement(/datum/element/basic_eating, food_types = food_types)
AddComponent(/datum/component/tameable, food_types = food_types, tame_chance = 25, bonus_tame_chance = 15, after_tame = CALLBACK(src, PROC_REF(tamed)))
/mob/living/basic/cow/moonicorn/tamed(mob/living/tamer)
diff --git a/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm
index b0926b41811a1..70e436b65d775 100644
--- a/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm
+++ b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm
@@ -23,6 +23,7 @@
response_harm_continuous = "thumps"
response_harm_simple = "thump"
speed = 0.5
+ melee_attack_cooldown = CLICK_CD_MELEE
melee_damage_lower = 15
melee_damage_upper = 18
damage_coeff = list(BRUTE = 1, BURN = 1.5, TOX = 1.5, CLONE = 0, STAMINA = 0, OXY = 1.5)
@@ -53,7 +54,7 @@
/mob/living/basic/gorilla/Initialize(mapload)
. = ..()
add_traits(list(TRAIT_ADVANCEDTOOLUSER, TRAIT_CAN_STRIP), ROUNDSTART_TRAIT)
- AddElement(/datum/element/wall_smasher)
+ AddElement(/datum/element/wall_tearer, allow_reinforced = FALSE)
AddElement(/datum/element/dextrous)
AddElement(/datum/element/footstep, FOOTSTEP_MOB_BAREFOOT)
AddElement(/datum/element/basic_eating, heal_amt = 10, food_types = gorilla_food)
diff --git a/code/modules/mob/living/basic/farm_animals/gorilla/gorilla_ai.dm b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla_ai.dm
index c62307542777d..daf5b7af0834b 100644
--- a/code/modules/mob/living/basic/farm_animals/gorilla/gorilla_ai.dm
+++ b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla_ai.dm
@@ -1,7 +1,8 @@
/// Pretty basic, just click people to death. Also hunt and eat bananas.
/datum/ai_controller/basic_controller/gorilla
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items/gorilla,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items,
+ BB_TARGET_MINIMUM_STAT = UNCONSCIOUS,
BB_EMOTE_KEY = "ooga",
BB_EMOTE_CHANCE = 40,
)
@@ -18,9 +19,6 @@
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
-/datum/targetting_datum/basic/allow_items/gorilla
- stat_attack = UNCONSCIOUS
-
/datum/ai_planning_subtree/attack_obstacle_in_path/gorilla
attack_behaviour = /datum/ai_behavior/attack_obstructions/gorilla
diff --git a/code/modules/mob/living/basic/festivus_pole.dm b/code/modules/mob/living/basic/festivus_pole.dm
index 058b74bfad872..f6dd1a41a2be2 100644
--- a/code/modules/mob/living/basic/festivus_pole.dm
+++ b/code/modules/mob/living/basic/festivus_pole.dm
@@ -40,15 +40,14 @@
///how much charge we give off to cells around us when rubbed
var/recharge_value = 75
+
/mob/living/basic/festivus/Initialize(mapload)
. = ..()
AddComponent(/datum/component/seethrough_mob)
var/static/list/death_loot = list(/obj/item/stack/rods)
AddElement(/datum/element/death_drops, death_loot)
AddComponent(/datum/component/aggro_emote, emote_list = string_list(list("growls")), emote_chance = 20)
- var/datum/action/cooldown/mob_cooldown/charge_apc/charge_ability = new(src)
- charge_ability.Grant(src)
- ai_controller.set_blackboard_key(BB_FESTIVE_APC, charge_ability)
+ grant_actions_by_list(list(/datum/action/cooldown/mob_cooldown/charge_apc = BB_FESTIVE_APC))
/datum/ai_controller/basic_controller/festivus_pole
blackboard = list(
diff --git a/code/modules/mob/living/basic/heretic/heretic_summon.dm b/code/modules/mob/living/basic/heretic/_heretic_summon.dm
similarity index 100%
rename from code/modules/mob/living/basic/heretic/heretic_summon.dm
rename to code/modules/mob/living/basic/heretic/_heretic_summon.dm
diff --git a/code/modules/mob/living/basic/heretic/ash_spirit.dm b/code/modules/mob/living/basic/heretic/ash_spirit.dm
index b2d4d8b4d2947..fed64db8adcb8 100644
--- a/code/modules/mob/living/basic/heretic/ash_spirit.dm
+++ b/code/modules/mob/living/basic/heretic/ash_spirit.dm
@@ -2,7 +2,7 @@
* Player-only mob which is fast, can jaunt a short distance, and is dangerous at close range
*/
/mob/living/basic/heretic_summon/ash_spirit
- name = "Ash Spirit"
+ name = "\improper Ash Spirit"
real_name = "Ashy"
desc = "A manifestation of ash, trailing a perpetual cloud of short-lived cinders."
icon_state = "ash_walker"
@@ -20,6 +20,4 @@
/datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash,
/datum/action/cooldown/spell/pointed/cleave,
)
- for (var/action in actions_to_add)
- var/datum/action/cooldown/new_action = new action(src)
- new_action.Grant(src)
+ grant_actions_by_list(actions_to_add)
diff --git a/code/modules/mob/living/basic/heretic/fire_shark.dm b/code/modules/mob/living/basic/heretic/fire_shark.dm
index ac1f1cc69702f..5a1f173a89b9c 100644
--- a/code/modules/mob/living/basic/heretic/fire_shark.dm
+++ b/code/modules/mob/living/basic/heretic/fire_shark.dm
@@ -1,5 +1,6 @@
/mob/living/basic/heretic_summon/fire_shark
- name = "fire shark"
+ name = "\improper Fire Shark"
+ real_name = "Fire Shark"
desc = "It is a eldritch dwarf space shark, also known as a fire shark."
icon_state = "fire_shark"
icon_living = "fire_shark"
diff --git a/code/modules/mob/living/basic/heretic/flesh_stalker.dm b/code/modules/mob/living/basic/heretic/flesh_stalker.dm
index 6f31b3ce7c523..f6a051334d22a 100644
--- a/code/modules/mob/living/basic/heretic/flesh_stalker.dm
+++ b/code/modules/mob/living/basic/heretic/flesh_stalker.dm
@@ -1,6 +1,6 @@
/// Durable ambush mob with an EMP ability
/mob/living/basic/heretic_summon/stalker
- name = "Flesh Stalker"
+ name = "\improper Flesh Stalker"
real_name = "Flesh Stalker"
desc = "An abomination cobbled together from varied remains. Its appearance changes slightly every time you blink."
icon_state = "stalker"
@@ -11,7 +11,7 @@
melee_damage_upper = 20
sight = SEE_MOBS
ai_controller = /datum/ai_controller/basic_controller/stalker
- /// Associative list of action types we would like to have, and what blackboard key (if any) to put it in
+ /// Actions to grant on spawn
var/static/list/actions_to_add = list(
/datum/action/cooldown/spell/emp/eldritch = BB_GENERIC_ACTION,
/datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash = null,
@@ -21,12 +21,7 @@
/mob/living/basic/heretic_summon/stalker/Initialize(mapload)
. = ..()
AddComponent(/datum/component/ai_target_timer)
- for (var/action_type in actions_to_add)
- var/datum/action/new_action = new action_type(src)
- new_action.Grant(src)
- var/blackboard_key = actions_to_add[action_type]
- if (!isnull(blackboard_key))
- ai_controller?.set_blackboard_key(blackboard_key, new_action)
+ grant_actions_by_list(actions_to_add)
/// Changes shape and lies in wait when it has no target, uses EMP and attacks once it does
/datum/ai_controller/basic_controller/stalker
diff --git a/code/modules/mob/living/basic/heretic/maid_in_the_mirror.dm b/code/modules/mob/living/basic/heretic/maid_in_the_mirror.dm
index 0976576255305..b83c4f253f362 100644
--- a/code/modules/mob/living/basic/heretic/maid_in_the_mirror.dm
+++ b/code/modules/mob/living/basic/heretic/maid_in_the_mirror.dm
@@ -1,6 +1,6 @@
/// Scout and assassin who can appear and disappear from glass surfaces. Damaged by being examined.
/mob/living/basic/heretic_summon/maid_in_the_mirror
- name = "Maid in the Mirror"
+ name = "\improper Maid in the Mirror"
real_name = "Maid in the Mirror"
desc = "A floating and flowing wisp of chilled air. Glancing at it causes it to shimmer slightly."
icon = 'icons/mob/simple/mob.dmi'
@@ -32,8 +32,7 @@
/obj/item/shard,
)
AddElement(/datum/element/death_drops, loot)
- var/datum/action/cooldown/spell/jaunt/mirror_walk/jaunt = new (src)
- jaunt.Grant(src)
+ GRANT_ACTION(/datum/action/cooldown/spell/jaunt/mirror_walk)
/mob/living/basic/heretic_summon/maid_in_the_mirror/death(gibbed)
var/turf/death_turf = get_turf(src)
diff --git a/code/modules/mob/living/basic/heretic/raw_prophet.dm b/code/modules/mob/living/basic/heretic/raw_prophet.dm
index 1a3b2d1aa923a..f3bb1efa7e91f 100644
--- a/code/modules/mob/living/basic/heretic/raw_prophet.dm
+++ b/code/modules/mob/living/basic/heretic/raw_prophet.dm
@@ -4,7 +4,7 @@
* It can blind people to make a getaway, but also get stronger if it attacks the same target consecutively.
*/
/mob/living/basic/heretic_summon/raw_prophet
- name = "Raw Prophet"
+ name = "\improper Raw Prophet"
real_name = "Raw Prophet"
desc = "An abomination stitched together from a few severed arms and one swollen, orphaned eye."
icon_state = "raw_prophet"
@@ -15,8 +15,12 @@
maxHealth = 65
health = 65
sight = SEE_MOBS|SEE_OBJS|SEE_TURFS
- /// Some ability we use to make people go blind
- var/blind_action_type = /datum/action/cooldown/spell/pointed/blind/eldritch
+ /// List of innate abilities we have to add.
+ var/static/list/innate_abilities = list(
+ /datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash/long = null,
+ /datum/action/cooldown/spell/list_target/telepathy/eldritch = null,
+ /datum/action/innate/expand_sight = null,
+ )
/mob/living/basic/heretic_summon/raw_prophet/Initialize(mapload)
. = ..()
@@ -39,19 +43,13 @@
unlink_message = on_unlink_message, \
)
- // We don't use these for AI so we can just repeat the same adding process
- var/static/list/add_abilities = list(
- /datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash/long,
- /datum/action/cooldown/spell/list_target/telepathy/eldritch,
- /datum/action/innate/expand_sight,
- )
- for (var/ability_type in add_abilities)
- var/datum/action/new_action = new ability_type(src)
- new_action.Grant(src)
+ grant_actions_by_list(get_innate_abilities())
- var/datum/action/cooldown/blind = new blind_action_type(src)
- blind.Grant(src)
- ai_controller?.set_blackboard_key(BB_TARGETTED_ACTION, blind)
+/// Returns a list of abilities that we should add.
+/mob/living/basic/heretic_summon/raw_prophet/proc/get_innate_abilities()
+ var/list/returnable_list = innate_abilities.Copy()
+ returnable_list += list(/datum/action/cooldown/spell/pointed/blind/eldritch = BB_TARGETTED_ACTION)
+ return returnable_list
/*
* Callback for the mind_linker component.
@@ -78,7 +76,11 @@
/// NPC variant with a less bullshit ability
/mob/living/basic/heretic_summon/raw_prophet/ruins
ai_controller = /datum/ai_controller/basic_controller/raw_prophet
- blind_action_type = /datum/action/cooldown/mob_cooldown/watcher_gaze
+
+/mob/living/basic/heretic_summon/raw_prophet/ruins/get_innate_abilities()
+ var/list/returnable_list = innate_abilities.Copy()
+ returnable_list += list(/datum/action/cooldown/mob_cooldown/watcher_gaze = BB_TARGETTED_ACTION)
+ return returnable_list
/// Walk and attack people, blind them when we can
/datum/ai_controller/basic_controller/raw_prophet
diff --git a/code/modules/mob/living/basic/heretic/rust_walker.dm b/code/modules/mob/living/basic/heretic/rust_walker.dm
index 070326c0281ae..9dff3c01e311b 100644
--- a/code/modules/mob/living/basic/heretic/rust_walker.dm
+++ b/code/modules/mob/living/basic/heretic/rust_walker.dm
@@ -1,6 +1,6 @@
/// Pretty simple mob which creates areas of rust and has a rust-creating projectile spell
/mob/living/basic/heretic_summon/rust_walker
- name = "Rust Walker"
+ name = "\improper Rust Walker"
real_name = "Rusty"
desc = "A grinding, clanking construct which leaches life from its surroundings with every armoured step."
icon_state = "rust_walker_s"
@@ -17,13 +17,12 @@
/mob/living/basic/heretic_summon/rust_walker/Initialize(mapload)
. = ..()
AddElement(/datum/element/footstep, FOOTSTEP_MOB_RUST)
- var/datum/action/cooldown/spell/aoe/rust_conversion/small/conversion = new(src)
- conversion.Grant(src)
- ai_controller?.set_blackboard_key(BB_GENERIC_ACTION, conversion)
- var/datum/action/cooldown/spell/basic_projectile/rust_wave/short/wave = new(src)
- wave.Grant(src)
- ai_controller?.set_blackboard_key(BB_TARGETTED_ACTION, wave)
+ var/static/list/grantable_spells = list(
+ /datum/action/cooldown/spell/aoe/rust_conversion/small = BB_GENERIC_ACTION,
+ /datum/action/cooldown/spell/basic_projectile/rust_wave/short = BB_TARGETTED_ACTION,
+ )
+ grant_actions_by_list(grantable_spells)
/mob/living/basic/heretic_summon/rust_walker/setDir(newdir)
. = ..()
diff --git a/code/modules/mob/living/basic/heretic/star_gazer.dm b/code/modules/mob/living/basic/heretic/star_gazer.dm
index b739da0831a21..a8af89a7f7177 100644
--- a/code/modules/mob/living/basic/heretic/star_gazer.dm
+++ b/code/modules/mob/living/basic/heretic/star_gazer.dm
@@ -1,5 +1,5 @@
/mob/living/basic/heretic_summon/star_gazer
- name = "Star Gazer"
+ name = "\improper Star Gazer"
desc = "A creature that has been tasked to watch over the stars."
icon = 'icons/mob/nonhuman-player/96x96eldritch_mobs.dmi'
icon_state = "star_gazer"
@@ -78,8 +78,9 @@
/datum/ai_controller/basic_controller/star_gazer
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/star_gazer(),
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends/attack_closed_turfs(),
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends/attack_closed_turfs,
)
ai_movement = /datum/ai_movement/basic_avoidance
@@ -92,9 +93,6 @@
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
-/datum/targetting_datum/basic/star_gazer
- stat_attack = HARD_CRIT
-
/datum/ai_planning_subtree/attack_obstacle_in_path/star_gazer
attack_behaviour = /datum/ai_behavior/attack_obstructions/star_gazer
diff --git a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm
index 960f875365bfa..a83953ae1c94d 100644
--- a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm
+++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm
@@ -28,15 +28,13 @@
/mob/living/basic/mining/ice_demon/Initialize(mapload)
. = ..()
- var/datum/action/cooldown/mob_cooldown/slippery_ice_floors/ice_floor = new(src)
- ice_floor.Grant(src)
- ai_controller.set_blackboard_key(BB_DEMON_SLIP_ABILITY, ice_floor)
- var/datum/action/cooldown/mob_cooldown/ice_demon_teleport/demon_teleport = new(src)
- demon_teleport.Grant(src)
- ai_controller.set_blackboard_key(BB_DEMON_TELEPORT_ABILITY, demon_teleport)
- var/datum/action/cooldown/spell/conjure/create_afterimages/afterimage = new(src)
- afterimage.Grant(src)
- ai_controller.set_blackboard_key(BB_DEMON_CLONE_ABILITY, afterimage)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/slippery_ice_floors = BB_DEMON_SLIP_ABILITY,
+ /datum/action/cooldown/mob_cooldown/ice_demon_teleport = BB_DEMON_TELEPORT_ABILITY,
+ /datum/action/cooldown/spell/conjure/limit_summons/create_afterimages = BB_DEMON_CLONE_ABILITY,
+ )
+ grant_actions_by_list(innate_actions)
+
AddComponent(\
/datum/component/ranged_attacks,\
projectile_type = /obj/projectile/temp/ice_demon,\
diff --git a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm
index 79c9ee9bd583e..b143f471138f4 100644
--- a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm
+++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm
@@ -82,7 +82,7 @@
/obj/effect/temp_visual/slippery_ice/proc/add_slippery_component()
AddComponent(/datum/component/slippery, 2 SECONDS)
-/datum/action/cooldown/spell/conjure/create_afterimages
+/datum/action/cooldown/spell/conjure/limit_summons/create_afterimages
name = "Create After Images"
button_icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
button_icon_state = "ice_demon"
@@ -91,27 +91,8 @@
summon_type = list(/mob/living/basic/mining/demon_afterimage)
summon_radius = 1
summon_amount = 2
- ///max number of after images
- var/max_afterimages = 2
- ///How many clones do we have summoned
- var/number_of_afterimages = 0
+ max_summons = 2
-/datum/action/cooldown/spell/conjure/create_afterimages/can_cast_spell(feedback = TRUE)
+/datum/action/cooldown/spell/conjure/limit_summons/create_afterimages/post_summon(atom/summoned_object, atom/cast_on)
. = ..()
- if(!.)
- return FALSE
- if(number_of_afterimages >= max_afterimages)
- return FALSE
- return TRUE
-
-/datum/action/cooldown/spell/conjure/create_afterimages/post_summon(atom/summoned_object, atom/cast_on)
- var/mob/living/basic/created_copy = summoned_object
- created_copy.AddComponent(/datum/component/joint_damage, overlord_mob = owner)
- RegisterSignals(created_copy, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(delete_copy))
- number_of_afterimages++
-
-/datum/action/cooldown/spell/conjure/create_afterimages/proc/delete_copy(mob/source)
- SIGNAL_HANDLER
-
- UnregisterSignal(source, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH))
- number_of_afterimages--
+ summoned_object.AddComponent(/datum/component/joint_damage, overlord_mob = owner)
diff --git a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm
index 28ddd38324ac6..3f197860618a7 100644
--- a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm
+++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm
@@ -5,7 +5,6 @@
/obj/item/weldingtool,
/obj/item/flashlight/flare,
),
- BB_MINIMUM_DISTANCE_RANGE = 3,
)
ai_movement = /datum/ai_movement/basic_avoidance
diff --git a/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp.dm b/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp.dm
index f89009cc2d9a5..768375cfce8a6 100644
--- a/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp.dm
+++ b/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp.dm
@@ -40,17 +40,20 @@
/mob/living/basic/mining/ice_whelp/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_NO_GLIDE, INNATE_TRAIT)
+
AddElement(/datum/element/footstep, FOOTSTEP_MOB_HEAVY)
AddComponent(/datum/component/basic_mob_ability_telegraph)
AddComponent(/datum/component/basic_mob_attack_telegraph, telegraph_duration = 0.6 SECONDS)
- var/datum/action/cooldown/mob_cooldown/ice_breath/flamethrower = new(src)
- var/datum/action/cooldown/mob_cooldown/ice_breathe_all_directions/wide_flames = new(src)
- flamethrower.Grant(src)
- wide_flames.Grant(src)
- ai_controller.set_blackboard_key(BB_WHELP_WIDESPREAD_FIRE, wide_flames)
- ai_controller.set_blackboard_key(BB_WHELP_STRAIGHTLINE_FIRE, flamethrower)
+
RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack))
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/fire_breath/ice = BB_WHELP_STRAIGHTLINE_FIRE,
+ /datum/action/cooldown/mob_cooldown/fire_breath/ice/cross = BB_WHELP_WIDESPREAD_FIRE,
+ )
+
+ grant_actions_by_list(innate_actions)
+
/mob/living/basic/mining/ice_whelp/proc/pre_attack(mob/living/sculptor, atom/target)
SIGNAL_HANDLER
diff --git a/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_abilities.dm b/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_abilities.dm
index d5dc50d0a692a..026106516fb67 100644
--- a/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_abilities.dm
+++ b/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_abilities.dm
@@ -1,23 +1,26 @@
-/datum/action/cooldown/mob_cooldown/ice_breath
+/// Breathe "fire" in a line (it's freezing cold)
+/datum/action/cooldown/mob_cooldown/fire_breath/ice
name = "Ice Breath"
desc = "Fire a cold line of fire towards the enemy!"
button_icon = 'icons/effects/magic.dmi'
button_icon_state = "fireball"
cooldown_time = 3 SECONDS
melee_cooldown_time = 0 SECONDS
- click_to_activate = TRUE
- ///the range of fire
- var/fire_range = 4
+ fire_range = 4
+ fire_damage = 10
-/datum/action/cooldown/mob_cooldown/ice_breath/Activate(atom/target_atom)
- var/turf/target_fire_turf = get_ranged_target_turf_direct(owner, target_atom, fire_range)
- var/list/burn_turfs = get_line(owner, target_fire_turf) - get_turf(owner)
- // This proc sleeps
- INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(dragon_fire_line), owner, /* burn_turfs = */ burn_turfs, /* frozen = */ TRUE)
- StartCooldown()
- return TRUE
+/datum/action/cooldown/mob_cooldown/fire_breath/ice/burn_turf(turf/fire_turf, list/hit_list, atom/source)
+ var/obj/effect/hotspot/fire_hotspot = ..()
+ fire_hotspot.add_atom_colour(COLOR_BLUE_LIGHT, FIXED_COLOUR_PRIORITY) // You're blue now, that's my attack
+ return fire_hotspot
-/datum/action/cooldown/mob_cooldown/ice_breathe_all_directions
+/datum/action/cooldown/mob_cooldown/fire_breath/ice/on_burn_mob(mob/living/barbecued, mob/living/source)
+ barbecued.apply_status_effect(/datum/status_effect/ice_block_talisman, 2 SECONDS)
+ to_chat(barbecued, span_userdanger("You're frozen solid by [source]'s icy breath!"))
+ barbecued.adjustFireLoss(fire_damage)
+
+/// Breathe really cold fire in a plus shape, like bomberman
+/datum/action/cooldown/mob_cooldown/fire_breath/ice/cross
name = "Fire all directions"
desc = "Unleash lines of cold fire in all directions"
button_icon = 'icons/effects/fire.dmi'
@@ -25,13 +28,10 @@
cooldown_time = 4 SECONDS
melee_cooldown_time = 0 SECONDS
click_to_activate = FALSE
- ///the range of fire
- var/fire_range = 6
+ fire_range = 6
-/datum/action/cooldown/mob_cooldown/ice_breathe_all_directions/Activate(atom/target_atom)
+/datum/action/cooldown/mob_cooldown/fire_breath/ice/cross/attack_sequence(atom/target)
+ playsound(owner.loc, fire_sound, 200, TRUE)
for(var/direction in GLOB.cardinals)
var/turf/target_fire_turf = get_ranged_target_turf(owner, direction, fire_range)
- var/list/burn_turfs = get_line(owner, target_fire_turf) - get_turf(owner)
- INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(dragon_fire_line), owner, burn_turfs, frozen = TRUE)
- StartCooldown()
- return TRUE
+ fire_line(target_fire_turf)
diff --git a/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_ai.dm b/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_ai.dm
index 47280af428169..2d9715caf3752 100644
--- a/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_ai.dm
+++ b/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_ai.dm
@@ -1,7 +1,8 @@
#define ENRAGE_ADDITION 25
/datum/ai_controller/basic_controller/ice_whelp
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items/goliath,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
BB_WHELP_ENRAGED = 0,
)
diff --git a/code/modules/mob/living/basic/jungle/leaper/leaper.dm b/code/modules/mob/living/basic/jungle/leaper/leaper.dm
new file mode 100644
index 0000000000000..6c866a6a955dc
--- /dev/null
+++ b/code/modules/mob/living/basic/jungle/leaper/leaper.dm
@@ -0,0 +1,100 @@
+/mob/living/basic/leaper
+ name = "leaper"
+ desc = "Commonly referred to as 'leapers', the Geron Toad is a massive beast that spits out highly pressurized bubbles containing a unique toxin, knocking down its prey and then crushing it with its girth."
+ icon = 'icons/mob/simple/jungle/leaper.dmi'
+ icon_state = "leaper"
+ icon_living = "leaper"
+ icon_dead = "leaper_dead"
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+
+ melee_damage_lower = 15
+ melee_damage_upper = 20
+ maxHealth = 350
+ health = 350
+ speed = 10
+
+ pixel_x = -16
+ base_pixel_x = -16
+
+ faction = list(FACTION_JUNGLE)
+ obj_damage = 30
+
+ unsuitable_atmos_damage = 0
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = INFINITY
+
+ attack_sound = 'sound/weapons/bladeslice.ogg'
+ attack_vis_effect = ATTACK_EFFECT_SLASH
+ status_flags = NONE
+ lighting_cutoff_red = 5
+ lighting_cutoff_green = 20
+ lighting_cutoff_blue = 25
+ mob_size = MOB_SIZE_LARGE
+ ai_controller = /datum/ai_controller/basic_controller/leaper
+ move_resist = MOVE_FORCE_VERY_STRONG
+ pull_force = MOVE_FORCE_VERY_STRONG
+ ///appearance when we dead
+ var/mutable_appearance/dead_overlay
+ ///appearance when we are alive
+ var/mutable_appearance/living_overlay
+ ///list of pet commands we can issue
+ var/list/pet_commands = list(
+ /datum/pet_command/idle,
+ /datum/pet_command/free,
+ /datum/pet_command/follow,
+ /datum/pet_command/untargetted_ability/blood_rain,
+ /datum/pet_command/untargetted_ability/summon_toad,
+ /datum/pet_command/point_targetting/attack,
+ /datum/pet_command/point_targetting/use_ability/flop,
+ /datum/pet_command/point_targetting/use_ability/bubble,
+ )
+
+/mob/living/basic/leaper/Initialize(mapload)
+ . = ..()
+ AddElement(\
+ /datum/element/change_force_on_death,\
+ move_resist = MOVE_RESIST_DEFAULT,\
+ pull_force = PULL_FORCE_DEFAULT,\
+ )
+ AddComponent(/datum/component/obeys_commands, pet_commands)
+ AddComponent(/datum/component/seethrough_mob)
+ AddElement(/datum/element/wall_smasher)
+ AddElement(/datum/element/ridable, component_type = /datum/component/riding/creature/leaper)
+ AddElement(/datum/element/footstep, footstep_type = FOOTSTEP_MOB_HEAVY)
+ var/datum/action/cooldown/mob_cooldown/blood_rain/volley = new(src)
+ volley.Grant(src)
+ ai_controller.set_blackboard_key(BB_LEAPER_VOLLEY, volley)
+ var/datum/action/cooldown/mob_cooldown/belly_flop/flop = new(src)
+ flop.Grant(src)
+ ai_controller.set_blackboard_key(BB_LEAPER_FLOP, flop)
+ var/datum/action/cooldown/mob_cooldown/projectile_attack/leaper_bubble/bubble = new(src)
+ bubble.Grant(src)
+ ai_controller.set_blackboard_key(BB_LEAPER_BUBBLE, bubble)
+ var/datum/action/cooldown/spell/conjure/limit_summons/create_suicide_toads/toads = new(src)
+ toads.Grant(src)
+ ai_controller.set_blackboard_key(BB_LEAPER_SUMMON, toads)
+
+/mob/living/basic/leaper/proc/set_color_overlay(toad_color)
+ dead_overlay = mutable_appearance(icon, "[icon_state]_dead_overlay")
+ dead_overlay.color = toad_color
+
+ living_overlay = mutable_appearance(icon, "[icon_state]_overlay")
+ living_overlay.color = toad_color
+ update_appearance(UPDATE_OVERLAYS)
+
+/mob/living/basic/leaper/update_overlays()
+ . = ..()
+ if(stat == DEAD && dead_overlay)
+ . += dead_overlay
+ return
+
+ if(living_overlay)
+ . += living_overlay
+
+/mob/living/basic/leaper/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, force, gentle = FALSE, quickstart = TRUE)
+ ADD_TRAIT(src, TRAIT_IMMOBILIZED, LEAPING_TRAIT)
+ return ..()
+
+/mob/living/basic/leaper/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
+ . = ..()
+ REMOVE_TRAIT(src, TRAIT_IMMOBILIZED, LEAPING_TRAIT)
diff --git a/code/modules/mob/living/basic/jungle/leaper/leaper_abilities.dm b/code/modules/mob/living/basic/jungle/leaper/leaper_abilities.dm
new file mode 100644
index 0000000000000..8ff9edd147604
--- /dev/null
+++ b/code/modules/mob/living/basic/jungle/leaper/leaper_abilities.dm
@@ -0,0 +1,256 @@
+// fire leaper bubble ability
+/datum/action/cooldown/mob_cooldown/projectile_attack/leaper_bubble
+ name = "Fire Leaper Bubble"
+ button_icon = 'icons/obj/weapons/guns/projectiles.dmi'
+ button_icon_state = "leaper"
+ desc = "Fires a poisonous leaper bubble towards the victim!"
+ background_icon_state = "bg_revenant"
+ overlay_icon_state = "bg_revenant_border"
+ cooldown_time = 7 SECONDS
+ projectile_type = /obj/projectile/leaper
+ projectile_sound = 'sound/effects/snap.ogg'
+ shared_cooldown = NONE
+ melee_cooldown_time = 0 SECONDS
+
+// bubble ability objects and effects
+/obj/projectile/leaper
+ name = "leaper bubble"
+ icon_state = "leaper"
+ paralyze = 5 SECONDS
+ damage = 0
+ range = 7
+ hitsound = 'sound/effects/snap.ogg'
+ nondirectional_sprite = TRUE
+ impact_effect_type = /obj/effect/temp_visual/leaper_projectile_impact
+
+/obj/projectile/leaper/on_hit(atom/target, blocked = 0, pierce_hit)
+ . = ..()
+ if (!isliving(target))
+ return
+ var/mob/living/bubbled = target
+ if(iscarbon(target))
+ bubbled.reagents.add_reagent(/datum/reagent/toxin/leaper_venom, 5)
+ return
+ bubbled.apply_damage(30)
+
+/obj/projectile/leaper/on_range()
+ new /obj/structure/leaper_bubble(get_turf(src))
+ return ..()
+
+/obj/effect/temp_visual/leaper_projectile_impact
+ name = "leaper bubble"
+ icon = 'icons/obj/weapons/guns/projectiles.dmi'
+ icon_state = "leaper_bubble_pop"
+ layer = ABOVE_ALL_MOB_LAYER
+ plane = GAME_PLANE_UPPER_FOV_HIDDEN
+ duration = 3 SECONDS
+
+/obj/effect/temp_visual/leaper_projectile_impact/Initialize(mapload)
+ . = ..()
+ new /obj/effect/decal/cleanable/leaper_sludge(get_turf(src))
+
+/obj/effect/decal/cleanable/leaper_sludge
+ name = "leaper sludge"
+ desc = "A small pool of sludge, containing trace amounts of leaper venom."
+ icon = 'icons/effects/tomatodecal.dmi'
+ icon_state = "tomato_floor1"
+
+// bubble ability reagent
+/datum/reagent/toxin/leaper_venom
+ name = "Leaper venom"
+ description = "A toxin spat out by leapers that, while harmless in small doses, quickly creates a toxic reaction if too much is in the body."
+ color = "#801E28" // rgb: 128, 30, 40
+ toxpwr = 0
+ taste_description = "french cuisine"
+ taste_mult = 1.3
+
+/datum/reagent/toxin/leaper_venom/on_mob_life(mob/living/carbon/poisoned_mob, seconds_per_tick, times_fired)
+ . = ..()
+ if(volume <= 5)
+ return
+ if(poisoned_mob.adjustToxLoss(2.5 * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
+
+// bubble ability structure
+/obj/structure/leaper_bubble
+ name = "leaper bubble"
+ desc = "A floating bubble containing leaper venom. The contents are under a surprising amount of pressure."
+ icon = 'icons/obj/weapons/guns/projectiles.dmi'
+ icon_state = "leaper"
+ max_integrity = 10
+ density = FALSE
+
+/obj/structure/leaper_bubble/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/movetype_handler)
+ ADD_TRAIT(src, TRAIT_MOVE_FLOATING, LEAPER_BUBBLE_TRAIT)
+ QDEL_IN(src, 10 SECONDS)
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+
+/obj/structure/leaper_bubble/Destroy()
+ new /obj/effect/temp_visual/leaper_projectile_impact(get_turf(src))
+ playsound(src,'sound/effects/snap.ogg', 50, TRUE)
+ return ..()
+
+/obj/structure/leaper_bubble/proc/on_entered(datum/source, atom/movable/bubbled)
+ SIGNAL_HANDLER
+ if(!isliving(bubbled) || istype(bubbled, /mob/living/basic/leaper))
+ return
+ var/mob/living/bubbled_mob = bubbled
+
+ playsound(src, 'sound/effects/snap.ogg',50, TRUE)
+ bubbled_mob.Paralyze(5 SECONDS)
+ if(iscarbon(bubbled_mob))
+ bubbled_mob.reagents.add_reagent(/datum/reagent/toxin/leaper_venom, 5)
+ else
+ bubbled_mob.apply_damage(30)
+ qdel(src)
+
+// blood rain ability
+/datum/action/cooldown/mob_cooldown/blood_rain
+ name = "Blood Rain"
+ button_icon = 'icons/effects/effects.dmi'
+ button_icon_state = "blood_effect_falling"
+ background_icon_state = "bg_revenant"
+ overlay_icon_state = "bg_revenant_border"
+ desc = "Rain down poisonous dropplets of blood!"
+ cooldown_time = 10 SECONDS
+ click_to_activate = FALSE
+ shared_cooldown = NONE
+ melee_cooldown_time = 0 SECONDS
+ /// how many droplets we will fire
+ var/volley_count = 8
+ /// time between each droplet launched
+ var/fire_interval = 0.1 SECONDS
+
+/datum/action/cooldown/mob_cooldown/blood_rain/Activate(mob/living/firer, atom/target)
+ var/list/possible_turfs = list()
+ for(var/turf/possible_turf in oview(5, owner))
+ if(possible_turf.is_blocked_turf() || isopenspaceturf(possible_turf) || isspaceturf(possible_turf))
+ continue
+ if(locate(/obj/structure/leaper_bubble) in possible_turf)
+ continue
+ possible_turfs += possible_turf
+
+ if(!length(possible_turfs))
+ return FALSE
+
+ playsound(owner, 'sound/magic/fireball.ogg', 70, TRUE)
+ new /obj/effect/temp_visual/blood_drop_rising(get_turf(owner))
+ addtimer(CALLBACK(src, PROC_REF(fire_droplets), possible_turfs), 1.5 SECONDS)
+ StartCooldown()
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/blood_rain/proc/fire_droplets(list/possible_turfs)
+ var/fire_count = min(volley_count, possible_turfs.len)
+ for(var/i in 1 to fire_count)
+ addtimer(CALLBACK(src, PROC_REF(fall_effect), pick_n_take(possible_turfs)), i * fire_interval)
+
+/datum/action/cooldown/mob_cooldown/blood_rain/proc/fall_effect(turf/selected_turf)
+ new /obj/effect/temp_visual/blood_drop_falling(selected_turf)
+ var/obj/effect/temp_visual/falling_shadow = new /obj/effect/temp_visual/shadow_telegraph(selected_turf)
+ animate(falling_shadow, transform = matrix().Scale(0.1, 0.1), time = falling_shadow.duration)
+
+// blood rain effects
+/obj/effect/temp_visual/blood_drop_rising
+ name = "leaper bubble"
+ icon = 'icons/obj/weapons/guns/projectiles.dmi'
+ icon_state = "leaper"
+ layer = ABOVE_ALL_MOB_LAYER
+ plane = GAME_PLANE_UPPER_FOV_HIDDEN
+ duration = 1 SECONDS
+
+/obj/effect/temp_visual/blood_drop_rising/Initialize(mapload)
+ . = ..()
+ animate(src, pixel_y = base_pixel_y + 150, time = duration)
+
+/obj/effect/temp_visual/blood_drop_falling
+ name = "leaper bubble"
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "blood_effect_falling"
+ layer = ABOVE_ALL_MOB_LAYER
+ plane = GAME_PLANE_UPPER_FOV_HIDDEN
+ duration = 0.7 SECONDS
+ pixel_y = 60
+
+/obj/effect/temp_visual/blood_drop_falling/Initialize(mapload)
+ . = ..()
+ addtimer(CALLBACK(src, PROC_REF(create_blood_structure)), duration)
+ animate(src, pixel_y = 0, time = duration)
+
+/obj/effect/temp_visual/blood_drop_falling/proc/create_blood_structure()
+ playsound(src, 'sound/effects/snap.ogg', 50, TRUE)
+ new /obj/structure/leaper_bubble(get_turf(src))
+
+/obj/effect/temp_visual/shadow_telegraph
+ name = "shadow"
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "shadow_telegraph"
+ duration = 1.5 SECONDS
+
+
+// flop ability
+/datum/action/cooldown/mob_cooldown/belly_flop
+ name = "Belly Flop"
+ desc = "Belly flop your enemy!"
+ cooldown_time = 14 SECONDS
+ background_icon_state = "bg_revenant"
+ overlay_icon_state = "bg_revenant_border"
+ shared_cooldown = NONE
+ melee_cooldown_time = 0 SECONDS
+
+/datum/action/cooldown/mob_cooldown/belly_flop/Activate(atom/target)
+ var/turf/target_turf = get_turf(target)
+ if(isclosedturf(target_turf) || isspaceturf(target_turf))
+ owner.balloon_alert(owner, "base not suitable!")
+ return FALSE
+ new /obj/effect/temp_visual/leaper_crush(target_turf)
+ owner.throw_at(target = target_turf, range = 7, speed = 1, spin = FALSE, callback = CALLBACK(src, PROC_REF(flop_on_turf), target_turf))
+ StartCooldown()
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/belly_flop/proc/flop_on_turf(turf/target, original_pixel_y)
+ playsound(get_turf(owner), 'sound/effects/meteorimpact.ogg', 200, TRUE)
+ for(var/mob/living/victim in oview(1, owner))
+ if(victim in owner.buckled_mobs)
+ continue
+ victim.apply_damage(35)
+ if(QDELETED(victim)) // Some mobs are deleted on death
+ continue
+ var/throw_dir = victim.loc == owner.loc ? get_dir(owner, victim) : pick(GLOB.alldirs)
+ var/throwtarget = get_edge_target_turf(victim, throw_dir)
+ victim.throw_at(target = throwtarget, range = 3, speed = 1)
+ victim.visible_message(span_warning("[victim] is thrown clear of [owner]!"))
+
+// flop ability effects
+/obj/effect/temp_visual/leaper_crush
+ name = "grim tidings"
+ desc = "Incoming leaper!"
+ icon = 'icons/effects/96x96.dmi'
+ icon_state = "lily_pad"
+ layer = BELOW_MOB_LAYER
+ plane = GAME_PLANE
+ SET_BASE_PIXEL(-32, -32)
+ duration = 3 SECONDS
+
+// summon toads ability
+/datum/action/cooldown/spell/conjure/limit_summons/create_suicide_toads
+ name = "Summon Suicide Toads"
+ button_icon = 'icons/mob/simple/animal.dmi'
+ button_icon_state = "frog_trash"
+ background_icon_state = "bg_revenant"
+ overlay_icon_state = "bg_revenant_border"
+ spell_requirements = NONE
+ cooldown_time = 30 SECONDS
+ summon_type = list(/mob/living/basic/frog/frog_suicide)
+ summon_radius = 2
+ summon_amount = 2
+ max_summons = 2
+
+/datum/action/cooldown/spell/conjure/limit_summons/create_suicide_toads/post_summon(atom/summoned_object, atom/cast_on)
+ . = ..()
+ var/mob/living/summoned_toad = summoned_object
+ summoned_toad.faction = owner.faction ///so they dont attack the leaper or the wizard master
diff --git a/code/modules/mob/living/basic/jungle/leaper/leaper_ai.dm b/code/modules/mob/living/basic/jungle/leaper/leaper_ai.dm
new file mode 100644
index 0000000000000..63e32f069ed40
--- /dev/null
+++ b/code/modules/mob/living/basic/jungle/leaper/leaper_ai.dm
@@ -0,0 +1,73 @@
+/datum/ai_controller/basic_controller/leaper
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk/less_walking
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/pet_planning,
+ /datum/ai_planning_subtree/targeted_mob_ability/pointed_bubble,
+ /datum/ai_planning_subtree/targeted_mob_ability/flop,
+ /datum/ai_planning_subtree/targeted_mob_ability/volley,
+ /datum/ai_planning_subtree/targeted_mob_ability/summon,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/go_for_swim,
+ )
+
+/datum/ai_planning_subtree/targeted_mob_ability/pointed_bubble
+ ability_key = BB_LEAPER_BUBBLE
+ finish_planning = FALSE
+
+/datum/ai_planning_subtree/targeted_mob_ability/flop
+ ability_key = BB_LEAPER_FLOP
+ finish_planning = FALSE
+
+/datum/ai_planning_subtree/targeted_mob_ability/flop/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/atom/current_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ if(isclosedturf(current_target) || isspaceturf(current_target))
+ return
+ return ..()
+
+/datum/ai_planning_subtree/targeted_mob_ability/volley
+ ability_key = BB_LEAPER_VOLLEY
+ finish_planning = FALSE
+
+/datum/ai_planning_subtree/targeted_mob_ability/summon
+ ability_key = BB_LEAPER_SUMMON
+ finish_planning = FALSE
+
+/datum/pet_command/point_targetting/use_ability/flop
+ command_name = "Flop"
+ command_desc = "Command your pet to belly flop your target!"
+ radial_icon = 'icons/mob/actions/actions_items.dmi'
+ radial_icon_state = "sniper_zoom"
+ speech_commands = list("flop", "crush")
+ pet_ability_key = BB_LEAPER_FLOP
+
+/datum/pet_command/point_targetting/use_ability/bubble
+ command_name = "Poison Bubble"
+ command_desc = "Launch poisonous bubbles at your target!"
+ radial_icon = 'icons/obj/weapons/guns/projectiles.dmi'
+ radial_icon_state = "leaper"
+ speech_commands = list("bubble", "shoot")
+ pet_ability_key = BB_LEAPER_BUBBLE
+
+/datum/pet_command/untargetted_ability/blood_rain
+ command_name = "Blood Rain"
+ command_desc = "Let it rain poisonous blood!"
+ radial_icon = 'icons/effects/effects.dmi'
+ radial_icon_state = "blood_effect_falling"
+ speech_commands = list("blood", "rain", "volley")
+ ability_key = BB_LEAPER_VOLLEY
+
+
+/datum/pet_command/untargetted_ability/summon_toad
+ command_name = "Summon Toads"
+ command_desc = "Summon crazy suicide frogs!"
+ radial_icon = 'icons/mob/simple/animal.dmi'
+ radial_icon_state = "frog_trash"
+ speech_commands = list("frogs", "bombers")
+ ability_key = BB_LEAPER_SUMMON
diff --git a/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid.dm b/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid.dm
index bb109fdde61a8..3a6ed50db1086 100644
--- a/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid.dm
+++ b/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid.dm
@@ -36,16 +36,16 @@
/mob/living/basic/mega_arachnid/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/seethrough_mob)
- var/datum/action/cooldown/spell/pointed/projectile/flesh_restraints/restrain = new(src)
- var/datum/action/cooldown/mob_cooldown/secrete_acid/acid_spray = new(src)
- acid_spray.Grant(src)
- restrain.Grant(src)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/secrete_acid = BB_ARACHNID_SLIP,
+ /datum/action/cooldown/spell/pointed/projectile/flesh_restraints = BB_ARACHNID_RESTRAIN,
+ )
+ grant_actions_by_list(innate_actions)
+
AddElement(/datum/element/swabable, CELL_LINE_TABLE_MEGA_ARACHNID, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+ AddComponent(/datum/component/seethrough_mob)
AddComponent(/datum/component/appearance_on_aggro, alpha_on_aggro = 255, alpha_on_deaggro = alpha)
AddComponent(/datum/component/tree_climber, climbing_distance = 15)
- ai_controller.set_blackboard_key(BB_ARACHNID_RESTRAIN, restrain)
- ai_controller.set_blackboard_key(BB_ARACHNID_SLIP, acid_spray)
/mob/living/basic/mega_arachnid/Login()
. = ..()
diff --git a/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_abilities.dm b/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_abilities.dm
index e8c4d1723e790..6e8ff992891bf 100644
--- a/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_abilities.dm
+++ b/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_abilities.dm
@@ -17,7 +17,7 @@
icon_state = "tentacle_end"
damage = 0
-/obj/projectile/mega_arachnid/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/mega_arachnid/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(!iscarbon(target) || blocked >= 100)
return
diff --git a/code/modules/mob/living/basic/jungle/seedling/seedling.dm b/code/modules/mob/living/basic/jungle/seedling/seedling.dm
index 998f693d6da17..46c67ffa0e4d8 100644
--- a/code/modules/mob/living/basic/jungle/seedling/seedling.dm
+++ b/code/modules/mob/living/basic/jungle/seedling/seedling.dm
@@ -56,12 +56,12 @@
/mob/living/basic/seedling/Initialize(mapload)
. = ..()
- var/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling/seed_attack = new(src)
- seed_attack.Grant(src)
- ai_controller.set_blackboard_key(BB_RAPIDSEEDS_ABILITY, seed_attack)
- var/datum/action/cooldown/mob_cooldown/solarbeam/beam_attack = new(src)
- beam_attack.Grant(src)
- ai_controller.set_blackboard_key(BB_SOLARBEAM_ABILITY, beam_attack)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling = BB_RAPIDSEEDS_ABILITY,
+ /datum/action/cooldown/mob_cooldown/solarbeam = BB_SOLARBEAM_ABILITY,
+ )
+
+ grant_actions_by_list(innate_actions)
var/petal_color = pick(possible_colors)
@@ -77,7 +77,7 @@
petal_dead = mutable_appearance(icon, "[icon_state]_dead_overlay")
petal_dead.color = petal_color
- AddElement(/datum/element/wall_smasher)
+ AddElement(/datum/element/wall_tearer, allow_reinforced = FALSE)
AddComponent(/datum/component/obeys_commands, seedling_commands)
RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack))
RegisterSignal(src, COMSIG_KB_MOB_DROPITEM_DOWN, PROC_REF(drop_can))
diff --git a/code/modules/mob/living/basic/jungle/seedling/seedling_ai.dm b/code/modules/mob/living/basic/jungle/seedling/seedling_ai.dm
index 4d67a71d4d42d..e3f9fe083a63a 100644
--- a/code/modules/mob/living/basic/jungle/seedling/seedling_ai.dm
+++ b/code/modules/mob/living/basic/jungle/seedling/seedling_ai.dm
@@ -1,7 +1,7 @@
/datum/ai_controller/basic_controller/seedling
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends,
BB_WEEDLEVEL_THRESHOLD = 3,
BB_WATERLEVEL_THRESHOLD = 90,
)
@@ -142,7 +142,7 @@
/datum/ai_controller/basic_controller/seedling/meanie
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends,
)
planning_subtrees = list(
/datum/ai_planning_subtree/pet_planning,
diff --git a/code/modules/mob/living/basic/jungle/seedling/seedling_projectiles.dm b/code/modules/mob/living/basic/jungle/seedling/seedling_projectiles.dm
index 726b81059335d..37a3ec1c0aca9 100644
--- a/code/modules/mob/living/basic/jungle/seedling/seedling_projectiles.dm
+++ b/code/modules/mob/living/basic/jungle/seedling/seedling_projectiles.dm
@@ -11,7 +11,7 @@
hitsound_wall = 'sound/weapons/effects/searwall.ogg'
nondirectional_sprite = TRUE
-/obj/projectile/seedling/on_hit(atom/target)
+/obj/projectile/seedling/on_hit(atom/target, blocked = 0, pierce_hit)
if(!isliving(target))
return ..()
diff --git a/code/modules/mob/living/basic/jungle/venus_human_trap.dm b/code/modules/mob/living/basic/jungle/venus_human_trap.dm
index a997a8d266b1a..bfe7c1e849735 100644
--- a/code/modules/mob/living/basic/jungle/venus_human_trap.dm
+++ b/code/modules/mob/living/basic/jungle/venus_human_trap.dm
@@ -126,7 +126,7 @@
* Akin to certain spiders, venus human traps can also be possessed and controlled by ghosts.
*
*/
-
+
/mob/living/basic/venus_human_trap
name = "venus human trap"
desc = "Now you know how the fly feels."
@@ -174,9 +174,10 @@
/mob/living/basic/venus_human_trap/Initialize(mapload)
. = ..()
AddElement(/datum/element/lifesteal, 5)
- var/datum/action/cooldown/vine_tangle/tangle = new(src)
- tangle.Grant(src)
- ai_controller.set_blackboard_key(BB_TARGETTED_ACTION, tangle)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/vine_tangle = BB_TARGETTED_ACTION,
+ )
+ grant_actions_by_list(innate_actions)
/mob/living/basic/venus_human_trap/RangedAttack(atom/victim)
if(!combat_mode)
@@ -198,7 +199,7 @@
else if(vines_in_range)
alert_shown = FALSE
- apply_damage(vines_in_range ? weed_heal : no_weed_damage, BRUTE) //every life tick take 20 brute if not near vines or heal 10 if near vines, 5 times out of weeds = u ded
+ adjustBruteLoss(vines_in_range ? -weed_heal : no_weed_damage) //every life tick take 20 damage if not near vines or heal 10 if near vines, 5 times out of weeds = u ded
/datum/action/cooldown/vine_tangle
name = "Tangle"
diff --git a/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm b/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm
index 45bfd74d23b57..d4fef239bf84f 100644
--- a/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm
+++ b/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm
@@ -43,6 +43,9 @@
/mob/living/basic/mining/basilisk/bullet_act(obj/projectile/bullet, def_zone, piercing_hit)
. = ..()
+ if(. != BULLET_ACT_HIT)
+ return
+
if (istype(bullet, /obj/projectile/temp))
var/obj/projectile/temp/heat_bullet = bullet
if (heat_bullet.temperature < 0)
diff --git a/code/modules/mob/living/basic/lavaland/bileworm/_bileworm.dm b/code/modules/mob/living/basic/lavaland/bileworm/_bileworm.dm
index 3d6bc299ccd81..d2e04b60d26a7 100644
--- a/code/modules/mob/living/basic/lavaland/bileworm/_bileworm.dm
+++ b/code/modules/mob/living/basic/lavaland/bileworm/_bileworm.dm
@@ -37,8 +37,6 @@
/mob/living/basic/mining/bileworm/Initialize(mapload)
. = ..()
- //traits and elements
-
ADD_TRAIT(src, TRAIT_IMMOBILIZED, INNATE_TRAIT)
if(ispath(evolve_path))
@@ -47,16 +45,14 @@
//setup mob abilities
- var/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/spew_bile = new attack_action_path(src)
- spew_bile.Grant(src)
//well, one of them has to start on infinite cooldown
+ var/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/spew_bile = new(src)
+ spew_bile.Grant(src)
spew_bile.StartCooldownSelf(INFINITY)
- var/datum/action/cooldown/mob_cooldown/resurface/resurface = new(src)
- resurface.Grant(src)
- var/datum/action/cooldown/mob_cooldown/devour/devour = new(src)
- devour.Grant(src)
- var/datum/action/adjust_vision/bileworm/adjust_vision = new(src)
- adjust_vision.Grant(src)
- ai_controller.set_blackboard_key(BB_BILEWORM_SPEW_BILE, spew_bile)
- ai_controller.set_blackboard_key(BB_BILEWORM_RESURFACE, resurface)
- ai_controller.set_blackboard_key(BB_BILEWORM_DEVOUR, devour)
+ ai_controller?.set_blackboard_key(BB_BILEWORM_SPEW_BILE, spew_bile)
+
+ var/static/list/other_innate_actions = list(
+ /datum/action/adjust_vision/bileworm = null,
+ /datum/action/cooldown/mob_cooldown/devour = BB_BILEWORM_DEVOUR,
+ /datum/action/cooldown/mob_cooldown/resurface = BB_BILEWORM_RESURFACE,
+ )
diff --git a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_actions.dm b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_actions.dm
index b6f7468697a40..9c5e2697f63b2 100644
--- a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_actions.dm
+++ b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_actions.dm
@@ -20,14 +20,14 @@
playsound(burrower, 'sound/effects/break_stone.ogg', 50, TRUE)
new /obj/effect/temp_visual/mook_dust(get_turf(burrower))
burrower.status_flags |= GODMODE
- burrower.invisibility = INVISIBILITY_MAXIMUM
+ burrower.SetInvisibility(INVISIBILITY_MAXIMUM, id=type)
burrower.forceMove(unburrow_turf)
//not that it's gonna die with godmode but still
SLEEP_CHECK_DEATH(rand(0.7 SECONDS, 1.2 SECONDS), burrower)
playsound(burrower, 'sound/effects/break_stone.ogg', 50, TRUE)
new /obj/effect/temp_visual/mook_dust(unburrow_turf)
burrower.status_flags &= ~GODMODE
- burrower.invisibility = 0
+ burrower.RemoveInvisibility(type)
/datum/action/cooldown/mob_cooldown/resurface/proc/get_unburrow_turf(mob/living/burrower, atom/target)
//we want the worm to try guaranteeing a hit on a living target if it thinks it can
@@ -105,14 +105,14 @@
playsound(devourer, 'sound/effects/break_stone.ogg', 50, TRUE)
new /obj/effect/temp_visual/mook_dust(get_turf(devourer))
devourer.status_flags |= GODMODE
- devourer.invisibility = INVISIBILITY_MAXIMUM
+ devourer.SetInvisibility(INVISIBILITY_MAXIMUM, id=type)
devourer.forceMove(devour_turf)
//not that it's gonna die with godmode but still
SLEEP_CHECK_DEATH(rand(0.7 SECONDS, 1.2 SECONDS), devourer)
playsound(devourer, 'sound/effects/break_stone.ogg', 50, TRUE)
new /obj/effect/temp_visual/mook_dust(devour_turf)
devourer.status_flags &= ~GODMODE
- devourer.invisibility = 0
+ devourer.RemoveInvisibility(type)
if(!(target in devour_turf))
to_chat(devourer, span_warning("Someone stole your dinner!"))
return
diff --git a/code/modules/mob/living/basic/lavaland/brimdemon/brimbeam.dm b/code/modules/mob/living/basic/lavaland/brimdemon/brimbeam.dm
index 29a3de60029f6..b8e2bb68c69ea 100644
--- a/code/modules/mob/living/basic/lavaland/brimdemon/brimbeam.dm
+++ b/code/modules/mob/living/basic/lavaland/brimdemon/brimbeam.dm
@@ -9,7 +9,6 @@
click_to_activate = TRUE
cooldown_time = 5 SECONDS
melee_cooldown_time = 0
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
/// How far does our beam go?
var/beam_range = 10
/// How long does our beam last?
diff --git a/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon.dm b/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon.dm
index 2c22977d9c637..dbd017837a913 100644
--- a/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon.dm
+++ b/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon.dm
@@ -43,10 +43,6 @@
beam.Grant(src)
ai_controller.set_blackboard_key(BB_TARGETTED_ACTION, beam)
-/mob/living/basic/mining/brimdemon/Destroy()
- QDEL_NULL(beam)
- return ..()
-
/mob/living/basic/mining/brimdemon/RangedAttack(atom/target, modifiers)
beam.Trigger(target = target)
diff --git a/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon_ai.dm b/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon_ai.dm
index e5d793fa15046..18f614359c066 100644
--- a/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon_ai.dm
@@ -3,7 +3,8 @@
*/
/datum/ai_controller/basic_controller/brimdemon
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/brimdemon,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
ai_traits = PAUSE_DURING_DO_AFTER
@@ -16,9 +17,6 @@
/datum/ai_planning_subtree/targeted_mob_ability/brimbeam,
)
-/datum/targetting_datum/basic/brimdemon
- stat_attack = HARD_CRIT
-
/datum/ai_planning_subtree/move_to_cardinal/brimdemon
move_behaviour = /datum/ai_behavior/move_to_cardinal/brimdemon
diff --git a/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub.dm b/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub.dm
index 58b0e1bbbdb3c..28a76cb2d9733 100644
--- a/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub.dm
+++ b/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub.dm
@@ -47,13 +47,13 @@
else
can_lay_eggs = FALSE
- var/datum/action/cooldown/mob_cooldown/spit_ore/spit = new(src)
- var/datum/action/cooldown/mob_cooldown/burrow/burrow = new(src)
- spit.Grant(src)
- burrow.Grant(src)
- ai_controller.set_blackboard_key(BB_SPIT_ABILITY, spit)
- ai_controller.set_blackboard_key(BB_BURROW_ABILITY, burrow)
- AddElement(/datum/element/wall_smasher)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/spit_ore = BB_SPIT_ABILITY,
+ /datum/action/cooldown/mob_cooldown/burrow = BB_BURROW_ABILITY,
+ )
+ grant_actions_by_list(innate_actions)
+
+ AddElement(/datum/element/wall_tearer, allow_reinforced = FALSE)
AddComponent(/datum/component/ai_listen_to_weather)
AddComponent(\
/datum/component/appearance_on_aggro,\
@@ -65,6 +65,8 @@
if(can_lay_eggs)
make_egg_layer()
+ RegisterSignal(src, COMSIG_ATOM_PRE_BULLET_ACT, PROC_REF(block_bullets))
+
/mob/living/basic/mining/goldgrub/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
. = ..()
if(!.)
@@ -76,12 +78,14 @@
if(istype(attack_target, /obj/item/stack/ore))
consume_ore(attack_target)
-/mob/living/basic/mining/goldgrub/bullet_act(obj/projectile/bullet)
- if(stat == DEAD)
- return BULLET_ACT_FORCE_PIERCE
+/mob/living/basic/mining/goldgrub/proc/block_bullets(datum/source, obj/projectile/hitting_projectile)
+ SIGNAL_HANDLER
- visible_message(span_danger("The [bullet.name] is repelled by [src]'s girth!"))
- return BULLET_ACT_BLOCK
+ if(stat != CONSCIOUS)
+ return COMPONENT_BULLET_PIERCED
+
+ visible_message(span_danger("[hitting_projectile] is repelled by [source]'s girth!"))
+ return COMPONENT_BULLET_BLOCKED
/mob/living/basic/mining/goldgrub/proc/barf_contents(gibbed)
playsound(src, 'sound/effects/splat.ogg', 50, TRUE)
@@ -188,4 +192,3 @@
current_growth = 0,\
location_allowlist = typecacheof(list(/turf)),\
)
-
diff --git a/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub_ai.dm b/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub_ai.dm
index fe1c4150315b0..7e7a72ec41206 100644
--- a/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub_ai.dm
@@ -1,7 +1,7 @@
/datum/ai_controller/basic_controller/goldgrub
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends,
BB_ORE_IGNORE_TYPES = list(/obj/item/stack/ore/iron, /obj/item/stack/ore/glass),
BB_STORM_APPROACHING = FALSE,
)
diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm
index b99e254853ead..20beac22176bb 100644
--- a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm
+++ b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm
@@ -44,6 +44,8 @@
COOLDOWN_DECLARE(ability_animation_cooldown)
/// Our base tentacles ability
var/datum/action/cooldown/mob_cooldown/goliath_tentacles/tentacles
+ /// Our melee tentacles ability
+ var/datum/action/cooldown/mob_cooldown/tentacle_burst/melee_tentacles
/// Our long-ranged tentacles ability
var/datum/action/cooldown/mob_cooldown/tentacle_grasp/tentacle_line
/// Things we want to eat off the floor (or a plate, we're not picky)
@@ -55,6 +57,13 @@
AddElement(/datum/element/ai_retaliate)
AddElement(/datum/element/footstep, FOOTSTEP_MOB_HEAVY)
AddElement(/datum/element/basic_eating, heal_amt = 10, food_types = goliath_foods)
+ AddElement(\
+ /datum/element/change_force_on_death,\
+ move_force = MOVE_FORCE_DEFAULT,\
+ move_resist = MOVE_RESIST_DEFAULT,\
+ pull_force = PULL_FORCE_DEFAULT,\
+ )
+
AddComponent(/datum/component/ai_target_timer)
AddComponent(/datum/component/basic_mob_attack_telegraph)
AddComponentFrom(INNATE_TRAIT, /datum/component/shovel_hands)
@@ -69,7 +78,7 @@
tentacles = new (src)
tentacles.Grant(src)
- var/datum/action/cooldown/mob_cooldown/tentacle_burst/melee_tentacles = new (src)
+ melee_tentacles = new(src)
melee_tentacles.Grant(src)
AddComponent(/datum/component/revenge_ability, melee_tentacles, targetting = ai_controller.blackboard[BB_TARGETTING_DATUM], max_range = 1, target_self = TRUE)
tentacle_line = new (src)
@@ -81,37 +90,15 @@
ai_controller.set_blackboard_key(BB_BASIC_FOODS, goliath_foods)
ai_controller.set_blackboard_key(BB_GOLIATH_TENTACLES, tentacles)
-
-/mob/living/basic/mining/goliath/Destroy()
- QDEL_NULL(tentacles)
- QDEL_NULL(tentacle_line)
- return ..()
-
/mob/living/basic/mining/goliath/examine(mob/user)
. = ..()
if (saddled)
. += span_info("Someone appears to have attached a saddle to this one.")
-/mob/living/basic/mining/goliath/revive(full_heal_flags, excess_healing, force_grab_ghost)
- . = ..()
- if (!.)
- return
- move_force = initial(move_force)
- move_resist = initial(move_resist)
- pull_force = initial(pull_force)
-
-/mob/living/basic/mining/goliath/death(gibbed)
- move_force = MOVE_FORCE_DEFAULT
- move_resist = MOVE_RESIST_DEFAULT
- pull_force = PULL_FORCE_DEFAULT
- return ..()
-
// Goliaths can summon tentacles more frequently as they take damage, scary.
/mob/living/basic/mining/goliath/apply_damage(damage, damagetype, def_zone, blocked, forced, spread_damage, wound_bonus, bare_wound_bonus, sharpness, attack_direction, attacking_item)
. = ..()
- if (!.)
- return
- if (damage <= 0)
+ if (. <= 0)
return
if (tentacles.cooldown_time > 1 SECONDS)
tentacles.cooldown_time -= 1 SECONDS
diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath_actions.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath_actions.dm
index bb8adaf61c3ec..b484afa0cea36 100644
--- a/code/modules/mob/living/basic/lavaland/goliath/goliath_actions.dm
+++ b/code/modules/mob/living/basic/lavaland/goliath/goliath_actions.dm
@@ -9,7 +9,6 @@
click_to_activate = TRUE
cooldown_time = 12 SECONDS
melee_cooldown_time = 0
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
shared_cooldown = NONE
/// Furthest range we can activate ability at
var/max_range = 7
@@ -44,7 +43,6 @@
overlay_icon_state = "bg_demon_border"
cooldown_time = 24 SECONDS
melee_cooldown_time = 0
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
shared_cooldown = NONE
click_to_activate = FALSE
@@ -69,7 +67,6 @@
click_to_activate = TRUE
cooldown_time = 12 SECONDS
melee_cooldown_time = 0
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
shared_cooldown = NONE
/datum/action/cooldown/mob_cooldown/tentacle_grasp/Activate(atom/target)
diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm
index 7e723f5d93afa..48f0e213e73fb 100644
--- a/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm
@@ -3,7 +3,8 @@
/datum/ai_controller/basic_controller/goliath
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items/goliath,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
ai_movement = /datum/ai_movement/basic_avoidance
@@ -19,9 +20,6 @@
/datum/ai_planning_subtree/goliath_dig,
)
-/datum/targetting_datum/basic/allow_items/goliath
- stat_attack = HARD_CRIT
-
/datum/ai_planning_subtree/basic_melee_attack_subtree/goliath
operational_datums = list(/datum/component/ai_target_timer)
melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/goliath
diff --git a/code/modules/mob/living/basic/lavaland/hivelord/spawn_hivelord_brood.dm b/code/modules/mob/living/basic/lavaland/hivelord/spawn_hivelord_brood.dm
index 3fee2a003f309..63a66d4c4e326 100644
--- a/code/modules/mob/living/basic/lavaland/hivelord/spawn_hivelord_brood.dm
+++ b/code/modules/mob/living/basic/lavaland/hivelord/spawn_hivelord_brood.dm
@@ -9,7 +9,6 @@
click_to_activate = TRUE
cooldown_time = 2 SECONDS
melee_cooldown_time = 0
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
shared_cooldown = NONE
/// If a mob is not clicked directly, inherit targetting data from this blackboard key and setting it upon this target key
var/ai_target_key = BB_BASIC_MOB_CURRENT_TARGET
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion.dm b/code/modules/mob/living/basic/lavaland/legion/legion.dm
index 7c6bd0fd170a7..5ed501b452dc6 100644
--- a/code/modules/mob/living/basic/lavaland/legion/legion.dm
+++ b/code/modules/mob/living/basic/lavaland/legion/legion.dm
@@ -41,7 +41,7 @@
var/datum/action/cooldown/mob_cooldown/skull_launcher/skull_launcher = new(src)
skull_launcher.Grant(src)
skull_launcher.spawn_type = brood_type
- ai_controller.blackboard[BB_TARGETTED_ACTION] = skull_launcher
+ ai_controller.set_blackboard_key(BB_TARGETTED_ACTION, skull_launcher)
/// Create what we want to drop on death, in proc form so we can always return a static list
/mob/living/basic/mining/legion/proc/get_loot_list()
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm b/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
index 310bbeb708ebb..9167846b6853e 100644
--- a/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
@@ -1,7 +1,8 @@
/// Keep away and launch skulls at every opportunity, prioritising injured allies
/datum/ai_controller/basic_controller/legion
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead/legion,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/legion,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
BB_AGGRO_RANGE = 5, // Unobservant
BB_BASIC_MOB_FLEE_DISTANCE = 6,
)
@@ -18,7 +19,8 @@
/// Chase and attack whatever we are targetting, if it's friendly we will heal them
/datum/ai_controller/basic_controller/legion_brood
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead/legion,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/legion,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
ai_movement = /datum/ai_movement/basic_avoidance
@@ -29,10 +31,10 @@
)
/// Target nearby friendlies if they are hurt (and are not themselves Legions)
-/datum/targetting_datum/basic/attack_until_dead/legion
+/datum/targetting_datum/basic/legion
-/datum/targetting_datum/basic/attack_until_dead/legion/faction_check(datum/ai_controller/controller, mob/living/living_mob, mob/living/the_target)
- if (!living_mob.faction_check_mob(the_target, exact_match = check_factions_exactly))
+/datum/targetting_datum/basic/legion/faction_check(datum/ai_controller/controller, mob/living/living_mob, mob/living/the_target)
+ if (!living_mob.faction_check_atom(the_target, exact_match = check_factions_exactly))
return FALSE
if (istype(the_target, living_mob.type))
return TRUE
@@ -46,7 +48,7 @@
/datum/ai_planning_subtree/flee_target/legion/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/target = controller.blackboard[target_key]
- if (QDELETED(target) || target.faction_check_mob(controller.pawn))
+ if (QDELETED(target) || target.faction_check_atom(controller.pawn))
return // Only flee if we have a hostile target
return ..()
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm b/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm
index 962d232c5ef84..ff5bfff52573e 100644
--- a/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm
+++ b/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm
@@ -49,7 +49,7 @@
if (ishuman(target) && target.stat > SOFT_CRIT)
infest(target)
return
- if (isliving(target) && faction_check_mob(target) && !istype(target, created_by?.type))
+ if (isliving(target) && faction_check_atom(target) && !istype(target, created_by?.type))
visible_message(span_warning("[src] melds with [target]'s flesh!"))
target.apply_status_effect(/datum/status_effect/regenerative_core)
new /obj/effect/temp_visual/heal(get_turf(target), COLOR_HEALING_CYAN)
@@ -95,5 +95,9 @@
icon_living = "snowlegion_head"
icon_dead = "snowlegion_head"
+/mob/living/basic/legion_brood/snow/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_SNOWSTORM_IMMUNE, INNATE_TRAIT)
+
/mob/living/basic/legion_brood/snow/get_legion_type(mob/living/target)
return /mob/living/basic/mining/legion/snow
diff --git a/code/modules/mob/living/basic/lavaland/legion/spawn_legions.dm b/code/modules/mob/living/basic/lavaland/legion/spawn_legions.dm
index 1ffcafecd56aa..f68c5f7fafe03 100644
--- a/code/modules/mob/living/basic/lavaland/legion/spawn_legions.dm
+++ b/code/modules/mob/living/basic/lavaland/legion/spawn_legions.dm
@@ -9,7 +9,6 @@
click_to_activate = TRUE
cooldown_time = 2 SECONDS
melee_cooldown_time = 0
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
shared_cooldown = NONE
/// If a mob is not clicked directly, inherit targetting data from this blackboard key and setting it upon this target key
var/ai_target_key = BB_BASIC_MOB_CURRENT_TARGET
diff --git a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm
index a048fe77ab146..bfd25a7c09b30 100644
--- a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm
+++ b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm
@@ -33,7 +33,6 @@
/mob/living/basic/mining/lobstrosity/Initialize(mapload)
. = ..()
- ADD_TRAIT(src, TRAIT_SNOWSTORM_IMMUNE, INNATE_TRAIT)
AddElement(/datum/element/mob_grabber)
AddElement(/datum/element/footstep, FOOTSTEP_MOB_CLAW)
AddElement(/datum/element/basic_eating, food_types = target_foods)
diff --git a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm
index 7b6926fca04e7..c89cc3b9c07b7 100644
--- a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm
@@ -1,6 +1,7 @@
/datum/ai_controller/basic_controller/lobstrosity
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/lobster,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
BB_LOBSTROSITY_EXPLOIT_TRAITS = list(TRAIT_INCAPACITATED, TRAIT_FLOORED, TRAIT_IMMOBILIZED, TRAIT_KNOCKEDOUT),
BB_LOBSTROSITY_FINGER_LUST = 0
)
@@ -18,9 +19,6 @@
/datum/ai_planning_subtree/find_fingers,
)
-/datum/targetting_datum/basic/lobster
- stat_attack = HARD_CRIT
-
/datum/ai_planning_subtree/basic_melee_attack_subtree/lobster
melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/lobster
diff --git a/code/modules/mob/living/basic/lavaland/mining.dm b/code/modules/mob/living/basic/lavaland/mining.dm
index 0b6c4f321b66b..3bcdd1ceaa64b 100644
--- a/code/modules/mob/living/basic/lavaland/mining.dm
+++ b/code/modules/mob/living/basic/lavaland/mining.dm
@@ -22,7 +22,7 @@
/mob/living/basic/mining/Initialize(mapload)
. = ..()
- add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE), INNATE_TRAIT)
+ add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE, TRAIT_SNOWSTORM_IMMUNE), INNATE_TRAIT)
AddElement(/datum/element/mob_killed_tally, "mobs_killed_mining")
var/static/list/vulnerable_projectiles
if(!vulnerable_projectiles)
diff --git a/code/modules/mob/living/basic/lavaland/mook/mook.dm b/code/modules/mob/living/basic/lavaland/mook/mook.dm
index da833437715c6..463c9d5de792e 100644
--- a/code/modules/mob/living/basic/lavaland/mook/mook.dm
+++ b/code/modules/mob/living/basic/lavaland/mook/mook.dm
@@ -12,16 +12,14 @@
maxHealth = 150
faction = list(FACTION_MINING, FACTION_NEUTRAL)
health = 150
- move_resist = MOVE_FORCE_OVERPOWERING
+ move_resist = MOVE_FORCE_VERY_STRONG
melee_damage_lower = 8
melee_damage_upper = 8
- pass_flags_self = LETPASSTHROW
attack_sound = 'sound/weapons/rapierhit.ogg'
attack_vis_effect = ATTACK_EFFECT_SLASH
death_sound = 'sound/voice/mook_death.ogg'
ai_controller = /datum/ai_controller/basic_controller/mook/support
speed = 5
-
pixel_x = -16
base_pixel_x = -16
pixel_y = -16
@@ -49,15 +47,17 @@
/mob/living/basic/mining/mook/Initialize(mapload)
. = ..()
+ AddElement(\
+ /datum/element/change_force_on_death,\
+ move_resist = MOVE_RESIST_DEFAULT,\
+ )
AddComponent(/datum/component/ai_retaliate_advanced, CALLBACK(src, PROC_REF(attack_intruder)))
- var/datum/action/cooldown/mob_cooldown/mook_ability/mook_jump/jump = new(src)
- jump.Grant(src)
- ai_controller.set_blackboard_key(BB_MOOK_JUMP_ABILITY, jump)
+ grant_actions_by_list(get_innate_abilities())
ore_overlay = mutable_appearance(icon, "mook_ore_overlay")
AddComponent(/datum/component/ai_listen_to_weather)
- AddElement(/datum/element/wall_smasher)
+ AddElement(/datum/element/wall_tearer, allow_reinforced = FALSE)
RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack))
RegisterSignal(src, COMSIG_KB_MOB_DROPITEM_DOWN, PROC_REF(drop_ore))
@@ -66,6 +66,13 @@
AddComponent(/datum/component/obeys_commands, pet_commands)
+/// Returns a list of actions and blackboard keys to pass into `grant_actions_by_list`.
+/mob/living/basic/mining/mook/proc/get_innate_abilities()
+ var/static/list/innate_abilities = list(
+ /datum/action/cooldown/mob_cooldown/mook_ability/mook_jump = BB_MOOK_JUMP_ABILITY,
+ )
+ return innate_abilities
+
/mob/living/basic/mining/mook/proc/grant_healer_abilities()
AddComponent(\
/datum/component/healing_touch,\
@@ -154,6 +161,9 @@
/mob/living/basic/mining/mook/CanAllowThrough(atom/movable/mover, border_dir)
. = ..()
+ if(.)
+ return TRUE
+
if(!istype(mover, /mob/living/basic/mining/mook))
return FALSE
@@ -193,9 +203,18 @@
neutral_stance = mutable_appearance(icon, "mook_axe_overlay")
attack_stance = mutable_appearance(icon, "axe_strike_overlay")
update_appearance()
- var/datum/action/cooldown/mob_cooldown/mook_ability/mook_leap/leap = new(src)
- leap.Grant(src)
- ai_controller.set_blackboard_key(BB_MOOK_LEAP_ABILITY, leap)
+
+/mob/living/basic/mining/mook/worker/get_innate_abilities()
+ var/static/list/worker_innate_abilites = null
+
+ if(isnull(worker_innate_abilites))
+ worker_innate_abilites = list()
+ worker_innate_abilites += ..()
+ worker_innate_abilites += list(
+ /datum/action/cooldown/mob_cooldown/mook_ability/mook_leap = BB_MOOK_LEAP_ABILITY,
+ )
+
+ return worker_innate_abilites
/mob/living/basic/mining/mook/worker/attack_sequence(atom/target)
. = ..()
diff --git a/code/modules/mob/living/basic/lavaland/mook/mook_abilities.dm b/code/modules/mob/living/basic/lavaland/mook/mook_abilities.dm
index cfc359bd54fcc..6ca9c59c9268f 100644
--- a/code/modules/mob/living/basic/lavaland/mook/mook_abilities.dm
+++ b/code/modules/mob/living/basic/lavaland/mook/mook_abilities.dm
@@ -135,6 +135,8 @@
icon_state = "mook_leap_cloud"
layer = BELOW_MOB_LAYER
plane = GAME_PLANE
+ pixel_x = -16
+ pixel_y = -16
base_pixel_y = -16
base_pixel_x = -16
duration = 1 SECONDS
diff --git a/code/modules/mob/living/basic/lavaland/mook/mook_ai.dm b/code/modules/mob/living/basic/lavaland/mook/mook_ai.dm
index 6a04742d47152..0fc2873531e07 100644
--- a/code/modules/mob/living/basic/lavaland/mook/mook_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/mook/mook_ai.dm
@@ -267,7 +267,7 @@ GLOBAL_LIST_INIT(mook_commands, list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/mook,
BB_MAXIMUM_DISTANCE_TO_VILLAGE = 10,
BB_STORM_APPROACHING = FALSE,
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends,
)
idle_behavior = /datum/idle_behavior/walk_near_target/mook_village
planning_subtrees = list(
diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm
index 4b322c220ed3e..06a221db2dc79 100644
--- a/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm
+++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm
@@ -9,7 +9,6 @@
background_icon_state = "bg_demon"
overlay_icon_state = "bg_demon_border"
cooldown_time = 20 SECONDS
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
click_to_activate = FALSE
shared_cooldown = NONE
melee_cooldown_time = 0 SECONDS
diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_overwatch.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_overwatch.dm
index 0c8194c524a93..541a4ed9e1c7e 100644
--- a/code/modules/mob/living/basic/lavaland/watcher/watcher_overwatch.dm
+++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_overwatch.dm
@@ -10,7 +10,6 @@
background_icon_state = "bg_demon"
overlay_icon_state = "bg_demon_border"
cooldown_time = 20 SECONDS
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
click_to_activate = TRUE
shared_cooldown = NONE
/// Furthest range we can activate ability at
diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_projectiles.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_projectiles.dm
index 2680e9aa914cb..40afd58c1da26 100644
--- a/code/modules/mob/living/basic/lavaland/watcher/watcher_projectiles.dm
+++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_projectiles.dm
@@ -7,7 +7,7 @@
armor_flag = ENERGY
temperature = -50
-/obj/projectile/temp/watcher/on_hit(mob/living/target, blocked = 0)
+/obj/projectile/temp/watcher/on_hit(mob/living/target, blocked = 0, pierce_hit)
. = ..()
if (!isliving(target))
return
diff --git a/code/modules/mob/living/basic/minebots/minebot.dm b/code/modules/mob/living/basic/minebots/minebot.dm
index 061c9a624f719..ad03da1e697de 100644
--- a/code/modules/mob/living/basic/minebots/minebot.dm
+++ b/code/modules/mob/living/basic/minebots/minebot.dm
@@ -58,14 +58,13 @@
after_tame = CALLBACK(src, PROC_REF(activate_bot)),\
)
- var/datum/action/cooldown/mob_cooldown/minedrone/toggle_light/toggle_light_action = new(src)
- var/datum/action/cooldown/mob_cooldown/minedrone/toggle_meson_vision/toggle_meson_vision_action = new(src)
- var/datum/action/cooldown/mob_cooldown/minedrone/dump_ore/dump_ore_action = new(src)
- toggle_light_action.Grant(src)
- toggle_meson_vision_action.Grant(src)
- dump_ore_action.Grant(src)
- ai_controller.set_blackboard_key(BB_MINEBOT_LIGHT_ABILITY, toggle_light_action)
- ai_controller.set_blackboard_key(BB_MINEBOT_DUMP_ABILITY, dump_ore_action)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/minedrone/toggle_light = BB_MINEBOT_LIGHT_ABILITY,
+ /datum/action/cooldown/mob_cooldown/minedrone/toggle_meson_vision = null,
+ /datum/action/cooldown/mob_cooldown/minedrone/dump_ore = BB_MINEBOT_DUMP_ABILITY,
+ )
+
+ grant_actions_by_list(innate_actions)
stored_gun = new(src)
var/obj/item/implant/radio/mining/comms = new(src)
diff --git a/code/modules/mob/living/basic/minebots/minebot_ai.dm b/code/modules/mob/living/basic/minebots/minebot_ai.dm
index 33e9821dbc4bb..f4a2adda9e1fd 100644
--- a/code/modules/mob/living/basic/minebots/minebot_ai.dm
+++ b/code/modules/mob/living/basic/minebots/minebot_ai.dm
@@ -1,7 +1,7 @@
/datum/ai_controller/basic_controller/minebot
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends,
BB_BLACKLIST_MINERAL_TURFS = list(/turf/closed/mineral/gibtonite),
BB_AUTOMATED_MINING = FALSE,
)
diff --git a/code/modules/mob/living/basic/ruin_defender/flesh.dm b/code/modules/mob/living/basic/ruin_defender/flesh.dm
new file mode 100644
index 0000000000000..7d13c7e66217f
--- /dev/null
+++ b/code/modules/mob/living/basic/ruin_defender/flesh.dm
@@ -0,0 +1,168 @@
+/datum/ai_controller/basic_controller/living_limb_flesh
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ )
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree
+ )
+
+/mob/living/basic/living_limb_flesh
+ name = "living flesh"
+ desc = "A vaguely leg or arm shaped flesh abomination. It pulses, like a heart."
+ icon = 'icons/mob/simple/animal.dmi'
+ icon_state = "limb"
+ icon_living = "limb"
+ mob_size = MOB_SIZE_SMALL
+ basic_mob_flags = DEL_ON_DEATH
+ faction = list(FACTION_HOSTILE)
+ melee_damage_lower = 10
+ melee_damage_upper = 10
+ health = 20
+ maxHealth = 20
+ attack_sound = 'sound/weapons/bite.ogg'
+ attack_vis_effect = ATTACK_EFFECT_BITE
+ attack_verb_continuous = "tries desperately to attach to"
+ attack_verb_simple = "try to attach to"
+ mob_biotypes = MOB_ORGANIC | MOB_SPECIAL
+ ai_controller = /datum/ai_controller/basic_controller/living_limb_flesh
+ /// the meat bodypart we are currently inside, used to like drain nutrition and dismember and shit
+ var/obj/item/bodypart/current_bodypart
+
+/mob/living/basic/living_limb_flesh/Initialize(mapload, obj/item/bodypart/limb)
+ . = ..()
+ AddComponent(/datum/component/swarming, max_x = 8, max_y = 8)
+ AddElement(/datum/element/death_drops, string_list(list(/obj/effect/gibspawner/generic)))
+ if(!isnull(limb))
+ register_to_limb(limb)
+
+/mob/living/basic/living_limb_flesh/Destroy(force)
+ . = ..()
+ QDEL_NULL(current_bodypart)
+
+/mob/living/basic/living_limb_flesh/Life(seconds_per_tick = SSMOBS_DT, times_fired)
+ . = ..()
+ if(stat == DEAD)
+ return
+ if(isnull(current_bodypart) || isnull(current_bodypart.owner))
+ return
+ var/mob/living/carbon/human/victim = current_bodypart.owner
+ if(prob(SPT_PROB(3, SSMOBS_DT)))
+ to_chat(victim, span_warning("The thing posing as your limb makes you feel funny...")) //warn em
+ //firstly as a sideeffect we drain nutrition from our host
+ victim.adjust_nutrition(-1.5)
+
+ if(!prob(SPT_PROB(1.5, SSMOBS_DT)))
+ return
+
+ if(istype(current_bodypart, /obj/item/bodypart/arm))
+ var/list/candidates = list()
+ for(var/atom/movable/movable in orange(victim, 1))
+ if(movable.anchored)
+ continue
+ if(movable == victim)
+ continue
+ if(!victim.CanReach(movable))
+ continue
+ candidates += movable
+ var/atom/movable/candidate = pick(candidates)
+ if(isnull(candidate))
+ return
+ victim.start_pulling(candidate, supress_message = TRUE)
+ victim.visible_message(span_warning("[victim][victim.p_s()] [current_bodypart] instinctually starts feeling [candidate]!"))
+ return
+
+ if(HAS_TRAIT(victim, TRAIT_IMMOBILIZED))
+ return
+ step(victim, pick(GLOB.cardinals))
+ to_chat(victim, span_warning("Your [current_bodypart] moves on its own!"))
+
+
+/mob/living/basic/living_limb_flesh/melee_attack(mob/living/carbon/human/target, list/modifiers, ignore_cooldown)
+ . = ..()
+ if (!ishuman(target) || target.stat == DEAD || HAS_TRAIT(target, TRAIT_NODISMEMBER))
+ return
+
+ var/list/zone_candidates = target.get_missing_limbs()
+ for(var/obj/item/bodypart/bodypart in target.bodyparts)
+ if(bodypart.body_zone == BODY_ZONE_HEAD || bodypart.body_zone == BODY_ZONE_CHEST)
+ continue
+ if(HAS_TRAIT(bodypart, TRAIT_IGNORED_BY_LIVING_FLESH))
+ continue
+ if(bodypart.bodypart_flags & BODYPART_UNREMOVABLE)
+ continue
+ if(bodypart.brute_dam < 20)
+ continue
+ zone_candidates += bodypart.body_zone
+
+ if(!length(zone_candidates))
+ return
+
+ var/target_zone = pick(zone_candidates)
+ var/obj/item/bodypart/target_part = target.get_bodypart(target_zone)
+ if(isnull(target_part))
+ target.emote("scream") // dismember already makes them scream so only do this if we aren't doing that
+ else
+ target_part.dismember()
+
+ var/part_type
+ switch(target_zone)
+ if(BODY_ZONE_L_ARM)
+ part_type = /obj/item/bodypart/arm/left/flesh
+ if(BODY_ZONE_R_ARM)
+ part_type = /obj/item/bodypart/arm/right/flesh
+ if(BODY_ZONE_L_LEG)
+ part_type = /obj/item/bodypart/leg/left/flesh
+ if(BODY_ZONE_R_LEG)
+ part_type = /obj/item/bodypart/leg/right/flesh
+
+ target.visible_message(span_danger("[src] [target_part ? "tears off and attaches itself" : "attaches itself"] to where [target][target.p_s()] limb used to be!"))
+ current_bodypart = new part_type(TRUE) //dont_spawn_flesh, we cant use named arguments here
+ current_bodypart.replace_limb(target, TRUE)
+ forceMove(current_bodypart)
+ register_to_limb(current_bodypart)
+
+/mob/living/basic/living_limb_flesh/proc/register_to_limb(obj/item/bodypart/part)
+ ai_controller.set_ai_status(AI_STATUS_OFF)
+ RegisterSignal(part, COMSIG_BODYPART_REMOVED, PROC_REF(on_limb_lost))
+ RegisterSignal(part.owner, COMSIG_LIVING_DEATH, PROC_REF(owner_died))
+ RegisterSignal(part.owner, COMSIG_LIVING_ELECTROCUTE_ACT, PROC_REF(owner_shocked)) //detach if we are shocked, not beneficial for the host but hey its a sideeffect
+
+/mob/living/basic/living_limb_flesh/proc/owner_shocked(datum/source, shock_damage, source, siemens_coeff, flags)
+ SIGNAL_HANDLER
+ if(shock_damage < 10)
+ return
+ var/mob/living/carbon/human/part_owner = current_bodypart.owner
+ if(!detach_self())
+ return
+ var/turf/our_location = get_turf(src)
+ our_location.visible_message(span_warning("[part_owner][part_owner.p_s()] [current_bodypart] begins to convulse wildly!"))
+
+/mob/living/basic/living_limb_flesh/proc/owner_died(datum/source, gibbed)
+ SIGNAL_HANDLER
+ if(gibbed)
+ return
+ addtimer(CALLBACK(src, PROC_REF(detach_self)), 1 SECONDS) //we need new hosts, dead people suck!
+
+/mob/living/basic/living_limb_flesh/proc/detach_self()
+ if(isnull(current_bodypart))
+ return FALSE
+ current_bodypart.dismember()
+ return TRUE//on_limb_lost should be called after that
+
+/mob/living/basic/living_limb_flesh/proc/on_limb_lost(atom/movable/source, mob/living/carbon/old_owner, dismembered)
+ SIGNAL_HANDLER
+ UnregisterSignal(source, COMSIG_BODYPART_REMOVED)
+ UnregisterSignal(old_owner, COMSIG_LIVING_ELECTROCUTE_ACT)
+ UnregisterSignal(old_owner, COMSIG_LIVING_DEATH)
+ addtimer(CALLBACK(src, PROC_REF(wake_up), source), 2 SECONDS)
+
+/mob/living/basic/living_limb_flesh/proc/wake_up(atom/limb)
+ ai_controller.set_ai_status(AI_STATUS_ON)
+ forceMove(limb.drop_location())
+ current_bodypart = null
+ qdel(limb)
+ visible_message(span_warning("[src] begins flailing around!"))
+ Shake(6, 6, 0.5 SECONDS)
diff --git a/code/modules/mob/living/basic/ruin_defender/living_floor.dm b/code/modules/mob/living/basic/ruin_defender/living_floor.dm
new file mode 100644
index 0000000000000..f7e8cefa12e89
--- /dev/null
+++ b/code/modules/mob/living/basic/ruin_defender/living_floor.dm
@@ -0,0 +1,96 @@
+/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic/on_top/SelectBehaviors(datum/ai_controller/controller, delta_time)
+ var/mob/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ if(!target || QDELETED(target))
+ return
+ if(target.loc != controller.pawn.loc)
+ return
+ return ..()
+
+/datum/ai_controller/basic_controller/living_floor
+ max_target_distance = 2
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ )
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic/on_top
+ )
+
+/mob/living/basic/living_floor
+ name = "floor"
+ desc = "The floor you walk on. It looks near-impervious to damage."
+ icon = 'icons/turf/floors.dmi'
+ icon_state = "floor"
+ icon_living = "floor"
+ mob_size = MOB_SIZE_HUGE
+ mob_biotypes = MOB_SPECIAL
+ status_flags = GODMODE //nothing but crowbars may kill us
+ death_message = ""
+ unsuitable_atmos_damage = 0
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = INFINITY
+ basic_mob_flags = DEL_ON_DEATH
+ move_resist = INFINITY
+ density = FALSE
+ combat_mode = TRUE
+ layer = TURF_LAYER
+ plane = FLOOR_PLANE
+ faction = list(FACTION_HOSTILE)
+ melee_damage_lower = 20
+ melee_damage_upper = 40 //pranked.....
+ attack_sound = 'sound/weapons/bite.ogg'
+ attack_vis_effect = ATTACK_EFFECT_BITE
+ attack_verb_continuous = "bites"
+ attack_verb_simple = "bite"
+ ai_controller = /datum/ai_controller/basic_controller/living_floor
+ melee_attack_cooldown = 0.5 SECONDS // get real
+
+ var/icon_aggro = "floor-hostile"
+ var/desc_aggro = "This flooring is alive and filled with teeth, better not step on that. Being covered in plating, it is immune to damage. Seems vulnerable to prying though."
+
+/mob/living/basic/living_floor/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_IMMOBILIZED, INNATE_TRAIT)
+ var/static/list/connections = list(COMSIG_ATOM_ENTERED = PROC_REF(look_aggro), COMSIG_ATOM_EXITED = PROC_REF(look_deaggro))
+ AddComponent(/datum/component/connect_range, tracked = src, connections = connections, range = 1, works_in_containers = FALSE)
+
+/mob/living/basic/living_floor/proc/look_aggro(datum/source, mob/living/victim)
+ SIGNAL_HANDLER
+ if(!istype(victim) || istype(victim, /mob/living/basic/living_floor) || victim.stat == DEAD)
+ return
+ if(victim.loc == loc) //guaranteed bite
+ var/datum/targetting_datum/basic/targetting = ai_controller.blackboard[BB_TARGETTING_DATUM]
+ if(targetting.can_attack(src, victim))
+ melee_attack(victim)
+ icon_state = icon_aggro
+ desc = desc_aggro
+
+/mob/living/basic/living_floor/proc/look_deaggro(datum/source, mob/living/victim)
+ SIGNAL_HANDLER
+ if(!istype(victim) && !istype(victim, /mob/living/basic/living_floor))
+ return
+ icon_state = initial(icon_state)
+ desc = initial(desc_aggro)
+
+/mob/living/basic/living_floor/med_hud_set_health()
+ return
+
+/mob/living/basic/living_floor/med_hud_set_status()
+ return
+
+/mob/living/basic/living_floor/attackby(obj/item/weapon, mob/user, params)
+ if(weapon.tool_behaviour != TOOL_CROWBAR)
+ return ..()
+ balloon_alert(user, "prying...")
+ playsound(src, 'sound/items/crowbar.ogg', 45, TRUE)
+ if(!do_after(user, 5 SECONDS, src))
+ return
+ new /obj/effect/gibspawner/generic(loc)
+ qdel(src)
+
+/mob/living/basic/living_floor/white
+ icon_state = "white"
+ icon_living = "white"
+ icon_aggro = "whitefloor-hostile"
diff --git a/code/modules/mob/living/simple_animal/hostile/skeleton.dm b/code/modules/mob/living/basic/ruin_defender/skeleton.dm
similarity index 60%
rename from code/modules/mob/living/simple_animal/hostile/skeleton.dm
rename to code/modules/mob/living/basic/ruin_defender/skeleton.dm
index aba636c11bfe3..125787319b094 100644
--- a/code/modules/mob/living/simple_animal/hostile/skeleton.dm
+++ b/code/modules/mob/living/basic/ruin_defender/skeleton.dm
@@ -1,57 +1,69 @@
-/mob/living/simple_animal/hostile/skeleton
+/mob/living/basic/skeleton
name = "reanimated skeleton"
desc = "A real bonefied skeleton, doesn't seem like it wants to socialize."
gender = NEUTER
icon = 'icons/mob/simple/simple_human.dmi'
mob_biotypes = MOB_UNDEAD|MOB_HUMANOID
- turns_per_move = 5
speak_emote = list("rattles")
- emote_see = list("rattles")
- combat_mode = TRUE
maxHealth = 40
health = 40
- speed = 1
- harm_intent_damage = 5
+ basic_mob_flags = DEL_ON_DEATH
melee_damage_lower = 15
melee_damage_upper = 15
- minbodytemp = 0
- maxbodytemp = 1500
+ unsuitable_atmos_damage = 0
+ unsuitable_cold_damage = 0
+ unsuitable_heat_damage = 0
attack_verb_continuous = "slashes"
attack_verb_simple = "slash"
- attack_sound = 'sound/hallucinations/growl1.ogg'
+ attack_sound = 'sound/weapons/slash.ogg'
attack_vis_effect = ATTACK_EFFECT_CLAW
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- unsuitable_atmos_damage = 5
- robust_searching = 1
- stat_attack = HARD_CRIT
faction = list(FACTION_SKELETON)
// Going for a sort of pale bluegreen here, shooting for boneish
lighting_cutoff_red = 15
lighting_cutoff_green = 25
lighting_cutoff_blue = 35
- footstep_type = FOOTSTEP_MOB_SHOE
death_message = "collapses into a pile of bones!"
- del_on_death = TRUE
- loot = list(/obj/effect/decal/remains/human)
+ ai_controller = /datum/ai_controller/basic_controller/skeleton
+ /// Loot this mob drops on death.
+ var/list/loot = list(/obj/effect/decal/remains/human)
/// Path of the outfit we give to the mob's visuals.
var/outfit = null
/// Path of the species we give to the mob's visuals.
var/species = /datum/species/skeleton
/// Path of the held item we give to the mob's visuals.
var/held_item
+ /// Types of milk skeletons like to drink
+ var/static/list/good_drinks = list(
+ /obj/item/reagent_containers/condiment/milk,
+ )
+ /// Bad milk that skeletons hate
+ var/static/list/bad_drinks = list(
+ /obj/item/reagent_containers/condiment/soymilk,
+ )
-/mob/living/simple_animal/hostile/skeleton/Initialize(mapload)
+/mob/living/basic/skeleton/Initialize(mapload)
. = ..()
apply_dynamic_human_appearance(src, outfit, species, r_hand = held_item)
+ AddElement(/datum/element/footstep, FOOTSTEP_MOB_SHOE)
+ if(LAZYLEN(loot))
+ loot = string_list(loot)
+ AddElement(/datum/element/death_drops, loot)
+ AddElement(/datum/element/basic_eating, heal_amt = 50, drinking = TRUE, food_types = good_drinks)
+ AddElement(/datum/element/basic_eating, heal_amt = 0, damage_amount = 25, damage_type = BURN, drinking = TRUE, food_types = bad_drinks)
+ ADD_TRAIT(src, TRAIT_SNOWSTORM_IMMUNE, INNATE_TRAIT)
+ ai_controller?.set_blackboard_key(BB_BASIC_FOODS, good_drinks + bad_drinks)
-/mob/living/simple_animal/hostile/skeleton/eskimo
- name = "undead eskimo"
- desc = "The reanimated remains of some poor traveler."
+/mob/living/basic/skeleton/settler
+ name = "undead settler"
+ desc = "The reanimated remains of some poor settler."
maxHealth = 55
health = 55
- weather_immunities = list(TRAIT_SNOWSTORM_IMMUNE)
melee_damage_lower = 17
melee_damage_upper = 20
+ attack_verb_continuous = "jabs"
+ attack_verb_simple = "jab"
+ attack_sound = 'sound/weapons/bladeslice.ogg'
+ attack_vis_effect = ATTACK_EFFECT_SLASH
death_message = "collapses into a pile of bones, its gear falling to the floor!"
loot = list(
/obj/effect/decal/remains/human,
@@ -59,27 +71,28 @@
/obj/item/clothing/shoes/winterboots,
/obj/item/clothing/suit/hooded/wintercoat,
)
- outfit = /datum/outfit/eskimo
+ outfit = /datum/outfit/settler
held_item = /obj/item/spear
-/datum/outfit/eskimo
- name = "Eskimo"
+/datum/outfit/settler
+ name = "Settler"
suit = /obj/item/clothing/suit/hooded/wintercoat
shoes = /obj/item/clothing/shoes/winterboots
-/mob/living/simple_animal/hostile/skeleton/templar
+/mob/living/basic/skeleton/templar
name = "undead templar"
desc = "The reanimated remains of a holy templar knight."
maxHealth = 150
health = 150
- weather_immunities = list(TRAIT_SNOWSTORM_IMMUNE)
speed = 2
- speak_chance = 1
- speak = list("THE GODS WILL IT!","DEUS VULT!","REMOVE KABAB!")
force_threshold = 10 //trying to simulate actually having armor
obj_damage = 50
melee_damage_lower = 25
melee_damage_upper = 30
+ attack_verb_continuous = "slices"
+ attack_verb_simple = "slice"
+ attack_sound = 'sound/weapons/bladeslice.ogg'
+ attack_vis_effect = ATTACK_EFFECT_SLASH
death_message = "collapses into a pile of bones, its gear clanging as it hits the ground!"
loot = list(
/obj/effect/decal/remains/human,
@@ -95,17 +108,16 @@
suit = /obj/item/clothing/suit/chaplainsuit/armor/templar
r_hand = /obj/item/claymore/weak
-/mob/living/simple_animal/hostile/skeleton/ice
+/mob/living/basic/skeleton/ice
name = "ice skeleton"
desc = "A reanimated skeleton protected by a thick sheet of natural ice armor. Looks slow, though."
speed = 5
maxHealth = 75
health = 75
- weather_immunities = list(TRAIT_SNOWSTORM_IMMUNE)
color = rgb(114,228,250)
loot = list(/obj/effect/decal/remains/human{color = rgb(114,228,250)})
-/mob/living/simple_animal/hostile/skeleton/plasmaminer
+/mob/living/basic/skeleton/plasmaminer
name = "shambling miner"
desc = "A plasma-soaked miner, their exposed limbs turned into a grossly incandescent bone seemingly made of plasma."
icon_state = "plasma_miner"
@@ -113,7 +125,6 @@
icon_dead = "plasma_miner"
maxHealth = 150
health = 150
- harm_intent_damage = 10
melee_damage_lower = 15
melee_damage_upper = 20
light_color = LIGHT_COLOR_PURPLE
@@ -123,20 +134,19 @@
outfit = /datum/outfit/plasma_miner
species = /datum/species/plasmaman
-/mob/living/simple_animal/hostile/skeleton/plasmaminer/jackhammer
+/mob/living/basic/skeleton/plasmaminer/jackhammer
desc = "A plasma-soaked miner, their exposed limbs turned into a grossly incandescent bone seemingly made of plasma. They seem to still have their mining tool in their hand, gripping tightly."
icon_state = "plasma_miner_tool"
icon_living = "plasma_miner_tool"
icon_dead = "plasma_miner_tool"
maxHealth = 185
health = 185
- harm_intent_damage = 15
melee_damage_lower = 20
melee_damage_upper = 25
attack_verb_continuous = "blasts"
attack_verb_simple = "blast"
attack_sound = 'sound/weapons/sonic_jackhammer.ogg'
- attack_vis_effect = null // jackhammer moment
+ attack_vis_effect = null
loot = list(/obj/effect/decal/remains/plasma, /obj/item/pickaxe/drill/jackhammer)
held_item = /obj/item/pickaxe/drill/jackhammer
@@ -145,3 +155,24 @@
uniform = /obj/item/clothing/under/rank/cargo/miner/lavaland
suit = /obj/item/clothing/suit/hooded/explorer
mask = /obj/item/clothing/mask/gas/explorer
+
+// Skeleton AI
+
+/// Skeletons mostly just beat people to death, but they'll also find and drink milk.
+/datum/ai_controller/basic_controller/skeleton
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ BB_EMOTE_KEY = "rattles",
+ BB_EMOTE_CHANCE = 20,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/run_emote,
+ /datum/ai_planning_subtree/find_food,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
diff --git a/code/modules/mob/living/basic/ruin_defender/wizard/wizard.dm b/code/modules/mob/living/basic/ruin_defender/wizard/wizard.dm
new file mode 100644
index 0000000000000..7c35184af3717
--- /dev/null
+++ b/code/modules/mob/living/basic/ruin_defender/wizard/wizard.dm
@@ -0,0 +1,90 @@
+/mob/living/basic/wizard
+ name = "Space Wizard"
+ desc = "A wizard is never early. Nor is he late. He arrives exactly at the worst possible moment."
+ icon = 'icons/mob/simple/simple_human.dmi'
+ icon_state = "wizard"
+ icon_living = "wizard"
+ icon_dead = "wizard_dead"
+ mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+ sentience_type = SENTIENCE_HUMANOID
+ speed = 0
+ maxHealth = 100
+ health = 100
+ melee_damage_lower = 5
+ melee_damage_upper = 5
+ attack_verb_continuous = "punches"
+ attack_verb_simple = "punch"
+ attack_sound = 'sound/weapons/punch1.ogg'
+ combat_mode = TRUE
+ habitable_atmos = list("min_oxy" = 5, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+ unsuitable_atmos_damage = 7.5
+ faction = list(ROLE_WIZARD)
+ basic_mob_flags = DEL_ON_DEATH
+ ai_controller = /datum/ai_controller/basic_controller/wizard
+
+ /// A list of possible wizard corpses, and therefore wizard outfits, to select from
+ var/static/list/wizard_outfits = list(
+ /obj/effect/mob_spawn/corpse/human/wizard = 5,
+ /obj/effect/mob_spawn/corpse/human/wizard/red = 3,
+ /obj/effect/mob_spawn/corpse/human/wizard/yellow = 3,
+ /obj/effect/mob_spawn/corpse/human/wizard/black = 3,
+ /obj/effect/mob_spawn/corpse/human/wizard/marisa = 1,
+ //The tape wizard should go here, but its hat doesn't render correctly for some reason.
+ )
+ /// A specified wizard corpse spawner to use. If null, picks from the list above instead.
+ var/selected_outfit
+
+ /// Typepath for the wizard's targeted spell. If null, selects randomly.
+ var/targeted_spell_path
+ /// List of possible targeted spells to pick from
+ var/static/list/targeted_spell_list = list(
+ /datum/action/cooldown/spell/pointed/projectile/fireball/lesser,
+ /datum/action/cooldown/spell/pointed/projectile/lightningbolt,
+ )
+
+ /// Typepath for the wizard's secondary spell. If null, selects randomly.
+ var/secondary_spell_path
+ /// List of possible secondary spells to pick from
+ var/static/list/secondary_spell_list = list(
+ /datum/action/cooldown/spell/aoe/magic_missile,
+ /datum/action/cooldown/spell/charged/beam/tesla,
+ /datum/action/cooldown/spell/aoe/repulse,
+ /datum/action/cooldown/spell/conjure/the_traps,
+ )
+
+/mob/living/basic/wizard/Initialize(mapload)
+ . = ..()
+ if(!selected_outfit)
+ selected_outfit = pick_weight(wizard_outfits)
+ apply_dynamic_human_appearance(src, mob_spawn_path = selected_outfit, r_hand = /obj/item/staff)
+ var/list/remains = string_list(list(
+ selected_outfit,
+ /obj/item/staff
+ ))
+ AddElement(/datum/element/death_drops, remains)
+ AddElement(/datum/element/footstep, footstep_type = FOOTSTEP_MOB_SHOE)
+
+ if(isnull(targeted_spell_path))
+ targeted_spell_path = pick(targeted_spell_list)
+ if(isnull(secondary_spell_path))
+ secondary_spell_path = pick(secondary_spell_list)
+
+ var/datum/action/cooldown/spell/targeted_spell = new targeted_spell_path(src)
+ targeted_spell.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND)
+ targeted_spell.Grant(src)
+ ai_controller.set_blackboard_key(BB_WIZARD_TARGETED_SPELL, targeted_spell)
+
+ var/datum/action/cooldown/spell/secondary_spell = new secondary_spell_path(src)
+ secondary_spell.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND)
+ secondary_spell.Grant(src)
+ ai_controller.set_blackboard_key(BB_WIZARD_SECONDARY_SPELL, secondary_spell)
+
+ var/datum/action/cooldown/spell/teleport/radius_turf/blink/lesser/blink_spell = new(src)
+ blink_spell.Grant(src)
+ ai_controller.set_blackboard_key(BB_WIZARD_BLINK_SPELL, blink_spell)
+
+/// Uses the colors and loadout of the original wizard simplemob
+/mob/living/basic/wizard/classic
+ selected_outfit = /obj/effect/mob_spawn/corpse/human/wizard
+ targeted_spell_path = /datum/action/cooldown/spell/pointed/projectile/fireball/lesser
+ secondary_spell_path = /datum/action/cooldown/spell/aoe/magic_missile
diff --git a/code/modules/mob/living/basic/ruin_defender/wizard/wizard_ai.dm b/code/modules/mob/living/basic/ruin_defender/wizard/wizard_ai.dm
new file mode 100644
index 0000000000000..ad3fd51b0f977
--- /dev/null
+++ b/code/modules/mob/living/basic/ruin_defender/wizard/wizard_ai.dm
@@ -0,0 +1,53 @@
+#define WIZARD_SPELL_COOLDOWN (1 SECONDS)
+
+/**
+ * Wizards run away from their targets while flinging spells at them and blinking constantly.
+ */
+/datum/ai_controller/basic_controller/wizard
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ )
+
+ 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/maintain_distance/cover_minimum_distance,
+ /datum/ai_planning_subtree/targeted_mob_ability/wizard_spell/primary,
+ /datum/ai_planning_subtree/targeted_mob_ability/wizard_spell/secondary,
+ /datum/ai_planning_subtree/targeted_mob_ability/wizard_spell/blink,
+ )
+
+/**
+ * Cast a wizard spell. There is a minimum cooldown between spellcasts to prevent overwhelming spam.
+ *
+ * Though only the primary spell is actually targeted, all spells use targeted behavior so that they
+ * only get used in combat.
+ */
+/datum/ai_planning_subtree/targeted_mob_ability/wizard_spell
+ use_ability_behaviour = /datum/ai_behavior/targeted_mob_ability/wizard_spell
+
+/datum/ai_planning_subtree/targeted_mob_ability/wizard_spell/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if (controller.blackboard[BB_WIZARD_SPELL_COOLDOWN] > world.time)
+ return
+ return ..()
+
+/datum/ai_behavior/targeted_mob_ability/wizard_spell/perform(seconds_per_tick, datum/ai_controller/controller, ability_key, target_key)
+ . = ..()
+ controller.set_blackboard_key(BB_WIZARD_SPELL_COOLDOWN, world.time + WIZARD_SPELL_COOLDOWN)
+
+/datum/ai_planning_subtree/targeted_mob_ability/wizard_spell/primary
+ ability_key = BB_WIZARD_TARGETED_SPELL
+
+/datum/ai_planning_subtree/targeted_mob_ability/wizard_spell/secondary
+ ability_key = BB_WIZARD_SECONDARY_SPELL
+
+/datum/ai_planning_subtree/targeted_mob_ability/wizard_spell/blink
+ ability_key = BB_WIZARD_BLINK_SPELL
+
+/datum/ai_behavior/use_mob_ability/wizard_spell/perform(seconds_per_tick, datum/ai_controller/controller, ability_key)
+ . = ..()
+ controller.set_blackboard_key(BB_WIZARD_SPELL_COOLDOWN, world.time + WIZARD_SPELL_COOLDOWN)
+
+#undef WIZARD_SPELL_COOLDOWN
diff --git a/code/modules/mob/living/basic/ruin_defender/wizard/wizard_spells.dm b/code/modules/mob/living/basic/ruin_defender/wizard/wizard_spells.dm
new file mode 100644
index 0000000000000..c49d87c730a55
--- /dev/null
+++ b/code/modules/mob/living/basic/ruin_defender/wizard/wizard_spells.dm
@@ -0,0 +1,17 @@
+// Lesser versions of wizard spells to be used by AI wizards
+
+/// Lesser fireball, which is slightly less "instant death" than the normal one
+/datum/action/cooldown/spell/pointed/projectile/fireball/lesser
+ name = "Lesser Fireball"
+ projectile_type = /obj/projectile/magic/fireball/lesser
+ cooldown_time = 10 SECONDS
+
+/obj/projectile/magic/fireball/lesser
+ damage = 0
+ exp_light = 1
+
+/// Lesser Blink, shorter range than the normal blink spell
+/datum/action/cooldown/spell/teleport/radius_turf/blink/lesser
+ name = "Lesser Blink"
+ outer_tele_radius = 3
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
diff --git a/code/modules/mob/living/basic/space_fauna/bear/_bear.dm b/code/modules/mob/living/basic/space_fauna/bear/_bear.dm
index 924cf854276d1..8ba6699648e60 100644
--- a/code/modules/mob/living/basic/space_fauna/bear/_bear.dm
+++ b/code/modules/mob/living/basic/space_fauna/bear/_bear.dm
@@ -130,7 +130,7 @@
AddComponent(/datum/component/regenerator,\
regeneration_delay = 1 SECONDS,\
- health_per_second = 5,\
+ brute_per_second = 5,\
outline_colour = COLOR_YELLOW,\
)
@@ -167,4 +167,3 @@
victim.Knockdown(20)
playsound(loc, 'sound/misc/slip.ogg', 15)
victim.visible_message(span_danger("[victim] slips on [src]'s butter!"))
-
diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp.dm b/code/modules/mob/living/basic/space_fauna/carp/carp.dm
index 9a4b149fd1a00..75be113f651a0 100644
--- a/code/modules/mob/living/basic/space_fauna/carp/carp.dm
+++ b/code/modules/mob/living/basic/space_fauna/carp/carp.dm
@@ -105,15 +105,10 @@
ai_controller.set_blackboard_key(BB_CARP_RIFT, teleport)
ai_controller.set_blackboard_key(BB_OBSTACLE_TARGETTING_WHITELIST, allowed_obstacle_targets)
-
-/mob/living/basic/carp/Destroy()
- QDEL_NULL(teleport)
- return ..()
-
/// Tell the elements and the blackboard what food we want to eat
/mob/living/basic/carp/proc/setup_eating()
- AddElement(/datum/element/basic_eating, 10, 0, null, desired_food)
- AddElement(/datum/element/basic_eating, 0, 10, BRUTE, desired_trash) // We are killing our planet
+ AddElement(/datum/element/basic_eating, food_types = desired_food)
+ AddElement(/datum/element/basic_eating, heal_amt = 0, damage_amount = 10, damage_type = BRUTE, food_types = desired_trash) // We are killing our planet
ai_controller.set_blackboard_key(BB_BASIC_FOODS, desired_food + desired_trash)
/// Set a random colour on the carp, override to do something else
@@ -252,6 +247,7 @@
/mob/living/basic/carp/advanced
health = 40
+ maxHealth = 40
obj_damage = 15
#undef RARE_CAYENNE_CHANCE
diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp_controllers.dm b/code/modules/mob/living/basic/space_fauna/carp/carp_controllers.dm
index 9d967c5a8b009..5bf664d168784 100644
--- a/code/modules/mob/living/basic/space_fauna/carp/carp_controllers.dm
+++ b/code/modules/mob/living/basic/space_fauna/carp/carp_controllers.dm
@@ -9,8 +9,9 @@
*/
/datum/ai_controller/basic_controller/carp
blackboard = list(
+ BB_BASIC_MOB_STOP_FLEEING = TRUE,
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items,
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends
)
ai_movement = /datum/ai_movement/basic_avoidance
@@ -35,9 +36,9 @@
*/
/datum/ai_controller/basic_controller/carp/pet
blackboard = list(
- BB_ALWAYS_IGNORE_FACTION = TRUE,
+ BB_BASIC_MOB_STOP_FLEEING = TRUE,
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends
)
ai_traits = STOP_MOVING_WHEN_PULLED
planning_subtrees = list(
@@ -79,8 +80,9 @@
*/
/datum/ai_controller/basic_controller/carp/passive
blackboard = list(
+ BB_BASIC_MOB_STOP_FLEEING = TRUE,
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends,
)
ai_traits = STOP_MOVING_WHEN_PULLED
planning_subtrees = list(
diff --git a/code/modules/mob/living/basic/space_fauna/changeling/flesh_spider.dm b/code/modules/mob/living/basic/space_fauna/changeling/flesh_spider.dm
index c73b008d6b48b..606df8f136f75 100644
--- a/code/modules/mob/living/basic/space_fauna/changeling/flesh_spider.dm
+++ b/code/modules/mob/living/basic/space_fauna/changeling/flesh_spider.dm
@@ -59,17 +59,16 @@
AddComponent(\
/datum/component/regenerator,\
regeneration_delay = 4 SECONDS,\
- health_per_second = maxHealth / 6,\
+ brute_per_second = maxHealth / 6,\
outline_colour = COLOR_PINK,\
)
- var/datum/action/cooldown/mob_cooldown/lay_web/webbing = new(src)
- webbing.webbing_time *= 0.7
- webbing.Grant(src)
- ai_controller?.set_blackboard_key(BB_SPIDER_WEB_ACTION, webbing)
-
- var/datum/action/cooldown/mob_cooldown/lay_web/web_spikes/spikes_web = new(src)
- spikes_web.Grant(src)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/lay_web = BB_SPIDER_WEB_ACTION,
+ /datum/action/cooldown/mob_cooldown/lay_web/sticky_web = null,
+ /datum/action/cooldown/mob_cooldown/lay_web/web_spikes = null,
+ )
+ grant_actions_by_list(innate_actions)
- var/datum/action/cooldown/mob_cooldown/lay_web/sticky_web/web_sticky = new(src)
- web_sticky.Grant(src)
+/datum/action/cooldown/mob_cooldown/lay_web/flesh
+ webbing_time = 3 SECONDS
diff --git a/code/modules/mob/living/basic/space_fauna/demon/demon_subtypes.dm b/code/modules/mob/living/basic/space_fauna/demon/demon_subtypes.dm
index e52eb09ac0b75..d8c419ea2e3bc 100644
--- a/code/modules/mob/living/basic/space_fauna/demon/demon_subtypes.dm
+++ b/code/modules/mob/living/basic/space_fauna/demon/demon_subtypes.dm
@@ -37,8 +37,7 @@
/mob/living/basic/demon/slaughter/Initialize(mapload)
. = ..()
- var/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/crawl = new crawl_type(src)
- crawl.Grant(src)
+ GRANT_ACTION(/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon)
RegisterSignal(src, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_attack))
RegisterSignals(src, list(COMSIG_MOB_ENTER_JAUNT, COMSIG_MOB_AFTER_EXIT_JAUNT), PROC_REF(on_crawl))
@@ -99,16 +98,19 @@
/mob/living/basic/demon/slaughter/proc/on_attack(mob/living/source, atom/attack_target, proximity_flag, list/modifiers)
SIGNAL_HANDLER
+ if(!proximity_flag)
+ return NONE
+
if(LAZYACCESS(modifiers, RIGHT_CLICK))
bodyslam(attack_target)
return COMPONENT_CANCEL_ATTACK_CHAIN
if(!iscarbon(attack_target))
- return
+ return NONE
var/mob/living/carbon/target = attack_target
if(target.stat == DEAD || isnull(target.mind) || (current_hitstreak > wound_bonus_hitstreak_max))
- return
+ return NONE
current_hitstreak++
wound_bonus += wound_bonus_per_hit
diff --git a/code/modules/mob/living/basic/space_fauna/eyeball/_eyeball.dm b/code/modules/mob/living/basic/space_fauna/eyeball/_eyeball.dm
index 47e43704079b4..b72815d8325ab 100644
--- a/code/modules/mob/living/basic/space_fauna/eyeball/_eyeball.dm
+++ b/code/modules/mob/living/basic/space_fauna/eyeball/_eyeball.dm
@@ -52,9 +52,11 @@
/mob/living/basic/eyeball/Initialize(mapload)
. = ..()
- var/datum/action/cooldown/spell/pointed/death_glare/glare = new(src)
- glare.Grant(src)
- ai_controller.set_blackboard_key(BB_GLARE_ABILITY, glare)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/spell/pointed/death_glare = BB_GLARE_ABILITY
+ )
+ grant_actions_by_list(innate_actions)
+
AddElement(/datum/element/simple_flying)
AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/grown/carrot), tame_chance = 100, after_tame = CALLBACK(src, PROC_REF(on_tame)))
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
diff --git a/code/modules/mob/living/basic/space_fauna/faithless.dm b/code/modules/mob/living/basic/space_fauna/faithless.dm
index c1dc297ea46a6..5cfdd4b47b6e2 100644
--- a/code/modules/mob/living/basic/space_fauna/faithless.dm
+++ b/code/modules/mob/living/basic/space_fauna/faithless.dm
@@ -38,9 +38,9 @@
/mob/living/basic/faithless/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
+ AddElement(/datum/element/door_pryer)
AddElement(/datum/element/footstep, FOOTSTEP_MOB_SHOE)
AddElement(/datum/element/mob_grabber, steal_from_others = FALSE)
- AddComponent(/datum/component/pry_open_door)
/mob/living/basic/faithless/melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
@@ -55,7 +55,8 @@
/datum/ai_controller/basic_controller/faithless
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/faithless(),
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = UNCONSCIOUS,
BB_LOW_PRIORITY_HUNTING_TARGET = null, // lights
)
@@ -69,6 +70,3 @@
/datum/ai_planning_subtree/find_and_hunt_target/look_for_light_fixtures,
/datum/ai_planning_subtree/random_speech/faithless,
)
-
-/datum/targetting_datum/basic/faithless
- stat_attack = UNCONSCIOUS
diff --git a/code/modules/mob/living/basic/space_fauna/ghost.dm b/code/modules/mob/living/basic/space_fauna/ghost.dm
index 35afaade990c8..ad79fefc3e6bc 100644
--- a/code/modules/mob/living/basic/space_fauna/ghost.dm
+++ b/code/modules/mob/living/basic/space_fauna/ghost.dm
@@ -71,10 +71,11 @@
ghost_facial_hair_color = ghost_hair_color
if(!isnull(ghost_hairstyle) && ghost_hairstyle != "Bald") //Bald hairstyle and the Shaved facial hairstyle lack an associated sprite and will not properly generate hair, and just cause runtimes.
- var/datum/sprite_accessory/hair_style = GLOB.hairstyles_list[ghost_hairstyle] //We use the hairstyle name to get the sprite accessory, which we copy the icon_state from.
+ var/datum/sprite_accessory/hair/hair_style = GLOB.hairstyles_list[ghost_hairstyle] //We use the hairstyle name to get the sprite accessory, which we copy the icon_state from.
ghost_hair = mutable_appearance('icons/mob/human/human_face.dmi', "[hair_style.icon_state]", -HAIR_LAYER)
ghost_hair.alpha = 200
ghost_hair.color = ghost_hair_color
+ ghost_hair.pixel_y = hair_style.y_offset
add_overlay(ghost_hair)
if(!isnull(ghost_facial_hairstyle) && ghost_facial_hairstyle != "Shaved")
diff --git a/code/modules/mob/living/basic/space_fauna/hivebot/_hivebot.dm b/code/modules/mob/living/basic/space_fauna/hivebot/_hivebot.dm
index db0d310a71c77..d914e0589c48f 100644
--- a/code/modules/mob/living/basic/space_fauna/hivebot/_hivebot.dm
+++ b/code/modules/mob/living/basic/space_fauna/hivebot/_hivebot.dm
@@ -95,8 +95,7 @@
/mob/living/basic/hivebot/mechanic/Initialize(mapload)
. = ..()
- var/datum/action/cooldown/spell/conjure/foam_wall/foam = new(src)
- foam.Grant(src)
+ GRANT_ACTION(/datum/action/cooldown/spell/conjure/foam_wall)
RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack))
/mob/living/basic/hivebot/mechanic/proc/pre_attack(mob/living/fixer, atom/target)
diff --git a/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_heart.dm b/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_heart.dm
index 5829b52e43732..72f2001011cc0 100644
--- a/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_heart.dm
+++ b/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_heart.dm
@@ -2,6 +2,8 @@
#define HEARTBEAT_FAST (0.6 SECONDS)
#define HEARTBEAT_FRANTIC (0.4 SECONDS)
+#define SPIKES_ABILITY_TYPEPATH /datum/action/cooldown/mob_cooldown/chasing_spikes
+
/mob/living/basic/meteor_heart
name = "meteor heart"
desc = "A pulsing lump of flesh and bone growing directly out of the ground."
@@ -25,10 +27,7 @@
maximum_survivable_temperature = 1500
combat_mode = TRUE
move_resist = INFINITY // This mob IS the floor
- /// Action which sends a line of spikes chasing a player
- var/datum/action/cooldown/mob_cooldown/chasing_spikes/spikes
- /// Action which summons areas the player can't stand in
- var/datum/action/cooldown/mob_cooldown/spine_traps/traps
+
/// Looping heartbeat sound
var/datum/looping_sound/heartbeat/soundloop
@@ -39,14 +38,11 @@
AddElement(/datum/element/death_drops, death_loot)
AddElement(/datum/element/relay_attackers)
- spikes = new(src)
- spikes.Grant(src)
- ai_controller.set_blackboard_key(BB_METEOR_HEART_GROUND_SPIKES, spikes)
-
- traps = new(src)
- traps.Grant(src)
- ai_controller.set_blackboard_key(BB_METEOR_HEART_SPINE_TRAPS, traps)
-
+ var/static/list/innate_actions = list(
+ SPIKES_ABILITY_TYPEPATH = BB_METEOR_HEART_GROUND_SPIKES,
+ /datum/action/cooldown/mob_cooldown/spine_traps = BB_METEOR_HEART_SPINE_TRAPS,
+ )
+ grant_actions_by_list(innate_actions)
ai_controller.set_ai_status(AI_STATUS_OFF)
RegisterSignal(src, COMSIG_MOB_ABILITY_FINISHED, PROC_REF(used_ability))
@@ -60,6 +56,13 @@
soundloop.pressure_affected = FALSE
soundloop.start()
+ AddComponent(\
+ /datum/component/bloody_spreader,\
+ blood_left = INFINITY,\
+ blood_dna = list("meaty DNA" = "MT-"),\
+ diseases = null,\
+ )
+
/// Called when we get mad at something, either for attacking us or attacking the nearby area
/mob/living/basic/meteor_heart/proc/aggro()
if (ai_controller.ai_status == AI_STATUS_ON)
@@ -79,13 +82,11 @@
/// Animate when using certain abilities
/mob/living/basic/meteor_heart/proc/used_ability(mob/living/owner, datum/action/cooldown/mob_cooldown/ability)
SIGNAL_HANDLER
- if (ability != spikes)
+ if(!istype(ability, SPIKES_ABILITY_TYPEPATH))
return
Shake(1, 0, 1.5 SECONDS)
/mob/living/basic/meteor_heart/Destroy()
- QDEL_NULL(spikes)
- QDEL_NULL(traps)
QDEL_NULL(soundloop)
return ..()
@@ -130,3 +131,5 @@
#undef HEARTBEAT_NORMAL
#undef HEARTBEAT_FAST
#undef HEARTBEAT_FRANTIC
+
+#undef SPIKES_ABILITY_TYPEPATH
diff --git a/code/modules/mob/living/basic/space_fauna/mushroom.dm b/code/modules/mob/living/basic/space_fauna/mushroom.dm
index 96280db29235b..056a4558081ed 100644
--- a/code/modules/mob/living/basic/space_fauna/mushroom.dm
+++ b/code/modules/mob/living/basic/space_fauna/mushroom.dm
@@ -58,6 +58,7 @@
/datum/ai_controller/basic_controller/mushroom
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/mushroom,
+ BB_TARGET_MINIMUM_STAT = DEAD,
)
ai_movement = /datum/ai_movement/basic_avoidance
@@ -70,11 +71,10 @@
/datum/targetting_datum/basic/mushroom
- stat_attack = DEAD
///we only attacked another mushrooms
/datum/targetting_datum/basic/mushroom/faction_check(datum/ai_controller/controller, mob/living/living_mob, mob/living/the_target)
- return !living_mob.faction_check_mob(the_target, exact_match = check_factions_exactly)
+ return !living_mob.faction_check_atom(the_target, exact_match = check_factions_exactly)
/datum/ai_planning_subtree/find_and_hunt_target/mushroom_food
target_key = BB_LOW_PRIORITY_HUNTING_TARGET
diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm b/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm
index cdde6ad05e4c8..c55376c4fcc8f 100644
--- a/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm
+++ b/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm
@@ -39,8 +39,7 @@
min_health_slowdown = -1.5,\
)
- var/datum/action/cooldown/spell/jaunt/creature_teleport/teleport = new(src)
- teleport.Grant(src)
+ GRANT_ACTION(/datum/action/cooldown/spell/jaunt/creature_teleport)
/mob/living/basic/creature/proc/can_be_seen(turf/location)
// Check for darkness
diff --git a/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm b/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm
index 519e8ba1a7390..c4ea41cad37a7 100644
--- a/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm
+++ b/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm
@@ -36,21 +36,16 @@
AddElement(/datum/element/effect_trail, /obj/effect/temp_visual/paper_scatter)
/mob/living/basic/paper_wizard/proc/grant_abilities()
- summon = new(src)
- summon.Grant(src)
- ai_controller.set_blackboard_key(BB_WIZARD_SUMMON_MINIONS, summon)
- mimic = new(src)
- mimic.Grant(src)
- ai_controller.set_blackboard_key(BB_WIZARD_MIMICS, mimic)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/spell/conjure/wizard_summon_minions = BB_WIZARD_SUMMON_MINIONS,
+ /datum/action/cooldown/spell/pointed/wizard_mimic = BB_WIZARD_MIMICS,
+ )
+
+ grant_actions_by_list(innate_actions)
/mob/living/basic/paper_wizard/proc/grant_loot()
AddElement(/datum/element/death_drops, dropped_loot)
-/mob/living/basic/paper_wizard/Destroy()
- QDEL_NULL(summon)
- QDEL_NULL(mimic)
- return ..()
-
/datum/ai_controller/basic_controller/paper_wizard
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
diff --git a/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat.dm b/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat.dm
index 164c25fb896d2..43108c67ef377 100644
--- a/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat.dm
+++ b/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat.dm
@@ -54,6 +54,7 @@
AddElement(/datum/element/waddling)
AddElement(/datum/element/ai_retaliate)
+ AddElement(/datum/element/door_pryer, pry_time = 5 SECONDS, interaction_key = REGALRAT_INTERACTION)
AddComponent(\
/datum/component/ghost_direct_control,\
poll_candidates = poll_ghosts,\
@@ -63,13 +64,12 @@
after_assumed_control = CALLBACK(src, PROC_REF(became_player_controlled)),\
)
- var/datum/action/cooldown/mob_cooldown/domain/domain = new(src)
- domain.Grant(src)
- ai_controller.set_blackboard_key(BB_DOMAIN_ABILITY, domain)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/domain = BB_DOMAIN_ABILITY,
+ /datum/action/cooldown/mob_cooldown/riot = BB_RAISE_HORDE_ABILITY,
+ )
- var/datum/action/cooldown/mob_cooldown/riot/riot = new(src)
- riot.Grant(src)
- ai_controller.set_blackboard_key(BB_RAISE_HORDE_ABILITY, riot)
+ grant_actions_by_list(innate_actions)
/mob/living/basic/regal_rat/examine(mob/user)
. = ..()
@@ -81,7 +81,7 @@
return
if(ismouse(user))
- if(user.faction_check_mob(src, exact_match = TRUE))
+ if(user.faction_check_atom(src, exact_match = TRUE))
. += span_notice("This is your king. Long live [p_their()] majesty!")
else
. += span_warning("This is a false king! Strike [p_them()] down!")
@@ -103,7 +103,7 @@
"All rise for [name], ascendant to the throne in \the [get_area(src)].",
source = src,
action = NOTIFY_ORBIT,
- flashwindow = FALSE,
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
header = "Sentient Rat Created",
)
@@ -174,10 +174,6 @@
if(isnull(mind))
return
- if(istype(target, /obj/machinery/door/airlock))
- INVOKE_ASYNC(src, PROC_REF(pry_door), target)
- return COMPONENT_HOSTILE_NO_ATTACK
-
if(!combat_mode)
INVOKE_ASYNC(src, PROC_REF(poison_target), target)
return COMPONENT_HOSTILE_NO_ATTACK
@@ -195,7 +191,7 @@
balloon_alert(src, "already dead!")
return FALSE
- if(living_target.faction_check_mob(src, exact_match = TRUE))
+ if(living_target.faction_check_atom(src, exact_match = TRUE))
balloon_alert(src, "one of your soldiers!")
return FALSE
@@ -235,43 +231,7 @@
heal_bodypart_damage(amount)
qdel(target)
-/**
- * Allows rat king to pry open an airlock if it isn't locked.
- *
- * A proc used for letting the rat king pry open airlocks instead of just attacking them.
- * This allows the rat king to traverse the station when there is a lack of vents or
- * accessible doors, something which is common in certain rat king spawn points.
- *
- * Returns TRUE if the door opens, FALSE otherwise.
- */
-/mob/living/basic/regal_rat/proc/pry_door(target)
- if(DOING_INTERACTION(src, REGALRAT_INTERACTION))
- return FALSE
-
- var/obj/machinery/door/airlock/prying_door = target
- if(!prying_door.density || prying_door.locked || prying_door.welded || prying_door.seal)
- return FALSE
-
- visible_message(
- span_warning("[src] begins prying open the airlock..."),
- span_notice("You begin digging your claws into the airlock..."),
- span_warning("You hear groaning metal..."),
- )
- var/time_to_open = 0.5 SECONDS
-
- if(prying_door.hasPower())
- time_to_open = 5 SECONDS
- playsound(src, 'sound/machines/airlock_alien_prying.ogg', 100, vary = TRUE)
-
- if(!do_after(src, time_to_open, prying_door, interaction_key = REGALRAT_INTERACTION))
- return FALSE
-
- if(!prying_door.open(BYPASS_DOOR_CHECKS))
- balloon_alert(src, "failed to open!")
- return FALSE
-
- return TRUE
-
+/// Regal rat subtype which can be possessed by ghosts
/mob/living/basic/regal_rat/controlled
poll_ghosts = TRUE
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
index b3c6935c92efe..6da3352c9d6dd 100644
--- a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
+++ b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
@@ -97,9 +97,7 @@
AddElement(/datum/element/simple_flying)
add_traits(list(TRAIT_SPACEWALK, TRAIT_SIXTHSENSE, TRAIT_FREE_HYPERSPACE_MOVEMENT), INNATE_TRAIT)
- for(var/ability in abilities)
- var/datum/action/spell = new ability(src)
- spell.Grant(src)
+ grant_actions_by_list(abilities)
RegisterSignal(src, COMSIG_LIVING_BANED, PROC_REF(on_baned))
RegisterSignal(src, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_move))
@@ -202,6 +200,18 @@
if(ishuman(A) && in_range(src, A))
attempt_harvest(A)
+ return
+
+ // This is probably the most cringe place I could put this but whatever -
+ // Revenants can click on spirit boards for seances like ghosts
+ if(istype(A, /obj/structure/spirit_board) \
+ && !HAS_TRAIT(src, TRAIT_REVENANT_REVEALED) \
+ && !HAS_TRAIT(src, TRAIT_NO_TRANSFORM) \
+ && !HAS_TRAIT(src, TRAIT_REVENANT_INHIBITED))
+
+ var/obj/structure/spirit_board/board = A
+ board.spirit_board_pick_letter(src)
+ return
/mob/living/basic/revenant/ranged_secondary_attack(atom/target, modifiers)
if(HAS_TRAIT(src, TRAIT_REVENANT_INHIBITED) || HAS_TRAIT(src, TRAIT_REVENANT_REVEALED) || HAS_TRAIT(src, TRAIT_NO_TRANSFORM) || !Adjacent(target) || !incorporeal_move_check(target))
@@ -290,7 +300,7 @@
span_revendanger("NO! No... it's too late, you can feel your essence [pick("breaking apart", "drifting away")]..."),
)
- invisibility = 0
+ SetInvisibility(INVISIBILITY_NONE, id=type)
icon_state = "revenant_draining"
playsound(src, 'sound/effects/screech.ogg', 100, TRUE)
@@ -416,7 +426,7 @@
draining = FALSE
dormant = FALSE
incorporeal_move = INCORPOREAL_MOVE_JAUNT
- invisibility = INVISIBILITY_REVENANT
+ RemoveInvisibility(type)
alpha = 255
/mob/living/basic/revenant/proc/change_essence_amount(essence_to_change_by, silent = FALSE, source = null)
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/revenant_effects.dm b/code/modules/mob/living/basic/space_fauna/revenant/revenant_effects.dm
index 0eeec231973ee..b7bc6e34dcf7e 100644
--- a/code/modules/mob/living/basic/space_fauna/revenant/revenant_effects.dm
+++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_effects.dm
@@ -16,7 +16,7 @@
owner.orbiting?.end_orbit(src)
ADD_TRAIT(owner, TRAIT_REVENANT_REVEALED, TRAIT_STATUS_EFFECT(id))
- owner.invisibility = 0
+ owner.SetInvisibility(INVISIBILITY_NONE, id=type, priority=INVISIBILITY_PRIORITY_BASIC_ANTI_INVISIBILITY)
owner.incorporeal_move = FALSE
owner.update_appearance(UPDATE_ICON)
owner.update_mob_action_buttons()
@@ -25,7 +25,7 @@
REMOVE_TRAIT(owner, TRAIT_REVENANT_REVEALED, TRAIT_STATUS_EFFECT(id))
owner.incorporeal_move = INCORPOREAL_MOVE_JAUNT
- owner.invisibility = INVISIBILITY_REVENANT
+ owner.RemoveInvisibility(type)
owner.update_appearance(UPDATE_ICON)
owner.update_mob_action_buttons()
return ..()
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/revenant_harvest.dm b/code/modules/mob/living/basic/space_fauna/revenant/revenant_harvest.dm
index b8bb05db48414..c162ecf2c213e 100644
--- a/code/modules/mob/living/basic/space_fauna/revenant/revenant_harvest.dm
+++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_harvest.dm
@@ -14,6 +14,10 @@
to_chat(src, span_revenwarning("You are already siphoning the essence of a soul!"))
return FALSE
+ if(target.flags_1 & HOLOGRAM_1)
+ target.balloon_alert(src, "doesn't possess a soul!") // it's a machine generated visual
+ return
+
draining = TRUE
var/value_to_return = harvest_soul(target)
if(!value_to_return)
diff --git a/code/modules/mob/living/basic/space_fauna/robot_customer.dm b/code/modules/mob/living/basic/space_fauna/robot_customer.dm
index e084e11f403c9..aa5e4635f5888 100644
--- a/code/modules/mob/living/basic/space_fauna/robot_customer.dm
+++ b/code/modules/mob/living/basic/space_fauna/robot_customer.dm
@@ -35,7 +35,7 @@
. = ..()
- ADD_TRAIT(src, list(TRAIT_NOMOBSWAP, TRAIT_NO_TELEPORT, TRAIT_STRONG_GRABBER), INNATE_TRAIT) // never suffer a bitch to fuck with you
+ add_traits(list(TRAIT_NOMOBSWAP, TRAIT_NO_TELEPORT, TRAIT_STRONG_GRABBER), INNATE_TRAIT) // never suffer a bitch to fuck with you
AddElement(/datum/element/footstep, FOOTSTEP_OBJ_ROBOT, 1, -6, sound_vary = TRUE)
ai_controller.set_blackboard_key(BB_CUSTOMER_CUSTOMERINFO, customer_info)
diff --git a/code/modules/mob/living/basic/space_fauna/snake/snake.dm b/code/modules/mob/living/basic/space_fauna/snake/snake.dm
index 13b9a327cc5c8..164752e277d97 100644
--- a/code/modules/mob/living/basic/space_fauna/snake/snake.dm
+++ b/code/modules/mob/living/basic/space_fauna/snake/snake.dm
@@ -49,7 +49,7 @@
AddElement(/datum/element/ai_retaliate)
AddElement(/datum/element/swabable, CELL_LINE_TABLE_SNAKE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
- AddElement(/datum/element/basic_eating, 2, 0, null, edibles)
+ AddElement(/datum/element/basic_eating, heal_amt = 2, food_types = edibles)
ai_controller.set_blackboard_key(BB_BASIC_FOODS, edibles)
AddComponent(\
diff --git a/code/modules/mob/living/basic/space_fauna/space_dragon/dragon_breath.dm b/code/modules/mob/living/basic/space_fauna/space_dragon/dragon_breath.dm
new file mode 100644
index 0000000000000..5be5038b3a416
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/space_dragon/dragon_breath.dm
@@ -0,0 +1,32 @@
+/// A space dragon's fire breath, toasts lunch AND buffs your friends
+/datum/action/cooldown/mob_cooldown/fire_breath/carp
+ desc = "A Space Dragon's burning breath not only chars its foes, but invigorates Space Carp as well."
+ fire_damage = 30
+ mech_damage = 50
+ fire_range = 20
+ fire_temperature = 700 // Even hotter than a megafauna for some reason
+ shared_cooldown = NONE
+ melee_cooldown_time = 0 SECONDS
+
+/datum/action/cooldown/mob_cooldown/fire_breath/carp/on_burn_mob(mob/living/barbecued, mob/living/source)
+ if (!source.faction_check_atom(barbecued))
+ return ..()
+ to_chat(barbecued, span_notice("[source]'s fiery breath fills you with energy!"))
+ barbecued.apply_status_effect(/datum/status_effect/carp_invigoration)
+
+/// Makes you run faster for the duration
+/datum/status_effect/carp_invigoration
+ id = "carp_invigorated"
+ alert_type = null
+ duration = 8 SECONDS
+
+/datum/status_effect/carp_invigoration/on_apply()
+ . = ..()
+ if (!.)
+ return
+ owner.add_filter("anger_glow", 3, list("type" = "outline", "color" = COLOR_CARP_RIFT_RED, "size" = 2))
+ owner.add_movespeed_modifier(/datum/movespeed_modifier/dragon_rage)
+
+/datum/status_effect/carp_invigoration/on_remove()
+ owner.remove_filter("anger_glow")
+ owner.remove_movespeed_modifier(/datum/movespeed_modifier/dragon_rage)
diff --git a/code/modules/mob/living/basic/space_fauna/space_dragon/dragon_gust.dm b/code/modules/mob/living/basic/space_fauna/space_dragon/dragon_gust.dm
new file mode 100644
index 0000000000000..074804de86c97
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/space_dragon/dragon_gust.dm
@@ -0,0 +1,94 @@
+/// Default additional time to spend stunned per usage of ability
+#define DEFAULT_ACTIVATED_ENDLAG 3 DECISECONDS
+
+/// Rise into the air and slam down, knocking people away. No real cooldown but has escalating endlag if used in quick succession.
+/datum/action/cooldown/mob_cooldown/wing_buffet
+ name = "Wing Buffet"
+ desc = "Rise into the air and release a powerful gust from your wings, blowing attackers away. Becomes more tiring if used in quick succession."
+ button_icon = 'icons/effects/magic.dmi'
+ button_icon_state = "tornado"
+ cooldown_time = 1 SECONDS
+ melee_cooldown_time = 0
+ click_to_activate = FALSE
+ shared_cooldown = NONE
+ /// Timer we use to track our current action
+ var/active_timer
+ /// How far away can we reach people?
+ var/gust_distance = 4
+ /// How long to animate for before we start?
+ var/windup_time = 1.2 SECONDS
+ /// Minimum amount of stun time following use of wing buffet
+ var/minimum_endlag = 4 DECISECONDS
+ /// Amount of extra time to stay stunned after the end of the ability
+ var/additional_endlag = 0 DECISECONDS
+ /// Amount of time to add to endlag after each successful use of the ability
+ var/endlag_per_activation = DEFAULT_ACTIVATED_ENDLAG
+ /// How much accumulated stun time do we subtract every second? Takes a full minute to regen off a single use :(
+ var/endlag_decay_per_second = DEFAULT_ACTIVATED_ENDLAG / 60
+ /// Increase the effect of our accumulated additional stun time by this much if space dragon has lost some rifts
+ var/exhaustion_multiplier = 5
+ /// List of traits we apply while the ability is ongoing, stops us from moving around and such
+ var/static/list/applied_traits = list(
+ TRAIT_IMMOBILIZED,
+ TRAIT_INCAPACITATED,
+ TRAIT_NO_FLOATING_ANIM,
+ TRAIT_WING_BUFFET,
+ )
+
+/datum/action/cooldown/mob_cooldown/wing_buffet/Grant(mob/granted_to)
+ . = ..()
+ RegisterSignal(granted_to, COMSIG_LIVING_LIFE, PROC_REF(on_life))
+
+/datum/action/cooldown/mob_cooldown/wing_buffet/Remove(mob/removed_from)
+ . = ..()
+ deltimer(active_timer)
+ UnregisterSignal(removed_from, COMSIG_LIVING_LIFE)
+ removed_from.remove_traits(applied_traits + TRAIT_WING_BUFFET_TIRED, REF(src))
+
+/// Decay our accumulated additional tiredness
+/datum/action/cooldown/mob_cooldown/wing_buffet/proc/on_life(mob/living/liver, seconds_per_tick, times_fired)
+ SIGNAL_HANDLER
+ if (liver.stat == DEAD)
+ return // not so life now buddy
+ additional_endlag = max(0, additional_endlag - (endlag_decay_per_second * seconds_per_tick))
+
+/datum/action/cooldown/mob_cooldown/wing_buffet/Activate(atom/target)
+ begin_sequence()
+ StartCooldown()
+ return TRUE
+
+/// Rise up into the air
+/datum/action/cooldown/mob_cooldown/wing_buffet/proc/begin_sequence()
+ owner.add_traits(applied_traits, REF(src)) // No moving till we're done
+ owner.update_appearance(UPDATE_ICON)
+ animate(owner, pixel_y = 20, time = windup_time)
+ active_timer = addtimer(CALLBACK(src, PROC_REF(ground_pound)), windup_time, TIMER_DELETE_ME | TIMER_STOPPABLE)
+
+/// Slam into the ground
+/datum/action/cooldown/mob_cooldown/wing_buffet/proc/ground_pound()
+ if (QDELETED(owner))
+ return
+ owner.pixel_y = 0
+ playsound(owner, 'sound/effects/gravhit.ogg', 100, TRUE)
+ for (var/mob/living/candidate in view(gust_distance, owner))
+ if(candidate == owner || candidate.faction_check_atom(owner))
+ continue
+ owner.visible_message(span_boldwarning("[candidate] is knocked back by the gust!"))
+ to_chat(candidate, span_userdanger("You're knocked back by the gust!"))
+ var/dir_to_target = get_dir(get_turf(owner), get_turf(candidate))
+ var/throwtarget = get_edge_target_turf(target, dir_to_target)
+ candidate.safe_throw_at(throwtarget, range = 10, speed = 1, thrower = owner)
+ candidate.Paralyze(5 SECONDS)
+
+ var/endlag_multiplier = HAS_TRAIT(owner, TRAIT_RIFT_FAILURE) ? exhaustion_multiplier : 1
+ var/stun_time = minimum_endlag + (additional_endlag * endlag_multiplier)
+ additional_endlag += endlag_per_activation * endlag_multiplier // double dips, rough
+ ADD_TRAIT(owner, TRAIT_WING_BUFFET_TIRED, REF(src))
+ owner.update_appearance(UPDATE_ICON)
+ active_timer = addtimer(CALLBACK(src, PROC_REF(complete_ability)), stun_time, TIMER_DELETE_ME | TIMER_STOPPABLE)
+
+/datum/action/cooldown/mob_cooldown/wing_buffet/proc/complete_ability()
+ owner.remove_traits(applied_traits + TRAIT_WING_BUFFET_TIRED, REF(src))
+ owner.update_appearance(UPDATE_ICON)
+
+#undef DEFAULT_ACTIVATED_ENDLAG
diff --git a/code/modules/mob/living/basic/space_fauna/space_dragon/space_dragon.dm b/code/modules/mob/living/basic/space_fauna/space_dragon/space_dragon.dm
new file mode 100644
index 0000000000000..547e35cf413a9
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/space_dragon/space_dragon.dm
@@ -0,0 +1,225 @@
+/// You can't make a dragon darker than this, it'd be hard to see
+#define REJECT_DARK_COLOUR_THRESHOLD 50
+/// Any interactions executed by the space dragon
+#define DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION "space dragon interaction"
+
+/**
+ * Advanced stage of the space carp life cycle, spawned as a midround antagonist or via traitor transformation.
+ * Can eat corpses to heal, blow people back with its wings, and obviously as a dragon it breathes fire. It can even tear through walls.
+ * The midround even version also creates rifts which summon carp, and heals when near them.
+ */
+/mob/living/basic/space_dragon
+ name = "Space Dragon"
+ desc = "A serpentine leviathan whose flight defies all modern understanding of physics. Said to be the ultimate stage in the life cycle of the Space Carp."
+ icon = 'icons/mob/nonhuman-player/spacedragon.dmi'
+ icon_state = "spacedragon"
+ icon_living = "spacedragon"
+ icon_dead = "spacedragon_dead"
+ health_doll_icon = "spacedragon"
+ faction = list(FACTION_CARP)
+ flags_1 = PREVENT_CONTENTS_EXPLOSION_1
+ gender = NEUTER
+ maxHealth = 400
+ health = 400
+ unsuitable_cold_damage = 0
+ unsuitable_heat_damage = 0
+ unsuitable_atmos_damage = 0
+ damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0.5, OXY = 1)
+ combat_mode = TRUE
+ speed = 0
+ movement_type = FLYING
+ attack_verb_continuous = "chomps"
+ attack_verb_simple = "chomp"
+ attack_sound = 'sound/magic/demon_attack1.ogg'
+ attack_vis_effect = ATTACK_EFFECT_BITE
+ obj_damage = 50
+ melee_damage_upper = 35
+ melee_damage_lower = 35
+ melee_attack_cooldown = CLICK_CD_MELEE
+ mob_size = MOB_SIZE_LARGE
+ armour_penetration = 30
+ pixel_x = -16
+ base_pixel_x = -16
+ maptext_height = 64
+ maptext_width = 64
+ mouse_opacity = MOUSE_OPACITY_ICON
+ death_sound = 'sound/creatures/space_dragon_roar.ogg'
+ death_message = "screeches in agony as it collapses to the floor, its life extinguished."
+ butcher_results = list(/obj/item/stack/ore/diamond = 5, /obj/item/stack/sheet/sinew = 5, /obj/item/stack/sheet/bone = 30)
+
+ /// The colour of the space dragon
+ var/chosen_colour
+ /// Minimum devastation damage dealt coefficient based on max health
+ var/devastation_damage_min_percentage = 0.4
+ /// Maximum devastation damage dealt coefficient based on max health
+ var/devastation_damage_max_percentage = 0.75
+ /// Our fire breath action
+ var/datum/action/cooldown/mob_cooldown/fire_breath/carp/fire_breath
+ /// Our wing flap action
+ var/datum/action/cooldown/mob_cooldown/wing_buffet/buffet
+
+/mob/living/basic/space_dragon/Initialize(mapload)
+ . = ..()
+ add_traits(list(TRAIT_SPACEWALK, TRAIT_FREE_HYPERSPACE_MOVEMENT, TRAIT_NO_FLOATING_ANIM, TRAIT_HEALS_FROM_CARP_RIFTS), INNATE_TRAIT)
+ AddElement(/datum/element/simple_flying)
+ AddElement(/datum/element/content_barfer)
+ AddElement(/datum/element/wall_tearer, tear_time = 4 SECONDS, reinforced_multiplier = 3, do_after_key = DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION)
+ AddElement(/datum/element/door_pryer, pry_time = 4 SECONDS, interaction_key = DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION)
+ AddComponent(/datum/component/seethrough_mob)
+ RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack))
+ RegisterSignal(src, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_changed))
+ RegisterSignal(src, COMSIG_ATOM_PRE_EX_ACT, PROC_REF(on_exploded))
+
+ fire_breath = new(src)
+ fire_breath.Grant(src)
+
+ buffet = new(src)
+ buffet.Grant(src)
+
+/mob/living/basic/space_dragon/Login()
+ . = ..()
+ if(!isnull(chosen_colour))
+ return
+ rename_dragon()
+ select_colour()
+
+/// Allows the space dragon to pick a funny name
+/mob/living/basic/space_dragon/proc/rename_dragon()
+ var/chosen_name = sanitize_name(reject_bad_text(tgui_input_text(src, "What would you like your name to be?", "Choose Your Name", real_name, MAX_NAME_LEN)))
+ if(!chosen_name) // Null or empty or rejected
+ to_chat(src, span_warning("Not a valid name, please try again."))
+ rename_dragon()
+ return
+ to_chat(src, span_notice("Your name is now [span_name("[chosen_name]")], the feared Space Dragon."))
+ fully_replace_character_name(null, chosen_name)
+
+/// Select scale colour with the colour picker
+/mob/living/basic/space_dragon/proc/select_colour()
+ chosen_colour = input(src, "What colour would you like to be?" ,"Colour Selection", COLOR_WHITE) as color|null
+ if(!chosen_colour) // Redo proc until we get a color
+ to_chat(src, span_warning("Not a valid colour, please try again."))
+ select_colour()
+ return
+ var/temp_hsv = RGBtoHSV(chosen_colour)
+ if(ReadHSV(temp_hsv)[3] < REJECT_DARK_COLOUR_THRESHOLD)
+ to_chat(src, span_danger("Invalid colour. Your colour is not bright enough."))
+ select_colour()
+ return
+ add_atom_colour(chosen_colour, FIXED_COLOUR_PRIORITY)
+ update_appearance(UPDATE_OVERLAYS)
+
+/mob/living/basic/space_dragon/update_icon_state()
+ . = ..()
+ if (stat == DEAD)
+ return
+ if (!HAS_TRAIT(src, TRAIT_WING_BUFFET))
+ icon_state = icon_living
+ return
+ if (HAS_TRAIT(src, TRAIT_WING_BUFFET_TIRED))
+ icon_state = "spacedragon_gust_2"
+ return
+ icon_state = "spacedragon_gust"
+
+/mob/living/basic/space_dragon/update_overlays()
+ . = ..()
+ var/overlay_state = "overlay_base"
+ if (stat == DEAD)
+ overlay_state = "overlay_dead"
+ else if (HAS_TRAIT(src, TRAIT_WING_BUFFET_TIRED))
+ overlay_state = "overlay_gust_2"
+ else if (HAS_TRAIT(src, TRAIT_WING_BUFFET))
+ overlay_state = "overlay_gust"
+
+ var/mutable_appearance/overlay = mutable_appearance(icon, overlay_state)
+ overlay.appearance_flags = RESET_COLOR
+ . += overlay
+
+/mob/living/basic/space_dragon/melee_attack(obj/vehicle/sealed/mecha/target, list/modifiers, ignore_cooldown)
+ . = ..()
+ if (!. || !ismecha(target))
+ return
+ target.take_damage(obj_damage, BRUTE, MELEE)
+
+/// Before we attack something, check if we want to do something else instead
+/mob/living/basic/space_dragon/proc/pre_attack(mob/living/source, atom/target)
+ SIGNAL_HANDLER
+ if (target == src)
+ return COMPONENT_HOSTILE_NO_ATTACK // Easy to misclick yourself, let's not
+ if (DOING_INTERACTION(source, DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION))
+ balloon_alert(source, "busy!")
+ return COMPONENT_HOSTILE_NO_ATTACK
+ if (!isliving(target))
+ return
+ var/mob/living/living_target = target
+ if (living_target.stat != DEAD)
+ return
+ INVOKE_ASYNC(src, PROC_REF(try_eat), living_target)
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+/// Try putting something inside us
+/mob/living/basic/space_dragon/proc/try_eat(mob/living/food)
+ balloon_alert(src, "swallowing...")
+ if (do_after(src, 3 SECONDS, target = food))
+ eat(food)
+
+/// Succeed in putting something inside us
+/mob/living/basic/space_dragon/proc/eat(mob/living/food)
+ adjust_health(-food.maxHealth * 0.25)
+ if (QDELETED(food) || food.loc == src)
+ return FALSE
+ playsound(src, 'sound/magic/demon_attack1.ogg', 60, TRUE)
+ visible_message(span_boldwarning("[src] swallows [food] whole!"))
+ food.extinguish_mob() // It's wet in there, and our food is likely to be on fire. Let's be decent and not husk them.
+ food.forceMove(src)
+ return TRUE
+
+/mob/living/basic/space_dragon/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ . = ..()
+ if (isliving(arrived))
+ RegisterSignal(arrived, COMSIG_MOB_STATCHANGE, PROC_REF(eaten_stat_changed))
+
+/mob/living/basic/space_dragon/Exited(atom/movable/gone, direction)
+ . = ..()
+ if (isliving(gone))
+ UnregisterSignal(gone, COMSIG_MOB_STATCHANGE)
+
+/// Release consumed mobs if they transition from dead to alive
+/mob/living/basic/space_dragon/proc/eaten_stat_changed(mob/living/eaten)
+ SIGNAL_HANDLER
+ if (eaten.stat == DEAD)
+ return
+ new /obj/effect/decal/cleanable/vomit(loc)
+ playsound(src, 'sound/effects/splat.ogg', vol = 50, vary = TRUE)
+ visible_message(span_danger("[src] vomits up [eaten]!"))
+ eaten.forceMove(loc)
+ eaten.Paralyze(5 SECONDS)
+
+/mob/living/basic/space_dragon/RangedAttack(atom/target, modifiers)
+ fire_breath.Trigger(target = target)
+
+/mob/living/basic/space_dragon/ranged_secondary_attack(atom/target, modifiers)
+ buffet.Trigger()
+
+/// When our stat changes, make sure we are using the correct overlay
+/mob/living/basic/space_dragon/proc/on_stat_changed()
+ SIGNAL_HANDLER
+ update_appearance(UPDATE_OVERLAYS)
+
+/// We take devastating bomb damage as a random percentage of our maximum health instead of being gibbed
+/mob/living/basic/space_dragon/proc/on_exploded(mob/living/source, severity, target, origin)
+ SIGNAL_HANDLER
+ if (severity != EXPLODE_DEVASTATE)
+ return
+ var/damage_coefficient = rand(devastation_damage_min_percentage, devastation_damage_max_percentage)
+ adjustBruteLoss(initial(maxHealth)*damage_coefficient)
+ return COMPONENT_CANCEL_EX_ACT // we handled it
+
+/// Subtype used by the midround/event
+/mob/living/basic/space_dragon/spawn_with_antag
+
+/mob/living/basic/space_dragon/spawn_with_antag/mind_initialize()
+ . = ..()
+ mind.add_antag_datum(/datum/antagonist/space_dragon)
+
+#undef REJECT_DARK_COLOUR_THRESHOLD
+#undef DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION
diff --git a/code/modules/mob/living/basic/space_fauna/spider/giant_spider/giant_spiders.dm b/code/modules/mob/living/basic/space_fauna/spider/giant_spider/giant_spiders.dm
index 8cb7d8398bf36..c79731bcdd1a8 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/giant_spider/giant_spiders.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/giant_spider/giant_spiders.dm
@@ -19,7 +19,12 @@
melee_damage_upper = 25
gold_core_spawnable = HOSTILE_SPAWN
ai_controller = /datum/ai_controller/basic_controller/giant_spider
+ /// Actions to grant on Initialize
+ var/list/innate_actions = null
+/mob/living/basic/spider/giant/Initialize(mapload)
+ . = ..()
+ grant_actions_by_list(innate_actions)
/**
* ### Ambush Spider
@@ -37,11 +42,13 @@
maxHealth = 125
health = 125
obj_damage = 45
+
melee_damage_lower = 25
melee_damage_upper = 30
speed = 5
player_speed_modifier = -3.1
- menu_description = "Slow spider variant specializing in stalking and ambushing prey, above avarage health and damage with a strong grip."
+ menu_description = "Slow spider, with a strong disarming pull and above average health and damage."
+ innate_actions = list(/datum/action/cooldown/mob_cooldown/sneak/spider)
/mob/living/basic/spider/giant/ambush/Initialize(mapload)
. = ..()
@@ -49,9 +56,6 @@
AddElement(/datum/element/web_walker, /datum/movespeed_modifier/slow_web)
- var/datum/action/cooldown/mob_cooldown/sneak/spider/sneak_web = new(src)
- sneak_web.Grant(src)
-
/**
* ### Guard Spider
* A subtype of the giant spider which is similar on every single way,
@@ -72,14 +76,12 @@
obj_damage = 45
speed = 5
player_speed_modifier = -4
- menu_description = "Tanky and strong for the defense of the nest and other spiders."
+ menu_description = "Tanky and strong able to shed a carcass for protection."
+ innate_actions = list(/datum/action/cooldown/mob_cooldown/web_effigy)
/mob/living/basic/spider/giant/guard/Initialize(mapload)
. = ..()
-
AddElement(/datum/element/web_walker, /datum/movespeed_modifier/average_web)
- var/datum/action/cooldown/mob_cooldown/web_effigy/shed = new(src)
- shed.Grant(src)
/**
* ### Hunter Spider
@@ -100,11 +102,10 @@
poison_per_bite = 5
speed = 3
player_speed_modifier = -3.1
- menu_description = "Fast spider variant specializing in catching running prey and toxin injection, but has less health and damage."
+ menu_description = "Fast spider with toxin injection, but has less health and damage."
/mob/living/basic/spider/giant/hunter/Initialize(mapload)
. = ..()
-
AddElement(/datum/element/web_walker, /datum/movespeed_modifier/fast_web)
/**
@@ -129,15 +130,13 @@
speed = 2.8
player_speed_modifier = -3.1
sight = SEE_SELF|SEE_MOBS
- menu_description = "Fast spider variant specializing in scouting and alerting of prey, with the ability to travel in vents."
+ menu_description = "Fast spider able to see enemies through walls, send messages to the nest and the ability to travel in vents."
+ innate_actions = list(/datum/action/cooldown/mob_cooldown/command_spiders/communication_spiders)
/mob/living/basic/spider/giant/scout/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
- var/datum/action/cooldown/mob_cooldown/command_spiders/communication_spiders/spiders_communication = new(src)
- spiders_communication.Grant(src)
-
/**
* ### Nurse Spider
*
@@ -162,7 +161,7 @@
player_speed_modifier = -3.1
web_speed = 0.25
web_type = /datum/action/cooldown/mob_cooldown/lay_web/sealer
- menu_description = "Support spider variant specializing in healing their brethren and placing webbings very swiftly, but has very low amount of health and deals low damage."
+ menu_description = "Avarage speed spider able to heal other spiders and itself together with a fast web laying capability, has low damage and health."
///The health HUD applied to the mob.
var/health_hud = DATA_HUD_MEDICAL_ADVANCED
@@ -209,21 +208,16 @@
speed = 4
player_speed_modifier = -3.1
web_type = /datum/action/cooldown/mob_cooldown/lay_web/sealer
- menu_description = "Support spider variant specializing in contruction to protect their brethren, but has very low amount of health and deals low damage."
+ menu_description = "Average speed spider with self healing abilities and multiple web types to reinforce the nest with little to no damage and low health."
+ innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/lay_web/solid_web,
+ /datum/action/cooldown/mob_cooldown/lay_web/sticky_web,
+ /datum/action/cooldown/mob_cooldown/lay_web/web_passage,
+ /datum/action/cooldown/mob_cooldown/lay_web/web_spikes,
+ )
/mob/living/basic/spider/giant/tangle/Initialize(mapload)
. = ..()
- var/datum/action/cooldown/mob_cooldown/lay_web/solid_web/web_solid = new(src)
- web_solid.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/lay_web/web_passage/passage_web = new(src)
- passage_web.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/lay_web/web_spikes/spikes_web = new(src)
- spikes_web.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/lay_web/sticky_web/web_sticky = new(src)
- web_sticky.Grant(src)
AddElement(/datum/element/web_walker, /datum/movespeed_modifier/average_web)
@@ -246,6 +240,97 @@
return FALSE
return TRUE
+/**
+ * ### Spider Tank
+ * A subtype of the giant spider, specialized in taking damage.
+ * This spider is only slightly slower than a human.
+ */
+/mob/living/basic/spider/giant/tank
+ name = "tank spider"
+ desc = "Furry and Purple with a white top, it makes you shudder to look at it. This one has bright yellow eyes."
+ icon_state = "tank"
+ icon_living = "tank"
+ icon_dead = "tank_dead"
+ maxHealth = 500
+ health = 500
+ damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 1, OXY = 1)
+ melee_damage_lower = 5
+ melee_damage_upper = 5
+ obj_damage = 15
+ speed = 5
+ player_speed_modifier = -4
+ menu_description = "Extremly tanky with very poor offence. Able to self heal and lay reflective silk screens."
+
+/mob/living/basic/spider/giant/tank/Initialize(mapload)
+ . = ..()
+ var/datum/action/cooldown/mob_cooldown/lay_web/web_reflector/reflector_web = new(src)
+ reflector_web.Grant(src)
+
+ var/datum/action/cooldown/mob_cooldown/lay_web/web_passage/passage_web = new(src)
+ passage_web.Grant(src)
+
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/below_average_web)
+
+ AddComponent(/datum/component/healing_touch,\
+ heal_brute = 50,\
+ heal_burn = 50,\
+ heal_time = 5 SECONDS,\
+ self_targetting = HEALING_TOUCH_SELF_ONLY,\
+ interaction_key = DOAFTER_SOURCE_SPIDER,\
+ valid_targets_typecache = typecacheof(list(/mob/living/basic/spider/growing/young/tank, /mob/living/basic/spider/giant/tank)),\
+ extra_checks = CALLBACK(src, PROC_REF(can_mend)),\
+ action_text = "%SOURCE% begins mending themselves...",\
+ complete_text = "%SOURCE%'s wounds mend together.",\
+ )
+
+/// Prevent you from healing when on fire
+/mob/living/basic/spider/giant/tank/proc/can_mend(mob/living/source, mob/living/target)
+ if (on_fire)
+ balloon_alert(src, "on fire!")
+ return FALSE
+ return TRUE
+
+/**
+ * ### Spider Breacher
+ * A subtype of the giant spider, specialized in breaching and invasion.
+ * This spider is only slightly slower than a human.
+ */
+/mob/living/basic/spider/giant/breacher
+ name = "breacher spider"
+ desc = "Furry and light brown with dark brown and red highlights, it makes you shudder to look at it. This one has bright red eyes."
+ icon_state = "breacher"
+ icon_living = "breacher"
+ icon_dead = "breacher_dead"
+ maxHealth = 120
+ health = 120
+ melee_damage_lower = 5
+ melee_damage_upper = 10
+ unsuitable_atmos_damage = 0
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = 700
+ unsuitable_cold_damage = 0
+ wound_bonus = 25
+ bare_wound_bonus = 50
+ sharpness = SHARP_EDGED
+ obj_damage = 60
+ web_speed = 0.25
+ limb_destroyer = 50
+ speed = 5
+ player_speed_modifier = -4
+ sight = SEE_TURFS
+ menu_description = "Atmospherically resistant with the ability to destroy walls and limbs, and to send warnings to the nest."
+
+/mob/living/basic/spider/giant/breacher/Initialize(mapload)
+ . = ..()
+ var/datum/action/cooldown/mob_cooldown/lay_web/solid_web/web_solid = new(src)
+ web_solid.Grant(src)
+
+ var/datum/action/cooldown/mob_cooldown/command_spiders/warning_spiders/spiders_warning = new(src)
+ spiders_warning.Grant(src)
+
+ AddElement(/datum/element/wall_tearer)
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/below_average_web)
+
/**
* ### Tarantula
*
@@ -259,8 +344,8 @@
icon_state = "tarantula"
icon_living = "tarantula"
icon_dead = "tarantula_dead"
- maxHealth = 360 // woah nelly
- health = 360
+ maxHealth = 400 // woah nelly
+ health = 400
melee_damage_lower = 35
melee_damage_upper = 40
obj_damage = 100
@@ -272,21 +357,19 @@
web_speed = 0.7
web_type = /datum/action/cooldown/mob_cooldown/lay_web/sealer
menu_description = "Tank spider variant with an enormous amount of health and damage, but is very slow when not on webbing. It also has a charge ability to close distance with a target after a small windup."
- /// Charging ability
+ innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/charge/basic_charge,
+ /datum/action/cooldown/mob_cooldown/lay_web/solid_web,
+ /datum/action/cooldown/mob_cooldown/lay_web/web_passage,
+ )
+ /// Charging ability, kept seperate from innate_actions due to implementation details
var/datum/action/cooldown/mob_cooldown/charge/basic_charge/charge
/mob/living/basic/spider/giant/tarantula/Initialize(mapload)
. = ..()
- var/datum/action/cooldown/mob_cooldown/lay_web/solid_web/web_solid = new(src)
- web_solid.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/lay_web/web_passage/passage_web = new(src)
- passage_web.Grant(src)
-
charge = new /datum/action/cooldown/mob_cooldown/charge/basic_charge()
charge.Grant(src)
- AddElement(/datum/element/tear_wall)
AddElement(/datum/element/web_walker, /datum/movespeed_modifier/slow_web)
/mob/living/basic/spider/giant/tarantula/Destroy()
@@ -320,15 +403,14 @@
player_speed_modifier = -2.5
gold_core_spawnable = NO_SPAWN
menu_description = "Assassin spider variant with an unmatched speed and very deadly poison, but has very low amount of health and damage."
+ innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/defensive_mode,
+ )
/mob/living/basic/spider/giant/viper/Initialize(mapload)
. = ..()
-
AddElement(/datum/element/bonus_damage)
- var/datum/action/cooldown/mob_cooldown/defensive_mode/defensive_action = new(src)
- defensive_action.Grant(src)
-
/**
* ### Spider Broodmother
*
@@ -355,35 +437,21 @@
web_speed = 0.5
web_type = /datum/action/cooldown/mob_cooldown/lay_web/sealer
menu_description = "Royal spider variant specializing in reproduction and leadership, deals low damage."
+ innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/command_spiders,
+ /datum/action/cooldown/mob_cooldown/lay_eggs,
+ /datum/action/cooldown/mob_cooldown/lay_eggs/abnormal,
+ /datum/action/cooldown/mob_cooldown/lay_eggs/enriched,
+ /datum/action/cooldown/mob_cooldown/lay_web/solid_web,
+ /datum/action/cooldown/mob_cooldown/lay_web/sticky_web,
+ /datum/action/cooldown/mob_cooldown/lay_web/web_passage,
+ /datum/action/cooldown/mob_cooldown/lay_web/web_spikes,
+ /datum/action/cooldown/mob_cooldown/set_spider_directive,
+ /datum/action/cooldown/mob_cooldown/wrap,
+ )
/mob/living/basic/spider/giant/midwife/Initialize(mapload)
. = ..()
- var/datum/action/cooldown/mob_cooldown/lay_web/solid_web/web_solid = new(src)
- web_solid.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/lay_web/web_passage/passage_web = new(src)
- passage_web.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/lay_web/web_spikes/spikes_web = new(src)
- spikes_web.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/lay_web/sticky_web/web_sticky = new(src)
- web_sticky.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/wrap/wrapping = new(src)
- wrapping.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/lay_eggs/make_eggs = new(src)
- make_eggs.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/lay_eggs/enriched/make_better_eggs = new(src)
- make_better_eggs.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/set_spider_directive/give_orders = new(src)
- give_orders.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/command_spiders/not_hivemind_talk = new(src)
- not_hivemind_talk.Grant(src)
AddElement(/datum/element/web_walker, /datum/movespeed_modifier/average_web)
@@ -498,18 +566,15 @@
unsuitable_heat_damage = 1
menu_description = "Stronger assassin spider variant with an unmatched speed, high amount of health and very deadly poison, but deals very low amount of damage. It also has ability to ventcrawl."
apply_spider_antag = FALSE
+ innate_actions = list(
+ /datum/action/cooldown/mob_cooldown/lay_web/sticky_web,
+ /datum/action/cooldown/mob_cooldown/lay_web/web_spikes,
+ )
/mob/living/basic/spider/giant/viper/wizard/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
- var/datum/action/cooldown/mob_cooldown/lay_web/web_spikes/spikes_web = new(src)
- spikes_web.Grant(src)
-
- var/datum/action/cooldown/mob_cooldown/lay_web/sticky_web/web_sticky = new(src)
- web_sticky.Grant(src)
-
-
/**
* ### Sergeant Araneus
*
diff --git a/code/modules/mob/living/basic/space_fauna/spider/spider.dm b/code/modules/mob/living/basic/space_fauna/spider/spider.dm
index 53b48129e2ed4..9ce7f50d0174d 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/spider.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/spider.dm
@@ -54,6 +54,7 @@
AddElement(/datum/element/nerfed_pulling, GLOB.typecache_general_bad_things_to_easily_move)
AddElement(/datum/element/prevent_attacking_of_types, GLOB.typecache_general_bad_hostile_attack_targets, "this tastes awful!")
AddElement(/datum/element/cliff_walking)
+ AddComponent(/datum/component/health_scaling_effects, min_health_slowdown = 1.5)
if(poison_per_bite)
AddElement(/datum/element/venomous, poison_type, poison_per_bite)
diff --git a/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/hivemind.dm b/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/hivemind.dm
index bbeb2b28bb549..790879b0de2c1 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/hivemind.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/hivemind.dm
@@ -84,3 +84,16 @@
/datum/action/cooldown/mob_cooldown/command_spiders/communication_spiders/format_message(mob/living/user, message)
return span_spiderscout("Report from [user]: [message]")
+
+/**
+ * Sends a smaller message to all currently living spiders.
+ */
+/datum/action/cooldown/mob_cooldown/command_spiders/warning_spiders
+ name = "Warning"
+ desc = "Send a warning to all living spiders."
+ button_icon = 'icons/mob/actions/actions_animal.dmi'
+ button_icon_state = "warning"
+
+/datum/action/cooldown/mob_cooldown/command_spiders/warning_spiders/format_message(mob/living/user, message)
+ return span_spiderbreacher("Warning from [user]: [message]")
+
diff --git a/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/lay_eggs.dm b/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/lay_eggs.dm
index 28a543be3acfb..5979c98448ded 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/lay_eggs.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/lay_eggs.dm
@@ -5,9 +5,8 @@
button_icon_state = "lay_eggs"
background_icon_state = "bg_alien"
overlay_icon_state = "bg_alien_border"
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
- cooldown_time = 0
- melee_cooldown_time = 0
+ cooldown_time = 20 SECONDS
+ melee_cooldown_time = 5 SECONDS
shared_cooldown = NONE
click_to_activate = FALSE
///How long it takes for a broodmother to lay eggs.
@@ -61,10 +60,18 @@
if (spider_directive)
new_eggs.directive = spider_directive.current_directive
+/datum/action/cooldown/mob_cooldown/lay_eggs/abnormal
+ name = "Lay Abnormal Eggs"
+ desc = "Lay a cluster of eggs, which will soon grow into a uncommon spider."
+ button_icon_state = "lay_abnormal_eggs"
+ cooldown_time = 180 SECONDS
+ egg_type = /obj/effect/mob_spawn/ghost_role/spider/abnormal
+
/datum/action/cooldown/mob_cooldown/lay_eggs/enriched
name = "Lay Enriched Eggs"
- desc = "Lay a cluster of eggs, which will soon grow into a greater spider. Requires you drain a human per cluster of these eggs."
+ desc = "Lay a cluster of eggs, which will soon grow into a rare spider. Requires you drain a human per cluster of these eggs."
button_icon_state = "lay_enriched_eggs"
+ cooldown_time = 60 SECONDS
egg_type = /obj/effect/mob_spawn/ghost_role/spider/enriched
/// How many charges we have to make eggs
var/charges = 0
diff --git a/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/web.dm b/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/web.dm
index b7062a2dc17c3..fa44cb35b2d12 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/web.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/web.dm
@@ -6,7 +6,6 @@
button_icon_state = "lay_web"
background_icon_state = "bg_alien"
overlay_icon_state = "bg_alien_border"
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
cooldown_time = 0 SECONDS
melee_cooldown_time = 0
shared_cooldown = NONE
@@ -118,7 +117,6 @@
/datum/action/cooldown/mob_cooldown/lay_web/web_passage/plant_web(turf/target_turf, obj/structure/spider/stickyweb/existing_web)
new /obj/structure/spider/passage(target_turf)
-
/datum/action/cooldown/mob_cooldown/lay_web/sticky_web
name = "Spin Sticky Web"
desc = "Spin a sticky web to trap intruders."
@@ -172,3 +170,16 @@
/datum/action/cooldown/mob_cooldown/web_effigy/Activate()
new /obj/structure/spider/effigy(get_turf(owner))
return ..()
+
+/datum/action/cooldown/mob_cooldown/lay_web/web_reflector
+ name = "Spin reflective silk screen"
+ desc = "Spin a web to reflect missiles from the nest."
+ button_icon_state = "lay_web_reflector"
+ cooldown_time = 30 SECONDS
+ webbing_time = 4 SECONDS
+
+/datum/action/cooldown/mob_cooldown/lay_web/web_reflector/obstructed_by_other_web()
+ return !!(locate(/obj/structure/spider/reflector) in get_turf(owner))
+
+/datum/action/cooldown/mob_cooldown/lay_web/web_reflector/plant_web(turf/target_turf, obj/structure/spider/stickyweb/existing_web)
+ new /obj/structure/spider/reflector(target_turf)
diff --git a/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/wrap.dm b/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/wrap.dm
index 536f09cef8d1b..e7771f075a871 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/wrap.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/spider_abilities/wrap.dm
@@ -8,7 +8,6 @@
overlay_icon_state = "bg_alien_border"
button_icon = 'icons/mob/actions/actions_animal.dmi'
button_icon_state = "wrap_0"
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
click_to_activate = TRUE
ranged_mousepointer = 'icons/effects/mouse_pointers/wrap_target.dmi'
shared_cooldown = NONE
diff --git a/code/modules/mob/living/basic/space_fauna/spider/spiderlings/spiderling.dm b/code/modules/mob/living/basic/space_fauna/spider/spiderlings/spiderling.dm
index c949b438683cb..f36b1bc46ba35 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/spiderlings/spiderling.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/spiderlings/spiderling.dm
@@ -39,6 +39,7 @@
AddElement(/datum/element/footstep, FOOTSTEP_MOB_CLAW, volume = 0.2) // they're small but you can hear 'em
AddElement(/datum/element/web_walker, /datum/movespeed_modifier/spiderling_web)
AddElement(/datum/element/ai_retaliate)
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/fast_web)
// keep in mind we have infinite range (the entire pipenet is our playground, it's just a matter of random choice as to where we end up) so lower and upper both have their gives and takes.
// but, also remember the more time we aren't in a vent, the more susceptible we are to dying to anything and everything.
diff --git a/code/modules/mob/living/basic/space_fauna/spider/spiderlings/spiderling_subtypes.dm b/code/modules/mob/living/basic/space_fauna/spider/spiderlings/spiderling_subtypes.dm
index 5d42ca5cb6189..06d086d89672b 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/spiderlings/spiderling_subtypes.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/spiderlings/spiderling_subtypes.dm
@@ -1,5 +1,4 @@
-// This whole file is just a container for the spiderling subtypes that actually differentiate into different young spiders. None of them are particularly special as of now.
-
+/// This whole file is just a container for the spiderling subtypes that actually differentiate into different young spiders. None of them are particularly special as of now.
/// Will differentiate into the base young spider (known colloquially as the "guard" spider).
/mob/living/basic/spider/growing/spiderling/guard
grow_as = /mob/living/basic/spider/growing/young/guard
@@ -52,6 +51,24 @@
icon_state = "tangle_spiderling"
icon_dead = "tangle_spiderling_dead"
+/// Will differentiate into the "tank" young spider.
+/mob/living/basic/spider/growing/spiderling/tank
+ grow_as = /mob/living/basic/spider/growing/young/tank
+ name = "tank spiderling"
+ desc = "Furry and purple, it looks defenseless. This one has dim yellow eyes."
+ icon = 'icons/mob/simple/arachnoid.dmi'
+ icon_state = "tank_spiderling"
+ icon_dead = "tank_spiderling_dead"
+
+/// Will differentiate into the "breacher" young spider.
+/mob/living/basic/spider/growing/spiderling/breacher
+ grow_as = /mob/living/basic/spider/growing/young/breacher
+ name = "breacher spiderling"
+ desc = "Furry and baige, it looks defenseless. This one has dim red eyes."
+ icon = 'icons/mob/simple/arachnoid.dmi'
+ icon_state = "breacher_spiderling"
+ icon_dead = "breacher_spiderling_dead"
+
/// Will differentiate into the "midwife" young spider.
/mob/living/basic/spider/growing/spiderling/midwife
grow_as = /mob/living/basic/spider/growing/young/midwife
diff --git a/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider.dm b/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider.dm
index 50ec85e342c91..bdaf7d03faa6e 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider.dm
@@ -47,3 +47,6 @@
/datum/ai_planning_subtree/find_unwebbed_turf,
/datum/ai_planning_subtree/spin_web,
)
+
+/mob/living/basic/spider/growing/young/start_pulling(atom/movable/pulled_atom, state, force = move_force, supress_message = FALSE) // we're TOO FUCKING WEAK
+ return
diff --git a/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider_subtypes.dm b/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider_subtypes.dm
index f5d128e41b709..809755e355514 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider_subtypes.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider_subtypes.dm
@@ -11,6 +11,11 @@
melee_damage_upper = 15
speed = 0.7
+/mob/living/basic/spider/growing/young/guard/Initialize(mapload)
+ . = ..()
+
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/average_web)
+
/// Will differentiate into the "ambush" giant spider.
/mob/living/basic/spider/growing/young/ambush
grow_as = /mob/living/basic/spider/giant/ambush
@@ -27,8 +32,9 @@
/mob/living/basic/spider/growing/young/ambush/Initialize(mapload)
. = ..()
- var/datum/action/cooldown/mob_cooldown/sneak/spider/sneak_web = new(src)
- sneak_web.Grant(src)
+
+ GRANT_ACTION(/datum/action/cooldown/mob_cooldown/sneak/spider)
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/slow_web)
/// Will differentiate into the "scout" giant spider.
/mob/living/basic/spider/growing/young/scout
@@ -66,6 +72,11 @@
speed = 0.5
poison_per_bite = 2
+/mob/living/basic/spider/growing/young/Initialize(mapload)
+ . = ..()
+
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/fast_web)
+
/// Will differentiate into the "nurse" giant spider.
/mob/living/basic/spider/growing/young/nurse
grow_as = /mob/living/basic/spider/giant/nurse
@@ -98,6 +109,8 @@
complete_text = "%SOURCE% wraps the wounds of %TARGET%.",\
)
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/average_web)
+
/// Will differentiate into the "tangle" giant spider.
/mob/living/basic/spider/growing/young/tangle
grow_as = /mob/living/basic/spider/giant/tangle
@@ -130,6 +143,8 @@
complete_text = "%SOURCE%'s wounds mend together.",\
)
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/average_web)
+
/// Prevent you from healing other tangle spiders, or healing when on fire
/mob/living/basic/spider/growing/young/tangle/proc/can_mend(mob/living/source, mob/living/target)
if (on_fire)
@@ -137,6 +152,64 @@
return FALSE
return TRUE
+
+/// Will differentiate into the "tank" giant spider.
+/mob/living/basic/spider/growing/young/tank
+ grow_as = /mob/living/basic/spider/giant/tank
+ name = "young tank spider"
+ desc = "Furry and purple, it looks defenseless. This one has dim yellow eyes."
+ icon = 'icons/mob/simple/arachnoid.dmi'
+ icon_state = "young_tank"
+ icon_dead = "young_tank_dead"
+ maxHealth = 50
+ health = 50
+ damage_coeff = list(BRUTE = 0.5, BURN = 0.5, TOX = 0.5, CLONE = 0.5, STAMINA = 0.5, OXY = 1)
+ melee_damage_lower = 10
+ melee_damage_upper = 15
+ speed = 1
+
+/mob/living/basic/spider/growing/young/tank/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/healing_touch,\
+ heal_brute = 5,\
+ heal_burn = 5,\
+ heal_time = 2 SECONDS,\
+ self_targetting = HEALING_TOUCH_SELF_ONLY,\
+ interaction_key = DOAFTER_SOURCE_SPIDER,\
+ valid_targets_typecache = typecacheof(list(/mob/living/basic/spider/growing/young/tank, /mob/living/basic/spider/giant/tank)),\
+ extra_checks = CALLBACK(src, PROC_REF(can_mend)),\
+ action_text = "%SOURCE% begins mending themselves...",\
+ complete_text = "%SOURCE%'s wounds mend together.",\
+ )
+
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/below_average_web)
+
+/// Prevent you from healing when on fire
+/mob/living/basic/spider/growing/young/tank/proc/can_mend(mob/living/source, mob/living/target)
+ if (on_fire)
+ balloon_alert(src, "on fire!")
+ return FALSE
+ return TRUE
+
+/// Will differentiate into the "breacher" giant spider.
+/mob/living/basic/spider/growing/young/breacher
+ grow_as = /mob/living/basic/spider/giant/breacher
+ name = "young breacher spider"
+ desc = "Furry and baige, it looks defenseless. This one has dim red eyes."
+ icon = 'icons/mob/simple/arachnoid.dmi'
+ icon_state = "young_breacher"
+ icon_dead = "young_breacher_dead"
+ maxHealth = 60
+ health = 60
+ melee_damage_lower = 5
+ melee_damage_upper = 10
+ speed = 1
+
+/mob/living/basic/spider/growing/young/breacher/Initialize(mapload)
+ . = ..()
+
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/below_average_web)
+
/// Will differentiate into the "midwife" giant spider.
/mob/living/basic/spider/growing/young/midwife
grow_as = /mob/living/basic/spider/giant/midwife
@@ -153,6 +226,11 @@
web_speed = 0.5
web_type = /datum/action/cooldown/mob_cooldown/lay_web/sealer
+/mob/living/basic/spider/growing/young/midwife/Initialize(mapload)
+ . = ..()
+
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/average_web)
+
/// Will differentiate into the "viper" giant spider.
/mob/living/basic/spider/growing/young/viper
grow_as = /mob/living/basic/spider/giant/viper
@@ -183,3 +261,8 @@
melee_damage_upper = 25
speed = 1
obj_damage = 40
+
+/mob/living/basic/spider/growing/young/tarantula/Initialize(mapload)
+ . = ..()
+
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/slow_web)
diff --git a/code/modules/mob/living/basic/space_fauna/statue/statue.dm b/code/modules/mob/living/basic/space_fauna/statue/statue.dm
index d2ea5e8a831d0..c525c03bda017 100644
--- a/code/modules/mob/living/basic/space_fauna/statue/statue.dm
+++ b/code/modules/mob/living/basic/space_fauna/statue/statue.dm
@@ -56,14 +56,14 @@
/mob/living/basic/statue/Initialize(mapload, mob/living/creator)
. = ..()
- AddComponent(/datum/component/unobserved_actor, unobserved_flags = NO_OBSERVED_MOVEMENT | NO_OBSERVED_ATTACKS)
ADD_TRAIT(src, TRAIT_UNOBSERVANT, INNATE_TRAIT)
+ AddComponent(/datum/component/unobserved_actor, unobserved_flags = NO_OBSERVED_MOVEMENT | NO_OBSERVED_ATTACKS)
- // Give spells
- var/datum/action/cooldown/spell/aoe/flicker_lights/flicker = new(src)
- flicker.Grant(src)
- var/datum/action/cooldown/spell/aoe/blindness/blind = new(src)
- blind.Grant(src)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/spell/aoe/blindness,
+ /datum/action/cooldown/spell/aoe/flicker_lights,
+ )
+ grant_actions_by_list(innate_actions)
// Set creator
if(creator)
diff --git a/code/modules/mob/living/basic/space_fauna/wumborian_fugu/fugu_gland.dm b/code/modules/mob/living/basic/space_fauna/wumborian_fugu/fugu_gland.dm
index 63eb39c74e6fd..3dbb4a743ccf6 100644
--- a/code/modules/mob/living/basic/space_fauna/wumborian_fugu/fugu_gland.dm
+++ b/code/modules/mob/living/basic/space_fauna/wumborian_fugu/fugu_gland.dm
@@ -38,6 +38,6 @@
animal.melee_damage_lower = max((animal.melee_damage_lower * 2), 10)
animal.melee_damage_upper = max((animal.melee_damage_upper * 2), 10)
animal.transform *= 2
- animal.AddElement(/datum/element/wall_smasher, strength_flag = ENVIRONMENT_SMASH_RWALLS)
+ AddElement(/datum/element/wall_tearer)
to_chat(user, span_info("You increase the size of [animal], giving [animal.p_them()] a surge of strength!"))
qdel(src)
diff --git a/code/modules/mob/living/basic/space_fauna/wumborian_fugu/inflation.dm b/code/modules/mob/living/basic/space_fauna/wumborian_fugu/inflation.dm
index a9e2b538bdd74..70b3506527a18 100644
--- a/code/modules/mob/living/basic/space_fauna/wumborian_fugu/inflation.dm
+++ b/code/modules/mob/living/basic/space_fauna/wumborian_fugu/inflation.dm
@@ -58,7 +58,7 @@
RegisterSignal(fugu, COMSIG_MOB_STATCHANGE, PROC_REF(check_death))
fugu.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/inflated)
ADD_TRAIT(fugu, TRAIT_FUGU_GLANDED, TRAIT_STATUS_EFFECT(id))
- fugu.AddElement(/datum/element/wall_smasher)
+ fugu.AddElement(/datum/element/wall_tearer, allow_reinforced = FALSE)
fugu.mob_size = MOB_SIZE_LARGE
fugu.icon_state = "Fugu1"
fugu.melee_damage_lower = 15
@@ -76,7 +76,7 @@
UnregisterSignal(fugu, COMSIG_MOB_STATCHANGE)
fugu.remove_movespeed_modifier(/datum/movespeed_modifier/status_effect/inflated)
REMOVE_TRAIT(fugu, TRAIT_FUGU_GLANDED, TRAIT_STATUS_EFFECT(id))
- fugu.RemoveElement(/datum/element/wall_smasher)
+ fugu.RemoveElement(/datum/element/wall_tearer, allow_reinforced = FALSE)
fugu.mob_size = MOB_SIZE_SMALL
fugu.melee_damage_lower = 0
fugu.melee_damage_upper = 0
diff --git a/code/modules/mob/living/basic/syndicate/syndicate_ai.dm b/code/modules/mob/living/basic/syndicate/syndicate_ai.dm
deleted file mode 100644
index be84c1a568505..0000000000000
--- a/code/modules/mob/living/basic/syndicate/syndicate_ai.dm
+++ /dev/null
@@ -1,67 +0,0 @@
-/datum/ai_controller/basic_controller/syndicate
- blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead
- )
-
- 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/syndicate,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- )
-
-/datum/ai_planning_subtree/basic_melee_attack_subtree/syndicate
- melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/syndicate
-
-/datum/ai_behavior/basic_melee_attack/syndicate
- action_cooldown = 1.2 SECONDS
-
-/datum/ai_planning_subtree/attack_obstacle_in_path/syndicate
- attack_behaviour = /datum/ai_behavior/attack_obstructions/syndicate
-
-/datum/ai_behavior/attack_obstructions/syndicate
- action_cooldown = 1.2 SECONDS
-
-/datum/ai_controller/basic_controller/syndicate/ranged
- planning_subtrees = list(
- /datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/basic_ranged_attack_subtree/syndicate,
- )
-
-/datum/ai_planning_subtree/basic_ranged_attack_subtree/syndicate
- ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack/syndicate
-
-/datum/ai_behavior/basic_ranged_attack/syndicate
- action_cooldown = 1 SECONDS
- required_distance = 5
-
-/datum/ai_controller/basic_controller/syndicate/ranged/burst
- planning_subtrees = list(
- /datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/basic_ranged_attack_subtree/syndicate_burst
- )
-
-/datum/ai_planning_subtree/basic_ranged_attack_subtree/syndicate_burst
- ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack/syndicate_burst
-
-/datum/ai_behavior/basic_ranged_attack/syndicate_burst
- action_cooldown = 3 SECONDS
-
-/datum/ai_controller/basic_controller/syndicate/ranged/shotgunner
- planning_subtrees = list(
- /datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/basic_ranged_attack_subtree/syndicate_shotgun
- )
-
-/datum/ai_planning_subtree/basic_ranged_attack_subtree/syndicate_shotgun
- ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack/syndicate_shotgun
-
-/datum/ai_behavior/basic_ranged_attack/syndicate_shotgun
- action_cooldown = 3 SECONDS
- required_distance = 1
-
-/datum/ai_controller/basic_controller/syndicate/viscerator
- blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic()
- )
diff --git a/code/modules/mob/living/basic/trader/trader.dm b/code/modules/mob/living/basic/trader/trader.dm
new file mode 100644
index 0000000000000..29a2bda419930
--- /dev/null
+++ b/code/modules/mob/living/basic/trader/trader.dm
@@ -0,0 +1,79 @@
+/mob/living/basic/trader
+ name = "Trader"
+ desc = "Come buy some!"
+ unique_name = FALSE
+ icon = 'icons/mob/simple/simple_human.dmi'
+ maxHealth = 200
+ health = 200
+ melee_damage_lower = 10
+ melee_damage_upper = 10
+ attack_verb_continuous = "punches"
+ attack_verb_simple = "punch"
+ attack_sound = 'sound/weapons/punch1.ogg'
+ basic_mob_flags = DEL_ON_DEATH
+ unsuitable_atmos_damage = 2.5
+ combat_mode = FALSE
+ move_resist = MOVE_FORCE_STRONG
+ mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+ sentience_type = SENTIENCE_HUMANOID
+ speed = 0
+
+ ai_controller = /datum/ai_controller/basic_controller/trader
+
+ ///Sound used when item sold/bought
+ var/sell_sound = 'sound/effects/cashregister.ogg'
+ ///The currency name
+ var/currency_name = "credits"
+ ///The spawner we use to create our look
+ var/spawner_path = /obj/effect/mob_spawn/corpse/human/generic_assistant
+ ///Our species to create our look
+ var/species_path = /datum/species/human
+ ///The loot we drop when we die
+ var/loot = list(/obj/effect/mob_spawn/corpse/human/generic_assistant)
+ ///Casing used to shoot during retaliation
+ var/ranged_attack_casing = /obj/item/ammo_casing/shotgun/buckshot
+ ///Sound to make while doing a retalitory attack
+ var/ranged_attack_sound = 'sound/weapons/gun/pistol/shot.ogg'
+ ///Weapon path, for visuals
+ var/held_weapon_visual = /obj/item/gun/ballistic/shotgun
+
+ ///Type path for the trader datum to use for retrieving the traders wares, speech, etc
+ var/trader_data_path = /datum/trader_data
+
+
+/mob/living/basic/trader/Initialize(mapload)
+ . = ..()
+ apply_dynamic_human_appearance(src, species_path = species_path, mob_spawn_path = spawner_path, r_hand = held_weapon_visual)
+
+ var/datum/trader_data/trader_data = new trader_data_path
+ AddComponent(/datum/component/trader, trader_data = trader_data)
+ AddComponent(/datum/component/ranged_attacks, casing_type = ranged_attack_casing, projectile_sound = ranged_attack_sound, cooldown_time = 3 SECONDS)
+ AddElement(/datum/element/ai_retaliate)
+ AddElement(/datum/element/ai_swap_combat_mode, BB_BASIC_MOB_CURRENT_TARGET, string_list(trader_data.say_phrases[TRADER_BATTLE_START_PHRASE]), string_list(trader_data.say_phrases[TRADER_BATTLE_END_PHRASE]))
+ if(LAZYLEN(loot))
+ loot = string_list(loot)
+ AddElement(/datum/element/death_drops, loot)
+
+ var/datum/action/setup_shop/setup_shop = new (src, trader_data.shop_spot_type, trader_data.sign_type, trader_data.sell_sound, trader_data.say_phrases[TRADER_SHOP_OPENING_PHRASE])
+ setup_shop.Grant(src)
+ ai_controller.set_blackboard_key(BB_SETUP_SHOP, setup_shop)
+
+/mob/living/basic/trader/mrbones
+ name = "Mr. Bones"
+ desc = "A skeleton merchant, he seems very humerus."
+ speak_emote = list("rattles")
+ speech_span = SPAN_SANS
+ mob_biotypes = MOB_UNDEAD|MOB_HUMANOID
+ icon_state = "mrbones"
+ gender = MALE
+
+ ai_controller = /datum/ai_controller/basic_controller/trader/jumpscare
+
+ sell_sound = 'sound/voice/hiss2.ogg'
+ species_path = /datum/species/skeleton
+ spawner_path = /obj/effect/mob_spawn/corpse/human/skeleton/mrbones
+ loot = list(/obj/effect/decal/remains/human)
+ ranged_attack_casing = /obj/item/ammo_casing/energy/bolt/halloween
+ held_weapon_visual = /obj/item/gun/ballistic/revolver
+
+ trader_data_path = /datum/trader_data/mr_bones
diff --git a/code/modules/mob/living/basic/trader/trader_actions.dm b/code/modules/mob/living/basic/trader/trader_actions.dm
new file mode 100644
index 0000000000000..ded9fbd46d0d5
--- /dev/null
+++ b/code/modules/mob/living/basic/trader/trader_actions.dm
@@ -0,0 +1,72 @@
+/datum/action/setup_shop
+ name = "Setup shop"
+ desc = "Summons a wacky sales sign, and a comfy sitting spot to conduct your business from."
+ button_icon = 'icons/mob/actions/actions_trader.dmi'
+ button_icon_state = "setup_shop"
+ /// The shop spot
+ var/datum/weakref/shop_spot_ref
+ /// The server this console is connected to.
+ var/datum/weakref/sign_ref
+ /// The type of the chair we sit on
+ var/shop_spot_type
+ /// The type of our advertising sign
+ var/sign_type
+ /// The sound we make when we summon our shop gear
+ var/shop_sound
+ /// Lines we say when we open our shop
+ var/opening_lines
+
+/datum/action/setup_shop/IsAvailable(feedback = FALSE)
+ . = ..()
+ if (!.)
+ return FALSE
+ if(shop_spot_ref?.resolve())
+ if(feedback)
+ owner.balloon_alert(owner, "already set up!")
+ return FALSE
+ return TRUE
+
+/datum/action/setup_shop/New(Target, shop_spot_type = /obj/structure/chair/plastic, sign_type = /obj/structure/trader_sign, sell_sound = 'sound/effects/cashregister.ogg', opening_lines = list("Welcome to my shop, friend!"))
+ . = ..()
+
+ src.shop_spot_type = shop_spot_type
+ src.sign_type = sign_type
+ src.shop_sound = sell_sound
+ src.opening_lines = opening_lines
+
+/datum/action/setup_shop/Trigger(trigger_flags)
+ . = ..()
+ if(!.)
+ return
+
+ owner.say(pick(opening_lines))
+ var/obj/shop_spot = new shop_spot_type(owner.loc)
+ shop_spot.dir = owner.dir
+ shop_spot_ref = WEAKREF(shop_spot)
+ owner.ai_controller?.set_blackboard_key(BB_SHOP_SPOT, shop_spot)
+
+ playsound(owner, shop_sound, 50, TRUE)
+
+ var/turf/sign_turf
+
+ sign_turf = try_find_valid_spot(owner.loc, turn(shop_spot.dir, -90))
+ if(isnull(sign_turf)) //No space to my left, lets try right
+ sign_turf = try_find_valid_spot(owner.loc, turn(shop_spot.dir, 90))
+
+ if(isnull(sign_turf))
+ return
+
+ var/obj/sign = sign_ref?.resolve()
+ if(QDELETED(sign))
+ var/obj/new_sign = new sign_type(sign_turf)
+ sign_ref = WEAKREF(sign)
+ do_sparks(3, FALSE, new_sign)
+ else
+ do_teleport(sign,sign_turf)
+
+///Look for a spot we can place our sign on
+/datum/action/setup_shop/proc/try_find_valid_spot(origin_turf, direction_to_check)
+ var/turf/sign_turf = get_step(origin_turf, direction_to_check)
+ if(sign_turf && !isgroundlessturf(sign_turf) && !isclosedturf(sign_turf) && !sign_turf.is_blocked_turf())
+ return sign_turf
+ return null
diff --git a/code/modules/mob/living/basic/trader/trader_ai.dm b/code/modules/mob/living/basic/trader/trader_ai.dm
new file mode 100644
index 0000000000000..d6cf0095c7d83
--- /dev/null
+++ b/code/modules/mob/living/basic/trader/trader_ai.dm
@@ -0,0 +1,95 @@
+/datum/ai_controller/basic_controller/trader
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk/not_while_on_target/trader
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/basic_ranged_attack_subtree/trader,
+ /datum/ai_planning_subtree/prepare_travel_to_destination/trader,
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target,
+ /datum/ai_planning_subtree/setup_shop,
+ )
+
+/datum/ai_controller/basic_controller/trader/jumpscare
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/basic_ranged_attack_subtree/trader,
+ /datum/ai_planning_subtree/prepare_travel_to_destination/trader,
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target,
+ /datum/ai_planning_subtree/setup_shop/jumpscare,
+ )
+
+/datum/ai_planning_subtree/basic_ranged_attack_subtree/trader
+ ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack/trader
+
+/datum/ai_behavior/basic_ranged_attack/trader
+ action_cooldown = 3 SECONDS
+ avoid_friendly_fire = TRUE
+
+///Subtree to find our very first customer and set up our shop after walking right into their face
+/datum/ai_planning_subtree/setup_shop
+ ///What do we do in order to offer our deals?
+ var/datum/ai_behavior/setup_shop/setup_shop_behavior = /datum/ai_behavior/setup_shop
+
+/datum/ai_planning_subtree/setup_shop/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+
+ //If we don't have our ability, return
+ if(!controller.blackboard_key_exists(BB_SETUP_SHOP))
+ return
+
+ //If we already have a shop spot, return
+ if(controller.blackboard_key_exists(BB_SHOP_SPOT))
+ return
+
+ //If we don't have a costurmer to greet, look for one
+ if(!controller.blackboard_key_exists(BB_FIRST_CUSTOMER))
+ controller.queue_behavior(/datum/ai_behavior/find_and_set/conscious_person, BB_FIRST_CUSTOMER, /mob/living/carbon/human)
+ return
+
+ //We have our first customer, time to tell them about incredible deals
+ controller.queue_behavior(setup_shop_behavior, BB_FIRST_CUSTOMER)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+///The ai will create a shop the moment they see a potential costumer
+/datum/ai_behavior/setup_shop
+
+/datum/ai_behavior/setup_shop/setup(datum/ai_controller/controller, target_key)
+ var/obj/target = controller.blackboard[target_key]
+ return !QDELETED(target)
+
+/datum/ai_behavior/setup_shop/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+
+ //We lost track of our costumer or our ability, abort
+ if(!controller.blackboard_key_exists(target_key) || !controller.blackboard_key_exists(BB_SETUP_SHOP))
+ finish_action(controller, FALSE, target_key)
+ return
+
+ var/datum/action/setup_shop/shop = controller.blackboard[BB_SETUP_SHOP]
+ shop.Trigger()
+
+ controller.clear_blackboard_key(BB_FIRST_CUSTOMER)
+
+ finish_action(controller, TRUE, target_key)
+
+/datum/idle_behavior/idle_random_walk/not_while_on_target/trader
+ target_key = BB_SHOP_SPOT
+
+///Version of setup show where the trader will run at you to assault you with incredible deals
+/datum/ai_planning_subtree/setup_shop/jumpscare
+ setup_shop_behavior = /datum/ai_behavior/setup_shop/jumpscare
+
+/datum/ai_behavior/setup_shop/jumpscare
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
+
+/datum/ai_behavior/setup_shop/jumpscare/setup(datum/ai_controller/controller, target_key)
+ . = ..()
+ if(.)
+ set_movement_target(controller, controller.blackboard[target_key])
+
+/datum/ai_behavior/setup_shop/finish_action(datum/ai_controller/controller, succeeded, target_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
diff --git a/code/modules/mob/living/basic/trader/trader_data.dm b/code/modules/mob/living/basic/trader/trader_data.dm
new file mode 100644
index 0000000000000..9762dc02be500
--- /dev/null
+++ b/code/modules/mob/living/basic/trader/trader_data.dm
@@ -0,0 +1,156 @@
+///Used to contain the traders initial wares, and speech
+/datum/trader_data
+
+ ///The item that marks the shopkeeper will sit on
+ var/shop_spot_type = /obj/structure/chair/plastic
+ ///The sign that will greet the customers
+ var/sign_type = /obj/structure/trader_sign
+ ///Sound used when item sold/bought
+ var/sell_sound = 'sound/effects/cashregister.ogg'
+ ///The currency name
+ var/currency_name = "credits"
+ ///The initial products that the trader offers
+ var/list/initial_products = list(
+ /obj/item/food/burger/ghost = list(PAYCHECK_CREW * 4, INFINITY),
+ )
+ ///The initial products that the trader buys
+ var/list/initial_wanteds = list(
+ /obj/item/ectoplasm = list(PAYCHECK_CREW * 2, INFINITY, ""),
+ )
+ ///The speech data of the trader
+ var/list/say_phrases = list(
+ ITEM_REJECTED_PHRASE = list(
+ "Sorry, I'm not a fan of anything you're showing me. Give me something better and we'll talk.",
+ ),
+ ITEM_SELLING_CANCELED_PHRASE = list(
+ "What a shame, tell me if you changed your mind.",
+ ),
+ ITEM_SELLING_ACCEPTED_PHRASE = list(
+ "Pleasure doing business with you.",
+ ),
+ INTERESTED_PHRASE = list(
+ "Hey, you've got an item that interests me, I'd like to buy it, I'll give you some cash for it, deal?",
+ ),
+ BUY_PHRASE = list(
+ "Pleasure doing business with you.",
+ ),
+ NO_CASH_PHRASE = list(
+ "Sorry adventurer, I can't give credit! Come back when you're a little mmmmm... richer!",
+ ),
+ NO_STOCK_PHRASE = list(
+ "Sorry adventurer, but that item is not in stock at the moment.",
+ ),
+ NOT_WILLING_TO_BUY_PHRASE = list(
+ "I don't want to buy that item for the time being, check back another time.",
+ ),
+ ITEM_IS_WORTHLESS_PHRASE = list(
+ "This item seems to be worthless on a closer look, I won't buy this.",
+ ),
+ TRADER_HAS_ENOUGH_ITEM_PHRASE = list(
+ "I already bought enough of this for the time being.",
+ ),
+ TRADER_LORE_PHRASE = list(
+ "Hello! I am the test trader.",
+ "Oooooooo~!",
+ ),
+ TRADER_NOT_BUYING_ANYTHING = list(
+ "I'm currently buying nothing at the moment.",
+ ),
+ TRADER_NOT_SELLING_ANYTHING = list(
+ "I'm currently selling nothing at the moment.",
+ ),
+ TRADER_BATTLE_START_PHRASE = list(
+ "Thief!",
+ ),
+ TRADER_BATTLE_END_PHRASE = list(
+ "That is a discount I call death.",
+ ),
+ TRADER_SHOP_OPENING_PHRASE = list(
+ "Welcome to my shop, friend!",
+ ),
+ )
+
+/**
+ * Depending on the passed parameter/override, returns a randomly picked string out of a list
+ *
+ * Do note when overriding this argument, you will need to ensure pick(the list) doesn't get supplied with a list of zero length
+ * Arguments:
+ * * say_text - (String) a define that matches the key of a entry in say_phrases
+ */
+/datum/trader_data/proc/return_trader_phrase(say_text)
+ if(!length(say_phrases[say_text]))
+ return
+ return pick(say_phrases[say_text])
+
+/datum/trader_data/mr_bones
+ shop_spot_type = /obj/structure/chair/wood/wings
+ sign_type = /obj/structure/trader_sign/mrbones
+ sell_sound = 'sound/voice/hiss2.ogg'
+
+ initial_products = list(
+ /obj/item/clothing/head/helmet/skull = list(PAYCHECK_CREW * 3, INFINITY),
+ /obj/item/clothing/mask/bandana/skull/black = list(PAYCHECK_CREW, INFINITY),
+ /obj/item/food/cookie/sugar/spookyskull = list(PAYCHECK_CREW * 0.2, INFINITY),
+ /obj/item/instrument/trombone/spectral = list(PAYCHECK_CREW * 200, INFINITY),
+ /obj/item/shovel/serrated = list(PAYCHECK_CREW * 3, INFINITY),
+ )
+
+ initial_wanteds = list(
+ /obj/item/reagent_containers/condiment/milk = list(PAYCHECK_CREW * 20, INFINITY, ""),
+ /obj/item/stack/sheet/bone = list(PAYCHECK_CREW * 8.4, INFINITY, ", per sheet of bone"),
+ )
+
+ say_phrases = list(
+ ITEM_REJECTED_PHRASE = list(
+ "Sorry, I'm not a fan of anything you're showing me. Give me something better and we'll talk.",
+ ),
+ ITEM_SELLING_CANCELED_PHRASE = list(
+ "What a shame, tell me if you changed your mind.",
+ ),
+ ITEM_SELLING_ACCEPTED_PHRASE = list(
+ "Pleasure doing business with you.",
+ ),
+ INTERESTED_PHRASE = list(
+ "Hey, you've got an item that interests me, I'd like to buy it, I'll give you some cash for it, deal?",
+ ),
+ BUY_PHRASE = list(
+ "Bone appetit!",
+ ),
+ NO_CASH_PHRASE = list(
+ "Sorry adventurer, I can't give credit! Come back when you're a little mmmmm... richer!",
+ ),
+ NO_STOCK_PHRASE = list(
+ "Sorry adventurer, but that item is not in stock at the moment.",
+ ),
+ NOT_WILLING_TO_BUY_PHRASE = list(
+ "I don't want to buy that item for the time being, check back another time.",
+ ),
+ ITEM_IS_WORTHLESS_PHRASE = list(
+ "This item seems to be worthless on a closer look, I won't buy this.",
+ ),
+ TRADER_HAS_ENOUGH_ITEM_PHRASE = list(
+ "I already bought enough of this for the time being.",
+ ),
+ TRADER_LORE_PHRASE = list(
+ "Hello, I am Mr. Bones!",
+ "The ride never ends!",
+ "I'd really like a refreshing carton of milk!",
+ "I'm willing to play big prices for BONES! Need materials to make merch, eh?",
+ "It's a beautiful day outside. Birds are singing, Flowers are blooming... On days like these, kids like you... Should be buying my wares!",
+ ),
+ TRADER_NOT_BUYING_ANYTHING = list(
+ "I'm currently buying nothing at the moment.",
+ ),
+ TRADER_NOT_SELLING_ANYTHING = list(
+ "I'm currently selling nothing at the moment.",
+ ),
+ TRADER_BATTLE_START_PHRASE = list(
+ "The ride ends for you!",
+ ),
+ TRADER_BATTLE_END_PHRASE = list(
+ "Mr. Bones never misses!",
+ ),
+ TRADER_SHOP_OPENING_PHRASE = list(
+ "My wild ride is open!",
+ ),
+ )
diff --git a/code/modules/mob/living/basic/trader/trader_items.dm b/code/modules/mob/living/basic/trader/trader_items.dm
new file mode 100644
index 0000000000000..173e33d4c2f62
--- /dev/null
+++ b/code/modules/mob/living/basic/trader/trader_items.dm
@@ -0,0 +1,36 @@
+///Sale signs
+/obj/structure/trader_sign
+ name = "holographic store sign"
+ desc = "A holographic sign that promises great deals."
+ icon = 'icons/obj/trader_signs.dmi'
+ icon_state = "faceless"
+ anchored = TRUE
+ armor_type = /datum/armor/trader_sign
+ max_integrity = 15
+ layer = FLY_LAYER
+
+/datum/armor/trader_sign
+ bullet = 50
+ laser = 50
+ energy = 50
+ fire = 20
+ acid = 20
+
+/obj/structure/trader_sign/Initialize(mapload)
+ . = ..()
+ add_overlay("sign")
+ makeHologram()
+
+
+/obj/structure/trader_sign/mrbones
+ icon_state = "mrbones"
+
+
+///Spawners for outfits
+/obj/effect/mob_spawn/corpse/human/skeleton/mrbones
+ mob_species = /datum/species/skeleton
+ outfit = /datum/outfit/mrbonescorpse
+
+/datum/outfit/mrbonescorpse
+ name = "Mr Bones' Corpse"
+ head = /obj/item/clothing/head/hats/tophat
diff --git a/code/modules/mob/living/basic/trooper/nanotrasen.dm b/code/modules/mob/living/basic/trooper/nanotrasen.dm
new file mode 100644
index 0000000000000..7dfab5298c208
--- /dev/null
+++ b/code/modules/mob/living/basic/trooper/nanotrasen.dm
@@ -0,0 +1,97 @@
+/// Nanotrasen Private Security forces
+/mob/living/basic/trooper/nanotrasen
+ name = "\improper Nanotrasen Private Security Officer"
+ desc = "An officer of Nanotrasen's private security force. Seems rather unpleased to meet you."
+ speed = 0
+ melee_damage_lower = 10
+ melee_damage_upper = 15
+ faction = list(ROLE_DEATHSQUAD)
+ loot = list(/obj/effect/mob_spawn/corpse/human/nanotrasensoldier)
+ mob_spawner = /obj/effect/mob_spawn/corpse/human/nanotrasensoldier
+
+/// A variant that calls for reinforcements on spotting a target
+/mob/living/basic/trooper/nanotrasen/screaming
+ ai_controller = /datum/ai_controller/basic_controller/trooper/calls_reinforcements
+
+/mob/living/basic/trooper/nanotrasen/ranged
+ ai_controller = /datum/ai_controller/basic_controller/trooper/ranged
+ r_hand = /obj/item/gun/ballistic/automatic/pistol/m1911
+ /// Type of bullet we use
+ var/casingtype = /obj/item/ammo_casing/c45
+ /// Sound to play when firing weapon
+ var/projectilesound = 'sound/weapons/gun/pistol/shot_alt.ogg'
+ /// number of burst shots
+ var/burst_shots
+ /// Time between taking shots
+ var/ranged_cooldown = 1 SECONDS
+
+/mob/living/basic/trooper/nanotrasen/ranged/Initialize(mapload)
+ . = ..()
+ AddComponent(\
+ /datum/component/ranged_attacks,\
+ casing_type = casingtype,\
+ projectile_sound = projectilesound,\
+ cooldown_time = ranged_cooldown,\
+ burst_shots = burst_shots,\
+ )
+
+/mob/living/basic/trooper/nanotrasen/ranged/smg
+ ai_controller = /datum/ai_controller/basic_controller/trooper/ranged/burst
+ casingtype = /obj/item/ammo_casing/c46x30mm
+ projectilesound = 'sound/weapons/gun/smg/shot.ogg'
+ r_hand = /obj/item/gun/ballistic/automatic/wt550
+ burst_shots = 3
+ ranged_cooldown = 3 SECONDS
+
+/mob/living/basic/trooper/nanotrasen/ranged/assault
+ name = "Nanotrasen Assault Officer"
+ desc = "Nanotrasen Assault Officer. Contact CentCom if you saw him on your station. Prepare to die, if you've been found near Syndicate property."
+
+ casingtype = /obj/item/ammo_casing/a223/weak
+ burst_shots = 4
+ ranged_cooldown = 3 SECONDS
+ projectilesound = 'sound/weapons/gun/smg/shot.ogg'
+ r_hand = /obj/item/gun/ballistic/automatic/ar
+ loot = list(/obj/effect/mob_spawn/corpse/human/nanotrasenassaultsoldier)
+ mob_spawner = /obj/effect/mob_spawn/corpse/human/nanotrasenassaultsoldier
+
+/mob/living/basic/trooper/nanotrasen/ranged/elite
+ name = "Nanotrasen Elite Assault Officer"
+ desc = "Pray for your life, syndicate. Run while you can."
+ maxHealth = 150
+ health = 150
+ habitable_atmos = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
+ unsuitable_cold_damage = 0
+ casingtype = /obj/item/ammo_casing/energy/laser
+ burst_shots = 3
+ projectilesound = 'sound/weapons/laser.ogg'
+ ranged_cooldown = 5 SECONDS
+ faction = list(ROLE_DEATHSQUAD)
+ loot = list(/obj/effect/gibspawner/human)
+ mob_spawner = /obj/effect/mob_spawn/corpse/human/nanotrasenelitesoldier
+ r_hand = /obj/item/gun/energy/pulse/carbine/lethal
+
+/// A more peaceful variant that will only attack when attacked, or when another Nanotrasen officer calls for help.
+/mob/living/basic/trooper/nanotrasen/peaceful
+ desc = "An officer of Nanotrasen's private security force."
+ ai_controller = /datum/ai_controller/basic_controller/trooper/peaceful
+
+/mob/living/basic/trooper/nanotrasen/peaceful/Initialize(mapload)
+ . = ..()
+ var/datum/callback/retaliate_callback = CALLBACK(src, PROC_REF(ai_retaliate_behaviour))
+ AddComponent(/datum/component/ai_retaliate_advanced, retaliate_callback)
+
+/mob/living/basic/trooper/nanotrasen/ranged/smg/peaceful
+ desc = "An officer of Nanotrasen's private security force."
+ ai_controller = /datum/ai_controller/basic_controller/trooper/ranged/burst/peaceful
+
+/mob/living/basic/trooper/nanotrasen/ranged/smg/peaceful/Initialize(mapload)
+ . = ..()
+ var/datum/callback/retaliate_callback = CALLBACK(src, PROC_REF(ai_retaliate_behaviour))
+ AddComponent(/datum/component/ai_retaliate_advanced, retaliate_callback)
+
+/mob/living/basic/trooper/nanotrasen/proc/ai_retaliate_behaviour(mob/living/attacker)
+ if (!istype(attacker))
+ return
+ for (var/mob/living/basic/trooper/nanotrasen/potential_trooper in oview(src, 7))
+ potential_trooper.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
diff --git a/code/modules/mob/living/basic/trooper/pirate.dm b/code/modules/mob/living/basic/trooper/pirate.dm
new file mode 100644
index 0000000000000..714fc53856e23
--- /dev/null
+++ b/code/modules/mob/living/basic/trooper/pirate.dm
@@ -0,0 +1,89 @@
+/// Pirate trooper subtype
+/mob/living/basic/trooper/pirate
+ name = "Pirate"
+ desc = "Does what he wants cause a pirate is free."
+ response_help_continuous = "pushes"
+ response_help_simple = "push"
+ speed = 0
+ speak_emote = list("yarrs")
+ faction = list(FACTION_PIRATE)
+ loot = list(/obj/effect/mob_spawn/corpse/human/pirate)
+ mob_spawner = /obj/effect/mob_spawn/corpse/human/pirate
+
+ /// The amount of money to steal with a melee attack
+ var/plunder_credits = 25
+
+/mob/living/basic/trooper/pirate/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/plundering_attacks, plunder_amount = plunder_credits)
+
+/mob/living/basic/trooper/pirate/melee
+ name = "Pirate Swashbuckler"
+ melee_damage_lower = 30
+ melee_damage_upper = 30
+ armour_penetration = 35
+ attack_verb_continuous = "slashes"
+ attack_verb_simple = "slash"
+ attack_sound = 'sound/weapons/blade1.ogg'
+ attack_vis_effect = ATTACK_EFFECT_SLASH
+ loot = list(/obj/effect/mob_spawn/corpse/human/pirate/melee)
+ light_range = 2
+ light_power = 2.5
+ light_color = COLOR_SOFT_RED
+ loot = list(
+ /obj/effect/mob_spawn/corpse/human/pirate/melee,
+ /obj/item/melee/energy/sword/pirate,
+ )
+ mob_spawner = /obj/effect/mob_spawn/corpse/human/pirate/melee
+ r_hand = /obj/item/melee/energy/sword/pirate
+ plunder_credits = 50 //they hit hard so they steal more
+
+/mob/living/basic/trooper/pirate/melee/space
+ name = "Space Pirate Swashbuckler"
+ unsuitable_atmos_damage = 0
+ minimum_survivable_temperature = 0
+ speed = 1
+ loot = list(/obj/effect/mob_spawn/corpse/human/pirate/melee/space)
+ mob_spawner = /obj/effect/mob_spawn/corpse/human/pirate/melee/space
+
+/mob/living/basic/trooper/pirate/melee/space/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
+
+/mob/living/basic/trooper/pirate/ranged
+ name = "Pirate Gunner"
+ loot = list(/obj/effect/mob_spawn/corpse/human/pirate/ranged)
+ mob_spawner = /obj/effect/mob_spawn/corpse/human/pirate/ranged
+ r_hand = /obj/item/gun/energy/laser
+ ai_controller = /datum/ai_controller/basic_controller/trooper/ranged
+ /// Type of bullet we use
+ var/casingtype = /obj/item/ammo_casing/energy/laser
+ /// Sound to play when firing weapon
+ var/projectilesound = 'sound/weapons/laser.ogg'
+ /// number of burst shots
+ var/burst_shots = 2
+ /// Time between taking shots
+ var/ranged_cooldown = 6 SECONDS
+
+/mob/living/basic/trooper/pirate/ranged/Initialize(mapload)
+ . = ..()
+ AddComponent(\
+ /datum/component/ranged_attacks,\
+ casing_type = casingtype,\
+ projectile_sound = projectilesound,\
+ cooldown_time = ranged_cooldown,\
+ burst_shots = burst_shots,\
+ )
+
+/mob/living/basic/trooper/pirate/ranged/space
+ name = "Space Pirate Gunner"
+ unsuitable_atmos_damage = 0
+ minimum_survivable_temperature = 0
+ speed = 1
+ loot = list(/obj/effect/mob_spawn/corpse/human/pirate/ranged/space)
+ mob_spawner = /obj/effect/mob_spawn/corpse/human/pirate/ranged/space
+ r_hand = /obj/item/gun/energy/e_gun/lethal
+
+/mob/living/basic/trooper/pirate/ranged/space/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
diff --git a/code/modules/mob/living/basic/syndicate/russian.dm b/code/modules/mob/living/basic/trooper/russian.dm
similarity index 66%
rename from code/modules/mob/living/basic/syndicate/russian.dm
rename to code/modules/mob/living/basic/trooper/russian.dm
index 2de74b9b4d58a..6e5e34d16b920 100644
--- a/code/modules/mob/living/basic/syndicate/russian.dm
+++ b/code/modules/mob/living/basic/trooper/russian.dm
@@ -1,8 +1,5 @@
-/**
- * Russian subtype of Syndicate troops
- * We're a subtype because we are nearly the same mob with a different Faction.
- */
-/mob/living/basic/syndicate/russian
+/// Russian trooper subtype
+/mob/living/basic/trooper/russian
name = "Russian Mobster"
desc = "For the Motherland!"
speed = 0
@@ -11,6 +8,10 @@
unsuitable_cold_damage = 1
unsuitable_heat_damage = 1
faction = list(FACTION_RUSSIAN)
+ attack_verb_continuous = "slashes"
+ attack_verb_simple = "slash"
+ attack_sound = 'sound/weapons/bladeslice.ogg'
+ attack_vis_effect = ATTACK_EFFECT_SLASH
mob_spawner = /obj/effect/mob_spawn/corpse/human/russian
r_hand = /obj/item/knife/kitchen
@@ -19,8 +20,8 @@
/obj/item/knife/kitchen,
)
-/mob/living/basic/syndicate/russian/ranged
- ai_controller = /datum/ai_controller/basic_controller/syndicate/ranged
+/mob/living/basic/trooper/russian/ranged
+ ai_controller = /datum/ai_controller/basic_controller/trooper/ranged
mob_spawner = /obj/effect/mob_spawn/corpse/human/russian/ranged
r_hand = /obj/item/gun/ballistic/automatic/pistol
loot = list(
@@ -30,9 +31,9 @@
var/casingtype = /obj/item/ammo_casing/n762
var/projectilesound = 'sound/weapons/gun/revolver/shot.ogg'
-/mob/living/basic/syndicate/russian/ranged/Initialize(mapload)
+/mob/living/basic/trooper/russian/ranged/Initialize(mapload)
. = ..()
AddComponent(/datum/component/ranged_attacks, casing_type = casingtype, projectile_sound = projectilesound, cooldown_time = 1 SECONDS)
-/mob/living/basic/syndicate/russian/ranged/lootless
+/mob/living/basic/trooper/russian/ranged/lootless
loot = list()
diff --git a/code/modules/mob/living/basic/syndicate/syndicate.dm b/code/modules/mob/living/basic/trooper/syndicate.dm
similarity index 65%
rename from code/modules/mob/living/basic/syndicate/syndicate.dm
rename to code/modules/mob/living/basic/trooper/syndicate.dm
index a4fd0981198de..7075e293add78 100644
--- a/code/modules/mob/living/basic/syndicate/syndicate.dm
+++ b/code/modules/mob/living/basic/trooper/syndicate.dm
@@ -1,45 +1,13 @@
-///////////////Base Mob////////////
-
-/mob/living/basic/syndicate
+/// Syndicate troopers
+/mob/living/basic/trooper/syndicate
name = "Syndicate Operative"
desc = "Death to Nanotrasen."
- icon = 'icons/mob/simple/simple_human.dmi'
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- sentience_type = SENTIENCE_HUMANOID
- maxHealth = 100
- health = 100
- basic_mob_flags = DEL_ON_DEATH
speed = 1.1
- melee_damage_lower = 10
- melee_damage_upper = 10
- attack_verb_continuous = "punches"
- attack_verb_simple = "punch"
- attack_sound = 'sound/weapons/punch1.ogg'
- melee_attack_cooldown = 1.2 SECONDS
- combat_mode = TRUE
- unsuitable_atmos_damage = 7.5
- unsuitable_cold_damage = 7.5
- unsuitable_heat_damage = 7.5
faction = list(ROLE_SYNDICATE)
- ai_controller = /datum/ai_controller/basic_controller/syndicate
- /// Loot this mob drops on death.
- var/loot = list(/obj/effect/mob_spawn/corpse/human/syndicatesoldier)
- /// Path of the mob spawner we base the mob's visuals off of.
- var/mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatesoldier
- /// Path of the right hand held item we give to the mob's visuals.
- var/r_hand
- /// Path of the left hand held item we give to the mob's visuals.
- var/l_hand
-
-/mob/living/basic/syndicate/Initialize(mapload)
- . = ..()
- apply_dynamic_human_appearance(src, mob_spawn_path = mob_spawner, r_hand = r_hand, l_hand = l_hand)
- if(LAZYLEN(loot))
- loot = string_list(loot)
- AddElement(/datum/element/death_drops, loot)
- AddElement(/datum/element/footstep, footstep_type = FOOTSTEP_MOB_SHOE)
+ loot = list(/obj/effect/mob_spawn/corpse/human/syndicatesoldier)
+ mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatesoldier
-/mob/living/basic/syndicate/space
+/mob/living/basic/trooper/syndicate/space
name = "Syndicate Commando"
maxHealth = 170
health = 170
@@ -48,18 +16,18 @@
minimum_survivable_temperature = 0
mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatecommando
-/mob/living/basic/syndicate/space/Initialize(mapload)
+/mob/living/basic/trooper/syndicate/space/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
set_light(4)
-/mob/living/basic/syndicate/space/stormtrooper
+/mob/living/basic/trooper/syndicate/space/stormtrooper
name = "Syndicate Stormtrooper"
maxHealth = 250
health = 250
mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatestormtrooper
-/mob/living/basic/syndicate/melee //dude with a knife and no shields
+/mob/living/basic/trooper/syndicate/melee //dude with a knife and no shields
melee_damage_lower = 15
melee_damage_upper = 15
loot = list(/obj/effect/gibspawner/human)
@@ -70,13 +38,13 @@
r_hand = /obj/item/knife/combat/survival
var/projectile_deflect_chance = 0
-/mob/living/basic/syndicate/melee/bullet_act(obj/projectile/projectile)
+/mob/living/basic/trooper/syndicate/melee/bullet_act(obj/projectile/projectile)
if(prob(projectile_deflect_chance))
visible_message(span_danger("[src] blocks [projectile] with its shield!"))
return BULLET_ACT_BLOCK
return ..()
-/mob/living/basic/syndicate/melee/space
+/mob/living/basic/trooper/syndicate/melee/space
name = "Syndicate Commando"
maxHealth = 170
health = 170
@@ -84,18 +52,18 @@
minimum_survivable_temperature = 0
mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatecommando
-/mob/living/basic/syndicate/melee/space/Initialize(mapload)
+/mob/living/basic/trooper/syndicate/melee/space/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
set_light(4)
-/mob/living/basic/syndicate/melee/space/stormtrooper
+/mob/living/basic/trooper/syndicate/melee/space/stormtrooper
name = "Syndicate Stormtrooper"
maxHealth = 250
health = 250
mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatestormtrooper
-/mob/living/basic/syndicate/melee/sword
+/mob/living/basic/trooper/syndicate/melee/sword
melee_damage_lower = 30
melee_damage_upper = 30
attack_verb_continuous = "slashes"
@@ -109,7 +77,7 @@
r_hand = /obj/item/melee/energy/sword/saber/red
l_hand = /obj/item/shield/energy
-/mob/living/basic/syndicate/melee/sword/space
+/mob/living/basic/trooper/syndicate/melee/sword/space
name = "Syndicate Commando"
maxHealth = 170
health = 170
@@ -118,11 +86,11 @@
projectile_deflect_chance = 50
mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatecommando
-/mob/living/basic/syndicate/melee/sword/space/Initialize(mapload)
+/mob/living/basic/trooper/syndicate/melee/sword/space/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
-/mob/living/basic/syndicate/melee/sword/space/stormtrooper
+/mob/living/basic/trooper/syndicate/melee/sword/space/stormtrooper
name = "Syndicate Stormtrooper"
maxHealth = 250
health = 250
@@ -131,9 +99,9 @@
///////////////Guns////////////
-/mob/living/basic/syndicate/ranged
+/mob/living/basic/trooper/syndicate/ranged
loot = list(/obj/effect/gibspawner/human)
- ai_controller = /datum/ai_controller/basic_controller/syndicate/ranged
+ ai_controller = /datum/ai_controller/basic_controller/trooper/ranged
r_hand = /obj/item/gun/ballistic/automatic/pistol
/// Type of bullet we use
var/casingtype = /obj/item/ammo_casing/c9mm
@@ -144,7 +112,7 @@
/// Time between taking shots
var/ranged_cooldown = 1 SECONDS
-/mob/living/basic/syndicate/ranged/Initialize(mapload)
+/mob/living/basic/trooper/syndicate/ranged/Initialize(mapload)
. = ..()
AddComponent(\
/datum/component/ranged_attacks,\
@@ -154,11 +122,11 @@
burst_shots = burst_shots,\
)
-/mob/living/basic/syndicate/ranged/infiltrator //shuttle loan event
+/mob/living/basic/trooper/syndicate/ranged/infiltrator //shuttle loan event
projectilesound = 'sound/weapons/gun/smg/shot_suppressed.ogg'
loot = list(/obj/effect/mob_spawn/corpse/human/syndicatesoldier)
-/mob/living/basic/syndicate/ranged/space
+/mob/living/basic/trooper/syndicate/ranged/space
name = "Syndicate Commando"
maxHealth = 170
health = 170
@@ -166,31 +134,31 @@
minimum_survivable_temperature = 0
mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatecommando
-/mob/living/basic/syndicate/ranged/space/Initialize(mapload)
+/mob/living/basic/trooper/syndicate/ranged/space/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
set_light(4)
-/mob/living/basic/syndicate/ranged/space/stormtrooper
+/mob/living/basic/trooper/syndicate/ranged/space/stormtrooper
name = "Syndicate Stormtrooper"
maxHealth = 250
health = 250
mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatestormtrooper
-/mob/living/basic/syndicate/ranged/smg
+/mob/living/basic/trooper/syndicate/ranged/smg
casingtype = /obj/item/ammo_casing/c45
projectilesound = 'sound/weapons/gun/smg/shot.ogg'
- ai_controller = /datum/ai_controller/basic_controller/syndicate/ranged/burst
+ ai_controller = /datum/ai_controller/basic_controller/trooper/ranged/burst
burst_shots = 3
ranged_cooldown = 3 SECONDS
r_hand = /obj/item/gun/ballistic/automatic/c20r
-/mob/living/basic/syndicate/ranged/smg/pilot //caravan ambush ruin
+/mob/living/basic/trooper/syndicate/ranged/smg/pilot //caravan ambush ruin
name = "Syndicate Salvage Pilot"
loot = list(/obj/effect/mob_spawn/corpse/human/syndicatepilot)
mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatepilot
-/mob/living/basic/syndicate/ranged/smg/space
+/mob/living/basic/trooper/syndicate/ranged/smg/space
name = "Syndicate Commando"
maxHealth = 170
health = 170
@@ -198,25 +166,25 @@
minimum_survivable_temperature = 0
mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatecommando
-/mob/living/basic/syndicate/ranged/smg/space/Initialize(mapload)
+/mob/living/basic/trooper/syndicate/ranged/smg/space/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
set_light(4)
-/mob/living/basic/syndicate/ranged/smg/space/stormtrooper
+/mob/living/basic/trooper/syndicate/ranged/smg/space/stormtrooper
name = "Syndicate Stormtrooper"
maxHealth = 250
health = 250
mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatestormtrooper
-/mob/living/basic/syndicate/ranged/shotgun
+/mob/living/basic/trooper/syndicate/ranged/shotgun
casingtype = /obj/item/ammo_casing/shotgun/buckshot //buckshot (up to 72.5 brute) fired in a two-round burst
- ai_controller = /datum/ai_controller/basic_controller/syndicate/ranged/shotgunner
+ ai_controller = /datum/ai_controller/basic_controller/trooper/ranged/shotgunner
ranged_cooldown = 3 SECONDS
burst_shots = 2
r_hand = /obj/item/gun/ballistic/shotgun/bulldog
-/mob/living/basic/syndicate/ranged/shotgun/space
+/mob/living/basic/trooper/syndicate/ranged/shotgun/space
name = "Syndicate Commando"
maxHealth = 170
health = 170
@@ -225,12 +193,12 @@
speed = 1
mob_spawner = /obj/effect/mob_spawn/corpse/human/syndicatecommando
-/mob/living/basic/syndicate/ranged/shotgun/space/Initialize(mapload)
+/mob/living/basic/trooper/syndicate/ranged/shotgun/space/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
set_light(4)
-/mob/living/basic/syndicate/ranged/shotgun/space/stormtrooper
+/mob/living/basic/trooper/syndicate/ranged/shotgun/space/stormtrooper
name = "Syndicate Stormtrooper"
maxHealth = 250
health = 250
@@ -270,7 +238,7 @@
bubble_icon = "syndibot"
gold_core_spawnable = HOSTILE_SPAWN
death_message = "is smashed into pieces!"
- ai_controller = /datum/ai_controller/basic_controller/syndicate/viscerator
+ ai_controller = /datum/ai_controller/basic_controller/trooper/viscerator
/mob/living/basic/viscerator/Initialize(mapload)
. = ..()
diff --git a/code/modules/mob/living/basic/trooper/trooper.dm b/code/modules/mob/living/basic/trooper/trooper.dm
new file mode 100644
index 0000000000000..1886c8fc2ff5e
--- /dev/null
+++ b/code/modules/mob/living/basic/trooper/trooper.dm
@@ -0,0 +1,36 @@
+/mob/living/basic/trooper
+ icon = 'icons/mob/simple/simple_human.dmi'
+ mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+ sentience_type = SENTIENCE_HUMANOID
+ maxHealth = 100
+ health = 100
+ basic_mob_flags = DEL_ON_DEATH
+ speed = 1.1
+ melee_damage_lower = 10
+ melee_damage_upper = 10
+ attack_verb_continuous = "punches"
+ attack_verb_simple = "punch"
+ attack_sound = 'sound/weapons/punch1.ogg'
+ melee_attack_cooldown = 1.2 SECONDS
+ combat_mode = TRUE
+ unsuitable_atmos_damage = 7.5
+ unsuitable_cold_damage = 7.5
+ unsuitable_heat_damage = 7.5
+ ai_controller = /datum/ai_controller/basic_controller/trooper
+
+ /// Loot this mob drops on death.
+ var/loot = list(/obj/effect/mob_spawn/corpse/human)
+ /// Path of the mob spawner we base the mob's visuals off of.
+ var/mob_spawner = /obj/effect/mob_spawn/corpse/human
+ /// Path of the right hand held item we give to the mob's visuals.
+ var/r_hand
+ /// Path of the left hand held item we give to the mob's visuals.
+ var/l_hand
+
+/mob/living/basic/trooper/Initialize(mapload)
+ . = ..()
+ apply_dynamic_human_appearance(src, mob_spawn_path = mob_spawner, r_hand = r_hand, l_hand = l_hand)
+ if(LAZYLEN(loot))
+ loot = string_list(loot)
+ AddElement(/datum/element/death_drops, loot)
+ AddElement(/datum/element/footstep, footstep_type = FOOTSTEP_MOB_SHOE)
diff --git a/code/modules/mob/living/basic/trooper/trooper_ai.dm b/code/modules/mob/living/basic/trooper/trooper_ai.dm
new file mode 100644
index 0000000000000..3b89807ea62c2
--- /dev/null
+++ b/code/modules/mob/living/basic/trooper/trooper_ai.dm
@@ -0,0 +1,99 @@
+/datum/ai_controller/basic_controller/trooper
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ BB_REINFORCEMENTS_SAY = "411 in progress, requesting backup!"
+ )
+
+ 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/trooper,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target/reinforce,
+ )
+
+/datum/ai_planning_subtree/basic_melee_attack_subtree/trooper
+ melee_attack_behavior = /datum/ai_behavior/basic_melee_attack
+
+/datum/ai_planning_subtree/attack_obstacle_in_path/trooper
+ attack_behaviour = /datum/ai_behavior/attack_obstructions/trooper
+
+/datum/ai_behavior/attack_obstructions/trooper
+ action_cooldown = 1.2 SECONDS
+
+/datum/ai_controller/basic_controller/trooper/calls_reinforcements
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/call_reinforcements,
+ /datum/ai_planning_subtree/attack_obstacle_in_path/trooper,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target/reinforce,
+ )
+
+/datum/ai_controller/basic_controller/trooper/peaceful
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/call_reinforcements,
+ /datum/ai_planning_subtree/attack_obstacle_in_path/trooper,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target/reinforce,
+ )
+
+/datum/ai_controller/basic_controller/trooper/ranged
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_ranged_attack_subtree/trooper,
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target/reinforce,
+ )
+
+/datum/ai_planning_subtree/basic_ranged_attack_subtree/trooper
+ ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack/trooper
+
+/datum/ai_behavior/basic_ranged_attack/trooper
+ action_cooldown = 1 SECONDS
+ required_distance = 5
+ avoid_friendly_fire = TRUE
+
+/datum/ai_controller/basic_controller/trooper/ranged/burst
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_ranged_attack_subtree/trooper_burst,
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target/reinforce,
+ )
+
+/datum/ai_planning_subtree/basic_ranged_attack_subtree/trooper_burst
+ ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack/trooper_burst
+
+/datum/ai_behavior/basic_ranged_attack/trooper_burst
+ action_cooldown = 3 SECONDS
+ avoid_friendly_fire = TRUE
+
+/datum/ai_controller/basic_controller/trooper/ranged/burst/peaceful
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/call_reinforcements,
+ /datum/ai_planning_subtree/basic_ranged_attack_subtree/trooper_burst,
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target/reinforce,
+ )
+
+/datum/ai_controller/basic_controller/trooper/ranged/shotgunner
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_ranged_attack_subtree/trooper_shotgun,
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target/reinforce,
+ )
+
+/datum/ai_planning_subtree/basic_ranged_attack_subtree/trooper_shotgun
+ ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack/trooper_shotgun
+
+/datum/ai_behavior/basic_ranged_attack/trooper_shotgun
+ action_cooldown = 3 SECONDS
+ required_distance = 1
+ avoid_friendly_fire = TRUE
+
+/datum/ai_controller/basic_controller/trooper/viscerator
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ )
diff --git a/code/modules/mob/living/basic/vermin/cockroach.dm b/code/modules/mob/living/basic/vermin/cockroach.dm
index 5c69ad904474f..639a9720dbce3 100644
--- a/code/modules/mob/living/basic/vermin/cockroach.dm
+++ b/code/modules/mob/living/basic/vermin/cockroach.dm
@@ -61,7 +61,7 @@
/datum/ai_controller/basic_controller/cockroach
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(),
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends(),
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends,
)
ai_traits = STOP_MOVING_WHEN_PULLED
diff --git a/code/modules/mob/living/basic/vermin/crab.dm b/code/modules/mob/living/basic/vermin/crab.dm
index bb81fd29c4d50..276ed86d4de62 100644
--- a/code/modules/mob/living/basic/vermin/crab.dm
+++ b/code/modules/mob/living/basic/vermin/crab.dm
@@ -92,4 +92,5 @@
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/random_speech/crab,
+ /datum/ai_planning_subtree/go_for_swim,
)
diff --git a/code/modules/mob/living/basic/vermin/frog.dm b/code/modules/mob/living/basic/vermin/frog.dm
index 191ea12b4df33..1ffe12e063ba5 100644
--- a/code/modules/mob/living/basic/vermin/frog.dm
+++ b/code/modules/mob/living/basic/vermin/frog.dm
@@ -75,10 +75,27 @@
if(L.mob_size > MOB_SIZE_TINY)
playsound(src, stepped_sound, 50, TRUE)
+/mob/living/basic/frog/frog_suicide
+ name = "suicide frog"
+ desc = "Driven by sheer will."
+ icon_state = "frog_trash"
+ icon_living = "frog_trash"
+ icon_dead = "frog_trash_dead"
+ maxHealth = 5
+ health = 5
+ ai_controller = /datum/ai_controller/basic_controller/frog/suicide_frog
+ ///how long do we exist for
+ var/existence_period = 15 SECONDS
+
+/mob/living/basic/frog/frog_suicide/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/explode_on_attack, mob_type_dont_bomb = typecacheof(list(/mob/living/basic/frog, /mob/living/basic/leaper)))
+ addtimer(CALLBACK(src, PROC_REF(death)), existence_period)
+
/datum/ai_controller/basic_controller/frog
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(),
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends(),
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends
)
ai_movement = /datum/ai_movement/basic_avoidance
@@ -87,6 +104,7 @@
/datum/ai_planning_subtree/target_retaliate,
/datum/ai_planning_subtree/random_speech/frog,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/go_for_swim,
)
/datum/ai_controller/basic_controller/frog/trash
@@ -96,3 +114,9 @@
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
+
+/datum/ai_controller/basic_controller/frog/suicide_frog
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
diff --git a/code/modules/mob/living/basic/vermin/lizard.dm b/code/modules/mob/living/basic/vermin/lizard.dm
index ab73224c6aca3..d1a30826f4ac4 100644
--- a/code/modules/mob/living/basic/vermin/lizard.dm
+++ b/code/modules/mob/living/basic/vermin/lizard.dm
@@ -50,7 +50,7 @@
. = ..()
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
AddElement(/datum/element/pet_bonus, "sticks its tongue out contentedly!")
- AddElement(/datum/element/basic_eating, 5, 0, null, edibles)
+ AddElement(/datum/element/basic_eating, heal_amt = 5, food_types = edibles)
ai_controller.set_blackboard_key(BB_BASIC_FOODS, edibles)
/datum/ai_controller/basic_controller/lizard
diff --git a/code/modules/mob/living/basic/vermin/mouse.dm b/code/modules/mob/living/basic/vermin/mouse.dm
index 87d549ac5234e..2929085fb939f 100644
--- a/code/modules/mob/living/basic/vermin/mouse.dm
+++ b/code/modules/mob/living/basic/vermin/mouse.dm
@@ -72,7 +72,7 @@
/mob/living/basic/mouse/examine(mob/user)
. = ..()
- var/sameside = user.faction_check_mob(src, exact_match = TRUE)
+ var/sameside = user.faction_check_atom(src, exact_match = TRUE)
if(isregalrat(user))
if(sameside)
. += span_notice("This rat serves under you.")
@@ -411,8 +411,8 @@
/// AI controller for rats, slightly more complex than mice becuase they attack people
/datum/ai_controller/basic_controller/mouse/rat
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(),
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends(),
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends,
BB_BASIC_MOB_CURRENT_TARGET = null, // heathen
BB_CURRENT_HUNTING_TARGET = null, // cheese
BB_LOW_PRIORITY_HUNTING_TARGET = null, // cable
diff --git a/code/modules/mob/living/brain/posibrain.dm b/code/modules/mob/living/brain/posibrain.dm
index b9080a8673a38..1ba926a210e0c 100644
--- a/code/modules/mob/living/brain/posibrain.dm
+++ b/code/modules/mob/living/brain/posibrain.dm
@@ -40,16 +40,18 @@ GLOBAL_VAR(posibrain_notify_cooldown)
///List of all ckeys who has already entered this posibrain once before.
var/list/ckeys_entered = list()
-/obj/item/mmi/posibrain/Topic(href, href_list)
- if(href_list["activate"])
- var/mob/dead/observer/ghost = usr
- if(istype(ghost))
- activate(ghost)
-
///Notify ghosts that the posibrain is up for grabs
/obj/item/mmi/posibrain/proc/ping_ghosts(msg, newlymade)
if(newlymade || GLOB.posibrain_notify_cooldown <= world.time)
- notify_ghosts("[name] [msg] in [get_area(src)]! [ask_role ? "Personality requested: \[[ask_role]\]" : ""]", ghost_sound = !newlymade ? 'sound/effects/ghost2.ogg':null, notify_volume = 75, enter_link = "(Click to enter)", source = src, action = NOTIFY_ATTACK, flashwindow = FALSE, ignore_key = POLL_IGNORE_POSIBRAIN, notify_suiciders = FALSE)
+ notify_ghosts(
+ "[name] [msg] in [get_area(src)]! [ask_role ? "Personality requested: \[[ask_role]\]" : ""]",
+ ghost_sound = !newlymade ? 'sound/effects/ghost2.ogg':null,
+ notify_volume = 75,
+ source = src,
+ action = NOTIFY_PLAY,
+ notify_flags = (GHOST_NOTIFY_IGNORE_MAPLOAD),
+ ignore_key = POLL_IGNORE_POSIBRAIN,
+ )
if(!newlymade)
GLOB.posibrain_notify_cooldown = world.time + ask_delay
diff --git a/code/modules/mob/living/carbon/alien/adult/caste/drone.dm b/code/modules/mob/living/carbon/alien/adult/caste/drone.dm
index 3a1843dd93c23..ff208baabd229 100644
--- a/code/modules/mob/living/carbon/alien/adult/caste/drone.dm
+++ b/code/modules/mob/living/carbon/alien/adult/caste/drone.dm
@@ -6,8 +6,7 @@
icon_state = "aliend"
/mob/living/carbon/alien/adult/drone/Initialize(mapload)
- var/datum/action/cooldown/alien/evolve_to_praetorian/evolution = new(src)
- evolution.Grant(src)
+ GRANT_ACTION(/datum/action/cooldown/alien/evolve_to_praetorian)
return ..()
/mob/living/carbon/alien/adult/drone/create_internal_organs()
diff --git a/code/modules/mob/living/carbon/alien/adult/caste/praetorian.dm b/code/modules/mob/living/carbon/alien/adult/caste/praetorian.dm
index a26eb31231c03..8fa142a38f05f 100644
--- a/code/modules/mob/living/carbon/alien/adult/caste/praetorian.dm
+++ b/code/modules/mob/living/carbon/alien/adult/caste/praetorian.dm
@@ -9,11 +9,12 @@
/mob/living/carbon/alien/adult/royal/praetorian/Initialize(mapload)
real_name = name
- var/datum/action/cooldown/spell/aoe/repulse/xeno/tail_whip = new(src)
- tail_whip.Grant(src)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/alien/evolve_to_queen,
+ /datum/action/cooldown/spell/aoe/repulse/xeno,
+ )
- var/datum/action/cooldown/alien/evolve_to_queen/evolution = new(src)
- evolution.Grant(src)
+ grant_actions_by_list(innate_actions)
return ..()
diff --git a/code/modules/mob/living/carbon/alien/adult/caste/sentinel.dm b/code/modules/mob/living/carbon/alien/adult/caste/sentinel.dm
index 7fffdf35522c7..bef621905f442 100644
--- a/code/modules/mob/living/carbon/alien/adult/caste/sentinel.dm
+++ b/code/modules/mob/living/carbon/alien/adult/caste/sentinel.dm
@@ -7,8 +7,7 @@
alien_speed = 0.2
/mob/living/carbon/alien/adult/sentinel/Initialize(mapload)
- var/datum/action/cooldown/mob_cooldown/sneak/alien/sneaky_beaky = new(src)
- sneaky_beaky.Grant(src)
+ GRANT_ACTION(/datum/action/cooldown/mob_cooldown/sneak/alien)
return ..()
/mob/living/carbon/alien/adult/sentinel/create_internal_organs()
diff --git a/code/modules/mob/living/carbon/alien/adult/queen.dm b/code/modules/mob/living/carbon/alien/adult/queen.dm
index dd8e61b6699fc..df03ce3fa9eae 100644
--- a/code/modules/mob/living/carbon/alien/adult/queen.dm
+++ b/code/modules/mob/living/carbon/alien/adult/queen.dm
@@ -55,11 +55,11 @@
real_name = src.name
- var/datum/action/cooldown/spell/aoe/repulse/xeno/tail_whip = new(src)
- tail_whip.Grant(src)
-
- var/datum/action/cooldown/alien/promote/promotion = new(src)
- promotion.Grant(src)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/alien/promote,
+ /datum/action/cooldown/spell/aoe/repulse/xeno,
+ )
+ grant_actions_by_list(innate_actions)
return ..()
diff --git a/code/modules/mob/living/carbon/alien/larva/larva.dm b/code/modules/mob/living/carbon/alien/larva/larva.dm
index 0b1396520b321..f4159813ed9bc 100644
--- a/code/modules/mob/living/carbon/alien/larva/larva.dm
+++ b/code/modules/mob/living/carbon/alien/larva/larva.dm
@@ -31,10 +31,12 @@
//This is fine right now, if we're adding organ specific damage this needs to be updated
/mob/living/carbon/alien/larva/Initialize(mapload)
- var/datum/action/cooldown/alien/larva_evolve/evolution = new(src)
- evolution.Grant(src)
- var/datum/action/cooldown/alien/hide/hide = new(src)
- hide.Grant(src)
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/alien/hide,
+ /datum/action/cooldown/alien/larva_evolve,
+ )
+ grant_actions_by_list(innate_actions)
+
return ..()
/mob/living/carbon/alien/larva/create_internal_organs()
diff --git a/code/modules/mob/living/carbon/alien/special/alien_embryo.dm b/code/modules/mob/living/carbon/alien/special/alien_embryo.dm
index 22c528b00953d..eeb33bd3e891f 100644
--- a/code/modules/mob/living/carbon/alien/special/alien_embryo.dm
+++ b/code/modules/mob/living/carbon/alien/special/alien_embryo.dm
@@ -84,26 +84,33 @@
return
attempt_grow()
-///Attempt to burst an alien outside of the host, getting a ghost to play as the xeno.
+/// Attempt to burst an alien outside of the host, getting a ghost to play as the xeno.
/obj/item/organ/internal/body_egg/alien_embryo/proc/attempt_grow(gib_on_success = TRUE)
- if(!owner || bursting)
+ if(QDELETED(owner) || bursting)
return
bursting = TRUE
- var/list/candidates = poll_ghost_candidates("Do you want to play as an alien larva that will burst out of [owner.real_name]?", ROLE_ALIEN, ROLE_ALIEN, 100, POLL_IGNORE_ALIEN_LARVA)
-
- if(QDELETED(src) || QDELETED(owner))
+ var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), gib_on_success)
+ owner.AddComponent(/datum/component/orbit_poll, \
+ ignore_key = POLL_IGNORE_ALIEN_LARVA, \
+ job_bans = ROLE_ALIEN, \
+ to_call = to_call, \
+ custom_message = "An alien is bursting out of [owner.real_name]", \
+ title = "alien larva" \
+ )
+
+/// Poll has concluded with a suitor
+/obj/item/organ/internal/body_egg/alien_embryo/proc/on_poll_concluded(gib_on_success, mob/dead/observer/ghost)
+ if(QDELETED(owner))
return
- if(!candidates.len || !owner)
+ if(isnull(ghost))
bursting = FALSE
stage = 5 // If no ghosts sign up for the Larva, let's regress our growth by one minute, we will try again!
addtimer(CALLBACK(src, PROC_REF(advance_embryo_stage)), growth_time)
return
- var/mob/dead/observer/ghost = pick(candidates)
-
var/mutable_appearance/overlay = mutable_appearance('icons/mob/nonhuman-player/alien.dmi', "burst_lie")
owner.add_overlay(overlay)
@@ -112,7 +119,7 @@
new_xeno.key = ghost.key
SEND_SOUND(new_xeno, sound('sound/voice/hiss5.ogg',0,0,0,100)) //To get the player's attention
new_xeno.add_traits(list(TRAIT_HANDS_BLOCKED, TRAIT_IMMOBILIZED, TRAIT_NO_TRANSFORM), type) //so we don't move during the bursting animation
- new_xeno.invisibility = INVISIBILITY_MAXIMUM
+ new_xeno.SetInvisibility(INVISIBILITY_MAXIMUM, id=type)
sleep(0.6 SECONDS)
@@ -122,7 +129,7 @@
if(!isnull(new_xeno))
new_xeno.remove_traits(list(TRAIT_HANDS_BLOCKED, TRAIT_IMMOBILIZED, TRAIT_NO_TRANSFORM), type)
- new_xeno.invisibility = 0
+ new_xeno.RemoveInvisibility(type)
if(gib_on_success)
new_xeno.visible_message(span_danger("[new_xeno] bursts out of [owner] in a shower of gore!"), span_userdanger("You exit [owner], your previous host."), span_hear("You hear organic matter ripping and tearing!"))
diff --git a/code/modules/mob/living/carbon/alien/special/facehugger.dm b/code/modules/mob/living/carbon/alien/special/facehugger.dm
index ff046dc87b4ee..1c4a19b78d63b 100644
--- a/code/modules/mob/living/carbon/alien/special/facehugger.dm
+++ b/code/modules/mob/living/carbon/alien/special/facehugger.dm
@@ -103,7 +103,7 @@
if(CanHug(AM) && Adjacent(AM))
return Leap(AM)
-/obj/item/clothing/mask/facehugger/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, quickstart = TRUE)
+/obj/item/clothing/mask/facehugger/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, gentle, quickstart = TRUE)
. = ..()
if(!.)
return
diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm
index 7447638fa9265..cb77b0fc86066 100644
--- a/code/modules/mob/living/carbon/carbon_defense.dm
+++ b/code/modules/mob/living/carbon/carbon_defense.dm
@@ -175,10 +175,10 @@
return TRUE
-/mob/living/carbon/attack_drone(mob/living/simple_animal/drone/user)
+/mob/living/carbon/attack_drone(mob/living/basic/drone/user)
return //so we don't call the carbon's attack_hand().
-/mob/living/carbon/attack_drone_secondary(mob/living/simple_animal/drone/user)
+/mob/living/carbon/attack_drone_secondary(mob/living/basic/drone/user)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
//ATTACK HAND IGNORING PARENT RETURN VALUE
@@ -703,15 +703,14 @@
check_passout()
/**
-* Check to see if we should be passed out from oyxloss
+* Check to see if we should be passed out from oxyloss
*/
/mob/living/carbon/proc/check_passout()
- if(!isnum(oxyloss))
- return
- if(oxyloss <= 50)
- if(getOxyLoss() > 50)
+ var/mob_oxyloss = getOxyLoss()
+ if(mob_oxyloss >= 50)
+ if(!HAS_TRAIT_FROM(src, TRAIT_KNOCKEDOUT, OXYLOSS_TRAIT))
ADD_TRAIT(src, TRAIT_KNOCKEDOUT, OXYLOSS_TRAIT)
- else if(getOxyLoss() <= 50)
+ else if(mob_oxyloss < 50)
REMOVE_TRAIT(src, TRAIT_KNOCKEDOUT, OXYLOSS_TRAIT)
/mob/living/carbon/get_organic_health()
diff --git a/code/modules/mob/living/carbon/carbon_defines.dm b/code/modules/mob/living/carbon/carbon_defines.dm
index b22b32583ab01..f68c39771e212 100644
--- a/code/modules/mob/living/carbon/carbon_defines.dm
+++ b/code/modules/mob/living/carbon/carbon_defines.dm
@@ -47,7 +47,7 @@
///only used by humans.
var/obj/item/clothing/ears = null
- /// Carbon
+ /// Carbon, you should really only be accessing this through has_dna() but it's your life
var/datum/dna/dna = null
///last mind to control this mob, for blood-based cloning
var/datum/mind/last_mind = null
@@ -122,6 +122,4 @@
/// A bitfield of "bodytypes", updated by /obj/item/bodypart/proc/synchronize_bodytypes()
var/bodytype = BODYTYPE_HUMANOID | BODYTYPE_ORGANIC
- var/is_leaning = FALSE
-
COOLDOWN_DECLARE(bleeding_message_cd)
diff --git a/code/modules/mob/living/carbon/damage_procs.dm b/code/modules/mob/living/carbon/damage_procs.dm
index 040bf76a3db02..6ecc62b04981d 100644
--- a/code/modules/mob/living/carbon/damage_procs.dm
+++ b/code/modules/mob/living/carbon/damage_procs.dm
@@ -1,44 +1,79 @@
-/mob/living/carbon/apply_damage(damage, damagetype = BRUTE, def_zone = null, blocked = 0, forced = FALSE, spread_damage = FALSE, wound_bonus = 0, bare_wound_bonus = 0, sharpness = NONE, attack_direction = null, attacking_item)
- SEND_SIGNAL(src, COMSIG_MOB_APPLY_DAMAGE, damage, damagetype, def_zone)
- var/hit_percent = (100-blocked)/100
- if(!damage || (!forced && hit_percent <= 0))
- return 0
+/mob/living/carbon/apply_damage(
+ damage = 0,
+ damagetype = BRUTE,
+ def_zone = null,
+ blocked = 0,
+ forced = FALSE,
+ spread_damage = FALSE,
+ wound_bonus = 0,
+ bare_wound_bonus = 0,
+ sharpness = NONE,
+ attack_direction = null,
+ attacking_item,
+)
+ // Spread damage should always have def zone be null
+ if(spread_damage)
+ def_zone = null
+
+ // Otherwise if def zone is null, we'll get a random bodypart / zone to hit.
+ // ALso we'll automatically covnert string def zones into bodyparts to pass into parent call.
+ else if(!isbodypart(def_zone))
+ var/random_zone = check_zone(def_zone || get_random_valid_zone(def_zone))
+ def_zone = get_bodypart(random_zone) || bodyparts[1]
- var/obj/item/bodypart/BP = null
- if(!spread_damage)
- if(isbodypart(def_zone)) //we specified a bodypart object
- BP = def_zone
- else
- if(!def_zone)
- def_zone = get_random_valid_zone(def_zone)
- BP = get_bodypart(check_zone(def_zone))
- if(!BP)
- BP = bodyparts[1]
+ . = ..()
+ // Taking brute or burn to bodyparts gives a damage flash
+ if(def_zone && (damagetype == BRUTE || damagetype == BURN))
+ damageoverlaytemp += .
+
+ return .
+
+/mob/living/carbon/human/apply_damage(
+ damage = 0,
+ damagetype = BRUTE,
+ def_zone = null,
+ blocked = 0,
+ forced = FALSE,
+ spread_damage = FALSE,
+ wound_bonus = 0,
+ bare_wound_bonus = 0,
+ sharpness = NONE,
+ attack_direction = null,
+ attacking_item,
+)
+
+ // Add relevant DR modifiers into blocked value to pass to parent
+ blocked += physiology?.damage_resistance
+ blocked += dna?.species?.damage_modifier
+ return ..()
+
+/mob/living/carbon/human/get_incoming_damage_modifier(
+ damage = 0,
+ damagetype = BRUTE,
+ def_zone = null,
+ sharpness = NONE,
+ attack_direction = null,
+ attacking_item,
+)
+ var/final_mod = ..()
- var/damage_amount = forced ? damage : damage * hit_percent
switch(damagetype)
if(BRUTE)
- if(BP)
- if(BP.receive_damage(damage_amount, 0, wound_bonus = wound_bonus, bare_wound_bonus = bare_wound_bonus, sharpness = sharpness, attack_direction = attack_direction))
- update_damage_overlays()
- else //no bodypart, we deal damage with a more general method.
- adjustBruteLoss(damage_amount, forced = forced)
+ final_mod *= physiology.brute_mod
if(BURN)
- if(BP)
- if(BP.receive_damage(0, damage_amount, wound_bonus = wound_bonus, bare_wound_bonus = bare_wound_bonus, sharpness = sharpness, attack_direction = attack_direction))
- update_damage_overlays()
- else
- adjustFireLoss(damage_amount, forced = forced)
+ final_mod *= physiology.burn_mod
if(TOX)
- adjustToxLoss(damage_amount, forced = forced)
+ final_mod *= physiology.tox_mod
if(OXY)
- adjustOxyLoss(damage_amount, forced = forced)
+ final_mod *= physiology.oxy_mod
if(CLONE)
- adjustCloneLoss(damage_amount, forced = forced)
+ final_mod *= physiology.clone_mod
if(STAMINA)
- adjustStaminaLoss(damage_amount, forced = forced)
- SEND_SIGNAL(src, COMSIG_MOB_AFTER_APPLY_DAMAGE, damage, damagetype, def_zone)
- return TRUE
+ final_mod *= physiology.stamina_mod
+ if(BRAIN)
+ final_mod *= physiology.brain_mod
+
+ return final_mod
//These procs fetch a cumulative total damage from all bodyparts
/mob/living/carbon/getBruteLoss()
diff --git a/code/modules/mob/living/carbon/death.dm b/code/modules/mob/living/carbon/death.dm
index bbf82ccefc56a..78b8554361b03 100644
--- a/code/modules/mob/living/carbon/death.dm
+++ b/code/modules/mob/living/carbon/death.dm
@@ -19,7 +19,7 @@
BT.on_death()
/mob/living/carbon/proc/inflate_gib() // Plays an animation that makes mobs appear to inflate before finally gibbing
- addtimer(CALLBACK(src, PROC_REF(gib), null, null, TRUE, TRUE), 25)
+ addtimer(CALLBACK(src, PROC_REF(gib), DROP_BRAIN|DROP_ORGANS|DROP_ITEMS), 25)
var/matrix/M = matrix()
M.Scale(1.8, 1.2)
animate(src, time = 40, transform = M, easing = SINE_EASING)
diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm
index 5cf1a02a9aabb..f4789613064fb 100644
--- a/code/modules/mob/living/carbon/human/_species.dm
+++ b/code/modules/mob/living/carbon/human/_species.dm
@@ -99,10 +99,8 @@ GLOBAL_LIST_EMPTY(features_by_species)
///Replaces default appendix with a different organ.
var/obj/item/organ/internal/appendix/mutantappendix = /obj/item/organ/internal/appendix
- /**
- * Percentage modifier for overall defense of the race, or less defense, if it's negative
- * THIS MODIFIES ALL DAMAGE TYPES.
- **/
+ /// Flat modifier on all damage taken via [apply_damage][/mob/living/proc/apply_damage] (so being punched, shot, etc.)
+ /// IE: 10 = 10% less damage taken.
var/damage_modifier = 0
///multiplier for damage from cold temperature
var/coldmod = 1
@@ -129,8 +127,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
/// A path to an outfit that is important for species life e.g. plasmaman outfit
var/datum/outfit/outfit_important_for_life
- //Dictates which wing icons are allowed for a given species. If count is >1 a radial menu is used to choose between all icons in list
- var/list/wing_types = list(/obj/item/organ/external/wings/functional/angel)
/// The natural temperature for a body
var/bodytemp_normal = BODYTEMP_NORMAL
/// Minimum amount of kelvin moved toward normal body temperature per tick.
@@ -202,8 +198,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
/datum/species/New()
- wing_types = string_list(wing_types)
-
if(!plural_form)
plural_form = "[name]\s"
if(!examine_limb_id)
@@ -874,9 +868,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
if(HAS_TRAIT(H, TRAIT_NOBREATH) && (H.health < H.crit_threshold) && !HAS_TRAIT(H, TRAIT_NOCRITDAMAGE))
H.adjustBruteLoss(0.5 * seconds_per_tick)
-/datum/species/proc/spec_death(gibbed, mob/living/carbon/human/H)
- return
-
/datum/species/proc/can_equip(obj/item/I, slot, disable_warning, mob/living/carbon/human/H, bypass_equip_delay_self = FALSE, ignore_equipped = FALSE, indirect_action = FALSE)
if(no_equip_flags & slot)
if(!I.species_exception || !is_type_in_list(src, I.species_exception))
@@ -1058,9 +1049,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
affected.log_message("has started overdosing on [chem.name] at [chem.volume] units.", LOG_GAME)
return SEND_SIGNAL(affected, COMSIG_SPECIES_HANDLE_CHEMICAL, chem, seconds_per_tick, times_fired)
-/datum/species/proc/check_species_weakness(obj/item, mob/living/attacker)
- return 1 //This is not a boolean, it's the multiplier for the damage that the user takes from the item. The force of the item is multiplied by this value
-
/**
* Equip the outfit required for life. Replaces items currently worn.
*/
@@ -1201,7 +1189,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
target.lastattacker = user.real_name
target.lastattackerckey = user.ckey
- user.dna.species.spec_unarmedattacked(user, target)
if(user.limb_destroyer)
target.dismembering_strike(user, affecting.body_zone)
@@ -1228,9 +1215,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
target.apply_effect(knockdown_duration, EFFECT_KNOCKDOWN, armor_block)
log_combat(user, target, "got a stun punch with their previous punch")
-/datum/species/proc/spec_unarmedattacked(mob/living/carbon/human/user, mob/living/carbon/human/target)
- return
-
/datum/species/proc/disarm(mob/living/carbon/human/user, mob/living/carbon/human/target, datum/martial_art/attacker_style)
if(target.check_block())
target.visible_message(span_warning("[user]'s shove is blocked by [target]!"), \
@@ -1247,10 +1231,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
return FALSE
user.disarm(target)
-
-/datum/species/proc/spec_hitby(atom/movable/AM, mob/living/carbon/human/H)
- return
-
/datum/species/proc/spec_attack_hand(mob/living/carbon/human/owner, mob/living/carbon/human/target, datum/martial_art/attacker_style, modifiers)
if(!istype(owner))
return
@@ -1261,7 +1241,7 @@ GLOBAL_LIST_EMPTY(features_by_species)
return
if(owner.mind)
attacker_style = owner.mind.martial_art
- if((owner != target) && owner.combat_mode && target.check_shields(owner, 0, owner.name, attack_type = UNARMED_ATTACK))
+ if((owner != target) && target.check_shields(owner, 0, owner.name, attack_type = UNARMED_ATTACK))
log_combat(owner, target, "attempted to touch")
target.visible_message(span_warning("[owner] attempts to touch [target]!"), \
span_danger("[owner] attempts to touch you!"), span_hear("You hear a swoosh!"), COMBAT_MESSAGE_RANGE, owner)
@@ -1288,30 +1268,36 @@ GLOBAL_LIST_EMPTY(features_by_species)
span_userdanger("You block [weapon]!"))
return FALSE
- var/hit_area
- if(!affecting) //Something went wrong. Maybe the limb is missing?
- affecting = human.bodyparts[1]
-
- hit_area = affecting.plaintext_zone
- var/def_zone = affecting.body_zone
-
- var/armor_block = human.run_armor_check(affecting, MELEE, span_notice("Your armor has protected your [hit_area]!"), span_warning("Your armor has softened a hit to your [hit_area]!"),weapon.armour_penetration, weak_against_armour = weapon.weak_against_armour)
- armor_block = min(ARMOR_MAX_BLOCK, armor_block) //cap damage reduction at 90%
- var/Iwound_bonus = weapon.wound_bonus
-
+ affecting ||= human.bodyparts[1] //Something went wrong. Maybe the limb is missing?
+ var/hit_area = affecting.plaintext_zone
+ var/armor_block = min(human.run_armor_check(
+ def_zone = affecting,
+ attack_flag = MELEE,
+ absorb_text = span_notice("Your armor has protected your [hit_area]!"),
+ soften_text = span_warning("Your armor has softened a hit to your [hit_area]!"),
+ armour_penetration = weapon.armour_penetration,
+ weak_against_armour = weapon.weak_against_armour,
+ ), ARMOR_MAX_BLOCK) //cap damage reduction at 90%
+
+ var/modified_wound_bonus = weapon.wound_bonus
// this way, you can't wound with a surgical tool on help intent if they have a surgery active and are lying down, so a misclick with a circular saw on the wrong limb doesn't bleed them dry (they still get hit tho)
if((weapon.item_flags & SURGICAL_TOOL) && !user.combat_mode && human.body_position == LYING_DOWN && (LAZYLEN(human.surgeries) > 0))
- Iwound_bonus = CANT_WOUND
-
- var/weakness = check_species_weakness(weapon, user)
+ modified_wound_bonus = CANT_WOUND
human.send_item_attack_message(weapon, user, hit_area, affecting)
+ var/damage_dealt = human.apply_damage(
+ damage = weapon.force,
+ damagetype = weapon.damtype,
+ def_zone = affecting,
+ blocked = armor_block,
+ wound_bonus = modified_wound_bonus,
+ bare_wound_bonus = weapon.bare_wound_bonus,
+ sharpness = weapon.get_sharpness(),
+ attack_direction = get_dir(user, human),
+ attacking_item = weapon,
+ )
-
- var/attack_direction = get_dir(user, human)
- apply_damage(weapon.force * weakness, weapon.damtype, def_zone, armor_block, human, wound_bonus = Iwound_bonus, bare_wound_bonus = weapon.bare_wound_bonus, sharpness = weapon.get_sharpness(), attack_direction = attack_direction, attacking_item = weapon)
-
- if(!weapon.force)
+ if(damage_dealt <= 0)
return FALSE //item force is zero
var/bloody = FALSE
if(weapon.damtype != BRUTE)
@@ -1381,73 +1367,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
return TRUE
-/datum/species/proc/apply_damage(damage, damagetype = BRUTE, def_zone = null, blocked, mob/living/carbon/human/H, forced = FALSE, spread_damage = FALSE, wound_bonus = 0, bare_wound_bonus = 0, sharpness = NONE, attack_direction = null, attacking_item)
- SEND_SIGNAL(H, COMSIG_MOB_APPLY_DAMAGE, damage, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction, attacking_item)
- var/hit_percent = (100-(damage_modifier+blocked))/100
- hit_percent = (hit_percent * (100-H.physiology.damage_resistance))/100
- if(!damage || (!forced && hit_percent <= 0))
- return 0
-
- var/obj/item/bodypart/BP = null
- if(!spread_damage)
- if(isbodypart(def_zone))
- BP = def_zone
- else
- if(!def_zone)
- def_zone = H.get_random_valid_zone(def_zone)
- BP = H.get_bodypart(check_zone(def_zone))
- if(!BP)
- BP = H.bodyparts[1]
-
- switch(damagetype)
- if(BRUTE)
- H.damageoverlaytemp = 20
- var/damage_amount = forced ? damage : damage * hit_percent * H.physiology.brute_mod
- if(BP)
- if(BP.receive_damage(damage_amount, 0, wound_bonus = wound_bonus, bare_wound_bonus = bare_wound_bonus, sharpness = sharpness, attack_direction = attack_direction, damage_source = attacking_item))
- H.update_damage_overlays()
- else//no bodypart, we deal damage with a more general method.
- H.adjustBruteLoss(damage_amount)
- if(BURN)
- H.damageoverlaytemp = 20
- var/damage_amount = forced ? damage : damage * hit_percent * H.physiology.burn_mod
- if(BP)
- if(BP.receive_damage(0, damage_amount, wound_bonus = wound_bonus, bare_wound_bonus = bare_wound_bonus, sharpness = sharpness, attack_direction = attack_direction, damage_source = attacking_item))
- H.update_damage_overlays()
- else
- H.adjustFireLoss(damage_amount)
- if(TOX)
- var/damage_amount = forced ? damage : damage * hit_percent * H.physiology.tox_mod
- H.adjustToxLoss(damage_amount)
- if(OXY)
- var/damage_amount = forced ? damage : damage * hit_percent * H.physiology.oxy_mod
- H.adjustOxyLoss(damage_amount)
- if(CLONE)
- var/damage_amount = forced ? damage : damage * hit_percent * H.physiology.clone_mod
- H.adjustCloneLoss(damage_amount)
- if(STAMINA)
- var/damage_amount = forced ? damage : damage * hit_percent * H.physiology.stamina_mod
- H.adjustStaminaLoss(damage_amount)
- if(BRAIN)
- var/damage_amount = forced ? damage : damage * hit_percent * H.physiology.brain_mod
- H.adjustOrganLoss(ORGAN_SLOT_BRAIN, damage_amount)
- SEND_SIGNAL(H, COMSIG_MOB_AFTER_APPLY_DAMAGE, damage, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction, attacking_item)
- return TRUE
-
-/datum/species/proc/on_hit(obj/projectile/P, mob/living/carbon/human/H)
- // called when hit by a projectile
- switch(P.type)
- if(/obj/projectile/energy/floramut) // overwritten by plants/pods
- H.show_message(span_notice("The radiation beam dissipates harmlessly through your body."))
- if(/obj/projectile/energy/florayield)
- H.show_message(span_notice("The radiation beam dissipates harmlessly through your body."))
- if(/obj/projectile/energy/florarevolution)
- H.show_message(span_notice("The radiation beam dissipates harmlessly through your body."))
-
-/datum/species/proc/bullet_act(obj/projectile/P, mob/living/carbon/human/H)
- // called before a projectile hit
- return 0
-
//////////////////////////
// ENVIRONMENT HANDLERS //
//////////////////////////
@@ -1727,11 +1646,12 @@ GLOBAL_LIST_EMPTY(features_by_species)
switch(adjusted_pressure)
// Very high pressure, show an alert and take damage
if(HAZARD_HIGH_PRESSURE to INFINITY)
- if(!HAS_TRAIT(H, TRAIT_RESISTHIGHPRESSURE))
- H.adjustBruteLoss(min(((adjusted_pressure / HAZARD_HIGH_PRESSURE) - 1) * PRESSURE_DAMAGE_COEFFICIENT, MAX_HIGH_PRESSURE_DAMAGE) * H.physiology.pressure_mod * seconds_per_tick, required_bodytype = BODYTYPE_ORGANIC)
- H.throw_alert(ALERT_PRESSURE, /atom/movable/screen/alert/highpressure, 2)
- else
+ if(HAS_TRAIT(H, TRAIT_RESISTHIGHPRESSURE))
H.clear_alert(ALERT_PRESSURE)
+ else
+ var/pressure_damage = min(((adjusted_pressure / HAZARD_HIGH_PRESSURE) - 1) * PRESSURE_DAMAGE_COEFFICIENT, MAX_HIGH_PRESSURE_DAMAGE) * H.physiology.pressure_mod * H.physiology.brute_mod * seconds_per_tick
+ H.adjustBruteLoss(pressure_damage, required_bodytype = BODYTYPE_ORGANIC)
+ H.throw_alert(ALERT_PRESSURE, /atom/movable/screen/alert/highpressure, 2)
// High pressure, show an alert
if(WARNING_HIGH_PRESSURE to HAZARD_HIGH_PRESSURE)
@@ -1755,7 +1675,8 @@ GLOBAL_LIST_EMPTY(features_by_species)
if(HAS_TRAIT(H, TRAIT_RESISTLOWPRESSURE))
H.clear_alert(ALERT_PRESSURE)
else
- H.adjustBruteLoss(LOW_PRESSURE_DAMAGE * H.physiology.pressure_mod * seconds_per_tick, required_bodytype = BODYTYPE_ORGANIC)
+ var/pressure_damage = LOW_PRESSURE_DAMAGE * H.physiology.pressure_mod * H.physiology.brute_mod * seconds_per_tick
+ H.adjustBruteLoss(pressure_damage, required_bodytype = BODYTYPE_ORGANIC)
H.throw_alert(ALERT_PRESSURE, /atom/movable/screen/alert/lowpressure, 2)
@@ -1797,10 +1718,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
former_tail_owner.clear_mood_event("tail_balance_lost")
former_tail_owner.clear_mood_event("wrong_tail_regained")
-///Species override for unarmed attacks because the attack_hand proc was made by a mouth-breathing troglodyte on a tricycle. Also to whoever thought it would be a good idea to make it so the original spec_unarmedattack was not actually linked to unarmed attack needs to be checked by a doctor because they clearly have a vast empty space in their head.
-/datum/species/proc/spec_unarmedattack(mob/living/carbon/human/user, atom/target, modifiers)
- return FALSE
-
/// Returns a list of strings representing features this species has.
/// Used by the preferences UI to know what buttons to show.
/datum/species/proc/get_features()
@@ -2363,3 +2280,9 @@ GLOBAL_LIST_EMPTY(features_by_species)
/datum/species/proc/check_head_flags(check_flags = NONE)
var/obj/item/bodypart/head/fake_head = bodypart_overrides[BODY_ZONE_HEAD]
return (initial(fake_head.head_flags) & check_flags)
+
+/datum/species/dump_harddel_info()
+ if(harddel_deets_dumped)
+ return
+ harddel_deets_dumped = TRUE
+ return "Gained / Owned: [properly_gained ? "Yes" : "No"]"
diff --git a/code/modules/mob/living/carbon/human/damage_procs.dm b/code/modules/mob/living/carbon/human/damage_procs.dm
deleted file mode 100644
index d4fc0b403656a..0000000000000
--- a/code/modules/mob/living/carbon/human/damage_procs.dm
+++ /dev/null
@@ -1,4 +0,0 @@
-
-/// depending on the species, it will run the corresponding apply_damage code there
-/mob/living/carbon/human/apply_damage(damage = 0, damagetype = BRUTE, def_zone = null, blocked = 0, forced = FALSE, spread_damage = FALSE, wound_bonus = 0, bare_wound_bonus = 0, sharpness = NONE, attack_direction = null, attacking_item)
- return dna.species.apply_damage(damage, damagetype, def_zone, blocked, src, forced, spread_damage, wound_bonus, bare_wound_bonus, sharpness, attack_direction, attacking_item)
diff --git a/code/modules/mob/living/carbon/human/death.dm b/code/modules/mob/living/carbon/human/death.dm
index b451d86935b55..f8cee3b4851f8 100644
--- a/code/modules/mob/living/carbon/human/death.dm
+++ b/code/modules/mob/living/carbon/human/death.dm
@@ -30,9 +30,6 @@ GLOBAL_LIST_EMPTY(dead_players_during_shift)
if(client && !HAS_TRAIT(src, TRAIT_SUICIDED) && !(client in GLOB.dead_players_during_shift))
GLOB.dead_players_during_shift += client
- if(!QDELETED(dna)) //The gibbed param is bit redundant here since dna won't exist at this point if they got deleted.
- dna.species.spec_death(gibbed, src)
-
if(SSticker.HasRoundStarted())
SSblackbox.ReportDeath(src)
log_message("has died (BRUTE: [src.getBruteLoss()], BURN: [src.getFireLoss()], TOX: [src.getToxLoss()], OXY: [src.getOxyLoss()], CLONE: [src.getCloneLoss()])", LOG_ATTACK)
diff --git a/code/modules/mob/living/carbon/human/emote.dm b/code/modules/mob/living/carbon/human/emote.dm
index 8dc2e7bfc1a1a..7c7e84a9ffa65 100644
--- a/code/modules/mob/living/carbon/human/emote.dm
+++ b/code/modules/mob/living/carbon/human/emote.dm
@@ -116,6 +116,7 @@
message = "salutes."
message_param = "salutes to %t."
hands_use_check = TRUE
+ sound = 'sound/misc/salute.ogg'
/datum/emote/living/carbon/human/shrug
key = "shrug"
diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm
index fafc9b49a69d9..6a6714b084441 100644
--- a/code/modules/mob/living/carbon/human/human_defense.dm
+++ b/code/modules/mob/living/carbon/human/human_defense.dm
@@ -44,59 +44,33 @@
covering_part += C
return covering_part
-/mob/living/carbon/human/on_hit(obj/projectile/P)
- if(dna?.species)
- dna.species.on_hit(P, src)
-
-
-/mob/living/carbon/human/bullet_act(obj/projectile/P, def_zone, piercing_hit = FALSE)
- if(dna?.species)
- var/spec_return = dna.species.bullet_act(P, src)
- if(spec_return)
- return spec_return
-
- //MARTIAL ART STUFF
- if(mind)
- if(mind.martial_art && mind.martial_art.can_use(src)) //Some martial arts users can deflect projectiles!
- var/martial_art_result = mind.martial_art.on_projectile_hit(src, P, def_zone)
- if(!(martial_art_result == BULLET_ACT_HIT))
- return martial_art_result
-
- if(!(P.original == src && P.firer == src)) //can't block or reflect when shooting yourself
- if(P.reflectable & REFLECT_NORMAL)
- if(check_reflect(def_zone)) // Checks if you've passed a reflection% check
- visible_message(span_danger("The [P.name] gets reflected by [src]!"), \
- span_userdanger("The [P.name] gets reflected by [src]!"))
- // Finds and plays the block_sound of item which reflected
- for(var/obj/item/I in held_items)
- if(I.IsReflect(def_zone))
- playsound(src, I.block_sound, BLOCK_SOUND_VOLUME, TRUE)
- // Find a turf near or on the original location to bounce to
- if(!isturf(loc)) //Open canopy mech (ripley) check. if we're inside something and still got hit
- P.force_hit = TRUE //The thing we're in passed the bullet to us. Pass it back, and tell it to take the damage.
- loc.bullet_act(P, def_zone, piercing_hit)
- return BULLET_ACT_HIT
- if(P.starting)
- var/new_x = P.starting.x + pick(0, 0, 0, 0, 0, -1, 1, -2, 2)
- var/new_y = P.starting.y + pick(0, 0, 0, 0, 0, -1, 1, -2, 2)
- var/turf/curloc = get_turf(src)
-
- // redirect the projectile
- P.original = locate(new_x, new_y, P.z)
- P.starting = curloc
- P.firer = src
- P.yo = new_y - curloc.y
- P.xo = new_x - curloc.x
- var/new_angle_s = P.Angle + rand(120,240)
- while(new_angle_s > 180) // Translate to regular projectile degrees
- new_angle_s -= 360
- P.set_angle(new_angle_s)
-
- return BULLET_ACT_FORCE_PIERCE // complete projectile permutation
-
- if(check_shields(P, P.damage, "the [P.name]", PROJECTILE_ATTACK, P.armour_penetration, P.damage_type))
- P.on_hit(src, 100, def_zone, piercing_hit)
- return BULLET_ACT_HIT
+/mob/living/carbon/human/bullet_act(obj/projectile/bullet, def_zone, piercing_hit = FALSE)
+
+ if(bullet.firer == src && bullet.original == src) //can't block or reflect when shooting yourself
+ return ..()
+
+ if(bullet.reflectable & REFLECT_NORMAL)
+ if(check_reflect(def_zone)) // Checks if you've passed a reflection% check
+ visible_message(
+ span_danger("The [bullet.name] gets reflected by [src]!"),
+ span_userdanger("The [bullet.name] gets reflected by [src]!"),
+ )
+ // Finds and plays the block_sound of item which reflected
+ for(var/obj/item/held_item in held_items)
+ if(held_item.IsReflect(def_zone))
+ playsound(src, held_item.block_sound, BLOCK_SOUND_VOLUME, TRUE)
+ // Find a turf near or on the original location to bounce to
+ if(!isturf(loc)) //Open canopy mech (ripley) check. if we're inside something and still got hit
+ bullet.force_hit = TRUE //The thing we're in passed the bullet to us. Pass it back, and tell it to take the damage.
+ loc.bullet_act(bullet, def_zone, piercing_hit)
+ return BULLET_ACT_HIT
+ bullet.reflect(src)
+
+ return BULLET_ACT_FORCE_PIERCE // complete projectile permutation
+
+ if(check_shields(bullet, bullet.damage, "the [bullet.name]", PROJECTILE_ATTACK, bullet.armour_penetration, bullet.damage_type))
+ bullet.on_hit(src, 100, def_zone, piercing_hit)
+ return BULLET_ACT_HIT
return ..()
@@ -148,10 +122,6 @@
return FALSE
/mob/living/carbon/human/hitby(atom/movable/AM, skipcatch = FALSE, hitpush = TRUE, blocked = FALSE, datum/thrownthing/throwingdatum)
- if(dna?.species)
- var/spec_return = dna.species.spec_hitby(AM, src)
- if(spec_return)
- return spec_return
var/obj/item/I
var/damage_type = BRUTE
var/throwpower = 30
@@ -260,14 +230,13 @@
if(try_inject(user, affecting, injection_flags = INJECT_TRY_SHOW_ERROR_MESSAGE))//Thick suits can stop monkey bites.
if(..()) //successful monkey bite, this handles disease contraction.
- var/obj/item/bodypart/arm/active_arm = user.get_active_hand()
- var/damage = rand(active_arm.unarmed_damage_low, active_arm.unarmed_damage_high)
+ var/obj/item/bodypart/head/monkey_mouth = user.get_bodypart(BODY_ZONE_HEAD)
+ var/damage = HAS_TRAIT(user, TRAIT_PERFECT_ATTACKER) ? monkey_mouth.unarmed_damage_high : rand(monkey_mouth.unarmed_damage_low, monkey_mouth.unarmed_damage_high)
if(!damage)
- return
+ return FALSE
if(check_shields(user, damage, "the [user.name]"))
return FALSE
- if(stat != DEAD)
- apply_damage(damage, BRUTE, affecting, run_armor_check(affecting, MELEE))
+ apply_damage(damage, BRUTE, affecting, run_armor_check(affecting, MELEE))
return TRUE
/mob/living/carbon/human/attack_alien(mob/living/carbon/alien/adult/user, list/modifiers)
diff --git a/code/modules/mob/living/carbon/human/human_update_icons.dm b/code/modules/mob/living/carbon/human/human_update_icons.dm
index ccb36715e852e..45ea2ef2b6277 100644
--- a/code/modules/mob/living/carbon/human/human_update_icons.dm
+++ b/code/modules/mob/living/carbon/human/human_update_icons.dm
@@ -644,6 +644,7 @@ generate/load female uniform sprites matching all previously decided variables
female_uniform = NO_FEMALE_UNIFORM,
override_state = null,
override_file = null,
+ use_height_offset = TRUE,
)
//Find a valid icon_state from variables+arguments
@@ -672,7 +673,7 @@ generate/load female uniform sprites matching all previously decided variables
//eg: ammo counters, primed grenade flashes, etc.
var/list/worn_overlays = worn_overlays(standing, isinhands, file2use)
if(worn_overlays?.len)
- if(!isinhands && default_layer && ishuman(loc))
+ if(!isinhands && default_layer && ishuman(loc) && use_height_offset)
var/mob/living/carbon/human/human_loc = loc
if(human_loc.get_mob_height() != HUMAN_HEIGHT_MEDIUM)
var/string_form_layer = num2text(default_layer)
diff --git a/code/modules/mob/living/carbon/human/monkey.dm b/code/modules/mob/living/carbon/human/monkey.dm
index db8f2f4f1e3d3..76bf7cc5288a3 100644
--- a/code/modules/mob/living/carbon/human/monkey.dm
+++ b/code/modules/mob/living/carbon/human/monkey.dm
@@ -4,7 +4,7 @@
ai_controller = /datum/ai_controller/monkey
faction = list(FACTION_NEUTRAL, FACTION_MONKEY)
-/mob/living/carbon/human/species/monkey/Initialize(mapload, cubespawned=FALSE, mob/spawner)
+/mob/living/carbon/human/species/monkey/Initialize(mapload, cubespawned = FALSE, mob/spawner)
if (cubespawned)
var/cap = CONFIG_GET(number/monkeycap)
if (LAZYLEN(SSmobs.cubemonkeys) > cap)
@@ -21,7 +21,7 @@
/mob/living/carbon/human/species/monkey/angry
ai_controller = /datum/ai_controller/monkey/angry
-/mob/living/carbon/human/species/monkey/angry/Initialize(mapload)
+/mob/living/carbon/human/species/monkey/angry/Initialize(mapload, cubespawned = FALSE, mob/spawner)
. = ..()
if(prob(10))
INVOKE_ASYNC(src, PROC_REF(give_ape_escape_helmet))
@@ -32,6 +32,20 @@
equip_to_slot_or_del(helmet, ITEM_SLOT_HEAD)
helmet.attack_self(src) // todo encapsulate toggle
+/mob/living/carbon/human/species/monkey/holodeck
+ race = /datum/species/monkey/holodeck
+
+/mob/living/carbon/human/species/monkey/holodeck/spawn_gibs() // no blood and no gibs
+ return
+
+/mob/living/carbon/human/species/monkey/holodeck/has_dna()
+ return null
+
+/mob/living/carbon/human/species/monkey/holodeck/create_bodyparts(list/overrides) // done like this in case people add more limbs to monkeys or something
+ . = ..()
+ for(var/obj/item/bodypart/limb as anything in bodyparts)
+ limb.bodypart_flags |= BODYPART_UNREMOVABLE // no farming organs or limbs from these fellers. get a monkey cube
+
GLOBAL_DATUM(the_one_and_only_punpun, /mob/living/carbon/human/species/monkey/punpun)
/mob/living/carbon/human/species/monkey/punpun
diff --git a/code/modules/mob/living/carbon/human/physiology.dm b/code/modules/mob/living/carbon/human/physiology.dm
index bfd3dc8d73c15..398f8d0c5e98d 100644
--- a/code/modules/mob/living/carbon/human/physiology.dm
+++ b/code/modules/mob/living/carbon/human/physiology.dm
@@ -1,18 +1,33 @@
//Stores several modifiers in a way that isn't cleared by changing species
/datum/physiology
- var/brute_mod = 1 // % of brute damage taken from all sources
- var/burn_mod = 1 // % of burn damage taken from all sources
- var/tox_mod = 1 // % of toxin damage taken from all sources
- var/oxy_mod = 1 // % of oxygen damage taken from all sources
- var/clone_mod = 1 // % of clone damage taken from all sources
- var/stamina_mod = 1 // % of stamina damage taken from all sources
- var/brain_mod = 1 // % of brain damage taken from all sources
+ /// Multiplier to brute damage received.
+ /// IE: A brute mod of 0.9 = 10% less brute damage.
+ /// Only applies to damage dealt via [apply_damage][/mob/living/proc/apply_damage] unless factored in manually.
+ var/brute_mod = 1
+ /// Multiplier to burn damage received
+ var/burn_mod = 1
+ /// Multiplier to toxin damage received
+ var/tox_mod = 1
+ /// Multiplier to oxygen damage received
+ var/oxy_mod = 1
+ /// Multiplier to clone damage received
+ var/clone_mod = 1
+ /// Multiplier to stamina damage received
+ var/stamina_mod = 1
+ /// Multiplier to brain damage received
+ var/brain_mod = 1
- var/pressure_mod = 1 // % of brute damage taken from low or high pressure (stacks with brute_mod)
- var/heat_mod = 1 // % of burn damage taken from heat (stacks with burn_mod)
- var/cold_mod = 1 // % of burn damage taken from cold (stacks with burn_mod)
+ /// Multiplier to damage taken from high / low pressure exposure, stacking with the brute modifier
+ var/pressure_mod = 1
+ /// Multiplier to damage taken from high temperature exposure, stacking with the burn modifier
+ var/heat_mod = 1
+ /// Multiplier to damage taken from low temperature exposure, stacking with the toxin modifier
+ var/cold_mod = 1
- var/damage_resistance = 0 // %damage reduction from all sources
+ /// Flat damage reduction from taking damage
+ /// Unlike the other modifiers, this is not a multiplier.
+ /// IE: DR of 10 = 10% less damage.
+ var/damage_resistance = 0
var/siemens_coeff = 1 // resistance to shocks
diff --git a/code/modules/mob/living/carbon/human/species_types/android.dm b/code/modules/mob/living/carbon/human/species_types/android.dm
index 570aa91dc342c..c1afc0c026611 100644
--- a/code/modules/mob/living/carbon/human/species_types/android.dm
+++ b/code/modules/mob/living/carbon/human/species_types/android.dm
@@ -3,7 +3,6 @@
id = SPECIES_ANDROID
examine_limb_id = SPECIES_HUMAN
inherent_traits = list(
- TRAIT_CAN_USE_FLIGHT_POTION,
TRAIT_GENELESS,
TRAIT_LIMBATTACHMENT,
TRAIT_LIVERLESS_METABOLISM,
@@ -13,6 +12,7 @@
TRAIT_NOFIRE,
TRAIT_NOHUNGER,
TRAIT_NO_DNA_COPY,
+ TRAIT_NO_PLASMA_TRANSFORM,
TRAIT_NO_UNDERWEAR,
TRAIT_PIERCEIMMUNE,
TRAIT_RADIMMUNE,
@@ -21,6 +21,7 @@
TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
TRAIT_TOXIMMUNE,
+ TRAIT_NOCRITDAMAGE,
)
inherent_biotypes = MOB_ROBOTIC|MOB_HUMANOID
@@ -34,7 +35,6 @@
mutanteyes = /obj/item/organ/internal/eyes/robotic
mutantears = /obj/item/organ/internal/ears/cybernetic
species_language_holder = /datum/language_holder/synthetic
- wing_types = list(/obj/item/organ/external/wings/functional/robotic)
changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_PRIDE | MIRROR_MAGIC | RACE_SWAP | ERT_SPAWN | SLIME_EXTRACT
bodypart_overrides = list(
diff --git a/code/modules/mob/living/carbon/human/species_types/felinid.dm b/code/modules/mob/living/carbon/human/species_types/felinid.dm
index a6a229d0b2c46..49f18aa9f6b3c 100644
--- a/code/modules/mob/living/carbon/human/species_types/felinid.dm
+++ b/code/modules/mob/living/carbon/human/species_types/felinid.dm
@@ -11,7 +11,6 @@
/obj/item/organ/external/tail/cat = "Cat",
)
inherent_traits = list(
- TRAIT_CAN_USE_FLIGHT_POTION,
TRAIT_HATED_BY_DOGS,
TRAIT_USES_SKINTONES,
)
diff --git a/code/modules/mob/living/carbon/human/species_types/flypeople.dm b/code/modules/mob/living/carbon/human/species_types/flypeople.dm
index c36252bbcb2dc..44e8981c55315 100644
--- a/code/modules/mob/living/carbon/human/species_types/flypeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/flypeople.dm
@@ -3,7 +3,6 @@
plural_form = "Flypeople"
id = SPECIES_FLYPERSON
inherent_traits = list(
- TRAIT_CAN_USE_FLIGHT_POTION,
TRAIT_TACKLING_FRAIL_ATTACKER,
TRAIT_ANTENNAE,
)
@@ -12,7 +11,6 @@
mutanteyes = /obj/item/organ/internal/eyes/fly
changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_PRIDE | MIRROR_MAGIC | RACE_SWAP | ERT_SPAWN | SLIME_EXTRACT
species_language_holder = /datum/language_holder/fly
- wing_types = list(/obj/item/organ/external/wings/functional/fly)
payday_modifier = 1.0
mutanttongue = /obj/item/organ/internal/tongue/fly
@@ -32,10 +30,19 @@
BODY_ZONE_CHEST = /obj/item/bodypart/chest/fly,
)
-/datum/species/fly/check_species_weakness(obj/item/weapon, mob/living/attacker)
- if(istype(weapon, /obj/item/melee/flyswatter))
- return 30 //Flyswatters deal 30x damage to flypeople.
- return 1
+/datum/species/fly/on_species_gain(mob/living/carbon/human/human_who_gained_species, datum/species/old_species, pref_load)
+ . = ..()
+ RegisterSignal(human_who_gained_species, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS, PROC_REF(damage_weakness))
+
+/datum/species/fly/on_species_loss(mob/living/carbon/human/C, datum/species/new_species, pref_load)
+ . = ..()
+ UnregisterSignal(C, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS)
+
+/datum/species/fly/proc/damage_weakness(datum/source, list/damage_mods, damage_amount, damagetype, def_zone, sharpness, attack_direction, obj/item/attacking_item)
+ SIGNAL_HANDLER
+
+ if(istype(attacking_item, /obj/item/melee/flyswatter))
+ damage_mods += 30 // Yes, a 30x damage modifier
/datum/species/fly/get_physical_attributes()
return "These hideous creatures suffer from pesticide immensely, eat waste, and are incredibly vulnerable to bright lights. They do have wings though."
diff --git a/code/modules/mob/living/carbon/human/species_types/golems.dm b/code/modules/mob/living/carbon/human/species_types/golems.dm
index 19427def4d2ed..0a9232476cc3c 100644
--- a/code/modules/mob/living/carbon/human/species_types/golems.dm
+++ b/code/modules/mob/living/carbon/human/species_types/golems.dm
@@ -12,9 +12,12 @@
TRAIT_NOFIRE,
TRAIT_NO_AUGMENTS,
TRAIT_NO_DNA_COPY,
+ TRAIT_NO_PLASMA_TRANSFORM,
TRAIT_NO_UNDERWEAR,
TRAIT_PIERCEIMMUNE,
TRAIT_RADIMMUNE,
+ TRAIT_SNOWSTORM_IMMUNE, // Shared with plasma river... but I guess if you can survive a plasma river a blizzard isn't a big deal
+ TRAIT_UNHUSKABLE,
)
mutantheart = null
mutantlungs = null
diff --git a/code/modules/mob/living/carbon/human/species_types/humans.dm b/code/modules/mob/living/carbon/human/species_types/humans.dm
index 2afa32a6b6919..c7a181027e64e 100644
--- a/code/modules/mob/living/carbon/human/species_types/humans.dm
+++ b/code/modules/mob/living/carbon/human/species_types/humans.dm
@@ -2,7 +2,6 @@
name = "\improper Human"
id = SPECIES_HUMAN
inherent_traits = list(
- TRAIT_CAN_USE_FLIGHT_POTION,
TRAIT_USES_SKINTONES,
)
mutant_bodyparts = list("wings" = "None")
diff --git a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
index bd662d63ad142..cf581ac97eecb 100644
--- a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
@@ -206,6 +206,7 @@
// so if someone mindswapped into them, they'd still be shared.
bodies = null
C.blood_volume = min(C.blood_volume, BLOOD_VOLUME_NORMAL)
+ UnregisterSignal(C, COMSIG_LIVING_DEATH)
..()
/datum/species/jelly/slime/on_species_gain(mob/living/carbon/C, datum/species/old_species)
@@ -221,20 +222,25 @@
else
bodies |= C
-/datum/species/jelly/slime/spec_death(gibbed, mob/living/carbon/human/H)
- if(slime_split)
- if(!H.mind || !H.mind.active)
- return
+ RegisterSignal(C, COMSIG_LIVING_DEATH, PROC_REF(on_death_move_body))
- var/list/available_bodies = (bodies - H)
- for(var/mob/living/L in available_bodies)
- if(!swap_body.can_swap(L))
- available_bodies -= L
+/datum/species/jelly/slime/proc/on_death_move_body(mob/living/carbon/human/source, gibbed)
+ SIGNAL_HANDLER
- if(!LAZYLEN(available_bodies))
- return
+ if(!slime_split)
+ return
+ if(!source.mind?.active)
+ return
+
+ var/list/available_bodies = bodies - source
+ for(var/mob/living/other_body as anything in available_bodies)
+ if(!swap_body.can_swap(other_body))
+ available_bodies -= other_body
+
+ if(!length(available_bodies))
+ return
- swap_body.swap_to_dupe(H.mind, pick(available_bodies))
+ swap_body.swap_to_dupe(source.mind, pick(available_bodies))
//If you're cloned you get your body pool back
/datum/species/jelly/slime/copy_properties_from(datum/species/jelly/slime/old_species)
diff --git a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
index e02813826b13a..e77e36d9c17ca 100644
--- a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
@@ -5,7 +5,6 @@
id = SPECIES_LIZARD
inherent_traits = list(
TRAIT_MUTANT_COLORS,
- TRAIT_CAN_USE_FLIGHT_POTION,
TRAIT_TACKLING_TAILED_DEFENDER,
)
inherent_biotypes = MOB_ORGANIC|MOB_HUMANOID|MOB_REPTILE
@@ -28,7 +27,6 @@
exotic_bloodtype = "L"
inert_mutation = /datum/mutation/human/firebreath
death_sound = 'sound/voice/lizard/deathsound.ogg'
- wing_types = list(/obj/item/organ/external/wings/functional/dragon)
species_language_holder = /datum/language_holder/lizard
digitigrade_customization = DIGITIGRADE_OPTIONAL
diff --git a/code/modules/mob/living/carbon/human/species_types/monkeys.dm b/code/modules/mob/living/carbon/human/species_types/monkeys.dm
index f8aee4d83b17c..8fb36e7c4e2c1 100644
--- a/code/modules/mob/living/carbon/human/species_types/monkeys.dm
+++ b/code/modules/mob/living/carbon/human/species_types/monkeys.dm
@@ -40,8 +40,6 @@
payday_modifier = 1.5
ai_controlled_species = TRUE
-
-
/datum/species/monkey/random_name(gender,unique,lastname)
var/randname = "monkey ([rand(1,999)])"
@@ -52,70 +50,13 @@
passtable_on(H, SPECIES_TRAIT)
H.dna.add_mutation(/datum/mutation/human/race, MUT_NORMAL)
H.dna.activate_mutation(/datum/mutation/human/race)
+ H.AddElement(/datum/element/human_biter)
/datum/species/monkey/on_species_loss(mob/living/carbon/C)
. = ..()
passtable_off(C, SPECIES_TRAIT)
C.dna.remove_mutation(/datum/mutation/human/race)
-
-/datum/species/monkey/spec_unarmedattack(mob/living/carbon/human/user, atom/target, modifiers)
- // If our hands are not blocked, dont try to bite them
- if(!HAS_TRAIT(user, TRAIT_HANDS_BLOCKED))
- // if we aren't an advanced tool user, we call attack_paw and cancel the preceeding attack chain
- if(!ISADVANCEDTOOLUSER(user))
- target.attack_paw(user, modifiers)
- return TRUE
- return ..()
-
- // this shouldn't even be possible, but I'm sure the check was here for a reason
- if(!iscarbon(target))
- stack_trace("HEY LISTEN! We are performing a species spec_unarmed attack with a non-carbon user. How did you fuck this up?")
- return TRUE
- var/mob/living/carbon/victim = target
- if(user.is_muzzled())
- return TRUE // cannot bite them if we're muzzled
-
- var/obj/item/bodypart/affecting
- if(ishuman(victim))
- var/mob/living/carbon/human/human_victim = victim
- affecting = human_victim.get_bodypart(human_victim.get_random_valid_zone(even_weights = TRUE))
- var/armor = victim.run_armor_check(affecting, MELEE)
-
- if(prob(MONKEY_SPEC_ATTACK_BITE_MISS_CHANCE))
- victim.visible_message(
- span_danger("[user]'s bite misses [victim]!"),
- span_danger("You avoid [user]'s bite!"),
- span_hear("You hear jaws snapping shut!"),
- COMBAT_MESSAGE_RANGE,
- user,
- )
- to_chat(user, span_danger("Your bite misses [victim]!"))
- return TRUE
-
- var/obj/item/bodypart/head/mouth = user.get_bodypart(BODY_ZONE_HEAD)
- if(!mouth) // check for them having a head, ala HARS
- return TRUE
-
- var/damage_roll = rand(mouth.unarmed_damage_low, mouth.unarmed_damage_high)
- victim.apply_damage(damage_roll, BRUTE, affecting, armor)
-
- victim.visible_message(
- span_danger("[name] bites [victim]!"),
- span_userdanger("[name] bites you!"),
- span_hear("You hear a chomp!"),
- COMBAT_MESSAGE_RANGE,
- name,
- )
- to_chat(user, span_danger("You bite [victim]!"))
-
- if(armor >= 2) // if they have basic armor on the limb we bit, don't spread diseases
- return TRUE
- for(var/datum/disease/bite_infection as anything in user.diseases)
- if(bite_infection.spread_flags & (DISEASE_SPREAD_SPECIAL | DISEASE_SPREAD_NON_CONTAGIOUS))
- continue // ignore diseases that have special spread logic, or are not contagious
- victim.ForceContractDisease(bite_infection)
-
- return TRUE
+ C.RemoveElement(/datum/element/human_biter)
/datum/species/monkey/check_roundstart_eligible()
if(check_holidays(MONKEYDAY))
@@ -249,4 +190,31 @@
/obj/item/organ/internal/brain/primate/get_attacking_limb(mob/living/carbon/human/target)
return owner.get_bodypart(BODY_ZONE_HEAD)
+/// Virtual monkeys that crave virtual bananas. Everything about them is ephemeral (except that bite).
+/datum/species/monkey/holodeck
+ id = SPECIES_MONKEY_HOLODECK
+ knife_butcher_results = list()
+ meat = null
+ skinned_type = null
+ inherent_traits = list(
+ TRAIT_GENELESS,
+ TRAIT_GUN_NATURAL,
+ TRAIT_NO_AUGMENTS,
+ TRAIT_NO_BLOOD_OVERLAY,
+ TRAIT_NO_DNA_COPY,
+ TRAIT_NO_UNDERWEAR,
+ TRAIT_NO_ZOMBIFY,
+ TRAIT_NOBLOOD,
+ TRAIT_NOHUNGER,
+ TRAIT_VENTCRAWLER_NUDE,
+ )
+ bodypart_overrides = list(
+ BODY_ZONE_L_ARM = /obj/item/bodypart/arm/left/monkey,
+ BODY_ZONE_R_ARM = /obj/item/bodypart/arm/right/monkey,
+ BODY_ZONE_HEAD = /obj/item/bodypart/head/monkey,
+ BODY_ZONE_L_LEG = /obj/item/bodypart/leg/left/monkey,
+ BODY_ZONE_R_LEG = /obj/item/bodypart/leg/right/monkey,
+ BODY_ZONE_CHEST = /obj/item/bodypart/chest/monkey,
+ )
+
#undef MONKEY_SPEC_ATTACK_BITE_MISS_CHANCE
diff --git a/code/modules/mob/living/carbon/human/species_types/mothmen.dm b/code/modules/mob/living/carbon/human/species_types/mothmen.dm
index a337b8cee0578..68fb87142f6c9 100644
--- a/code/modules/mob/living/carbon/human/species_types/mothmen.dm
+++ b/code/modules/mob/living/carbon/human/species_types/mothmen.dm
@@ -4,7 +4,6 @@
id = SPECIES_MOTH
inherent_traits = list(
TRAIT_HAS_MARKINGS,
- TRAIT_CAN_USE_FLIGHT_POTION,
TRAIT_TACKLING_WINGED_ATTACKER,
TRAIT_ANTENNAE,
)
@@ -17,7 +16,6 @@
changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_MAGIC | MIRROR_PRIDE | ERT_SPAWN | RACE_SWAP | SLIME_EXTRACT
species_language_holder = /datum/language_holder/moth
death_sound = 'sound/voice/moth/moth_death.ogg'
- wing_types = list(/obj/item/organ/external/wings/functional/moth/megamoth, /obj/item/organ/external/wings/functional/moth/mothra)
payday_modifier = 1.0
family_heirlooms = list(/obj/item/flashlight/lantern/heirloom_moth)
@@ -47,10 +45,19 @@
return randname
-/datum/species/moth/check_species_weakness(obj/item/weapon, mob/living/attacker)
- if(istype(weapon, /obj/item/melee/flyswatter))
- return 10 //flyswatters deal 10x damage to moths
- return 1
+/datum/species/moth/on_species_gain(mob/living/carbon/human/human_who_gained_species, datum/species/old_species, pref_load)
+ . = ..()
+ RegisterSignal(human_who_gained_species, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS, PROC_REF(damage_weakness))
+
+/datum/species/moth/on_species_loss(mob/living/carbon/human/C, datum/species/new_species, pref_load)
+ . = ..()
+ UnregisterSignal(C, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS)
+
+/datum/species/moth/proc/damage_weakness(datum/source, list/damage_mods, damage_amount, damagetype, def_zone, sharpness, attack_direction, obj/item/attacking_item)
+ SIGNAL_HANDLER
+
+ if(istype(attacking_item, /obj/item/melee/flyswatter))
+ damage_mods += 10 // Yes, a 10x damage modifier
/datum/species/moth/randomize_features()
var/list/features = ..()
diff --git a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
index 9facc517c1c79..8ce8be6f6d6b5 100644
--- a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
+++ b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
@@ -10,8 +10,10 @@
TRAIT_HARDLY_WOUNDED,
TRAIT_NOBLOOD,
TRAIT_NO_DNA_COPY,
+ TRAIT_NO_PLASMA_TRANSFORM,
TRAIT_RADIMMUNE,
TRAIT_RESISTCOLD,
+ TRAIT_UNHUSKABLE,
)
inherent_biotypes = MOB_HUMANOID|MOB_MINERAL
diff --git a/code/modules/mob/living/carbon/human/species_types/skeletons.dm b/code/modules/mob/living/carbon/human/species_types/skeletons.dm
index 9e2d59d03007b..e52ca3225aea9 100644
--- a/code/modules/mob/living/carbon/human/species_types/skeletons.dm
+++ b/code/modules/mob/living/carbon/human/species_types/skeletons.dm
@@ -5,7 +5,6 @@
sexes = FALSE
meat = /obj/item/food/meat/slab/human/mutant/skeleton
inherent_traits = list(
- TRAIT_CAN_USE_FLIGHT_POTION,
TRAIT_EASYDISMEMBER,
TRAIT_FAKEDEATH,
TRAIT_GENELESS,
@@ -22,6 +21,7 @@
TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
TRAIT_TOXIMMUNE,
+ TRAIT_UNHUSKABLE,
TRAIT_XENO_IMMUNE,
)
inherent_biotypes = MOB_UNDEAD|MOB_HUMANOID
@@ -31,7 +31,6 @@
mutantheart = null
mutantliver = /obj/item/organ/internal/liver/bone
mutantlungs = null
- wing_types = list(/obj/item/organ/external/wings/functional/skeleton)
//They can technically be in an ERT
changesource_flags = MIRROR_BADMIN | WABBAJACK | ERT_SPAWN
species_cookie = /obj/item/reagent_containers/condiment/milk
diff --git a/code/modules/mob/living/carbon/human/species_types/vampire.dm b/code/modules/mob/living/carbon/human/species_types/vampire.dm
index c07f0478c03ea..4c2f1ffa70839 100644
--- a/code/modules/mob/living/carbon/human/species_types/vampire.dm
+++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm
@@ -40,6 +40,11 @@
new_vampire.skin_tone = "albino"
new_vampire.update_body(0)
new_vampire.set_safe_hunger_level()
+ RegisterSignal(new_vampire, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS, PROC_REF(damage_weakness))
+
+/datum/species/vampire/on_species_loss(mob/living/carbon/human/C, datum/species/new_species, pref_load)
+ . = ..()
+ UnregisterSignal(C, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS)
/datum/species/vampire/spec_life(mob/living/carbon/human/vampire, seconds_per_tick, times_fired)
. = ..()
@@ -64,10 +69,11 @@
vampire.adjust_fire_stacks(3 * seconds_per_tick)
vampire.ignite_mob()
-/datum/species/vampire/check_species_weakness(obj/item/weapon, mob/living/attacker)
- if(istype(weapon, /obj/item/nullrod/whip))
- return 2 //Whips deal 2x damage to vampires. Vampire killer.
- return 1
+/datum/species/vampire/proc/damage_weakness(datum/source, list/damage_mods, damage_amount, damagetype, def_zone, sharpness, attack_direction, obj/item/attacking_item)
+ SIGNAL_HANDLER
+
+ if(istype(attacking_item, /obj/item/nullrod/whip))
+ damage_mods += 2
/datum/species/vampire/get_physical_attributes()
return "Vampires are afflicted with the Thirst, needing to sate it by draining the blood out of another living creature. However, they do not need to breathe or eat normally. \
diff --git a/code/modules/mob/living/carbon/human/species_types/zombies.dm b/code/modules/mob/living/carbon/human/species_types/zombies.dm
index d07badff51ef2..7c42e75618718 100644
--- a/code/modules/mob/living/carbon/human/species_types/zombies.dm
+++ b/code/modules/mob/living/carbon/human/species_types/zombies.dm
@@ -1,5 +1,3 @@
-#define REGENERATION_DELAY 6 SECONDS // After taking damage, how long it takes for automatic regeneration to begin
-
/datum/species/zombie
// 1spooky
name = "High-Functioning Zombie"
@@ -133,72 +131,51 @@
BODY_ZONE_L_LEG = /obj/item/bodypart/leg/left/zombie/infectious,
BODY_ZONE_R_LEG = /obj/item/bodypart/leg/right/zombie/infectious,
)
- /// The rate the zombies regenerate at
- var/heal_rate = 0.5
- /// The cooldown before the zombie can start regenerating
- COOLDOWN_DECLARE(regen_cooldown)
/datum/species/zombie/infectious/on_species_gain(mob/living/carbon/human/new_zombie, datum/species/old_species)
. = ..()
- new_zombie.AddComponent(/datum/component/mutant_hands, mutant_hand_path = /obj/item/mutant_hand/zombie)
+ new_zombie.set_combat_mode(TRUE)
+
+ // Deal with the source of this zombie corruption
+ // Infection organ needs to be handled separately from mutant_organs
+ // because it persists through species transitions
+ var/obj/item/organ/internal/zombie_infection/infection = new_zombie.get_organ_slot(ORGAN_SLOT_ZOMBIE)
+ if(isnull(infection))
+ infection = new()
+ infection.Insert(new_zombie)
+
+ new_zombie.AddComponent( \
+ /datum/component/mutant_hands, \
+ mutant_hand_path = /obj/item/mutant_hand/zombie, \
+ )
+ new_zombie.AddComponent( \
+ /datum/component/regenerator, \
+ regeneration_delay = 6 SECONDS, \
+ brute_per_second = 0.5, \
+ burn_per_second = 0.5, \
+ tox_per_second = 0.5, \
+ oxy_per_second = 0.25, \
+ heals_wounds = TRUE, \
+ )
/datum/species/zombie/infectious/on_species_loss(mob/living/carbon/human/was_zombie, datum/species/new_species, pref_load)
. = ..()
qdel(was_zombie.GetComponent(/datum/component/mutant_hands))
+ qdel(was_zombie.GetComponent(/datum/component/regenerator))
/datum/species/zombie/infectious/check_roundstart_eligible()
return FALSE
/datum/species/zombie/infectious/spec_stun(mob/living/carbon/human/H,amount)
- . = min(20, amount)
-
-/datum/species/zombie/infectious/apply_damage(damage, damagetype = BRUTE, def_zone = null, blocked, mob/living/carbon/human/H, spread_damage = FALSE, forced = FALSE, wound_bonus = 0, bare_wound_bonus = 0, sharpness = NONE, attack_direction = null, attacking_item)
- . = ..()
- if(.)
- COOLDOWN_START(src, regen_cooldown, REGENERATION_DELAY)
+ return min(2 SECONDS, amount)
/datum/species/zombie/infectious/spec_life(mob/living/carbon/carbon_mob, seconds_per_tick, times_fired)
. = ..()
carbon_mob.set_combat_mode(TRUE) // THE SUFFERING MUST FLOW
- //Zombies never actually die, they just fall down until they regenerate enough to rise back up.
- //They must be restrained, beheaded or gibbed to stop being a threat.
- if(COOLDOWN_FINISHED(src, regen_cooldown))
- var/heal_amt = heal_rate
- if(HAS_TRAIT(carbon_mob, TRAIT_CRITICAL_CONDITION))
- heal_amt *= 2
- var/need_mob_update = FALSE
- need_mob_update += carbon_mob.heal_overall_damage(heal_amt * seconds_per_tick, heal_amt * seconds_per_tick, updating_health = FALSE)
- need_mob_update += carbon_mob.adjustToxLoss(-heal_amt * seconds_per_tick, updating_health = FALSE)
- if(need_mob_update)
- carbon_mob.updatehealth()
- for(var/i in carbon_mob.all_wounds)
- var/datum/wound/iter_wound = i
- if(SPT_PROB(2-(iter_wound.severity/2), seconds_per_tick))
- iter_wound.remove_wound()
if(!HAS_TRAIT(carbon_mob, TRAIT_CRITICAL_CONDITION) && SPT_PROB(2, seconds_per_tick))
playsound(carbon_mob, pick(spooks), 50, TRUE, 10)
-//Congrats you somehow died so hard you stopped being a zombie
-/datum/species/zombie/infectious/spec_death(gibbed, mob/living/carbon/C)
- . = ..()
- var/obj/item/organ/internal/zombie_infection/infection
- infection = C.get_organ_slot(ORGAN_SLOT_ZOMBIE)
- if(infection)
- qdel(infection)
-
-/datum/species/zombie/infectious/on_species_gain(mob/living/carbon/C, datum/species/old_species)
- . = ..()
-
- // Deal with the source of this zombie corruption
- // Infection organ needs to be handled separately from mutant_organs
- // because it persists through species transitions
- var/obj/item/organ/internal/zombie_infection/infection
- infection = C.get_organ_slot(ORGAN_SLOT_ZOMBIE)
- if(!infection)
- infection = new()
- infection.Insert(C)
-
// Your skin falls off
/datum/species/human/krokodil_addict
name = "\improper Krokodil Human"
@@ -214,5 +191,3 @@
BODY_ZONE_L_LEG = /obj/item/bodypart/leg/left/zombie,
BODY_ZONE_R_LEG = /obj/item/bodypart/leg/right/zombie
)
-
-#undef REGENERATION_DELAY
diff --git a/code/modules/mob/living/carbon/human/status_procs.dm b/code/modules/mob/living/carbon/human/status_procs.dm
index 8f6dc8efeb940..6f4e8570099af 100644
--- a/code/modules/mob/living/carbon/human/status_procs.dm
+++ b/code/modules/mob/living/carbon/human/status_procs.dm
@@ -30,11 +30,3 @@
. = ..()
if(.)
update_body_parts()
-
-/mob/living/carbon/human/become_husk(source)
- if(istype(dna.species, /datum/species/skeleton)) //skeletons shouldn't be husks.
- cure_husk()
- return
- . = ..()
- if(.)
- update_body_parts()
diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm
index f3b03cdab35e0..66948253d9e35 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -711,7 +711,7 @@
///Check to see if we have the liver, if not automatically gives you last-stage effects of lacking a liver.
/mob/living/carbon/proc/handle_liver(seconds_per_tick, times_fired)
- if(!dna)
+ if(isnull(has_dna()))
return
var/obj/item/organ/internal/liver/liver = get_organ_slot(ORGAN_SLOT_LIVER)
diff --git a/code/modules/mob/living/damage_procs.dm b/code/modules/mob/living/damage_procs.dm
index 45a35bb6e158b..d5114bdcc654a 100644
--- a/code/modules/mob/living/damage_procs.dm
+++ b/code/modules/mob/living/damage_procs.dm
@@ -1,58 +1,147 @@
/**
- * Applies damage to this mob
+ * Applies damage to this mob.
*
* Sends [COMSIG_MOB_APPLY_DAMAGE]
*
* Arguuments:
- * * damage - amount of damage
- * * damagetype - one of [BRUTE], [BURN], [TOX], [OXY], [CLONE], [STAMINA]
- * * def_zone - zone that is being hit if any
- * * blocked - armor value applied
- * * forced - bypass hit percentage
- * * spread_damage - used in overrides
+ * * damage - Amount of damage
+ * * damagetype - What type of damage to do. one of [BRUTE], [BURN], [TOX], [OXY], [CLONE], [STAMINA], [BRAIN].
+ * * def_zone - What body zone is being hit. Or a reference to what bodypart is being hit.
+ * * blocked - Percent modifier to damage. 100 = 100% less damage dealt, 50% = 50% less damage dealt.
+ * * forced - "Force" exactly the damage dealt. This means it skips damage modifier from blocked.
+ * * spread_damage - For carbons, spreads the damage across all bodyparts rather than just the targeted zone.
+ * * wound_bonus - Bonus modifier for wound chance.
+ * * bare_wound_bonus - Bonus modifier for wound chance on bare skin.
+ * * sharpness - Sharpness of the weapon.
+ * * attack_direction - Direction of the attack from the attacker to [src].
+ * * attacking_item - Item that is attacking [src].
*
- * Returns TRUE if damage applied
+ * Returns the amount of damage dealt.
*/
-/mob/living/proc/apply_damage(damage = 0, damagetype = BRUTE, def_zone = null, blocked = 0, forced = FALSE, spread_damage = FALSE, wound_bonus = 0, bare_wound_bonus = 0, sharpness = NONE, attack_direction = null, attacking_item)
- SEND_SIGNAL(src, COMSIG_MOB_APPLY_DAMAGE, damage, damagetype, def_zone)
- var/hit_percent = (100-blocked)/100
- if(!damage || (!forced && hit_percent <= 0))
- return FALSE
- var/damage_amount = forced ? damage : damage * hit_percent
+/mob/living/proc/apply_damage(
+ damage = 0,
+ damagetype = BRUTE,
+ def_zone = null,
+ blocked = 0,
+ forced = FALSE,
+ spread_damage = FALSE,
+ wound_bonus = 0,
+ bare_wound_bonus = 0,
+ sharpness = NONE,
+ attack_direction = null,
+ attacking_item,
+)
+ SHOULD_CALL_PARENT(TRUE)
+ var/damage_amount = damage
+ if(!forced)
+ damage_amount *= ((100 - blocked) / 100)
+ damage_amount *= get_incoming_damage_modifier(damage_amount, damagetype, def_zone, sharpness, attack_direction, attacking_item)
+ if(damage_amount <= 0)
+ return 0
+
+ SEND_SIGNAL(src, COMSIG_MOB_APPLY_DAMAGE, damage_amount, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction, attacking_item)
+
+ var/damage_dealt = 0
switch(damagetype)
if(BRUTE)
- adjustBruteLoss(damage_amount, forced = forced)
+ if(isbodypart(def_zone))
+ var/obj/item/bodypart/actual_hit = def_zone
+ var/delta = actual_hit.get_damage()
+ if(actual_hit.receive_damage(
+ brute = damage_amount,
+ burn = 0,
+ forced = forced,
+ wound_bonus = wound_bonus,
+ bare_wound_bonus = bare_wound_bonus,
+ sharpness = sharpness,
+ attack_direction = attack_direction,
+ damage_source = attacking_item,
+ ))
+ update_damage_overlays()
+ damage_dealt = actual_hit.get_damage() - delta // Unfortunately bodypart receive_damage doesn't return damage dealt so we do it manually
+ else
+ damage_dealt = adjustBruteLoss(damage_amount, forced = forced)
if(BURN)
- adjustFireLoss(damage_amount, forced = forced)
+ if(isbodypart(def_zone))
+ var/obj/item/bodypart/actual_hit = def_zone
+ var/delta = actual_hit.get_damage()
+ if(actual_hit.receive_damage(
+ brute = 0,
+ burn = damage_amount,
+ forced = forced,
+ wound_bonus = wound_bonus,
+ bare_wound_bonus = bare_wound_bonus,
+ sharpness = sharpness,
+ attack_direction = attack_direction,
+ damage_source = attacking_item,
+ ))
+ update_damage_overlays()
+ damage_dealt = delta - actual_hit.get_damage() // See above
+ else
+ damage_dealt = adjustFireLoss(damage_amount, forced = forced)
if(TOX)
- adjustToxLoss(damage_amount, forced = forced)
+ damage_dealt = adjustToxLoss(damage_amount, forced = forced)
if(OXY)
- adjustOxyLoss(damage_amount, forced = forced)
+ damage_dealt = adjustOxyLoss(damage_amount, forced = forced)
if(CLONE)
- adjustCloneLoss(damage_amount, forced = forced)
+ damage_dealt = adjustCloneLoss(damage_amount, forced = forced)
if(STAMINA)
- adjustStaminaLoss(damage_amount, forced = forced)
- SEND_SIGNAL(src, COMSIG_MOB_AFTER_APPLY_DAMAGE, damage, damagetype, def_zone)
- return TRUE
+ damage_dealt = adjustStaminaLoss(damage_amount, forced = forced)
+ if(BRAIN)
+ damage_dealt = adjustOrganLoss(ORGAN_SLOT_BRAIN, damage_amount)
+
+ SEND_SIGNAL(src, COMSIG_MOB_AFTER_APPLY_DAMAGE, damage_dealt, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction, attacking_item)
+ return damage_dealt
+
+/**
+ * Used in tandem with [/mob/living/proc/apply_damage] to calculate modifier applied into incoming damage
+ */
+/mob/living/proc/get_incoming_damage_modifier(
+ damage = 0,
+ damagetype = BRUTE,
+ def_zone = null,
+ sharpness = NONE,
+ attack_direction = null,
+ attacking_item,
+)
+ SHOULD_CALL_PARENT(TRUE)
+ SHOULD_BE_PURE(TRUE)
+
+ var/list/damage_mods = list()
+ SEND_SIGNAL(src, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS, damage_mods, damage, damagetype, def_zone, sharpness, attack_direction, attacking_item)
+
+ var/final_mod = 1
+ for(var/new_mod in damage_mods)
+ final_mod *= new_mod
+ return final_mod
+
+/**
+ * Simply a wrapper for calling mob adjustXLoss() procs to heal a certain damage type,
+ * when you don't know what damage type you're healing exactly.
+ */
+/mob/living/proc/heal_damage_type(heal_amount = 0, damagetype = BRUTE)
+ heal_amount = abs(heal_amount) * -1
-///like [apply_damage][/mob/living/proc/apply_damage] except it always uses the damage procs
-/mob/living/proc/apply_damage_type(damage = 0, damagetype = BRUTE)
switch(damagetype)
if(BRUTE)
- return adjustBruteLoss(damage)
+ return adjustBruteLoss(heal_amount)
if(BURN)
- return adjustFireLoss(damage)
+ return adjustFireLoss(heal_amount)
if(TOX)
- return adjustToxLoss(damage)
+ return adjustToxLoss(heal_amount)
if(OXY)
- return adjustOxyLoss(damage)
+ return adjustOxyLoss(heal_amount)
if(CLONE)
- return adjustCloneLoss(damage)
+ return adjustCloneLoss(heal_amount)
if(STAMINA)
- return adjustStaminaLoss(damage)
+ return adjustStaminaLoss(heal_amount)
/// return the damage amount for the type given
+/**
+ * Simply a wrapper for calling mob getXLoss() procs to get a certain damage type,
+ * when you don't know what damage type you're getting exactly.
+ */
/mob/living/proc/get_current_damage_of_type(damagetype = BRUTE)
switch(damagetype)
if(BRUTE)
@@ -68,26 +157,38 @@
if(STAMINA)
return getStaminaLoss()
-/// applies multiple damages at once via [/mob/living/proc/apply_damage]
-/mob/living/proc/apply_damages(brute = 0, burn = 0, tox = 0, oxy = 0, clone = 0, def_zone = null, blocked = FALSE, stamina = 0, brain = 0)
- if(blocked >= 100)
- return 0
+/// return the total damage of all types which update your health
+/mob/living/proc/get_total_damage(precision = DAMAGE_PRECISION)
+ return round(getBruteLoss() + getFireLoss() + getToxLoss() + getOxyLoss() + getCloneLoss(), precision)
+
+/// Applies multiple damages at once via [apply_damage][/mob/living/proc/apply_damage]
+/mob/living/proc/apply_damages(
+ brute = 0,
+ burn = 0,
+ tox = 0,
+ oxy = 0,
+ clone = 0,
+ def_zone = null,
+ blocked = 0,
+ stamina = 0,
+ brain = 0,
+)
+ var/total_damage = 0
if(brute)
- apply_damage(brute, BRUTE, def_zone, blocked)
+ total_damage += apply_damage(brute, BRUTE, def_zone, blocked)
if(burn)
- apply_damage(burn, BURN, def_zone, blocked)
+ total_damage += apply_damage(burn, BURN, def_zone, blocked)
if(tox)
- apply_damage(tox, TOX, def_zone, blocked)
+ total_damage += apply_damage(tox, TOX, def_zone, blocked)
if(oxy)
- apply_damage(oxy, OXY, def_zone, blocked)
+ total_damage += apply_damage(oxy, OXY, def_zone, blocked)
if(clone)
- apply_damage(clone, CLONE, def_zone, blocked)
+ total_damage += apply_damage(clone, CLONE, def_zone, blocked)
if(stamina)
- apply_damage(stamina, STAMINA, def_zone, blocked)
+ total_damage += apply_damage(stamina, STAMINA, def_zone, blocked)
if(brain)
- apply_damage(brain, BRAIN, def_zone, blocked)
- return 1
-
+ total_damage += apply_damage(brain, BRAIN, def_zone, blocked)
+ return total_damage
/// applies various common status effects or common hardcoded mob effects
/mob/living/proc/apply_effect(effect = 0,effecttype = EFFECT_STUN, blocked = 0)
@@ -246,7 +347,7 @@
/mob/living/proc/getToxLoss()
return toxloss
-/mob/living/proc/can_adjust_tox_loss(amount, forced, required_biotype)
+/mob/living/proc/can_adjust_tox_loss(amount, forced, required_biotype = ALL)
if(!forced && ((status_flags & GODMODE) || !(mob_biotypes & required_biotype)))
return FALSE
if(SEND_SIGNAL(src, COMSIG_LIVING_ADJUST_TOX_DAMAGE, TOX, amount, forced) & COMPONENT_IGNORE_CHANGE)
@@ -312,7 +413,7 @@
/mob/living/proc/getCloneLoss()
return cloneloss
-/mob/living/proc/can_adjust_clone_loss(amount, forced, required_biotype)
+/mob/living/proc/can_adjust_clone_loss(amount, forced, required_biotype = ALL)
if(!forced && (!(mob_biotypes & required_biotype) || status_flags & GODMODE || HAS_TRAIT(src, TRAIT_NOCLONELOSS)))
return FALSE
if(SEND_SIGNAL(src, COMSIG_LIVING_ADJUST_CLONE_DAMAGE, CLONE, amount, forced) & COMPONENT_IGNORE_CHANGE)
@@ -355,7 +456,7 @@
/mob/living/proc/getStaminaLoss()
return staminaloss
-/mob/living/proc/can_adjust_stamina_loss(amount, forced, required_biotype)
+/mob/living/proc/can_adjust_stamina_loss(amount, forced, required_biotype = ALL)
if(!forced && (!(mob_biotypes & required_biotype) || status_flags & GODMODE))
return FALSE
if(SEND_SIGNAL(src, COMSIG_LIVING_ADJUST_STAMINA_DAMAGE, STAMINA, amount, forced) & COMPONENT_IGNORE_CHANGE)
@@ -428,11 +529,11 @@
///heal up to amount damage, in a given order
/mob/living/proc/heal_ordered_damage(amount, list/damage_types)
- . = FALSE //we'll return the amount of damage healed
+ . = 0 //we'll return the amount of damage healed
for(var/damagetype in damage_types)
var/amount_to_heal = min(abs(amount), get_current_damage_of_type(damagetype)) //heal only up to the amount of damage we have
if(amount_to_heal)
- . += apply_damage_type(-amount_to_heal, damagetype)
+ . += heal_damage_type(amount_to_heal, damagetype)
amount -= amount_to_heal //remove what we healed from our current amount
if(!amount)
break
diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm
index ea7d99986310d..09478e995a130 100644
--- a/code/modules/mob/living/emote.dm
+++ b/code/modules/mob/living/emote.dm
@@ -107,9 +107,14 @@
message_animal_or_basic = custom_message
. = ..()
message_animal_or_basic = initial(message_animal_or_basic)
- if(. && user.death_sound)
- if(!user.can_speak() || user.oxyloss >= 50)
- return //stop the sound if oxyloss too high/cant speak
+ if(!. && !user.can_speak() || user.getOxyLoss() >= 50)
+ return //stop the sound if oxyloss too high/cant speak
+ var/mob/living/carbon/carbon_user = user
+ // For masks that give unique death sounds
+ if(istype(carbon_user) && isclothing(carbon_user.wear_mask) && carbon_user.wear_mask.unique_death)
+ playsound(carbon_user, carbon_user.wear_mask.unique_death, 200, TRUE, TRUE)
+ return
+ if(user.death_sound)
playsound(user, user.death_sound, 200, TRUE, TRUE)
/datum/emote/living/drool
@@ -547,7 +552,7 @@
if(!isliving(user))
return
- if(!TIMER_COOLDOWN_CHECK(user, COOLDOWN_YAWN_PROPAGATION))
+ if(TIMER_COOLDOWN_FINISHED(user, COOLDOWN_YAWN_PROPAGATION))
TIMER_COOLDOWN_START(user, COOLDOWN_YAWN_PROPAGATION, cooldown * 3)
var/mob/living/carbon/carbon_user = user
@@ -557,7 +562,7 @@
var/propagation_distance = user.client ? 5 : 2 // mindless mobs are less able to spread yawns
for(var/mob/living/iter_living in view(user, propagation_distance))
- if(IS_DEAD_OR_INCAP(iter_living) || TIMER_COOLDOWN_CHECK(iter_living, COOLDOWN_YAWN_PROPAGATION))
+ if(IS_DEAD_OR_INCAP(iter_living) || TIMER_COOLDOWN_RUNNING(iter_living, COOLDOWN_YAWN_PROPAGATION))
continue
var/dist_between = get_dist(user, iter_living)
@@ -576,7 +581,7 @@
/// This yawn has been triggered by someone else yawning specifically, likely after a delay. Check again if they don't have the yawned recently trait
/datum/emote/living/yawn/proc/propagate_yawn(mob/user)
- if(!istype(user) || TIMER_COOLDOWN_CHECK(user, COOLDOWN_YAWN_PROPAGATION))
+ if(!istype(user) || TIMER_COOLDOWN_RUNNING(user, COOLDOWN_YAWN_PROPAGATION))
return
user.emote("yawn")
diff --git a/code/modules/mob/living/inhand_holder.dm b/code/modules/mob/living/inhand_holder.dm
index c018b3c029bfe..b4c9fbd34aa1b 100644
--- a/code/modules/mob/living/inhand_holder.dm
+++ b/code/modules/mob/living/inhand_holder.dm
@@ -114,11 +114,11 @@
desc = "This drone is scared and has curled up into a ball!"
/obj/item/clothing/head/mob_holder/drone/update_visuals(mob/living/L)
- var/mob/living/simple_animal/drone/D = L
- if(!D)
+ var/mob/living/basic/drone/drone = L
+ if(!drone)
return ..()
icon = 'icons/mob/silicon/drone.dmi'
- icon_state = "[D.visualAppearance]_hat"
+ icon_state = "[drone.visualAppearance]_hat"
/obj/item/clothing/head/mob_holder/destructible
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index d2ccf6e8f87d0..71a818706b581 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -540,6 +540,15 @@
if(held_item)
. = held_item.GetID()
+/**
+ * Returns the access list for this mob
+ */
+/mob/living/proc/get_access()
+ var/obj/item/card/id/id = get_idcard()
+ if(isnull(id))
+ return list()
+ return id.GetAccess()
+
/mob/living/proc/get_id_in_hand()
var/obj/item/held_item = get_active_held_item()
if(!held_item)
@@ -825,7 +834,7 @@
// If they happen to be dead too, try to revive them - if possible.
if(stat == DEAD && can_be_revived())
// If the revive is successful, show our revival message (if present).
- if(revive(FALSE, FALSE, 10) && revive_message)
+ if(revive(excess_healing = 10) && revive_message)
visible_message(revive_message)
// Finally update health again after we're all done
@@ -1324,7 +1333,7 @@
add_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED, TRAIT_NO_TRANSFORM), MAGIC_TRAIT)
icon = null
cut_overlays()
- invisibility = INVISIBILITY_ABSTRACT
+ SetInvisibility(INVISIBILITY_ABSTRACT)
var/list/item_contents = list()
@@ -1363,7 +1372,7 @@
if(WABBAJACK_ROBOT)
var/static/list/robot_options = list(
/mob/living/silicon/robot = 200,
- /mob/living/simple_animal/drone/polymorphed = 200,
+ /mob/living/basic/drone/polymorphed = 200,
/mob/living/silicon/robot/model/syndicate = 1,
/mob/living/silicon/robot/model/syndicate/medical = 1,
/mob/living/silicon/robot/model/syndicate/saboteur = 1,
@@ -1374,7 +1383,7 @@
if(issilicon(new_mob))
var/mob/living/silicon/robot/created_robot = new_mob
new_mob.gender = gender
- new_mob.invisibility = 0
+ new_mob.SetInvisibility(INVISIBILITY_NONE)
new_mob.job = JOB_CYBORG
created_robot.lawupdate = FALSE
created_robot.connected_ai = null
@@ -1527,9 +1536,9 @@ GLOBAL_LIST_EMPTY(fire_appearances)
return fire_status.ignite(silent)
/mob/living/proc/update_fire()
- var/datum/status_effect/fire_handler/fire_handler = has_status_effect(/datum/status_effect/fire_handler)
- if(fire_handler)
- fire_handler.update_overlay()
+ var/datum/status_effect/fire_handler/fire_stacks/fire_stacks = has_status_effect(/datum/status_effect/fire_handler/fire_stacks)
+ if(fire_stacks)
+ fire_stacks.update_overlay()
/**
* Extinguish all fire on the mob
@@ -1737,10 +1746,10 @@ GLOBAL_LIST_EMPTY(fire_appearances)
return//dont open the mobs inventory if you are picking them up
. = ..()
-/mob/living/proc/mob_pickup(mob/living/L)
+/mob/living/proc/mob_pickup(mob/living/user)
var/obj/item/clothing/head/mob_holder/holder = new(get_turf(src), src, held_state, head_icon, held_lh, held_rh, worn_slot_flags)
- L.visible_message(span_warning("[L] scoops up [src]!"))
- L.put_in_hands(holder)
+ user.visible_message(span_warning("[user] scoops up [src]!"))
+ user.put_in_hands(holder)
/mob/living/proc/set_name()
numba = rand(1, 1000)
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index f31fd60b131fc..933a5bef3157f 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -91,22 +91,44 @@
/mob/living/proc/is_ears_covered()
return null
-/mob/living/proc/on_hit(obj/projectile/P)
- return BULLET_ACT_HIT
-
-/mob/living/bullet_act(obj/projectile/P, def_zone, piercing_hit = FALSE)
+/mob/living/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
. = ..()
- if(P.is_hostile_projectile() && (. != BULLET_ACT_BLOCK))
- var/attack_direction = get_dir(P.starting, src)
- // we need a second, silent armor check to actually know how much to reduce damage taken, as opposed to
- // on [/atom/proc/bullet_act] where it's just to pass it to the projectile's on_hit().
- var/armor_check = check_projectile_armor(def_zone, P, is_silent = TRUE)
- armor_check = min(ARMOR_MAX_BLOCK, armor_check) //cap damage reduction at 90%
- apply_damage(P.damage, P.damage_type, def_zone, armor_check, wound_bonus=P.wound_bonus, bare_wound_bonus=P.bare_wound_bonus, sharpness = P.sharpness, attack_direction = attack_direction)
- apply_effects(P.stun, P.knockdown, P.unconscious, P.slur, P.stutter, P.eyeblur, P.drowsy, armor_check, P.stamina, P.jitter, P.paralyze, P.immobilize)
- if(P.dismemberment)
- check_projectile_dismemberment(P, def_zone)
- return . ? BULLET_ACT_HIT : BULLET_ACT_BLOCK
+ if(. != BULLET_ACT_HIT)
+ return .
+ if(!hitting_projectile.is_hostile_projectile())
+ return BULLET_ACT_HIT
+
+ // we need a second, silent armor check to actually know how much to reduce damage taken, as opposed to
+ // on [/atom/proc/bullet_act] where it's just to pass it to the projectile's on_hit().
+ var/armor_check = check_projectile_armor(def_zone, hitting_projectile, is_silent = TRUE)
+
+ apply_damage(
+ damage = hitting_projectile.damage,
+ damagetype = hitting_projectile.damage_type,
+ def_zone = def_zone,
+ blocked = min(ARMOR_MAX_BLOCK, armor_check), //cap damage reduction at 90%
+ wound_bonus = hitting_projectile.wound_bonus,
+ bare_wound_bonus = hitting_projectile.bare_wound_bonus,
+ sharpness = hitting_projectile.sharpness,
+ attack_direction = get_dir(hitting_projectile.starting, src),
+ )
+ apply_effects(
+ stun = hitting_projectile.stun,
+ knockdown = hitting_projectile.knockdown,
+ unconscious = hitting_projectile.unconscious,
+ slur = (mob_biotypes & MOB_ROBOTIC) ? 0 SECONDS : hitting_projectile.slur, // Don't want your cyborgs to slur from being ebow'd
+ stutter = (mob_biotypes & MOB_ROBOTIC) ? 0 SECONDS : hitting_projectile.stutter, // Don't want your cyborgs to stutter from being tazed
+ eyeblur = hitting_projectile.eyeblur,
+ drowsy = hitting_projectile.drowsy,
+ blocked = armor_check,
+ stamina = hitting_projectile.stamina,
+ jitter = (mob_biotypes & MOB_ROBOTIC) ? 0 SECONDS : hitting_projectile.jitter, // Cyborgs can jitter but not from being shot
+ paralyze = hitting_projectile.paralyze,
+ immobilize = hitting_projectile.immobilize,
+ )
+ if(hitting_projectile.dismemberment)
+ check_projectile_dismemberment(hitting_projectile, def_zone)
+ return BULLET_ACT_HIT
/mob/living/check_projectile_armor(def_zone, obj/projectile/impacting_projectile, is_silent)
return run_armor_check(def_zone, impacting_projectile.armor_flag, "","",impacting_projectile.armour_penetration, "", is_silent, impacting_projectile.weak_against_armour)
@@ -309,10 +331,6 @@
return martial_result
/mob/living/attack_paw(mob/living/carbon/human/user, list/modifiers)
- if(isturf(loc) && istype(loc.loc, /area/misc/start))
- to_chat(user, "No attacking people at spawn, you jackass.")
- return FALSE
-
var/martial_result = user.apply_martial_art(src, modifiers)
if (martial_result != MARTIAL_ATTACK_INVALID)
return martial_result
@@ -327,11 +345,13 @@
to_chat(user, span_warning("You don't want to hurt anyone!"))
return FALSE
+ if(!user.get_bodypart(BODY_ZONE_HEAD))
+ return FALSE
if(user.is_muzzled() || user.is_mouth_covered(ITEM_SLOT_MASK))
to_chat(user, span_warning("You can't bite with your mouth covered!"))
return FALSE
user.do_attack_animation(src, ATTACK_EFFECT_BITE)
- if (prob(75))
+ if (HAS_TRAIT(user, TRAIT_PERFECT_ATTACKER) || prob(75))
log_combat(user, src, "attacked")
playsound(loc, 'sound/weapons/bite.ogg', 50, TRUE, -1)
visible_message(span_danger("[user.name] bites [src]!"), \
@@ -429,8 +449,8 @@
. = ..()
if(. & EMP_PROTECT_CONTENTS)
return
- for(var/obj/O in contents)
- O.emp_act(severity)
+ for(var/obj/inside in contents)
+ inside.emp_act(severity)
///Logs, gibs and returns point values of whatever mob is unfortunate enough to get eaten.
/mob/living/singularity_act()
@@ -456,13 +476,13 @@
else
switch(rand(1, 4))
if(1)
- new /mob/living/simple_animal/hostile/construct/juggernaut/hostile(get_turf(src))
+ new /mob/living/basic/construct/juggernaut/hostile(get_turf(src))
if(2)
- new /mob/living/simple_animal/hostile/construct/wraith/hostile(get_turf(src))
+ new /mob/living/basic/construct/wraith/hostile(get_turf(src))
if(3)
- new /mob/living/simple_animal/hostile/construct/artificer/hostile(get_turf(src))
+ new /mob/living/basic/construct/artificer/hostile(get_turf(src))
if(4)
- new /mob/living/simple_animal/hostile/construct/proteon/hostile(get_turf(src))
+ new /mob/living/basic/construct/proteon/hostile(get_turf(src))
spawn_dust()
investigate_log("has been gibbed by Nar'Sie.", INVESTIGATE_DEATHS)
gib()
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index e9d306875c9ec..766a7ef89a814 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -225,6 +225,3 @@
/// What our current gravity state is. Used to avoid duplicate animates and such
var/gravity_state = null
-
- /// Whether this mob can be mutated into a cybercop via quantum server get_valid_domain_targets(). Specifically dodges megafauna
- var/can_be_cybercop = TRUE
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index 26d0dd01a4db4..6ce7907abbb10 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -142,7 +142,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
say_dead(original_message)
return
- if(HAS_TRAIT(src, TRAIT_SOFTSPOKEN))
+ if(HAS_TRAIT(src, TRAIT_SOFTSPOKEN) && !HAS_TRAIT(src, TRAIT_SIGN_LANG)) // softspoken trait only applies to spoken languages
message_mods[WHISPER_MODE] = MODE_WHISPER
if(client && SSlag_switch.measures[SLOWMODE_SAY] && !HAS_TRAIT(src, TRAIT_BYPASS_MEASURES) && !forced && src == usr)
diff --git a/code/modules/mob/living/navigation.dm b/code/modules/mob/living/navigation.dm
index 44847a239c160..98f1080c79192 100644
--- a/code/modules/mob/living/navigation.dm
+++ b/code/modules/mob/living/navigation.dm
@@ -63,7 +63,7 @@
stack_trace("Navigate target ([navigate_target]) is not an atom, somehow.")
return
- var/list/path = get_path_to(src, navigate_target, MAX_NAVIGATE_RANGE, mintargetdist = 1, id = get_idcard(), skip_first = FALSE)
+ var/list/path = get_path_to(src, navigate_target, MAX_NAVIGATE_RANGE, mintargetdist = 1, access = get_access(), skip_first = FALSE)
if(!length(path))
balloon_alert(src, "no valid path with current access!")
return
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 3bb419998d10b..fdd10e43ab46a 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -1113,7 +1113,6 @@
if(deployed_shell) //Forcibly call back AI in event of things such as damage, EMP or power loss.
to_chat(src, span_danger("Your remote connection has been reset!"))
deployed_shell.undeploy()
- UnregisterSignal(deployed_shell, COMSIG_LIVING_DEATH)
diag_hud_set_deployed()
/mob/living/silicon/ai/resist()
diff --git a/code/modules/mob/living/silicon/ai/ai_defense.dm b/code/modules/mob/living/silicon/ai/ai_defense.dm
index 7445815c9d77b..ad9a965242e01 100644
--- a/code/modules/mob/living/silicon/ai/ai_defense.dm
+++ b/code/modules/mob/living/silicon/ai/ai_defense.dm
@@ -54,10 +54,6 @@
return TRUE
-/mob/living/silicon/ai/bullet_act(obj/projectile/Proj)
- . = ..(Proj)
- updatehealth()
-
/mob/living/silicon/ai/flash_act(intensity = 1, override_blindness_check = 0, affect_silicon = 0, visual = 0, type = /atom/movable/screen/fullscreen/flash, length = 25)
return // no eyes, no flashing
diff --git a/code/modules/mob/living/silicon/ai/freelook/eye.dm b/code/modules/mob/living/silicon/ai/freelook/eye.dm
index 6204ecdea6054..416bbb19912e8 100644
--- a/code/modules/mob/living/silicon/ai/freelook/eye.dm
+++ b/code/modules/mob/living/silicon/ai/freelook/eye.dm
@@ -218,7 +218,10 @@
if(!eyeobj)
return
eyeobj.mouse_opacity = state ? MOUSE_OPACITY_ICON : initial(eyeobj.mouse_opacity)
- eyeobj.invisibility = state ? INVISIBILITY_OBSERVER : initial(eyeobj.invisibility)
+ if(state)
+ eyeobj.SetInvisibility(INVISIBILITY_OBSERVER, id=type)
+ else
+ eyeobj.RemoveInvisibility(type)
/mob/living/silicon/ai/verb/toggle_acceleration()
set category = "AI Commands"
diff --git a/code/modules/mob/living/silicon/damage_procs.dm b/code/modules/mob/living/silicon/damage_procs.dm
index 4fe6e688632cb..16f6b8abb42f7 100644
--- a/code/modules/mob/living/silicon/damage_procs.dm
+++ b/code/modules/mob/living/silicon/damage_procs.dm
@@ -1,17 +1,3 @@
-
-/mob/living/silicon/apply_damage(damage = 0, damagetype = BRUTE, def_zone = null, blocked = 0, forced = FALSE, spread_damage = FALSE, wound_bonus = 0, bare_wound_bonus = 0, sharpness = NONE, attack_direction = null, attacking_item)
- var/hit_percent = (100-blocked)/100
- if((!damage || (!forced && hit_percent <= 0)))
- return 0
- var/damage_amount = forced ? damage : damage * hit_percent
- switch(damagetype)
- if(BRUTE)
- adjustBruteLoss(damage_amount, forced = forced)
- if(BURN)
- adjustFireLoss(damage_amount, forced = forced)
- return 1
-
-
/mob/living/silicon/apply_effect(effect = 0,effecttype = EFFECT_STUN, blocked = FALSE)
return FALSE //The only effect that can hit them atm is flashes and they still directly edit so this works for now. (This was written in at least 2016. Help)
diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm
index 24e40c38ae5f7..182aeb870a98c 100644
--- a/code/modules/mob/living/silicon/robot/robot.dm
+++ b/code/modules/mob/living/silicon/robot/robot.dm
@@ -60,7 +60,9 @@
//If this body is meant to be a borg controlled by the AI player
if(shell)
- make_shell()
+ var/obj/item/borg/upgrade/ai/board = new(src)
+ make_shell(board)
+ add_to_upgrades(board)
else
//MMI stuff. Held togheter by magic. ~Miauw
if(!mmi?.brainmob)
@@ -781,8 +783,10 @@
if(gone == mmi)
mmi = null
-///Use this to add upgrades to robots. It'll register signals for when the upgrade is moved or deleted, if not single use.
-/mob/living/silicon/robot/proc/add_to_upgrades(obj/item/borg/upgrade/new_upgrade, mob/user)
+///Called when a mob uses an upgrade on an open borg. Checks to make sure the upgrade can be applied
+/mob/living/silicon/robot/proc/apply_upgrade(obj/item/borg/upgrade/new_upgrade, mob/user)
+ if(isnull(user))
+ return FALSE
if(new_upgrade in upgrades)
return FALSE
if(!user.temporarilyRemoveItemFromInventory(new_upgrade)) //calling the upgrade's dropped() proc /before/ we add action buttons
@@ -792,6 +796,10 @@
new_upgrade.forceMove(loc) //gets lost otherwise
return FALSE
to_chat(user, span_notice("You apply the upgrade to [src]."))
+ add_to_upgrades(new_upgrade)
+
+///Moves the upgrade inside the robot and registers relevant signals.
+/mob/living/silicon/robot/proc/add_to_upgrades(obj/item/borg/upgrade/new_upgrade)
to_chat(src, "----------------\nNew hardware detected...Identified as \"[new_upgrade]\"...Setup complete.\n----------------")
if(new_upgrade.one_use)
logevent("Firmware [new_upgrade] run successfully.")
@@ -827,8 +835,10 @@
* * board - B.O.R.I.S. module board used for transforming the cyborg into AI shell
*/
/mob/living/silicon/robot/proc/make_shell(obj/item/borg/upgrade/ai/board)
- if(!board)
- upgrades |= new /obj/item/borg/upgrade/ai(src)
+ if(isnull(board))
+ stack_trace("make_shell was called without a board argument! This is never supposed to happen!")
+ return FALSE
+
shell = TRUE
braintype = "AI Shell"
name = "Empty AI Shell-[ident]"
@@ -893,15 +903,16 @@
/datum/action/innate/undeployment/Trigger(trigger_flags)
if(!..())
return FALSE
- var/mob/living/silicon/robot/R = owner
+ var/mob/living/silicon/robot/shell_to_disconnect = owner
- R.undeploy()
+ shell_to_disconnect.undeploy()
return TRUE
/mob/living/silicon/robot/proc/undeploy()
if(!deployed || !mind || !mainframe)
return
+ mainframe.UnregisterSignal(src, COMSIG_LIVING_DEATH)
mainframe.redeploy_action.Grant(mainframe)
mainframe.redeploy_action.last_used_shell = src
mind.transfer_to(mainframe)
diff --git a/code/modules/mob/living/silicon/robot/robot_defense.dm b/code/modules/mob/living/silicon/robot/robot_defense.dm
index 09440ec22a3f0..b9d3304ef8de7 100644
--- a/code/modules/mob/living/silicon/robot/robot_defense.dm
+++ b/code/modules/mob/living/silicon/robot/robot_defense.dm
@@ -78,7 +78,7 @@ GLOBAL_LIST_INIT(blacklisted_borg_hats, typecacheof(list( //Hats that don't real
to_chat(user, span_warning("[src] already has a defibrillator!"))
return
var/obj/item/borg/upgrade/defib/backpack/B = new(null, D)
- add_to_upgrades(B, user)
+ apply_upgrade(B, user)
return
if(istype(W, /obj/item/ai_module))
@@ -142,7 +142,7 @@ GLOBAL_LIST_INIT(blacklisted_borg_hats, typecacheof(list( //Hats that don't real
if(!user.canUnEquip(U))
to_chat(user, span_warning("The upgrade is stuck to you and you can't seem to let go of it!"))
return
- add_to_upgrades(U, user)
+ apply_upgrade(U, user)
return
if(istype(W, /obj/item/toner))
@@ -232,8 +232,8 @@ GLOBAL_LIST_INIT(blacklisted_borg_hats, typecacheof(list( //Hats that don't real
return
cell.update_appearance()
cell.add_fingerprint(user)
- user.put_in_active_hand(cell)
to_chat(user, span_notice("You remove \the [cell]."))
+ user.put_in_active_hand(cell)
update_icons()
diag_hud_set_borgcell()
@@ -433,11 +433,15 @@ GLOBAL_LIST_INIT(blacklisted_borg_hats, typecacheof(list( //Hats that don't real
return TRUE
-/mob/living/silicon/robot/bullet_act(obj/projectile/Proj, def_zone)
+/mob/living/silicon/robot/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
. = ..()
- updatehealth()
- if(prob(75) && Proj.damage > 0)
- spark_system.start()
+ if(prob(25) || . != BULLET_ACT_HIT)
+ return
+ if(hitting_projectile.damage_type != BRUTE && hitting_projectile.damage_type != BURN)
+ return
+ if(!hitting_projectile.is_hostile_projectile() || hitting_projectile.damage <= 0)
+ return
+ spark_system.start()
/mob/living/silicon/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
. = ..()
diff --git a/code/modules/mob/living/silicon/silicon_defense.dm b/code/modules/mob/living/silicon/silicon_defense.dm
index 847e686752150..75cbd83f54fec 100644
--- a/code/modules/mob/living/silicon/silicon_defense.dm
+++ b/code/modules/mob/living/silicon/silicon_defense.dm
@@ -80,13 +80,13 @@
user.add_mood_event("pet_borg", /datum/mood_event/pet_borg)
-/mob/living/silicon/attack_drone(mob/living/simple_animal/drone/M)
- if(M.combat_mode)
+/mob/living/silicon/attack_drone(mob/living/basic/drone/user)
+ if(user.combat_mode)
return
return ..()
-/mob/living/silicon/attack_drone_secondary(mob/living/simple_animal/drone/M)
- if(M.combat_mode)
+/mob/living/silicon/attack_drone_secondary(mob/living/basic/drone/user)
+ if(user.combat_mode)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
return ..()
@@ -115,21 +115,22 @@
M.visible_message(span_boldwarning("[M] is thrown off of [src]!"))
flash_act(affect_silicon = 1)
-/mob/living/silicon/bullet_act(obj/projectile/Proj, def_zone, piercing_hit = FALSE)
- SEND_SIGNAL(src, COMSIG_ATOM_BULLET_ACT, Proj, def_zone)
- if((Proj.damage_type == BRUTE || Proj.damage_type == BURN))
- adjustBruteLoss(Proj.damage)
- if(prob(Proj.damage*1.5))
- for(var/mob/living/M in buckled_mobs)
- M.visible_message(span_boldwarning("[M] is knocked off of [src]!"))
- unbuckle_mob(M)
- M.Paralyze(40)
- if(Proj.stun || Proj.knockdown || Proj.paralyze)
- for(var/mob/living/M in buckled_mobs)
- unbuckle_mob(M)
- M.visible_message(span_boldwarning("[M] is knocked off of [src] by the [Proj]!"))
- Proj.on_hit(src, 0, piercing_hit)
- return BULLET_ACT_HIT
+/mob/living/silicon/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
+ . = ..()
+ if(. != BULLET_ACT_HIT)
+ return .
+
+ var/prob_of_knocking_dudes_off = 0
+ if(hitting_projectile.damage_type == BRUTE || hitting_projectile.damage_type == BURN)
+ prob_of_knocking_dudes_off = hitting_projectile.damage * 1.5
+ if(hitting_projectile.stun || hitting_projectile.knockdown || hitting_projectile.paralyze)
+ prob_of_knocking_dudes_off = 100
+
+ if(prob(prob_of_knocking_dudes_off))
+ for(var/mob/living/buckled in buckled_mobs)
+ buckled.visible_message(span_boldwarning("[buckled] is knocked off of [src] by [hitting_projectile]!"))
+ unbuckle_mob(buckled)
+ buckled.Paralyze(4 SECONDS)
/mob/living/silicon/flash_act(intensity = 1, override_blindness_check = 0, affect_silicon = 0, visual = 0, type = /atom/movable/screen/fullscreen/flash/static, length = 25)
if(affect_silicon)
diff --git a/code/modules/mob/living/simple_animal/animal_defense.dm b/code/modules/mob/living/simple_animal/animal_defense.dm
index df97740e69cfc..103ea7aa147e5 100644
--- a/code/modules/mob/living/simple_animal/animal_defense.dm
+++ b/code/modules/mob/living/simple_animal/animal_defense.dm
@@ -98,20 +98,20 @@
var/damage = rand(user.melee_damage_lower, user.melee_damage_upper)
return attack_threshold_check(damage, user.melee_damage_type)
-/mob/living/simple_animal/attack_slime(mob/living/simple_animal/slime/M, list/modifiers)
+/mob/living/simple_animal/attack_slime(mob/living/simple_animal/slime/user, list/modifiers)
if(..()) //successful slime attack
var/damage = rand(15, 25)
- if(M.is_adult)
+ if(user.is_adult)
damage = rand(20, 35)
return attack_threshold_check(damage)
-/mob/living/simple_animal/attack_drone(mob/living/simple_animal/drone/M)
- if(M.combat_mode) //No kicking dogs even as a rogue drone. Use a weapon.
+/mob/living/simple_animal/attack_drone(mob/living/basic/drone/user)
+ if(user.combat_mode) //No kicking dogs even as a rogue drone. Use a weapon.
return
return ..()
-/mob/living/simple_animal/attack_drone_secondary(mob/living/simple_animal/drone/M)
- if(M.combat_mode)
+/mob/living/simple_animal/attack_drone_secondary(mob/living/basic/drone/user)
+ if(user.combat_mode)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
return ..()
diff --git a/code/modules/mob/living/simple_animal/bot/SuperBeepsky.dm b/code/modules/mob/living/simple_animal/bot/SuperBeepsky.dm
index 828be3deebcba..f2a2f9fbb65bd 100644
--- a/code/modules/mob/living/simple_animal/bot/SuperBeepsky.dm
+++ b/code/modules/mob/living/simple_animal/bot/SuperBeepsky.dm
@@ -12,6 +12,9 @@
var/block_chance = 50
+/mob/living/simple_animal/bot/secbot/grievous/Initialize(mapload)
+ . = ..()
+ RegisterSignal(src, COMSIG_ATOM_PRE_BULLET_ACT, PROC_REF(block_bullets))
/mob/living/simple_animal/bot/secbot/grievous/toy //A toy version of general beepsky!
name = "Genewul Bweepskee"
@@ -21,10 +24,15 @@
baton_type = /obj/item/toy/sword
weapon_force = 0
-/mob/living/simple_animal/bot/secbot/grievous/bullet_act(obj/projectile/P)
- visible_message(span_warning("[src] deflects [P] with its energy swords!"))
- playsound(src, 'sound/weapons/blade1.ogg', 50, TRUE)
- return BULLET_ACT_BLOCK
+/mob/living/simple_animal/bot/secbot/grievous/proc/block_bullets(datum/source, obj/projectile/hitting_projectile)
+ SIGNAL_HANDLER
+
+ if(stat != CONSCIOUS)
+ return NONE
+
+ visible_message(span_warning("[source] deflects [hitting_projectile] with its energy swords!"))
+ playsound(source, 'sound/weapons/blade1.ogg', 50, TRUE)
+ return COMPONENT_BULLET_BLOCKED
/mob/living/simple_animal/bot/secbot/grievous/on_entered(datum/source, atom/movable/AM)
. = ..()
diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm
index 7bd48ab62fbb8..c369058a2a317 100644
--- a/code/modules/mob/living/simple_animal/bot/bot.dm
+++ b/code/modules/mob/living/simple_animal/bot/bot.dm
@@ -502,11 +502,15 @@
return
do_sparks(5, TRUE, src)
-/mob/living/simple_animal/bot/bullet_act(obj/projectile/Proj, def_zone, piercing_hit = FALSE)
- if(Proj && (Proj.damage_type == BRUTE || Proj.damage_type == BURN))
- if(prob(75) && Proj.damage > 0)
- do_sparks(5, TRUE, src)
- return ..()
+/mob/living/simple_animal/bot/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
+ . = ..()
+ if(prob(25) || . != BULLET_ACT_HIT)
+ return
+ if(hitting_projectile.damage_type != BRUTE && hitting_projectile.damage_type != BURN)
+ return
+ if(!hitting_projectile.is_hostile_projectile() || hitting_projectile.damage <= 0)
+ return
+ do_sparks(5, TRUE, src)
/mob/living/simple_animal/bot/emp_act(severity)
. = ..()
@@ -708,9 +712,9 @@ Pass a positive integer as an argument to override a bot's default speed.
bot_reset() //Reset a bot before setting it to call mode.
//For giving the bot temporary all-access. This method is bad and makes me feel bad. Refactoring access to a component is for another PR.
- var/obj/item/card/id/all_access = new /obj/item/card/id/advanced/gold/captains_spare()
- set_path(get_path_to(src, waypoint, max_distance=200, id = all_access))
- qdel(all_access)
+ //Easier then building the list ourselves. I'm sorry.
+ var/static/obj/item/card/id/all_access = new /obj/item/card/id/advanced/gold/captains_spare()
+ set_path(get_path_to(src, waypoint, max_distance=200, access = all_access.GetAccess()))
calling_ai = caller //Link the AI to the bot!
ai_waypoint = waypoint
@@ -924,12 +928,12 @@ Pass a positive integer as an argument to override a bot's default speed.
// given an optional turf to avoid
/mob/living/simple_animal/bot/proc/calc_path(turf/avoid)
check_bot_access()
- set_path(get_path_to(src, patrol_target, max_distance=120, id=access_card, exclude=avoid))
+ set_path(get_path_to(src, patrol_target, max_distance=120, access=access_card.GetAccess(), exclude=avoid, diagonal_handling=DIAGONAL_REMOVE_ALL))
/mob/living/simple_animal/bot/proc/calc_summon_path(turf/avoid)
check_bot_access()
var/datum/callback/path_complete = CALLBACK(src, PROC_REF(on_summon_path_finish))
- SSpathfinder.pathfind(src, summon_target, max_distance=150, id=access_card, exclude=avoid, on_finish = path_complete)
+ SSpathfinder.pathfind(src, summon_target, max_distance=150, access=access_card.GetAccess(), exclude=avoid, diagonal_handling=DIAGONAL_REMOVE_ALL, on_finish=list(path_complete))
/mob/living/simple_animal/bot/proc/on_summon_path_finish(list/path)
set_path(path)
diff --git a/code/modules/mob/living/simple_animal/bot/cleanbot.dm b/code/modules/mob/living/simple_animal/bot/cleanbot.dm
index 710085884658a..21186be8bb0d6 100644
--- a/code/modules/mob/living/simple_animal/bot/cleanbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/cleanbot.dm
@@ -291,7 +291,7 @@
return
if(target && path.len == 0 && (get_dist(src,target) > 1))
- path = get_path_to(src, target, max_distance=30, mintargetdist=1, id=access_card)
+ path = get_path_to(src, target, max_distance=30, mintargetdist=1, access=access_card.GetAccess())
mode = BOT_MOVING
if(length(path) == 0)
add_to_ignore(target)
diff --git a/code/modules/mob/living/simple_animal/bot/firebot.dm b/code/modules/mob/living/simple_animal/bot/firebot.dm
index 29006613e42b1..59d08fe7339e0 100644
--- a/code/modules/mob/living/simple_animal/bot/firebot.dm
+++ b/code/modules/mob/living/simple_animal/bot/firebot.dm
@@ -239,7 +239,7 @@
if(target_fire && (get_dist(src, target_fire) > 2))
- path = get_path_to(src, target_fire, max_distance=30, mintargetdist=1, id=access_card)
+ path = get_path_to(src, target_fire, max_distance=30, mintargetdist=1, access=access_card.GetAccess())
mode = BOT_MOVING
if(!path.len)
soft_reset()
diff --git a/code/modules/mob/living/simple_animal/bot/floorbot.dm b/code/modules/mob/living/simple_animal/bot/floorbot.dm
index b2f7f418589ac..17e918f07958c 100644
--- a/code/modules/mob/living/simple_animal/bot/floorbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/floorbot.dm
@@ -249,9 +249,9 @@
if(!length(path))
if(!isturf(target))
var/turf/TL = get_turf(target)
- path = get_path_to(src, TL, max_distance=30, id=access_card,simulated_only = FALSE)
+ path = get_path_to(src, TL, max_distance=30, access=access_card.GetAccess(), simulated_only = FALSE)
else
- path = get_path_to(src, target, max_distance=30, id=access_card,simulated_only = FALSE)
+ path = get_path_to(src, target, max_distance=30, access=access_card.GetAccess(), simulated_only = FALSE)
if(!bot_move(target))
add_to_ignore(target)
diff --git a/code/modules/mob/living/simple_animal/bot/medbot.dm b/code/modules/mob/living/simple_animal/bot/medbot.dm
index cd23f9da844ea..efef97e3fe480 100644
--- a/code/modules/mob/living/simple_animal/bot/medbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/medbot.dm
@@ -493,10 +493,10 @@
return
if(patient && path.len == 0 && (get_dist(src,patient) > 1))
- path = get_path_to(src, patient, max_distance=30, id=access_card)
+ path = get_path_to(src, patient, max_distance=30, access=access_card.GetAccess())
mode = BOT_MOVING
if(!path.len) //try to get closer if you can't reach the patient directly
- path = get_path_to(src, patient, max_distance=30, mintargetdist=1, id=access_card)
+ path = get_path_to(src, patient, max_distance=30, mintargetdist=1, access=access_card.GetAccess())
if(!path.len) //Do not chase a patient we cannot reach.
soft_reset()
@@ -654,10 +654,10 @@
healies *= 1.1
if(bot_cover_flags & BOT_COVER_EMAGGED)
patient.reagents.add_reagent(/datum/reagent/toxin/chloralhydrate, 5)
- patient.apply_damage_type((healies*1),treatment_method)
+ patient.apply_damage((healies * 1), treatment_method, spread_damage = TRUE)
log_combat(src, patient, "pretended to tend wounds on", "internal tools", "([uppertext(treatment_method)]) (EMAGGED)")
else
- patient.apply_damage_type((healies*-1),treatment_method) //don't need to check treatment_method since we know by this point that they were actually damaged.
+ patient.heal_damage_type((healies * 1), treatment_method) //don't need to check treatment_method since we know by this point that they were actually damaged.
log_combat(src, patient, "tended the wounds of", "internal tools", "([uppertext(treatment_method)])")
C.visible_message(span_notice("[src] tends the wounds of [patient]!"), \
"[span_green("[src] tends your wounds!")]")
diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm
index c6ae677136a6b..df180631b1e9d 100644
--- a/code/modules/mob/living/simple_animal/bot/mulebot.dm
+++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm
@@ -588,7 +588,7 @@
// calculates a path to the current destination
// given an optional turf to avoid
/mob/living/simple_animal/bot/mulebot/calc_path(turf/avoid = null)
- path = get_path_to(src, target, max_distance=250, id=access_card, exclude=avoid)
+ path = get_path_to(src, target, max_distance=250, access=access_card.GetAccess(), exclude=avoid, diagonal_handling=DIAGONAL_REMOVE_ALL)
// sets the current destination
// signals all beacons matching the delivery code
diff --git a/code/modules/mob/living/simple_animal/bot/secbot.dm b/code/modules/mob/living/simple_animal/bot/secbot.dm
index e40a7ff405ecb..9e122219fe0bc 100644
--- a/code/modules/mob/living/simple_animal/bot/secbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/secbot.dm
@@ -262,11 +262,14 @@
return TRUE
/mob/living/simple_animal/bot/secbot/bullet_act(obj/projectile/Proj)
+ . = ..()
+ if(. != BULLET_ACT_HIT)
+ return
+
if(istype(Proj, /obj/projectile/beam) || istype(Proj, /obj/projectile/bullet))
if((Proj.damage_type == BURN) || (Proj.damage_type == BRUTE))
if(Proj.is_hostile_projectile() && Proj.damage < src.health && ishuman(Proj.firer))
retaliate(Proj.firer)
- return ..()
/mob/living/simple_animal/bot/secbot/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
if(!(bot_mode_flags & BOT_MODE_ON))
diff --git a/code/modules/mob/living/simple_animal/guardian/guardian.dm b/code/modules/mob/living/simple_animal/guardian/guardian.dm
index 4ca35a673577e..212867877554c 100644
--- a/code/modules/mob/living/simple_animal/guardian/guardian.dm
+++ b/code/modules/mob/living/simple_animal/guardian/guardian.dm
@@ -138,7 +138,9 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
/mob/living/simple_animal/hostile/guardian/proc/cut_summoner(different_person = FALSE)
if(is_deployed())
recall_effects()
- forceMove(get_turf(src))
+ var/summoner_turf = get_turf(src)
+ if (!isnull(summoner_turf))
+ forceMove(summoner_turf)
UnregisterSignal(summoner, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING, COMSIG_LIVING_DEATH, COMSIG_LIVING_HEALTH_UPDATE, COMSIG_LIVING_ON_WABBAJACKED, COMSIG_LIVING_SHAPESHIFTED, COMSIG_LIVING_UNSHAPESHIFTED))
if(different_person)
summoner.faction -= "[REF(src)]"
@@ -311,7 +313,8 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
SIGNAL_HANDLER
cut_summoner()
- forceMove(source.loc)
+ if (!isnull(source.loc))
+ forceMove(source.loc)
to_chat(src, span_danger("Your summoner has died!"))
visible_message(span_bolddanger("\The [src] dies along with its user!"))
source.visible_message(span_bolddanger("[source]'s body is completely consumed by the strain of sustaining [src]!"))
@@ -346,12 +349,12 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
return
to_chat(src, span_holoparasite("You moved out of range, and were pulled back! You can only move [range] meters from [summoner.real_name]!"))
visible_message(span_danger("\The [src] jumps back to its user."))
- if(istype(summoner.loc, /obj/effect))
+ new /obj/effect/temp_visual/guardian/phase/out(loc)
+ if(istype(summoner.loc, /obj/effect) || isnull(summoner.loc))
recall(forced = TRUE)
- else
- new /obj/effect/temp_visual/guardian/phase/out(loc)
- forceMove(summoner.loc)
- new /obj/effect/temp_visual/guardian/phase(loc)
+ return
+ forceMove(summoner.loc)
+ new /obj/effect/temp_visual/guardian/phase(loc)
/mob/living/simple_animal/hostile/guardian/can_suicide()
return FALSE
@@ -469,7 +472,7 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
//MANIFEST, RECALL, TOGGLE MODE/LIGHT, SHOW TYPE
/mob/living/simple_animal/hostile/guardian/proc/manifest(forced)
- if(is_deployed() || istype(summoner.loc, /obj/effect) || (!COOLDOWN_FINISHED(src, manifest_cooldown) && !forced) || locked)
+ if(is_deployed() || isnull(summoner.loc) || istype(summoner.loc, /obj/effect) || (!COOLDOWN_FINISHED(src, manifest_cooldown) && !forced) || locked)
return FALSE
forceMove(summoner.loc)
new /obj/effect/temp_visual/guardian/phase(loc)
diff --git a/code/modules/mob/living/simple_animal/hostile/alien.dm b/code/modules/mob/living/simple_animal/hostile/alien.dm
index 2d17820af0b40..593bf29535edc 100644
--- a/code/modules/mob/living/simple_animal/hostile/alien.dm
+++ b/code/modules/mob/living/simple_animal/hostile/alien.dm
@@ -160,7 +160,7 @@
. = ..()
AddElement(/datum/element/cleaning)
-/mob/living/simple_animal/hostile/alien/maid/AttackingTarget()
+/mob/living/simple_animal/hostile/alien/maid/AttackingTarget(atom/attacked_target)
if(ismovable(target))
target.wash(CLEAN_SCRUB)
if(istype(target, /obj/effect/decal/cleanable))
diff --git a/code/modules/mob/living/simple_animal/hostile/constructs/constructs.dm b/code/modules/mob/living/simple_animal/hostile/constructs/constructs.dm
deleted file mode 100644
index 31150a4dc89c1..0000000000000
--- a/code/modules/mob/living/simple_animal/hostile/constructs/constructs.dm
+++ /dev/null
@@ -1,133 +0,0 @@
-/mob/living/simple_animal/hostile/construct
- name = "Construct"
- real_name = "Construct"
- desc = ""
- gender = NEUTER
- mob_biotypes = MOB_MINERAL | MOB_SPECIAL
- speak_emote = list("hisses")
- response_help_continuous = "thinks better of touching"
- response_help_simple = "think better of touching"
- response_disarm_continuous = "flails at"
- response_disarm_simple = "flail at"
- response_harm_continuous = "punches"
- response_harm_simple = "punch"
- speak_chance = 1
- icon = 'icons/mob/nonhuman-player/cult.dmi'
- speed = 0
- combat_mode = TRUE
- stop_automated_movement = 1
- status_flags = CANPUSH
- attack_sound = 'sound/weapons/punch1.ogg'
- // Vivid red, cause cult theme
- lighting_cutoff_red = 30
- lighting_cutoff_green = 5
- lighting_cutoff_blue = 20
- damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- minbodytemp = 0
- maxbodytemp = INFINITY
- faction = list(FACTION_CULT)
- pressure_resistance = 100
- unique_name = TRUE
- AIStatus = AI_OFF //normal constructs don't have AI
- loot = list(/obj/item/ectoplasm)
- del_on_death = TRUE
- initial_language_holder = /datum/language_holder/construct
- death_message = "collapses in a shattered heap."
- /// List of spells that this construct can cast
- var/list/construct_spells = list()
- /// Flavor text shown to players when they spawn as this construct
- var/playstyle_string = "You are a generic construct! Your job is to not exist, and you should probably adminhelp this."
- /// The construct's master
- var/master = null
- /// Whether this construct is currently seeking nar nar
- var/seeking = FALSE
- /// Whether this construct can repair other constructs or cult buildings.
- var/can_repair = FALSE
- /// Whether this construct can repair itself. Works independently of can_repair.
- var/can_repair_self = FALSE
- /// Theme controls color. THEME_CULT is red THEME_WIZARD is purple and THEME_HOLY is blue
- var/theme = THEME_CULT
-
-/mob/living/simple_animal/hostile/construct/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/simple_flying)
- add_traits(list(TRAIT_HEALS_FROM_CULT_PYLONS, TRAIT_SPACEWALK), INNATE_TRAIT)
- for(var/spell in construct_spells)
- var/datum/action/new_spell = new spell(src)
- new_spell.Grant(src)
-
- var/spell_count = 1
- for(var/datum/action/spell as anything in actions)
- if(!(spell.type in construct_spells))
- continue
-
- var/pos = 2 + spell_count * 31
- if(construct_spells.len >= 4)
- pos -= 31 * (construct_spells.len - 4)
- spell.default_button_position = "6:[pos],4:-2" // Set the default position to this random position
- spell_count++
- update_action_buttons()
-
- if(icon_state)
- add_overlay("glow_[icon_state]_[theme]")
-
-/mob/living/simple_animal/hostile/construct/Login()
- . = ..()
- if(!. || !client)
- return FALSE
- to_chat(src, playstyle_string)
-
-/mob/living/simple_animal/hostile/construct/examine(mob/user)
- var/text_span
- switch(theme)
- if(THEME_CULT)
- text_span = "cult"
- if(THEME_WIZARD)
- text_span = "purple"
- if(THEME_HOLY)
- text_span = "blue"
- . = list("This is [icon2html(src, user)] \a [src]!\n[desc]")
- if(health < maxHealth)
- if(health >= maxHealth/2)
- . += span_warning("[p_They()] look[p_s()] slightly dented.")
- else
- . += span_warning("[p_They()] look[p_s()] severely dented!")
- . += ""
-
-/mob/living/simple_animal/hostile/construct/attack_animal(mob/living/simple_animal/user, list/modifiers)
- if(!isconstruct(user))
- if(src != user)
- return ..()
- return
-
- var/mob/living/simple_animal/hostile/construct/doll = user
- if(!doll.can_repair || (doll == src && !doll.can_repair_self))
- return ..()
- if(theme != doll.theme)
- return ..()
-
- if(health >= maxHealth)
- if(src != user)
- to_chat(user, span_cult("You cannot repair [src]'s dents, as [p_they()] [p_have()] none!"))
- else
- to_chat(user, span_cult("You cannot repair your own dents, as you have none!"))
- return
-
- adjustHealth(-5)
- if(src == user)
- user.visible_message(span_danger("[user] repairs some of [p_their()] own dents."), \
- span_cult("You repair some of your own dents, leaving you at [user.health]/[user.maxHealth] health."))
- return
-
- Beam(user, icon_state="sendbeam", time = 4)
- user.visible_message(span_danger("[user] repairs some of \the [src]'s dents."), \
- span_cult("You repair some of [src]'s dents, leaving [src] at [health]/[maxHealth] health."))
-
-
-/mob/living/simple_animal/hostile/construct/narsie_act()
- return
-
-/mob/living/simple_animal/hostile/construct/electrocute_act(shock_damage, source, siemens_coeff = 1, flags = NONE)
- return FALSE
-
diff --git a/code/modules/mob/living/simple_animal/hostile/constructs/juggernaut.dm b/code/modules/mob/living/simple_animal/hostile/constructs/juggernaut.dm
deleted file mode 100644
index 3eb80cf7a88e6..0000000000000
--- a/code/modules/mob/living/simple_animal/hostile/constructs/juggernaut.dm
+++ /dev/null
@@ -1,75 +0,0 @@
-/mob/living/simple_animal/hostile/construct/juggernaut
- name = "Juggernaut"
- real_name = "Juggernaut"
- desc = "A massive, armored construct built to spearhead attacks and soak up enemy fire."
- icon_state = "juggernaut"
- icon_living = "juggernaut"
- maxHealth = 150
- health = 150
- response_harm_continuous = "harmlessly punches"
- response_harm_simple = "harmlessly punch"
- harm_intent_damage = 0
- obj_damage = 90
- melee_damage_lower = 25
- melee_damage_upper = 25
- attack_verb_continuous = "smashes their armored gauntlet into"
- attack_verb_simple = "smash your armored gauntlet into"
- speed = 2.5
- environment_smash = ENVIRONMENT_SMASH_WALLS
- attack_sound = 'sound/weapons/punch3.ogg'
- status_flags = 0
- mob_size = MOB_SIZE_LARGE
- force_threshold = 10
- construct_spells = list(
- /datum/action/cooldown/spell/forcewall/cult,
- /datum/action/cooldown/spell/basic_projectile/juggernaut,
- /datum/action/innate/cult/create_rune/wall,
- )
- playstyle_string = "You are a Juggernaut. Though slow, your shell can withstand heavy punishment, \
- create shield walls, rip apart enemies and walls alike, and even deflect energy weapons."
-
-/mob/living/simple_animal/hostile/construct/juggernaut/hostile //actually hostile, will move around, hit things
- AIStatus = AI_ON
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES //only token destruction, don't smash the cult wall NO STOP
-
-/mob/living/simple_animal/hostile/construct/juggernaut/bullet_act(obj/projectile/bullet)
- if(!istype(bullet, /obj/projectile/energy) && !istype(bullet, /obj/projectile/beam))
- return ..()
- if(!prob(40 - round(bullet.damage / 3))) // reflect chance
- return ..()
-
- apply_damage(bullet.damage * 0.5, bullet.damage_type)
- visible_message(span_danger("The [bullet.name] is reflected by [src]'s armored shell!"), \
- span_userdanger("The [bullet.name] is reflected by your armored shell!"))
-
- if(!bullet.starting)
- return BULLET_ACT_FORCE_PIERCE
- // Find a turf near or on the original location to bounce to
- var/new_x = bullet.starting.x + pick(0, 0, -1, 1, -2, 2, -2, 2, -2, 2, -3, 3, -3, 3)
- var/new_y = bullet.starting.y + pick(0, 0, -1, 1, -2, 2, -2, 2, -2, 2, -3, 3, -3, 3)
- var/turf/current_tile = get_turf(src)
-
- // redirect the projectile
- bullet.original = locate(new_x, new_y, bullet.z)
- bullet.starting = current_tile
- bullet.firer = src
- bullet.yo = new_y - current_tile.y
- bullet.xo = new_x - current_tile.x
- var/new_angle_s = bullet.Angle + rand(120,240)
- while(new_angle_s > 180) // Translate to regular projectile degrees
- new_angle_s -= 360
- bullet.set_angle(new_angle_s)
-
- return BULLET_ACT_FORCE_PIERCE // complete projectile permutation
-
-
-//////////////////////////Juggernaut-alts////////////////////////////
-/mob/living/simple_animal/hostile/construct/juggernaut/angelic
- theme = THEME_HOLY
- loot = list(/obj/item/ectoplasm/angelic)
-
-/mob/living/simple_animal/hostile/construct/juggernaut/mystic
- theme = THEME_WIZARD
- loot = list(/obj/item/ectoplasm/mystic)
-
-/mob/living/simple_animal/hostile/construct/juggernaut/noncult
diff --git a/code/modules/mob/living/simple_animal/hostile/constructs/wraith.dm b/code/modules/mob/living/simple_animal/hostile/constructs/wraith.dm
deleted file mode 100644
index e7ef22a9e073b..0000000000000
--- a/code/modules/mob/living/simple_animal/hostile/constructs/wraith.dm
+++ /dev/null
@@ -1,78 +0,0 @@
-/mob/living/simple_animal/hostile/construct/wraith
- name = "Wraith"
- real_name = "Wraith"
- desc = "A wicked, clawed shell constructed to assassinate enemies and sow chaos behind enemy lines."
- icon_state = "wraith"
- icon_living = "wraith"
- maxHealth = 65
- health = 65
- melee_damage_lower = 20
- melee_damage_upper = 20
- retreat_distance = 2 //AI wraiths will move in and out of combat
- attack_verb_continuous = "slashes"
- attack_verb_simple = "slash"
- attack_sound = 'sound/weapons/bladeslice.ogg'
- attack_vis_effect = ATTACK_EFFECT_SLASH
- construct_spells = list(
- /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift,
- /datum/action/innate/cult/create_rune/tele,
- )
- playstyle_string = "You are a Wraith. Though relatively fragile, you are fast, deadly, \
- can phase through walls, and your attacks will lower the cooldown on phasing."
-
- // Accomplishing various things gives you a refund on jaunt, to jump in and out.
- /// The seconds refunded per attack
- var/attack_refund = 1 SECONDS
- /// The seconds refunded when putting a target into critical
- var/crit_refund = 5 SECONDS
-
-/mob/living/simple_animal/hostile/construct/wraith/AttackingTarget() //refund jaunt cooldown when attacking living targets
- var/prev_stat
- var/mob/living/living_target = target
-
- if(isliving(living_target) && !IS_CULTIST(living_target))
- prev_stat = living_target.stat
-
- . = ..()
- if(!. || !isnum(prev_stat))
- return
-
- var/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/jaunt = locate() in actions
- if(!jaunt)
- return
-
- var/total_refund = 0 SECONDS
- // they're dead, and you killed them - full refund
- if(QDELETED(living_target) || (living_target.stat == DEAD && prev_stat != DEAD))
- total_refund += jaunt.cooldown_time
- // you knocked them into critical
- else if(HAS_TRAIT(living_target, TRAIT_CRITICAL_CONDITION) && prev_stat == CONSCIOUS)
- total_refund += crit_refund
-
- if(living_target.stat != DEAD && prev_stat != DEAD)
- total_refund += attack_refund
-
- jaunt.next_use_time -= total_refund
- jaunt.build_all_button_icons()
-
-/mob/living/simple_animal/hostile/construct/wraith/hostile //actually hostile, will move around, hit things
- AIStatus = AI_ON
-
-//////////////////////////Wraith-alts////////////////////////////
-/mob/living/simple_animal/hostile/construct/wraith/angelic
- theme = THEME_HOLY
- construct_spells = list(
- /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/angelic,
- /datum/action/innate/cult/create_rune/tele,
- )
- loot = list(/obj/item/ectoplasm/angelic)
-
-/mob/living/simple_animal/hostile/construct/wraith/mystic
- theme = THEME_WIZARD
- construct_spells = list(
- /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/mystic,
- /datum/action/innate/cult/create_rune/tele,
- )
- loot = list(/obj/item/ectoplasm/mystic)
-
-/mob/living/simple_animal/hostile/construct/wraith/noncult
diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm
index 3248fd9c2aad1..45f75b229b138 100644
--- a/code/modules/mob/living/simple_animal/hostile/hostile.dm
+++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm
@@ -263,7 +263,7 @@
if(search_objects < 2)
if(isliving(the_target))
var/mob/living/L = the_target
- var/faction_check = faction_check_mob(L)
+ var/faction_check = faction_check_atom(L)
if(robust_searching)
if(faction_check && !attack_same)
return FALSE
@@ -314,14 +314,14 @@
for(var/i in 1 to rapid_melee)
addtimer(cb, (i - 1)*delay)
else
- AttackingTarget()
+ AttackingTarget(target)
if(patience)
GainPatience()
/mob/living/simple_animal/hostile/proc/CheckAndAttack()
var/atom/target_from = GET_TARGETS_FROM(src)
if(target && isturf(target_from.loc) && target.Adjacent(target_from) && !incapacitated())
- AttackingTarget()
+ AttackingTarget(target)
/mob/living/simple_animal/hostile/proc/MoveToTarget(list/possible_targets)//Step 5, handle movement between us and our target
stop_automated_movement = 1
@@ -432,7 +432,7 @@
playsound(loc, 'sound/machines/chime.ogg', 50, TRUE, -1)
var/atom/target_from = GET_TARGETS_FROM(src)
for(var/mob/living/simple_animal/hostile/M in oview(distance, target_from))
- if(faction_check_mob(M, TRUE))
+ if(faction_check_atom(M, TRUE))
if(M.AIStatus == AI_OFF)
return
else
@@ -444,7 +444,7 @@
for(var/mob/living/L in T)
if(L == src || L == A)
continue
- if(faction_check_mob(L) && !attack_same)
+ if(faction_check_atom(L) && !attack_same)
return TRUE
/mob/living/simple_animal/hostile/proc/OpenFire(atom/A)
diff --git a/code/modules/mob/living/simple_animal/hostile/jungle/_jungle_mobs.dm b/code/modules/mob/living/simple_animal/hostile/jungle/_jungle_mobs.dm
deleted file mode 100644
index 8cccf16b85089..0000000000000
--- a/code/modules/mob/living/simple_animal/hostile/jungle/_jungle_mobs.dm
+++ /dev/null
@@ -1,17 +0,0 @@
-/mob/living/simple_animal/hostile/jungle
- vision_range = 5
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- faction = list(FACTION_JUNGLE)
- obj_damage = 30
- environment_smash = ENVIRONMENT_SMASH_WALLS
- minbodytemp = 0
- maxbodytemp = 450
- response_harm_continuous = "strikes"
- response_harm_simple = "strike"
- status_flags = NONE
- combat_mode = TRUE
- // Let's do a blue, since they'll be on green turfs if this shit is ever finished
- lighting_cutoff_red = 5
- lighting_cutoff_green = 20
- lighting_cutoff_blue = 25
- mob_size = MOB_SIZE_LARGE
diff --git a/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm b/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm
deleted file mode 100644
index 04bb2f94cc9c6..0000000000000
--- a/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm
+++ /dev/null
@@ -1,289 +0,0 @@
-#define PLAYER_HOP_DELAY 25
-
-//Huge, carnivorous toads that spit an immobilizing toxin at its victims before leaping onto them.
-//It has no melee attack, and its damage comes from the toxin in its bubbles and its crushing leap.
-//Its eyes will turn red to signal an imminent attack!
-/mob/living/simple_animal/hostile/jungle/leaper
- name = "leaper"
- desc = "Commonly referred to as 'leapers', the Geron Toad is a massive beast that spits out highly pressurized bubbles containing a unique toxin, knocking down its prey and then crushing it with its girth."
- icon = 'icons/mob/simple/jungle/leaper.dmi'
- icon_state = "leaper"
- icon_living = "leaper"
- icon_dead = "leaper_dead"
- mob_biotypes = MOB_ORGANIC|MOB_BEAST
- maxHealth = 300
- health = 300
- ranged = TRUE
- projectiletype = /obj/projectile/leaper
- projectilesound = 'sound/weapons/pierce.ogg'
- ranged_cooldown_time = 30
- pixel_x = -16
- base_pixel_x = -16
- layer = LARGE_MOB_LAYER
- plane = GAME_PLANE_UPPER_FOV_HIDDEN
- speed = 10
- stat_attack = HARD_CRIT
- robust_searching = 1
- var/hopping = FALSE
- var/hop_cooldown = 0 //Strictly for player controlled leapers
- var/projectile_ready = FALSE //Stopping AI leapers from firing whenever they want, and only doing it after a hop has finished instead
-
- footstep_type = FOOTSTEP_MOB_HEAVY
-
-/obj/projectile/leaper
- name = "leaper bubble"
- icon_state = "leaper"
- paralyze = 50
- damage = 0
- range = 7
- hitsound = 'sound/effects/snap.ogg'
- nondirectional_sprite = TRUE
- impact_effect_type = /obj/effect/temp_visual/leaper_projectile_impact
-
-/obj/projectile/leaper/on_hit(atom/target, blocked = FALSE)
- ..()
- if (!isliving(target))
- return
- var/mob/living/bubbled = target
- if(iscarbon(target))
- bubbled.reagents.add_reagent(/datum/reagent/toxin/leaper_venom, 5)
- return
- if(isanimal(target))
- var/mob/living/simple_animal/bubbled_animal = bubbled
- bubbled_animal.adjustHealth(25)
- return
- if (isbasicmob(target))
- bubbled.adjustBruteLoss(25)
-
-/obj/projectile/leaper/on_range()
- var/turf/T = get_turf(src)
- ..()
- new /obj/structure/leaper_bubble(T)
-
-/obj/effect/temp_visual/leaper_projectile_impact
- name = "leaper bubble"
- icon = 'icons/obj/weapons/guns/projectiles.dmi'
- icon_state = "leaper_bubble_pop"
- layer = ABOVE_ALL_MOB_LAYER
- plane = GAME_PLANE_UPPER_FOV_HIDDEN
- duration = 3
-
-/obj/effect/temp_visual/leaper_projectile_impact/Initialize(mapload)
- . = ..()
- new /obj/effect/decal/cleanable/leaper_sludge(get_turf(src))
-
-/obj/effect/decal/cleanable/leaper_sludge
- name = "leaper sludge"
- desc = "A small pool of sludge, containing trace amounts of leaper venom."
- icon = 'icons/effects/tomatodecal.dmi'
- icon_state = "tomato_floor1"
-
-/obj/effect/decal/cleanable/leaper_sludge/Initialize(mapload, list/datum/disease/diseases)
- . = ..()
- AddElement(/datum/element/swabable, CELL_LINE_TABLE_LEAPER, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
-
-/obj/structure/leaper_bubble
- name = "leaper bubble"
- desc = "A floating bubble containing leaper venom. The contents are under a surprising amount of pressure."
- icon = 'icons/obj/weapons/guns/projectiles.dmi'
- icon_state = "leaper"
- max_integrity = 10
- density = FALSE
-
-/obj/structure/leaper_bubble/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/movetype_handler)
- ADD_TRAIT(src, TRAIT_MOVE_FLOATING, LEAPER_BUBBLE_TRAIT)
- QDEL_IN(src, 100)
- var/static/list/loc_connections = list(
- COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
- )
- AddElement(/datum/element/connect_loc, loc_connections)
- AddElement(/datum/element/swabable, CELL_LINE_TABLE_LEAPER, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
-
-/obj/structure/leaper_bubble/Destroy()
- new /obj/effect/temp_visual/leaper_projectile_impact(get_turf(src))
- playsound(src,'sound/effects/snap.ogg',50, TRUE, -1)
- return ..()
-
-/obj/structure/leaper_bubble/proc/on_entered(datum/source, atom/movable/bubbled)
- SIGNAL_HANDLER
- if(!isliving(bubbled) || istype(bubbled, /mob/living/simple_animal/hostile/jungle/leaper))
- return
- var/mob/living/bubbled_mob = bubbled
-
- playsound(src,'sound/effects/snap.ogg',50, TRUE, -1)
- bubbled_mob.Paralyze(50)
- if(iscarbon(bubbled_mob))
- bubbled_mob.reagents.add_reagent(/datum/reagent/toxin/leaper_venom, 5)
- else if(isanimal(bubbled_mob))
- var/mob/living/simple_animal/bubbled_animal = bubbled_mob
- bubbled_animal.adjustHealth(25)
- else if(isbasicmob(bubbled_mob))
- bubbled_mob.adjustBruteLoss(25)
- qdel(src)
-
-/datum/reagent/toxin/leaper_venom
- name = "Leaper venom"
- description = "A toxin spat out by leapers that, while harmless in small doses, quickly creates a toxic reaction if too much is in the body."
- color = "#801E28" // rgb: 128, 30, 40
- toxpwr = 0
- taste_description = "french cuisine"
- taste_mult = 1.3
-
-/datum/reagent/toxin/leaper_venom/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
- . = ..()
- if(volume >= 10)
- if(M.adjustToxLoss(5 * REM * seconds_per_tick, updating_health = FALSE))
- . = UPDATE_MOB_HEALTH
-
-/obj/effect/temp_visual/leaper_crush
- name = "grim tidings"
- desc = "Incoming leaper!"
- icon = 'icons/effects/96x96.dmi'
- icon_state = "lily_pad"
- layer = BELOW_MOB_LAYER
- plane = GAME_PLANE
- SET_BASE_PIXEL(-32, -32)
- duration = 30
-
-/mob/living/simple_animal/hostile/jungle/leaper/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/seethrough_mob)
- remove_verb(src, /mob/living/verb/pulled)
- add_cell_sample()
-
-/mob/living/simple_animal/hostile/jungle/leaper/CtrlClickOn(atom/A)
- face_atom(A)
- GiveTarget(A)
- if(!isturf(loc))
- return
- if(next_move > world.time)
- return
- if(hopping)
- return
- if(isliving(A))
- var/mob/living/L = A
- if(L.incapacitated())
- BellyFlop()
- return
- if(hop_cooldown <= world.time)
- Hop(player_hop = TRUE)
-
-/mob/living/simple_animal/hostile/jungle/leaper/AttackingTarget()
- if(isliving(target))
- return
- return ..()
-
-/mob/living/simple_animal/hostile/jungle/leaper/handle_automated_action()
- if(hopping || projectile_ready)
- return
- . = ..()
- if(target)
- if(isliving(target))
- var/mob/living/L = target
- if(L.incapacitated())
- BellyFlop()
- return
- if(!hopping)
- Hop()
-
-/mob/living/simple_animal/hostile/jungle/leaper/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- . = ..()
- update_icons()
-
-/mob/living/simple_animal/hostile/jungle/leaper/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
- if(prob(33) && !ckey)
- ranged_cooldown = 0 //Keeps em on their toes instead of a constant rotation
- ..()
-
-/mob/living/simple_animal/hostile/jungle/leaper/OpenFire()
- face_atom(target)
- if(ranged_cooldown <= world.time)
- if(ckey)
- if(hopping)
- return
- if(isliving(target))
- var/mob/living/L = target
- if(L.incapacitated())
- return //No stunlocking. Hop on them after you stun them, you donk.
- if(AIStatus == AI_ON && !projectile_ready && !ckey)
- return
- . = ..(target)
- projectile_ready = FALSE
- update_icons()
-
-/mob/living/simple_animal/hostile/jungle/leaper/proc/Hop(player_hop = FALSE)
- if(z != target.z)
- return
- hopping = TRUE
- add_traits(list(TRAIT_UNDENSE, TRAIT_NO_TRANSFORM), LEAPING_TRAIT)
- pass_flags |= PASSMOB
- var/turf/new_turf = locate((target.x + rand(-3,3)),(target.y + rand(-3,3)),target.z)
- if(player_hop)
- new_turf = get_turf(target)
- hop_cooldown = world.time + PLAYER_HOP_DELAY
- if(AIStatus == AI_ON && ranged_cooldown <= world.time)
- projectile_ready = TRUE
- update_icons()
- throw_at(new_turf, max(3,get_dist(src,new_turf)), 1, src, FALSE, callback = CALLBACK(src, PROC_REF(FinishHop)))
-
-/mob/living/simple_animal/hostile/jungle/leaper/proc/FinishHop()
- remove_traits(list(TRAIT_UNDENSE, TRAIT_NO_TRANSFORM), LEAPING_TRAIT)
- pass_flags &= ~PASSMOB
- hopping = FALSE
- playsound(src.loc, 'sound/effects/meteorimpact.ogg', 100, TRUE)
- if(target && AIStatus == AI_ON && projectile_ready && !ckey)
- face_atom(target)
- addtimer(CALLBACK(src, PROC_REF(OpenFire), target), 5)
-
-/mob/living/simple_animal/hostile/jungle/leaper/proc/BellyFlop()
- var/turf/new_turf = get_turf(target)
- hopping = TRUE
- ADD_TRAIT(src, TRAIT_NO_TRANSFORM, LEAPING_TRAIT)
- new /obj/effect/temp_visual/leaper_crush(new_turf)
- addtimer(CALLBACK(src, PROC_REF(BellyFlopHop), new_turf), 3 SECONDS)
-
-/mob/living/simple_animal/hostile/jungle/leaper/proc/BellyFlopHop(turf/T)
- ADD_TRAIT(src, TRAIT_UNDENSE, LEAPING_TRAIT)
- throw_at(T, get_dist(src,T),1,src, FALSE, callback = CALLBACK(src, PROC_REF(Crush)))
-
-/mob/living/simple_animal/hostile/jungle/leaper/proc/Crush()
- hopping = FALSE
- remove_traits(list(TRAIT_UNDENSE, TRAIT_NO_TRANSFORM), LEAPING_TRAIT)
- playsound(src, 'sound/effects/meteorimpact.ogg', 200, TRUE)
- for(var/mob/living/L in orange(1, src))
- L.adjustBruteLoss(35)
- if(!QDELETED(L)) // Some mobs are deleted on death
- var/throw_dir = get_dir(src, L)
- if(L.loc == loc)
- throw_dir = pick(GLOB.alldirs)
- var/throwtarget = get_edge_target_turf(src, throw_dir)
- L.throw_at(throwtarget, 3, 1)
- visible_message(span_warning("[L] is thrown clear of [src]!"))
- if(ckey)//Lessens ability to chain stun as a player
- ranged_cooldown = ranged_cooldown_time + world.time
- update_icons()
-
-/mob/living/simple_animal/hostile/jungle/leaper/Goto()
- return
-
-/mob/living/simple_animal/hostile/jungle/leaper/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
- return
-
-/mob/living/simple_animal/hostile/jungle/leaper/update_icons()
- . = ..()
- if(stat)
- icon_state = "leaper_dead"
- return
- if(ranged_cooldown <= world.time)
- if(AIStatus == AI_ON && projectile_ready || ckey)
- icon_state = "leaper_alert"
- return
- icon_state = "leaper"
-
-/mob/living/simple_animal/hostile/jungle/leaper/add_cell_sample()
- . = ..()
- AddElement(/datum/element/swabable, CELL_LINE_TABLE_LEAPER, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
-
-#undef PLAYER_HOP_DELAY
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm
index ea6a96e8911db..eed67aecde45d 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm
@@ -59,9 +59,7 @@
AddComponent(/datum/component/gps, gps_name)
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
add_traits(list(TRAIT_NO_TELEPORT, TRAIT_MARTIAL_ARTS_IMMUNE), MEGAFAUNA_TRAIT)
- for(var/action_type in attack_action_types)
- var/datum/action/innate/megafauna_attack/attack_action = new action_type()
- attack_action.Grant(src)
+ grant_actions_by_list(attack_action_types)
/mob/living/simple_animal/hostile/megafauna/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
//Safety check
@@ -114,7 +112,7 @@
return ..()
-/mob/living/simple_animal/hostile/megafauna/AttackingTarget()
+/mob/living/simple_animal/hostile/megafauna/AttackingTarget(atom/attacked_target)
if(recovery_time >= world.time)
return
. = ..()
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm
index 1bd196651fd8c..2e562eb565906 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm
@@ -67,21 +67,23 @@ Difficulty: Medium
. = ..()
miner_saw = new(src)
ADD_TRAIT(src, TRAIT_NO_FLOATING_ANIM, INNATE_TRAIT)
- dash = new /datum/action/cooldown/mob_cooldown/dash()
- kinetic_accelerator = new /datum/action/cooldown/mob_cooldown/projectile_attack/kinetic_accelerator()
- dash_attack = new /datum/action/cooldown/mob_cooldown/dash_attack()
- transform_weapon = new /datum/action/cooldown/mob_cooldown/transform_weapon()
+
+ dash = new /datum/action/cooldown/mob_cooldown/dash
+ kinetic_accelerator = new /datum/action/cooldown/mob_cooldown/projectile_attack/kinetic_accelerator
+ dash_attack = new /datum/action/cooldown/mob_cooldown/dash_attack
+ transform_weapon = new /datum/action/cooldown/mob_cooldown/transform_weapon
dash.Grant(src)
kinetic_accelerator.Grant(src)
dash_attack.Grant(src)
transform_weapon.Grant(src)
+
AddComponent(/datum/component/boss_music, 'sound/lavaland/bdm_boss.ogg', 167 SECONDS)
/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/Destroy()
- QDEL_NULL(dash)
- QDEL_NULL(kinetic_accelerator)
- QDEL_NULL(dash_attack)
- QDEL_NULL(transform_weapon)
+ dash = null
+ kinetic_accelerator = null
+ dash_attack = null
+ transform_weapon = null
return ..()
/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/OpenFire()
@@ -130,7 +132,7 @@ Difficulty: Medium
return FALSE
return ..()
-/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/AttackingTarget()
+/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/AttackingTarget(atom/attacked_target)
if(QDELETED(target))
return
face_atom(target)
@@ -185,7 +187,7 @@ Difficulty: Medium
/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/guidance
guidance = TRUE
-/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/hunter/AttackingTarget()
+/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/hunter/AttackingTarget(atom/attacked_target)
. = ..()
if(. && prob(12))
INVOKE_ASYNC(dash, TYPE_PROC_REF(/datum/action, Trigger), target)
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/bubblegum.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/bubblegum.dm
index 02d9e582b1a47..eaeaddc3e03ee 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/bubblegum.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/bubblegum.dm
@@ -86,10 +86,10 @@ Difficulty: Hard
/mob/living/simple_animal/hostile/megafauna/bubblegum/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_NO_FLOATING_ANIM, INNATE_TRAIT)
- triple_charge = new /datum/action/cooldown/mob_cooldown/charge/triple_charge()
- hallucination_charge = new /datum/action/cooldown/mob_cooldown/charge/hallucination_charge()
- hallucination_charge_surround = new /datum/action/cooldown/mob_cooldown/charge/hallucination_charge/hallucination_surround()
- blood_warp = new /datum/action/cooldown/mob_cooldown/blood_warp()
+ triple_charge = new(src)
+ hallucination_charge = new(src)
+ hallucination_charge_surround = new(src)
+ blood_warp = new(src)
triple_charge.Grant(src)
hallucination_charge.Grant(src)
hallucination_charge_surround.Grant(src)
@@ -105,10 +105,10 @@ Difficulty: Hard
sound_volume = 200)
/mob/living/simple_animal/hostile/megafauna/bubblegum/Destroy()
- QDEL_NULL(triple_charge)
- QDEL_NULL(hallucination_charge)
- QDEL_NULL(hallucination_charge_surround)
- QDEL_NULL(blood_warp)
+ triple_charge = null
+ hallucination_charge = null
+ hallucination_charge_surround = null
+ blood_warp = null
return ..()
/mob/living/simple_animal/hostile/megafauna/bubblegum/update_cooldowns(list/cooldown_updates, ignore_staggered = FALSE)
@@ -137,7 +137,7 @@ Difficulty: Hard
. = list()
for(var/mob/living/L in targets)
var/list/bloodpool = get_bloodcrawlable_pools(get_turf(L), 0)
- if(bloodpool.len && (!faction_check_mob(L) || L.stat == DEAD))
+ if(bloodpool.len && (!faction_check_atom(L) || L.stat == DEAD))
. += L
/**
@@ -201,7 +201,7 @@ Difficulty: Hard
new /obj/effect/temp_visual/bubblegum_hands/leftsmack(T)
SLEEP_CHECK_DEATH(4, src)
for(var/mob/living/L in T)
- if(!faction_check_mob(L))
+ if(!faction_check_atom(L))
to_chat(L, span_userdanger("[src] rends you!"))
playsound(T, attack_sound, 100, TRUE, -1)
var/limb_to_hit = L.get_bodypart(L.get_random_valid_zone(even_weights = TRUE))
@@ -217,7 +217,7 @@ Difficulty: Hard
new /obj/effect/temp_visual/bubblegum_hands/leftthumb(T)
SLEEP_CHECK_DEATH(6, src)
for(var/mob/living/L in T)
- if(!faction_check_mob(L))
+ if(!faction_check_atom(L))
if(L.stat != CONSCIOUS)
to_chat(L, span_userdanger("[src] drags you through the blood!"))
playsound(T, 'sound/magic/enter_blood.ogg', 100, TRUE, -1)
@@ -291,7 +291,7 @@ Difficulty: Hard
if(!(flags_1 & ADMIN_SPAWNED_1))
SSshuttle.shuttle_purchase_requirements_met[SHUTTLE_UNLOCK_BUBBLEGUM] = TRUE
-/mob/living/simple_animal/hostile/megafauna/bubblegum/AttackingTarget()
+/mob/living/simple_animal/hostile/megafauna/bubblegum/AttackingTarget(atom/attacked_target)
. = ..()
if(.)
recovery_time = world.time + 20 // can only attack melee once every 2 seconds but rapid_melee gives higher priority
@@ -348,7 +348,7 @@ Difficulty: Hard
/mob/living/simple_animal/hostile/megafauna/bubblegum/hallucination/OpenFire()
return
-/mob/living/simple_animal/hostile/megafauna/bubblegum/hallucination/AttackingTarget()
+/mob/living/simple_animal/hostile/megafauna/bubblegum/hallucination/AttackingTarget(atom/attacked_target)
return
/mob/living/simple_animal/hostile/megafauna/bubblegum/hallucination/try_bloodattack()
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm
index 5c63ca4e88400..9abc10c86b448 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm
@@ -71,11 +71,11 @@
/mob/living/simple_animal/hostile/megafauna/colossus/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_NO_FLOATING_ANIM, INNATE_TRAIT) //we don't want this guy to float, messes up his animations.
- spiral_shots = new /datum/action/cooldown/mob_cooldown/projectile_attack/spiral_shots/colossus()
- random_shots = new /datum/action/cooldown/mob_cooldown/projectile_attack/random_aoe/colossus()
- shotgun_blast = new /datum/action/cooldown/mob_cooldown/projectile_attack/shotgun_blast/colossus()
- dir_shots = new /datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/alternating/colossus()
- colossus_final = new /datum/action/cooldown/mob_cooldown/projectile_attack/colossus_final()
+ spiral_shots = new(src)
+ random_shots = new(src)
+ shotgun_blast = new(src)
+ dir_shots = new(src)
+ colossus_final = new(src)
spiral_shots.Grant(src)
random_shots.Grant(src)
shotgun_blast.Grant(src)
@@ -87,10 +87,10 @@
/mob/living/simple_animal/hostile/megafauna/colossus/Destroy()
RemoveElement(/datum/element/projectile_shield)
- QDEL_NULL(spiral_shots)
- QDEL_NULL(random_shots)
- QDEL_NULL(shotgun_blast)
- QDEL_NULL(dir_shots)
+ spiral_shots = null
+ random_shots = null
+ shotgun_blast = null
+ dir_shots = null
return ..()
/mob/living/simple_animal/hostile/megafauna/colossus/OpenFire()
@@ -180,7 +180,6 @@
damage = 25
armour_penetration = 100
speed = 2
- eyeblur = 0
damage_type = BRUTE
pass_flags = PASSTABLE
plane = GAME_PLANE
@@ -191,7 +190,7 @@
direct_target = TRUE
return ..(target, direct_target, ignore_loc, cross_failed)
-/obj/projectile/colossus/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/colossus/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(isliving(target))
var/mob/living/dust_mob = target
@@ -513,7 +512,13 @@
if(..() && !ready_to_deploy)
SSpoints_of_interest.make_point_of_interest(src)
ready_to_deploy = TRUE
- notify_ghosts("An anomalous crystal has been activated in [get_area(src)]! This crystal can always be used by ghosts hereafter.", enter_link = "(Click to enter)", ghost_sound = 'sound/effects/ghost2.ogg', source = src, action = NOTIFY_ATTACK, header = "Anomalous crystal activated")
+ notify_ghosts(
+ "An anomalous crystal has been activated in [get_area(src)]! This crystal can always be used by ghosts hereafter.",
+ ghost_sound = 'sound/effects/ghost2.ogg',
+ source = src,
+ action = NOTIFY_PLAY,
+ header = "Anomalous crystal activated",
+ )
/obj/machinery/anomalous_crystal/helpers/attack_ghost(mob/dead/observer/user)
. = ..()
@@ -525,13 +530,6 @@
var/mob/living/basic/lightgeist/deployable = new(get_turf(loc))
deployable.key = user.key
-
-/obj/machinery/anomalous_crystal/helpers/Topic(href, href_list)
- if(href_list["ghostjoin"])
- var/mob/dead/observer/ghost = usr
- if(istype(ghost))
- attack_ghost(ghost)
-
/obj/machinery/anomalous_crystal/possessor //Allows you to bodyjack small animals, then exit them at your leisure, but you can only do this once per activation. Because they blow up. Also, if the bodyjacked animal dies, SO DO YOU.
observer_desc = "When activated, this crystal allows you to take over small animals, and then exit them at the possessors leisure. Exiting the animal kills it, and if you die while possessing the animal, you die as well."
activation_method = ACTIVATE_TOUCH
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm
index 8a307cec2bd75..dc49d71f79608 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm
@@ -62,12 +62,12 @@ Difficulty: Extremely Hard
/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner/Initialize(mapload)
. = ..()
- frost_orbs = new /datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/shrapnel()
- hard_frost_orbs = new /datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/shrapnel/strong()
- snowball_machine_gun = new /datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire()
- hard_snowball_machine_gun = new /datum/action/cooldown/mob_cooldown/direct_and_aoe()
- ice_shotgun = new /datum/action/cooldown/mob_cooldown/projectile_attack/shotgun_blast/pattern()
- hard_ice_shotgun = new /datum/action/cooldown/mob_cooldown/projectile_attack/shotgun_blast/pattern/circular()
+ frost_orbs = new(src)
+ hard_frost_orbs = new(src)
+ snowball_machine_gun = new(src)
+ hard_snowball_machine_gun = new(src)
+ ice_shotgun = new(src)
+ hard_ice_shotgun = new(src)
frost_orbs.Grant(src)
hard_frost_orbs.Grant(src)
snowball_machine_gun.Grant(src)
@@ -83,17 +83,20 @@ Difficulty: Extremely Hard
AddComponent(/datum/component/boss_music, 'sound/lavaland/bdm_boss.ogg', 167 SECONDS)
/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner/Destroy()
- QDEL_NULL(frost_orbs)
- QDEL_NULL(hard_frost_orbs)
- QDEL_NULL(snowball_machine_gun)
- QDEL_NULL(hard_snowball_machine_gun)
- QDEL_NULL(ice_shotgun)
- QDEL_NULL(hard_ice_shotgun)
+ frost_orbs = null
+ hard_frost_orbs = null
+ snowball_machine_gun = null
+ hard_snowball_machine_gun = null
+ ice_shotgun = null
+ hard_ice_shotgun = null
return ..()
/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner/OpenFire()
if(client)
return
+ var/mob/living/living_target = target
+ if(istype(living_target) && living_target.stat == DEAD) //don't go out of our way to fire our disintegrating attacks at corpses
+ return
var/easy_attack = prob(80 - enraged * 40)
chosen_attack = rand(1, 3)
@@ -200,7 +203,7 @@ Difficulty: Extremely Hard
homing_turn_speed = 3
damage_type = BURN
-/obj/projectile/colossus/frost_orb/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/colossus/frost_orb/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(isturf(target) || isobj(target))
EX_ACT(target, EXPLODE_HEAVY)
@@ -226,7 +229,7 @@ Difficulty: Extremely Hard
range = 150
damage_type = BRUTE
-/obj/projectile/colossus/ice_blast/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/colossus/ice_blast/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(isturf(target) || isobj(target))
EX_ACT(target, EXPLODE_HEAVY)
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
index f516a52525cd6..abf93dcdaea17 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
@@ -1,7 +1,3 @@
-///used whenever the drake generates a hotspot
-#define DRAKE_FIRE_TEMP 500
-///used whenever the drake generates a hotspot
-#define DRAKE_FIRE_EXPOSURE 50
///used to see if the drake is enraged or not
#define DRAKE_ENRAGED (health < maxHealth*0.5)
@@ -84,10 +80,10 @@
/mob/living/simple_animal/hostile/megafauna/dragon/Initialize(mapload)
. = ..()
- fire_cone = new /datum/action/cooldown/mob_cooldown/fire_breath/cone()
- meteors = new /datum/action/cooldown/mob_cooldown/meteors()
- mass_fire = new /datum/action/cooldown/mob_cooldown/fire_breath/mass_fire()
- lava_swoop = new /datum/action/cooldown/mob_cooldown/lava_swoop()
+ fire_cone = new(src)
+ meteors = new(src)
+ mass_fire = new(src)
+ lava_swoop = new(src)
fire_cone.Grant(src)
meteors.Grant(src)
mass_fire.Grant(src)
@@ -96,22 +92,13 @@
RegisterSignal(src, COMSIG_MOB_ABILITY_FINISHED, PROC_REF(finished_attack))
RegisterSignal(src, COMSIG_SWOOP_INVULNERABILITY_STARTED, PROC_REF(swoop_invulnerability_started))
RegisterSignal(src, COMSIG_LAVA_ARENA_FAILED, PROC_REF(on_arena_fail))
+ AddElement(/datum/element/change_force_on_death, move_force = MOVE_FORCE_DEFAULT)
/mob/living/simple_animal/hostile/megafauna/dragon/Destroy()
- QDEL_NULL(fire_cone)
- QDEL_NULL(meteors)
- QDEL_NULL(mass_fire)
- QDEL_NULL(lava_swoop)
- return ..()
-
-/mob/living/simple_animal/hostile/megafauna/dragon/revive(full_heal_flags = NONE, excess_healing = 0, force_grab_ghost = FALSE)
- . = ..()
- if(!.)
- return
- pull_force = MOVE_FORCE_OVERPOWERING
-
-/mob/living/simple_animal/hostile/megafauna/dragon/death(gibbed)
- move_force = MOVE_FORCE_DEFAULT
+ fire_cone = null
+ meteors = null
+ mass_fire = null
+ lava_swoop = null
return ..()
/mob/living/simple_animal/hostile/megafauna/dragon/OpenFire()
@@ -175,36 +162,6 @@
remove_atom_colour(TEMPORARY_COLOUR_PRIORITY)
set_light_range(initial(light_range))
-//fire line keeps going even if dragon is deleted
-/proc/dragon_fire_line(atom/source, list/turfs, frozen = FALSE)
- var/list/hit_list = list()
- for(var/turf/T in turfs)
- if(isclosedturf(T))
- break
- var/obj/effect/hotspot/drake_fire_hotspot = new /obj/effect/hotspot(T)
- if(frozen)
- drake_fire_hotspot.add_atom_colour(COLOR_BLUE_LIGHT, FIXED_COLOUR_PRIORITY)
- T.hotspot_expose(DRAKE_FIRE_TEMP,DRAKE_FIRE_EXPOSURE,1)
- for(var/mob/living/L in T.contents)
- if(L in hit_list || istype(L, source.type))
- continue
- hit_list += L
- if(!frozen)
- L.adjustFireLoss(20)
- to_chat(L, span_userdanger("You're hit by [source]'s fire breath!"))
- continue
- L.adjustFireLoss(10)
- L.apply_status_effect(/datum/status_effect/ice_block_talisman, 20)
- to_chat(L, span_userdanger("You're hit by [source]'s freezing breath!"))
-
- // deals damage to mechs
- for(var/obj/vehicle/sealed/mecha/M in T.contents)
- if(M in hit_list)
- continue
- hit_list += M
- M.take_damage(45, BRUTE, MELEE, 1)
- sleep(0.15 SECONDS)
-
/mob/living/simple_animal/hostile/megafauna/dragon/ex_act(severity, target)
if(severity <= EXPLODE_LIGHT)
return FALSE
@@ -222,7 +179,7 @@
return
return ..()
-/mob/living/simple_animal/hostile/megafauna/dragon/AttackingTarget()
+/mob/living/simple_animal/hostile/megafauna/dragon/AttackingTarget(atom/attacked_target)
if(!swooping)
return ..()
@@ -365,14 +322,11 @@
/mob/living/simple_animal/hostile/megafauna/dragon/lesser/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
. = ..()
- lava_swoop.enraged = FALSE
+ lava_swoop?.enraged = FALSE // In case taking damage caused us to start deleting ourselves
/mob/living/simple_animal/hostile/megafauna/dragon/lesser/grant_achievement(medaltype,scoretype)
return
#undef DRAKE_ENRAGED
-#undef DRAKE_FIRE_EXPOSURE
-#undef DRAKE_FIRE_TEMP
-
#undef SWOOP_DAMAGEABLE
#undef SWOOP_INVULNERABLE
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
index 828a78ccfb1a0..375d4993cfdda 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
@@ -453,7 +453,7 @@ Difficulty: Hard
wander = TRUE
did_reset = FALSE
-/mob/living/simple_animal/hostile/megafauna/hierophant/AttackingTarget()
+/mob/living/simple_animal/hostile/megafauna/hierophant/AttackingTarget(atom/attacked_target)
if(!blinking)
if(target && isliving(target))
var/mob/living/L = target
@@ -694,7 +694,7 @@ Difficulty: Hard
return
for(var/mob/living/L in T.contents - hit_things) //find and damage mobs...
hit_things += L
- if((friendly_fire_check && caster?.faction_check_mob(L)) || L.stat == DEAD)
+ if((friendly_fire_check && caster?.faction_check_atom(L)) || L.stat == DEAD)
continue
if(L.client)
flash_color(L.client, "#660099", 1)
@@ -719,7 +719,7 @@ Difficulty: Hard
hit_things += M
for(var/O in M.occupants)
var/mob/living/occupant = O
- if(friendly_fire_check && caster?.faction_check_mob(occupant))
+ if(friendly_fire_check && caster?.faction_check_atom(occupant))
continue
to_chat(occupant, span_userdanger("Your [M.name] is struck by a [name]!"))
playsound(M,'sound/weapons/sear.ogg', 50, TRUE, -4)
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/legion.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/legion.dm
index 777cb3b878f73..eda344ad1daa6 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/legion.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/legion.dm
@@ -202,7 +202,7 @@
///In addition to parent functionality, this will also turn the target into a small legion if they are unconscious.
-/mob/living/simple_animal/hostile/megafauna/legion/AttackingTarget()
+/mob/living/simple_animal/hostile/megafauna/legion/AttackingTarget(atom/attacked_target)
. = ..()
if(!. || !ishuman(target))
return
@@ -269,14 +269,14 @@
density = TRUE
layer = ABOVE_OBJ_LAYER
armor_type = /datum/armor/structure_legionturret
+ //Compared with the targeted mobs. If they have the faction, turret won't shoot.
+ faction = list(FACTION_MINING)
///What kind of projectile the actual damaging part should be.
var/projectile_type = /obj/projectile/beam/legion
///Time until the tracer gets shot
var/initial_firing_time = 18
///How long it takes between shooting the tracer and the projectile.
var/shot_delay = 8
- ///Compared with the targeted mobs. If they have the faction, turret won't shoot.
- var/faction = list(FACTION_MINING)
/datum/armor/structure_legionturret
laser = 100
@@ -324,7 +324,6 @@
hitsound = 'sound/magic/magic_missile.ogg'
damage = 19
range = 6
- eyeblur = 0
light_color = COLOR_SOFT_RED
impact_effect_type = /obj/effect/temp_visual/kinetic_blast
tracer_type = /obj/effect/projectile/tracer/legion
diff --git a/code/modules/mob/living/simple_animal/hostile/mimic.dm b/code/modules/mob/living/simple_animal/hostile/mimic.dm
index d07775b42bd53..9540f5e1a4bba 100644
--- a/code/modules/mob/living/simple_animal/hostile/mimic.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mimic.dm
@@ -68,7 +68,7 @@
if(.)
trigger()
-/mob/living/simple_animal/hostile/mimic/crate/AttackingTarget()
+/mob/living/simple_animal/hostile/mimic/crate/AttackingTarget(atom/attacked_target)
. = ..()
if(.)
icon_state = initial(icon_state)
@@ -104,6 +104,7 @@ GLOBAL_LIST_INIT(animatable_blacklist, list(/obj/structure/table, /obj/structure
/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
@@ -184,7 +185,7 @@ GLOBAL_LIST_INIT(animatable_blacklist, list(/obj/structure/table, /obj/structure
if(destroy_objects)
..()
-/mob/living/simple_animal/hostile/mimic/copy/AttackingTarget()
+/mob/living/simple_animal/hostile/mimic/copy/AttackingTarget(atom/attacked_target)
. = ..()
if(knockdown_people && . && prob(15) && iscarbon(target))
var/mob/living/carbon/C = target
@@ -303,7 +304,7 @@ GLOBAL_LIST_INIT(animatable_blacklist, list(/obj/structure/table, /obj/structure
lock = new
lock.Grant(src)
-/mob/living/simple_animal/hostile/mimic/xenobio/AttackingTarget()
+/mob/living/simple_animal/hostile/mimic/xenobio/AttackingTarget(atom/attacked_target)
if(src == target)
toggle_open()
return
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm
index 6fe1fa1ecfc39..798fd51d69208 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm
@@ -29,15 +29,13 @@
/mob/living/simple_animal/hostile/asteroid/elite/Initialize(mapload)
. = ..()
AddComponent(/datum/component/seethrough_mob)
- for(var/action_type in attack_action_types)
- var/datum/action/innate/elite_attack/attack_action = new action_type()
- attack_action.Grant(src)
+ grant_actions_by_list(attack_action_types)
//Prevents elites from attacking members of their faction (can't hurt themselves either) and lets them mine rock with an attack despite not being able to smash walls.
-/mob/living/simple_animal/hostile/asteroid/elite/AttackingTarget()
+/mob/living/simple_animal/hostile/asteroid/elite/AttackingTarget(atom/attacked_target)
if(ishostile(target))
var/mob/living/simple_animal/hostile/M = target
- if(faction_check_mob(M))
+ if(faction_check_atom(M))
return FALSE
if(istype(target, /obj/structure/elite_tumor))
var/obj/structure/elite_tumor/T = target
@@ -214,7 +212,13 @@ While using this makes the system rely on OnFire, it still gives options for tim
if(boosted)
mychild.key = elitemind.key
mychild.sentience_act()
- notify_ghosts("\A [mychild] has been awakened in \the [get_area(src)]!", source = mychild, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "Lavaland Elite awakened")
+ notify_ghosts(
+ "\A [mychild] has been awakened in \the [get_area(src)]!",
+ source = mychild,
+ action = NOTIFY_ORBIT,
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ header = "Lavaland Elite awakened",
+ )
mychild.log_message("has been awakened by [key_name(activator)]!", LOG_GAME, color="#960000")
icon_state = "tumor_popped"
RegisterSignal(mychild, COMSIG_QDELETING, PROC_REF(onEliteLoss))
@@ -228,7 +232,13 @@ While using this makes the system rely on OnFire, it still gives options for tim
if(boosted)
mychild.maxHealth = mychild.maxHealth * 2
mychild.health = mychild.maxHealth
- notify_ghosts("\A [mychild] has been challenged in \the [get_area(src)]!", source = mychild, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "Lavaland Elite challenged")
+ notify_ghosts(
+ "\A [mychild] has been challenged in \the [get_area(src)]!",
+ source = mychild,
+ action = NOTIFY_ORBIT,
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ header = "Lavaland Elite challenged",
+ )
mychild.log_message("has been challenged by [key_name(activator)]!", LOG_GAME, color="#960000")
/obj/structure/elite_tumor/Initialize(mapload)
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/herald.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/herald.dm
index bf4d33a10ed95..9517bce4f92e7 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/herald.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/herald.dm
@@ -227,14 +227,13 @@
damage = 20
armour_penetration = 60
speed = 2
- eyeblur = 0
damage_type = BRUTE
pass_flags = PASSTABLE
-/obj/projectile/herald/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/herald/on_hit(atom/target, blocked = 0, pierce_hit)
if(ismob(target) && ismob(firer))
var/mob/living/mob_target = target
- if(mob_target.faction_check_mob(firer))
+ if(mob_target.faction_check_atom(firer))
damage = 0
. = ..()
@@ -247,7 +246,7 @@
damage = 0
color = rgb(255,255,102)
-/obj/projectile/herald/teleshot/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/herald/teleshot/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(!QDELETED(firer))
firer.forceMove(get_turf(src))
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
index ec6c843080c96..926f4e96baaef 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
@@ -149,7 +149,7 @@
var/throwtarget = get_edge_target_turf(src, move_dir)
for(var/mob/living/trample_target in T.contents - hit_things - src)
hit_things += trample_target
- if(faction_check_mob(trample_target))
+ if(faction_check_atom(trample_target))
continue
visible_message(span_boldwarning("[src] tramples and kicks [trample_target]!"))
to_chat(trample_target, span_userdanger("[src] tramples you and kicks you away!"))
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/gutlunch.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/gutlunch.dm
index 92ad5f3592746..7a3451e4ef86e 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/gutlunch.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/gutlunch.dm
@@ -60,7 +60,7 @@
if(isliving(the_target))
var/mob/living/L = the_target
- if(faction_check_mob(L) && !attack_same)
+ if(faction_check_atom(L) && !attack_same)
return FALSE
if(L.stat > stat_attack || L.stat != stat_attack && stat_exclusive)
return FALSE
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/polarbear.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/polarbear.dm
index d70a8f9eaa2c2..0b163124a8e7c 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/polarbear.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/polarbear.dm
@@ -36,6 +36,15 @@
/// Message for when the polar bear starts to attack faster
var/aggressive_message_said = FALSE
+/mob/living/simple_animal/hostile/asteroid/polarbear/Initialize(mapload)
+ . = ..()
+ AddElement(\
+ /datum/element/change_force_on_death,\
+ move_force = MOVE_FORCE_DEFAULT,\
+ move_resist = MOVE_RESIST_DEFAULT,\
+ pull_force = PULL_FORCE_DEFAULT,\
+ )
+
/mob/living/simple_animal/hostile/asteroid/polarbear/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
. = ..()
if(health > maxHealth*0.5)
@@ -52,20 +61,6 @@
return
aggressive_message_said = FALSE
-/mob/living/simple_animal/hostile/asteroid/polarbear/death(gibbed)
- move_force = MOVE_FORCE_DEFAULT
- move_resist = MOVE_RESIST_DEFAULT
- pull_force = PULL_FORCE_DEFAULT
- return ..()
-
-/mob/living/simple_animal/hostile/asteroid/polarbear/revive(full_heal_flags = NONE, excess_healing = 0, force_grab_ghost = FALSE)
- . = ..()
- if(!.)
- return
- move_force = initial(move_force)
- move_resist = initial(move_resist)
- pull_force = initial(pull_force)
-
/mob/living/simple_animal/hostile/asteroid/polarbear/lesser
name = "magic polar bear"
desc = "It seems sentient somehow."
diff --git a/code/modules/mob/living/simple_animal/hostile/nanotrasen.dm b/code/modules/mob/living/simple_animal/hostile/nanotrasen.dm
deleted file mode 100644
index 06c0cbfd22be6..0000000000000
--- a/code/modules/mob/living/simple_animal/hostile/nanotrasen.dm
+++ /dev/null
@@ -1,111 +0,0 @@
-/mob/living/simple_animal/hostile/nanotrasen
- name = "\improper Nanotrasen Private Security Officer"
- desc = "An officer part of Nanotrasen's private security force, he seems rather unpleased to meet you."
- icon = 'icons/mob/simple/simple_human.dmi'
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- sentience_type = SENTIENCE_HUMANOID
- speak_chance = 0
- turns_per_move = 5
- speed = 0
- stat_attack = HARD_CRIT
- robust_searching = 1
- maxHealth = 100
- health = 100
- harm_intent_damage = 5
- melee_damage_lower = 10
- melee_damage_upper = 15
- attack_verb_continuous = "punches"
- attack_verb_simple = "punch"
- attack_sound = 'sound/weapons/punch1.ogg'
- combat_mode = TRUE
- loot = list(/obj/effect/mob_spawn/corpse/human/nanotrasensoldier)
- atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
- unsuitable_atmos_damage = 7.5
- faction = list(ROLE_DEATHSQUAD)
- check_friendly_fire = TRUE
- status_flags = CANPUSH
- del_on_death = TRUE
- dodging = TRUE
- footstep_type = FOOTSTEP_MOB_SHOE
- /// Path of the mob spawner we base the mob's visuals off of.
- var/mob_spawner = /obj/effect/mob_spawn/corpse/human/nanotrasensoldier
- /// Path of the held item we give to the mob's visuals.
- var/held_item
-
-/mob/living/simple_animal/hostile/nanotrasen/Initialize(mapload)
- . = ..()
- apply_dynamic_human_appearance(src, mob_spawn_path = mob_spawner, r_hand = held_item)
-
-/mob/living/simple_animal/hostile/nanotrasen/screaming/Aggro()
- ..()
- summon_backup(15)
- say("411 in progress, requesting backup!")
-
-/mob/living/simple_animal/hostile/nanotrasen/ranged
- icon_state = "nanotrasenranged"
- icon_living = "nanotrasenranged"
- ranged = 1
- retreat_distance = 3
- minimum_distance = 5
- casingtype = /obj/item/ammo_casing/c45
- projectilesound = 'sound/weapons/gun/pistol/shot_alt.ogg'
- held_item = /obj/item/gun/ballistic/automatic/pistol/m1911
-
-/mob/living/simple_animal/hostile/nanotrasen/ranged/smg
- icon_state = "nanotrasenrangedsmg"
- icon_living = "nanotrasenrangedsmg"
- rapid = 3
- casingtype = /obj/item/ammo_casing/c46x30mm
- projectilesound = 'sound/weapons/gun/smg/shot.ogg'
- held_item = /obj/item/gun/ballistic/automatic/wt550
-
-
-/mob/living/simple_animal/hostile/retaliate/nanotrasenpeace
- name = "\improper Nanotrasen Private Security Officer"
- desc = "An officer part of Nanotrasen's private security force."
- icon = 'icons/mob/simple/simple_human.dmi'
- turns_per_move = 5
- speed = 0
- stat_attack = HARD_CRIT
- robust_searching = 1
- vision_range = 3
- maxHealth = 100
- health = 100
- harm_intent_damage = 5
- melee_damage_lower = 10
- melee_damage_upper = 15
- attack_verb_continuous = "punches"
- attack_verb_simple = "punch"
- attack_sound = 'sound/weapons/punch1.ogg'
- faction = list(FACTION_NANOTRASEN_PRIVATE)
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- sentience_type = SENTIENCE_HUMANOID
- combat_mode = TRUE
- loot = list(/obj/effect/mob_spawn/corpse/human/nanotrasensoldier)
- atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
- unsuitable_atmos_damage = 7.5
- status_flags = CANPUSH
- search_objects = 1
- /// Path of the held item we give to the mob's visuals.
- var/held_item
-
-/mob/living/simple_animal/hostile/retaliate/nanotrasenpeace/Initialize(mapload)
- . = ..()
- apply_dynamic_human_appearance(src, mob_spawn_path = /obj/effect/mob_spawn/corpse/human/nanotrasensoldier, r_hand = held_item)
-
-/mob/living/simple_animal/hostile/retaliate/nanotrasenpeace/Aggro()
- ..()
- summon_backup(15)
- say("411 in progress, requesting backup!")
-
-/mob/living/simple_animal/hostile/retaliate/nanotrasenpeace/ranged
- vision_range = 9
- rapid = 3
- ranged = 1
- retreat_distance = 3
- minimum_distance = 5
- casingtype = /obj/item/ammo_casing/c46x30mm
- projectilesound = 'sound/weapons/gun/smg/shot.ogg'
- loot = list(/obj/item/gun/ballistic/automatic/wt550,
- /obj/effect/mob_spawn/corpse/human/nanotrasensoldier)
- held_item = /obj/item/gun/ballistic/automatic/wt550
diff --git a/code/modules/mob/living/simple_animal/hostile/ooze.dm b/code/modules/mob/living/simple_animal/hostile/ooze.dm
index b1c8c55cacc66..2e1db456b5b28 100644
--- a/code/modules/mob/living/simple_animal/hostile/ooze.dm
+++ b/code/modules/mob/living/simple_animal/hostile/ooze.dm
@@ -41,6 +41,8 @@
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
AddElement(/datum/element/content_barfer)
+ grant_actions_by_list(get_innate_actions())
+
/mob/living/simple_animal/hostile/ooze/attacked_by(obj/item/I, mob/living/user)
if(!eat_atom(I, TRUE))
return ..()
@@ -71,6 +73,10 @@
if(ooze_nutrition <= 0)
adjustBruteLoss(0.25 * seconds_per_tick)
+/// Returns an applicable list of actions to grant to the mob. Will return a list or null.
+/mob/living/simple_animal/hostile/ooze/proc/get_innate_actions()
+ return null
+
///Does ooze_nutrition + supplied amount and clamps it within 0 and 500
/mob/living/simple_animal/hostile/ooze/proc/adjust_ooze_nutrition(amount)
ooze_nutrition = clamp(ooze_nutrition + amount, 0, 500)
@@ -78,6 +84,8 @@
///Tries to transfer the atoms reagents then delete it
/mob/living/simple_animal/hostile/ooze/proc/eat_atom(atom/eat_target, silent)
+ if(isnull(eat_target))
+ return
if(SEND_SIGNAL(eat_target, COMSIG_OOZE_EAT_ATOM, src, edible_food_types) & COMPONENT_ATOM_EATEN)
return
if(silent || !isitem(eat_target)) //Don't bother reporting it for everything
@@ -104,23 +112,20 @@
armour_penetration = 15
obj_damage = 20
death_message = "collapses into a pile of goo!"
- ///The ability to give yourself a metabolic speed boost which raises heat
- var/datum/action/cooldown/metabolicboost/boost
///The ability to consume mobs
var/datum/action/consume/consume
///Initializes the mobs abilities and gives them to the mob
/mob/living/simple_animal/hostile/ooze/gelatinous/Initialize(mapload)
. = ..()
- boost = new
- boost.Grant(src)
consume = new
consume.Grant(src)
-/mob/living/simple_animal/hostile/ooze/gelatinous/Destroy()
- . = ..()
- QDEL_NULL(boost)
- QDEL_NULL(consume)
+/mob/living/simple_animal/hostile/ooze/gelatinous/get_innate_actions()
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/metabolicboost,
+ )
+ return innate_actions
///If this mob gets resisted by something, its trying to escape consumption.
/mob/living/simple_animal/hostile/ooze/gelatinous/container_resist_act(mob/living/user)
@@ -283,12 +288,12 @@
death_message = "deflates and spills its vital juices!"
edible_food_types = MEAT | VEGETABLES
-/mob/living/simple_animal/hostile/ooze/grapes/Initialize(mapload)
- . = ..()
- var/datum/action/cooldown/globules/glob_shooter = new(src)
- glob_shooter.Grant(src)
- var/datum/action/cooldown/gel_cocoon/gel_cocoon = new(src)
- gel_cocoon.Grant(src)
+/mob/living/simple_animal/hostile/ooze/grapes/get_innate_actions()
+ var/static/list/innate_actions = list(
+ /datum/action/cooldown/globules,
+ /datum/action/cooldown/gel_cocoon,
+ )
+ return innate_actions
/mob/living/simple_animal/hostile/ooze/grapes/add_cell_sample()
AddElement(/datum/element/swabable, CELL_LINE_TABLE_GRAPE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
diff --git a/code/modules/mob/living/simple_animal/hostile/pirate.dm b/code/modules/mob/living/simple_animal/hostile/pirate.dm
deleted file mode 100644
index 24503f89bfdf1..0000000000000
--- a/code/modules/mob/living/simple_animal/hostile/pirate.dm
+++ /dev/null
@@ -1,91 +0,0 @@
-/mob/living/simple_animal/hostile/pirate
- name = "Pirate"
- desc = "Does what he wants cause a pirate is free."
- icon = 'icons/mob/simple/simple_human.dmi'
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- sentience_type = SENTIENCE_HUMANOID
- speak_chance = 0
- turns_per_move = 5
- response_help_continuous = "pushes"
- response_help_simple = "push"
- speed = 0
- maxHealth = 100
- health = 100
- harm_intent_damage = 5
- melee_damage_lower = 10
- melee_damage_upper = 10
- attack_verb_continuous = "punches"
- attack_verb_simple = "punch"
- attack_sound = 'sound/weapons/punch1.ogg'
- combat_mode = TRUE
- atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
- unsuitable_atmos_damage = 7.5
- speak_emote = list("yarrs")
- loot = list(/obj/effect/mob_spawn/corpse/human/pirate)
- del_on_death = TRUE
- faction = list(FACTION_PIRATE)
- /// Path of the mob spawner we base the mob's visuals off of.
- var/mob_spawner = /obj/effect/mob_spawn/corpse/human/pirate
- /// Path of the held item we give to the mob's visuals.
- var/held_item
-
-/mob/living/simple_animal/hostile/pirate/Initialize(mapload)
- . = ..()
- apply_dynamic_human_appearance(src, mob_spawn_path = mob_spawner, r_hand = held_item)
-
-/mob/living/simple_animal/hostile/pirate/melee
- name = "Pirate Swashbuckler"
- melee_damage_lower = 30
- melee_damage_upper = 30
- armour_penetration = 35
- attack_verb_continuous = "slashes"
- attack_verb_simple = "slash"
- attack_sound = 'sound/weapons/blade1.ogg'
- attack_vis_effect = ATTACK_EFFECT_SLASH
- loot = list(/obj/effect/mob_spawn/corpse/human/pirate/melee)
- light_range = 2
- light_power = 2.5
- light_color = COLOR_SOFT_RED
- footstep_type = FOOTSTEP_MOB_SHOE
- loot = list(
- /obj/effect/mob_spawn/corpse/human/pirate/melee,
- /obj/item/melee/energy/sword/pirate,
- )
- mob_spawner = /obj/effect/mob_spawn/corpse/human/pirate/melee
- held_item = /obj/item/melee/energy/sword/pirate
-
-/mob/living/simple_animal/hostile/pirate/melee/space
- name = "Space Pirate Swashbuckler"
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- minbodytemp = 0
- speed = 1
- mob_spawner = /obj/effect/mob_spawn/corpse/human/pirate/melee/space
-
-/mob/living/simple_animal/hostile/pirate/melee/space/Initialize(mapload)
- . = ..()
- ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
-
-/mob/living/simple_animal/hostile/pirate/ranged
- name = "Pirate Gunner"
- projectilesound = 'sound/weapons/laser.ogg'
- ranged = 1
- rapid = 2
- rapid_fire_delay = 6
- retreat_distance = 5
- minimum_distance = 5
- projectiletype = /obj/projectile/beam/laser
- loot = list(/obj/effect/mob_spawn/corpse/human/pirate/ranged)
- mob_spawner = /obj/effect/mob_spawn/corpse/human/pirate/ranged
- held_item = /obj/item/gun/energy/laser
-
-/mob/living/simple_animal/hostile/pirate/ranged/space
- name = "Space Pirate Gunner"
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- minbodytemp = 0
- speed = 1
- mob_spawner = /obj/effect/mob_spawn/corpse/human/pirate/ranged/space
- held_item = /obj/item/gun/energy/e_gun/lethal
-
-/mob/living/simple_animal/hostile/pirate/ranged/space/Initialize(mapload)
- . = ..()
- ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/retaliate.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/retaliate.dm
index 237360468f8da..bf1c12d5da1ee 100644
--- a/code/modules/mob/living/simple_animal/hostile/retaliate/retaliate.dm
+++ b/code/modules/mob/living/simple_animal/hostile/retaliate/retaliate.dm
@@ -37,7 +37,7 @@
continue
if(isliving(A))
var/mob/living/M = A
- if(faction_check_mob(M) && attack_same || !faction_check_mob(M))
+ if(faction_check_atom(M) && attack_same || !faction_check_atom(M))
enemies |= WEAKREF(M)
else if(ismecha(A))
var/obj/vehicle/sealed/mecha/M = A
@@ -46,7 +46,7 @@
add_enemies(M.occupants)
for(var/mob/living/simple_animal/hostile/retaliate/H in around)
- if(faction_check_mob(H) && !attack_same && !H.attack_same)
+ if(faction_check_atom(H) && !attack_same && !H.attack_same)
H.enemies |= enemies
/mob/living/simple_animal/hostile/retaliate/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/trader.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/trader.dm
deleted file mode 100644
index 5be6fc9575d8f..0000000000000
--- a/code/modules/mob/living/simple_animal/hostile/retaliate/trader.dm
+++ /dev/null
@@ -1,501 +0,0 @@
-#define ITEM_REJECTED_PHRASE "ITEM_REJECTED_PHRASE"
-#define ITEM_SELLING_CANCELED_PHRASE "ITEM_SELLING_CANCELED_PHRASE"
-#define ITEM_SELLING_ACCEPTED_PHRASE "ITEM_SELLING_ACCEPTED_PHRASE"
-#define INTERESTED_PHRASE "INTERESTED_PHRASE"
-#define BUY_PHRASE "BUY_PHRASE"
-#define NO_CASH_PHRASE "NO_CASH_PHRASE"
-#define NO_STOCK_PHRASE "NO_STOCK_PHRASE"
-#define NOT_WILLING_TO_BUY_PHRASE "NOT_WILLING_TO_BUY_PHRASE"
-#define ITEM_IS_WORTHLESS_PHRASE "ITEM_IS_WORTHLESS_PHRASE"
-#define TRADER_HAS_ENOUGH_ITEM_PHRASE "TRADER_HAS_ENOUGH_ITEM_PHRASE"
-#define TRADER_LORE_PHRASE "TRADER_LORE_PHRASE"
-#define TRADER_NOT_BUYING_ANYTHING "TRADER_NOT_BUYING_ANYTHING"
-#define TRADER_NOT_SELLING_ANYTHING "TRADER_NOT_SELLING_ANYTHING"
-
-#define TRADER_PRODUCT_INFO_PRICE 1
-#define TRADER_PRODUCT_INFO_QUANTITY 2
-//Only valid for wanted_items
-#define TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION 3
-
-/**
- * # Trader
- *
- * A mob that has some dialogue options with radials, allows for selling items and buying em'
- *
- */
-/mob/living/simple_animal/hostile/retaliate/trader
- name = "Trader"
- desc = "Come buy some!"
- icon = 'icons/mob/simple/traders.dmi'
- icon_state = "faceless"
- maxHealth = 200
- health = 200
- melee_damage_lower = 10
- melee_damage_upper = 10
- attack_verb_continuous = "punches"
- attack_verb_simple = "punch"
- attack_sound = 'sound/weapons/punch1.ogg'
- del_on_death = TRUE
- loot = list(/obj/effect/mob_spawn/corpse/human)
- atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
- unsuitable_atmos_damage = 2.5
- casingtype = /obj/item/ammo_casing/shotgun/buckshot
- wander = FALSE
- ranged = TRUE
- combat_mode = TRUE
- move_resist = MOVE_FORCE_STRONG
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- sentience_type = SENTIENCE_HUMANOID
- speed = 0
- stat_attack = HARD_CRIT
- robust_searching = TRUE
- check_friendly_fire = TRUE
- interaction_flags_atom = INTERACT_ATOM_NO_FINGERPRINT_ATTACK_HAND|INTERACT_ATOM_ATTACK_HAND|INTERACT_ATOM_NO_FINGERPRINT_INTERACT
- ///Sound used when item sold/bought
- var/sell_sound = 'sound/effects/cashregister.ogg'
- /**
- * Format; list(TYPEPATH = list(PRICE, QUANTITY))
- * Associated list of items the NPC sells with how much they cost and the quantity available before a restock
- * This list is filled by Initialize(), if you want to change the starting products, modify initial_products()
- * *
- */
- var/list/products
- /**
- * A list of wanted items that the trader would wish to buy, each typepath has a assigned value, quantity and additional flavor text
- *
- * CHILDREN OF TYPEPATHS INCLUDED IN WANTED_ITEMS WILL BE TREATED AS THE PARENT IF NO ENTRY EXISTS FOR THE CHILDREN
- *
- * As an additional note; if you include multiple children of a typepath; the typepath with the most children should be placed after all other typepaths
- * Bad; list(/obj/item/milk = list(100, 1, ""), /obj/item/milk/small = list(50, 2, ""))
- * Good; list(/obj/item/milk/small = list(50, 2, ""), /obj/item/milk = list(100, 1, ""))
- * This is mainly because sell_item() uses a istype(item_being_sold, item_in_entry) to determine what parent should the child be automatically considered as
- * If /obj/item/milk/small/spooky was being sold; /obj/item/milk/small would be the first to check against rather than /obj/item/milk
- *
- * Format; list(TYPEPATH = list(PRICE, QUANTITY, ADDITIONAL_DESCRIPTION))
- * Associated list of items able to be sold to the NPC with the money given for them.
- * The price given should be the "base" price; any price manipulation based on variables should be done with apply_sell_price_mods()
- * ADDITIONAL_DESCRIPTION is any additional text added to explain how the variables of the item effect the price; if it's stack based, it's final price depends how much is in the stack
- * EX; /obj/item/stack/sheet/mineral/diamond = list(500, INFINITY, ", per 100 cm3 sheet of diamond")
- * This list is filled by Initialize(), if you want to change the starting wanted items, modify initial_wanteds()
- */
- var/list/wanted_items
- ///Associated list of defines matched with list of phrases; phrase to be said is dealt by return_trader_phrase()
- var/list/say_phrases = list(
- ITEM_REJECTED_PHRASE = list(
- "Sorry, I'm not a fan of anything you're showing me. Give me something better and we'll talk."
- ),
- ITEM_SELLING_CANCELED_PHRASE = list(
- "What a shame, tell me if you changed your mind."
- ),
- ITEM_SELLING_ACCEPTED_PHRASE = list(
- "Pleasure doing business with you."
- ),
- INTERESTED_PHRASE = list(
- "Hey, you've got an item that interests me, I'd like to buy it, I'll give you some cash for it, deal?"
- ),
- BUY_PHRASE = list(
- "Pleasure doing business with you."
- ),
- NO_CASH_PHRASE = list(
- "Sorry adventurer, I can't give credit! Come back when you're a little mmmmm... richer!"
- ),
- NO_STOCK_PHRASE = list(
- "Sorry adventurer, but that item is not in stock at the moment."
- ),
- NOT_WILLING_TO_BUY_PHRASE = list(
- "I don't want to buy that item for the time being, check back another time."
- ),
- ITEM_IS_WORTHLESS_PHRASE = list(
- "This item seems to be worthless on a closer look, I won't buy this."
- ),
- TRADER_HAS_ENOUGH_ITEM_PHRASE = list(
- "I already bought enough of this for the time being."
- ),
- TRADER_LORE_PHRASE = list(
- "Hello! I am the test trader.",
- "Oooooooo~!"
- ),
- TRADER_NOT_BUYING_ANYTHING = list(
- "I'm currently buying nothing at the moment."
- ),
- TRADER_NOT_SELLING_ANYTHING = list(
- "I'm currently selling nothing at the moment."
- ),
- )
- ///The name of the currency that is used when buying or selling items
- var/currency_name = "credits"
-
-///Initializes the products and item demands of the trader
-/mob/living/simple_animal/hostile/retaliate/trader/Initialize(mapload)
- . = ..()
- restock_products()
- renew_item_demands()
-
-///Returns a list of the starting price/quanity/fluff text about the product listings; products = initial(products) doesn't work so this exists mainly for restock_products()
-/mob/living/simple_animal/hostile/retaliate/trader/proc/initial_products()
- return list(/obj/item/food/burger/ghost = list(200, INFINITY),
- )
-
-///Returns a list of the starting price/quanity/fluff text about the wanted items; wanted_items = initial(wanted_items) doesn't work so this exists mainly for renew_item_demands()
-/mob/living/simple_animal/hostile/retaliate/trader/proc/initial_wanteds()
- return list(/obj/item/ectoplasm = list(100, INFINITY, ""),
- )
-
-/**
- * Depending on the passed parameter/override, returns a randomly picked string out of a list
- *
- * Do note when overriding this argument, you will need to ensure pick(the list) doesn't get supplied with a list of zero length
- * Arguments:
- * * say_text - (String) a define that matches the key of a entry in say_phrases
- */
-/mob/living/simple_animal/hostile/retaliate/trader/proc/return_trader_phrase(say_text)
- if(!length(say_phrases[say_text]))
- return
- return pick(say_phrases[say_text])
- //return (length(say_phrases[say_text]) ? pick(say_phrases[say_text]) : "")
-
-///Sets up the radials for the user and calls procs related to the actions the user wants to take
-/mob/living/simple_animal/hostile/retaliate/trader/interact(mob/user)
- if(user == target)
- return FALSE
- var/list/npc_options = list()
- if(products.len)
- npc_options["Buy"] = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_buy")
- if(length(say_phrases[TRADER_LORE_PHRASE]))
- npc_options["Talk"] = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_talk")
- if(wanted_items.len)
- npc_options["Sell"] = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_sell")
- if(!npc_options.len)
- return FALSE
- var/npc_result = show_radial_menu(user, src, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE)
- switch(npc_result)
- if("Buy")
- buy_item(user)
- if("Sell")
- try_sell(user)
- if("Talk")
- discuss(user)
- face_atom(user)
- return TRUE
-
-/**
- * Checks if the user is ok to use the radial
- *
- * Checks if the user is not a mob or is incapacitated or not adjacent to the source of the radial, in those cases returns FALSE, otherwise returns TRUE
- * Arguments:
- * * user - (Mob REF) The mob checking the menu
- */
-/mob/living/simple_animal/hostile/retaliate/trader/proc/check_menu(mob/user)
- if(!istype(user))
- return FALSE
- if(user.incapacitated() || !user.Adjacent(src))
- return FALSE
- return TRUE
-
-///Talk about what items are being sold/wanted by the trader and in what quantity or lore
-/mob/living/simple_animal/hostile/retaliate/trader/proc/discuss(mob/user)
- var/list/npc_options = list(
- "Lore" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_lore"),
- "Selling?" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_selling"),
- "Buying?" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_buying"),
- )
- var/pick = show_radial_menu(user, src, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE)
- switch(pick)
- if("Lore")
- say(return_trader_phrase(TRADER_LORE_PHRASE))
- if("Buying?")
- trader_buys_what(user)
- if("Selling?")
- trader_sells_what(user)
-
-///Displays to the user what the trader is willing to buy and how much until a restock happens
-/mob/living/simple_animal/hostile/retaliate/trader/proc/trader_buys_what(mob/user)
- if(!wanted_items.len)
- say(return_trader_phrase(TRADER_NOT_BUYING_ANYTHING))
- return
- var/list/product_info
- to_chat(user, span_green("I'm willing to buy the following; "))
- for(var/obj/item/thing as anything in wanted_items)
- product_info = wanted_items[thing]
- var/tern_op_result = (product_info[TRADER_PRODUCT_INFO_QUANTITY] == INFINITY ? "as many as I can." : "[product_info[TRADER_PRODUCT_INFO_QUANTITY]]") //Coder friendly string concat
- if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //Zero demand
- to_chat(user, span_notice("[span_red("(DOESN'T WANT MORE)")] [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [currency_name][product_info[TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION]]; willing to buy [span_red("[tern_op_result]")] more."))
- else
- to_chat(user, span_notice("[initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [currency_name][product_info[TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION]]; willing to buy [span_green("[tern_op_result]")]"))
-
-///Displays to the user what the trader is selling and how much is in stock
-/mob/living/simple_animal/hostile/retaliate/trader/proc/trader_sells_what(mob/user)
- if(!products.len)
- say(return_trader_phrase(TRADER_NOT_SELLING_ANYTHING))
- return
- var/list/product_info
- to_chat(user, span_green("I'm currently selling the following; "))
- for(var/obj/item/thing as anything in products)
- product_info = products[thing]
- var/tern_op_result = (product_info[TRADER_PRODUCT_INFO_QUANTITY] == INFINITY ? "an infinite amount" : "[product_info[TRADER_PRODUCT_INFO_QUANTITY]]") //Coder friendly string concat
- if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //Out of stock
- to_chat(user, span_notice("[span_red("(OUT OF STOCK)")] [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [currency_name]; [span_red("[tern_op_result]")] left in stock"))
- else
- to_chat(user, span_notice("[initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [currency_name]; [span_green("[tern_op_result]")] left in stock"))
-
-/**
- * Generates a radial of the items the NPC sells and lets the user try to buy one
- * Arguments:
- * * user - (Mob REF) The mob trying to buy something
- */
-/mob/living/simple_animal/hostile/retaliate/trader/proc/buy_item(mob/user)
- if(!LAZYLEN(products))
- return
-
- var/list/display_names = list()
- var/list/items = list()
- var/list/product_info
- for(var/obj/item/thing as anything in products)
- display_names["[initial(thing.name)]"] = thing
- var/image/item_image = image(icon = initial(thing.icon), icon_state = initial(thing.icon_state))
- product_info = products[thing]
- if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //out of stock
- item_image.overlays += image(icon = 'icons/hud/radial.dmi', icon_state = "radial_center")
- items += list("[initial(thing.name)]" = item_image)
- var/pick = show_radial_menu(user, src, items, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE)
- if(!pick)
- return
- var/obj/item/item_to_buy = display_names[pick]
- face_atom(user)
- product_info = products[item_to_buy]
- if(!product_info[TRADER_PRODUCT_INFO_QUANTITY])
- say("[initial(item_to_buy.name)] appears to be out of stock.")
- return
- say("It will cost you [product_info[TRADER_PRODUCT_INFO_PRICE]] [currency_name] to buy \the [initial(item_to_buy.name)]. Are you sure you want to buy it?")
- var/list/npc_options = list(
- "Yes" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"),
- "No" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no")
- )
- var/buyer_will_buy = show_radial_menu(user, src, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE)
- if(buyer_will_buy != "Yes")
- return
- face_atom(user)
- if(!spend_buyer_offhand_money(user, product_info[TRADER_PRODUCT_INFO_PRICE]))
- say(return_trader_phrase(NO_CASH_PHRASE))
- return
- item_to_buy = new item_to_buy(get_turf(user))
- user.put_in_hands(item_to_buy)
- playsound(src, sell_sound, 50, TRUE)
- product_info[TRADER_PRODUCT_INFO_QUANTITY] -= 1
- say(return_trader_phrase(BUY_PHRASE))
-
-///Calculates the value of money in the hand of the buyer and spends it if it's sufficient
-/mob/living/simple_animal/hostile/retaliate/trader/proc/spend_buyer_offhand_money(mob/user, the_cost)
- var/value = 0
- var/obj/item/holochip/cash = user.is_holding_item_of_type(/obj/item/holochip)
- if(cash)
- value += cash.credits
- if((value >= the_cost) && cash)
- return cash.spend(the_cost)
- return FALSE //Purchase unsuccessful
-
-/**
- * Tries to call sell_item on one of the user's held items, if fail gives a chat message
- *
- * Gets both items in the user's hands, and then tries to call sell_item on them, if both fail, he gives a chat message
- * Arguments:
- * * user - (Mob REF) The mob trying to sell something
- */
-/mob/living/simple_animal/hostile/retaliate/trader/proc/try_sell(mob/user)
- var/sold_item = FALSE
- for(var/obj/item/an_item in user.held_items)
- if(sell_item(user, an_item))
- sold_item = TRUE
- break
- if(!sold_item)
- say(return_trader_phrase(ITEM_REJECTED_PHRASE))
-
-/**
- * Checks if an item is in the list of wanted items and if it is after a Yes/No radial returns generate_cash with the value of the item for the NPC
- * Arguments:
- * * user - (Mob REF) The mob trying to sell something
- * * selling - (Item REF) The item being sold
- */
-/mob/living/simple_animal/hostile/retaliate/trader/proc/sell_item(mob/user, obj/item/selling)
- var/cost
- if(!selling)
- return FALSE
- var/list/product_info
- //Keep track of the typepath; rather mundane but it's required for correctly modifying the wanted_items
- //should a product be sellable because even if it doesn't have a entry because it's a child of a parent that is present on the list
- var/typepath_for_product_info
- if(selling.type in wanted_items)
- product_info = wanted_items[selling.type]
- typepath_for_product_info = selling.type
- else //Assume wanted_items is setup in the correct way; read wanted_items documentation for more info
- for(var/typepath in wanted_items)
- if(istype(selling, typepath))
- product_info = wanted_items[typepath]
- typepath_for_product_info = typepath
- break
-
- if(!product_info) //Nothing interesting to sell
- return FALSE
- if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0)
- say(return_trader_phrase(TRADER_HAS_ENOUGH_ITEM_PHRASE))
- return FALSE
- cost = apply_sell_price_mods(selling, product_info[TRADER_PRODUCT_INFO_PRICE])
- if(cost <= 0)
- say(return_trader_phrase(ITEM_IS_WORTHLESS_PHRASE))
- return FALSE
- say(return_trader_phrase(INTERESTED_PHRASE))
- say("You will receive [cost] [currency_name] for the [selling].")
- var/list/npc_options = list(
- "Yes" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"),
- "No" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no"),
- )
- face_atom(user)
- var/npc_result = show_radial_menu(user, src, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE)
- if(npc_result != "Yes")
- say(return_trader_phrase(ITEM_SELLING_CANCELED_PHRASE))
- return TRUE
- say(return_trader_phrase(ITEM_SELLING_ACCEPTED_PHRASE))
- playsound(src, sell_sound, 50, TRUE)
- log_econ("[selling] has been sold to [src] (typepath used for product info; [typepath_for_product_info]) by [user] for [cost] cash.")
- exchange_sold_items(selling, cost, typepath_for_product_info)
- generate_cash(cost, user)
- return TRUE
-
-/**
- * Handles modifying/deleting the items to ensure that a proper amount is converted into cash; put into it's own proc to make the children of this not override a 30+ line sell_item()
- *
- * Arguments:
- * * selling - (Item REF) this is the item being sold
- * * value_exchanged_for - (Number) the "value", useful for a scenario where you want to remove enough items equal to the value
- * * original_typepath - (Typepath) For scenarios where a children of a parent is being sold but we want to modify the parent's product information
- */
-/mob/living/simple_animal/hostile/retaliate/trader/proc/exchange_sold_items(obj/item/selling, value_exchanged_for, original_typepath)
- var/list/product_info = wanted_items[original_typepath]
- if(isstack(selling))
- var/obj/item/stack/the_stack = selling
- var/actually_sold = min(the_stack.amount, product_info[TRADER_PRODUCT_INFO_QUANTITY])
- the_stack.use(actually_sold)
- product_info[TRADER_PRODUCT_INFO_QUANTITY] -= (actually_sold)
- else
- qdel(selling)
- product_info[TRADER_PRODUCT_INFO_QUANTITY] -= 1
-
-/**
- * Modifies the 'base' price of a item based on certain variables
- *
- * Arguments:
- * * Reference to the item; this is the item being sold
- * * Original cost; the original cost of the item, to be manipulated depending on the variables of the item, one example is using item.amount if it's a stack
- */
-/mob/living/simple_animal/hostile/retaliate/trader/proc/apply_sell_price_mods(obj/item/selling, original_cost)
- if(isstack(selling))
- var/obj/item/stack/stackoverflow = selling
- original_cost *= stackoverflow.amount
- return original_cost
-
-/**
- * Creates an item equal to the value set by the proc and puts it in the user's hands if possible
- * Arguments:
- * * value - A number; The amount of cash that will be on the holochip
- * * user - Reference to a mob; The mob we put the holochip in hands of
- */
-/mob/living/simple_animal/hostile/retaliate/trader/proc/generate_cash(value, mob/user)
- var/obj/item/holochip/chip = new /obj/item/holochip(get_turf(user), value)
- user.put_in_hands(chip)
-
-///Sets quantity of all products to initial(quanity); this proc is currently not called anywhere on the base class of traders
-/mob/living/simple_animal/hostile/retaliate/trader/proc/restock_products()
- products = initial_products()
-
-///Sets quantity of all wanted_items to initial(quanity); this proc is currently not called anywhere on the base class of traders
-/mob/living/simple_animal/hostile/retaliate/trader/proc/renew_item_demands()
- wanted_items = initial_wanteds()
-
-/mob/living/simple_animal/hostile/retaliate/trader/mrbones
- name = "Mr. Bones"
- desc = "A skeleton merchant, he seems very humerus."
- speak_emote = list("rattles")
- speech_span = SPAN_SANS
- sell_sound = 'sound/voice/hiss2.ogg'
- mob_biotypes = MOB_UNDEAD|MOB_HUMANOID
- icon_state = "mrbones"
- gender = MALE
- loot = list(/obj/effect/decal/remains/human)
-
- say_phrases = list(
- ITEM_REJECTED_PHRASE = list(
- "Sorry, I'm not a fan of anything you're showing me. Give me something better and we'll talk."
- ),
- ITEM_SELLING_CANCELED_PHRASE = list(
- "What a shame, tell me if you changed your mind."
- ),
- ITEM_SELLING_ACCEPTED_PHRASE = list(
- "Pleasure doing business with you."
- ),
- INTERESTED_PHRASE = list(
- "Hey, you've got an item that interests me, I'd like to buy it, I'll give you some cash for it, deal?"
- ),
- BUY_PHRASE = list(
- "Bone appetit!"
- ),
- NO_CASH_PHRASE = list(
- "Sorry adventurer, I can't give credit! Come back when you're a little mmmmm... richer!"
- ),
- NO_STOCK_PHRASE = list(
- "Sorry adventurer, but that item is not in stock at the moment."
- ),
- NOT_WILLING_TO_BUY_PHRASE = list(
- "I don't want to buy that item for the time being, check back another time."
- ),
- ITEM_IS_WORTHLESS_PHRASE = list(
- "This item seems to be worthless on a closer look, I won't buy this."
- ),
- TRADER_HAS_ENOUGH_ITEM_PHRASE = list(
- "I already bought enough of this for the time being."
- ),
- TRADER_LORE_PHRASE = list(
- "Hello, I am Mr. Bones!",
- "The ride never ends!",
- "I'd really like a refreshing carton of milk!",
- "I'm willing to play big prices for BONES! Need materials to make merch, eh?",
- "It's a beautiful day outside. Birds are singing, Flowers are blooming... On days like these, kids like you... Should be buying my wares!"
- ),
- TRADER_NOT_BUYING_ANYTHING = list(
- "I'm currently buying nothing at the moment."
- ),
- TRADER_NOT_SELLING_ANYTHING = list(
- "I'm currently selling nothing at the moment."
- ),
- )
-
-/mob/living/simple_animal/hostile/retaliate/trader/mrbones/initial_products()
- return list(
- /obj/item/clothing/head/helmet/skull = list(150, INFINITY),
- /obj/item/clothing/mask/bandana/skull/black = list(50, INFINITY),
- /obj/item/food/cookie/sugar/spookyskull = list(10, INFINITY),
- /obj/item/instrument/trombone/spectral = list(10000, INFINITY),
- /obj/item/shovel/serrated = list(150, INFINITY),
- )
-
-/mob/living/simple_animal/hostile/retaliate/trader/mrbones/initial_wanteds()
- return list(
- /obj/item/reagent_containers/condiment/milk = list(1000, INFINITY, ""),
- /obj/item/stack/sheet/bone = list(420, INFINITY, ", per sheet of bone"),
- )
-
-#undef ITEM_REJECTED_PHRASE
-#undef ITEM_SELLING_CANCELED_PHRASE
-#undef ITEM_SELLING_ACCEPTED_PHRASE
-#undef INTERESTED_PHRASE
-#undef BUY_PHRASE
-#undef NO_CASH_PHRASE
-#undef NO_STOCK_PHRASE
-#undef NOT_WILLING_TO_BUY_PHRASE
-#undef ITEM_IS_WORTHLESS_PHRASE
-#undef TRADER_HAS_ENOUGH_ITEM_PHRASE
-#undef TRADER_LORE_PHRASE
-#undef TRADER_NOT_BUYING_ANYTHING
-#undef TRADER_NOT_SELLING_ANYTHING
-#undef TRADER_PRODUCT_INFO_PRICE
-#undef TRADER_PRODUCT_INFO_QUANTITY
-#undef TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION
diff --git a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
deleted file mode 100644
index c536da22cd146..0000000000000
--- a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
+++ /dev/null
@@ -1,444 +0,0 @@
-/// The darkness threshold for space dragon when choosing a color
-#define DARKNESS_THRESHOLD 50
-/// Any interactions executed by the space dragon
-#define DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION "space dragon interaction"
-
-/**
- * # Space Dragon
- *
- * A space-faring leviathan-esque monster which breathes fire and summons carp. Spawned during its respective midround antagonist event.
- *
- * A space-faring monstrosity who has the ability to breathe dangerous fire breath and uses its powerful wings to knock foes away.
- * Normally spawned as an antagonist during the Space Dragon event, Space Dragon's main goal is to open three rifts from which to pull a great tide of carp onto the station.
- * Space Dragon can summon only one rift at a time, and can do so anywhere a blob is allowed to spawn. In order to trigger his victory condition, Space Dragon must summon and defend three rifts while they charge.
- * Space Dragon, when spawned, has five minutes to summon the first rift. Failing to do so will cause Space Dragon to return from whence he came.
- * When the rift spawns, ghosts can interact with it to spawn in as space carp to help complete the mission. One carp is granted when the rift is first summoned, with an extra one every 30 seconds.
- * Once the victory condition is met, all current rifts become invulnerable to damage, are allowed to spawn infinite sentient space carp, and Space Dragon gets unlimited rage.
- * Alternatively, if the shuttle arrives while Space Dragon is still active, their victory condition will automatically be met and all the rifts will immediately become fully charged.
- * If a charging rift is destroyed, Space Dragon will be incredibly slowed, and the endlag on his gust attack is greatly increased on each use.
- * Space Dragon has the following abilities to assist him with his objective:
- * - Can shoot fire in straight line, dealing 30 burn damage and setting those suseptible on fire.
- * - Can use his wings to temporarily stun and knock back any nearby mobs. This attack has no cooldown, but instead has endlag after the attack where Space Dragon cannot act. This endlag's time decreases over time, but is added to every time he uses the move.
- * - Can swallow mob corpses to heal for half their max health. Any corpses swallowed are stored within him, and will be regurgitated on death.
- * - Can tear through any type of wall. This takes 4 seconds for most walls, and 12 seconds for reinforced walls.
- */
-/mob/living/simple_animal/hostile/space_dragon
- name = "Space Dragon"
- desc = "A vile, leviathan-esque creature that flies in the most unnatural way. Looks slightly similar to a space carp."
- gender = NEUTER
- maxHealth = 400
- health = 400
- damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0.5, OXY = 1)
- combat_mode = TRUE
- speed = 0
- movement_type = FLYING
- attack_verb_continuous = "chomps"
- attack_verb_simple = "chomp"
- attack_sound = 'sound/magic/demon_attack1.ogg'
- attack_vis_effect = ATTACK_EFFECT_BITE
- death_sound = 'sound/creatures/space_dragon_roar.ogg'
- icon = 'icons/mob/nonhuman-player/spacedragon.dmi'
- icon_state = "spacedragon"
- icon_living = "spacedragon"
- icon_dead = "spacedragon_dead"
- health_doll_icon = "spacedragon"
- obj_damage = 50
- environment_smash = ENVIRONMENT_SMASH_NONE
- flags_1 = PREVENT_CONTENTS_EXPLOSION_1
- melee_damage_upper = 35
- melee_damage_lower = 35
- mob_size = MOB_SIZE_LARGE
- armour_penetration = 30
- pixel_x = -16
- base_pixel_x = -16
- maptext_height = 64
- maptext_width = 64
- turns_per_move = 5
- ranged = TRUE
- mouse_opacity = MOUSE_OPACITY_ICON
- butcher_results = list(/obj/item/stack/ore/diamond = 5, /obj/item/stack/sheet/sinew = 5, /obj/item/stack/sheet/bone = 30)
- death_message = "screeches as its wings turn to dust and it collapses on the floor, its life extinguished."
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- minbodytemp = 0
- maxbodytemp = 1500
- faction = list(FACTION_CARP)
- pressure_resistance = 200
- /// How much endlag using Wing Gust should apply. Each use of wing gust increments this, and it decreases over time.
- var/tiredness = 0
- /// A multiplier to how much each use of wing gust should add to the tiredness variable. Set to 5 if the current rift is destroyed.
- var/tiredness_mult = 1
- /// The distance Space Dragon's gust reaches
- var/gust_distance = 4
- /// The amount of tiredness to add to Space Dragon per use of gust
- var/gust_tiredness = 30
- /// Determines whether or not Space Dragon is in the middle of using wing gust. If set to true, prevents him from moving and doing certain actions.
- var/using_special = FALSE
- /// The color of the space dragon.
- var/chosen_color
- /// Minimum devastation damage dealt coefficient based on max health
- var/devastation_damage_min_percentage = 0.4
- /// Maximum devastation damage dealt coefficient based on max health
- var/devastation_damage_max_percentage = 0.75
-
-/mob/living/simple_animal/hostile/space_dragon/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/seethrough_mob)
- AddElement(/datum/element/simple_flying)
- add_traits(list(TRAIT_SPACEWALK, TRAIT_FREE_HYPERSPACE_MOVEMENT, TRAIT_NO_FLOATING_ANIM, TRAIT_HEALS_FROM_CARP_RIFTS), INNATE_TRAIT)
- AddElement(/datum/element/content_barfer)
- RegisterSignal(src, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(before_attack))
-
-/mob/living/simple_animal/hostile/space_dragon/Login()
- . = ..()
- if(!chosen_color)
- dragon_name()
- color_selection()
-
-/mob/living/simple_animal/hostile/space_dragon/ex_act_devastate()
- var/damage_coefficient = rand(devastation_damage_min_percentage, devastation_damage_max_percentage)
- adjustBruteLoss(initial(maxHealth)*damage_coefficient)
-
-/mob/living/simple_animal/hostile/space_dragon/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- . = ..()
- tiredness = max(tiredness - (0.5 * seconds_per_tick), 0)
-
-/mob/living/simple_animal/hostile/space_dragon/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
- . = ..()
- if (isliving(arrived))
- RegisterSignal(arrived, COMSIG_MOB_STATCHANGE, PROC_REF(eaten_stat_changed))
-
-/mob/living/simple_animal/hostile/space_dragon/Exited(atom/movable/gone, direction)
- . = ..()
- if (isliving(gone))
- UnregisterSignal(gone, COMSIG_MOB_STATCHANGE)
-
-/// Release consumed mobs if they transition from dead to alive
-/mob/living/simple_animal/hostile/space_dragon/proc/eaten_stat_changed(mob/living/eaten)
- SIGNAL_HANDLER
- if (eaten.stat == DEAD)
- return
- playsound(src, 'sound/effects/splat.ogg', vol = 50, vary = TRUE)
- visible_message(span_danger("[src] vomits up [eaten]!"))
- eaten.forceMove(loc)
- eaten.Paralyze(5 SECONDS)
-
-/mob/living/simple_animal/hostile/space_dragon/proc/before_attack(datum/source, atom/target)
- SIGNAL_HANDLER
- if(using_special)
- return COMPONENT_CANCEL_ATTACK_CHAIN
-
- if(target == src)
- to_chat(src, span_warning("You almost bite yourself, but then decide against it."))
- return COMPONENT_CANCEL_ATTACK_CHAIN
-
- if(DOING_INTERACTION(src, DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION)) // patience grasshopper
- target.balloon_alert(src, "finish current action first!")
- return COMPONENT_CANCEL_ATTACK_CHAIN
-
- if(ismecha(target))
- target.take_damage(50, BRUTE, MELEE, 1)
- return // don't block the rest of the attack chain
-
- if(iswallturf(target))
- INVOKE_ASYNC(src, PROC_REF(tear_down_wall), target)
- return COMPONENT_CANCEL_ATTACK_CHAIN
-
- if(isliving(target)) //Swallows corpses like a snake to regain health.
- var/mob/living/living_target = target
- if(living_target.stat != DEAD)
- return // go ham on slapping the shit out of them buddy
-
- INVOKE_ASYNC(src, PROC_REF(eat_this_corpse), living_target)
- return COMPONENT_CANCEL_ATTACK_CHAIN
-
-/// Handles tearing down a wall. Returns TRUE if successful, FALSE otherwise.
-/mob/living/simple_animal/hostile/space_dragon/proc/tear_down_wall(turf/closed/wall/wall_target)
- to_chat(src, span_warning("You begin tearing through the wall..."))
- playsound(src, 'sound/machines/airlock_alien_prying.ogg', 100, TRUE)
-
- var/time_to_tear = 4 SECONDS
- if(istype(wall_target, /turf/closed/wall/r_wall))
- time_to_tear = 12 SECONDS
-
- if(!do_after(src, time_to_tear, target = wall_target, interaction_key = DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION))
- return FALSE
-
- if(isopenturf(wall_target))
- return FALSE // well the thing was destroyed while we were sleeping so that's nice, but we didn't successfully tear it down. whatever
-
- wall_target.dismantle_wall(devastated = TRUE)
- playsound(src, 'sound/effects/meteorimpact.ogg', 100, TRUE)
- return TRUE
-
-/// Handles eating a corpse, giving us a bit of health back. Returns TRUE if we were sucessful in eating, FALSE otherwise.
-/mob/living/simple_animal/hostile/space_dragon/proc/eat_this_corpse(mob/living/corpse)
- to_chat(src, span_warning("You begin to swallow the body of [corpse] whole..."))
-
- if(!do_after(src, 3 SECONDS, target = corpse, interaction_key = DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION))
- return FALSE
- if(!eat(corpse))
- return FALSE
-
- adjustHealth(-(corpse.maxHealth * 0.25))
- return TRUE
-
-/mob/living/simple_animal/hostile/space_dragon/ranged_secondary_attack(atom/target, modifiers)
- if(using_special)
- return
- using_special = TRUE
- icon_state = "spacedragon_gust"
- add_dragon_overlay()
- useGust(0)
-
-/mob/living/simple_animal/hostile/space_dragon/Move()
- if(!using_special)
- ..()
-
-/mob/living/simple_animal/hostile/space_dragon/OpenFire()
- if(using_special)
- return
- ranged_cooldown = world.time + ranged_cooldown_time
- fire_stream()
-
-/mob/living/simple_animal/hostile/space_dragon/death(gibbed)
- . = ..()
- add_dragon_overlay()
-
-/mob/living/simple_animal/hostile/space_dragon/revive(full_heal_flags = NONE, excess_healing = 0, force_grab_ghost = FALSE)
- . = ..()
- add_dragon_overlay()
-
-/**
- * Allows space dragon to choose its own name.
- *
- * Prompts the space dragon to choose a name, which it will then apply to itself.
- * If the name is invalid, will re-prompt the dragon until a proper name is chosen.
- */
-/mob/living/simple_animal/hostile/space_dragon/proc/dragon_name()
- var/chosen_name = sanitize_name(reject_bad_text(tgui_input_text(src, "What would you like your name to be?", "Choose Your Name", real_name, MAX_NAME_LEN)))
- if(!chosen_name)
- to_chat(src, span_warning("Not a valid name, please try again."))
- dragon_name()
- return
- to_chat(src, span_notice("Your name is now [span_name("[chosen_name]")], the feared Space Dragon."))
- fully_replace_character_name(null, chosen_name)
-
-/**
- * Allows space dragon to choose a color for itself.
- *
- * Prompts the space dragon to choose a color, from which it will then apply to itself.
- * If an invalid color is given, will re-prompt the dragon until a proper color is chosen.
- */
-/mob/living/simple_animal/hostile/space_dragon/proc/color_selection()
- chosen_color = input(src,"What would you like your color to be?","Choose Your Color", COLOR_WHITE) as color|null
- if(!chosen_color) //redo proc until we get a color
- to_chat(src, span_warning("Not a valid color, please try again."))
- color_selection()
- return
- var/temp_hsv = RGBtoHSV(chosen_color)
- if(ReadHSV(temp_hsv)[3] < DARKNESS_THRESHOLD)
- to_chat(src, span_danger("Invalid color. Your color is not bright enough."))
- color_selection()
- return
- add_atom_colour(chosen_color, FIXED_COLOUR_PRIORITY)
- add_dragon_overlay()
-
-/**
- * Adds the proper overlay to the space dragon.
- *
- * Clears the current overlay on space dragon and adds a proper one for whatever animation he's in.
- */
-/mob/living/simple_animal/hostile/space_dragon/proc/add_dragon_overlay()
- cut_overlays()
- if(stat == DEAD)
- var/mutable_appearance/overlay = mutable_appearance(icon, "overlay_dead")
- overlay.appearance_flags = RESET_COLOR
- add_overlay(overlay)
- return
- if(!using_special)
- var/mutable_appearance/overlay = mutable_appearance(icon, "overlay_base")
- overlay.appearance_flags = RESET_COLOR
- add_overlay(overlay)
- return
- if(using_special)
- var/mutable_appearance/overlay = mutable_appearance(icon, "overlay_gust")
- overlay.appearance_flags = RESET_COLOR
- add_overlay(overlay)
-
-/**
- * Determines a line of turfs from sources's position to the target with length range.
- *
- * Determines a line of turfs from the source's position to the target with length range.
- * The line will extend on past the target if the range is large enough, and not reach the target if range is small enough.
- * Arguments:
- * * offset - whether or not to aim slightly to the left or right of the target
- * * range - how many turfs should we go out for
- * * atom/at - The target
- */
-/mob/living/simple_animal/hostile/space_dragon/proc/line_target(offset, range, atom/at = target)
- if(!at)
- return
- var/angle = ATAN2(at.x - src.x, at.y - src.y) + offset
- var/turf/T = get_turf(src)
- for(var/i in 1 to range)
- var/turf/check = locate(src.x + cos(angle) * i, src.y + sin(angle) * i, src.z)
- if(!check)
- break
- T = check
- return (get_line(src, T) - get_turf(src))
-
-/**
- * Spawns fire at each position in a line from the source to the target.
- *
- * Spawns fire at each position in a line from the source to the target.
- * Stops if it comes into contact with a solid wall, a window, or a door.
- * Delays the spawning of each fire by 1.5 deciseconds.
- * Arguments:
- * * atom/at - The target
- */
-/mob/living/simple_animal/hostile/space_dragon/proc/fire_stream(atom/at = target)
- playsound(get_turf(src),'sound/magic/fireball.ogg', 200, TRUE)
- var/range = 20
- var/list/turfs = list()
- var/list/hit_list_parameter = list(src)
- turfs = line_target(0, range, at)
- var/delayFire = -1.0
- for(var/turf/T in turfs)
- if(isclosedturf(T))
- return
- for(var/obj/structure/window/W in T.contents)
- return
- for(var/obj/machinery/door/D in T.contents)
- if(D.density)
- return
- delayFire += 1.0
- addtimer(CALLBACK(src, PROC_REF(dragon_fire_line), T, hit_list_parameter), delayFire)
-
-/**
- * What occurs on each tile to actually create the fire.
- *
- * Creates a fire on the given turf.
- * It creates a hotspot on the given turf, damages any living mob with 30 burn damage, and damages mechs by 50.
- * It can only hit any given target once.
- * Arguments:
- * * turf/T - The turf to trigger the effects on.
- * * list/hit_list - The list of targets that have already been hit in the fire_stream.
- */
-/mob/living/simple_animal/hostile/space_dragon/proc/dragon_fire_line(turf/fire_turf, list/hit_list)
- new /obj/effect/hotspot(fire_turf)
- fire_turf.hotspot_expose(700,50,1)
- for(var/mob/living/living_target in fire_turf.contents)
- if(living_target.faction_check_mob(src) && living_target != src)
- hit_list += living_target
- start_carp_speedboost(living_target)
- if(living_target in hit_list)
- continue
- if(living_target.mind?.has_antag_datum(/datum/antagonist/space_carp))
- continue
- hit_list += living_target
- living_target.adjustFireLoss(30)
- to_chat(living_target, span_userdanger("You're hit by [src]'s fire breath!"))
- // deals damage to mechs
- for(var/obj/vehicle/sealed/mecha/mech_target in fire_turf.contents)
- if(mech_target in hit_list)
- continue
- hit_list += mech_target
- mech_target.take_damage(50, BRUTE, MELEE, 1)
-
-/**
- * Applies the speed boost to carps when hit by space dragon's flame breath
- *
- * Applies the dragon rage effect to carps temporarily, giving them a glow and a speed boost.
- * This lasts for 8 seconds.
- * Arguments:
- * * mob/living/target - The carp being affected.
- */
-/mob/living/simple_animal/hostile/space_dragon/proc/start_carp_speedboost(mob/living/target)
- target.add_filter("anger_glow", 3, list("type" = "outline", "color" = "#ff330030", "size" = 2))
- target.add_movespeed_modifier(/datum/movespeed_modifier/dragon_rage)
- addtimer(CALLBACK(src, PROC_REF(end_carp_speedboost), target), 8 SECONDS)
-
-/**
- * Remove the speed boost from carps when hit by space dragon's flame breath
- *
- * Removes the dragon rage effect from carps, removing their glow and speed boost.
- * Arguments:
- * * mob/living/target - The carp being affected.
- */
-/mob/living/simple_animal/hostile/space_dragon/proc/end_carp_speedboost(mob/living/target)
- target.remove_filter("anger_glow")
- target.remove_movespeed_modifier(/datum/movespeed_modifier/dragon_rage)
-
-/**
- * Handles consuming and storing consumed things inside Space Dragon
- *
- * Plays a sound and then stores the consumed thing inside Space Dragon.
- * Used in AttackingTarget(), paired with a heal should it succeed.
- * Arguments:
- * * atom/movable/A - The thing being consumed
- */
-/mob/living/simple_animal/hostile/space_dragon/proc/eat(atom/movable/A)
- if(A && A.loc != src)
- playsound(src, 'sound/magic/demon_attack1.ogg', 60, TRUE)
- visible_message(span_warning("[src] swallows [A] whole!"))
- to_chat(src, span_notice("Your acids cleanse the flames off [A] on the way down. Delicious!"))
- A.extinguish()
- A.forceMove(src)
- return TRUE
- return FALSE
-
-/**
- * Resets Space Dragon's status after using wing gust.
- *
- * Resets Space Dragon's status after using wing gust.
- * If it isn't dead by the time it calls this method, reset the sprite back to the normal living sprite.
- * Also sets the using_special variable to FALSE, allowing Space Dragon to move and attack freely again.
- */
-/mob/living/simple_animal/hostile/space_dragon/proc/reset_status()
- if(stat != DEAD)
- icon_state = "spacedragon"
- using_special = FALSE
- add_dragon_overlay()
-
-/**
- * Handles wing gust from the windup all the way to the endlag at the end.
- *
- * Handles the wing gust attack from start to finish, based on the timer.
- * When intially triggered, starts at 0. Until the timer reaches 10, increase Space Dragon's y position by 2 and call back to the function in 1.5 deciseconds.
- * When the timer is at 10, trigger the attack. Change Space Dragon's sprite. reset his y position, and push all living creatures back in a 3 tile radius and stun them for 5 seconds.
- * Stay in the ending state for how much our tiredness dictates and add to our tiredness.
- * Arguments:
- * * timer - The timer used for the windup.
- */
-/mob/living/simple_animal/hostile/space_dragon/proc/useGust(timer)
- if(timer != 10)
- pixel_y = pixel_y + 2;
- addtimer(CALLBACK(src, PROC_REF(useGust), timer + 1), 1.2)
- return
- pixel_y = 0
- icon_state = "spacedragon_gust_2"
- cut_overlays()
- var/mutable_appearance/overlay = mutable_appearance(icon, "overlay_gust_2")
- overlay.appearance_flags = RESET_COLOR
- add_overlay(overlay)
- playsound(src, 'sound/effects/gravhit.ogg', 100, TRUE)
- for (var/mob/living/candidate in view(gust_distance, src))
- if(candidate == src || candidate.faction_check_mob(src))
- continue
- visible_message(span_boldwarning("[candidate] is knocked back by the gust!"))
- to_chat(candidate, span_userdanger("You're knocked back by the gust!"))
- var/dir_to_target = get_dir(get_turf(src), get_turf(candidate))
- var/throwtarget = get_edge_target_turf(target, dir_to_target)
- candidate.safe_throw_at(throwtarget, 10, 1, src)
- candidate.Paralyze(50)
- addtimer(CALLBACK(src, PROC_REF(reset_status)), 4 + ((tiredness * tiredness_mult) / 10))
- tiredness = tiredness + (gust_tiredness * tiredness_mult)
-
-/mob/living/simple_animal/hostile/space_dragon/spawn_with_antag
-
-/mob/living/simple_animal/hostile/space_dragon/spawn_with_antag/mind_initialize()
- . = ..()
- mind.add_antag_datum(/datum/antagonist/space_dragon)
-
-#undef DARKNESS_THRESHOLD
-#undef DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION
diff --git a/code/modules/mob/living/simple_animal/hostile/vatbeast.dm b/code/modules/mob/living/simple_animal/hostile/vatbeast.dm
index 7c6edeb88da4a..a0fe73b32bfe0 100644
--- a/code/modules/mob/living/simple_animal/hostile/vatbeast.dm
+++ b/code/modules/mob/living/simple_animal/hostile/vatbeast.dm
@@ -28,8 +28,7 @@
/mob/living/simple_animal/hostile/vatbeast/Initialize(mapload)
. = ..()
- var/datum/action/cooldown/tentacle_slap/slapper = new(src)
- slapper.Grant(src)
+ GRANT_ACTION(/datum/action/cooldown/tentacle_slap)
add_cell_sample()
AddComponent(/datum/component/tameable, list(/obj/item/food/fries, /obj/item/food/cheesyfries, /obj/item/food/cornchips, /obj/item/food/carrotfries), tame_chance = 30, bonus_tame_chance = 0, after_tame = CALLBACK(src, PROC_REF(tamed)))
diff --git a/code/modules/mob/living/simple_animal/hostile/wizard.dm b/code/modules/mob/living/simple_animal/hostile/wizard.dm
deleted file mode 100644
index d2957effd3cf9..0000000000000
--- a/code/modules/mob/living/simple_animal/hostile/wizard.dm
+++ /dev/null
@@ -1,83 +0,0 @@
-/mob/living/simple_animal/hostile/wizard
- name = "Space Wizard"
- desc = "EI NATH?"
- icon = 'icons/mob/simple/simple_human.dmi'
- icon_state = "wizard"
- icon_living = "wizard"
- icon_dead = "wizard_dead"
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- sentience_type = SENTIENCE_HUMANOID
- speak_chance = 0
- turns_per_move = 3
- speed = 0
- maxHealth = 100
- health = 100
- harm_intent_damage = 5
- melee_damage_lower = 5
- melee_damage_upper = 5
- attack_verb_continuous = "punches"
- attack_verb_simple = "punch"
- attack_sound = 'sound/weapons/punch1.ogg'
- combat_mode = TRUE
- atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
- unsuitable_atmos_damage = 7.5
- faction = list(ROLE_WIZARD)
- status_flags = CANPUSH
- footstep_type = FOOTSTEP_MOB_SHOE
-
- retreat_distance = 3 //out of fireball range
- minimum_distance = 3
- del_on_death = 1
- loot = list(
- /obj/effect/mob_spawn/corpse/human/wizard,
- /obj/item/staff,
- )
-
- var/next_cast = 0
- var/datum/action/cooldown/spell/pointed/projectile/fireball/fireball
- var/datum/action/cooldown/spell/teleport/radius_turf/blink/blink
- var/datum/action/cooldown/spell/aoe/magic_missile/magic_missile
-
-/mob/living/simple_animal/hostile/wizard/Initialize(mapload)
- . = ..()
- apply_dynamic_human_appearance(src, mob_spawn_path = /obj/effect/mob_spawn/corpse/human/wizard, r_hand = /obj/item/staff)
- var/obj/item/implant/exile/exiled = new /obj/item/implant/exile(src)
- exiled.implant(src)
-
- fireball = new(src)
- fireball.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND)
- fireball.Grant(src)
-
- magic_missile = new(src)
- magic_missile.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND)
- magic_missile.Grant(src)
-
- blink = new(src)
- blink.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND)
- blink.outer_tele_radius = 3
- blink.Grant(src)
-
-/mob/living/simple_animal/hostile/wizard/Destroy()
- QDEL_NULL(fireball)
- QDEL_NULL(magic_missile)
- QDEL_NULL(blink)
- return ..()
-
-/mob/living/simple_animal/hostile/wizard/handle_automated_action()
- . = ..()
- if(target && next_cast < world.time)
- if((get_dir(src, target) in list(SOUTH, EAST, WEST, NORTH)) && fireball.can_cast_spell(feedback = FALSE))
- setDir(get_dir(src, target))
- fireball.Trigger(null, target)
- next_cast = world.time + 1 SECONDS
- return
-
- if(magic_missile.IsAvailable())
- magic_missile.Trigger(null, target)
- next_cast = world.time + 1 SECONDS
- return
-
- if(blink.IsAvailable()) // Spam Blink when you can
- blink.Trigger(null, src)
- next_cast = world.time + 1 SECONDS
- return
diff --git a/code/modules/mob/living/simple_animal/hostile/zombie.dm b/code/modules/mob/living/simple_animal/hostile/zombie.dm
index b525c502e4fe6..f16449764de7d 100644
--- a/code/modules/mob/living/simple_animal/hostile/zombie.dm
+++ b/code/modules/mob/living/simple_animal/hostile/zombie.dm
@@ -31,7 +31,7 @@
. = ..()
apply_dynamic_human_appearance(src, outfit, /datum/species/zombie, bloody_slots = ITEM_SLOT_OCLOTHING)
-/mob/living/simple_animal/hostile/zombie/AttackingTarget()
+/mob/living/simple_animal/hostile/zombie/AttackingTarget(atom/attacked_target)
. = ..()
if(. && ishuman(target) && prob(infection_chance))
try_to_zombie_infect(target)
diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm
index 75950478edc6b..a1f12082ff1b7 100644
--- a/code/modules/mob/living/simple_animal/parrot.dm
+++ b/code/modules/mob/living/simple_animal/parrot.dm
@@ -324,7 +324,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
return
/mob/living/simple_animal/parrot/attack_paw(mob/living/carbon/human/user, list/modifiers)
- return attack_hand(modifiers)
+ return attack_hand(user, modifiers)
/mob/living/simple_animal/parrot/attack_alien(mob/living/carbon/alien/user, list/modifiers)
return attack_hand(user, modifiers)
diff --git a/code/modules/mob/living/simple_animal/shade.dm b/code/modules/mob/living/simple_animal/shade.dm
deleted file mode 100644
index dbb795e91c569..0000000000000
--- a/code/modules/mob/living/simple_animal/shade.dm
+++ /dev/null
@@ -1,71 +0,0 @@
-/mob/living/simple_animal/shade
- name = "Shade"
- real_name = "Shade"
- desc = "A bound spirit."
- gender = PLURAL
- icon = 'icons/mob/nonhuman-player/cult.dmi'
- icon_state = "shade_cult"
- icon_living = "shade_cult"
- mob_biotypes = MOB_SPIRIT
- maxHealth = 40
- health = 40
- speak_emote = list("hisses")
- emote_hear = list("wails.","screeches.")
- response_help_continuous = "puts their hand through"
- response_help_simple = "put your hand through"
- response_disarm_continuous = "flails at"
- response_disarm_simple = "flail at"
- response_harm_continuous = "punches"
- response_harm_simple = "punch"
- speak_chance = 1
- melee_damage_lower = 5
- melee_damage_upper = 12
- attack_verb_continuous = "metaphysically strikes"
- attack_verb_simple = "metaphysically strike"
- minbodytemp = 0
- maxbodytemp = INFINITY
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- speed = -1 //they don't have to lug a body made of runed metal around
- stop_automated_movement = 1
- faction = list(FACTION_CULT)
- status_flags = CANPUSH
- loot = list(/obj/item/ectoplasm)
- del_on_death = TRUE
- initial_language_holder = /datum/language_holder/construct
-
-/mob/living/simple_animal/shade/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/simple_flying)
- add_traits(list(TRAIT_HEALS_FROM_CULT_PYLONS, TRAIT_SPACEWALK, TRAIT_VENTCRAWLER_ALWAYS), INNATE_TRAIT)
-
-/mob/living/simple_animal/shade/death()
- if(death_message == initial(death_message))
- death_message = "lets out a contented sigh as [p_their()] form unwinds."
- ..()
-
-/mob/living/simple_animal/shade/can_suicide()
- if(istype(loc, /obj/item/soulstone)) //do not suicide inside the soulstone
- return FALSE
- return ..()
-
-/mob/living/simple_animal/shade/attack_animal(mob/living/simple_animal/user, list/modifiers)
- if(isconstruct(user))
- var/mob/living/simple_animal/hostile/construct/doll = user
- if(!doll.can_repair)
- return
- if(health < maxHealth)
- adjustHealth(-25)
- Beam(user,icon_state="sendbeam", time = 4)
- user.visible_message(span_danger("[user] heals \the [src]."), \
- span_cult("You heal [src], leaving [src] at [health]/[maxHealth] health."))
- else
- to_chat(user, span_cult("You cannot heal [src], as [p_theyre()] unharmed!"))
- else if(src != user)
- return ..()
-
-/mob/living/simple_animal/shade/attackby(obj/item/item, mob/user, params) //Marker -Agouri
- if(istype(item, /obj/item/soulstone))
- var/obj/item/soulstone/stone = item
- stone.capture_shade(src, user)
- else
- . = ..()
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index 85c146b29c827..4c382a59b2f64 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -509,7 +509,7 @@
else if(!is_child && M.gender == MALE && !(M.flags_1 & HOLOGRAM_1)) //Better safe than sorry ;_;
partner = M
- else if(isliving(M) && !faction_check_mob(M)) //shyness check. we're not shy in front of things that share a faction with us.
+ else if(isliving(M) && !faction_check_atom(M)) //shyness check. we're not shy in front of things that share a faction with us.
return //we never mate when not alone, so just abort early
if(alone && partner && children < 3)
diff --git a/code/modules/mob/living/simple_animal/slime/powers.dm b/code/modules/mob/living/simple_animal/slime/powers.dm
index 01197efc8edc9..a485b8342ca31 100644
--- a/code/modules/mob/living/simple_animal/slime/powers.dm
+++ b/code/modules/mob/living/simple_animal/slime/powers.dm
@@ -64,6 +64,10 @@
if(issilicon(meal) || meal.mob_biotypes & MOB_ROBOTIC)
return FALSE
+ if(meal.flags_1 & HOLOGRAM_1)
+ meal.balloon_alert(src, "no life energy!")
+ return FALSE
+
if(isanimal(meal))
var/mob/living/simple_animal/simple_meal = meal
if(simple_meal.damage_coeff[TOX] <= 0 && simple_meal.damage_coeff[BRUTE] <= 0) //The creature wouldn't take any damage, it must be too weird even for us.
@@ -160,8 +164,7 @@
amount_grown = 0
for(var/datum/action/innate/slime/evolve/E in actions)
E.Remove(src)
- var/datum/action/innate/slime/reproduce/reproduce_action = new
- reproduce_action.Grant(src)
+ GRANT_ACTION(/datum/action/innate/slime/reproduce)
regenerate_icons()
update_name()
else
@@ -180,54 +183,62 @@
/mob/living/simple_animal/slime/verb/Reproduce()
set category = "Slime"
- set desc = "This will make you split into four Slimes."
+ set desc = "This will make you split into four slimes."
- if(stat)
- to_chat(src, "I must be conscious to do this...")
+ if(stat != CONSCIOUS)
+ balloon_alert(src, "need to be conscious to split!")
return
- if(is_adult)
- if(amount_grown >= SLIME_EVOLUTION_THRESHOLD)
- if(stat)
- to_chat(src, "I must be conscious to do this...")
- return
-
- var/list/babies = list()
- var/new_nutrition = round(nutrition * 0.9)
- var/new_powerlevel = round(powerlevel / 4)
- var/turf/drop_loc = drop_location()
-
- for(var/i in 1 to 4)
- var/child_colour
- if(mutation_chance >= 100)
- child_colour = SLIME_TYPE_RAINBOW
- else if(prob(mutation_chance))
- child_colour = slime_mutation[rand(1,4)]
- else
- child_colour = colour
- var/mob/living/simple_animal/slime/M
- M = new(drop_loc, child_colour)
- if(ckey)
- M.set_nutrition(new_nutrition) //Player slimes are more robust at spliting. Once an oversight of poor copypasta, now a feature!
- M.powerlevel = new_powerlevel
- if(i != 1)
- step_away(M,src)
- M.set_friends(Friends)
- babies += M
- M.mutation_chance = clamp(mutation_chance+(rand(5,-5)),0,100)
- SSblackbox.record_feedback("tally", "slime_babies_born", 1, M.colour)
-
- var/mob/living/simple_animal/slime/new_slime = pick(babies)
- new_slime.set_combat_mode(TRUE)
- if(src.mind)
- src.mind.transfer_to(new_slime)
- else
- new_slime.key = src.key
- qdel(src)
+ if(!isopenturf(loc))
+ balloon_alert(src, "can't reproduce here!")
+
+ if(!is_adult)
+ balloon_alert(src, "not old enough to reproduce!")
+ return
+
+ if(amount_grown < SLIME_EVOLUTION_THRESHOLD)
+ to_chat(src, "I need to grow myself more before I can reproduce...")
+ return
+
+ var/list/babies = list()
+ var/new_nutrition = round(nutrition * 0.9)
+ var/new_powerlevel = round(powerlevel / 4)
+ var/turf/drop_loc = drop_location()
+
+ for(var/i in 1 to 4)
+ var/child_colour
+
+ if(mutation_chance >= 100)
+ child_colour = SLIME_TYPE_RAINBOW
+ else if(prob(mutation_chance))
+ child_colour = slime_mutation[rand(1,4)]
else
- to_chat(src, "I am not ready to reproduce yet...")
+ child_colour = colour
+
+ var/mob/living/simple_animal/slime/baby
+ baby = new(drop_loc, child_colour)
+
+ if(ckey)
+ baby.set_nutrition(new_nutrition) //Player slimes are more robust at spliting. Once an oversight of poor copypasta, now a feature!
+
+ baby.powerlevel = new_powerlevel
+ if(i != 1)
+ step_away(baby, src)
+
+ baby.set_friends(Friends)
+ babies += baby
+ baby.mutation_chance = clamp(mutation_chance+(rand(5,-5)),0,100)
+ SSblackbox.record_feedback("tally", "slime_babies_born", 1, baby.colour)
+
+ var/mob/living/simple_animal/slime/new_slime = pick(babies) // slime that the OG slime will move into.
+ new_slime.set_combat_mode(TRUE)
+
+ if(isnull(src.mind))
+ new_slime.key = src.key
else
- to_chat(src, "I am not old enough to reproduce yet...")
+ src.mind.transfer_to(new_slime)
+
+ qdel(src)
/datum/action/innate/slime/reproduce
name = "Reproduce"
@@ -235,8 +246,8 @@
needs_growth = GROWTH_NEEDED
/datum/action/innate/slime/reproduce/Activate()
- var/mob/living/simple_animal/slime/S = owner
- S.Reproduce()
+ var/mob/living/simple_animal/slime/slime_owner = owner
+ slime_owner.Reproduce()
#undef SIZE_DOESNT_MATTER
#undef BABIES_ONLY
diff --git a/code/modules/mob/living/simple_animal/slime/slime.dm b/code/modules/mob/living/simple_animal/slime/slime.dm
index 2bfe51051d387..57b4577866b73 100644
--- a/code/modules/mob/living/simple_animal/slime/slime.dm
+++ b/code/modules/mob/living/simple_animal/slime/slime.dm
@@ -298,15 +298,6 @@
amount = -abs(amount)
return ..() //Heals them
-/mob/living/simple_animal/slime/bullet_act(obj/projectile/Proj, def_zone, piercing_hit = FALSE)
- attacked += 10
- if((Proj.damage_type == BURN))
- adjustBruteLoss(-abs(Proj.damage)) //fire projectiles heals slimes.
- Proj.on_hit(src, 0, piercing_hit)
- else
- . = ..(Proj)
- . = . || BULLET_ACT_BLOCK
-
/mob/living/simple_animal/slime/emp_act(severity)
. = ..()
if(. & EMP_PROTECT_SELF)
diff --git a/code/modules/mob/living/status_procs.dm b/code/modules/mob/living/status_procs.dm
index bbc37911bb17f..96beb024fe922 100644
--- a/code/modules/mob/living/status_procs.dm
+++ b/code/modules/mob/living/status_procs.dm
@@ -497,18 +497,28 @@
/mob/living/proc/cure_husk(source)
REMOVE_TRAIT(src, TRAIT_HUSK, source)
- if(!HAS_TRAIT(src, TRAIT_HUSK))
- REMOVE_TRAIT(src, TRAIT_DISFIGURED, "husk")
- update_body()
- return TRUE
+ if(HAS_TRAIT(src, TRAIT_HUSK))
+ return FALSE
+ REMOVE_TRAIT(src, TRAIT_DISFIGURED, "husk")
+ update_body()
+ UnregisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_UNHUSKABLE))
+ return TRUE
/mob/living/proc/become_husk(source)
- if(!HAS_TRAIT(src, TRAIT_HUSK))
- ADD_TRAIT(src, TRAIT_HUSK, source)
- ADD_TRAIT(src, TRAIT_DISFIGURED, "husk")
- update_body()
- else
- ADD_TRAIT(src, TRAIT_HUSK, source)
+ if(HAS_TRAIT(src, TRAIT_UNHUSKABLE))
+ return
+ var/was_husk = HAS_TRAIT(src, TRAIT_HUSK)
+ ADD_TRAIT(src, TRAIT_HUSK, source)
+ if (was_husk)
+ return
+ ADD_TRAIT(src, TRAIT_DISFIGURED, "husk")
+ update_body()
+ RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_UNHUSKABLE), PROC_REF(became_unhuskable))
+
+/// Called when we become unhuskable while already husked
+/mob/living/proc/became_unhuskable()
+ SIGNAL_HANDLER
+ cure_husk()
/mob/living/proc/cure_fakedeath(source)
remove_traits(list(TRAIT_FAKEDEATH, TRAIT_DEATHCOMA), source)
diff --git a/code/modules/mob/living/taste.dm b/code/modules/mob/living/taste.dm
index 8f414a2e603e0..f0bc01eb84cfd 100644
--- a/code/modules/mob/living/taste.dm
+++ b/code/modules/mob/living/taste.dm
@@ -85,6 +85,14 @@
return TOXIC
return tongue.toxic_foodtypes
+/**
+ * Gets food this mob is allergic to
+ * Essentially toxic food+, not only disgusting but outright lethal
+ */
+/mob/living/proc/get_allergic_foodtypes()
+ var/datum/quirk/item_quirk/food_allergic/allergy = get_quirk(/datum/quirk/item_quirk/food_allergic)
+ return allergy?.target_foodtypes || NONE
+
/**
* Gets the food reaction a mob would normally have from the given food item,
* assuming that no check_liked callback was used in the edible component.
diff --git a/code/modules/mob/living/ventcrawling.dm b/code/modules/mob/living/ventcrawling.dm
index 4335a0c4857b5..1a732c41a4dcd 100644
--- a/code/modules/mob/living/ventcrawling.dm
+++ b/code/modules/mob/living/ventcrawling.dm
@@ -61,7 +61,10 @@
return
if(!do_after(src, 1 SECONDS, target = ventcrawl_target))
return
- visible_message(span_notice("[src] scrambles out from the ventilation ducts!"),span_notice("You scramble out from the ventilation ducts."))
+ if(ventcrawl_target.welded) // in case it got welded during our sleep
+ to_chat(src, span_warning("You can't crawl around a welded vent!"))
+ return
+ visible_message(span_notice("[src] scrambles out from the ventilation ducts!"), span_notice("You scramble out from the ventilation ducts."))
forceMove(ventcrawl_target.loc)
REMOVE_TRAIT(src, TRAIT_MOVE_VENTCRAWLING, VENTCRAWLING_TRAIT)
update_pipe_vision()
@@ -76,8 +79,11 @@
return
if(has_client && isnull(client))
return
+ if(ventcrawl_target.welded) // in case it got welded during our sleep
+ to_chat(src, span_warning("You can't crawl around a welded vent!"))
+ return
ventcrawl_target.flick_overlay_static(image('icons/effects/vent_indicator.dmi', "insert", ABOVE_MOB_LAYER), 1 SECONDS)
- visible_message(span_notice("[src] scrambles into the ventilation ducts!"),span_notice("You climb into the ventilation ducts."))
+ visible_message(span_notice("[src] scrambles into the ventilation ducts!"), span_notice("You climb into the ventilation ducts."))
move_into_vent(ventcrawl_target)
else
to_chat(src, span_warning("This ventilation duct is not connected to anything!"))
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index cb6469d16a836..f4e525904821b 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -28,7 +28,7 @@
if(client)
stack_trace("Mob with client has been deleted.")
else if(ckey)
- stack_trace("Mob without client but with associated ckey has been deleted.")
+ stack_trace("Mob without client but with associated ckey, [ckey], has been deleted.")
remove_from_mob_list()
remove_from_dead_mob_list()
@@ -46,7 +46,7 @@
qdel(hud_used)
QDEL_LIST(client_colours)
- ghostize() //False, since we're deleting it currently
+ ghostize(can_reenter_corpse = FALSE) //False, since we're deleting it currently
if(mind?.current == src) //Let's just be safe yeah? This will occasionally be cleared, but not always. Can't do it with ghostize without changing behavior
mind.set_current(null)
@@ -1155,21 +1155,7 @@
///Can this mob use storage
/mob/proc/canUseStorage()
return FALSE
-/**
- * Check if the other mob has any factions the same as us
- *
- * If exact match is set, then all our factions must match exactly
- */
-/mob/proc/faction_check_mob(mob/target, exact_match)
- if(exact_match) //if we need an exact match, we need to do some bullfuckery.
- var/list/faction_src = faction.Copy()
- var/list/faction_target = target.faction.Copy()
- if(!("[REF(src)]" in faction_target)) //if they don't have our ref faction, remove it from our factions list.
- faction_src -= "[REF(src)]" //if we don't do this, we'll never have an exact match.
- if(!("[REF(target)]" in faction_src))
- faction_target -= "[REF(target)]" //same thing here.
- return faction_check(faction_src, faction_target, TRUE)
- return faction_check(faction, target.faction, FALSE)
+
/*
* Compare two lists of factions, returning true if any match
*
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index 12905700318f3..6808728a833b7 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -20,6 +20,8 @@
// we never want to hide a turf because it's not lit
// We can rely on the lighting plane to handle that for us
see_in_dark = 1e6
+ // A list of factions that this mob is currently in, for hostile mob targetting, amongst other things
+ faction = list(FACTION_NEUTRAL)
/// The current client inhabiting this mob. Managed by login/logout
/// This exists so we can do cleanup in logout for occasions where a client was transfere rather then destroyed
/// We need to do this because the mob on logout never actually has a reference to client
@@ -144,9 +146,6 @@
/// What job does this mob have
var/job = null//Living
- /// A list of factions that this mob is currently in, for hostile mob targetting, amongst other things
- var/list/faction = list(FACTION_NEUTRAL)
-
/// Can this mob enter shuttles
var/move_on_shuttle = 1
@@ -215,3 +214,6 @@
var/active_thinking_indicator
/// User is thinking in character. Used to revert to thinking state after stop_typing
var/thinking_IC = FALSE
+
+ /// Whether invisimin is enabled on this mob
+ var/invisimin = FALSE
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index c80a9504b36d5..6b89495887682 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -84,7 +84,6 @@
CRASH("limbs is empty and the chest is blacklisted. this may not be intended!")
return (((chest_blacklisted && !base_zone) || even_weights) ? pick_weight(limbs) : ran_zone(base_zone, base_probability, limbs))
-
///Would this zone be above the neck
/proc/above_neck(zone)
var/list/zones = list(BODY_ZONE_HEAD, BODY_ZONE_PRECISE_MOUTH, BODY_ZONE_PRECISE_EYES)
@@ -252,68 +251,66 @@
* * source The source of the notification
* * alert_overlay The alert overlay to show in the alert message
* * action What action to take upon the ghost interacting with the notification, defaults to NOTIFY_JUMP
- * * flashwindow Flash the byond client window
* * ignore_key Ignore keys if they're in the GLOB.poll_ignore list
* * header The header of the notifiaction
- * * notify_suiciders If it should notify suiciders (who do not qualify for many ghost roles)
* * notify_volume How loud the sound should be to spook the user
*/
-/proc/notify_ghosts(message, ghost_sound, enter_link, atom/source, mutable_appearance/alert_overlay, action = NOTIFY_JUMP, flashwindow = TRUE, ignore_mapload = TRUE, ignore_key, header, notify_suiciders = TRUE, notify_volume = 100) //Easy notification of ghosts.
-
- if(ignore_mapload && SSatoms.initialized != INITIALIZATION_INNEW_REGULAR) //don't notify for objects created during a map load
+/proc/notify_ghosts(
+ message,
+ ghost_sound,
+ enter_link,
+ atom/source,
+ mutable_appearance/alert_overlay,
+ action = NOTIFY_JUMP,
+ notify_flags = NOTIFY_CATEGORY_DEFAULT,
+ ignore_key,
+ header = "",
+ notify_volume = 100
+)
+
+ if(notify_flags & GHOST_NOTIFY_IGNORE_MAPLOAD && SSatoms.initialized != INITIALIZATION_INNEW_REGULAR) //don't notify for objects created during a map load
return
+
+ if(source)
+ if(isnull(alert_overlay))
+ alert_overlay = get_small_overlay(source)
+
+ alert_overlay.appearance_flags |= TILE_BOUND
+ alert_overlay.layer = FLOAT_LAYER
+ alert_overlay.plane = FLOAT_PLANE
+
for(var/mob/dead/observer/ghost in GLOB.player_list)
- if(!notify_suiciders && HAS_TRAIT(ghost, TRAIT_SUICIDED))
+ if(!(notify_flags & GHOST_NOTIFY_NOTIFY_SUICIDERS) && HAS_TRAIT(ghost, TRAIT_SUICIDED))
continue
if(ignore_key && (ghost.ckey in GLOB.poll_ignore[ignore_key]))
continue
- var/orbit_link
- if(source && action == NOTIFY_ORBIT)
- orbit_link = " (Orbit)"
- to_chat(ghost, span_ghostalert("[message][(enter_link) ? " [enter_link]" : ""][orbit_link]"))
+
+ if(notify_flags & GHOST_NOTIFY_FLASH_WINDOW)
+ window_flash(ghost.client)
+
if(ghost_sound)
SEND_SOUND(ghost, sound(ghost_sound, volume = notify_volume))
- if(flashwindow)
- window_flash(ghost.client)
- if(!source)
- continue
- var/atom/movable/screen/alert/notify_action/alert = ghost.throw_alert("[REF(source)]_notify_action", /atom/movable/screen/alert/notify_action)
- if(!alert)
+
+ if(isnull(source))
+ to_chat(ghost, span_ghostalert(message))
continue
- var/ui_style = ghost.client?.prefs?.read_preference(/datum/preference/choiced/ui_style)
- if(ui_style)
- alert.icon = ui_style2icon(ui_style)
- if (header)
- alert.name = header
- alert.desc = message
- alert.action = action
- alert.target = source
- if(!alert_overlay)
- alert_overlay = new(source)
- alert_overlay.pixel_x = 0
- alert_overlay.pixel_y = 0
- var/icon/size_check = icon(source.icon, source.icon_state)
- var/scale = 1
- var/width = size_check.Width()
- var/height = size_check.Height()
- if(width > world.icon_size)
- alert_overlay.pixel_x = -(world.icon_size / 2) * ((width - world.icon_size) / world.icon_size)
- if(height > world.icon_size)
- alert_overlay.pixel_y = -(world.icon_size / 2) * ((height - world.icon_size) / world.icon_size)
- if(width > world.icon_size || height > world.icon_size)
- if(width >= height)
- scale = world.icon_size / width
- else
- scale = world.icon_size / height
- alert_overlay.transform = alert_overlay.transform.Scale(scale)
- alert_overlay.appearance_flags |= TILE_BOUND
- alert_overlay.layer = FLOAT_LAYER
- alert_overlay.plane = FLOAT_PLANE
- alert.add_overlay(alert_overlay)
-/**
- * Heal a robotic body part on a mob
- */
+ var/custom_link = enter_link ? " [enter_link]" : ""
+ var/link = " ([capitalize(action)])"
+
+ to_chat(ghost, span_ghostalert("[message][custom_link][link]"))
+
+ var/atom/movable/screen/alert/notify_action/toast = ghost.throw_alert(
+ category = "[REF(source)]_notify_action",
+ type = /atom/movable/screen/alert/notify_action,
+ )
+ toast.action = action
+ toast.add_overlay(alert_overlay)
+ toast.desc = "[message] -- Click to [action]."
+ toast.name = header
+ toast.target = source
+
+/// Heals a robotic limb on a mob
/proc/item_heal_robotic(mob/living/carbon/human/human, mob/user, brute_heal, burn_heal)
var/obj/item/bodypart/affecting = human.get_bodypart(check_zone(user.zone_selected))
if(!affecting || IS_ORGANIC_LIMB(affecting))
@@ -567,3 +564,21 @@
raw_lines += recent_speech[key]
return raw_lines
+
+/// Takes in an associated list (key `/datum/action` typepaths, value is the AI blackboard key) and handles granting the action and adding it to the mob's AI controller blackboard.
+/// This is only useful in instances where you don't want to store the reference to the action on a variable on the mob.
+/// You can set the value to null if you don't want to add it to the blackboard (like in player controlled instances). Is also safe with null AI controllers.
+/// Assumes that the action will be initialized and held in the mob itself, which is typically standard.
+/mob/proc/grant_actions_by_list(list/input)
+ if(length(input) <= 0)
+ return
+
+ for(var/action in input)
+ var/datum/action/ability = new action(src)
+ ability.Grant(src)
+
+ var/blackboard_key = input[action]
+ if(isnull(blackboard_key))
+ continue
+
+ ai_controller?.set_blackboard_key(blackboard_key, ability)
diff --git a/code/modules/mob/mob_transformation_simple.dm b/code/modules/mob/mob_transformation_simple.dm
index fe901b3ad9e2a..6d2a8a9850dc2 100644
--- a/code/modules/mob/mob_transformation_simple.dm
+++ b/code/modules/mob/mob_transformation_simple.dm
@@ -22,6 +22,9 @@
to_chat(usr, span_danger("Cannot convert into a new_player mob type."))
return
+ if (SEND_SIGNAL(src, COMSIG_PRE_MOB_CHANGED_TYPE) & COMPONENT_BLOCK_MOB_CHANGE)
+ return
+
return change_mob_type_unchecked(new_type, location, new_name, delete_old_mob)
/// Version of [change_mob_type] that does no usr prompting (may send an error message though). Satisfies procs with the SHOULD_NOT_SLEEP restriction
@@ -32,7 +35,7 @@
else
desired_mob = new new_type(src.loc)
- if(!desired_mob || !ismob(desired_mob))
+ if(!ismob(desired_mob))
to_chat(usr, "Type path is not a mob (new_type = [new_type]) in change_mob_type(). Contact a coder.")
qdel(desired_mob)
return
diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm
index 290177f5baf50..790f9312ea170 100644
--- a/code/modules/mob/transform_procs.dm
+++ b/code/modules/mob/transform_procs.dm
@@ -20,9 +20,11 @@
Paralyze(TRANSFORMATION_DURATION, ignore_canstun = TRUE)
icon = null
cut_overlays()
- invisibility = INVISIBILITY_MAXIMUM
- new /obj/effect/temp_visual/monkeyify(loc)
+ var/obj/effect = new /obj/effect/temp_visual/monkeyify(loc)
+ effect.SetInvisibility(invisibility)
+ SetInvisibility(INVISIBILITY_MAXIMUM, id=type)
+
transformation_timer = addtimer(CALLBACK(src, PROC_REF(finish_monkeyize)), TRANSFORMATION_DURATION, TIMER_UNIQUE)
/mob/living/carbon/proc/finish_monkeyize()
@@ -30,7 +32,7 @@
to_chat(src, span_boldnotice("You are now a monkey."))
REMOVE_TRAIT(src, TRAIT_NO_TRANSFORM, TEMPORARY_TRANSFORMATION_TRAIT)
icon = initial(icon)
- invisibility = 0
+ RemoveInvisibility(type)
set_species(/datum/species/monkey)
name = "monkey"
set_name()
@@ -57,9 +59,11 @@
Paralyze(TRANSFORMATION_DURATION, ignore_canstun = TRUE)
icon = null
cut_overlays()
- invisibility = INVISIBILITY_MAXIMUM
- new /obj/effect/temp_visual/monkeyify/humanify(loc)
+ var/obj/effect = new /obj/effect/temp_visual/monkeyify/humanify(loc)
+ effect.SetInvisibility(invisibility)
+ SetInvisibility(INVISIBILITY_MAXIMUM, id=type)
+
transformation_timer = addtimer(CALLBACK(src, PROC_REF(finish_humanize), species), TRANSFORMATION_DURATION, TIMER_UNIQUE)
/mob/living/carbon/proc/finish_humanize(species = /datum/species/human)
@@ -67,7 +71,7 @@
to_chat(src, span_boldnotice("You are now a human."))
REMOVE_TRAIT(src, TRAIT_NO_TRANSFORM, TEMPORARY_TRANSFORMATION_TRAIT)
icon = initial(icon)
- invisibility = 0
+ RemoveInvisibility(type)
set_species(species)
SEND_SIGNAL(src, COMSIG_MONKEY_HUMANIZE)
return src
@@ -117,7 +121,7 @@
dropItemToGround(W)
regenerate_icons()
icon = null
- invisibility = INVISIBILITY_MAXIMUM
+ SetInvisibility(INVISIBILITY_MAXIMUM)
return ..()
/mob/living/carbon/human/AIize(client/preference_source, transfer_after = TRUE)
@@ -135,7 +139,7 @@
var/mob/living/silicon/robot/new_borg = new /mob/living/silicon/robot(loc)
new_borg.gender = gender
- new_borg.invisibility = 0
+ new_borg.SetInvisibility(INVISIBILITY_NONE)
if(client)
new_borg.updatename(client)
@@ -176,7 +180,7 @@
dropItemToGround(W)
regenerate_icons()
icon = null
- invisibility = INVISIBILITY_MAXIMUM
+ SetInvisibility(INVISIBILITY_MAXIMUM)
REMOVE_TRAIT(src, TRAIT_NO_TRANSFORM, TEMPORARY_TRANSFORMATION_TRAIT)
return ..()
@@ -201,7 +205,7 @@
dropItemToGround(W)
regenerate_icons()
icon = null
- invisibility = INVISIBILITY_MAXIMUM
+ SetInvisibility(INVISIBILITY_MAXIMUM)
for(var/t in bodyparts)
qdel(t)
@@ -231,7 +235,7 @@
dropItemToGround(W)
regenerate_icons()
icon = null
- invisibility = INVISIBILITY_MAXIMUM
+ SetInvisibility(INVISIBILITY_MAXIMUM)
for(var/t in bodyparts)
qdel(t)
@@ -270,7 +274,7 @@
dropItemToGround(W)
regenerate_icons()
icon = null
- invisibility = INVISIBILITY_MAXIMUM
+ SetInvisibility(INVISIBILITY_MAXIMUM)
for(var/t in bodyparts) //this really should not be necessary
qdel(t)
@@ -297,7 +301,7 @@
regenerate_icons()
icon = null
- invisibility = INVISIBILITY_MAXIMUM
+ SetInvisibility(INVISIBILITY_MAXIMUM)
var/mob/living/basic/gorilla/new_gorilla = new (get_turf(src))
new_gorilla.set_combat_mode(TRUE)
if(mind)
@@ -328,7 +332,7 @@
regenerate_icons()
icon = null
- invisibility = INVISIBILITY_MAXIMUM
+ SetInvisibility(INVISIBILITY_MAXIMUM)
for(var/t in bodyparts)
qdel(t)
@@ -372,7 +376,7 @@
if(!MP)
return FALSE //Sanity, this should never happen.
- if(ispath(MP, /mob/living/simple_animal/hostile/construct) || ispath(MP, /mob/living/basic/construct))
+ if(ispath(MP, /mob/living/basic/construct))
return FALSE //Verbs do not appear for players.
//Good mobs!
@@ -386,7 +390,7 @@
return TRUE
if(ispath(MP, /mob/living/basic/mushroom))
return TRUE
- if(ispath(MP, /mob/living/simple_animal/shade))
+ if(ispath(MP, /mob/living/basic/shade))
return TRUE
if(ispath(MP, /mob/living/basic/killer_tomato))
return TRUE
diff --git a/code/modules/mob_spawn/corpses/mob_corpses.dm b/code/modules/mob_spawn/corpses/mob_corpses.dm
index 476c3f70a8491..f83dc13f1eded 100644
--- a/code/modules/mob_spawn/corpses/mob_corpses.dm
+++ b/code/modules/mob_spawn/corpses/mob_corpses.dm
@@ -221,6 +221,21 @@
facial_haircolor = COLOR_WHITE
skin_tone = "caucasian1"
+/obj/effect/mob_spawn/corpse/human/wizard/red
+ outfit = /datum/outfit/wizardcorpse/red
+
+/obj/effect/mob_spawn/corpse/human/wizard/yellow
+ outfit = /datum/outfit/wizardcorpse/yellow
+
+/obj/effect/mob_spawn/corpse/human/wizard/black
+ outfit = /datum/outfit/wizardcorpse/black
+
+/obj/effect/mob_spawn/corpse/human/wizard/marisa
+ outfit = /datum/outfit/wizardcorpse/marisa
+
+/obj/effect/mob_spawn/corpse/human/wizard/tape
+ outfit = /datum/outfit/wizardcorpse/tape
+
/datum/outfit/wizardcorpse
name = "Space Wizard Corpse"
uniform = /obj/item/clothing/under/color/lightpurple
@@ -228,6 +243,27 @@
shoes = /obj/item/clothing/shoes/sandal/magic
head = /obj/item/clothing/head/wizard
+/datum/outfit/wizardcorpse/red
+ suit = /obj/item/clothing/suit/wizrobe/red
+ head = /obj/item/clothing/head/wizard/red
+
+/datum/outfit/wizardcorpse/yellow
+ suit = /obj/item/clothing/suit/wizrobe/yellow
+ head = /obj/item/clothing/head/wizard/yellow
+
+/datum/outfit/wizardcorpse/black
+ suit = /obj/item/clothing/suit/wizrobe/black
+ head = /obj/item/clothing/head/wizard/black
+
+/datum/outfit/wizardcorpse/marisa
+ suit = /obj/item/clothing/suit/wizrobe/marisa
+ head = /obj/item/clothing/head/wizard/marisa
+ shoes = /obj/item/clothing/shoes/sneakers/marisa
+
+/datum/outfit/wizardcorpse/tape
+ suit = /obj/item/clothing/suit/wizrobe/tape
+ head = /obj/item/clothing/head/wizard/tape
+
/obj/effect/mob_spawn/corpse/human/wizard/dark
name = "Dark Wizard Corpse"
outfit = /datum/outfit/wizardcorpse/dark
diff --git a/code/modules/mob_spawn/ghost_roles/golem_roles.dm b/code/modules/mob_spawn/ghost_roles/golem_roles.dm
index c86a895823860..1fa23698e4a01 100644
--- a/code/modules/mob_spawn/ghost_roles/golem_roles.dm
+++ b/code/modules/mob_spawn/ghost_roles/golem_roles.dm
@@ -23,7 +23,13 @@
. = ..()
var/area/init_area = get_area(src)
if(!mapload && init_area)
- notify_ghosts("\A golem shell has been completed in \the [init_area.name].", source = src, action=NOTIFY_ATTACK, flashwindow = FALSE, ignore_key = POLL_IGNORE_GOLEM)
+ notify_ghosts(
+ "\A golem shell has been completed in \the [init_area.name].",
+ source = src,
+ action = NOTIFY_PLAY,
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ ignore_key = POLL_IGNORE_GOLEM,
+ )
/obj/effect/mob_spawn/ghost_role/human/golem/name_mob(mob/living/spawned_mob, forced_name)
if(forced_name || !iscarbon(spawned_mob))
diff --git a/code/modules/mob_spawn/ghost_roles/mining_roles.dm b/code/modules/mob_spawn/ghost_roles/mining_roles.dm
index 66837628cdcf4..d322fc4c3669b 100644
--- a/code/modules/mob_spawn/ghost_roles/mining_roles.dm
+++ b/code/modules/mob_spawn/ghost_roles/mining_roles.dm
@@ -253,7 +253,13 @@
eggshell.egg = src
src.forceMove(eggshell)
if(spawner_area)
- notify_ghosts("An ash walker egg is ready to hatch in \the [spawner_area.name].", source = src, action=NOTIFY_ATTACK, flashwindow = FALSE, ignore_key = POLL_IGNORE_ASHWALKER)
+ notify_ghosts(
+ "An ash walker egg is ready to hatch in \the [spawner_area.name].",
+ source = src,
+ action = NOTIFY_PLAY,
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ ignore_key = POLL_IGNORE_ASHWALKER,
+ )
/datum/outfit/ashwalker
name = "Ash Walker"
diff --git a/code/modules/mob_spawn/ghost_roles/space_roles.dm b/code/modules/mob_spawn/ghost_roles/space_roles.dm
index 7b4dba395c1ec..764d20c9a76c1 100644
--- a/code/modules/mob_spawn/ghost_roles/space_roles.dm
+++ b/code/modules/mob_spawn/ghost_roles/space_roles.dm
@@ -97,7 +97,7 @@
/obj/effect/mob_spawn/ghost_role/human/lavaland_syndicate/comms/space/Initialize(mapload)
. = ..()
if(prob(85)) //only has a 15% chance of existing, otherwise it'll just be a NPC syndie.
- new /mob/living/basic/syndicate/ranged(get_turf(src))
+ new /mob/living/basic/trooper/syndicate/ranged(get_turf(src))
return INITIALIZE_HINT_QDEL
///battlecruiser stuff
diff --git a/code/modules/mob_spawn/ghost_roles/spider_roles.dm b/code/modules/mob_spawn/ghost_roles/spider_roles.dm
index fb3d470f5aa80..4dada2a2468a7 100644
--- a/code/modules/mob_spawn/ghost_roles/spider_roles.dm
+++ b/code/modules/mob_spawn/ghost_roles/spider_roles.dm
@@ -36,6 +36,10 @@
if(100 to INFINITY)
. += span_info("These eggs are plump, teeming with life. Any moment now...")
+/obj/structure/spider/eggcluster/abnormal
+ name = "abnormal egg cluster"
+ color = rgb(0, 148, 211)
+
/obj/structure/spider/eggcluster/enriched
name = "enriched egg cluster"
color = rgb(148, 0, 211)
@@ -103,7 +107,16 @@
amount_grown += rand(5, 15) * seconds_per_tick
if(amount_grown >= 100 && !ready)
ready = TRUE
- notify_ghosts("[src] is ready to hatch!", null, enter_link = "(Click to play)", source = src, action = NOTIFY_ORBIT, ignore_key = POLL_IGNORE_SPIDER, flashwindow = flash_window)
+ var/notify_flags_to_pass = NOTIFY_CATEGORY_NOFLASH
+ if(flash_window)
+ notify_flags_to_pass &= GHOST_NOTIFY_FLASH_WINDOW
+ notify_ghosts(
+ "[src] is ready to hatch!",
+ source = src,
+ action = NOTIFY_PLAY,
+ ignore_key = POLL_IGNORE_SPIDER,
+ notify_flags = notify_flags_to_pass,
+ )
STOP_PROCESSING(SSobj, src)
/obj/effect/mob_spawn/ghost_role/spider/Topic(href, href_list)
@@ -134,6 +147,16 @@
var/datum/antagonist/spider/spider_antag = new granted_datum(directive)
spawned_mob.mind.add_antag_datum(spider_antag)
+/obj/effect/mob_spawn/ghost_role/spider/abnormal
+ name = "abnormal egg cluster"
+ color = rgb(0, 148, 211)
+ cluster_type = /obj/structure/spider/eggcluster/abnormal
+ potentialspawns = list(
+ /mob/living/basic/spider/growing/spiderling/tank,
+ /mob/living/basic/spider/growing/spiderling/breacher,
+ )
+ flash_window = TRUE
+
/obj/effect/mob_spawn/ghost_role/spider/enriched
name = "enriched egg cluster"
color = rgb(148, 0, 211)
@@ -168,7 +191,7 @@
directive = "Ensure the survival of the spider species and overtake whatever structure you find yourself in."
cluster_type = /obj/structure/spider/eggcluster/midwife
potentialspawns = list(
- /mob/living/basic/spider/giant/midwife, // We don't want the event to end instantly because of a 2 hp spiderling dying
+ /mob/living/basic/spider/growing/spiderling/midwife, // We don't want the event to end instantly because broodmothers got a bad spawn
)
flash_window = TRUE
diff --git a/code/modules/mob_spawn/ghost_roles/venus_human_trap.dm b/code/modules/mob_spawn/ghost_roles/venus_human_trap.dm
index 8ab475dce015d..1a1fd623e0d19 100644
--- a/code/modules/mob_spawn/ghost_roles/venus_human_trap.dm
+++ b/code/modules/mob_spawn/ghost_roles/venus_human_trap.dm
@@ -33,17 +33,12 @@
/// Called when the attached flower bud has borne fruit (ie. is ready)
/obj/effect/mob_spawn/ghost_role/venus_human_trap/proc/bear_fruit()
ready = TRUE
- notify_ghosts("[src] has borne fruit!", null, enter_link = "(Click to play)", source = src, action = NOTIFY_ATTACK, ignore_key = POLL_IGNORE_VENUSHUMANTRAP)
-
-/obj/effect/mob_spawn/ghost_role/venus_human_trap/Topic(href, href_list)
- . = ..()
- if(.)
- return
- if(href_list["activate"])
- var/mob/dead/observer/ghost = usr
- if(istype(ghost))
- ghost.ManualFollow(src)
- attack_ghost(ghost)
+ notify_ghosts(
+ "[src] has borne fruit!",
+ source = src,
+ action = NOTIFY_PLAY,
+ ignore_key = POLL_IGNORE_VENUSHUMANTRAP,
+ )
/obj/effect/mob_spawn/ghost_role/venus_human_trap/allow_spawn(mob/user, silent = FALSE)
. = ..()
diff --git a/code/modules/mob_spawn/mob_spawn.dm b/code/modules/mob_spawn/mob_spawn.dm
index 3bfff7855f03c..086254aae3881 100644
--- a/code/modules/mob_spawn/mob_spawn.dm
+++ b/code/modules/mob_spawn/mob_spawn.dm
@@ -11,8 +11,6 @@
var/mob_name
///the type of the mob, you best inherit this
var/mob_type = /mob/living/basic/cockroach
- ///Lazy string list of factions that the spawned mob will be in upon spawn
- var/list/faction
////Human specific stuff. Don't set these if you aren't using a human, the unit tests will put a stop to your sinful hand.
diff --git a/code/modules/mod/mod_core.dm b/code/modules/mod/mod_core.dm
index 45fa61677b2e1..d5c43497982a9 100644
--- a/code/modules/mod/mod_core.dm
+++ b/code/modules/mod/mod_core.dm
@@ -399,7 +399,7 @@
SIGNAL_HANDLER
if(mod.active)
particle_effect = new(mod.wearer, /particles/pollen, PARTICLE_ATTACH_MOB)
- mob_spawner = mod.wearer.AddComponent(/datum/component/spawner, spawn_types=list(spawned_mob_type), spawn_time=5 SECONDS, max_spawned=3)
+ mob_spawner = mod.wearer.AddComponent(/datum/component/spawner, spawn_types=list(spawned_mob_type), spawn_time=5 SECONDS, max_spawned=3, faction=mod.wearer.faction)
RegisterSignal(mob_spawner, COMSIG_SPAWNER_SPAWNED, PROC_REF(new_mob))
RegisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED, PROC_REF(spread_flowers))
diff --git a/code/modules/mod/mod_paint.dm b/code/modules/mod/mod_paint.dm
index ee162b2d9819a..351859e5f2bf9 100644
--- a/code/modules/mod/mod_paint.dm
+++ b/code/modules/mod/mod_paint.dm
@@ -144,8 +144,9 @@
balloon_alert(user, "no alternate skins!")
return
var/list/skins = list()
- for(var/mod_skin in mod.theme.skins)
- skins[mod_skin] = image(icon = mod.icon, icon_state = "[mod_skin]-control")
+ for(var/mod_skin_name in mod.theme.skins)
+ var/list/mod_skin = mod.theme.skins[mod_skin_name]
+ skins[mod_skin_name] = image(icon = mod_skin[MOD_ICON_OVERRIDE] || mod.icon, icon_state = "[mod_skin_name]-control")
var/pick = show_radial_menu(user, mod, skins, custom_check = CALLBACK(src, PROC_REF(check_menu), mod, user), require_near = TRUE)
if(!pick)
balloon_alert(user, "no skin picked!")
diff --git a/code/modules/mod/mod_theme.dm b/code/modules/mod/mod_theme.dm
index 716860db2dee6..5e579723f787e 100644
--- a/code/modules/mod/mod_theme.dm
+++ b/code/modules/mod/mod_theme.dm
@@ -702,8 +702,6 @@
slowdown_inactive = 1
slowdown_active = 0.5
allowed_suit_storage = list(
- /obj/item/ammo_box,
- /obj/item/ammo_casing,
/obj/item/reagent_containers/spray/pepper,
/obj/item/restraints/handcuffs,
/obj/item/assembly/flash,
@@ -765,8 +763,6 @@
slowdown_inactive = 0.75
slowdown_active = 0.25
allowed_suit_storage = list(
- /obj/item/ammo_box,
- /obj/item/ammo_casing,
/obj/item/reagent_containers/spray/pepper,
/obj/item/restraints/handcuffs,
/obj/item/assembly/flash,
@@ -832,8 +828,6 @@
slowdown_inactive = 0.75
slowdown_active = 0.25
allowed_suit_storage = list(
- /obj/item/ammo_box,
- /obj/item/ammo_casing,
/obj/item/restraints/handcuffs,
/obj/item/assembly/flash,
/obj/item/melee/baton,
@@ -958,8 +952,6 @@
resistance_flags = FIRE_PROOF
inbuilt_modules = list(/obj/item/mod/module/armor_booster)
allowed_suit_storage = list(
- /obj/item/ammo_box,
- /obj/item/ammo_casing,
/obj/item/restraints/handcuffs,
/obj/item/assembly/flash,
/obj/item/melee/baton,
@@ -1051,8 +1043,6 @@
ui_theme = "syndicate"
inbuilt_modules = list(/obj/item/mod/module/armor_booster)
allowed_suit_storage = list(
- /obj/item/ammo_box,
- /obj/item/ammo_casing,
/obj/item/restraints/handcuffs,
/obj/item/assembly/flash,
/obj/item/melee/baton,
@@ -1120,8 +1110,6 @@
slot_flags = ITEM_SLOT_BELT
inbuilt_modules = list(/obj/item/mod/module/infiltrator, /obj/item/mod/module/storage/belt, /obj/item/mod/module/demoralizer)
allowed_suit_storage = list(
- /obj/item/ammo_box,
- /obj/item/ammo_casing,
/obj/item/restraints/handcuffs,
/obj/item/assembly/flash,
/obj/item/melee/baton,
@@ -1185,8 +1173,6 @@
slowdown_active = -0.5
inbuilt_modules = list(/obj/item/mod/module/quick_carry/advanced, /obj/item/mod/module/organ_thrower)
allowed_suit_storage = list(
- /obj/item/ammo_box,
- /obj/item/ammo_casing,
/obj/item/assembly/flash,
/obj/item/healthanalyzer,
/obj/item/melee/baton,
@@ -1329,8 +1315,6 @@
inbuilt_modules = list(/obj/item/mod/module/welding/camera_vision, /obj/item/mod/module/hacker, /obj/item/mod/module/weapon_recall, /obj/item/mod/module/adrenaline_boost, /obj/item/mod/module/energy_net)
allowed_suit_storage = list(
/obj/item/gun,
- /obj/item/ammo_box,
- /obj/item/ammo_casing,
/obj/item/melee/baton,
/obj/item/restraints/handcuffs,
)
@@ -1454,8 +1438,6 @@
slowdown_inactive = 0.5
slowdown_active = 0
allowed_suit_storage = list(
- /obj/item/ammo_box,
- /obj/item/ammo_casing,
/obj/item/restraints/handcuffs,
/obj/item/assembly/flash,
/obj/item/melee/baton,
@@ -1540,8 +1522,6 @@
siemens_coefficient = 0
complexity_max = DEFAULT_MAX_COMPLEXITY + 10
allowed_suit_storage = list(
- /obj/item/ammo_box,
- /obj/item/ammo_casing,
/obj/item/restraints/handcuffs,
/obj/item/assembly/flash,
/obj/item/melee/baton,
@@ -1604,8 +1584,6 @@
slowdown_inactive = 0.5
slowdown_active = 0
allowed_suit_storage = list(
- /obj/item/ammo_box,
- /obj/item/ammo_casing,
/obj/item/restraints/handcuffs,
/obj/item/assembly/flash,
/obj/item/melee/baton,
diff --git a/code/modules/mod/mod_types.dm b/code/modules/mod/mod_types.dm
index f8daa9bab544f..3e4b89e6a52cc 100644
--- a/code/modules/mod/mod_types.dm
+++ b/code/modules/mod/mod_types.dm
@@ -216,6 +216,7 @@
/obj/item/mod/module/pathfinder,
/obj/item/mod/module/flashlight,
/obj/item/mod/module/dna_lock,
+ /obj/item/mod/module/hat_stabilizer/syndicate,
)
default_pins = list(
/obj/item/mod/module/armor_booster,
@@ -234,6 +235,7 @@
/obj/item/mod/module/jump_jet,
/obj/item/mod/module/flashlight,
/obj/item/mod/module/dna_lock,
+ /obj/item/mod/module/hat_stabilizer/syndicate,
)
default_pins = list(
/obj/item/mod/module/armor_booster,
@@ -253,6 +255,7 @@
/obj/item/mod/module/jetpack/advanced,
/obj/item/mod/module/jump_jet,
/obj/item/mod/module/flashlight,
+ /obj/item/mod/module/hat_stabilizer/syndicate,
)
default_pins = list(
/obj/item/mod/module/armor_booster,
@@ -287,6 +290,7 @@
/obj/item/mod/module/jetpack/advanced,
/obj/item/mod/module/jump_jet,
/obj/item/mod/module/flashlight,
+ /obj/item/mod/module/hat_stabilizer/syndicate,
)
default_pins = list(
/obj/item/mod/module/armor_booster,
@@ -303,6 +307,7 @@
/obj/item/mod/module/jetpack/advanced,
/obj/item/mod/module/jump_jet,
/obj/item/mod/module/flashlight,
+ /obj/item/mod/module/hat_stabilizer/syndicate,
/obj/item/mod/module/flamethrower,
)
default_pins = list(
@@ -321,6 +326,7 @@
/obj/item/mod/module/magnetic_harness,
/obj/item/mod/module/quick_carry,
/obj/item/mod/module/visor/diaghud,
+ /obj/item/mod/module/hat_stabilizer/syndicate,
)
@@ -335,7 +341,8 @@
/obj/item/mod/module/injector,
/obj/item/mod/module/surgical_processor/preloaded,
/obj/item/mod/module/storage/syndicate,
- /obj/item/mod/module/tether
+ /obj/item/mod/module/hat_stabilizer/syndicate,
+ /obj/item/mod/module/tether,
)
/obj/item/mod/control/pre_equipped/enchanted
diff --git a/code/modules/mod/modules/_module.dm b/code/modules/mod/modules/_module.dm
index 070d8127c5e38..c47b3e6bd1e63 100644
--- a/code/modules/mod/modules/_module.dm
+++ b/code/modules/mod/modules/_module.dm
@@ -336,6 +336,8 @@
var/list/accepted_anomalies = list(/obj/item/assembly/signaler/anomaly)
/// If this one starts with a core in.
var/prebuilt = FALSE
+ /// If the core is removable once socketed.
+ var/core_removable = TRUE
/obj/item/mod/module/anomaly_locked/Initialize(mapload)
. = ..()
@@ -354,13 +356,15 @@
if(!length(accepted_anomalies))
return
if(core)
- . += span_notice("There is a [core.name] installed in it. You could remove it with a screwdriver...")
+ . += span_notice("There is a [core.name] installed in it. [core_removable ? "You could remove it with a screwdriver..." : "Unfortunately, due to a design quirk, it's unremovable."]")
else
var/list/core_list = list()
for(var/path in accepted_anomalies)
var/atom/core_path = path
core_list += initial(core_path.name)
. += span_notice("You need to insert \a [english_list(core_list, and_text = " or ")] for this module to function.")
+ if(!core_removable)
+ . += span_notice("Due to some design quirk, once a core is inserted, it won't be removable.")
/obj/item/mod/module/anomaly_locked/on_select()
if(!core)
@@ -397,6 +401,9 @@
if(!core)
balloon_alert(user, "no core!")
return
+ if(!core_removable)
+ balloon_alert(user, "can't remove core!")
+ return
balloon_alert(user, "removing core...")
if(!do_after(user, 3 SECONDS, target = src))
balloon_alert(user, "interrupted!")
diff --git a/code/modules/mod/modules/module_kinesis.dm b/code/modules/mod/modules/module_kinesis.dm
index ff56a2329c697..0cd1387107188 100644
--- a/code/modules/mod/modules/module_kinesis.dm
+++ b/code/modules/mod/modules/module_kinesis.dm
@@ -252,12 +252,16 @@
/obj/item/mod/module/anomaly_locked/kinesis/prebuilt
prebuilt = TRUE
+/obj/item/mod/module/anomaly_locked/kinesis/prebuilt/locked
+ core_removable = FALSE
+
/obj/item/mod/module/anomaly_locked/kinesis/prototype
name = "MOD prototype kinesis module"
prebuilt = TRUE
complexity = 0
use_power_cost = DEFAULT_CHARGE_DRAIN * 5
removable = FALSE
+ core_removable = FALSE
/obj/item/mod/module/anomaly_locked/kinesis/plus
name = "MOD kinesis+ module"
diff --git a/code/modules/mod/modules/modules_engineering.dm b/code/modules/mod/modules/modules_engineering.dm
index 9c1f5e65d78a5..9095bbd2c2467 100644
--- a/code/modules/mod/modules/modules_engineering.dm
+++ b/code/modules/mod/modules/modules_engineering.dm
@@ -126,7 +126,7 @@
line = firer.Beam(src, "line", 'icons/obj/clothing/modsuit/mod_modules.dmi', emissive = FALSE)
return ..()
-/obj/projectile/tether/on_hit(atom/target)
+/obj/projectile/tether/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(firer)
firer.throw_at(target, 10, 1, firer, FALSE, FALSE, null, MOVE_FORCE_NORMAL, TRUE)
diff --git a/code/modules/mod/modules/modules_general.dm b/code/modules/mod/modules/modules_general.dm
index 6d47a5b60ba3b..58857b7730c44 100644
--- a/code/modules/mod/modules/modules_general.dm
+++ b/code/modules/mod/modules/modules_general.dm
@@ -751,6 +751,15 @@
return
mod.core.add_charge(power_per_step)
+/obj/item/mod/module/hat_stabilizer/syndicate
+ name = "MOD elite hat stabilizer module"
+ desc = "A simple set of deployable stands, directly atop one's head; \
+ these will deploy under a hat to keep it from falling off, allowing them to be worn atop the sealed helmet. \
+ You still need to take the hat off your head while the helmet deploys, though. This is a must-have for \
+ Syndicate Operatives and Agents alike, enabling them to continue to style on the opposition even while in their MODsuit."
+ complexity = 0
+ removable = FALSE
+
/// Module that shoves garbage inside its material container when the user crosses it, and eject the recycled material with MMB.
/obj/item/mod/module/recycler
name = "MOD recycler module"
diff --git a/code/modules/mod/modules/modules_medical.dm b/code/modules/mod/modules/modules_medical.dm
index ca662d01ef20b..c1a1cbce403d2 100644
--- a/code/modules/mod/modules/modules_medical.dm
+++ b/code/modules/mod/modules/modules_medical.dm
@@ -186,7 +186,7 @@
organ = null
return ..()
-/obj/projectile/organ/on_hit(atom/target)
+/obj/projectile/organ/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(!ishuman(target))
organ.forceMove(drop_location())
diff --git a/code/modules/mod/modules/modules_ninja.dm b/code/modules/mod/modules/modules_ninja.dm
index 7a38238594df3..427226071994f 100644
--- a/code/modules/mod/modules/modules_ninja.dm
+++ b/code/modules/mod/modules/modules_ninja.dm
@@ -24,7 +24,7 @@
return
if(bumpoff)
RegisterSignal(mod.wearer, COMSIG_LIVING_MOB_BUMP, PROC_REF(unstealth))
- RegisterSignal(mod.wearer, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, PROC_REF(on_unarmed_attack))
+ RegisterSignal(mod.wearer, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_unarmed_attack))
RegisterSignal(mod.wearer, COMSIG_ATOM_BULLET_ACT, PROC_REF(on_bullet_act))
RegisterSignals(mod.wearer, list(COMSIG_MOB_ITEM_ATTACK, COMSIG_ATOM_ATTACKBY, COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_HITBY, COMSIG_ATOM_HULK_ATTACK, COMSIG_ATOM_ATTACK_PAW, COMSIG_CARBON_CUFF_ATTEMPTED), PROC_REF(unstealth))
animate(mod.wearer, alpha = stealth_alpha, time = 1.5 SECONDS)
@@ -36,7 +36,7 @@
return
if(bumpoff)
UnregisterSignal(mod.wearer, COMSIG_LIVING_MOB_BUMP)
- UnregisterSignal(mod.wearer, list(COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_MOB_ITEM_ATTACK, COMSIG_ATOM_ATTACKBY, COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_BULLET_ACT, COMSIG_ATOM_HITBY, COMSIG_ATOM_HULK_ATTACK, COMSIG_ATOM_ATTACK_PAW, COMSIG_CARBON_CUFF_ATTEMPTED))
+ UnregisterSignal(mod.wearer, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_MOB_ITEM_ATTACK, COMSIG_ATOM_ATTACKBY, COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_BULLET_ACT, COMSIG_ATOM_HITBY, COMSIG_ATOM_HULK_ATTACK, COMSIG_ATOM_ATTACK_PAW, COMSIG_CARBON_CUFF_ATTEMPTED))
animate(mod.wearer, alpha = 255, time = 1.5 SECONDS)
/obj/item/mod/module/stealth/proc/unstealth(datum/source)
@@ -143,10 +143,10 @@
var/door_hack_counter = 0
/obj/item/mod/module/hacker/on_suit_activation()
- RegisterSignal(mod.wearer, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, PROC_REF(hack))
+ RegisterSignal(mod.wearer, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(hack))
/obj/item/mod/module/hacker/on_suit_deactivation(deleting = FALSE)
- UnregisterSignal(mod.wearer, COMSIG_HUMAN_EARLY_UNARMED_ATTACK)
+ UnregisterSignal(mod.wearer, COMSIG_LIVING_UNARMED_ATTACK)
/obj/item/mod/module/hacker/proc/hack(mob/living/carbon/human/source, atom/target, proximity, modifiers)
SIGNAL_HANDLER
@@ -374,7 +374,7 @@
line = firer.Beam(src, "net_beam", 'icons/obj/clothing/modsuit/mod_modules.dmi')
return ..()
-/obj/projectile/energy_net/on_hit(mob/living/target)
+/obj/projectile/energy_net/on_hit(mob/living/target, blocked = 0, pierce_hit)
. = ..()
if(!istype(target))
return
diff --git a/code/modules/mod/modules/modules_science.dm b/code/modules/mod/modules/modules_science.dm
index af0d43a12755c..c8d3cef027ae6 100644
--- a/code/modules/mod/modules/modules_science.dm
+++ b/code/modules/mod/modules/modules_science.dm
@@ -90,6 +90,9 @@
/obj/item/mod/module/anomaly_locked/antigrav/prebuilt
prebuilt = TRUE
+/obj/item/mod/module/anomaly_locked/antigrav/prebuilt/locked
+ core_removable = FALSE
+
///Teleporter - Lets the user teleport to a nearby location.
/obj/item/mod/module/anomaly_locked/teleporter
name = "MOD teleporter module"
@@ -128,3 +131,6 @@
/obj/item/mod/module/anomaly_locked/teleporter/prebuilt
prebuilt = TRUE
+
+/obj/item/mod/module/anomaly_locked/teleporter/prebuilt/locked
+ core_removable = FALSE
diff --git a/code/modules/mod/modules/modules_security.dm b/code/modules/mod/modules/modules_security.dm
index 4fe9c5767b3da..2044c3c428ab6 100644
--- a/code/modules/mod/modules/modules_security.dm
+++ b/code/modules/mod/modules/modules_security.dm
@@ -422,8 +422,10 @@
UnregisterSignal(creature, COMSIG_MOVABLE_MOVED)
return
- if(oldgroup != newgroup)
- sorted_creatures[oldgroup] -= creature
+ if(oldgroup == newgroup)
+ return
+
+ sorted_creatures[oldgroup] -= creature
sorted_creatures[newgroup] += creature
keyed_creatures[creature] = newgroup
diff --git a/code/modules/mod/modules/modules_timeline.dm b/code/modules/mod/modules/modules_timeline.dm
index f2440ba3833fa..7adf7b13cd931 100644
--- a/code/modules/mod/modules/modules_timeline.dm
+++ b/code/modules/mod/modules/modules_timeline.dm
@@ -297,11 +297,14 @@
///Reference to the tem... given by the tem! weakref because back in the day we didn't know about harddels- or maybe we didn't care.
var/datum/weakref/tem_weakref
-/obj/projectile/energy/chrono_beam/on_hit(atom/target)
+/obj/projectile/energy/chrono_beam/on_hit(atom/target, blocked = 0, pierce_hit)
var/obj/item/mod/module/tem/tem = tem_weakref.resolve()
if(target && tem && isliving(target))
var/obj/structure/chrono_field/field = new(target.loc, target, tem)
tem.field_connect(field)
+ return BULLET_ACT_HIT
+
+ return ..()
/obj/structure/chrono_field
name = "eradication field"
@@ -403,9 +406,10 @@
var/obj/item/mod/module/tem/linked_tem = beam.tem_weakref.resolve()
if(linked_tem && istype(linked_tem))
linked_tem.field_connect(src)
- else
return BULLET_ACT_HIT
+ return ..()
+
/obj/structure/chrono_field/assume_air()
return FALSE
diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm
index d69769c92c56d..8761606568281 100644
--- a/code/modules/modular_computers/computers/item/computer.dm
+++ b/code/modules/modular_computers/computers/item/computer.dm
@@ -500,7 +500,7 @@
if(!caller || !caller.alert_able || caller.alert_silenced || !alerttext) //Yeah, we're checking alert_able. No, you don't get to make alerts that the user can't silence.
return FALSE
playsound(src, sound, 50, TRUE)
- loc.visible_message(span_notice("[icon2html(src)] [span_notice("The [src] displays a [caller.filedesc] notification: [alerttext]")]"))
+ physical.loc.visible_message(span_notice("[icon2html(physical, viewers(physical.loc))] \The [src] displays a [caller.filedesc] notification: [alerttext]"))
/obj/item/modular_computer/proc/ring(ringtone) // bring bring
if(HAS_TRAIT(SSstation, STATION_TRAIT_PDA_GLITCHED))
@@ -517,7 +517,6 @@
var/list/data = list()
data["PC_device_theme"] = device_theme
- data["PC_showbatteryicon"] = !!internal_cell
if(internal_cell)
switch(internal_cell.percent())
@@ -535,8 +534,8 @@
data["PC_batteryicon"] = "batt_5.gif"
data["PC_batterypercent"] = "[round(internal_cell.percent())]%"
else
- data["PC_batteryicon"] = "batt_5.gif"
- data["PC_batterypercent"] = "N/C"
+ data["PC_batteryicon"] = null
+ data["PC_batterypercent"] = null
switch(get_ntnet_status())
if(NTNET_NO_SIGNAL)
diff --git a/code/modules/modular_computers/computers/item/laptop_presets.dm b/code/modules/modular_computers/computers/item/laptop_presets.dm
index 1eebf9f238775..4239a33dfd781 100644
--- a/code/modules/modular_computers/computers/item/laptop_presets.dm
+++ b/code/modules/modular_computers/computers/item/laptop_presets.dm
@@ -3,3 +3,9 @@
starting_programs = list(
/datum/computer_file/program/chatclient,
)
+
+//Used for Mafia testing purposes.
+/obj/item/modular_computer/laptop/preset/mafia
+ starting_programs = list(
+ /datum/computer_file/program/mafia,
+ )
diff --git a/code/modules/modular_computers/computers/item/role_tablet_presets.dm b/code/modules/modular_computers/computers/item/role_tablet_presets.dm
index 60f92c282a884..500077760a88a 100644
--- a/code/modules/modular_computers/computers/item/role_tablet_presets.dm
+++ b/code/modules/modular_computers/computers/item/role_tablet_presets.dm
@@ -47,6 +47,7 @@
name = "head of security PDA"
greyscale_config = /datum/greyscale_config/tablet/head
greyscale_colors = "#EA3232#0000CC"
+ inserted_item = /obj/item/pen/red/security
starting_programs = list(
/datum/computer_file/program/crew_manifest,
/datum/computer_file/program/status,
@@ -122,6 +123,7 @@
/obj/item/modular_computer/pda/security
name = "security PDA"
greyscale_colors = "#EA3232#0000cc"
+ inserted_item = /obj/item/pen/red/security
starting_programs = list(
/datum/computer_file/program/records/security,
/datum/computer_file/program/crew_manifest,
@@ -131,6 +133,7 @@
/obj/item/modular_computer/pda/detective
name = "detective PDA"
greyscale_colors = "#805A2F#990202"
+ inserted_item = /obj/item/pen/red/security
starting_programs = list(
/datum/computer_file/program/records/security,
/datum/computer_file/program/crew_manifest,
@@ -141,6 +144,7 @@
name = "warden PDA"
greyscale_config = /datum/greyscale_config/tablet/stripe_double
greyscale_colors = "#EA3232#0000CC#363636"
+ inserted_item = /obj/item/pen/red/security
starting_programs = list(
/datum/computer_file/program/records/security,
/datum/computer_file/program/crew_manifest,
diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm
index 6f693b5bf998b..8cb741d376ed9 100644
--- a/code/modules/modular_computers/file_system/program.dm
+++ b/code/modules/modular_computers/file_system/program.dm
@@ -132,7 +132,7 @@
if(isAdminGhostAI(user))
return TRUE
- if(!transfer && computer && (computer.obj_flags & EMAGGED)) //emags can bypass the execution locks but not the download ones.
+ if(computer && (computer.obj_flags & EMAGGED) && (available_on_syndinet || !transfer)) //emagged can run anything on syndinet, and can bypass execution locks, but not download.
return TRUE
// Defaults to required_access
diff --git a/code/modules/modular_computers/file_system/programs/antagonist/dos.dm b/code/modules/modular_computers/file_system/programs/antagonist/dos.dm
index 339dd3175ebef..856cc9b6b02b3 100644
--- a/code/modules/modular_computers/file_system/programs/antagonist/dos.dm
+++ b/code/modules/modular_computers/file_system/programs/antagonist/dos.dm
@@ -19,11 +19,11 @@
/datum/computer_file/program/ntnet_dos/process_tick(seconds_per_tick)
dos_speed = 0
switch(ntnet_status)
- if(1)
+ if(NTNET_LOW_SIGNAL)
dos_speed = NTNETSPEED_LOWSIGNAL * 10
- if(2)
+ if(NTNET_GOOD_SIGNAL)
dos_speed = NTNETSPEED_HIGHSIGNAL * 10
- if(3)
+ if(NTNET_ETHERNET_SIGNAL)
dos_speed = NTNETSPEED_ETHERNET * 10
if(target && executed)
target.dos_overload += dos_speed
diff --git a/code/modules/modular_computers/file_system/programs/frontier.dm b/code/modules/modular_computers/file_system/programs/frontier.dm
index b724892da7e1c..9c6e33bcde971 100644
--- a/code/modules/modular_computers/file_system/programs/frontier.dm
+++ b/code/modules/modular_computers/file_system/programs/frontier.dm
@@ -18,12 +18,9 @@
/// The file under consideration.
var/datum/computer_file/data/ordnance/selected_file
-/datum/computer_file/program/scipaper_program/New()
+/datum/computer_file/program/scipaper_program/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing)
. = ..()
paper_to_be = new
-
-/datum/computer_file/program/scipaper_program/on_start(mob/living/user)
- . = ..()
if(!CONFIG_GET(flag/no_default_techweb_link) && !linked_techweb)
CONNECT_TO_RND_SERVER_ROUNDSTART(linked_techweb, computer)
diff --git a/code/modules/modular_computers/file_system/programs/mafia_ntos.dm b/code/modules/modular_computers/file_system/programs/mafia_ntos.dm
new file mode 100644
index 0000000000000..bbcdd39d32c61
--- /dev/null
+++ b/code/modules/modular_computers/file_system/programs/mafia_ntos.dm
@@ -0,0 +1,72 @@
+/datum/computer_file/program/mafia
+ filename = "mafia"
+ filedesc = "Mafia"
+ program_icon_state = "mafia"
+ extended_desc = "A program that allows you to play the infamous Mafia game, straight from your Modular PC."
+ requires_ntnet = FALSE
+ size = 6
+ tgui_id = "NtosMafiaPanel"
+ program_icon = "user-secret"
+ alert_able = TRUE
+
+/datum/computer_file/program/mafia/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing)
+ . = ..()
+ RegisterSignal(SSdcs, COMSIG_MAFIA_GAME_START, PROC_REF(on_game_start))
+
+/datum/computer_file/program/mafia/Destroy(force)
+ var/datum/mafia_controller/game = GLOB.mafia_game
+ if(!game)
+ return ..()
+ UnregisterSignal(game, COMSIG_MAFIA_GAME_END)
+ var/datum/mafia_role/pda_role = game.get_role_player(computer)
+ if(!pda_role)
+ return ..()
+ game.send_message(span_notice("[pda_role.body] has deleted the game from their PDA, and therefore has left the game."))
+ pda_role.kill(game)
+ return ..()
+
+/datum/computer_file/program/mafia/ui_static_data(mob/user)
+ var/list/data = list()
+ var/datum/mafia_controller/game = GLOB.mafia_game
+ if(!game)
+ game = create_mafia_game()
+ data += game.ui_static_data(computer)
+ return data
+
+/datum/computer_file/program/mafia/ui_data(mob/user)
+ var/list/data = list()
+ var/datum/mafia_controller/game = GLOB.mafia_game
+ if(!game)
+ game = create_mafia_game()
+ data += game.ui_data(computer)
+ return data
+
+/datum/computer_file/program/mafia/ui_assets(mob/user)
+ var/list/data = list()
+ var/datum/mafia_controller/game = GLOB.mafia_game
+ if(!game)
+ game = create_mafia_game()
+ data += game.ui_assets(user)
+ return data
+
+/datum/computer_file/program/mafia/ui_act(mob/user, params, datum/tgui/ui, datum/ui_state/state)
+ var/datum/mafia_controller/game = GLOB.mafia_game
+ if(!game)
+ game = create_mafia_game()
+ return game.ui_act(user, params, ui, state)
+
+///Called when a game of Mafia starts, sets the ui header to the proper one.
+/datum/computer_file/program/mafia/proc/on_game_start(datum/controller/subsystem/processing/dcs/source, datum/mafia_controller/game)
+ SIGNAL_HANDLER
+ RegisterSignal(game, COMSIG_MAFIA_GAME_END, PROC_REF(on_game_end))
+ ui_header = "mafia.gif"
+ if(game.get_role_player(computer))
+ alert_pending = TRUE
+ computer.alert_call(src, "Mafia game started!")
+
+///Called when a game of Mafia ends, deletes its ui header.
+/datum/computer_file/program/mafia/proc/on_game_end(datum/mafia_controller/game)
+ SIGNAL_HANDLER
+ UnregisterSignal(game, COMSIG_MAFIA_GAME_END)
+ ui_header = null
+ update_static_data_for_all_viewers()
diff --git a/code/modules/modular_computers/file_system/programs/notepad.dm b/code/modules/modular_computers/file_system/programs/notepad.dm
index 01afaa08c19e0..c0232fe3888ae 100644
--- a/code/modules/modular_computers/file_system/programs/notepad.dm
+++ b/code/modules/modular_computers/file_system/programs/notepad.dm
@@ -7,7 +7,7 @@
size = 2
tgui_id = "NtosNotepad"
program_icon = "book"
- usage_flags = PROGRAM_TABLET
+ usage_flags = PROGRAM_ALL
var/written_note = "Congratulations on your station upgrading to the new NtOS and Thinktronic based collaboration effort, \
bringing you the best in electronics and software since 2467!\n\
diff --git a/code/modules/modular_computers/file_system/programs/ntdownloader.dm b/code/modules/modular_computers/file_system/programs/ntdownloader.dm
index efa61b2630cd5..c9723d905b5f1 100644
--- a/code/modules/modular_computers/file_system/programs/ntdownloader.dm
+++ b/code/modules/modular_computers/file_system/programs/ntdownloader.dm
@@ -7,19 +7,16 @@
size = 4
requires_ntnet = TRUE
available_on_ntnet = FALSE
- ui_header = "downloader_finished.gif"
tgui_id = "NtosNetDownloader"
program_icon = "download"
- var/datum/computer_file/program/downloaded_file = null
+ var/datum/computer_file/program/downloaded_file
var/hacked_download = FALSE
var/download_completion = FALSE //GQ of downloaded data.
var/download_netspeed = 0
var/downloaderror = ""
- var/list/main_repo
- var/list/antag_repo
- var/list/show_categories = list(
+ var/static/list/show_categories = list(
PROGRAM_CATEGORY_CREW,
PROGRAM_CATEGORY_ENGI,
PROGRAM_CATEGORY_SCI,
@@ -27,10 +24,9 @@
PROGRAM_CATEGORY_MISC,
)
-/datum/computer_file/program/ntnetdownload/on_start()
+/datum/computer_file/program/ntnetdownload/kill_program(mob/user)
. = ..()
- main_repo = SSmodular_computers.available_station_software
- antag_repo = SSmodular_computers.available_antag_software
+ ui_header = null
/datum/computer_file/program/ntnetdownload/proc/begin_file_download(filename)
if(downloaded_file)
@@ -50,10 +46,10 @@
ui_header = "downloader_running.gif"
- if(PRG in main_repo)
+ if(PRG in SSmodular_computers.available_station_software)
generate_network_log("Began downloading file [PRG.filename].[PRG.filetype] from NTNet Software Repository.")
hacked_download = FALSE
- else if(PRG in antag_repo)
+ else if(PRG in SSmodular_computers.available_antag_software)
generate_network_log("Began downloading file **ENCRYPTED**.[PRG.filetype] from unspecified server.")
hacked_download = TRUE
else
@@ -68,7 +64,7 @@
generate_network_log("Aborted download of file [hacked_download ? "**ENCRYPTED**" : "[downloaded_file.filename].[downloaded_file.filetype]"].")
downloaded_file = null
download_completion = FALSE
- ui_header = "downloader_finished.gif"
+ ui_header = null
/datum/computer_file/program/ntnetdownload/proc/complete_file_download()
if(!downloaded_file)
@@ -90,11 +86,11 @@
download_netspeed = 0
// Speed defines are found in misc.dm
switch(ntnet_status)
- if(1)
+ if(NTNET_LOW_SIGNAL)
download_netspeed = NTNETSPEED_LOWSIGNAL
- if(2)
+ if(NTNET_GOOD_SIGNAL)
download_netspeed = NTNETSPEED_HIGHSIGNAL
- if(3)
+ if(NTNET_ETHERNET_SIGNAL)
download_netspeed = NTNETSPEED_ETHERNET
download_completion += download_netspeed
@@ -132,7 +128,7 @@
data["disk_used"] = computer.used_capacity
data["emagged"] = (computer.obj_flags & EMAGGED)
- var/list/repo = antag_repo | main_repo
+ var/list/repo = SSmodular_computers.available_antag_software | SSmodular_computers.available_station_software
var/list/program_categories = list()
for(var/datum/computer_file/program/programs as anything in repo)
@@ -147,7 +143,7 @@
"installed" = !!computer.find_file_by_name(programs.filename),
"compatible" = check_compatibility(programs),
"size" = programs.size,
- "access" = (computer.obj_flags & EMAGGED) && programs.available_on_syndinet ? TRUE : programs.can_run(user, transfer = TRUE, access = access),
+ "access" = programs.can_run(user, transfer = TRUE, access = access),
"verifiedsource" = programs.available_on_ntnet,
))
diff --git a/code/modules/modular_computers/file_system/programs/robocontrol.dm b/code/modules/modular_computers/file_system/programs/robocontrol.dm
index cf094e683a983..52bfafdcf8e97 100644
--- a/code/modules/modular_computers/file_system/programs/robocontrol.dm
+++ b/code/modules/modular_computers/file_system/programs/robocontrol.dm
@@ -62,7 +62,7 @@
newbot["mule_check"] = TRUE
botlist += list(newbot)
- for(var/mob/living/simple_animal/drone/all_drones as anything in GLOB.drones_list)
+ for(var/mob/living/basic/drone/all_drones as anything in GLOB.drones_list)
if(all_drones.hacked)
continue
if(!is_valid_z_level(current_turf, get_turf(all_drones)))
diff --git a/code/modules/modular_computers/file_system/programs/signalcommander.dm b/code/modules/modular_computers/file_system/programs/signalcommander.dm
index 6d636bab370a9..e8140b62b17c2 100644
--- a/code/modules/modular_computers/file_system/programs/signalcommander.dm
+++ b/code/modules/modular_computers/file_system/programs/signalcommander.dm
@@ -14,6 +14,10 @@
var/signal_code = DEFAULT_SIGNALER_CODE
/// Radio connection datum used by signalers.
var/datum/radio_frequency/radio_connection
+ /// How long do we cooldown before we can send another signal?
+ var/signal_cooldown_time = 1 SECONDS
+ /// Cooldown store
+ COOLDOWN_DECLARE(signal_cooldown)
/datum/computer_file/program/signal_commander/on_start(mob/living/user)
. = ..()
@@ -26,6 +30,7 @@
/datum/computer_file/program/signal_commander/ui_data(mob/user)
var/list/data = list()
data["frequency"] = signal_frequency
+ data["cooldown"] = signal_cooldown_time
data["code"] = signal_code
data["minFrequency"] = MIN_FREE_FREQ
data["maxFrequency"] = MAX_FREE_FREQ
@@ -55,13 +60,18 @@
if(!radio_connection)
return
+ if(!COOLDOWN_FINISHED(src, signal_cooldown))
+ computer.balloon_alert(usr, "cooling down!")
+ return
+
+ COOLDOWN_START(src, signal_cooldown, signal_cooldown_time)
+ computer.balloon_alert(usr, "signaled")
+
var/time = time2text(world.realtime,"hh:mm:ss")
var/turf/T = get_turf(computer)
- var/logging_data
- if(usr)
- logging_data = "[time] : [usr.key] used [computer] @ location ([T.x],[T.y],[T.z]) : [format_frequency(signal_frequency)]/[signal_code]"
- GLOB.lastsignalers.Add(logging_data)
+ var/logging_data = "[time] : [key_name(usr)] used the computer '[initial(computer.name)]' @ location ([T.x],[T.y],[T.z]) : [format_frequency(signal_frequency)]/[signal_code]"
+ add_to_signaler_investigate_log(logging_data)
var/datum/signal/signal = new(list("code" = signal_code), logging_data = logging_data)
radio_connection.post_signal(computer, signal)
@@ -70,4 +80,3 @@
SSradio.remove_object(computer, signal_frequency)
signal_frequency = new_frequency
radio_connection = SSradio.add_object(computer, signal_frequency, RADIO_SIGNALER)
- return
diff --git a/code/modules/modular_computers/file_system/programs/techweb.dm b/code/modules/modular_computers/file_system/programs/techweb.dm
index dc9538cf3580d..77d0a0900e4a5 100644
--- a/code/modules/modular_computers/file_system/programs/techweb.dm
+++ b/code/modules/modular_computers/file_system/programs/techweb.dm
@@ -21,7 +21,7 @@
/// Sequence var for the id cache
var/id_cache_seq = 1
-/datum/computer_file/program/science/on_start(mob/living/user)
+/datum/computer_file/program/science/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing)
. = ..()
if(!CONFIG_GET(flag/no_default_techweb_link) && !stored_research)
CONNECT_TO_RND_SERVER_ROUNDSTART(stored_research, computer)
diff --git a/code/modules/movespeed/modifiers/mobs.dm b/code/modules/movespeed/modifiers/mobs.dm
index e5f293232239a..49358223e3508 100644
--- a/code/modules/movespeed/modifiers/mobs.dm
+++ b/code/modules/movespeed/modifiers/mobs.dm
@@ -109,6 +109,9 @@
/datum/movespeed_modifier/average_web
multiplicative_slowdown = 1.2
+/datum/movespeed_modifier/below_average_web
+ multiplicative_slowdown = 2.5
+
/datum/movespeed_modifier/slow_web
multiplicative_slowdown = 5
diff --git a/code/modules/pai/card.dm b/code/modules/pai/card.dm
index a652b745c9e50..086e188b4319c 100644
--- a/code/modules/pai/card.dm
+++ b/code/modules/pai/card.dm
@@ -234,7 +234,17 @@
playsound(src, 'sound/machines/ping.ogg', 20, TRUE)
balloon_alert(user, "pAI assistance requested")
var/mutable_appearance/alert_overlay = mutable_appearance('icons/obj/aicards.dmi', "pai")
- notify_ghosts("[user] is requesting a pAI companion! Use the pAI button to submit yourself as one.", source = user, alert_overlay = alert_overlay, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "pAI Request!", ignore_key = POLL_IGNORE_PAI)
+
+ notify_ghosts(
+ "[user] is requesting a pAI companion! Use the pAI button to submit yourself as one.",
+ source = user,
+ alert_overlay = alert_overlay,
+ action = NOTIFY_ORBIT,
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ header = "pAI Request!",
+ ignore_key = POLL_IGNORE_PAI,
+ )
+
addtimer(VARSET_CALLBACK(src, request_spam, FALSE), PAI_SPAM_TIME, TIMER_UNIQUE | TIMER_STOPPABLE | TIMER_CLIENT_TIME | TIMER_DELETE_ME)
return TRUE
diff --git a/code/modules/pai/defense.dm b/code/modules/pai/defense.dm
index 75c437c2546e0..61fadf820bfcf 100644
--- a/code/modules/pai/defense.dm
+++ b/code/modules/pai/defense.dm
@@ -56,11 +56,11 @@
if(user.put_in_hands(card))
user.visible_message(span_notice("[user] promptly scoops up [user.p_their()] pAI's card."))
-/mob/living/silicon/pai/bullet_act(obj/projectile/Proj)
- if(Proj.stun)
+/mob/living/silicon/pai/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
+ . = ..()
+ if(. == BULLET_ACT_HIT && (hitting_projectile.stun || hitting_projectile.paralyze))
fold_in(force = TRUE)
- src.visible_message(span_warning("The electrically-charged projectile disrupts [src]'s holomatrix, forcing [src] to fold in!"))
- . = ..(Proj)
+ visible_message(span_warning("The electrically-charged projectile disrupts [src]'s holomatrix, forcing [p_them()] to fold in!"))
/mob/living/silicon/pai/ignite_mob(silent)
return FALSE
diff --git a/code/modules/paperwork/paper_premade.dm b/code/modules/paperwork/paper_premade.dm
index bb38a27d38d75..47588a65706c7 100644
--- a/code/modules/paperwork/paper_premade.dm
+++ b/code/modules/paperwork/paper_premade.dm
@@ -72,6 +72,19 @@
Keep this manual for your records, failure to do so will void your 2 day limited liability warranty from Nanotrasen."}
+/obj/item/paper/fluff/jobs/engineering/frequencies
+ name = "Station Frequencies"
+ default_raw_text = {"Please remember the frequencies of each radio channel used on station:
+ * AI Private - 144.7
+ * Command - 135.3
+ * Common - 145.9
+ * Engineering - 135.7
+ * Medical - 135.5
+ * Science - 135.1
+ * Security - 135.9
+ * Service - 134.9
+ * Supply - 134.7"}
+
/obj/item/paper/fluff/jobs/security/beepsky_mom
name = "Note from Beepsky's Mom"
default_raw_text = "01001001 00100000 01101000 01101111 01110000 01100101 00100000 01111001 01101111 01110101 00100000 01110011 01110100 01100001 01111001 00100000 01110011 01100001 01100110 01100101 00101110 00100000 01001100 01101111 01110110 01100101 00101100 00100000 01101101 01101111 01101101 00101110"
diff --git a/code/modules/paperwork/paperplane.dm b/code/modules/paperwork/paperplane.dm
index 08d34dca863da..d3688ff2156ac 100644
--- a/code/modules/paperwork/paperplane.dm
+++ b/code/modules/paperwork/paperplane.dm
@@ -88,7 +88,7 @@
return ..()
-/obj/item/paperplane/throw_at(atom/target, range, speed, mob/thrower, spin=FALSE, diagonals_first = FALSE, datum/callback/callback, quickstart = TRUE)
+/obj/item/paperplane/throw_at(atom/target, range, speed, mob/thrower, spin=FALSE, diagonals_first = FALSE, datum/callback/callback, gentle, quickstart = TRUE)
. = ..(target, range, speed, thrower, FALSE, diagonals_first, callback, quickstart = quickstart)
/obj/item/paperplane/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
diff --git a/code/modules/paperwork/pen.dm b/code/modules/paperwork/pen.dm
index 05606ac3a2ef1..69af56d341902 100644
--- a/code/modules/paperwork/pen.dm
+++ b/code/modules/paperwork/pen.dm
@@ -400,3 +400,44 @@
. = ..()
icon_state = "[initial(icon_state)][HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) ? "_out" : null]"
inhand_icon_state = initial(inhand_icon_state) //since transforming component switches the icon.
+
+//The Security holopen
+/obj/item/pen/red/security
+ name = "security pen"
+ desc = "This is a red ink pen exclusively provided to members of the Security Department. Its opposite end features a built-in holographic projector designed for issuing arrest prompts to individuals."
+ icon_state = "pen_sec"
+ COOLDOWN_DECLARE(holosign_cooldown)
+
+/obj/item/pen/red/security/examine(mob/user)
+ . = ..()
+ . += span_notice("To initiate the surrender prompt, simply click on an individual within your proximity.")
+
+//Code from the medical penlight
+/obj/item/pen/red/security/afterattack(atom/target, mob/living/user, proximity)
+ . = ..()
+ if(!COOLDOWN_FINISHED(src, holosign_cooldown))
+ balloon_alert(user, "not ready!")
+ return
+
+ var/target_turf = get_turf(target)
+ var/mob/living/living_target = locate(/mob/living) in target_turf
+
+ if(!living_target || (living_target == user))
+ return
+
+ living_target.apply_status_effect(/datum/status_effect/surrender_timed)
+ to_chat(living_target, span_userdanger("[user] requests your immediate surrender! You are given 30 seconds to comply!"))
+ new /obj/effect/temp_visual/security_holosign(target_turf, user) //produce a holographic glow
+ COOLDOWN_START(src, holosign_cooldown, 30 SECONDS)
+
+/obj/effect/temp_visual/security_holosign
+ name = "security holosign"
+ desc = "A small holographic glow that indicates you're under arrest."
+ icon_state = "sec_holo"
+ duration = 60
+
+/obj/effect/temp_visual/security_holosign/Initialize(mapload, creator)
+ . = ..()
+ playsound(loc, 'sound/machines/chime.ogg', 50, FALSE) //make some noise!
+ if(creator)
+ visible_message(span_danger("[creator] created a security hologram!"))
diff --git a/code/modules/plumbing/plumbers/acclimator.dm b/code/modules/plumbing/plumbers/acclimator.dm
index 6b7a8caba4ac8..850ebaaa3ed1c 100644
--- a/code/modules/plumbing/plumbers/acclimator.dm
+++ b/code/modules/plumbing/plumbers/acclimator.dm
@@ -3,6 +3,9 @@
#define HEATING "Heating"
#define NEUTRAL "Neutral"
+///cool/heat power. converts temperature into joules
+#define HEATER_COEFFICIENT 0.05
+
///this the plumbing version of a heater/freezer.
/obj/machinery/plumbing/acclimator
name = "chemical acclimator"
@@ -17,8 +20,6 @@
var/target_temperature = 300
///I cant find a good name for this. Basically if target is 300, and this is 10, it will still target 300 but will start emptying itself at 290 and 310.
var/allowed_temperature_difference = 1
- ///cool/heat power
- var/heater_coefficient = 0.05
///Are we turned on or off? this is from the on and off button
var/enabled = TRUE
///COOLING, HEATING or NEUTRAL. We track this for change, so we dont needlessly update our icon
@@ -52,7 +53,7 @@
emptying = TRUE
if(!emptying) //suspend heating/cooling during emptying phase
- reagents.adjust_thermal_energy((target_temperature - reagents.chem_temp) * heater_coefficient * seconds_per_tick * SPECIFIC_HEAT_DEFAULT * reagents.total_volume) //keep constant with chem heater
+ reagents.adjust_thermal_energy((target_temperature - reagents.chem_temp) * HEATER_COEFFICIENT * seconds_per_tick * SPECIFIC_HEAT_DEFAULT * reagents.total_volume) //keep constant with chem heater
reagents.handle_reactions()
use_power(active_power_usage * seconds_per_tick)
else if(acclimate_state != NEUTRAL)
@@ -109,3 +110,4 @@
#undef COOLING
#undef HEATING
#undef NEUTRAL
+#undef HEATER_COEFFICIENT
diff --git a/code/modules/plumbing/plumbers/bottler.dm b/code/modules/plumbing/plumbers/bottler.dm
index 7ce4c2f55765d..eab1811ec6c5c 100644
--- a/code/modules/plumbing/plumbers/bottler.dm
+++ b/code/modules/plumbing/plumbers/bottler.dm
@@ -34,23 +34,24 @@
///changes the tile array
/obj/machinery/plumbing/bottler/setDir(newdir)
. = ..()
+ var/turf/target_turf = get_turf(src)
switch(dir)
if(NORTH)
- goodspot = get_step(get_turf(src), NORTH)
- inputspot = get_step(get_turf(src), SOUTH)
- badspot = get_step(get_turf(src), EAST)
+ goodspot = get_step(target_turf, NORTH)
+ inputspot = get_step(target_turf, SOUTH)
+ badspot = get_step(target_turf, EAST)
if(SOUTH)
- goodspot = get_step(get_turf(src), SOUTH)
- inputspot = get_step(get_turf(src), NORTH)
- badspot = get_step(get_turf(src), WEST)
+ goodspot = get_step(target_turf, SOUTH)
+ inputspot = get_step(target_turf, NORTH)
+ badspot = get_step(target_turf, WEST)
if(WEST)
- goodspot = get_step(get_turf(src), WEST)
- inputspot = get_step(get_turf(src), EAST)
- badspot = get_step(get_turf(src), NORTH)
+ goodspot = get_step(target_turf, WEST)
+ inputspot = get_step(target_turf, EAST)
+ badspot = get_step(target_turf, NORTH)
if(EAST)
- goodspot = get_step(get_turf(src), EAST)
- inputspot = get_step(get_turf(src), WEST)
- badspot = get_step(get_turf(src), SOUTH)
+ goodspot = get_step(target_turf, EAST)
+ inputspot = get_step(target_turf, WEST)
+ badspot = get_step(target_turf, SOUTH)
//If by some miracle
if( ( !valid_output_configuration ) && ( goodspot != null && inputspot != null && badspot != null ) )
@@ -63,7 +64,7 @@
if(!valid_output_configuration)
to_chat(user, span_warning("A flashing notification on the screen reads: \"Output location error!\""))
return .
- var/new_amount = tgui_input_number(user, "Set Amount to Fill", "Desired Amount", max_value = 100)
+ var/new_amount = tgui_input_number(user, "Set Amount to Fill", "Desired Amount", max_value = reagents.maximum_volume, round_value = TRUE)
if(!new_amount || QDELETED(user) || QDELETED(src) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
return .
wanted_amount = new_amount
@@ -81,8 +82,13 @@
if(reagents.total_volume >= wanted_amount && anchored && length(inputspot.contents))
use_power(active_power_usage * seconds_per_tick)
var/obj/AM = pick(inputspot.contents)///pick a reagent_container that could be used
- if((is_reagent_container(AM) && !istype(AM, /obj/item/reagent_containers/hypospray/medipen)) || istype(AM, /obj/item/ammo_casing/shotgun/dart))
- var/obj/item/reagent_containers/B = AM
+ //allowed containers
+ var/static/list/allowed_containers = list(
+ /obj/item/reagent_containers/cup,
+ /obj/item/ammo_casing/shotgun/dart,
+ )
+ if(is_type_in_list(AM, allowed_containers))
+ var/obj/item/B = AM
///see if it would overflow else inject
if((B.reagents.total_volume + wanted_amount) <= B.reagents.maximum_volume)
reagents.trans_to(B, wanted_amount, transferred_by = src)
@@ -90,10 +96,8 @@
return
///glass was full so we move it away
AM.forceMove(badspot)
- if(istype(AM, /obj/item/slime_extract)) ///slime extracts need inject
+ else if(istype(AM, /obj/item/slime_extract)) ///slime extracts need inject
AM.forceMove(goodspot)
reagents.trans_to(AM, wanted_amount, transferred_by = src, methods = INJECT)
- return
- if(istype(AM, /obj/item/slimecross/industrial)) ///no need to move slimecross industrial things
+ else if(istype(AM, /obj/item/slimecross/industrial)) ///no need to move slimecross industrial things
reagents.trans_to(AM, wanted_amount, transferred_by = src, methods = INJECT)
- return
diff --git a/code/modules/plumbing/plumbers/iv_drip.dm b/code/modules/plumbing/plumbers/iv_drip.dm
new file mode 100644
index 0000000000000..1db36c137e6d7
--- /dev/null
+++ b/code/modules/plumbing/plumbers/iv_drip.dm
@@ -0,0 +1,41 @@
+///modified IV that can be anchored and takes plumbing in- and output
+/obj/machinery/iv_drip/plumbing
+ name = "automated IV drip"
+ desc = "A modified IV drip with plumbing connects. Reagents received from the connect are injected directly into their bloodstream, blood that is drawn goes to the internal storage and then into the ducting."
+ icon_state = "plumb"
+ base_icon_state = "plumb"
+ density = TRUE
+ use_internal_storage = TRUE
+
+/obj/machinery/iv_drip/plumbing/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/plumbing/iv_drip, anchored)
+ AddComponent(/datum/component/simple_rotation)
+
+/obj/machinery/iv_drip/plumbing/add_context(atom/source, list/context, obj/item/held_item, mob/living/user)
+ if(attached)
+ context[SCREENTIP_CONTEXT_RMB] = "Take needle out"
+ else if(reagent_container && !use_internal_storage)
+ context[SCREENTIP_CONTEXT_RMB] = "Eject container"
+ else if(!inject_only)
+ context[SCREENTIP_CONTEXT_RMB] = "Change direction"
+
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/machinery/iv_drip/plumbing/plunger_act(obj/item/plunger/P, mob/living/user, reinforced)
+ to_chat(user, span_notice("You start furiously plunging [name]."))
+ if(do_after(user, 30, target = src))
+ to_chat(user, span_notice("You finish plunging the [name]."))
+ reagents.expose(get_turf(src), TOUCH) //splash on the floor
+ reagents.clear_reagents()
+
+/obj/machinery/iv_drip/plumbing/can_use_alt_click(mob/user)
+ return FALSE //Alt click is used for rotation
+
+/obj/machinery/iv_drip/plumbing/wrench_act(mob/living/user, obj/item/tool)
+ . = ..()
+ if(default_unfasten_wrench(user, tool) == SUCCESSFUL_UNFASTEN)
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/machinery/iv_drip/plumbing/deconstruct(disassembled = TRUE)
+ qdel(src)
diff --git a/code/modules/plumbing/plumbers/pill_press.dm b/code/modules/plumbing/plumbers/pill_press.dm
index df6a627b10359..26835e7c92d6f 100644
--- a/code/modules/plumbing/plumbers/pill_press.dm
+++ b/code/modules/plumbing/plumbers/pill_press.dm
@@ -1,3 +1,10 @@
+///the minimum size of a pill or patch
+#define MIN_VOLUME 5
+///the maximum size a pill or patch can be
+#define MAX_VOLUME 50
+///max amount of pills allowed on our tile before we start storing them instead
+#define MAX_FLOOR_PRODUCTS 10
+
///We take a constant input of reagents, and produce a pill once a set volume is reached
/obj/machinery/plumbing/pill_press
name = "chemical press"
@@ -5,107 +12,104 @@
icon_state = "pill_press"
active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 2
- ///maximum size of a pill
- var/max_pill_volume = 50
- ///maximum size of a patch
- var/max_patch_volume = 40
- ///maximum size of a bottle
- var/max_bottle_volume = 50
- ///current operating product (pills or patches)
+ /// current operating product (pills or patches)
var/product = "pill"
- ///the minimum size a pill or patch can be
- var/min_volume = 5
- ///the maximum size a pill or patch can be
- var/max_volume = 50
- ///selected size of the product
+ /// selected size of the product
var/current_volume = 10
- ///prefix for the product name
+ /// prefix for the product name
var/product_name = "factory"
- ///the icon_state number for the pill.
- var/pill_number = RANDOM_PILL_STYLE
- ///list of id's and icons for the pill selection of the ui
- var/list/pill_styles
- /// Currently selected patch style
- var/patch_style = DEFAULT_PATCH_STYLE
- /// List of available patch styles for UI
- var/list/patch_styles
- ///list of products stored in the machine, so we dont have 610 pills on one tile
+ /// All packaging types wrapped up in 1 big list
+ var/static/list/packaging_types = null
+ ///The type of packaging to use
+ var/packaging_type
+ ///Category of packaging
+ var/packaging_category
+ /// list of products stored in the machine, so we dont have 610 pills on one tile
var/list/stored_products = list()
- ///max amount of pills allowed on our tile before we start storing them instead
- var/max_floor_products = 10
-
-/obj/machinery/plumbing/pill_press/examine(mob/user)
- . = ..()
- . += span_notice("The [name] currently has [stored_products.len] stored. There needs to be less than [max_floor_products] on the floor to continue dispensing.")
/obj/machinery/plumbing/pill_press/Initialize(mapload, bolt, layer)
. = ..()
+ if(!packaging_types)
+ var/datum/asset/spritesheet/simple/assets = get_asset_datum(/datum/asset/spritesheet/chemmaster)
+
+ var/list/types = list(
+ CAT_PILLS = GLOB.reagent_containers[CAT_PILLS],
+ CAT_PATCHES = GLOB.reagent_containers[CAT_PATCHES],
+ "Bottles" = list(/obj/item/reagent_containers/cup/bottle),
+ )
+
+ packaging_types = list()
+ for(var/category in types)
+ var/list/packages = types[category]
+
+ var/list/category_item = list("cat_name" = category)
+ for(var/obj/item/reagent_containers/container as anything in packages)
+ var/list/package_item = list(
+ "class_name" = assets.icon_class_name(sanitize_css_class_name("[container]")),
+ "ref" = REF(container)
+ )
+ category_item["products"] += list(package_item)
+
+ packaging_types += list(category_item)
+
+ packaging_type = REF(GLOB.reagent_containers[CAT_PILLS][1])
+ decode_category()
+
AddComponent(/datum/component/plumbing/simple_demand, bolt, layer)
+/obj/machinery/plumbing/pill_press/examine(mob/user)
+ . = ..()
+ . += span_notice("The [name] currently has [stored_products.len] stored. There needs to be less than [MAX_FLOOR_PRODUCTS] on the floor to continue dispensing.")
+
+/// decode product category from it's type path and returns the decoded typepath
+/obj/machinery/plumbing/pill_press/proc/decode_category()
+ var/obj/item/reagent_containers/container = locate(packaging_type)
+ if(ispath(container, /obj/item/reagent_containers/pill/patch))
+ packaging_category = CAT_PATCHES
+ else if(ispath(container, /obj/item/reagent_containers/pill))
+ packaging_category = CAT_PILLS
+ else
+ packaging_category = "Bottles"
+ return container
+
/obj/machinery/plumbing/pill_press/process(seconds_per_tick)
if(machine_stat & NOPOWER)
return
+
+ //shift & check to account for floating point inaccuracies
if(reagents.total_volume >= current_volume)
- if (product == "pill")
- var/obj/item/reagent_containers/pill/P = new(src)
- reagents.trans_to(P, current_volume)
- P.name = trim("[product_name] pill")
- stored_products += P
- if(pill_number == RANDOM_PILL_STYLE)
- P.icon_state = "pill[rand(1,21)]"
+ var/obj/item/reagent_containers/container = locate(packaging_type)
+ container = new container(src)
+ var/suffix
+ switch(packaging_category)
+ if(CAT_PILLS)
+ suffix = "Pill"
+ if(CAT_PATCHES)
+ suffix = "Patch"
else
- P.icon_state = "pill[pill_number]"
- if(P.icon_state == "pill4") //mirrored from chem masters
- P.desc = "A tablet or capsule, but not just any, a red one, one taken by the ones not scared of knowledge, freedom, uncertainty and the brutal truths of reality."
- else if (product == "patch")
- var/obj/item/reagent_containers/pill/patch/P = new(src)
- reagents.trans_to(P, current_volume)
- P.name = trim("[product_name] patch")
- P.icon_state = patch_style
- stored_products += P
- else if (product == "bottle")
- var/obj/item/reagent_containers/cup/bottle/P = new(src)
- reagents.trans_to(P, current_volume)
- P.name = trim("[product_name] bottle")
- stored_products += P
+ suffix = "Bottle"
+ container.name = "[product_name] [suffix]"
+ reagents.trans_to(container, current_volume)
+ stored_products += container
+
+ //dispense stored products on the floor
if(stored_products.len)
var/pill_amount = 0
- for(var/thing in loc)
- if(!istype(thing, /obj/item/reagent_containers/cup/bottle) && !istype(thing, /obj/item/reagent_containers/pill))
- continue
+ for(var/obj/item/reagent_containers/thing in loc)
pill_amount++
- if(pill_amount >= max_floor_products) //too much so just stop
+ if(pill_amount >= MAX_FLOOR_PRODUCTS) //too much so just stop
break
- if(pill_amount < max_floor_products && anchored)
+ if(pill_amount < MAX_FLOOR_PRODUCTS && anchored)
var/atom/movable/AM = stored_products[1] //AM because forceMove is all we need
stored_products -= AM
AM.forceMove(drop_location())
use_power(active_power_usage * seconds_per_tick)
-/obj/machinery/plumbing/pill_press/proc/load_styles()
- //expertly copypasted from chemmasters
- var/datum/asset/spritesheet/simple/assets = get_asset_datum(/datum/asset/spritesheet/simple/pills)
- pill_styles = list()
- for (var/x in 1 to PILL_STYLE_COUNT)
- var/list/SL = list()
- SL["id"] = x
- SL["class_name"] = assets.icon_class_name("pill[x]")
- pill_styles += list(SL)
- var/datum/asset/spritesheet/simple/patches_assets = get_asset_datum(/datum/asset/spritesheet/simple/patches)
- patch_styles = list()
- for (var/raw_patch_style in PATCH_STYLE_LIST)
- //adding class_name for use in UI
- var/list/patch_style = list()
- patch_style["style"] = raw_patch_style
- patch_style["class_name"] = patches_assets.icon_class_name(raw_patch_style)
- patch_styles += list(patch_style)
-
/obj/machinery/plumbing/pill_press/ui_assets(mob/user)
return list(
- get_asset_datum(/datum/asset/spritesheet/simple/pills),
- get_asset_datum(/datum/asset/spritesheet/simple/patches),
+ get_asset_datum(/datum/asset/spritesheet/chemmaster)
)
/obj/machinery/plumbing/pill_press/ui_interact(mob/user, datum/tgui/ui)
@@ -114,45 +118,45 @@
ui = new(user, src, "ChemPress", name)
ui.open()
+/obj/machinery/plumbing/pill_press/ui_static_data(mob/user)
+ var/list/data = list()
+
+ data["min_volume"] = MIN_VOLUME
+ data["max_volume"] = MAX_VOLUME
+ data["packaging_types"] = packaging_types
+
+ return data
+
/obj/machinery/plumbing/pill_press/ui_data(mob/user)
- if(!pill_styles || !patch_styles)
- load_styles()
var/list/data = list()
- data["pill_style"] = pill_number
+
data["current_volume"] = current_volume
data["product_name"] = product_name
- data["pill_styles"] = pill_styles
- data["product"] = product
- data["min_volume"] = min_volume
- data["max_volume"] = max_volume
- data["patch_style"] = patch_style
- data["patch_styles"] = patch_styles
+ data["packaging_type"] = packaging_type
+ data["packaging_category"] = packaging_category
+
return data
/obj/machinery/plumbing/pill_press/ui_act(action, params)
. = ..()
if(.)
return
+
. = TRUE
switch(action)
- if("change_pill_style")
- pill_number = clamp(text2num(params["id"]), 1 , PILL_STYLE_COUNT)
if("change_current_volume")
- current_volume = clamp(text2num(params["volume"]), min_volume, max_volume)
+ current_volume = round(clamp(text2num(params["volume"]), MIN_VOLUME, MAX_VOLUME))
if("change_product_name")
var/formatted_name = html_encode(params["name"])
if (length(formatted_name) > MAX_NAME_LEN)
- product_name = copytext(formatted_name, 1, MAX_NAME_LEN+1)
+ product_name = copytext(formatted_name, 1, MAX_NAME_LEN + 1)
else
product_name = formatted_name
if("change_product")
- product = params["product"]
- if (product == "pill")
- max_volume = max_pill_volume
- else if (product == "patch")
- max_volume = max_patch_volume
- else if (product == "bottle")
- max_volume = max_bottle_volume
- current_volume = clamp(current_volume, min_volume, max_volume)
- if("change_patch_style")
- patch_style = params["patch_style"]
+ packaging_type = params["ref"]
+ var/obj/item/reagent_containers/container = decode_category()
+ current_volume = clamp(current_volume, MIN_VOLUME, initial(container.volume))
+
+#undef MIN_VOLUME
+#undef MAX_VOLUME
+#undef MAX_FLOOR_PRODUCTS
diff --git a/code/modules/plumbing/plumbers/reaction_chamber.dm b/code/modules/plumbing/plumbers/reaction_chamber.dm
index 36320f18184fe..01a6b4a0b82fd 100644
--- a/code/modules/plumbing/plumbers/reaction_chamber.dm
+++ b/code/modules/plumbing/plumbers/reaction_chamber.dm
@@ -3,6 +3,9 @@
/// coefficient to convert temperature to joules. same lvl as acclimator
#define HEATER_COEFFICIENT 0.05
+/// maximum number of attempts the reaction chamber will make to balance the ph(More means better results but higher tick usage)
+#define MAX_PH_ADJUSTMENTS 5
+
/obj/machinery/plumbing/reaction_chamber
name = "mixing chamber"
desc = "Keeps chemicals separated until given conditions are met."
@@ -43,7 +46,7 @@
/obj/machinery/plumbing/reaction_chamber/proc/on_reagent_change(datum/reagents/holder, ...)
SIGNAL_HANDLER
- if(!holder.total_volume && emptying) //we were emptying, but now we aren't
+ if(holder.total_volume <= CHEMICAL_VOLUME_ROUNDING && emptying) //we were emptying, but now we aren't
emptying = FALSE
holder.flags |= NO_REACT
return NONE
@@ -53,9 +56,6 @@
var/power_usage = active_power_usage * 0.5
if(!emptying || reagents.is_reacting)
- //do reactions and stuff
- reagents.handle_reactions()
-
//adjust temperature of final solution
var/temp_diff = target_temperature - reagents.chem_temp
if(abs(temp_diff) > 0.01) //if we are not close enough keep going
@@ -173,10 +173,17 @@
return ..()
/obj/machinery/plumbing/reaction_chamber/chem/handle_reagents(seconds_per_tick)
- while(reagents.ph < acidic_limit || reagents.ph > alkaline_limit)
+ var/ph_balance_attempts = 0
+ while(ph_balance_attempts < MAX_PH_ADJUSTMENTS && (reagents.ph < acidic_limit || reagents.ph > alkaline_limit))
+ //no power
if(machine_stat & NOPOWER)
return
+ //nothing to react with
+ var/num_of_reagents = length(reagents.reagent_list)
+ if(!num_of_reagents)
+ return
+
/**
* figure out which buffer to transfer to restore balance
* if solution is getting too basic(high ph) add some acid to lower it's value
@@ -186,13 +193,15 @@
if(!buffer.total_volume)
return
- //transfer buffer and handle reactions, not a proven math but looks logical
- var/transfer_amount = FLOOR((reagents.ph > alkaline_limit ? (reagents.ph - alkaline_limit) : (acidic_limit - reagents.ph)) * seconds_per_tick, CHEMICAL_QUANTISATION_LEVEL)
- if(transfer_amount <= CHEMICAL_QUANTISATION_LEVEL || !buffer.trans_to(reagents, transfer_amount))
+ //transfer buffer and handle reactions
+ var/ph_change = (reagents.ph > alkaline_limit ? (reagents.ph - alkaline_limit) : (acidic_limit - reagents.ph))
+ var/buffer_amount = ((ph_change * reagents.total_volume) / (BUFFER_IONIZING_STRENGTH * num_of_reagents))
+ if(!buffer.trans_to(reagents, buffer_amount * seconds_per_tick))
return
- //some power for accurate ph balancing
- use_power(active_power_usage * 0.2 * seconds_per_tick)
+ //some power for accurate ph balancing & keep track of attempts made
+ use_power(active_power_usage * 0.03 * buffer_amount * seconds_per_tick)
+ ph_balance_attempts += 1
/obj/machinery/plumbing/reaction_chamber/chem/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -211,11 +220,11 @@
switch(action)
if("acidic")
- acidic_limit = clamp(round(text2num(params["target"])), 0, alkaline_limit)
+ acidic_limit = clamp(round(text2num(params["target"])), CHEMICAL_MIN_PH, alkaline_limit - 1)
if("alkaline")
- alkaline_limit = clamp(round(text2num(params["target"])), acidic_limit + 0.01, 14)
+ alkaline_limit = clamp(round(text2num(params["target"])), acidic_limit + 1, CHEMICAL_MAX_PH)
else
return FALSE
-
#undef HEATER_COEFFICIENT
+#undef MAX_PH_ADJUSTMENTS
diff --git a/code/modules/plumbing/plumbers/synthesizer.dm b/code/modules/plumbing/plumbers/synthesizer.dm
index a4521dd9b2e5a..3dddd648e6165 100644
--- a/code/modules/plumbing/plumbers/synthesizer.dm
+++ b/code/modules/plumbing/plumbers/synthesizer.dm
@@ -10,11 +10,11 @@
///Amount we produce for every process. Ideally keep under 5 since thats currently the standard duct capacity
var/amount = 1
///I track them here because I have no idea how I'd make tgui loop like that
- var/static/list/possible_amounts = list(0,1,2,3,4,5)
+ var/static/list/possible_amounts = list(0, 1, 2, 3, 4, 5)
///The reagent we are producing. We are a typepath, but are also typecast because there's several occations where we need to use initial.
var/datum/reagent/reagent_id = null
///straight up copied from chem dispenser. Being a subtype would be extremely tedious and making it global would restrict potential subtypes using different dispensable_reagents
- var/list/dispensable_reagents = list(
+ var/static/list/default_reagents = list(
/datum/reagent/aluminium,
/datum/reagent/bromine,
/datum/reagent/carbon,
@@ -41,10 +41,13 @@
/datum/reagent/water,
/datum/reagent/fuel,
)
+ //reagents this synthesizer can dispense
+ var/list/dispensable_reagents
/obj/machinery/plumbing/synthesizer/Initialize(mapload, bolt, layer)
. = ..()
AddComponent(/datum/component/plumbing/simple_supply, bolt, layer)
+ dispensable_reagents = default_reagents
/obj/machinery/plumbing/synthesizer/process(seconds_per_tick)
if(machine_stat & NOPOWER || !reagent_id || !amount)
@@ -114,7 +117,7 @@
icon_state = "synthesizer_soda"
//Copied from soda dispenser
- dispensable_reagents = list(
+ var/static/list/soda_reagents = list(
/datum/reagent/consumable/coffee,
/datum/reagent/consumable/space_cola,
/datum/reagent/consumable/cream,
@@ -140,6 +143,11 @@
/datum/reagent/water,
)
+/obj/machinery/plumbing/synthesizer/soda/Initialize(mapload, bolt, layer)
+ . = ..()
+
+ dispensable_reagents = soda_reagents
+
/obj/machinery/plumbing/synthesizer/beer
name = "beer synthesizer"
desc = "Produces a single chemical at a given volume. Must be plumbed."
@@ -147,7 +155,7 @@
icon_state = "synthesizer_booze"
//Copied from beer dispenser
- dispensable_reagents = list(
+ var/static/list/beer_reagents = list(
/datum/reagent/consumable/ethanol/absinthe,
/datum/reagent/consumable/ethanol/ale,
/datum/reagent/consumable/ethanol/applejack,
@@ -172,3 +180,7 @@
/datum/reagent/consumable/ethanol/wine,
)
+/obj/machinery/plumbing/synthesizer/beer/Initialize(mapload, bolt, layer)
+ . = ..()
+
+ dispensable_reagents = beer_reagents
diff --git a/code/modules/research/xenobiology/vatgrowing/vatgrower.dm b/code/modules/plumbing/plumbers/vatgrower.dm
similarity index 100%
rename from code/modules/research/xenobiology/vatgrowing/vatgrower.dm
rename to code/modules/plumbing/plumbers/vatgrower.dm
diff --git a/code/modules/point/point.dm b/code/modules/point/point.dm
index 3a3d97e25655b..6e61b1154d59c 100644
--- a/code/modules/point/point.dm
+++ b/code/modules/point/point.dm
@@ -77,7 +77,7 @@
abstract_move(get_turf(src))
pixel_x = old_loc.pixel_x
pixel_y = old_loc.pixel_y
- invisibility = set_invis
+ SetInvisibility(set_invis)
#undef POINT_TIME
diff --git a/code/modules/power/apc/apc_attack.dm b/code/modules/power/apc/apc_attack.dm
index 3ff3d640c6240..aaa63c05d853a 100644
--- a/code/modules/power/apc/apc_attack.dm
+++ b/code/modules/power/apc/apc_attack.dm
@@ -260,10 +260,6 @@
user.visible_message(span_notice("[user] removes \the [cell] from [src]!"))
balloon_alert(user, "cell removed")
user.put_in_hands(cell)
- cell.update_appearance()
- cell = null
- charging = APC_NOT_CHARGING
- update_appearance()
return
if((machine_stat & MAINT) && !opened) //no board; no interface
return
diff --git a/code/modules/power/apc/apc_main.dm b/code/modules/power/apc/apc_main.dm
index 9bfdaf1e249b8..3f5a7590c39a6 100644
--- a/code/modules/power/apc/apc_main.dm
+++ b/code/modules/power/apc/apc_main.dm
@@ -264,6 +264,7 @@
/obj/machinery/power/apc/Exited(atom/movable/gone, direction)
. = ..()
if(gone == cell)
+ cell.update_appearance()
cell = null
charging = APC_NOT_CHARGING
update_appearance()
diff --git a/code/modules/power/rtg.dm b/code/modules/power/rtg.dm
index 974c2e6673794..af48e9c5944f8 100644
--- a/code/modules/power/rtg.dm
+++ b/code/modules/power/rtg.dm
@@ -71,7 +71,7 @@
visible_message(span_danger("\The [src] lets out a shower of sparks as it starts to lose stability!"),\
span_hear("You hear a loud electrical crack!"))
playsound(src.loc, 'sound/magic/lightningshock.ogg', 100, TRUE, extrarange = 5)
- tesla_zap(src, 5, power_gen * 20)
+ tesla_zap(source = src, zap_range = 5, power = power_gen * 20)
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(explosion), src, 2, 3, 4, null, 8), 10 SECONDS) // Not a normal explosion.
/obj/machinery/power/rtg/abductor/bullet_act(obj/projectile/Proj)
diff --git a/code/modules/power/singularity/containment_field.dm b/code/modules/power/singularity/containment_field.dm
index 52be991cb69d9..d9fc0671e31d7 100644
--- a/code/modules/power/singularity/containment_field.dm
+++ b/code/modules/power/singularity/containment_field.dm
@@ -123,7 +123,7 @@
if(isliving(mover))
shock(mover)
return
- if(ismachinery(mover) || isstructure(mover) || ismecha(mover))
+ if(ismachinery(mover) || isstructure(mover) || isvehicle(mover))
bump_field(mover)
return
diff --git a/code/modules/power/singularity/narsie.dm b/code/modules/power/singularity/narsie.dm
index be2cccbcde43c..b464a406b2032 100644
--- a/code/modules/power/singularity/narsie.dm
+++ b/code/modules/power/singularity/narsie.dm
@@ -58,7 +58,13 @@
var/area/area = get_area(src)
if(area)
var/mutable_appearance/alert_overlay = mutable_appearance('icons/effects/cult/effects.dmi', "ghostalertsie")
- notify_ghosts("Nar'Sie has risen in [area]. Reach out to the Geometer to be given a new shell for your soul.", source = src, alert_overlay = alert_overlay, action = NOTIFY_ATTACK)
+ notify_ghosts(
+ "Nar'Sie has risen in [area]. Reach out to the Geometer to be given a new shell for your soul.",
+ source = src,
+ alert_overlay = alert_overlay,
+ action = NOTIFY_PLAY,
+ )
+
narsie_spawn_animation()
GLOB.cult_narsie = src
@@ -218,21 +224,25 @@
///First crew last second win check and flufftext for [/proc/begin_the_end()]
/proc/narsie_end_begin_check()
if(QDELETED(GLOB.cult_narsie)) // uno
- priority_announce("Status report? We detected an anomaly, but it disappeared almost immediately.","Central Command Higher Dimensional Affairs", 'sound/misc/notice1.ogg')
+ priority_announce("Status report? We detected an anomaly, but it disappeared almost immediately.","[command_name()] Higher Dimensional Affairs", 'sound/misc/notice1.ogg')
GLOB.cult_narsie = null
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(cult_ending_helper), CULT_FAILURE_NARSIE_KILLED), 2 SECONDS)
return
- priority_announce("An acausal dimensional event has been detected in your sector. Event has been flagged EXTINCTION-CLASS. Directing all available assets toward simulating solutions. SOLUTION ETA: 60 SECONDS.","Central Command Higher Dimensional Affairs", 'sound/misc/airraid.ogg')
+ priority_announce(
+ text = "An acausal dimensional event has been detected in your sector. Event has been flagged EXTINCTION-CLASS. Directing all available assets toward simulating solutions. SOLUTION ETA: 60 SECONDS.",
+ title = "[command_name()] Higher Dimensional Affairs",
+ sound = 'sound/misc/airraid.ogg',
+ )
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(narsie_end_second_check)), 50 SECONDS)
///Second crew last second win check and flufftext for [/proc/begin_the_end()]
/proc/narsie_end_second_check()
if(QDELETED(GLOB.cult_narsie)) // dos
- priority_announce("Simulations aborted, sensors report that the acasual event is normalizing. Good work, crew.","Central Command Higher Dimensional Affairs", 'sound/misc/notice1.ogg')
+ priority_announce("Simulations aborted, sensors report that the acasual event is normalizing. Good work, crew.","[command_name()] Higher Dimensional Affairs", 'sound/misc/notice1.ogg')
GLOB.cult_narsie = null
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(cult_ending_helper), CULT_FAILURE_NARSIE_KILLED), 2 SECONDS)
return
- priority_announce("Simulations on acausal dimensional event complete. Deploying solution package now. Deployment ETA: ONE MINUTE. ","Central Command Higher Dimensional Affairs")
+ priority_announce("Simulations on acausal dimensional event complete. Deploying solution package now. Deployment ETA: ONE MINUTE. ","[command_name()] Higher Dimensional Affairs")
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(narsie_start_destroy_station)), 5 SECONDS)
///security level and shuttle lockdowns for [/proc/begin_the_end()]
@@ -245,7 +255,7 @@
///Third crew last second win check and flufftext for [/proc/begin_the_end()]
/proc/narsie_apocalypse()
if(QDELETED(GLOB.cult_narsie)) // tres
- priority_announce("Normalization detected! Abort the solution package!","Central Command Higher Dimensional Affairs", 'sound/misc/notice1.ogg')
+ priority_announce("Normalization detected! Abort the solution package!","[command_name()] Higher Dimensional Affairs", 'sound/misc/notice1.ogg')
SSshuttle.clearHostileEnvironment(GLOB.cult_narsie)
GLOB.cult_narsie = null
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(narsie_last_second_win)), 2 SECONDS)
diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/power/singularity/singularity.dm
index 0a797a2d7d681..fc2836a4e6dd4 100644
--- a/code/modules/power/singularity/singularity.dm
+++ b/code/modules/power/singularity/singularity.dm
@@ -80,10 +80,10 @@
ghost_notification_message,
source = src,
action = NOTIFY_ORBIT,
- flashwindow = FALSE,
+ notify_flags = NOTIFY_CATEGORY_DEFAULT,
ghost_sound = 'sound/machines/warning-buzzer.ogg',
header = ghost_notification_message,
- notify_volume = 75
+ notify_volume = 75,
)
@@ -267,6 +267,11 @@
new_consume_range = 5
dissipate = FALSE
+ if(temp_allowed_size == STAGE_SIX)
+ AddComponent(/datum/component/vision_hurting)
+ else
+ qdel(GetComponent(/datum/component/vision_hurting))
+
var/datum/component/singularity/resolved_singularity = singularity_component.resolve()
if (!isnull(resolved_singularity))
resolved_singularity.consume_range = new_consume_range
diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm
index a51afd28a8b97..fc822877910ee 100644
--- a/code/modules/power/supermatter/supermatter.dm
+++ b/code/modules/power/supermatter/supermatter.dm
@@ -203,7 +203,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
RegisterSignal(src, COMSIG_ATOM_BSA_BEAM, PROC_REF(force_delam))
RegisterSignal(src, COMSIG_ATOM_TIMESTOP_FREEZE, PROC_REF(time_frozen))
RegisterSignal(src, COMSIG_ATOM_TIMESTOP_UNFREEZE, PROC_REF(time_unfrozen))
-
+ RegisterSignal(src, COMSIG_ATOM_PRE_BULLET_ACT, PROC_REF(eat_bullets))
var/static/list/loc_connections = list(
COMSIG_TURF_INDUSTRIAL_LIFT_ENTER = PROC_REF(tram_contents_consume),
)
@@ -534,7 +534,12 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
final_countdown = TRUE
- notify_ghosts("[src] has begun the delamination process!", source = src, header = "Meltdown Incoming")
+ notify_ghosts(
+ "[src] has begun the delamination process!",
+ source = src,
+ header = "Meltdown Incoming",
+ notify_flags = NOTIFY_CATEGORY_DEFAULT,
+ )
var/datum/sm_delam/last_delamination_strategy = delamination_strategy
var/list/count_down_messages = delamination_strategy.count_down_messages()
diff --git a/code/modules/power/supermatter/supermatter_delamination/cascade_delam_objects.dm b/code/modules/power/supermatter/supermatter_delamination/cascade_delam_objects.dm
index 34a7d6f420aef..d19d17452e221 100644
--- a/code/modules/power/supermatter/supermatter_delamination/cascade_delam_objects.dm
+++ b/code/modules/power/supermatter/supermatter_delamination/cascade_delam_objects.dm
@@ -36,6 +36,9 @@
if(our_turf)
our_turf.opacity = FALSE
+ // Ideally this'd be part of the SM component, but the SM itself snowflakes bullets (emitters are bullets).
+ RegisterSignal(src, COMSIG_ATOM_PRE_BULLET_ACT, PROC_REF(eat_bullets))
+
/obj/crystal_mass/process()
if(!COOLDOWN_FINISHED(src, sm_wall_cooldown))
@@ -70,9 +73,18 @@
new /obj/crystal_mass(next_turf, get_dir(next_turf, src))
-/obj/crystal_mass/bullet_act(obj/projectile/projectile)
- visible_message(span_notice("[src] is unscathed!"))
- return BULLET_ACT_HIT
+/obj/crystal_mass/proc/eat_bullets(datum/source, obj/projectile/hitting_projectile)
+ SIGNAL_HANDLER
+
+ visible_message(
+ span_warning("[hitting_projectile] flies into [src] with a loud crack, before rapidly flashing into ash."),
+ null,
+ span_hear("You hear a loud crack as you are washed with a wave of heat."),
+ )
+
+ playsound(src, 'sound/effects/supermatter.ogg', 50, TRUE)
+ qdel(hitting_projectile)
+ return COMPONENT_BULLET_BLOCKED
/obj/crystal_mass/singularity_act()
return
@@ -166,4 +178,3 @@
span_hear("You hear a loud crack as a small distortion passes through you."))
qdel(consumed_object)
-
diff --git a/code/modules/power/supermatter/supermatter_delamination/delamination_effects.dm b/code/modules/power/supermatter/supermatter_delamination/delamination_effects.dm
index 25283c8d09ae9..a6c3f171b61af 100644
--- a/code/modules/power/supermatter/supermatter_delamination/delamination_effects.dm
+++ b/code/modules/power/supermatter/supermatter_delamination/delamination_effects.dm
@@ -137,8 +137,13 @@
// say goodbye to that shuttle of yours
if(SSshuttle.emergency.mode != SHUTTLE_ESCAPE)
- priority_announce("Fatal error occurred in emergency shuttle uplink during transit. Unable to reestablish connection.",
- "Emergency Shuttle Uplink Alert", 'sound/misc/announce_dig.ogg')
+ priority_announce(
+ text = "Fatal error occurred in emergency shuttle uplink during transit. Unable to reestablish connection.",
+ title = "Shuttle Failure",
+ sound = 'sound/misc/announce_dig.ogg',
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "grey",
+ )
else
// except if you are on it already, then you are safe c:
minor_announce("ERROR: Corruption detected in navigation protocols. Connection with Transponder #XCC-P5831-ES13 lost. \
diff --git a/code/modules/power/supermatter/supermatter_hit_procs.dm b/code/modules/power/supermatter/supermatter_hit_procs.dm
index 452b37e054100..6f01b5ff7e3f4 100644
--- a/code/modules/power/supermatter/supermatter_hit_procs.dm
+++ b/code/modules/power/supermatter/supermatter_hit_procs.dm
@@ -5,16 +5,20 @@
for(var/atom/thing_to_consume as anything in tram_contents)
Bumped(thing_to_consume)
-/obj/machinery/power/supermatter_crystal/bullet_act(obj/projectile/projectile)
+/obj/machinery/power/supermatter_crystal/proc/eat_bullets(datum/source, obj/projectile/projectile)
+ SIGNAL_HANDLER
+
var/turf/local_turf = loc
+ if(!istype(local_turf))
+ return NONE
+
var/kiss_power = 0
switch(projectile.type)
if(/obj/projectile/kiss)
kiss_power = 60
if(/obj/projectile/kiss/death)
kiss_power = 20000
- if(!istype(local_turf))
- return FALSE
+
if(!istype(projectile.firer, /obj/machinery/power/emitter))
investigate_log("has been hit by [projectile] fired by [key_name(projectile.firer)]", INVESTIGATE_ENGINE)
if(projectile.armor_flag != BULLET || kiss_power)
@@ -29,7 +33,10 @@
var/damage_to_be = damage + external_damage_immediate * clamp((emergency_point - damage) / emergency_point, 0, 1)
if(damage_to_be > danger_point)
visible_message(span_notice("[src] compresses under stress, resisting further impacts!"))
- return BULLET_ACT_HIT
+ playsound(src, 'sound/effects/supermatter.ogg', 50, TRUE)
+
+ qdel(projectile)
+ return COMPONENT_BULLET_BLOCKED
/obj/machinery/power/supermatter_crystal/singularity_act()
var/gain = 100
diff --git a/code/modules/power/tesla/coil.dm b/code/modules/power/tesla/coil.dm
index 3cf040b76ef78..972c98d4862c4 100644
--- a/code/modules/power/tesla/coil.dm
+++ b/code/modules/power/tesla/coil.dm
@@ -116,7 +116,7 @@
power = min(surplus(), power) //Take the smaller of the two
add_load(power)
playsound(src.loc, 'sound/magic/lightningshock.ogg', zap_sound_volume, TRUE, zap_sound_range)
- tesla_zap(src, 10, power, zap_flags)
+ tesla_zap(source = src, zap_range = 10, power = power, cutoff = 1e3, zap_flags = zap_flags)
zap_buckle_check(power)
/obj/machinery/power/energy_accumulator/grounding_rod
diff --git a/code/modules/power/tesla/energy_ball.dm b/code/modules/power/tesla/energy_ball.dm
index 609a8375dac45..2b625b36d5bf7 100644
--- a/code/modules/power/tesla/energy_ball.dm
+++ b/code/modules/power/tesla/energy_ball.dm
@@ -76,7 +76,7 @@
pixel_y = 0
shocked_things.Cut(1, shocked_things.len / 1.3)
var/list/shocking_info = list()
- tesla_zap(src, 3, TESLA_DEFAULT_POWER, shocked_targets = shocking_info)
+ tesla_zap(source = src, zap_range = 3, power = TESLA_DEFAULT_POWER, shocked_targets = shocking_info)
pixel_x = -32
pixel_y = -32
@@ -84,7 +84,7 @@
var/range = rand(1, clamp(orbiting_balls.len, 2, 3))
var/list/temp_shock = list()
//We zap off the main ball instead of ourselves to make things looks proper
- tesla_zap(src, range, TESLA_MINI_POWER/7*range, shocked_targets = temp_shock)
+ tesla_zap(source = src, zap_range = range, power = TESLA_MINI_POWER / 7 * range, shocked_targets = temp_shock)
shocking_info += temp_shock
shocked_things += shocking_info
@@ -199,13 +199,13 @@
C.investigate_log("has been dusted by an energy ball.", INVESTIGATE_DEATHS)
C.dust()
-/proc/tesla_zap(atom/source, zap_range = 3, power, zap_flags = ZAP_DEFAULT_FLAGS, list/shocked_targets = list())
+/proc/tesla_zap(atom/source, zap_range = 3, power, cutoff = 4e5, zap_flags = ZAP_DEFAULT_FLAGS, list/shocked_targets = list())
if(QDELETED(source))
return
if(!(zap_flags & ZAP_ALLOW_DUPLICATES))
LAZYSET(shocked_targets, source, TRUE) //I don't want no null refs in my list yeah?
. = source.dir
- if(power < 4e5)
+ if(power < cutoff)
return
/*
@@ -334,7 +334,7 @@
var/mob/living/closest_mob = closest_atom
ADD_TRAIT(closest_mob, TRAIT_BEING_SHOCKED, WAS_SHOCKED)
addtimer(TRAIT_CALLBACK_REMOVE(closest_mob, TRAIT_BEING_SHOCKED, WAS_SHOCKED), 1 SECONDS)
- var/shock_damage = (zap_flags & ZAP_MOB_DAMAGE) ? (min(round(power/2.4e5), 90) + rand(-5, 5)) : 0
+ var/shock_damage = (zap_flags & ZAP_MOB_DAMAGE) ? (min(round(power / 600), 90) + rand(-5, 5)) : 0
closest_mob.electrocute_act(shock_damage, source, 1, SHOCK_TESLA | ((zap_flags & ZAP_MOB_STUN) ? NONE : SHOCK_NOSTUN))
if(issilicon(closest_mob))
var/mob/living/silicon/S = closest_mob
@@ -350,11 +350,11 @@
if(prob(20))//I know I know
var/list/shocked_copy = shocked_targets.Copy()
- tesla_zap(closest_atom, next_range, power * 0.5, zap_flags, shocked_copy)//Normally I'd copy here so grounding rods work properly, but it fucks with movement
- tesla_zap(closest_atom, next_range, power * 0.5, zap_flags, shocked_targets)
+ tesla_zap(source = closest_atom, zap_range = next_range, power = power * 0.5, cutoff = cutoff, zap_flags = zap_flags, shocked_targets = shocked_copy)
+ tesla_zap(source = closest_atom, zap_range = next_range, power = power * 0.5, cutoff = cutoff, zap_flags = zap_flags, shocked_targets = shocked_targets)
shocked_targets += shocked_copy
else
- tesla_zap(closest_atom, next_range, power, zap_flags, shocked_targets)
+ tesla_zap(source = closest_atom, zap_range = next_range, power = power, cutoff = cutoff, zap_flags = zap_flags, shocked_targets = shocked_targets)
#undef BIKE
#undef COIL
diff --git a/code/modules/procedural_mapping/mapGenerator.dm b/code/modules/procedural_mapping/mapGenerator.dm
index 4a79ad4c3059a..8408e1d97b7a9 100644
--- a/code/modules/procedural_mapping/mapGenerator.dm
+++ b/code/modules/procedural_mapping/mapGenerator.dm
@@ -1,10 +1,12 @@
+///This type is responsible for any map generation behavior that is done in areas, override this to allow for
+///area-specific map generation. This generation is ran by areas in initialize.
/datum/map_generator
- //Map information
- var/list/map = list()
+ ///Map information, such as the start and end turfs of the map generation.
+ var/list/turf/map = list()
- //mapGeneratorModule information
- var/list/modules = list()
+ ///The map generator modules that we will generate and sync to.
+ var/list/datum/map_generator_module/modules = list()
var/buildmode_name = "Undocumented"
@@ -14,6 +16,18 @@
buildmode_name = copytext_char("[type]", 20) // / d a t u m / m a p g e n e r a t o r / = 20 characters.
initialiseModules()
+/datum/map_generator/Destroy(force, ...)
+ . = ..()
+ QDEL_LIST(modules)
+
+///This proc will be ran by areas on Initialize, and provides the areas turfs as argument to allow for generation.
+/datum/map_generator/proc/generate_terrain(list/turfs, area/generate_in)
+ return
+
+/// Populate terrain with flora, fauna, features and basically everything that isn't a turf.
+/datum/map_generator/proc/populate_terrain(list/turfs, area/generate_in)
+ return
+
//Defines the region the map represents, sets map
//Returns the map
/datum/map_generator/proc/defineRegion(turf/Start, turf/End, replace = 0)
@@ -22,7 +36,7 @@
if(replace)
undefineRegion()
- map |= block(Start,End)
+ map |= block(Start, End)
return map
@@ -56,7 +70,7 @@
theRadius = max(radius/max((2*abs(sphereMagic-i)),1),1)
- map |= circle_range(locate(centerX,centerY,i),theRadius)
+ map |= circle_range(locate(centerX, centerY, i),theRadius)
return map
@@ -87,7 +101,7 @@
syncModules()
if(!modules || !modules.len)
return
- for(var/datum/map_generator_module/mod in modules)
+ for(var/datum/map_generator_module/mod as anything in modules)
INVOKE_ASYNC(mod, TYPE_PROC_REF(/datum/map_generator_module, generate))
@@ -98,7 +112,7 @@
syncModules()
if(!modules || !modules.len)
return
- for(var/datum/map_generator_module/mod in modules)
+ for(var/datum/map_generator_module/mod as anything in modules)
INVOKE_ASYNC(mod, TYPE_PROC_REF(/datum/map_generator_module, place), T)
@@ -113,7 +127,7 @@
//Sync mapGeneratorModule(s) to mapGenerator
/datum/map_generator/proc/syncModules()
- for(var/datum/map_generator_module/mod in modules)
+ for(var/datum/map_generator_module/mod as anything in modules)
mod.sync(src)
@@ -127,12 +141,12 @@
set category = "Debug"
var/datum/map_generator/nature/N = new()
- var/startInput = input(usr,"Start turf of Map, (X;Y;Z)", "Map Gen Settings", "1;1;1") as text|null
+ var/startInput = input(usr, "Start turf of Map, (X;Y;Z)", "Map Gen Settings", "1;1;1") as text|null
if (isnull(startInput))
return
- var/endInput = input(usr,"End turf of Map (X;Y;Z)", "Map Gen Settings", "[world.maxx];[world.maxy];[mob ? mob.z : 1]") as text|null
+ var/endInput = input(usr, "End turf of Map (X;Y;Z)", "Map Gen Settings", "[world.maxx];[world.maxy];[mob ? mob.z : 1]") as text|null
if (isnull(endInput))
return
@@ -158,9 +172,18 @@
to_chat(src, "End Coords: [endCoords[1]] - [endCoords[2]] - [endCoords[3]]")
return
- var/list/clusters = list("None"=CLUSTER_CHECK_NONE,"All"=CLUSTER_CHECK_ALL,"Sames"=CLUSTER_CHECK_SAMES,"Differents"=CLUSTER_CHECK_DIFFERENTS, \
- "Same turfs"=CLUSTER_CHECK_SAME_TURFS, "Same atoms"=CLUSTER_CHECK_SAME_ATOMS, "Different turfs"=CLUSTER_CHECK_DIFFERENT_TURFS, \
- "Different atoms"=CLUSTER_CHECK_DIFFERENT_ATOMS, "All turfs"=CLUSTER_CHECK_ALL_TURFS,"All atoms"=CLUSTER_CHECK_ALL_ATOMS)
+ var/static/list/clusters = list(
+ "None" = CLUSTER_CHECK_NONE,
+ "All" = CLUSTER_CHECK_ALL,
+ "Sames" = CLUSTER_CHECK_SAMES,
+ "Differents" = CLUSTER_CHECK_DIFFERENTS,
+ "Same turfs" = CLUSTER_CHECK_SAME_TURFS,
+ "Same atoms" = CLUSTER_CHECK_SAME_ATOMS,
+ "Different turfs" = CLUSTER_CHECK_DIFFERENT_TURFS,
+ "Different atoms" = CLUSTER_CHECK_DIFFERENT_ATOMS,
+ "All turfs" = CLUSTER_CHECK_ALL_TURFS,
+ "All atoms" = CLUSTER_CHECK_ALL_ATOMS,
+ )
var/moduleClusters = input("Cluster Flags (Cancel to leave unchanged from defaults)","Map Gen Settings") as null|anything in clusters
//null for default
@@ -175,7 +198,7 @@
theCluster = CLUSTER_CHECK_NONE
if(theCluster)
- for(var/datum/map_generator_module/M in N.modules)
+ for(var/datum/map_generator_module/M as anything in N.modules)
M.clusterCheckFlags = theCluster
diff --git a/code/modules/procedural_mapping/mapGeneratorModule.dm b/code/modules/procedural_mapping/mapGeneratorModule.dm
index d5742fdb85ab0..aa3f0f0e93cc8 100644
--- a/code/modules/procedural_mapping/mapGeneratorModule.dm
+++ b/code/modules/procedural_mapping/mapGeneratorModule.dm
@@ -8,6 +8,9 @@
var/clusterCheckFlags = CLUSTER_CHECK_SAME_ATOMS
var/allowAtomsOnSpace = FALSE
+/datum/map_generator_module/Destroy(force, ...)
+ mother = null
+ return ..()
//Syncs the module up with its mother
/datum/map_generator_module/proc/sync(datum/map_generator/mum)
diff --git a/code/modules/procedural_mapping/mapGenerators/syndicate.dm b/code/modules/procedural_mapping/mapGenerators/syndicate.dm
index b9dc00e13642d..74d2d153d06a5 100644
--- a/code/modules/procedural_mapping/mapGenerators/syndicate.dm
+++ b/code/modules/procedural_mapping/mapGenerators/syndicate.dm
@@ -17,10 +17,12 @@
/obj/structure/closet/syndicate = 25, /obj/machinery/suit_storage_unit/syndicate = 15)
/datum/map_generator_module/splatter_layer/syndie_mobs
- spawnableAtoms = list(/mob/living/basic/syndicate = 30, \
- /mob/living/basic/syndicate/melee = 20, \
- /mob/living/basic/syndicate/ranged = 20, \
- /mob/living/basic/viscerator = 30)
+ spawnableAtoms = list(
+ /mob/living/basic/trooper/syndicate = 30,
+ /mob/living/basic/trooper/syndicate/melee = 20,
+ /mob/living/basic/trooper/syndicate/ranged = 20,
+ /mob/living/basic/viscerator = 30
+ )
spawnableTurfs = list()
// Generators
diff --git a/code/modules/projectiles/ammunition/ballistic/shotgun.dm b/code/modules/projectiles/ammunition/ballistic/shotgun.dm
index da5d4161286e0..078f4bba1c4fd 100644
--- a/code/modules/projectiles/ammunition/ballistic/shotgun.dm
+++ b/code/modules/projectiles/ammunition/ballistic/shotgun.dm
@@ -121,14 +121,23 @@
pellets = 4
variance = 35
-/obj/item/ammo_casing/shotgun/laserslug
+/obj/item/ammo_casing/shotgun/scatterlaser
name = "scatter laser shell"
desc = "An advanced shotgun shell that uses a micro laser to replicate the effects of a scatter laser weapon in a ballistic package."
icon_state = "lshell"
- projectile_type = /obj/projectile/beam/weak
+ projectile_type = /obj/projectile/beam/scatter
pellets = 6
variance = 35
+/obj/item/ammo_casing/shotgun/scatterlaser/emp_act(severity)
+ . = ..()
+ if(isnull(loaded_projectile) || !prob(40/severity))
+ return
+ name = "malfunctioning laser shell"
+ desc = "An advanced shotgun shell that uses a micro laser to replicate the effects of a scatter laser weapon in a ballistic package. The capacitor powering this assembly appears to be smoking."
+ projectile_type = /obj/projectile/beam/scatter/pathetic
+ loaded_projectile = new projectile_type(src)
+
/obj/item/ammo_casing/shotgun/techshell
name = "unloaded technological shell"
desc = "A high-tech shotgun shell which can be loaded with materials to produce unique effects."
@@ -138,7 +147,7 @@
/obj/item/ammo_casing/shotgun/techshell/Initialize(mapload)
. = ..()
- var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/meteorslug, /datum/crafting_recipe/pulseslug, /datum/crafting_recipe/dragonsbreath, /datum/crafting_recipe/ionslug, /datum/crafting_recipe/laserslug)
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/meteorslug, /datum/crafting_recipe/pulseslug, /datum/crafting_recipe/dragonsbreath, /datum/crafting_recipe/ionslug)
AddComponent(
/datum/component/slapcrafting,\
diff --git a/code/modules/projectiles/ammunition/energy/_energy.dm b/code/modules/projectiles/ammunition/energy/_energy.dm
index d90da88db9457..877dc7784d02d 100644
--- a/code/modules/projectiles/ammunition/energy/_energy.dm
+++ b/code/modules/projectiles/ammunition/energy/_energy.dm
@@ -7,4 +7,4 @@
var/e_cost = LASER_SHOTS(10, STANDARD_CELL_CHARGE) //The amount of energy a cell needs to expend to create this shot.
var/select_name = CALIBER_ENERGY
fire_sound = 'sound/weapons/laser.ogg'
- firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/energy
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/red
diff --git a/code/modules/projectiles/ammunition/energy/ebow.dm b/code/modules/projectiles/ammunition/energy/ebow.dm
index 535ea126576cb..a6a928c25095d 100644
--- a/code/modules/projectiles/ammunition/energy/ebow.dm
+++ b/code/modules/projectiles/ammunition/energy/ebow.dm
@@ -3,6 +3,7 @@
select_name = "bolt"
e_cost = LASER_SHOTS(1, STANDARD_CELL_CHARGE * 0.5)
fire_sound = 'sound/weapons/gun/general/heavy_shot_suppressed.ogg' // Even for non-suppressed crossbows, this is the most appropriate sound
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect
/obj/item/ammo_casing/energy/bolt/halloween
projectile_type = /obj/projectile/energy/bolt/halloween
diff --git a/code/modules/projectiles/ammunition/energy/gravity.dm b/code/modules/projectiles/ammunition/energy/gravity.dm
index fe73ad26883ed..6ad3a776475ce 100644
--- a/code/modules/projectiles/ammunition/energy/gravity.dm
+++ b/code/modules/projectiles/ammunition/energy/gravity.dm
@@ -4,6 +4,7 @@
select_name = "gravity"
delay = 50
var/obj/item/gun/energy/gravity_gun/gun
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect
/obj/item/ammo_casing/energy/gravity/Initialize(mapload)
if(istype(loc,/obj/item/gun/energy/gravity_gun))
diff --git a/code/modules/projectiles/ammunition/energy/laser.dm b/code/modules/projectiles/ammunition/energy/laser.dm
index c47181688a482..6eb2d238bb061 100644
--- a/code/modules/projectiles/ammunition/energy/laser.dm
+++ b/code/modules/projectiles/ammunition/energy/laser.dm
@@ -78,6 +78,7 @@
pellets = 3
variance = 15
harmful = FALSE
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/blue
/obj/item/ammo_casing/energy/laser/heavy
projectile_type = /obj/projectile/beam/laser/heavylaser
@@ -89,6 +90,7 @@
e_cost = LASER_SHOTS(200, STANDARD_CELL_CHARGE * 40)
select_name = "DESTROY"
fire_sound = 'sound/weapons/pulse.ogg'
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/blue
/obj/item/ammo_casing/energy/laser/bluetag
projectile_type = /obj/projectile/beam/lasertag/bluetag
@@ -134,6 +136,7 @@
/obj/item/ammo_casing/energy/nanite/cryo
projectile_type = /obj/projectile/energy/cryo
select_name = "cryo"
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/blue
///not exactly an energy ammo casing, but it's used by the laser gatling.
/obj/item/ammo_casing/laser
@@ -145,7 +148,7 @@
slot_flags = null
projectile_type = /obj/projectile/beam
fire_sound = 'sound/weapons/laser.ogg'
- firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/energy
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/red
/obj/item/ammo_casing/laser/Initialize(mapload)
. = ..()
diff --git a/code/modules/projectiles/ammunition/energy/portal.dm b/code/modules/projectiles/ammunition/energy/portal.dm
index 0ef63491f11d4..787f2e4eac76c 100644
--- a/code/modules/projectiles/ammunition/energy/portal.dm
+++ b/code/modules/projectiles/ammunition/energy/portal.dm
@@ -6,10 +6,12 @@
select_name = "blue"
//Weakref to the gun that shot us
var/datum/weakref/gun
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/blue
/obj/item/ammo_casing/energy/wormhole/orange
projectile_type = /obj/projectile/beam/wormhole/orange
select_name = "orange"
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/red
/obj/item/ammo_casing/energy/wormhole/Initialize(mapload, obj/item/gun/energy/wormhole_projector/wh)
. = ..()
diff --git a/code/modules/projectiles/ammunition/energy/special.dm b/code/modules/projectiles/ammunition/energy/special.dm
index f2fc274ee8a10..9a733dbc47a84 100644
--- a/code/modules/projectiles/ammunition/energy/special.dm
+++ b/code/modules/projectiles/ammunition/energy/special.dm
@@ -2,6 +2,7 @@
projectile_type = /obj/projectile/ion
select_name = "ion"
fire_sound = 'sound/weapons/ionrifle.ogg'
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/blue
/obj/item/ammo_casing/energy/ion/hos
projectile_type = /obj/projectile/ion/weak
@@ -20,15 +21,15 @@
harmful = FALSE
/obj/item/ammo_casing/energy/flora/yield
- projectile_type = /obj/projectile/energy/florayield
+ projectile_type = /obj/projectile/energy/flora/yield
select_name = "yield"
/obj/item/ammo_casing/energy/flora/mut
- projectile_type = /obj/projectile/energy/floramut
+ projectile_type = /obj/projectile/energy/flora/mut
select_name = "mutation"
/obj/item/ammo_casing/energy/flora/revolution
- projectile_type = /obj/projectile/energy/florarevolution
+ projectile_type = /obj/projectile/energy/flora/evolution
select_name = "revolution"
e_cost = LASER_SHOTS(4, STANDARD_CELL_CHARGE)
@@ -37,10 +38,12 @@
select_name = "freeze"
e_cost = LASER_SHOTS(40, STANDARD_CELL_CHARGE * 10)
fire_sound = 'sound/weapons/pulse3.ogg'
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/blue
/obj/item/ammo_casing/energy/temp/hot
projectile_type = /obj/projectile/temp/hot
select_name = "bake"
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/red
/obj/item/ammo_casing/energy/meteor
projectile_type = /obj/projectile/meteor
@@ -63,6 +66,7 @@
e_cost = LASER_SHOTS(33, STANDARD_CELL_CHARGE)
select_name = "shock"
projectile_type = /obj/projectile/energy/tesla_cannon
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/blue
/obj/item/ammo_casing/energy/shrink
projectile_type = /obj/projectile/beam/shrink
diff --git a/code/modules/projectiles/ammunition/energy/stun.dm b/code/modules/projectiles/ammunition/energy/stun.dm
index ee2f9fa17eef5..a7c3f61ee750a 100644
--- a/code/modules/projectiles/ammunition/energy/stun.dm
+++ b/code/modules/projectiles/ammunition/energy/stun.dm
@@ -4,6 +4,7 @@
fire_sound = 'sound/weapons/taser.ogg'
e_cost = LASER_SHOTS(5, STANDARD_CELL_CHARGE)
harmful = FALSE
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect
/obj/item/ammo_casing/energy/electrode/spec
e_cost = LASER_SHOTS(10, STANDARD_CELL_CHARGE)
@@ -21,6 +22,12 @@
e_cost = LASER_SHOTS(20, STANDARD_CELL_CHARGE)
fire_sound = 'sound/weapons/taser2.ogg'
harmful = FALSE
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/blue
+
+/obj/item/ammo_casing/energy/disabler/smg
+ projectile_type = /obj/projectile/beam/disabler/weak
+ e_cost = LASER_SHOTS(40, STANDARD_CELL_CHARGE)
+ fire_sound = 'sound/weapons/taser3.ogg'
/obj/item/ammo_casing/energy/disabler/hos
e_cost = LASER_SHOTS(20, STANDARD_CELL_CHARGE * 1.2)
diff --git a/code/modules/projectiles/boxes_magazines/_box_magazine.dm b/code/modules/projectiles/boxes_magazines/_box_magazine.dm
index b018c06317e40..f6555548a58e6 100644
--- a/code/modules/projectiles/boxes_magazines/_box_magazine.dm
+++ b/code/modules/projectiles/boxes_magazines/_box_magazine.dm
@@ -19,6 +19,8 @@
var/list/stored_ammo = list()
///type that the magazine will be searching for, rejects if not a subtype of
var/ammo_type = /obj/item/ammo_casing
+ /// wording used for individual units of ammo, e.g. cartridges (regular ammo), shells (shotgun shells)
+ var/casing_phrasing = "cartridge"
///maximum amount of ammo in the magazine
var/max_ammo = 7
///Controls how sprites are updated for the ammo box; see defines in combat.dm: AMMO_BOX_ONE_SPRITE; AMMO_BOX_PER_BULLET; AMMO_BOX_FULL_EMPTY
@@ -66,7 +68,7 @@
var/list/readout = list()
if(caliber && max_ammo) // Text references a 'magazine' as only magazines generally have the caliber variable initialized
- readout += "Up to [span_warning("[max_ammo] [caliber] rounds")] can be found within this magazine. \
+ readout += "Up to [span_warning("[max_ammo] [caliber] [casing_phrasing]s")] can be found within this magazine. \
\nAccidentally discharging any of these projectiles may void your insurance contract."
var/obj/item/ammo_casing/mag_ammo = get_round(TRUE)
@@ -158,7 +160,7 @@
if(num_loaded)
if(!silent)
- to_chat(user, span_notice("You load [num_loaded] shell\s into \the [src]!"))
+ to_chat(user, span_notice("You load [num_loaded > 1 ? "[num_loaded] [casing_phrasing]s" : "a [casing_phrasing]"] into \the [src]!"))
playsound(src, 'sound/weapons/gun/general/mag_bullet_insert.ogg', 60, TRUE)
update_appearance()
@@ -173,13 +175,23 @@
if(!user.is_holding(src) || !user.put_in_hands(A)) //incase they're using TK
A.bounce_away(FALSE, NONE)
playsound(src, 'sound/weapons/gun/general/mag_bullet_insert.ogg', 60, TRUE)
- to_chat(user, span_notice("You remove a round from [src]!"))
+ to_chat(user, span_notice("You remove a [casing_phrasing] from [src]!"))
update_appearance()
+/obj/item/ammo_box/examine(mob/user)
+ . = ..()
+ var/top_round = get_round()
+ if(!top_round)
+ return
+ // this is kind of awkward phrasing, but it's the top/ready ammo in the box
+ // intended for people who have like three mislabeled magazines
+ . += span_notice("The [top_round] is ready in [src].")
+
+
/obj/item/ammo_box/update_desc(updates)
. = ..()
var/shells_left = LAZYLEN(stored_ammo)
- desc = "[initial(desc)] There [(shells_left == 1) ? "is" : "are"] [shells_left] shell\s left!"
+ desc = "[initial(desc)] There [(shells_left == 1) ? "is" : "are"] [shells_left] [casing_phrasing]\s left!"
/obj/item/ammo_box/update_icon_state()
. = ..()
diff --git a/code/modules/projectiles/boxes_magazines/external/shotgun.dm b/code/modules/projectiles/boxes_magazines/external/shotgun.dm
index 398df79771a3e..dbf071f6aee6c 100644
--- a/code/modules/projectiles/boxes_magazines/external/shotgun.dm
+++ b/code/modules/projectiles/boxes_magazines/external/shotgun.dm
@@ -6,6 +6,7 @@
ammo_type = /obj/item/ammo_casing/shotgun/buckshot
caliber = CALIBER_SHOTGUN
max_ammo = 8
+ casing_phrasing = "shell"
/obj/item/ammo_box/magazine/m12g/update_icon_state()
. = ..()
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index ba45baac6d3af..b8713fae26f70 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -211,8 +211,8 @@
/obj/item/gun/emp_act(severity)
. = ..()
if(!(. & EMP_PROTECT_CONTENTS))
- for(var/obj/O in contents)
- O.emp_act(severity)
+ for(var/obj/inside in contents)
+ inside.emp_act(severity)
/obj/item/gun/attack_self_secondary(mob/user, modifiers)
. = ..()
diff --git a/code/modules/projectiles/guns/ballistic/automatic.dm b/code/modules/projectiles/guns/ballistic/automatic.dm
index 3e286f6c58b18..70e2210a4e992 100644
--- a/code/modules/projectiles/guns/ballistic/automatic.dm
+++ b/code/modules/projectiles/guns/ballistic/automatic.dm
@@ -146,6 +146,17 @@
mag_display = TRUE
rack_sound = 'sound/weapons/gun/pistol/slide_lock.ogg'
+/**
+ * Weak uzi for syndicate chimps. It comes in a 4 TC kit.
+ * Roughly 9 damage per bullet every 0.2 seconds, equaling out to downing an opponent in a bit over a second, if they have no armor.
+ */
+/obj/item/gun/ballistic/automatic/mini_uzi/chimpgun
+ name = "\improper MONK-10"
+ desc = "Developed by Syndicate monkeys, for syndicate Monkeys. Despite the name, this weapon resembles an Uzi significantly more than a MAC-10. Uses 9mm rounds. There's a label on the other side of the gun that says \"Do what comes natural.\""
+ projectile_damage_multiplier = 0.4
+ projectile_wound_bonus = -25
+ pin = /obj/item/firing_pin/monkey
+
/obj/item/gun/ballistic/automatic/m90
name = "\improper M-90gl Carbine"
desc = "A three-round burst 5.56 toploading carbine, designated 'M-90gl'. Has an attached underbarrel grenade launcher."
@@ -218,10 +229,25 @@
bolt_type = BOLT_TYPE_OPEN
empty_indicator = TRUE
show_bolt_icon = FALSE
+ /// Rate of fire, set on initialize only
+ var/rof = 0.1 SECONDS
/obj/item/gun/ballistic/automatic/tommygun/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/automatic_fire, 0.1 SECONDS)
+ AddComponent(/datum/component/automatic_fire, rof)
+
+/**
+ * Weak tommygun for syndicate chimps. It comes in a 4 TC kit.
+ * Roughly 9 damage per bullet every 0.2 seconds, equaling out to downing an opponent in a bit over a second, if they have no armor.
+ */
+/obj/item/gun/ballistic/automatic/tommygun/chimpgun
+ name = "\improper Typewriter"
+ desc = "It was the best of times, it was the BLURST of times!? You stupid monkeys!"
+ fire_delay = 2
+ rof = 0.2 SECONDS
+ projectile_damage_multiplier = 0.4
+ projectile_wound_bonus = -25
+ pin = /obj/item/firing_pin/monkey
/obj/item/gun/ballistic/automatic/ar
name = "\improper NT-ARG 'Boarder'"
diff --git a/code/modules/projectiles/guns/ballistic/pistol.dm b/code/modules/projectiles/guns/ballistic/pistol.dm
index b247bd3c2d681..270e7edd93078 100644
--- a/code/modules/projectiles/guns/ballistic/pistol.dm
+++ b/code/modules/projectiles/guns/ballistic/pistol.dm
@@ -55,6 +55,18 @@
lock_back_sound = 'sound/weapons/gun/pistol/slide_lock.ogg'
bolt_drop_sound = 'sound/weapons/gun/pistol/slide_drop.ogg'
+/**
+ * Weak 1911 for syndicate chimps. It comes in a 4 TC kit.
+ * 15 damage every.. second? 7 shots to kill. Not fast.
+ */
+/obj/item/gun/ballistic/automatic/pistol/m1911/chimpgun
+ name = "\improper CH1M911"
+ desc = "For the monkey mafioso on-the-go. Uses .45 rounds and has the distinct smell of bananas."
+ projectile_damage_multiplier = 0.5
+ projectile_wound_bonus = -12
+ pin = /obj/item/firing_pin/monkey
+
+
/obj/item/gun/ballistic/automatic/pistol/m1911/no_mag
spawnwithmagazine = FALSE
diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm
index f87f473ae459b..2b943a5f6db63 100644
--- a/code/modules/projectiles/guns/ballistic/revolver.dm
+++ b/code/modules/projectiles/guns/ballistic/revolver.dm
@@ -139,11 +139,17 @@
desc = "A modernized 7 round revolver manufactured by Waffle Co. Uses .357 ammo."
icon_state = "revolversyndie"
+/obj/item/gun/ballistic/revolver/syndicate/nuclear
+ pin = /obj/item/firing_pin/implant/pindicate
+
/obj/item/gun/ballistic/revolver/syndicate/cowboy
desc = "A classic revolver, refurbished for modern use. Uses .357 ammo."
//There's already a cowboy sprite in there!
icon_state = "lucky"
+/obj/item/gun/ballistic/revolver/syndicate/cowboy/nuclear
+ pin = /obj/item/firing_pin/implant/pindicate
+
/obj/item/gun/ballistic/revolver/mateba
name = "\improper Unica 6 auto-revolver"
desc = "A retro high-powered autorevolver typically used by officers of the New Russia military. Uses .357 ammo."
diff --git a/code/modules/projectiles/guns/energy/beam_rifle.dm b/code/modules/projectiles/guns/energy/beam_rifle.dm
index a451b14de9aeb..1c5b025baea80 100644
--- a/code/modules/projectiles/guns/energy/beam_rifle.dm
+++ b/code/modules/projectiles/guns/energy/beam_rifle.dm
@@ -520,8 +520,8 @@
if(!QDELETED(target))
handle_impact(target)
-/obj/projectile/beam/beam_rifle/on_hit(atom/target, blocked = FALSE, piercing_hit = FALSE)
- handle_hit(target, piercing_hit)
+/obj/projectile/beam/beam_rifle/on_hit(atom/target, blocked = 0, pierce_hit)
+ handle_hit(target, pierce_hit)
return ..()
/obj/projectile/beam/beam_rifle/is_hostile_projectile()
@@ -567,7 +567,8 @@
/obj/projectile/beam/beam_rifle/hitscan/aiming_beam/prehit_pierce(atom/target)
return PROJECTILE_DELETE_WITHOUT_HITTING
-/obj/projectile/beam/beam_rifle/hitscan/aiming_beam/on_hit()
+/obj/projectile/beam/beam_rifle/hitscan/aiming_beam/on_hit(atom/target, blocked = 0, pierce_hit)
+ SHOULD_CALL_PARENT(FALSE) // This is some snowflake stuff so whatever
qdel(src)
return BULLET_ACT_BLOCK
diff --git a/code/modules/projectiles/guns/energy/dueling.dm b/code/modules/projectiles/guns/energy/dueling.dm
index 7b3929d7574cf..932117abb5bcd 100644
--- a/code/modules/projectiles/guns/energy/dueling.dm
+++ b/code/modules/projectiles/guns/energy/dueling.dm
@@ -327,7 +327,7 @@
if(DUEL_SETTING_C)
color = "blue"
-/obj/projectile/energy/duel/on_hit(atom/target, blocked)
+/obj/projectile/energy/duel/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
var/turf/T = get_turf(target)
var/obj/effect/temp_visual/dueling_chaff/C = locate() in T
diff --git a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
index a5dd70b17cb13..ec5a34379a5c3 100644
--- a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
+++ b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
@@ -209,7 +209,7 @@
strike_thing()
..()
-/obj/projectile/kinetic/on_hit(atom/target)
+/obj/projectile/kinetic/on_hit(atom/target, blocked = 0, pierce_hit)
strike_thing(target)
. = ..()
@@ -616,4 +616,3 @@
var/new_color = input(user,"","Choose Color",bolt_color) as color|null
bolt_color = new_color || bolt_color
-
diff --git a/code/modules/projectiles/guns/energy/laser.dm b/code/modules/projectiles/guns/energy/laser.dm
index 4c716f679efe3..27710eabed8c1 100644
--- a/code/modules/projectiles/guns/energy/laser.dm
+++ b/code/modules/projectiles/guns/energy/laser.dm
@@ -40,11 +40,10 @@
desc = "A modified laser gun which can shoot far faster, but each shot is far less damaging."
icon_state = "laser_carbine"
ammo_type = list(/obj/item/ammo_casing/energy/lasergun/carbine)
- var/allow_akimbo = FALSE
/obj/item/gun/energy/laser/carbine/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/automatic_fire, 0.15 SECONDS, allow_akimbo = allow_akimbo)
+ AddComponent(/datum/component/automatic_fire, 0.15 SECONDS, allow_akimbo = FALSE)
/obj/item/gun/energy/laser/carbine/practice
name = "practice laser carbine"
@@ -53,7 +52,6 @@
clumsy_check = FALSE
item_flags = NONE
gun_flags = NOT_A_REAL_GUN
- allow_akimbo = TRUE
/obj/item/gun/energy/laser/retro/old
name ="laser gun"
diff --git a/code/modules/projectiles/guns/energy/pulse.dm b/code/modules/projectiles/guns/energy/pulse.dm
index 4db8e626bda2e..582ee474f4db1 100644
--- a/code/modules/projectiles/guns/energy/pulse.dm
+++ b/code/modules/projectiles/guns/energy/pulse.dm
@@ -26,7 +26,13 @@
message_admins("A pulse rifle prize has been created at [ADMIN_VERBOSEJMP(T)]")
log_game("A pulse rifle prize has been created at [AREACOORD(T)]")
- notify_ghosts("Someone won a pulse rifle as a prize!", source = src, action = NOTIFY_ORBIT, header = "Pulse rifle prize")
+ notify_ghosts(
+ "Someone won a pulse rifle as a prize!",
+ source = src,
+ action = NOTIFY_ORBIT,
+ header = "Pulse rifle prize",
+ notify_flags = NOTIFY_CATEGORY_DEFAULT,
+ )
/obj/item/gun/energy/pulse/loyalpin
pin = /obj/item/firing_pin/implant/mindshield
diff --git a/code/modules/projectiles/guns/energy/stun.dm b/code/modules/projectiles/guns/energy/stun.dm
index 635570fbb15ce..e099176ddd018 100644
--- a/code/modules/projectiles/guns/energy/stun.dm
+++ b/code/modules/projectiles/guns/energy/stun.dm
@@ -40,6 +40,26 @@
overlay_x = 15, \
overlay_y = 10)
+/obj/item/gun/energy/disabler/smg
+ name = "disabler smg"
+ desc = "An automatic disabler variant, as opposed to the conventional model, boasts a higher ammunition capacity at the cost of slightly reduced beam effectiveness."
+ icon_state = "disabler_smg"
+ ammo_type = list(/obj/item/ammo_casing/energy/disabler/smg)
+ shaded_charge = 1
+
+/obj/item/gun/energy/disabler/smg/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/automatic_fire, 0.15 SECONDS, allow_akimbo = FALSE)
+
+/obj/item/gun/energy/disabler/add_seclight_point()
+ AddComponent(\
+ /datum/component/seclite_attachable, \
+ light_overlay_icon = 'icons/obj/weapons/guns/flashlights.dmi', \
+ light_overlay = "flight", \
+ overlay_x = 15, \
+ overlay_y = 13, \
+ )
+
/obj/item/gun/energy/disabler/cyborg
name = "cyborg disabler"
desc = "An integrated disabler that draws from a cyborg's power cell. This weapon contains a limiter to prevent the cyborg's power cell from overheating."
diff --git a/code/modules/projectiles/guns/special/hand_of_midas.dm b/code/modules/projectiles/guns/special/hand_of_midas.dm
index 9907352e3f505..69d3430cf9e73 100644
--- a/code/modules/projectiles/guns/special/hand_of_midas.dm
+++ b/code/modules/projectiles/guns/special/hand_of_midas.dm
@@ -111,7 +111,7 @@
..()
// Gives human targets Midas Blight.
-/obj/projectile/magic/midas_round/on_hit(atom/target)
+/obj/projectile/magic/midas_round/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(ishuman(target))
var/mob/living/carbon/human/my_guy = target
diff --git a/code/modules/projectiles/guns/special/meat_hook.dm b/code/modules/projectiles/guns/special/meat_hook.dm
index 4add1bb77d4cb..e96e51fbfefa3 100644
--- a/code/modules/projectiles/guns/special/meat_hook.dm
+++ b/code/modules/projectiles/guns/special/meat_hook.dm
@@ -29,7 +29,7 @@
desc = "A hook."
projectile_type = /obj/projectile/hook
caliber = CALIBER_HOOK
- firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/energy
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect
/obj/projectile/hook
name = "hook"
@@ -50,7 +50,7 @@
..()
//TODO: root the firer until the chain returns
-/obj/projectile/hook/on_hit(atom/target)
+/obj/projectile/hook/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(ismovable(target))
var/atom/movable/A = target
diff --git a/code/modules/projectiles/pins.dm b/code/modules/projectiles/pins.dm
index 6c2914b495dbb..45763feff043b 100644
--- a/code/modules/projectiles/pins.dm
+++ b/code/modules/projectiles/pins.dm
@@ -373,6 +373,17 @@
suit_requirement = /obj/item/clothing/suit/bluetag
tagcolor = "blue"
+/obj/item/firing_pin/monkey
+ name = "monkeylock firing pin"
+ desc = "This firing pin prevents non-monkeys from firing a gun."
+ fail_message = "not a monkey!"
+
+/obj/item/firing_pin/monkey/pin_auth(mob/living/user)
+ if(!is_simian(user))
+ playsound(get_turf(src), "sound/creatures/monkey/monkey_screech_[rand(1,7)].ogg", 75, TRUE)
+ return FALSE
+ return TRUE
+
/obj/item/firing_pin/Destroy()
if(gun)
gun.pin = null
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index 37d36b13f6ece..68dc20f2bfd0e 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -159,13 +159,15 @@
var/decayedRange //stores original range
var/reflect_range_decrease = 5 //amount of original range that falls off when reflecting, so it doesn't go forever
var/reflectable = NONE // Can it be reflected or not?
+
// Status effects applied on hit
- var/stun = 0
- var/knockdown = 0
- var/paralyze = 0
- var/immobilize = 0
- var/unconscious = 0
- var/eyeblur = 0
+ var/stun = 0 SECONDS
+ var/knockdown = 0 SECONDS
+ var/paralyze = 0 SECONDS
+ var/immobilize = 0 SECONDS
+ var/unconscious = 0 SECONDS
+ /// Seconds of blurry eyes applied on projectile hit
+ var/eyeblur = 0 SECONDS
/// Drowsiness applied on projectile hit
var/drowsy = 0 SECONDS
/// Jittering applied on projectile hit
@@ -192,6 +194,10 @@
var/hit_prone_targets = FALSE
///For what kind of brute wounds we're rolling for, if we're doing such a thing. Lasers obviously don't care since they do burn instead.
var/sharpness = NONE
+ ///How much we want to drop damage per tile as it travels through the air
+ var/damage_falloff_tile
+ ///How much we want to drop stamina damage (defined by the stamina variable) per tile as it travels through the air
+ var/stamina_falloff_tile
///How much we want to drop both wound_bonus and bare_wound_bonus (to a minimum of 0 for the latter) per tile, for falloff purposes
var/wound_falloff_tile
///How much we want to drop the embed_chance value, if we can embed, per tile, for falloff purposes
@@ -219,10 +225,18 @@
bare_wound_bonus = max(0, bare_wound_bonus + wound_falloff_tile)
if(embedding)
embedding["embed_chance"] += embed_falloff_tile
+ if(damage_falloff_tile && damage >= 0)
+ damage += damage_falloff_tile
+ if(stamina_falloff_tile && stamina >= 0)
+ stamina += stamina_falloff_tile
+
SEND_SIGNAL(src, COMSIG_PROJECTILE_RANGE)
if(range <= 0 && loc)
on_range()
+ if(damage_falloff_tile && damage <= 0 || stamina_falloff_tile && stamina <= 0)
+ on_range()
+
/obj/projectile/proc/on_range() //if we want there to be effects when they reach the end of their range
SEND_SIGNAL(src, COMSIG_PROJECTILE_RANGE_OUT)
qdel(src)
@@ -241,12 +255,22 @@
/**
* Called when the projectile hits something
*
- * @params
- * target - thing hit
- * blocked - percentage of hit blocked
- * pierce_hit - are we piercing through or regular hitting
+ * By default parent call will always return [BULLET_ACT_HIT] (unless qdeleted)
+ * so it is save to assume a successful hit in children (though not necessarily successfully damaged - it could've been blocked)
+ *
+ * Arguments
+ * * target - thing hit
+ * * blocked - percentage of hit blocked (0 to 100)
+ * * pierce_hit - boolean, are we piercing through or regular hitting
+ *
+ * Returns
+ * * Returns [BULLET_ACT_HIT] if we hit something. Default return value.
+ * * Returns [BULLET_ACT_BLOCK] if we were hit but sustained no effects (blocked it). Note, Being "blocked" =/= "blocked is 100".
+ * * Returns [BULLET_ACT_FORCE_PIERCE] to have the projectile keep going instead of "hitting", as if we were not hit at all.
*/
-/obj/projectile/proc/on_hit(atom/target, blocked = FALSE, pierce_hit)
+/obj/projectile/proc/on_hit(atom/target, blocked = 0, pierce_hit)
+ SHOULD_CALL_PARENT(TRUE)
+
// i know that this is probably more with wands and gun mods in mind, but it's a bit silly that the projectile on_hit signal doesn't ping the projectile itself.
// maybe we care what the projectile thinks! See about combining these via args some time when it's not 5AM
var/hit_limb_zone
@@ -258,7 +282,7 @@
SEND_SIGNAL(src, COMSIG_PROJECTILE_SELF_ON_HIT, firer, target, Angle, hit_limb_zone)
if(QDELETED(src)) // in case one of the above signals deleted the projectile for whatever reason
- return
+ return BULLET_ACT_BLOCK
var/turf/target_turf = get_turf(target)
var/hitx
@@ -293,8 +317,8 @@
if(blocked != 100) // not completely blocked
var/obj/item/bodypart/hit_bodypart = living_target.get_bodypart(hit_limb_zone)
- if (damage)
- if (living_target.blood_volume && damage_type == BRUTE && (isnull(hit_bodypart) || hit_bodypart.can_bleed()))
+ if (damage && damage_type == BRUTE)
+ if (living_target.blood_volume && (isnull(hit_bodypart) || hit_bodypart.can_bleed()))
var/splatter_dir = dir
if(starting)
splatter_dir = get_dir(starting, target_turf)
@@ -304,7 +328,7 @@
new /obj/effect/temp_visual/dir_setting/bloodsplatter(target_turf, splatter_dir)
if(prob(33))
living_target.add_splatter_floor(target_turf)
- else if (!isnull(hit_bodypart) && (hit_bodypart.biological_state & (BIO_METAL|BIO_WIRED)))
+ else if (hit_bodypart?.biological_state & (BIO_METAL|BIO_WIRED))
var/random_damage_mult = RANDOM_DECIMAL(0.85, 1.15) // SOMETIMES you can get more or less sparks
var/damage_dealt = ((damage / (1 - (blocked / 100))) * random_damage_mult)
@@ -331,7 +355,6 @@
span_userdanger("You're hit by \a [src][organ_hit_text]!"), null, COMBAT_MESSAGE_RANGE)
if(living_target.is_blind())
to_chat(living_target, span_userdanger("You feel something hit you[organ_hit_text]!"))
- living_target.on_hit(src)
var/reagent_note
if(reagents?.reagent_list)
@@ -1139,6 +1162,25 @@
/obj/projectile/proc/can_embed_into(atom/hit)
return embedding && shrapnel_type && iscarbon(hit) && !HAS_TRAIT(hit, TRAIT_PIERCEIMMUNE)
+/// Reflects the projectile off of something
+/obj/projectile/proc/reflect(atom/hit_atom)
+ if(!starting)
+ return
+ var/new_x = starting.x + pick(0, 0, 0, 0, 0, -1, 1, -2, 2)
+ var/new_y = starting.y + pick(0, 0, 0, 0, 0, -1, 1, -2, 2)
+ var/turf/current_tile = get_turf(hit_atom)
+
+ // redirect the projectile
+ original = locate(new_x, new_y, z)
+ starting = current_tile
+ firer = hit_atom
+ yo = new_y - current_tile.y
+ xo = new_x - current_tile.x
+ var/new_angle_s = Angle + rand(120,240)
+ while(new_angle_s > 180) // Translate to regular projectile degrees
+ new_angle_s -= 360
+ set_angle(new_angle_s)
+
#undef MOVES_HITSCAN
#undef MUZZLE_EFFECT_PIXEL_INCREMENT
diff --git a/code/modules/projectiles/projectile/beams.dm b/code/modules/projectiles/projectile/beams.dm
index 08a71bff1b86a..771d2639e2b4c 100644
--- a/code/modules/projectiles/projectile/beams.dm
+++ b/code/modules/projectiles/projectile/beams.dm
@@ -7,7 +7,7 @@
hitsound = 'sound/weapons/sear.ogg'
hitsound_wall = 'sound/weapons/effects/searwall.ogg'
armor_flag = LASER
- eyeblur = 2
+ eyeblur = 4 SECONDS
impact_effect_type = /obj/effect/temp_visual/impact_effect/red_laser
light_system = MOVABLE_LIGHT
light_range = 1
@@ -57,7 +57,7 @@
muzzle_type = /obj/effect/projectile/muzzle/heavy_laser
impact_type = /obj/effect/projectile/impact/heavy_laser
-/obj/projectile/beam/laser/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/beam/laser/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(iscarbon(target))
var/mob/living/carbon/M = target
@@ -93,7 +93,20 @@
/obj/projectile/beam/scatter
name = "laser pellet"
icon_state = "scatterlaser"
- damage = 5
+ damage = 7.5
+ wound_bonus = 5
+ bare_wound_bonus = 5
+ damage_falloff_tile = -0.45
+ wound_falloff_tile = -2.5
+
+/obj/projectile/beam/scatter/pathetic
+ name = "extremely weak laser pellet"
+ damage = 1
+ wound_bonus = 0
+ damage_falloff_tile = -0.1
+ color = "#dbc11d"
+ hitsound = 'sound/items/bikehorn.ogg' //honk
+ hitsound_wall = 'sound/items/bikehorn.ogg'
/obj/projectile/beam/xray
name = "\improper X-ray beam"
@@ -116,7 +129,6 @@
damage_type = STAMINA
armor_flag = ENERGY
hitsound = 'sound/weapons/sear_disabler.ogg'
- eyeblur = 0
impact_effect_type = /obj/effect/temp_visual/impact_effect/blue_laser
light_color = LIGHT_COLOR_BLUE
tracer_type = /obj/effect/projectile/tracer/disabler
@@ -146,7 +158,7 @@
impact_type = /obj/effect/projectile/impact/pulse
wound_bonus = 10
-/obj/projectile/beam/pulse/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/beam/pulse/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if (!QDELETED(target) && (isturf(target) || isstructure(target)))
if(isobj(target))
@@ -163,7 +175,7 @@
projectile_piercing = ALL
var/pierce_hits = 2
-/obj/projectile/beam/pulse/heavy/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/beam/pulse/heavy/on_hit(atom/target, blocked = 0, pierce_hit)
if(pierce_hits <= 0)
projectile_piercing = NONE
pierce_hits -= 1
@@ -207,7 +219,7 @@
impact_effect_type = /obj/effect/temp_visual/impact_effect/blue_laser
light_color = LIGHT_COLOR_BLUE
-/obj/projectile/beam/lasertag/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/beam/lasertag/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(ishuman(target))
var/mob/living/carbon/human/M = target
@@ -249,7 +261,7 @@
light_color = LIGHT_COLOR_BLUE
var/shrink_time = 90
-/obj/projectile/beam/shrink/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/beam/shrink/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(isopenturf(target) || isindestructiblewall(target))//shrunk floors wouldnt do anything except look weird, i-walls shouldn't be bypassable
return
diff --git a/code/modules/projectiles/projectile/bullets/_incendiary.dm b/code/modules/projectiles/projectile/bullets/_incendiary.dm
index 6808f7c48fd5e..85c2dce80c51a 100644
--- a/code/modules/projectiles/projectile/bullets/_incendiary.dm
+++ b/code/modules/projectiles/projectile/bullets/_incendiary.dm
@@ -5,7 +5,7 @@
/// If TRUE, leaves a trail of hotspots as it flies, very very chaotic
var/leaves_fire_trail = TRUE
-/obj/projectile/bullet/incendiary/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/incendiary/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(iscarbon(target))
var/mob/living/carbon/M = target
@@ -41,7 +41,7 @@
wound_falloff_tile = -4
fire_stacks = 3
-/obj/projectile/bullet/incendiary/fire/on_hit(atom/target, blocked)
+/obj/projectile/bullet/incendiary/fire/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
var/turf/location = get_turf(target)
if(isopenturf(location))
diff --git a/code/modules/projectiles/projectile/bullets/cannonball.dm b/code/modules/projectiles/projectile/bullets/cannonball.dm
index 11ffa603cbf74..2f57a3dcc99bd 100644
--- a/code/modules/projectiles/projectile/bullets/cannonball.dm
+++ b/code/modules/projectiles/projectile/bullets/cannonball.dm
@@ -22,7 +22,7 @@
/// How much our object damage decreases on hit, similar to normal damage.
var/object_damage_decrease_on_hit = 0
-/obj/projectile/bullet/cannonball/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/cannonball/on_hit(atom/target, blocked = 0, pierce_hit)
damage -= damage_decrease_on_hit
if(object_damage_decreases)
object_damage -= min(damage, object_damage_decrease_on_hit)
@@ -46,7 +46,7 @@
projectile_piercing = NONE
damage = 40 //set to 30 before first mob impact, but they're gonna be gibbed by the explosion
-/obj/projectile/bullet/cannonball/explosive/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/cannonball/explosive/on_hit(atom/target, blocked = 0, pierce_hit)
explosion(target, devastation_range = 2, heavy_impact_range = 3, light_impact_range = 4, explosion_cause = src)
. = ..()
@@ -56,7 +56,7 @@
projectile_piercing = NONE
damage = 15 //very low
-/obj/projectile/bullet/cannonball/emp/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/cannonball/emp/on_hit(atom/target, blocked = 0, pierce_hit)
empulse(src, 4, 10)
. = ..()
@@ -65,7 +65,7 @@
icon_state = "biggest_one"
damage = 70 //low pierce
-/obj/projectile/bullet/cannonball/biggest_one/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/cannonball/biggest_one/on_hit(atom/target, blocked = 0, pierce_hit)
if(projectile_piercing == NONE)
explosion(target, devastation_range = GLOB.MAX_EX_DEVESTATION_RANGE, heavy_impact_range = GLOB.MAX_EX_HEAVY_RANGE, light_impact_range = GLOB.MAX_EX_LIGHT_RANGE, flash_range = GLOB.MAX_EX_FLASH_RANGE, explosion_cause = src)
. = ..()
diff --git a/code/modules/projectiles/projectile/bullets/dart_syringe.dm b/code/modules/projectiles/projectile/bullets/dart_syringe.dm
index 1f853127858a4..405552a8909c2 100644
--- a/code/modules/projectiles/projectile/bullets/dart_syringe.dm
+++ b/code/modules/projectiles/projectile/bullets/dart_syringe.dm
@@ -10,7 +10,7 @@
. = ..()
create_reagents(50, NO_REACT)
-/obj/projectile/bullet/dart/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/dart/on_hit(atom/target, blocked = 0, pierce_hit)
if(iscarbon(target))
var/mob/living/carbon/M = target
if(blocked != 100) // not completely blocked
diff --git a/code/modules/projectiles/projectile/bullets/dnainjector.dm b/code/modules/projectiles/projectile/bullets/dnainjector.dm
index 139f20c339ca2..fdb051e7f8006 100644
--- a/code/modules/projectiles/projectile/bullets/dnainjector.dm
+++ b/code/modules/projectiles/projectile/bullets/dnainjector.dm
@@ -7,7 +7,7 @@
embedding = null
shrapnel_type = null
-/obj/projectile/bullet/dnainjector/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/dnainjector/on_hit(atom/target, blocked = 0, pierce_hit)
if(iscarbon(target))
var/mob/living/carbon/M = target
if(blocked != 100)
diff --git a/code/modules/projectiles/projectile/bullets/grenade.dm b/code/modules/projectiles/projectile/bullets/grenade.dm
index b1d7278228f7a..a99a7b57ff3ec 100644
--- a/code/modules/projectiles/projectile/bullets/grenade.dm
+++ b/code/modules/projectiles/projectile/bullets/grenade.dm
@@ -8,7 +8,7 @@
embedding = null
shrapnel_type = null
-/obj/projectile/bullet/a40mm/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/a40mm/on_hit(atom/target, blocked = 0, pierce_hit)
..()
explosion(target, devastation_range = -1, light_impact_range = 2, flame_range = 3, flash_range = 1, adminlog = FALSE, explosion_cause = src)
return BULLET_ACT_HIT
diff --git a/code/modules/projectiles/projectile/bullets/revolver.dm b/code/modules/projectiles/projectile/bullets/revolver.dm
index b5411c937be8f..417f61534bcb7 100644
--- a/code/modules/projectiles/projectile/bullets/revolver.dm
+++ b/code/modules/projectiles/projectile/bullets/revolver.dm
@@ -65,7 +65,7 @@
damage = 10
ricochets_max = 0
-/obj/projectile/bullet/c38/trac/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/c38/trac/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
var/mob/living/carbon/M = target
if(!istype(M))
@@ -83,7 +83,7 @@
damage = 20
ricochets_max = 0
-/obj/projectile/bullet/c38/hotshot/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/c38/hotshot/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(iscarbon(target))
var/mob/living/carbon/M = target
@@ -96,7 +96,7 @@
var/temperature = 100
ricochets_max = 0
-/obj/projectile/bullet/c38/iceblox/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/c38/iceblox/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(isliving(target))
var/mob/living/M = target
diff --git a/code/modules/projectiles/projectile/bullets/shotgun.dm b/code/modules/projectiles/projectile/bullets/shotgun.dm
index 639939e150fb7..9bdd5a145ead2 100644
--- a/code/modules/projectiles/projectile/bullets/shotgun.dm
+++ b/code/modules/projectiles/projectile/bullets/shotgun.dm
@@ -63,24 +63,14 @@
damage = 15
paralyze = 10
-/obj/projectile/bullet/shotgun_frag12/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/shotgun_frag12/on_hit(atom/target, blocked = 0, pierce_hit)
..()
explosion(target, devastation_range = -1, light_impact_range = 1, explosion_cause = src)
return BULLET_ACT_HIT
/obj/projectile/bullet/pellet
icon_state = "pellet"
- var/tile_dropoff = 0.45
- var/tile_dropoff_s = 0.25
-
-/obj/projectile/bullet/pellet/Range()
- ..()
- if(damage > 0)
- damage -= tile_dropoff
- if(stamina > 0)
- stamina -= tile_dropoff_s
- if(damage < 0 && stamina < 0)
- qdel(src)
+ damage_falloff_tile = -0.45
/obj/projectile/bullet/pellet/shotgun_buckshot
name = "buckshot pellet"
@@ -96,6 +86,7 @@
sharpness = NONE
embedding = null
speed = 1.2
+ stamina_falloff_tile = -0.25
ricochets_max = 4
ricochet_chance = 120
ricochet_decay_chance = 0.9
diff --git a/code/modules/projectiles/projectile/bullets/sniper.dm b/code/modules/projectiles/projectile/bullets/sniper.dm
index bc4f69eb946a9..4425b20eeedc4 100644
--- a/code/modules/projectiles/projectile/bullets/sniper.dm
+++ b/code/modules/projectiles/projectile/bullets/sniper.dm
@@ -14,7 +14,7 @@
///Determines how much additional damage the round does to mechs.
var/mecha_damage = 10
-/obj/projectile/bullet/p50/on_hit(atom/target, blocked = 0)
+/obj/projectile/bullet/p50/on_hit(atom/target, blocked = 0, pierce_hit)
if(isobj(target) && (blocked != 100))
var/obj/thing_to_break = target
var/damage_to_deal = object_damage
@@ -41,7 +41,7 @@
mecha_damage = 100
var/emp_radius = 2
-/obj/projectile/bullet/p50/disruptor/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/p50/disruptor/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if((blocked != 100) && isliving(target))
var/mob/living/living_guy = target
@@ -60,7 +60,7 @@
object_damage = 30
mecha_damage = 0
-/obj/projectile/bullet/p50/incendiary/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/p50/incendiary/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(iscarbon(target))
var/mob/living/carbon/poor_burning_dork = target
diff --git a/code/modules/projectiles/projectile/bullets/special.dm b/code/modules/projectiles/projectile/bullets/special.dm
index c424f2cd6bed4..f595c3e116510 100644
--- a/code/modules/projectiles/projectile/bullets/special.dm
+++ b/code/modules/projectiles/projectile/bullets/special.dm
@@ -16,7 +16,7 @@
. = ..()
SpinAnimation()
-/obj/projectile/bullet/honker/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/honker/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
var/mob/M = target
if(istype(M))
@@ -30,7 +30,7 @@
/obj/projectile/bullet/mime
damage = 40
-/obj/projectile/bullet/mime/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/mime/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(!isliving(target))
return
diff --git a/code/modules/projectiles/projectile/energy/ebow.dm b/code/modules/projectiles/projectile/energy/ebow.dm
index 73faaffc22f53..e1da23495f4a0 100644
--- a/code/modules/projectiles/projectile/energy/ebow.dm
+++ b/code/modules/projectiles/projectile/energy/ebow.dm
@@ -4,7 +4,7 @@
damage = 15
damage_type = TOX
stamina = 60
- eyeblur = 10
+ eyeblur = 20 SECONDS
knockdown = 10
slur = 10 SECONDS
diff --git a/code/modules/projectiles/projectile/energy/net_snare.dm b/code/modules/projectiles/projectile/energy/net_snare.dm
index 440ab9438e2dd..c60c0c35d172e 100644
--- a/code/modules/projectiles/projectile/energy/net_snare.dm
+++ b/code/modules/projectiles/projectile/energy/net_snare.dm
@@ -10,7 +10,7 @@
. = ..()
SpinAnimation()
-/obj/projectile/energy/net/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/energy/net/on_hit(atom/target, blocked = 0, pierce_hit)
if(isliving(target))
var/turf/Tloc = get_turf(target)
if(!locate(/obj/effect/nettingportal) in Tloc)
@@ -64,7 +64,7 @@
hitsound = 'sound/weapons/taserhit.ogg'
range = 4
-/obj/projectile/energy/trap/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/energy/trap/on_hit(atom/target, blocked = 0, pierce_hit)
if(!ismob(target) || blocked >= 100) //Fully blocked by mob or collided with dense object - drop a trap
new/obj/item/restraints/legcuffs/beartrap/energy(get_turf(loc))
else if(iscarbon(target))
@@ -82,7 +82,7 @@
hitsound = 'sound/weapons/taserhit.ogg'
range = 10
-/obj/projectile/energy/trap/cyborg/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/energy/trap/cyborg/on_hit(atom/target, blocked = 0, pierce_hit)
if(!ismob(target) || blocked >= 100)
do_sparks(1, TRUE, src)
qdel(src)
diff --git a/code/modules/projectiles/projectile/energy/stun.dm b/code/modules/projectiles/projectile/energy/stun.dm
index 03cf5f85d84df..7f36bf437ed65 100644
--- a/code/modules/projectiles/projectile/energy/stun.dm
+++ b/code/modules/projectiles/projectile/energy/stun.dm
@@ -11,7 +11,7 @@
muzzle_type = /obj/effect/projectile/muzzle/stun
impact_type = /obj/effect/projectile/impact/stun
-/obj/projectile/energy/electrode/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/energy/electrode/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(!ismob(target) || blocked >= 100) //Fully blocked by mob or collided with dense object - burst into sparks!
do_sparks(1, TRUE, src)
diff --git a/code/modules/projectiles/projectile/energy/tesla.dm b/code/modules/projectiles/projectile/energy/tesla.dm
index 687bd1b8e73f9..9dfe043a01565 100644
--- a/code/modules/projectiles/projectile/energy/tesla.dm
+++ b/code/modules/projectiles/projectile/energy/tesla.dm
@@ -5,24 +5,24 @@
damage = 10 //A worse lasergun
var/zap_flags = ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_LOW_POWER_GEN
var/zap_range = 3
- var/power = 4e6
+ var/power = 1e4
-/obj/projectile/energy/tesla/on_hit(atom/target)
+/obj/projectile/energy/tesla/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
- tesla_zap(src, zap_range, power, zap_flags)
+ tesla_zap(source = src, zap_range = zap_range, power = power, cutoff = 1e3, zap_flags = zap_flags)
qdel(src)
/obj/projectile/energy/tesla/process()
. = ..()
//Many coders have given their blood for this speed
- tesla_zap(src, zap_range, power, zap_flags)
+ tesla_zap(source = src, zap_range = zap_range, power = power, cutoff = 1e3, zap_flags = zap_flags)
/obj/projectile/energy/tesla/revolver
name = "energy orb"
/obj/projectile/energy/tesla/cannon
name = "tesla orb"
- power = 8e6
+ power = 2e4
damage = 15 //Mech man big
/obj/projectile/energy/tesla_cannon
@@ -32,7 +32,7 @@
speed = 1.5
var/shock_damage = 5
-/obj/projectile/energy/tesla_cannon/on_hit(atom/target)
+/obj/projectile/energy/tesla_cannon/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(isliving(target))
var/mob/living/victim = target
diff --git a/code/modules/projectiles/projectile/energy/thermal.dm b/code/modules/projectiles/projectile/energy/thermal.dm
index 41efd21475c6d..0efb983eb3b69 100644
--- a/code/modules/projectiles/projectile/energy/thermal.dm
+++ b/code/modules/projectiles/projectile/energy/thermal.dm
@@ -10,8 +10,8 @@
bare_wound_bonus = 10
impact_effect_type = /obj/effect/temp_visual/impact_effect/red_laser
-/obj/projectile/energy/inferno/on_hit(atom/target, blocked, pierce_hit)
- ..()
+/obj/projectile/energy/inferno/on_hit(atom/target, blocked = 0, pierce_hit)
+ . = ..()
if(!ishuman(target))
return
@@ -35,8 +35,8 @@
wound_bonus = 0
bare_wound_bonus = 10
-/obj/projectile/energy/cryo/on_hit(atom/target, blocked, pierce_hit)
- ..()
+/obj/projectile/energy/cryo/on_hit(atom/target, blocked = 0, pierce_hit)
+ . = ..()
if(!ishuman(target))
return
diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm
index c8da91b9dde14..2f3214d5d90ab 100644
--- a/code/modules/projectiles/projectile/magic.dm
+++ b/code/modules/projectiles/projectile/magic.dm
@@ -31,7 +31,7 @@
name = "bolt of death"
icon_state = "pulse1_bl"
-/obj/projectile/magic/death/on_hit(atom/target)
+/obj/projectile/magic/death/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(isliving(target))
@@ -57,7 +57,7 @@
name = "bolt of resurrection"
icon_state = "ion"
-/obj/projectile/magic/resurrection/on_hit(atom/target)
+/obj/projectile/magic/resurrection/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(isliving(target))
@@ -85,7 +85,7 @@
var/inner_tele_radius = 0
var/outer_tele_radius = 6
-/obj/projectile/magic/teleport/on_hit(mob/target)
+/obj/projectile/magic/teleport/on_hit(mob/target, blocked = 0, pierce_hit)
. = ..()
var/teleammount = 0
var/teleloc = target
@@ -104,7 +104,7 @@
name = "bolt of safety"
icon_state = "bluespace"
-/obj/projectile/magic/safety/on_hit(atom/target)
+/obj/projectile/magic/safety/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(isturf(target))
return BULLET_ACT_HIT
@@ -123,7 +123,7 @@
icon_state = "energy"
var/list/door_types = list(/obj/structure/mineral_door/wood, /obj/structure/mineral_door/iron, /obj/structure/mineral_door/silver, /obj/structure/mineral_door/gold, /obj/structure/mineral_door/uranium, /obj/structure/mineral_door/sandstone, /obj/structure/mineral_door/transparent/plasma, /obj/structure/mineral_door/transparent/diamond)
-/obj/projectile/magic/door/on_hit(atom/target)
+/obj/projectile/magic/door/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(istype(target, /obj/machinery/door))
OpenDoor(target)
@@ -153,7 +153,7 @@
/// If set, this projectile will only pass certain changeflags to wabbajack
var/set_wabbajack_changeflags
-/obj/projectile/magic/change/on_hit(atom/target)
+/obj/projectile/magic/change/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(isliving(target))
@@ -171,7 +171,7 @@
icon_state = "red_1"
damage_type = BURN
-/obj/projectile/magic/animate/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/magic/animate/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
target.animate_atom_living(firer)
@@ -251,7 +251,7 @@
target.forceMove(src)
return PROJECTILE_PIERCE_PHASE
-/obj/projectile/magic/locker/on_hit(target)
+/obj/projectile/magic/locker/on_hit(atom/target, blocked = 0, pierce_hit)
if(created)
return ..()
if(LAZYLEN(contents))
@@ -313,7 +313,7 @@
name = "bolt of flying"
icon_state = "flight"
-/obj/projectile/magic/flying/on_hit(mob/living/target)
+/obj/projectile/magic/flying/on_hit(mob/living/target, blocked = 0, pierce_hit)
. = ..()
if(isliving(target))
var/atom/throw_target = get_edge_target_turf(target, angle2dir(Angle))
@@ -323,7 +323,7 @@
name = "bolt of bounty"
icon_state = "bounty"
-/obj/projectile/magic/bounty/on_hit(mob/living/target)
+/obj/projectile/magic/bounty/on_hit(mob/living/target, blocked = 0, pierce_hit)
. = ..()
if(isliving(target))
target.apply_status_effect(/datum/status_effect/bounty, firer)
@@ -332,16 +332,16 @@
name = "bolt of antimagic"
icon_state = "antimagic"
-/obj/projectile/magic/antimagic/on_hit(mob/living/target)
+/obj/projectile/magic/antimagic/on_hit(mob/living/target, blocked = 0, pierce_hit)
. = ..()
- if(isliving(target))
+ if(istype(target))
target.apply_status_effect(/datum/status_effect/song/antimagic)
/obj/projectile/magic/fetch
name = "bolt of fetching"
icon_state = "fetch"
-/obj/projectile/magic/fetch/on_hit(mob/living/target)
+/obj/projectile/magic/fetch/on_hit(mob/living/target, blocked = 0, pierce_hit)
. = ..()
if(isliving(target))
var/atom/throw_target = get_edge_target_turf(target, get_dir(target, firer))
@@ -351,7 +351,7 @@
name = "bolt of babel"
icon_state = "babel"
-/obj/projectile/magic/babel/on_hit(mob/living/carbon/target)
+/obj/projectile/magic/babel/on_hit(mob/living/carbon/target, blocked = 0, pierce_hit)
. = ..()
if(iscarbon(target))
if(curse_of_babel(target))
@@ -361,7 +361,7 @@
name = "bolt of necropotence"
icon_state = "necropotence"
-/obj/projectile/magic/necropotence/on_hit(mob/living/target)
+/obj/projectile/magic/necropotence/on_hit(mob/living/target, blocked = 0, pierce_hit)
. = ..()
if(!isliving(target))
return
@@ -378,7 +378,7 @@
name = "bolt of possession"
icon_state = "wipe"
-/obj/projectile/magic/wipe/on_hit(mob/living/carbon/target)
+/obj/projectile/magic/wipe/on_hit(mob/living/carbon/target, blocked = 0, pierce_hit)
. = ..()
if(iscarbon(target))
for(var/x in target.get_traumas())//checks to see if the victim is already going through possession
@@ -481,7 +481,7 @@
speed = 0.3
/// The power of the zap itself when it electrocutes someone
- var/zap_power = 8e6
+ var/zap_power = 2e4
/// The range of the zap itself when it electrocutes someone
var/zap_range = 15
/// The flags of the zap itself when it electrocutes someone
@@ -494,16 +494,16 @@
chain = firer.Beam(src, icon_state = "lightning[rand(1, 12)]")
return ..()
-/obj/projectile/magic/aoe/lightning/on_hit(target)
+/obj/projectile/magic/aoe/lightning/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
- tesla_zap(src, zap_range, zap_power, zap_flags)
+ tesla_zap(source = src, zap_range = zap_range, power = zap_power, cutoff = 1e3, zap_flags = zap_flags)
/obj/projectile/magic/aoe/lightning/Destroy()
QDEL_NULL(chain)
return ..()
/obj/projectile/magic/aoe/lightning/no_zap
- zap_power = 4e6
+ zap_power = 1e4
zap_range = 4
zap_flags = ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_LOW_POWER_GEN
@@ -522,7 +522,7 @@
/// Flash radius of the fireball
var/exp_flash = 3
-/obj/projectile/magic/fireball/on_hit(atom/target, blocked = FALSE, pierce_hit)
+/obj/projectile/magic/fireball/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(isliving(target))
var/mob/living/mob_target = target
@@ -577,7 +577,7 @@
speed = 1
pixel_speed_multiplier = 1/7
-/obj/projectile/magic/aoe/juggernaut/on_hit(atom/target, blocked)
+/obj/projectile/magic/aoe/juggernaut/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
var/turf/target_turf = get_turf(src)
playsound(target_turf, 'sound/weapons/resonator_blast.ogg', 100, FALSE)
diff --git a/code/modules/projectiles/projectile/special/curse.dm b/code/modules/projectiles/projectile/special/curse.dm
index 4e7283810c93f..49abb8c4f3d32 100644
--- a/code/modules/projectiles/projectile/special/curse.dm
+++ b/code/modules/projectiles/projectile/special/curse.dm
@@ -18,6 +18,7 @@
/obj/projectile/curse_hand/Initialize(mapload)
. = ..()
+ ADD_TRAIT(src, TRAIT_FREE_HYPERSPACE_MOVEMENT, INNATE_TRAIT)
handedness = prob(50)
icon_state = "[base_icon_state][handedness]"
diff --git a/code/modules/projectiles/projectile/special/floral.dm b/code/modules/projectiles/projectile/special/floral.dm
index 0fef1ef7443d4..608679bf6da24 100644
--- a/code/modules/projectiles/projectile/special/floral.dm
+++ b/code/modules/projectiles/projectile/special/floral.dm
@@ -1,59 +1,67 @@
-/obj/projectile/energy/floramut
- name = "alpha somatoray"
- icon_state = "energy"
+/obj/projectile/energy/flora
damage = 0
damage_type = TOX
armor_flag = ENERGY
-/obj/projectile/energy/floramut/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/energy/flora/on_hit(atom/target, blocked, pierce_hit)
+ if(!isliving(target))
+ return ..()
+
+ var/mob/living/hit_plant = target
+ if(!(hit_plant.mob_biotypes & MOB_PLANT))
+ hit_plant.show_message(span_notice("The radiation beam dissipates harmlessly through your body."))
+ return BULLET_ACT_BLOCK
+
. = ..()
- if(isliving(target))
- var/mob/living/L = target
- if(L.mob_biotypes & MOB_PLANT)
- if(prob(15))
- L.adjustToxLoss(rand(3, 6))
- L.Paralyze(100)
- L.visible_message(span_warning("[L] writhes in pain as [L.p_their()] vacuoles boil."), span_userdanger("You writhe in pain as your vacuoles boil!"), span_hear("You hear the crunching of leaves."))
- if(iscarbon(L) && L.has_dna())
- var/mob/living/carbon/C = L
- if(prob(80))
- C.easy_random_mutate(NEGATIVE + MINOR_NEGATIVE)
- else
- C.easy_random_mutate(POSITIVE)
- C.random_mutate_unique_identity()
- C.random_mutate_unique_features()
- C.domutcheck()
- else
- L.adjustFireLoss(rand(5, 15))
- L.show_message(span_userdanger("The radiation beam singes you!"))
-
-/obj/projectile/energy/florayield
+ if(. == BULLET_ACT_HIT && blocked < 100)
+ on_hit_plant_effect(target)
+
+ return .
+
+/// Called when we hit a mob with plant biotype
+/obj/projectile/energy/flora/proc/on_hit_plant_effect(mob/living/hit_plant)
+ return
+
+/obj/projectile/energy/flora/mut
+ name = "alpha somatoray"
+ icon_state = "energy"
+
+/obj/projectile/energy/flora/mut/on_hit_plant_effect(mob/living/hit_plant)
+ if(prob(85))
+ hit_plant.adjustFireLoss(rand(5, 15))
+ hit_plant.show_message(span_userdanger("The radiation beam singes you!"))
+ return
+
+ hit_plant.adjustToxLoss(rand(3, 6))
+ hit_plant.Paralyze(10 SECONDS)
+ hit_plant.visible_message(
+ span_warning("[hit_plant] writhes in pain as [hit_plant.p_their()] vacuoles boil."),
+ span_userdanger("You writhe in pain as your vacuoles boil!"),
+ span_hear("You hear the crunching of leaves."),
+ )
+ if(iscarbon(hit_plant) && hit_plant.has_dna())
+ var/mob/living/carbon/carbon_plant = hit_plant
+ if(prob(80))
+ carbon_plant.easy_random_mutate(NEGATIVE + MINOR_NEGATIVE)
+ else
+ carbon_plant.easy_random_mutate(POSITIVE)
+ carbon_plant.random_mutate_unique_identity()
+ carbon_plant.random_mutate_unique_features()
+ carbon_plant.domutcheck()
+
+/obj/projectile/energy/flora/yield
name = "beta somatoray"
icon_state = "energy2"
- damage = 0
- damage_type = TOX
- armor_flag = ENERGY
-/obj/projectile/energy/florayield/on_hit(atom/target, blocked = FALSE)
- . = ..()
- if(isliving(target))
- var/mob/living/L = target
- if(L.mob_biotypes & MOB_PLANT)
- L.set_nutrition(min(L.nutrition + 30, NUTRITION_LEVEL_FULL))
+/obj/projectile/energy/flora/yield/on_hit_plant_effect(mob/living/hit_plant)
+ hit_plant.set_nutrition(min(hit_plant.nutrition + 30, NUTRITION_LEVEL_FULL))
-/obj/projectile/energy/florarevolution
+/obj/projectile/energy/flora/evolution
name = "gamma somatoray"
icon_state = "energy3"
- damage = 0
- damage_type = TOX
- armor_flag = ENERGY
-/obj/projectile/energy/florarevolution/on_hit(atom/target, blocked = FALSE)
- . = ..()
- if(isliving(target))
- var/mob/living/L = target
- if(L.mob_biotypes & MOB_PLANT)
- L.show_message(span_notice("The radiation beam leaves you feeling disoriented!"))
- L.set_dizzy_if_lower(30 SECONDS)
- L.emote("flip")
- L.emote("spin")
+/obj/projectile/energy/flora/evolution/on_hit_plant_effect(mob/living/hit_plant)
+ hit_plant.show_message(span_notice("The radiation beam leaves you feeling disoriented!"))
+ hit_plant.set_dizzy_if_lower(30 SECONDS)
+ hit_plant.emote("flip")
+ hit_plant.emote("spin")
diff --git a/code/modules/projectiles/projectile/special/gravity.dm b/code/modules/projectiles/projectile/special/gravity.dm
index 2a0df1b510b54..1a23b653a0519 100644
--- a/code/modules/projectiles/projectile/special/gravity.dm
+++ b/code/modules/projectiles/projectile/special/gravity.dm
@@ -16,7 +16,7 @@
if(istype(C)) //Hard-coded maximum power so servers can't be crashed by trying to throw the entire Z level's items
power = min(C.gun?.power, 15)
-/obj/projectile/gravityrepulse/on_hit()
+/obj/projectile/gravityrepulse/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
T = get_turf(src)
for(var/atom/movable/A in range(T, power))
@@ -50,7 +50,7 @@
if(istype(C)) //Hard-coded maximum power so servers can't be crashed by trying to throw the entire Z level's items
power = min(C.gun?.power, 15)
-/obj/projectile/gravityattract/on_hit()
+/obj/projectile/gravityattract/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
T = get_turf(src)
for(var/atom/movable/A in range(T, power))
@@ -83,7 +83,7 @@
if(istype(C)) //Hard-coded maximum power so servers can't be crashed by trying to throw the entire Z level's items
power = min(C.gun?.power, 15)
-/obj/projectile/gravitychaos/on_hit()
+/obj/projectile/gravitychaos/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
T = get_turf(src)
for(var/atom/movable/A in range(T, power))
diff --git a/code/modules/projectiles/projectile/special/ion.dm b/code/modules/projectiles/projectile/special/ion.dm
index 6dc0246d35bb0..9d25f1504cde5 100644
--- a/code/modules/projectiles/projectile/special/ion.dm
+++ b/code/modules/projectiles/projectile/special/ion.dm
@@ -7,7 +7,7 @@
impact_effect_type = /obj/effect/temp_visual/impact_effect/ion
var/emp_radius = 1
-/obj/projectile/ion/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/ion/on_hit(atom/target, blocked = 0, pierce_hit)
..()
empulse(target, emp_radius, emp_radius)
return BULLET_ACT_HIT
diff --git a/code/modules/projectiles/projectile/special/meteor.dm b/code/modules/projectiles/projectile/special/meteor.dm
index a0020a573d371..7cecbecc6aa31 100644
--- a/code/modules/projectiles/projectile/special/meteor.dm
+++ b/code/modules/projectiles/projectile/special/meteor.dm
@@ -9,7 +9,7 @@
damage_type = BRUTE
armor_flag = BULLET
-/obj/projectile/meteor/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/meteor/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(. == BULLET_ACT_HIT && isliving(target))
explosion(target, devastation_range = -1, light_impact_range = 2, flame_range = 0, flash_range = 1, adminlog = FALSE)
diff --git a/code/modules/projectiles/projectile/special/mindflayer.dm b/code/modules/projectiles/projectile/special/mindflayer.dm
index 54889bbced1c6..9f15e9389d591 100644
--- a/code/modules/projectiles/projectile/special/mindflayer.dm
+++ b/code/modules/projectiles/projectile/special/mindflayer.dm
@@ -1,7 +1,7 @@
/obj/projectile/beam/mindflayer
name = "flayer ray"
-/obj/projectile/beam/mindflayer/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/beam/mindflayer/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(ishuman(target))
var/mob/living/carbon/human/human_hit = target
diff --git a/code/modules/projectiles/projectile/special/neurotoxin.dm b/code/modules/projectiles/projectile/special/neurotoxin.dm
index 24d24f68d3087..077b3a275e99e 100644
--- a/code/modules/projectiles/projectile/special/neurotoxin.dm
+++ b/code/modules/projectiles/projectile/special/neurotoxin.dm
@@ -7,7 +7,7 @@
impact_effect_type = /obj/effect/temp_visual/impact_effect/neurotoxin
armour_penetration = 50
-/obj/projectile/neurotoxin/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/neurotoxin/on_hit(atom/target, blocked = 0, pierce_hit)
if(isalien(target))
damage = 0
return ..()
diff --git a/code/modules/projectiles/projectile/special/plasma.dm b/code/modules/projectiles/projectile/special/plasma.dm
index cf8778fe4deb6..5564ba14dab0e 100644
--- a/code/modules/projectiles/projectile/special/plasma.dm
+++ b/code/modules/projectiles/projectile/special/plasma.dm
@@ -11,7 +11,7 @@
muzzle_type = /obj/effect/projectile/muzzle/plasma_cutter
impact_type = /obj/effect/projectile/impact/plasma_cutter
-/obj/projectile/plasma/on_hit(atom/target)
+/obj/projectile/plasma/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(ismineralturf(target))
var/turf/closed/mineral/M = target
diff --git a/code/modules/projectiles/projectile/special/rocket.dm b/code/modules/projectiles/projectile/special/rocket.dm
index 08a2c18c2f73d..22082d3809fcb 100644
--- a/code/modules/projectiles/projectile/special/rocket.dm
+++ b/code/modules/projectiles/projectile/special/rocket.dm
@@ -5,7 +5,7 @@
embedding = null
shrapnel_type = null
-/obj/projectile/bullet/gyro/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/gyro/on_hit(atom/target, blocked = 0, pierce_hit)
..()
explosion(target, devastation_range = -1, light_impact_range = 2, explosion_cause = src)
return BULLET_ACT_HIT
@@ -25,7 +25,7 @@
/// Whether the rocket is capable of instantly killing a living target
var/random_crits_enabled = TRUE // Worst thing Valve ever added
-/obj/projectile/bullet/rocket/on_hit(atom/target, blocked = FALSE)
+/obj/projectile/bullet/rocket/on_hit(atom/target, blocked = 0, pierce_hit)
var/random_crit_gib = FALSE
if(isliving(target) && prob(1) && random_crits_enabled)
var/mob/living/gibbed_dude = target
diff --git a/code/modules/projectiles/projectile/special/temperature.dm b/code/modules/projectiles/projectile/special/temperature.dm
index 7eae3edfa2036..3d88c40fdfb9c 100644
--- a/code/modules/projectiles/projectile/special/temperature.dm
+++ b/code/modules/projectiles/projectile/special/temperature.dm
@@ -9,7 +9,7 @@
/obj/projectile/temp/is_hostile_projectile()
return temperature != 0 // our damage is done by cooling or heating (casting to boolean here)
-/obj/projectile/temp/on_hit(atom/target, blocked = 0)
+/obj/projectile/temp/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(iscarbon(target))
var/mob/living/carbon/hit_mob = target
@@ -26,6 +26,7 @@
/obj/projectile/temp/hot
name = "heat beam"
+ icon_state = "lava"
temperature = 100 // Raise the body temp by 100 points
/obj/projectile/temp/cryo
diff --git a/code/modules/projectiles/projectile/special/wormhole.dm b/code/modules/projectiles/projectile/special/wormhole.dm
index 26873daac871b..90eadd0bb097b 100644
--- a/code/modules/projectiles/projectile/special/wormhole.dm
+++ b/code/modules/projectiles/projectile/special/wormhole.dm
@@ -22,9 +22,11 @@
gun = casing.gun
-/obj/projectile/beam/wormhole/on_hit(atom/target)
+/obj/projectile/beam/wormhole/on_hit(atom/target, blocked = 0, pierce_hit)
var/obj/item/gun/energy/wormhole_projector/projector = gun.resolve()
if(!projector)
qdel(src)
- return
+ return BULLET_ACT_BLOCK
+
+ . = ..()
projector.create_portal(src, get_turf(src))
diff --git a/code/modules/reagents/chemistry/equilibrium.dm b/code/modules/reagents/chemistry/equilibrium.dm
index f46c636c50f59..073eaf69e5ae1 100644
--- a/code/modules/reagents/chemistry/equilibrium.dm
+++ b/code/modules/reagents/chemistry/equilibrium.dm
@@ -324,11 +324,11 @@
//keep limited
if(delta_chem_factor > step_target_vol)
delta_chem_factor = step_target_vol
- else if (delta_chem_factor < CHEMICAL_QUANTISATION_LEVEL)
- delta_chem_factor = CHEMICAL_QUANTISATION_LEVEL
+ else if (delta_chem_factor < CHEMICAL_VOLUME_ROUNDING)
+ delta_chem_factor = CHEMICAL_VOLUME_ROUNDING
//Normalise to multiproducts
delta_chem_factor /= product_ratio
- //delta_chem_factor = round(delta_chem_factor, CHEMICAL_QUANTISATION_LEVEL) // Might not be needed - left here incase testmerge shows that it does. Remove before full commit.
+ delta_chem_factor = round(delta_chem_factor, CHEMICAL_VOLUME_ROUNDING) // Might not be needed - left here incase testmerge shows that it does. Remove before full commit.
//Calculate how much product to make and how much reactant to remove factors..
for(var/reagent in reaction.required_reagents)
diff --git a/code/modules/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm
index 298b0077cb2e9..41611b19b715f 100644
--- a/code/modules/reagents/chemistry/holder.dm
+++ b/code/modules/reagents/chemistry/holder.dm
@@ -93,11 +93,6 @@
if(SEND_SIGNAL(src, COMSIG_REAGENTS_PRE_ADD_REAGENT, reagent_type, amount, reagtemp, data, no_react) & COMPONENT_CANCEL_REAGENT_ADD)
return FALSE
- // Prevents small amount problems, as well as zero and below zero amounts.
- amount = FLOOR(amount, CHEMICAL_QUANTISATION_LEVEL)
- if(amount <= 0)
- return FALSE
-
var/datum/reagent/glob_reagent = GLOB.chemical_reagents_list[reagent_type]
if(!glob_reagent)
stack_trace("[my_atom] attempted to add a reagent called '[reagent_type]' which doesn't exist. ([usr])")
@@ -110,9 +105,9 @@
//Split up the reagent if it's in a mob
var/has_split = FALSE
if(!ignore_splitting && (flags & REAGENT_HOLDER_ALIVE)) //Stomachs are a pain - they will constantly call on_mob_add unless we split on addition to stomachs, but we also want to make sure we don't double split
- var/adjusted_vol = FLOOR(process_mob_reagent_purity(glob_reagent, amount, added_purity), CHEMICAL_QUANTISATION_LEVEL)
- if(adjusted_vol <= 0) //If we're inverse or FALSE cancel addition
- return TRUE
+ var/adjusted_vol = process_mob_reagent_purity(glob_reagent, amount, added_purity)
+ if(adjusted_vol <= CHEMICAL_QUANTISATION_LEVEL) //If we're inverse or FALSE cancel addition
+ return amount
/* We return true here because of #63301
The only cases where this will be false or 0 if its an inverse chem, an impure chem of 0 purity (highly unlikely if even possible), or if glob_reagent is null (which shouldn't happen at all as there's a check for that a few lines up),
In the first two cases, we would want to return TRUE so trans_to and other similar methods actually delete the corresponding chemical from the original reagent holder.
@@ -123,9 +118,8 @@
update_total()
var/cached_total = total_volume
if(cached_total + amount > maximum_volume)
- amount = FLOOR(maximum_volume - cached_total, CHEMICAL_QUANTISATION_LEVEL) //Doesnt fit in. Make it disappear. shouldn't happen. Will happen.
- if(amount <= 0)
- return FALSE
+ amount = maximum_volume - cached_total //Doesnt fit in. Make it disappear. shouldn't happen. Will happen.
+ amount = round(amount, CHEMICAL_QUANTISATION_LEVEL)
var/cached_temp = chem_temp
var/list/cached_reagents = reagent_list
@@ -143,8 +137,8 @@
added_ph = iter_reagent.ph
iter_reagent.purity = ((iter_reagent.creation_purity * iter_reagent.volume) + (added_purity * amount)) /(iter_reagent.volume + amount) //This should add the purity to the product
iter_reagent.creation_purity = iter_reagent.purity
- iter_reagent.ph = ((iter_reagent.ph*(iter_reagent.volume))+(added_ph*amount))/(iter_reagent.volume+amount)
- iter_reagent.volume = FLOOR(iter_reagent.volume + amount, CHEMICAL_QUANTISATION_LEVEL)
+ iter_reagent.ph = ((iter_reagent.ph * (iter_reagent.volume)) + (added_ph * amount)) / (iter_reagent.volume + amount)
+ iter_reagent.volume += amount
update_total()
iter_reagent.on_merge(data, amount)
@@ -158,7 +152,7 @@
SEND_SIGNAL(src, COMSIG_REAGENTS_ADD_REAGENT, iter_reagent, amount, reagtemp, data, no_react)
if(!no_react && !is_reacting) //To reduce the amount of calculations for a reaction the reaction list is only updated on a reagents addition.
handle_reactions()
- return TRUE
+ return amount
//otherwise make a new one
var/datum/reagent/new_reagent = new reagent_type(data)
@@ -187,7 +181,7 @@
SEND_SIGNAL(src, COMSIG_REAGENTS_NEW_REAGENT, new_reagent, amount, reagtemp, data, no_react)
if(!no_react)
handle_reactions()
- return TRUE
+ return amount
/**
* Like add_reagent but you can enter a list.
@@ -219,21 +213,23 @@
stack_trace("non finite amount passed to remove reagent [amount] [reagent_type]")
return FALSE
- // Prevents small amount problems, as well as zero and below zero amounts.
- amount = FLOOR(amount, CHEMICAL_QUANTISATION_LEVEL)
+ amount = round(amount, CHEMICAL_QUANTISATION_LEVEL)
if(amount <= 0)
return FALSE
var/list/cached_reagents = reagent_list
for(var/datum/reagent/cached_reagent as anything in cached_reagents)
if(cached_reagent.type == reagent_type)
- cached_reagent.volume = FLOOR(max(cached_reagent.volume - amount, 0), CHEMICAL_QUANTISATION_LEVEL)
+ cached_reagent.volume -= amount
+
update_total()
if(!safety)//So it does not handle reactions when it need not to
handle_reactions()
+
SEND_SIGNAL(src, COMSIG_REAGENTS_REM_REAGENT, QDELING(cached_reagent) ? reagent_type : cached_reagent, amount)
return TRUE
+
return FALSE
/**
@@ -247,7 +243,7 @@
stack_trace("non finite amount passed to remove any reagent [amount]")
return FALSE
- amount = FLOOR(amount, CHEMICAL_QUANTISATION_LEVEL)
+ amount = round(amount, CHEMICAL_QUANTISATION_LEVEL)
if(amount <= 0)
return FALSE
@@ -274,7 +270,6 @@
current_list_element++
total_removed += remove_amt
- update_total()
handle_reactions()
return total_removed //this should be amount unless the loop is prematurely broken, in which case it'll be lower. It shouldn't ever go OVER amount.
@@ -295,8 +290,7 @@
stack_trace("non finite amount passed to remove all reagents [amount]")
return FALSE
- // Prevents small amount problems, as well as zero and below zero amounts.
- amount = FLOOR(amount, CHEMICAL_QUANTISATION_LEVEL)
+ amount = round(amount, CHEMICAL_QUANTISATION_LEVEL)
if(amount <= 0)
return FALSE
@@ -306,12 +300,12 @@
var/removed_amount = 0
for(var/datum/reagent/reagent as anything in cached_reagents)
- remove_amount = FLOOR(reagent.volume * part, CHEMICAL_QUANTISATION_LEVEL)
+ remove_amount = reagent.volume * part
remove_reagent(reagent.type, remove_amount)
removed_amount += remove_amount
handle_reactions()
- return removed_amount
+ return round(removed_amount, CHEMICAL_VOLUME_ROUNDING)
/**
* Removes all reagent of X type
@@ -330,8 +324,7 @@
stack_trace("non finite amount passed to remove all type reagent [amount] [reagent_type]")
return FALSE
- // Prevents small amount problems, as well as zero and below zero amounts.
- amount = FLOOR(amount, CHEMICAL_QUANTISATION_LEVEL)
+ amount = round(amount, CHEMICAL_QUANTISATION_LEVEL)
if(amount <= 0)
return FALSE
@@ -491,7 +484,7 @@
* Arguments:
* * obj/target - Target to attempt transfer to
* * amount - amount of reagent volume to transfer
- * * multiplier - multiplies amount of each reagent by this number
+ * * multiplier - multiplies each reagent amount by this number well byond their available volume before transfering. used to create reagents from thin air if you ever need to
* * preserve_data - if preserve_data=0, the reagents data will be lost. Usefull if you use data for some strange stuff and don't want it to be transferred.
* * no_react - passed through to [/datum/reagents/proc/add_reagent]
* * mob/transferred_by - used for logging
@@ -531,7 +524,7 @@
var/mob/living/carbon/eater = target
var/obj/item/organ/internal/stomach/belly = eater.get_organ_slot(ORGAN_SLOT_STOMACH)
if(!belly)
- var/expel_amount = FLOOR(amount, CHEMICAL_QUANTISATION_LEVEL)
+ var/expel_amount = round(amount, CHEMICAL_QUANTISATION_LEVEL)
if(expel_amount > 0 )
eater.expel_ingested(my_atom, expel_amount)
return
@@ -546,9 +539,9 @@
var/cached_amount = amount
// Prevents small amount problems, as well as zero and below zero amounts.
- amount = FLOOR(min(amount * multiplier, total_volume, target_holder.maximum_volume - target_holder.total_volume), CHEMICAL_QUANTISATION_LEVEL)
- if(amount <= 0)
- return FALSE
+ amount = FLOOR(min(amount, total_volume, target_holder.maximum_volume - target_holder.total_volume), CHEMICAL_QUANTISATION_LEVEL)
+ if(amount <= CHEMICAL_QUANTISATION_LEVEL)
+ return
//Set up new reagents to inherit the old ongoing reactions
if(!no_react)
@@ -561,7 +554,8 @@
var/part = amount / total_volume
var/transfer_amount
- var/transfered_amount = 0
+ var/transfered_amount
+ var/total_transfered_amount = 0
//first add reagents to target
for(var/datum/reagent/reagent as anything in cached_reagents)
@@ -571,13 +565,14 @@
trans_data = copy_data(reagent)
if(reagent.intercept_reagents_transfer(target_holder, cached_amount))
continue
- transfer_amount = FLOOR(reagent.volume * part, CHEMICAL_QUANTISATION_LEVEL)
- if(!target_holder.add_reagent(reagent.type, transfer_amount, trans_data, chem_temp, reagent.purity, reagent.ph, no_react = TRUE, ignore_splitting = reagent.chemical_flags & REAGENT_DONOTSPLIT)) //we only handle reaction after every reagent has been transferred.
+ transfer_amount = reagent.volume * part
+ transfered_amount = target_holder.add_reagent(reagent.type, transfer_amount * multiplier, trans_data, chem_temp, reagent.purity, reagent.ph, no_react = TRUE, ignore_splitting = reagent.chemical_flags & REAGENT_DONOTSPLIT) //we only handle reaction after every reagent has been transferred.
+ if(!transfered_amount)
continue
if(methods)
r_to_send += reagent
reagents_to_remove += list(list("R" = reagent, "T" = transfer_amount))
- transfered_amount += transfer_amount
+ total_transfered_amount += transfered_amount
//expose target to reagent changes
target_holder.expose_multiple(r_to_send, isorgan(target_atom) ? target : target_atom, methods, part, show_message)
@@ -600,7 +595,8 @@
if(!no_react)
target_holder.handle_reactions()
src.handle_reactions()
- return transfered_amount
+
+ return round(total_transfered_amount, CHEMICAL_VOLUME_ROUNDING)
/**
* Transfer a specific reagent id to the target object
@@ -637,7 +633,7 @@
// Prevents small amount problems, as well as zero and below zero amounts.
amount = FLOOR(min(amount, available_volume, holder.maximum_volume - holder.total_volume), CHEMICAL_QUANTISATION_LEVEL)
- if(amount <= 0)
+ if(amount <= CHEMICAL_QUANTISATION_LEVEL)
return
var/list/cached_reagents = reagent_list
@@ -665,7 +661,7 @@
* Arguments
*
* * [target][obj] - the target to transfer reagents to
- * * multiplier - the multiplier applied on all reagent volumes before transfering
+ * * multiplier - multiplies each reagent amount by this number well byond their available volume before transfering. used to create reagents from thin air if you ever need to
* * preserve_data - preserve user data of all reagents after transfering
* * no_react - if TRUE will not handle reactions
*/
@@ -692,23 +688,25 @@
target_holder = target.reagents
// Prevents small amount problems, as well as zero and below zero amounts.
- amount = FLOOR(min(amount * multiplier, total_volume, target_holder.maximum_volume - target_holder.total_volume), CHEMICAL_QUANTISATION_LEVEL)
- if(amount <= 0)
+ amount = FLOOR(min(amount, total_volume, target_holder.maximum_volume - target_holder.total_volume), CHEMICAL_QUANTISATION_LEVEL)
+ if(amount <= CHEMICAL_QUANTISATION_LEVEL)
return
var/list/cached_reagents = reagent_list
var/part = amount / total_volume
var/transfer_amount
var/transfered_amount = 0
+ var/total_transfered_amount = 0
var/trans_data = null
for(var/datum/reagent/reagent as anything in cached_reagents)
- transfer_amount = FLOOR(reagent.volume * part, CHEMICAL_QUANTISATION_LEVEL)
+ transfer_amount = reagent.volume * part * multiplier
if(preserve_data)
trans_data = reagent.data
- if(!target_holder.add_reagent(reagent.type, transfer_amount, trans_data, chem_temp, reagent.purity, reagent.ph, no_react = TRUE, ignore_splitting = reagent.chemical_flags & REAGENT_DONOTSPLIT))
+ transfered_amount = target_holder.add_reagent(reagent.type, transfer_amount, trans_data, chem_temp, reagent.purity, reagent.ph, no_react = TRUE, ignore_splitting = reagent.chemical_flags & REAGENT_DONOTSPLIT)
+ if(!transfered_amount)
continue
- transfered_amount += transfer_amount
+ total_transfered_amount += transfered_amount
if(!no_react)
// pass over previous ongoing reactions before handle_reactions is called
@@ -717,7 +715,7 @@
target_holder.update_total()
target_holder.handle_reactions()
- return transfered_amount
+ return round(total_transfered_amount, CHEMICAL_VOLUME_ROUNDING)
/**
* Multiplies the reagents inside this holder by a specific amount
@@ -805,7 +803,7 @@
var/datum/reagent/toxin/toxin = reagent
var/amount = toxin.volume
if(belly)
- amount = FLOOR(amount + belly.reagents.get_reagent_amount(toxin.type), CHEMICAL_QUANTISATION_LEVEL)
+ amount += belly.reagents.get_reagent_amount(toxin.type)
if(amount <= liver_tolerance)
owner.reagents.remove_reagent(toxin.type, toxin.metabolization_rate * owner.metabolism_efficiency * seconds_per_tick)
@@ -1139,9 +1137,6 @@
/datum/reagents/proc/finish_reacting()
STOP_PROCESSING(SSreagents, src)
is_reacting = FALSE
- //Cap off values
- for(var/datum/reagent/reagent as anything in reagent_list)
- reagent.volume = FLOOR(reagent.volume, CHEMICAL_QUANTISATION_LEVEL)//To prevent runaways.
LAZYNULL(previous_reagent_list) //reset it to 0 - because any change will be different now.
update_total()
if(!QDELING(src))
@@ -1241,9 +1236,9 @@
var/datum/cached_my_atom = my_atom
var/multiplier = INFINITY
for(var/reagent in cached_required_reagents)
- multiplier = FLOOR(min(multiplier, get_reagent_amount(reagent) / cached_required_reagents[reagent]), CHEMICAL_QUANTISATION_LEVEL)
+ multiplier = round(min(multiplier, get_reagent_amount(reagent) / cached_required_reagents[reagent]))
- if(multiplier == 0)//Incase we're missing reagents - usually from on_reaction being called in an equlibrium when the results.len == 0 handlier catches a misflagged reaction
+ if(!multiplier)//Incase we're missing reagents - usually from on_reaction being called in an equlibrium when the results.len == 0 handlier catches a misflagged reaction
return FALSE
var/sum_purity = 0
for(var/_reagent in cached_required_reagents)//this is not an object
@@ -1286,6 +1281,7 @@
var/chem_index = 1
var/num_reagents = length(cached_reagents)
var/total_ph = 0
+ var/reagent_volume = 0
. = 0
//responsible for removing reagents and computing total ph & volume
@@ -1293,9 +1289,10 @@
while(chem_index <= num_reagents)
var/datum/reagent/reagent = cached_reagents[chem_index]
chem_index += 1
+ reagent_volume = round(reagent.volume, CHEMICAL_QUANTISATION_LEVEL) //round to this many decimal places
//remove very small amounts of reagents
- if((reagent.volume <= 0.05 && !is_reacting) || reagent.volume <= CHEMICAL_QUANTISATION_LEVEL)
+ if((reagent_volume <= 0.05 && !is_reacting) || reagent_volume <= CHEMICAL_QUANTISATION_LEVEL)
//end metabolization
if(isliving(my_atom))
if(reagent.metabolizing)
@@ -1314,15 +1311,18 @@
continue
//compute volume & ph like we would normally
- . += reagent.volume
- total_ph += (reagent.ph * reagent.volume)
+ . += reagent_volume
+ total_ph += (reagent.ph * reagent_volume)
+
+ //reasign floored value
+ reagent.volume = reagent_volume
- //assign the final values
- total_volume = .
+ //assign the final values, rounding up can sometimes cause overflow so bring it down
+ total_volume = min(round(., CHEMICAL_VOLUME_ROUNDING), maximum_volume)
if(!.)
ph = CHEMICAL_NORMAL_PH
else
- ph = clamp(total_ph / total_volume, 0, 14)
+ ph = clamp(total_ph / total_volume, CHEMICAL_MIN_PH, CHEMICAL_MAX_PH)
//now send the signals after the volume & ph has been computed
for(var/datum/reagent/deleted_reagent as anything in deleted_reagents)
@@ -1390,7 +1390,7 @@
if((!include_subtypes && cached_reagent.type == reagent) || (include_subtypes && ispath(cached_reagent.type, reagent)))
total_amount += cached_reagent.volume
- return FLOOR(total_amount, CHEMICAL_QUANTISATION_LEVEL)
+ return round(total_amount, CHEMICAL_VOLUME_ROUNDING)
/**
* Gets the sum of volumes of all reagent type paths present in the list
@@ -1402,8 +1402,9 @@
var/total_amount = 0
for(var/datum/reagent/cached_reagent as anything in cached_reagents)
if(cached_reagent.type in reagents)
- total_amount += FLOOR(cached_reagent.volume, CHEMICAL_QUANTISATION_LEVEL)
- return total_amount
+ total_amount += cached_reagent.volume
+
+ return round(total_amount, CHEMICAL_VOLUME_ROUNDING)
/**
* Get the purity of this reagent
@@ -1419,6 +1420,7 @@
for(var/datum/reagent/cached_reagent as anything in cached_reagents)
if(cached_reagent.type == reagent)
return round(cached_reagent.purity, 0.01)
+
return 0
/**
@@ -1587,9 +1589,9 @@
* Arguments:
* * value - How much to adjust the base pH by
*/
-/datum/reagents/proc/adjust_all_reagents_ph(value, lower_limit = 0, upper_limit = 14)
+/datum/reagents/proc/adjust_all_reagents_ph(value)
for(var/datum/reagent/reagent as anything in reagent_list)
- reagent.ph = clamp(reagent.ph + value, lower_limit, upper_limit)
+ reagent.ph = clamp(reagent.ph + value, CHEMICAL_MIN_PH, CHEMICAL_MAX_PH)
/*
* Adjusts the base pH of a specific type
@@ -1599,14 +1601,12 @@
* Arguments:
* * input_reagent - type path of the reagent
* * value - How much to adjust the base pH by
-* * lower_limit - how low the pH can go
-* * upper_limit - how high the pH can go
*/
-/datum/reagents/proc/adjust_specific_reagent_ph(input_reagent, value, lower_limit = 0, upper_limit = 14)
+/datum/reagents/proc/adjust_specific_reagent_ph(input_reagent, value)
var/datum/reagent/reagent = get_reagent(input_reagent)
if(!reagent) //We can call this with missing reagents.
return FALSE
- reagent.ph = clamp(reagent.ph + value, lower_limit, upper_limit)
+ reagent.ph = clamp(reagent.ph + value, CHEMICAL_MIN_PH, CHEMICAL_MAX_PH)
/**
* Outputs a log-friendly list of reagents based on an external reagent list.
@@ -1622,7 +1622,7 @@
for(var/reagent_type in external_list)
var/list/qualities = external_list[reagent_type]
- data += "[reagent_type] ([FLOOR(qualities[REAGENT_TRANSFER_AMOUNT], CHEMICAL_QUANTISATION_LEVEL)]u, [qualities[REAGENT_PURITY]] purity)"
+ data += "[reagent_type] ([round(qualities[REAGENT_TRANSFER_AMOUNT], CHEMICAL_QUANTISATION_LEVEL)]u, [qualities[REAGENT_PURITY]] purity)"
return english_list(data)
@@ -1634,7 +1634,7 @@
var/list/data = list()
for(var/datum/reagent/reagent as anything in reagent_list)
- data += "[reagent.type] ([FLOOR(reagent.volume, CHEMICAL_QUANTISATION_LEVEL)]u, [reagent.purity] purity)"
+ data += "[reagent.type] ([round(reagent.volume, CHEMICAL_QUANTISATION_LEVEL)]u, [reagent.purity] purity)"
return english_list(data)
@@ -1808,7 +1808,7 @@
to_chat(user, "Could not find reagent!")
ui_reagent_id = null
else
- data["reagent_mode_reagent"] = list("name" = reagent.name, "id" = reagent.type, "desc" = reagent.description, "reagentCol" = reagent.color, "pH" = reagent.ph, "pHCol" = convert_ph_to_readable_color(reagent.ph), "metaRate" = (reagent.metabolization_rate/2), "OD" = reagent.overdose_threshold)
+ data["reagent_mode_reagent"] = list("name" = reagent.name, "id" = reagent.type, "desc" = reagent.description, "reagentCol" = reagent.color, "pH" = reagent.ph, "pHCol" = convert_ph_to_readable_color(reagent.ph), "metaRate" = reagent.metabolization_rate, "OD" = reagent.overdose_threshold)
data["reagent_mode_reagent"]["addictions"] = list()
data["reagent_mode_reagent"]["addictions"] = parse_addictions(reagent)
diff --git a/code/modules/reagents/chemistry/machinery/chem_master.dm b/code/modules/reagents/chemistry/machinery/chem_master.dm
index 464df7bf36397..6955cede78de4 100644
--- a/code/modules/reagents/chemistry/machinery/chem_master.dm
+++ b/code/modules/reagents/chemistry/machinery/chem_master.dm
@@ -2,48 +2,6 @@
#define TRANSFER_MODE_MOVE 1
#define TARGET_BEAKER "beaker"
#define TARGET_BUFFER "buffer"
-#define CAT_CONDIMENTS "condiments"
-#define CAT_TUBES "tubes"
-#define CAT_PILLS "pills"
-#define CAT_PATCHES "patches"
-
-/// List of containers the Chem Master machine can print
-GLOBAL_LIST_INIT(chem_master_containers, list(
- CAT_CONDIMENTS = list(
- /obj/item/reagent_containers/cup/bottle,
- /obj/item/reagent_containers/condiment/flour,
- /obj/item/reagent_containers/condiment/sugar,
- /obj/item/reagent_containers/condiment/rice,
- /obj/item/reagent_containers/condiment/cornmeal,
- /obj/item/reagent_containers/condiment/milk,
- /obj/item/reagent_containers/condiment/soymilk,
- /obj/item/reagent_containers/condiment/yoghurt,
- /obj/item/reagent_containers/condiment/saltshaker,
- /obj/item/reagent_containers/condiment/peppermill,
- /obj/item/reagent_containers/condiment/soysauce,
- /obj/item/reagent_containers/condiment/bbqsauce,
- /obj/item/reagent_containers/condiment/enzyme,
- /obj/item/reagent_containers/condiment/hotsauce,
- /obj/item/reagent_containers/condiment/coldsauce,
- /obj/item/reagent_containers/condiment/mayonnaise,
- /obj/item/reagent_containers/condiment/ketchup,
- /obj/item/reagent_containers/condiment/olive_oil,
- /obj/item/reagent_containers/condiment/vegetable_oil,
- /obj/item/reagent_containers/condiment/peanut_butter,
- /obj/item/reagent_containers/condiment/cherryjelly,
- /obj/item/reagent_containers/condiment/honey,
- /obj/item/reagent_containers/condiment/pack,
- ),
- CAT_TUBES = list(
- /obj/item/reagent_containers/cup/tube
- ),
- CAT_PILLS = typecacheof(list(
- /obj/item/reagent_containers/pill/style
- )),
- CAT_PATCHES = typecacheof(list(
- /obj/item/reagent_containers/pill/patch/style
- ))
-))
/obj/machinery/chem_master
name = "ChemMaster 3000"
@@ -59,7 +17,7 @@ GLOBAL_LIST_INIT(chem_master_containers, list(
/// Icons for different percentages of buffer reagents
var/fill_icon = 'icons/obj/medical/reagent_fillings.dmi'
var/fill_icon_state = "chemmaster"
- var/list/fill_icon_thresholds = list(10,20,30,40,50,60,70,80,90,100)
+ var/static/list/fill_icon_thresholds = list(10, 20, 30, 40, 50, 60, 70, 80, 90, 100)
/// Inserted reagent container
var/obj/item/reagent_containers/beaker
/// Whether separated reagents should be moved back to container or destroyed.
@@ -213,9 +171,9 @@ GLOBAL_LIST_INIT(chem_master_containers, list(
/obj/machinery/chem_master/proc/load_printable_containers()
printable_containers = list(
- CAT_TUBES = GLOB.chem_master_containers[CAT_TUBES],
- CAT_PILLS = GLOB.chem_master_containers[CAT_PILLS],
- CAT_PATCHES = GLOB.chem_master_containers[CAT_PATCHES],
+ CAT_TUBES = GLOB.reagent_containers[CAT_TUBES],
+ CAT_PILLS = GLOB.reagent_containers[CAT_PILLS],
+ CAT_PATCHES = GLOB.reagent_containers[CAT_PATCHES],
)
/obj/machinery/chem_master/ui_assets(mob/user)
@@ -489,14 +447,10 @@ GLOBAL_LIST_INIT(chem_master_containers, list(
/obj/machinery/chem_master/condimaster/load_printable_containers()
printable_containers = list(
- CAT_CONDIMENTS = GLOB.chem_master_containers[CAT_CONDIMENTS],
+ CAT_CONDIMENTS = GLOB.reagent_containers[CAT_CONDIMENTS],
)
#undef TRANSFER_MODE_DESTROY
#undef TRANSFER_MODE_MOVE
#undef TARGET_BEAKER
#undef TARGET_BUFFER
-#undef CAT_CONDIMENTS
-#undef CAT_TUBES
-#undef CAT_PILLS
-#undef CAT_PATCHES
diff --git a/code/modules/reagents/chemistry/machinery/reagentgrinder.dm b/code/modules/reagents/chemistry/machinery/reagentgrinder.dm
index 26ebfa1d18d9f..d98044102d30a 100644
--- a/code/modules/reagents/chemistry/machinery/reagentgrinder.dm
+++ b/code/modules/reagents/chemistry/machinery/reagentgrinder.dm
@@ -308,18 +308,23 @@
if(!beaker || machine_stat & (NOPOWER|BROKEN) || beaker.reagents.holder_full())
return
operate_for(50, juicing = TRUE)
- for(var/obj/item/i in holdingitems)
+ for(var/obj/item/ingredient in holdingitems)
if(beaker.reagents.holder_full())
break
- var/obj/item/I = i
- if(I.juice_typepath)
- juice_item(I, user)
-/obj/machinery/reagentgrinder/proc/juice_item(obj/item/I, mob/user) //Juicing results can be found in respective object definitions
- if(!I.juice(beaker.reagents, user))
- to_chat(usr, span_danger("[src] shorts out as it tries to juice up [I], and transfers it back to storage."))
+ if(ingredient.flags_1 & HOLOGRAM_1)
+ to_chat(user, span_notice("You try to juice [ingredient], but it fades away!"))
+ qdel(ingredient)
+ continue
+
+ if(ingredient.juice_typepath)
+ juice_item(ingredient, user)
+
+/obj/machinery/reagentgrinder/proc/juice_item(obj/item/ingredient, mob/user) //Juicing results can be found in respective object definitions
+ if(!ingredient.juice(beaker.reagents, user))
+ to_chat(user, span_danger("[src] shorts out as it tries to juice up [ingredient], and transfers it back to storage."))
return
- remove_object(I)
+ remove_object(ingredient)
/obj/machinery/reagentgrinder/proc/grind(mob/user)
power_change()
@@ -327,21 +332,26 @@
return
operate_for(60)
warn_of_dust() // don't breathe this.
- for(var/i in holdingitems)
+ for(var/obj/item/ingredient in holdingitems)
if(beaker.reagents.holder_full())
break
- var/obj/item/I = i
- if(I.grind_results)
- grind_item(i, user)
-
-/obj/machinery/reagentgrinder/proc/grind_item(obj/item/I, mob/user) //Grind results can be found in respective object definitions
- if(!I.grind(beaker.reagents, user))
- if(isstack(I))
- to_chat(usr, span_notice("[src] attempts to grind as many pieces of [I] as possible."))
+
+ if(ingredient.flags_1 & HOLOGRAM_1)
+ to_chat(user, span_notice("You try to grind [ingredient], but it fades away!"))
+ qdel(ingredient)
+ continue
+
+ if(ingredient.grind_results)
+ grind_item(ingredient, user)
+
+/obj/machinery/reagentgrinder/proc/grind_item(obj/item/ingredient, mob/user) //Grind results can be found in respective object definitions
+ if(!ingredient.grind(beaker.reagents, user))
+ if(isstack(ingredient))
+ to_chat(user, span_notice("[src] attempts to grind as many pieces of [ingredient] as possible."))
else
- to_chat(usr, span_danger("[src] shorts out as it tries to grind up [I], and transfers it back to storage."))
+ to_chat(user, span_danger("[src] shorts out as it tries to grind up [ingredient], and transfers it back to storage."))
return
- remove_object(I)
+ remove_object(ingredient)
/obj/machinery/reagentgrinder/proc/mix(mob/user)
//For butter and other things that would change upon shaking or mixing
diff --git a/code/modules/reagents/chemistry/machinery/smoke_machine.dm b/code/modules/reagents/chemistry/machinery/smoke_machine.dm
index 3a8754bfe4f89..88072874e0fff 100644
--- a/code/modules/reagents/chemistry/machinery/smoke_machine.dm
+++ b/code/modules/reagents/chemistry/machinery/smoke_machine.dm
@@ -13,7 +13,6 @@
var/efficiency = 20
var/on = FALSE
var/cooldown = 0
- var/screen = "home"
var/useramount = 30 // Last used amount
var/setting = 1 // displayed range is 3 * setting
var/max_range = 3 // displayed max range is 3 * max range
@@ -126,18 +125,16 @@
/obj/machinery/smoke_machine/ui_data(mob/user)
var/data = list()
- var/TankContents[0]
- var/TankCurrentVolume = 0
+ var/tank_contents = list()
+ var/tank_current_volume = 0
for(var/datum/reagent/R in reagents.reagent_list)
- TankContents.Add(list(list("name" = R.name, "volume" = R.volume))) // list in a list because Byond merges the first list...
- TankCurrentVolume += R.volume
- data["TankContents"] = TankContents
- data["isTankLoaded"] = reagents.total_volume ? TRUE : FALSE
- data["TankCurrentVolume"] = reagents.total_volume ? reagents.total_volume : null
- data["TankMaxVolume"] = reagents.maximum_volume
+ tank_contents += list(list("name" = R.name, "volume" = R.volume)) // list in a list because Byond merges the first list...
+ tank_current_volume += R.volume
+ data["tankContents"] = tank_contents
+ data["tankCurrentVolume"] = reagents.total_volume ? reagents.total_volume : null
+ data["tankMaxVolume"] = reagents.maximum_volume
data["active"] = on
data["setting"] = setting
- data["screen"] = screen
data["maxSetting"] = max_range
return data
@@ -163,8 +160,5 @@
message_admins("[ADMIN_LOOKUPFLW(usr)] activated a smoke machine that contains [english_list(reagents.reagent_list)] at [ADMIN_VERBOSEJMP(src)].")
usr.log_message("activated a smoke machine that contains [english_list(reagents.reagent_list)]", LOG_GAME)
log_combat(usr, src, "has activated [src] which contains [english_list(reagents.reagent_list)] at [AREACOORD(src)].")
- if("goScreen")
- screen = params["screen"]
- . = TRUE
#undef REAGENTS_BASE_VOLUME
diff --git a/code/modules/reagents/chemistry/reagents/cat2_medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/cat2_medicine_reagents.dm
index 0c1dd26e17399..5b9d87e96c625 100644
--- a/code/modules/reagents/chemistry/reagents/cat2_medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/cat2_medicine_reagents.dm
@@ -45,7 +45,13 @@
. = UPDATE_MOB_HEALTH
if(good_kind_of_healing && !reaping && SPT_PROB(0.00005, seconds_per_tick)) //janken with the grim reaper!
- notify_ghosts("[affected_mob] has entered a game of rock-paper-scissors with death!", source = affected_mob, action = NOTIFY_ORBIT, header = "Who Will Win?")
+ notify_ghosts(
+ "[affected_mob] has entered a game of rock-paper-scissors with death!",
+ source = affected_mob,
+ action = NOTIFY_ORBIT,
+ header = "Who Will Win?",
+ notify_flags = NOTIFY_CATEGORY_DEFAULT,
+ )
reaping = TRUE
if(affected_mob.apply_status_effect(/datum/status_effect/necropolis_curse, CURSE_BLINDING))
helbent = TRUE
@@ -491,13 +497,15 @@
show_message = 0
if(!(methods & (PATCH|TOUCH|VAPOR)))
return
- var/harmies = min(carbies.getBruteLoss(), carbies.adjustBruteLoss(-1.25 * reac_volume, updating_health = FALSE, required_bodytype = affected_bodytype)*-1)
- var/burnies = min(carbies.getFireLoss(), carbies.adjustFireLoss(-1.25 * reac_volume, updating_health = FALSE, required_bodytype = affected_bodytype)*-1)
+ var/current_bruteloss = carbies.getBruteLoss() // because this will be changed after calling adjustBruteLoss()
+ var/current_fireloss = carbies.getFireLoss() // because this will be changed after calling adjustFireLoss()
+ var/harmies = clamp(carbies.adjustBruteLoss(-1.25 * reac_volume, updating_health = FALSE, required_bodytype = affected_bodytype), 0, current_bruteloss)
+ var/burnies = clamp(carbies.adjustFireLoss(-1.25 * reac_volume, updating_health = FALSE, required_bodytype = affected_bodytype), 0, current_fireloss)
for(var/i in carbies.all_wounds)
var/datum/wound/iter_wound = i
iter_wound.on_synthflesh(reac_volume)
var/need_mob_update = harmies + burnies
- need_mob_update += carbies.adjustToxLoss((harmies+burnies)*(0.5 + (0.25*(1-creation_purity))), updating_health = FALSE, required_biotype = affected_biotype) //0.5 - 0.75
+ need_mob_update = carbies.adjustToxLoss((harmies + burnies)*(0.5 + (0.25*(1-creation_purity))), updating_health = FALSE, required_biotype = affected_biotype) || need_mob_update //0.5 - 0.75
if(need_mob_update)
carbies.updatehealth()
diff --git a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
index be3069e7d4683..7a3dc6ab8fba6 100644
--- a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
@@ -1247,7 +1247,7 @@
/datum/reagent/consumable/ethanol/bananahonk/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
. = ..()
var/obj/item/organ/internal/liver/liver = drinker.get_organ_slot(ORGAN_SLOT_LIVER)
- if((liver && HAS_TRAIT(liver, TRAIT_COMEDY_METABOLISM)) || ismonkey(drinker))
+ if((liver && HAS_TRAIT(liver, TRAIT_COMEDY_METABOLISM)) || is_simian(drinker))
if(drinker.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 1 * REM * seconds_per_tick, updating_health = FALSE))
return UPDATE_MOB_HEALTH
diff --git a/code/modules/reagents/chemistry/reagents/drinks/drink_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks/drink_reagents.dm
index 8270b42f502d0..28039f8b5b63a 100644
--- a/code/modules/reagents/chemistry/reagents/drinks/drink_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drinks/drink_reagents.dm
@@ -116,7 +116,7 @@
/datum/reagent/consumable/banana/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
var/obj/item/organ/internal/liver/liver = affected_mob.get_organ_slot(ORGAN_SLOT_LIVER)
- if((liver && HAS_TRAIT(liver, TRAIT_COMEDY_METABOLISM)) || ismonkey(affected_mob))
+ if((liver && HAS_TRAIT(liver, TRAIT_COMEDY_METABOLISM)) || is_simian(affected_mob))
if(affected_mob.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 1 * REM * seconds_per_tick, updating_health = FALSE))
return UPDATE_MOB_HEALTH
@@ -670,7 +670,7 @@
/datum/reagent/consumable/monkey_energy/on_mob_metabolize(mob/living/affected_mob)
. = ..()
- if(ismonkey(affected_mob))
+ if(is_simian(affected_mob))
affected_mob.add_movespeed_modifier(/datum/movespeed_modifier/reagent/monkey_energy)
/datum/reagent/consumable/monkey_energy/on_mob_end_metabolize(mob/living/affected_mob)
diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm
index fb6e6ea2c0066..3701b16320ef6 100644
--- a/code/modules/reagents/chemistry/reagents/food_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm
@@ -222,6 +222,13 @@
nutriment_factor = 10
default_container = /obj/item/reagent_containers/condiment/olive_oil
+/datum/reagent/consumable/nutriment/fat/oil/corn
+ name = "Corn Oil"
+ description = "An oil derived from various types of corn."
+ color = "#302000" // rgb: 48, 32, 0
+ taste_description = "slime"
+ nutriment_factor = 5 //it's a very cheap oil
+
/datum/reagent/consumable/nutriment/organ_tissue
name = "Organ Tissue"
description = "Natural tissues that make up the bulk of organs, providing many vitamins and minerals."
diff --git a/code/modules/reagents/chemistry/reagents/reaction_agents_reagents.dm b/code/modules/reagents/chemistry/reagents/reaction_agents_reagents.dm
index c1da8b7424f2a..d6c4f0009b403 100644
--- a/code/modules/reagents/chemistry/reagents/reaction_agents_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/reaction_agents_reagents.dm
@@ -24,22 +24,23 @@
inverse_chem = null
fallback_icon = 'icons/obj/drinks/drink_effects.dmi'
fallback_icon_state = "acid_buffer_fallback"
- ///The strength of the buffer where (volume/holder.total_volume)*strength. So for 1u added to 50u the ph will decrease by 0.4
- var/strength = 30
//Consumes self on addition and shifts ph
/datum/reagent/reaction_agent/acidic_buffer/intercept_reagents_transfer(datum/reagents/target, amount)
. = ..()
if(!.)
return
+
+ //do the ph change
+ var/message
if(target.ph <= ph)
- target.my_atom.audible_message(span_warning("The beaker froths as the buffer is added, to no effect."))
- playsound(target.my_atom, 'sound/chemistry/bufferadd.ogg', 50, TRUE)
- holder.remove_reagent(type, amount)//Remove from holder because it's not transferred
- return
- var/ph_change = -((amount/target.total_volume)*strength)
- target.adjust_all_reagents_ph(ph_change, ph, 14)
- target.my_atom.audible_message(span_warning("The beaker fizzes as the ph changes!"))
+ message = "The beaker froths as the buffer is added, to no effect."
+ else
+ message = "The beaker froths as the pH changes!"
+ target.adjust_all_reagents_ph((-(amount / target.total_volume) * BUFFER_IONIZING_STRENGTH))
+
+ //give feedback & remove from holder because it's not transferred
+ target.my_atom.audible_message(span_warning(message))
playsound(target.my_atom, 'sound/chemistry/bufferadd.ogg', 50, TRUE)
holder.remove_reagent(type, amount)
@@ -51,21 +52,22 @@
inverse_chem = null
fallback_icon = 'icons/obj/drinks/drink_effects.dmi'
fallback_icon_state = "base_buffer_fallback"
- ///The strength of the buffer where (volume/holder.total_volume)*strength. So for 1u added to 50u the ph will increase by 0.4
- var/strength = 30
/datum/reagent/reaction_agent/basic_buffer/intercept_reagents_transfer(datum/reagents/target, amount)
. = ..()
if(!.)
return
+
+ //do the ph change
+ var/message
if(target.ph >= ph)
- target.my_atom.audible_message(span_warning("The beaker froths as the buffer is added, to no effect."))
- playsound(target.my_atom, 'sound/chemistry/bufferadd.ogg', 50, TRUE)
- holder.remove_reagent(type, amount)//Remove from holder because it's not transferred
- return
- var/ph_change = (amount/target.total_volume)*strength
- target.adjust_all_reagents_ph(ph_change, 0, ph)
- target.my_atom.audible_message(span_warning("The beaker froths as the ph changes!"))
+ message = "The beaker froths as the buffer is added, to no effect."
+ else
+ message = "The beaker froths as the pH changes!"
+ target.adjust_all_reagents_ph(((amount / target.total_volume) * BUFFER_IONIZING_STRENGTH))
+
+ //give feedback & remove from holder because it's not transferred
+ target.my_atom.audible_message(span_warning(message))
playsound(target.my_atom, 'sound/chemistry/bufferadd.ogg', 50, TRUE)
holder.remove_reagent(type, amount)
@@ -103,14 +105,14 @@
target.my_atom.audible_message(span_warning("The added reagent doesn't seem to do much."))
holder.remove_reagent(type, amount)
+///How much the reaction speed is sped up by - for 5u added to 100u, an additional step of 1 will be done up to a max of 2x
+#define SPEED_REAGENT_STRENGTH 20
+
/datum/reagent/reaction_agent/speed_agent
name = "Tempomyocin"
description = "This reagent will consume itself and speed up an ongoing reaction, modifying the current reaction's purity by it's own."
ph = 10
color = "#e61f82"
- ///How much the reaction speed is sped up by - for 5u added to 100u, an additional step of 1 will be done up to a max of 2x
- var/strength = 20
-
/datum/reagent/reaction_agent/speed_agent/intercept_reagents_transfer(datum/reagents/target, amount)
. = ..()
@@ -123,8 +125,10 @@
var/datum/equilibrium/reaction = _reaction
if(!reaction)
CRASH("[_reaction] is in the reaction list, but is not an equilibrium")
- var/power = (amount/reaction.target_vol)*strength
+ var/power = (amount / reaction.target_vol) * SPEED_REAGENT_STRENGTH
power *= creation_purity
power = clamp(power, 0, 2)
reaction.react_timestep(power, creation_purity)
holder.remove_reagent(type, amount)
+
+#undef SPEED_REAGENT_STRENGTH
diff --git a/code/modules/reagents/chemistry/recipes/medicine.dm b/code/modules/reagents/chemistry/recipes/medicine.dm
index 7c4c9dfe7e951..13d1882653e48 100644
--- a/code/modules/reagents/chemistry/recipes/medicine.dm
+++ b/code/modules/reagents/chemistry/recipes/medicine.dm
@@ -359,7 +359,7 @@
new /obj/item/stack/medical/suture/medicated(location)
/datum/chemical_reaction/medicine/medmesh
- required_reagents = list(/datum/reagent/cellulose = 20, /datum/reagent/consumable/aloejuice = 20, /datum/reagent/space_cleaner/sterilizine = 10)
+ required_reagents = list(/datum/reagent/cellulose = 10, /datum/reagent/consumable/aloejuice = 20, /datum/reagent/space_cleaner/sterilizine = 10)
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_HEALING | REACTION_TAG_BURN
/datum/chemical_reaction/medicine/medmesh/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
diff --git a/code/modules/reagents/chemistry/recipes/others.dm b/code/modules/reagents/chemistry/recipes/others.dm
index 2723d72eff136..dcdef3d350baf 100644
--- a/code/modules/reagents/chemistry/recipes/others.dm
+++ b/code/modules/reagents/chemistry/recipes/others.dm
@@ -31,7 +31,7 @@
/datum/chemical_reaction/glycerol
results = list(/datum/reagent/glycerol = 1)
- required_reagents = list(/datum/reagent/consumable/nutriment/fat/oil = 3, /datum/reagent/toxin/acid = 1)
+ required_reagents = list(/datum/reagent/consumable/nutriment/fat/oil/corn = 3, /datum/reagent/toxin/acid = 1)
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE | REACTION_TAG_EXPLOSIVE
/datum/chemical_reaction/sodiumchloride
diff --git a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
index d49976ac10eed..05bc622faa70f 100644
--- a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
+++ b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
@@ -542,9 +542,9 @@
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_EXPLOSIVE | REACTION_TAG_DANGEROUS
/datum/chemical_reaction/reagent_explosion/teslium_lightning/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- var/T1 = created_volume * 8e3 //100 units : Zap 3 times, with powers 8e5/2e6/4.8e6. Tesla revolvers have a power of 10000 for comparison.
- var/T2 = created_volume * 2e4
- var/T3 = created_volume * 4.8e4
+ var/T1 = created_volume * 20 //100 units : Zap 3 times, with powers 8e5/2e6/4.8e6. Tesla revolvers have a power of 10000 for comparison.
+ var/T2 = created_volume * 50
+ var/T3 = created_volume * 120
var/added_delay = 0.5 SECONDS
if(created_volume >= 75)
addtimer(CALLBACK(src, PROC_REF(zappy_zappy), holder, T1), added_delay)
@@ -557,10 +557,11 @@
addtimer(CALLBACK(src, PROC_REF(default_explode), holder, created_volume, modifier, strengthdiv), added_delay)
/datum/chemical_reaction/reagent_explosion/teslium_lightning/proc/zappy_zappy(datum/reagents/holder, power)
- if(QDELETED(holder.my_atom))
+ var/atom/holder_atom = holder.my_atom
+ if(QDELETED(holder_atom))
return
- tesla_zap(holder.my_atom, 7, power, zap_flags)
- playsound(holder.my_atom, 'sound/machines/defib_zap.ogg', 50, TRUE)
+ tesla_zap(source = holder_atom, zap_range = 7, power = power, cutoff = 1e3, zap_flags = zap_flags)
+ playsound(holder_atom, 'sound/machines/defib_zap.ogg', 50, TRUE)
/datum/chemical_reaction/reagent_explosion/teslium_lightning/heat
required_temp = 474
diff --git a/code/modules/reagents/reagent_containers/cups/_cup.dm b/code/modules/reagents/reagent_containers/cups/_cup.dm
index c15ba1a017f65..7ccc3209ab0d7 100644
--- a/code/modules/reagents/reagent_containers/cups/_cup.dm
+++ b/code/modules/reagents/reagent_containers/cups/_cup.dm
@@ -124,7 +124,7 @@
return
var/trans = reagents.trans_to(target, amount_per_transfer_from_this, transferred_by = user)
- to_chat(user, span_notice("You transfer [round(trans, 0.01)] unit\s of the solution to [target]."))
+ to_chat(user, span_notice("You transfer [trans] unit\s of the solution to [target]."))
else if(target.is_drainable()) //A dispenser. Transfer FROM it TO us.
if(!target.reagents.total_volume)
@@ -136,7 +136,7 @@
return
var/trans = target.reagents.trans_to(src, amount_per_transfer_from_this, transferred_by = user)
- to_chat(user, span_notice("You fill [src] with [round(trans, 0.01)] unit\s of the contents of [target]."))
+ to_chat(user, span_notice("You fill [src] with [trans] unit\s of the contents of [target]."))
target.update_appearance()
@@ -157,7 +157,7 @@
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
var/trans = target.reagents.trans_to(src, amount_per_transfer_from_this, transferred_by = user)
- to_chat(user, span_notice("You fill [src] with [round(trans, 0.01)] unit\s of the contents of [target]."))
+ to_chat(user, span_notice("You fill [src] with [trans] unit\s of the contents of [target]."))
target.update_appearance()
return SECONDARY_ATTACK_CONTINUE_CHAIN
diff --git a/code/modules/reagents/reagent_containers/cups/drinks.dm b/code/modules/reagents/reagent_containers/cups/drinks.dm
index 2cf34da1a6228..88b7d5baabfe3 100644
--- a/code/modules/reagents/reagent_containers/cups/drinks.dm
+++ b/code/modules/reagents/reagent_containers/cups/drinks.dm
@@ -227,8 +227,8 @@
custom_price = PAYCHECK_LOWER * 0.8
/obj/item/reagent_containers/cup/glass/waterbottle/Initialize(mapload)
- . = ..()
cap_overlay = mutable_appearance(cap_icon, cap_icon_state)
+ . = ..()
if(cap_on)
spillable = FALSE
update_appearance()
diff --git a/code/modules/reagents/reagent_containers/dropper.dm b/code/modules/reagents/reagent_containers/dropper.dm
index ac8d0af0d1c6b..beb6f3e6314cd 100644
--- a/code/modules/reagents/reagent_containers/dropper.dm
+++ b/code/modules/reagents/reagent_containers/dropper.dm
@@ -46,7 +46,7 @@
target.visible_message(span_danger("[user] tries to squirt something into [target]'s eyes, but fails!"), \
span_userdanger("[user] tries to squirt something into your eyes, but fails!"))
- to_chat(user, span_notice("You transfer [round(trans, 0.01)] unit\s of the solution."))
+ to_chat(user, span_notice("You transfer [trans] unit\s of the solution."))
update_appearance()
return
else if(isalien(target)) //hiss-hiss has no eyes!
@@ -66,7 +66,7 @@
log_combat(user, M, "squirted", R)
trans = src.reagents.trans_to(target, amount_per_transfer_from_this, transferred_by = user)
- to_chat(user, span_notice("You transfer [round(trans, 0.01)] unit\s of the solution."))
+ to_chat(user, span_notice("You transfer [trans] unit\s of the solution."))
update_appearance()
target.update_appearance()
@@ -82,7 +82,7 @@
var/trans = target.reagents.trans_to(src, amount_per_transfer_from_this, transferred_by = user)
- to_chat(user, span_notice("You fill [src] with [round(trans, 0.01)] unit\s of the solution."))
+ to_chat(user, span_notice("You fill [src] with [trans] unit\s of the solution."))
update_appearance()
target.update_appearance()
diff --git a/code/modules/reagents/reagent_containers/spray.dm b/code/modules/reagents/reagent_containers/spray.dm
index 20f72688644ef..52968cac22fe1 100644
--- a/code/modules/reagents/reagent_containers/spray.dm
+++ b/code/modules/reagents/reagent_containers/spray.dm
@@ -50,18 +50,23 @@
to_chat(user, span_warning("Not enough left!"))
return
- spray(target, user)
+ if(proximity_flag && (target.density || ismob(target)))
+ // If we're spraying an adjacent mob or a dense object, we start the spray on ITS tile rather than OURs
+ // This is so we can use a spray bottle to clean stuff like windows without getting blocked by passflags
+ spray(target, user, get_turf(target))
+ else
+ spray(target, user)
- playsound(src.loc, spray_sound, 50, TRUE, -6)
+ playsound(src, spray_sound, 50, TRUE, -6)
user.changeNext_move(CLICK_CD_RANGE*2)
user.newtonian_move(get_dir(target, user))
return
/// Handles creating a chem puff that travels towards the target atom, exposing reagents to everything it hits on the way.
-/obj/item/reagent_containers/spray/proc/spray(atom/target, mob/user)
+/obj/item/reagent_containers/spray/proc/spray(atom/target, mob/user, turf/start_turf = get_turf(src))
var/range = max(min(current_range, get_dist(src, target)), 1)
- var/obj/effect/decal/chempuff/reagent_puff = new /obj/effect/decal/chempuff(get_turf(src))
+ var/obj/effect/decal/chempuff/reagent_puff = new(start_turf)
reagent_puff.create_reagents(amount_per_transfer_from_this)
var/puff_reagent_left = range //how many turf, mob or dense objet we can react with before we consider the chem puff consumed
@@ -74,9 +79,7 @@
var/wait_step = max(round(2+3/range), 2)
var/puff_reagent_string = reagent_puff.reagents.get_reagent_log_string()
- var/turf/src_turf = get_turf(src)
-
- log_combat(user, src_turf, "fired a puff of reagents from", src, addition="with a range of \[[range]\], containing [puff_reagent_string].")
+ log_combat(user, start_turf, "fired a puff of reagents from", src, addition="with a range of \[[range]\], containing [puff_reagent_string].")
user.log_message("fired a puff of reagents from \a [src] with a range of \[[range]\] and containing [puff_reagent_string].", LOG_ATTACK)
// do_spray includes a series of step_towards and sleeps. As a result, it will handle deletion of the chempuff.
@@ -84,11 +87,20 @@
/// Handles exposing atoms to the reagents contained in a spray's chempuff. Deletes the chempuff when it's completed.
/obj/item/reagent_containers/spray/proc/do_spray(atom/target, wait_step, obj/effect/decal/chempuff/reagent_puff, range, puff_reagent_left, mob/user)
- var/datum/move_loop/our_loop = SSmove_manager.move_towards_legacy(reagent_puff, target, wait_step, timeout = range * wait_step, flags = MOVEMENT_LOOP_START_FAST, priority = MOVEMENT_ABOVE_SPACE_PRIORITY)
reagent_puff.user = user
reagent_puff.sprayer = src
reagent_puff.lifetime = puff_reagent_left
reagent_puff.stream = stream_mode
+
+ var/turf/target_turf = get_turf(target)
+ var/turf/start_turf = get_turf(reagent_puff)
+ if(target_turf == start_turf) // Don't need to bother movelooping if we don't move
+ reagent_puff.setDir(user.dir)
+ reagent_puff.spray_down_turf(target_turf)
+ reagent_puff.end_life()
+ return
+
+ var/datum/move_loop/our_loop = SSmove_manager.move_towards_legacy(reagent_puff, target, wait_step, timeout = range * wait_step, flags = MOVEMENT_LOOP_START_FAST, priority = MOVEMENT_ABOVE_SPACE_PRIORITY)
reagent_puff.RegisterSignal(our_loop, COMSIG_QDELETING, TYPE_PROC_REF(/obj/effect/decal/chempuff, loop_ended))
reagent_puff.RegisterSignal(our_loop, COMSIG_MOVELOOP_POSTPROCESS, TYPE_PROC_REF(/obj/effect/decal/chempuff, check_move))
@@ -181,7 +193,7 @@
if(do_after(user, 3 SECONDS, user))
if(reagents.total_volume >= amount_per_transfer_from_this)//if not empty
user.visible_message(span_suicide("[user] pulls the trigger!"))
- spray(user)
+ spray(user, user)
return BRUTELOSS
else
user.visible_message(span_suicide("[user] pulls the trigger...but \the [src] is empty!"))
diff --git a/code/modules/reagents/reagent_containers/syringes.dm b/code/modules/reagents/reagent_containers/syringes.dm
index beed3d17ba6f4..4366482460e77 100644
--- a/code/modules/reagents/reagent_containers/syringes.dm
+++ b/code/modules/reagents/reagent_containers/syringes.dm
@@ -145,7 +145,9 @@
/obj/item/reagent_containers/syringe/update_overlays()
. = ..()
- . += update_reagent_overlay()
+ var/list/reagent_overlays = update_reagent_overlay()
+ if(reagent_overlays)
+ . += reagent_overlays
/// Returns a list of overlays to add that relate to the reagents inside the syringe
/obj/item/reagent_containers/syringe/proc/update_reagent_overlay()
diff --git a/code/modules/reagents/reagent_dispenser.dm b/code/modules/reagents/reagent_dispenser.dm
index d1b7afd4b7800..20da98599d14a 100644
--- a/code/modules/reagents/reagent_dispenser.dm
+++ b/code/modules/reagents/reagent_dispenser.dm
@@ -178,7 +178,7 @@
// It did not account for how much fuel was actually in the tank at all, just the size of the tank.
// I encourage others to better scale these numbers in the future.
// As it stands this is a minor nerf in exchange for an easy bombing technique working that has been broken for a while.
- switch(volatiles.volume)
+ switch(fuel_amt)
if(25 to 150)
explosion(src, light_impact_range = 1, flame_range = 2)
if(150 to 300)
diff --git a/code/modules/recycling/sortingmachinery.dm b/code/modules/recycling/sortingmachinery.dm
index e6e98cf73761b..5fa4d5dd9b7dd 100644
--- a/code/modules/recycling/sortingmachinery.dm
+++ b/code/modules/recycling/sortingmachinery.dm
@@ -83,7 +83,7 @@
if(do_after(user, 50, target = object))
if(!user || user.stat != CONSCIOUS || user.loc != object || object.loc != src)
return
- to_chat(user, span_notice("You successfully removed [object]'s wrapping !"))
+ to_chat(user, span_notice("You successfully removed [object]'s wrapping!"))
object.forceMove(loc)
unwrap_contents()
post_unwrap_contents(user)
diff --git a/code/modules/religion/pyre_rites.dm b/code/modules/religion/pyre_rites.dm
index d974ef756f269..c36783e6b1223 100644
--- a/code/modules/religion/pyre_rites.dm
+++ b/code/modules/religion/pyre_rites.dm
@@ -43,7 +43,7 @@
/datum/religion_rites/burning_sacrifice
name = "Burning Offering"
- desc = "Sacrifice a buckled burning corpse for favor, the more burn damage the corpse has the more favor you will receive."
+ desc = "Sacrifice a buckled burning or husked corpse for favor, the more burn damage the corpse has the more favor you will receive."
ritual_length = 15 SECONDS
ritual_invocations = list("Burning body ...",
"... cleansed by the flame ...",
@@ -71,8 +71,8 @@
if(chosen_sacrifice.stat != DEAD)
to_chat(user, span_warning("You can only sacrifice dead bodies, this one is still alive!"))
return FALSE
- if(!chosen_sacrifice.on_fire)
- to_chat(user, span_warning("This corpse needs to be on fire to be sacrificed!"))
+ if(!chosen_sacrifice.on_fire && !HAS_TRAIT_FROM(chosen_sacrifice, TRAIT_HUSK, BURN))
+ to_chat(user, span_warning("This corpse needs to be on fire or husked to be sacrificed!"))
return FALSE
return ..()
@@ -82,8 +82,8 @@
to_chat(user, span_warning("The right sacrifice is no longer on the altar!"))
chosen_sacrifice = null
return FALSE
- if(!chosen_sacrifice.on_fire)
- to_chat(user, span_warning("The sacrifice is no longer on fire, it needs to burn until the end of the rite!"))
+ if(!chosen_sacrifice.on_fire && !HAS_TRAIT_FROM(chosen_sacrifice, TRAIT_HUSK, BURN))
+ to_chat(user, span_warning("The sacrifice has to be on fire or husked to finish the end of the rite!"))
chosen_sacrifice = null
return FALSE
if(chosen_sacrifice.stat != DEAD)
@@ -92,7 +92,7 @@
return FALSE
var/favor_gained = 100 + round(chosen_sacrifice.getFireLoss())
GLOB.religious_sect.adjust_favor(favor_gained, user)
- to_chat(user, span_notice("[GLOB.deity] absorbs the burning corpse and any trace of fire with it. [GLOB.deity] rewards you with [favor_gained] favor."))
+ to_chat(user, span_notice("[GLOB.deity] absorbs the charred corpse and any trace of fire with it. [GLOB.deity] rewards you with [favor_gained] favor."))
chosen_sacrifice.dust(force = TRUE)
playsound(get_turf(religious_tool), 'sound/effects/supermatter.ogg', 50, TRUE)
chosen_sacrifice = null
diff --git a/code/modules/religion/religion_sects.dm b/code/modules/religion/religion_sects.dm
index ebd90388fda64..3135fddeace8d 100644
--- a/code/modules/religion/religion_sects.dm
+++ b/code/modules/religion/religion_sects.dm
@@ -228,7 +228,7 @@
/datum/religion_sect/pyre/on_sacrifice(obj/item/flashlight/flare/candle/offering, mob/living/user)
if(!istype(offering))
return
- if(!offering.on)
+ if(!offering.light_on)
to_chat(user, span_notice("The candle needs to be lit to be offered!"))
return
to_chat(user, span_notice("[GLOB.deity] is pleased with your sacrifice."))
diff --git a/code/modules/research/designs.dm b/code/modules/research/designs.dm
index f5421be6da69a..4d21aef796219 100644
--- a/code/modules/research/designs.dm
+++ b/code/modules/research/designs.dm
@@ -39,7 +39,7 @@ other types of metals and chemistry for reagents).
var/make_reagent
/// What categories this design falls under. Used for sorting in production machines.
var/list/category = list()
- /// List of reagents required to create one unit of the product.
+ /// List of reagents required to create one unit of the product. Currently only supported by the limb grower.
var/list/reagents_list = list()
/// The maximum number of units of whatever is produced by this can be produced in one go.
var/maxstack = 1
diff --git a/code/modules/research/designs/autolathe/engineering_designs.dm b/code/modules/research/designs/autolathe/engineering_designs.dm
index 4d065ff1dda2c..6249f5c645a1f 100644
--- a/code/modules/research/designs/autolathe/engineering_designs.dm
+++ b/code/modules/research/designs/autolathe/engineering_designs.dm
@@ -392,3 +392,37 @@
RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MOUNTS,
)
departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING
+
+/datum/design/tram_controller
+ name = "Tram Controller Cabinet"
+ id = "tram_controller"
+ build_type = PROTOLATHE
+ materials = list(
+ /datum/material/titanium = SHEET_MATERIAL_AMOUNT * 4,
+ /datum/material/iron = SHEET_MATERIAL_AMOUNT * 2,
+ /datum/material/gold = SHEET_MATERIAL_AMOUNT * 7,
+ /datum/material/silver = SHEET_MATERIAL_AMOUNT * 7,
+ /datum/material/diamond = SHEET_MATERIAL_AMOUNT * 4,
+ )
+ build_path = /obj/item/wallframe/tram/controller
+ category = list(
+ RND_CATEGORY_INITIAL,
+ RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MOUNTS,
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING
+
+/datum/design/tram_display
+ name = "Tram Indicator Display"
+ id = "tram_display"
+ build_type = PROTOLATHE
+ materials = list(
+ /datum/material/titanium = SHEET_MATERIAL_AMOUNT * 4,
+ /datum/material/iron = SHEET_MATERIAL_AMOUNT * 1,
+ /datum/material/glass =SHEET_MATERIAL_AMOUNT * 2,
+ )
+ build_path = /obj/item/wallframe/indicator_display
+ category = list(
+ RND_CATEGORY_INITIAL,
+ RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MOUNTS,
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING
diff --git a/code/modules/research/designs/autolathe/service_designs.dm b/code/modules/research/designs/autolathe/service_designs.dm
index 687e85d64361b..cccef8e740dfa 100644
--- a/code/modules/research/designs/autolathe/service_designs.dm
+++ b/code/modules/research/designs/autolathe/service_designs.dm
@@ -527,6 +527,18 @@
)
departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+/datum/design/fish_case
+ name = "Stasis Fish Case"
+ id = "fish_case"
+ build_type = AUTOLATHE | PROTOLATHE | AWAY_LATHE
+ materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT, /datum/material/plastic = SMALL_MATERIAL_AMOUNT)
+ build_path = /obj/item/storage/fish_case
+ category = list(
+ RND_CATEGORY_INITIAL,
+ RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_SERVICE,
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+
/datum/design/ticket_machine
name = "Ticket Machine Frame"
id = "ticket_machine"
diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index 3b0bf62c26781..a73745399e11d 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -734,6 +734,26 @@
)
departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_SCIENCE
+/datum/design/board/crossing_signal
+ name = "Crossing Signal Board"
+ desc = "The circuit board for a tram crossing signal."
+ id = "crossing_signal"
+ build_path = /obj/item/circuitboard/machine/crossing_signal
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_TELECOMMS
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING
+
+/datum/design/board/guideway_sensor
+ name = "Guideway Sensor Board"
+ desc = "The circuit board for a tram proximity sensor."
+ id = "guideway_sensor"
+ build_path = /obj/item/circuitboard/machine/guideway_sensor
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_TELECOMMS
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING
+
/datum/design/board/limbgrower
name = "Limb Grower Board"
desc = "The circuit board for a limb grower."
diff --git a/code/modules/research/designs/mechfabricator_designs.dm b/code/modules/research/designs/mechfabricator_designs.dm
index 8290be9b2ee0a..e9d31c0ba85c2 100644
--- a/code/modules/research/designs/mechfabricator_designs.dm
+++ b/code/modules/research/designs/mechfabricator_designs.dm
@@ -1183,7 +1183,7 @@
category = list(
RND_CATEGORY_MECHFAB_CYBORG_MODULES + RND_SUBCATEGORY_MECHFAB_CYBORG_MODULES_SERVICE
)
-
+
/datum/design/borg_upgrade_service_apparatus
name = "Service Apparatus"
id = "borg_upgrade_service_apparatus"
diff --git a/code/modules/research/designs/medical_designs.dm b/code/modules/research/designs/medical_designs.dm
index 285878e7f2647..150c69bc21fd2 100644
--- a/code/modules/research/designs/medical_designs.dm
+++ b/code/modules/research/designs/medical_designs.dm
@@ -359,6 +359,28 @@
)
departmental_flags = DEPARTMENT_BITFLAG_MEDICAL
+/datum/design/penlight
+ name = "Penlight"
+ id = "penlight"
+ build_type = PROTOLATHE | AWAY_LATHE
+ materials = list(/datum/material/iron =SMALL_MATERIAL_AMOUNT*5, /datum/material/glass =SMALL_MATERIAL_AMOUNT*0.5)
+ build_path = /obj/item/flashlight/pen
+ category = list(
+ RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_MEDICAL
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_MEDICAL
+
+/datum/design/penlight_paramedic
+ name = "Paramedic Penlight"
+ id = "penlight_paramedic"
+ build_type = PROTOLATHE | AWAY_LATHE
+ materials = list(/datum/material/iron =SMALL_MATERIAL_AMOUNT*5, /datum/material/glass =SMALL_MATERIAL_AMOUNT*1)
+ build_path = /obj/item/flashlight/pen/paramedic
+ category = list(
+ RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_MEDICAL
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_MEDICAL
+
/////////////////////////////////////////
//////////Cybernetic Implants////////////
/////////////////////////////////////////
diff --git a/code/modules/research/designs/misc_designs.dm b/code/modules/research/designs/misc_designs.dm
index 3f4d4b8f8ec2a..a99c5e24217c9 100644
--- a/code/modules/research/designs/misc_designs.dm
+++ b/code/modules/research/designs/misc_designs.dm
@@ -588,7 +588,6 @@
id = "paint_remover"
build_type = PROTOLATHE | AWAY_LATHE
materials = list(/datum/material/iron =HALF_SHEET_MATERIAL_AMOUNT)
- reagents_list = list(/datum/reagent/acetone = 60)
build_path = /obj/item/paint/paint_remover
category = list(
RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_JANITORIAL
@@ -820,6 +819,17 @@
)
departmental_flags = DEPARTMENT_BITFLAG_SECURITY
+/datum/design/sec_pen
+ name = "Security Pen"
+ id = "sec_pen"
+ build_type = PROTOLATHE | AUTOLATHE
+ materials = list(/datum/material/iron =SMALL_MATERIAL_AMOUNT)
+ build_path = /obj/item/pen/red/security
+ category = list(
+ RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_SECURITY
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SECURITY
+
/datum/design/plumbing_rcd
name = "Plumbing Constructor"
id = "plumbing_rcd"
diff --git a/code/modules/research/designs/weapon_designs.dm b/code/modules/research/designs/weapon_designs.dm
index 59cfd2643cec5..f0bafdaec29d4 100644
--- a/code/modules/research/designs/weapon_designs.dm
+++ b/code/modules/research/designs/weapon_designs.dm
@@ -365,6 +365,20 @@
)
departmental_flags = DEPARTMENT_BITFLAG_SECURITY
+/datum/design/lasershell
+ name = "Scatter Laser Shotgun Shell (Lethal)"
+ desc = "A high-tech shotgun shell which houses an internal capacitor and laser focusing crystal inside of a shell casing. \
+ Able to be fired from conventional ballistic shotguns with minimal rifling degradation. Also leaves most targets covered \
+ in grotesque burns."
+ id = "lasershell"
+ build_type = PROTOLATHE | AWAY_LATHE
+ materials = list(/datum/material/iron = HALF_SHEET_MATERIAL_AMOUNT, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 2, /datum/material/gold = HALF_SHEET_MATERIAL_AMOUNT)
+ build_path = /obj/item/ammo_casing/shotgun/scatterlaser
+ category = list(
+ RND_CATEGORY_WEAPONS + RND_SUBCATEGORY_WEAPONS_AMMO
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SECURITY
+
/datum/design/techshell
name = "Unloaded Technological Shotshell"
desc = "A high-tech shotgun shell which can be crafted into more advanced shells to produce unique effects. \
diff --git a/code/modules/research/machinery/_production.dm b/code/modules/research/machinery/_production.dm
index e5a426d3a1ef1..4668753f0483f 100644
--- a/code/modules/research/machinery/_production.dm
+++ b/code/modules/research/machinery/_production.dm
@@ -45,7 +45,6 @@
TRUE, \
)
- create_reagents(0, OPENCONTAINER)
RefreshParts()
update_icon(UPDATE_OVERLAYS)
@@ -185,13 +184,6 @@
/obj/machinery/rnd/production/proc/calculate_efficiency()
efficiency_coeff = 1
- if(reagents)
- reagents.maximum_volume = 0
-
- for(var/obj/item/reagent_containers/cup/beaker in component_parts)
- reagents.maximum_volume += beaker.volume
- beaker.reagents.trans_to(src, beaker.reagents.total_volume)
-
if(materials)
var/total_storage = 0
@@ -207,12 +199,6 @@
efficiency_coeff = max(total_rating, 0)
-/obj/machinery/rnd/production/on_deconstruction()
- for(var/obj/item/reagent_containers/cup/G in component_parts)
- reagents.trans_to(G, G.reagents.maximum_volume)
-
- return ..()
-
/obj/machinery/rnd/production/proc/do_print(path, amount)
for(var/i in 1 to amount)
new path(get_turf(src))
@@ -263,14 +249,10 @@
print_quantity = clamp(print_quantity, 1, 50)
var/coefficient = build_efficiency(design.build_path)
- //check if sufficient materials/reagents are available
+ //check if sufficient materials are available
if(!materials.mat_container.has_materials(design.materials, coefficient, print_quantity))
say("Not enough materials to complete prototype[print_quantity > 1? "s" : ""].")
return FALSE
- for(var/reagent in design.reagents_list)
- if(!reagents.has_reagent(reagent, design.reagents_list[reagent] * print_quantity * coefficient))
- say("Not enough reagents to complete prototype[print_quantity > 1? "s" : ""].")
- return FALSE
//use power
var/power = active_power_usage
@@ -305,8 +287,6 @@
//consume materials
materials.use_materials(design.materials, coefficient, print_quantity, "built", "[design.name]")
- for(var/reagent in design.reagents_list)
- reagents.remove_reagent(reagent, design.reagents_list[reagent] * print_quantity * coefficient)
//produce item
busy = TRUE
if(production_animation)
diff --git a/code/modules/research/server.dm b/code/modules/research/server.dm
index 45a0a520fa0d4..56be6864b5467 100644
--- a/code/modules/research/server.dm
+++ b/code/modules/research/server.dm
@@ -154,7 +154,7 @@
if(HDD_OVERLOADED)
. += "The front panel is dangling open. The hdd inside is destroyed and the wires are all burned."
-/obj/machinery/rnd/server/master/tool_act(mob/living/user, obj/item/tool, tool_type)
+/obj/machinery/rnd/server/master/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
// Only antags are given the training and knowledge to disassemble this thing.
if(is_special_character(user))
return ..()
diff --git a/code/modules/research/techweb/_techweb.dm b/code/modules/research/techweb/_techweb.dm
index 8a06607ec59a8..bd1e5cc4a80d8 100644
--- a/code/modules/research/techweb/_techweb.dm
+++ b/code/modules/research/techweb/_techweb.dm
@@ -365,6 +365,10 @@
add_experiments(unlocked_node.discount_experiments)
update_node_status(unlocked_node)
+ // Gain more new experiments
+ if (node.experiments_to_unlock.len)
+ add_experiments(node.experiments_to_unlock)
+
// Unlock what the research actually unlocks
for(var/id in node.design_ids)
add_design_by_id(id)
diff --git a/code/modules/research/techweb/_techweb_node.dm b/code/modules/research/techweb/_techweb_node.dm
index ae50ea7f65f9e..f5e1481e62cff 100644
--- a/code/modules/research/techweb/_techweb_node.dm
+++ b/code/modules/research/techweb/_techweb_node.dm
@@ -36,6 +36,8 @@
var/list/required_experiments = list()
/// If completed, these experiments give a specific point amount discount to the node.area
var/list/discount_experiments = list()
+ /// When this node is completed, allows these experiments to be performed.
+ var/list/experiments_to_unlock = list()
/// Whether or not this node should show on the wiki
var/show_on_wiki = TRUE
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index 441e6f0038b5e..3da5c4bb23405 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -48,6 +48,7 @@
"experimentor",
"extinguisher",
"fax",
+ "fish_case",
"fishing_rod",
"fishing_portal_generator",
"flashlight",
@@ -97,6 +98,7 @@
"sec_dart",
"sec_Islug",
"sec_rshot",
+ "sec_pen",
"servingtray",
"shaker",
"shot_glass",
@@ -128,6 +130,14 @@
"voice_analyzer",
"watering_can",
)
+ experiments_to_unlock = list(
+ /datum/experiment/autopsy/nonhuman,
+ /datum/experiment/scanning/random/material/medium/one,
+ /datum/experiment/scanning/random/material/medium/three,
+ /datum/experiment/scanning/random/material/hard/one,
+ /datum/experiment/scanning/random/material/hard/two,
+ /datum/experiment/scanning/people/novel_organs,
+ )
/datum/techweb_node/mmi
id = "mmi"
@@ -289,6 +299,7 @@
"plumbing_rcd_service",
"plumbing_rcd_sci",
"portable_chem_mixer",
+ "penlight",
"retractor",
"scalpel",
"stethoscope",
@@ -407,6 +418,7 @@
"medigel",
"medipen_refiller",
"pandemic",
+ "penlight_paramedic",
"soda_dispenser",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
@@ -630,6 +642,7 @@
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 12500)
discount_experiments = list(/datum/experiment/scanning/random/material/easy = 7500)
+ experiments_to_unlock = list(/datum/experiment/scanning/points/machinery_pinpoint_scan/tier2_microlaser)
/datum/techweb_node/adv_engi
id = "adv_engi"
@@ -911,7 +924,7 @@
id = "adv_robotics"
display_name = "Advanced Robotics Research"
description = "Machines using actual neural networks to simulate human lives."
- prereq_ids = list("neural_programming", "robotics")
+ prereq_ids = list("robotics")
design_ids = list(
"mmi_posi",
)
@@ -1243,6 +1256,19 @@
"s_treatment",
)
+/datum/techweb_node/tram
+ id = "tram"
+ display_name = "Tram Technology"
+ description = "Technology for linear induction transportation systems."
+ prereq_ids = list("telecomms")
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 1500)
+ design_ids = list(
+ "tram_controller",
+ "tram_display",
+ "crossing_signal",
+ "guideway_sensor",
+ )
+
/datum/techweb_node/integrated_hud
id = "integrated_HUDs"
display_name = "Integrated HUDs"
@@ -1577,6 +1603,7 @@
design_ids = list(
"pin_testing",
"tele_shield",
+ "lasershell",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 20000)
discount_experiments = list(/datum/experiment/ordnance/explosive/pressurebomb = 10000)
diff --git a/code/modules/research/xenobiology/crossbreeding/_status_effects.dm b/code/modules/research/xenobiology/crossbreeding/_status_effects.dm
index 6d4d6a7b27200..04d37c62b7caa 100644
--- a/code/modules/research/xenobiology/crossbreeding/_status_effects.dm
+++ b/code/modules/research/xenobiology/crossbreeding/_status_effects.dm
@@ -995,7 +995,7 @@
healing_types += CLONE
if(length(healing_types))
- owner.apply_damage_type(-heal_amount, damagetype = pick(healing_types))
+ owner.heal_damage_type(heal_amount, damagetype = pick(healing_types))
owner.adjust_nutrition(3)
drained.apply_damage(heal_amount * DRAIN_DAMAGE_MULTIPLIER, damagetype = BRUTE, spread_damage = TRUE)
diff --git a/code/modules/research/xenobiology/crossbreeding/_weapons.dm b/code/modules/research/xenobiology/crossbreeding/_weapons.dm
index 61f4e7a72e0c8..a7e54fa2c7030 100644
--- a/code/modules/research/xenobiology/crossbreeding/_weapons.dm
+++ b/code/modules/research/xenobiology/crossbreeding/_weapons.dm
@@ -128,7 +128,7 @@ Slimecrossing Weapons
icon_state = "pulse0_bl"
hitsound = 'sound/effects/splat.ogg'
-/obj/projectile/magic/bloodchill/on_hit(mob/living/target)
+/obj/projectile/magic/bloodchill/on_hit(mob/living/target, blocked = 0, pierce_hit)
. = ..()
if(isliving(target))
target.apply_status_effect(/datum/status_effect/bloodchill)
diff --git a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
index 0152b343c45df..5e9045e751f98 100644
--- a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
+++ b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
@@ -684,30 +684,6 @@
virus_suspectibility = 0
resulting_atoms = list(/mob/living/basic/butterfly = 3)
-/datum/micro_organism/cell_line/leaper
- desc = "atypical amphibian cells"
- required_reagents = list(
- /datum/reagent/consumable/nutriment/protein,
- /datum/reagent/ants,
- /datum/reagent/consumable/eggyolk,
- /datum/reagent/medicine/c2/synthflesh)
-
- supplementary_reagents = list(
- /datum/reagent/growthserum = 4,
- /datum/reagent/drug/blastoff = 3,
- /datum/reagent/drug/space_drugs = 2,
- /datum/reagent/consumable/ethanol/eggnog = 2,
- /datum/reagent/consumable/vanilla = 2,
- /datum/reagent/consumable/banana = 1,
- /datum/reagent/consumable/nutriment/vitamin = 1)
-
- suppressive_reagents = list(
- /datum/reagent/toxin/cyanide = -5,
- /datum/reagent/consumable/mold = -2,
- /datum/reagent/toxin/spore = -1)
-
- resulting_atoms = list(/mob/living/simple_animal/hostile/jungle/leaper = 1)
-
/datum/micro_organism/cell_line/mega_arachnid
desc = "pseudoarachnoid cells"
required_reagents = list(
diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm
index b3945fe60c437..2bbefb64ec343 100644
--- a/code/modules/research/xenobiology/xenobiology.dm
+++ b/code/modules/research/xenobiology/xenobiology.dm
@@ -705,19 +705,28 @@
balloon_alert(user, "offering...")
being_used = TRUE
- var/list/candidates = poll_candidates_for_mob("Do you want to play as [dumb_mob.name]?", ROLE_SENTIENCE, ROLE_SENTIENCE, 5 SECONDS, dumb_mob, POLL_IGNORE_SENTIENCE_POTION) // see poll_ignore.dm
- if(!LAZYLEN(candidates))
+ var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), user, dumb_mob)
+ dumb_mob.AddComponent(/datum/component/orbit_poll, \
+ ignore_key = POLL_IGNORE_SENTIENCE_POTION, \
+ job_bans = ROLE_SENTIENCE, \
+ to_call = to_call, \
+ )
+
+/// Assign the chosen ghost to the mob
+/obj/item/slimepotion/slime/sentience/proc/on_poll_concluded(mob/user, mob/living/dumb_mob, mob/dead/observer/ghost)
+ if(isnull(ghost))
balloon_alert(user, "try again later!")
being_used = FALSE
- return ..()
+ return
- var/mob/dead/observer/C = pick(candidates)
- dumb_mob.key = C.key
+ dumb_mob.key = ghost.key
dumb_mob.mind.enslave_mind_to_creator(user)
SEND_SIGNAL(dumb_mob, COMSIG_SIMPLEMOB_SENTIENCEPOTION, user)
+
if(isanimal(dumb_mob))
var/mob/living/simple_animal/smart_animal = dumb_mob
smart_animal.sentience_act()
+
dumb_mob.mind.add_antag_datum(/datum/antagonist/sentient_creature)
balloon_alert(user, "success")
after_success(user, dumb_mob)
diff --git a/code/modules/security_levels/security_level_datums.dm b/code/modules/security_levels/security_level_datums.dm
index 175b79d1c8771..b3402f643c6bf 100644
--- a/code/modules/security_levels/security_level_datums.dm
+++ b/code/modules/security_levels/security_level_datums.dm
@@ -9,6 +9,8 @@
/datum/security_level
/// The name of this security level.
var/name = "not set"
+ /// The color of our announcement divider.
+ var/announcement_color = "default"
/// The numerical level of this security level, see defines for more information.
var/number_level = -1
/// The sound that we will play when this security level is set
@@ -22,7 +24,7 @@
/// Our announcement when lowering to this level
var/lowering_to_announcement
/// Our announcement when elevating to this level
- var/elevating_to_announcemnt
+ var/elevating_to_announcement
/// Our configuration key for lowering to text, if set, will override the default lowering to announcement.
var/lowering_to_configuration_key
/// Our configuration key for elevating to text, if set, will override the default elevating to announcement.
@@ -33,7 +35,7 @@
if(lowering_to_configuration_key) // I'm not sure about you, but isn't there an easier way to do this?
lowering_to_announcement = global.config.Get(lowering_to_configuration_key)
if(elevating_to_configuration_key)
- elevating_to_announcemnt = global.config.Get(elevating_to_configuration_key)
+ elevating_to_announcement = global.config.Get(elevating_to_configuration_key)
/**
* GREEN
@@ -42,6 +44,7 @@
*/
/datum/security_level/green
name = "green"
+ announcement_color = "green"
sound = 'sound/misc/notice2.ogg' // Friendly beep
number_level = SEC_LEVEL_GREEN
lowering_to_configuration_key = /datum/config_entry/string/alert_green
@@ -54,6 +57,7 @@
*/
/datum/security_level/blue
name = "blue"
+ announcement_color = "blue"
sound = 'sound/misc/notice1.ogg' // Angry alarm
number_level = SEC_LEVEL_BLUE
lowering_to_configuration_key = /datum/config_entry/string/alert_blue_downto
@@ -67,6 +71,7 @@
*/
/datum/security_level/red
name = "red"
+ announcement_color = "red"
sound = 'sound/misc/notice3.ogg' // More angry alarm
number_level = SEC_LEVEL_RED
lowering_to_configuration_key = /datum/config_entry/string/alert_red_downto
@@ -80,6 +85,7 @@
*/
/datum/security_level/delta
name = "delta"
+ announcement_color = "purple"
sound = 'sound/misc/airraid.ogg' // Air alarm to signify importance
number_level = SEC_LEVEL_DELTA
elevating_to_configuration_key = /datum/config_entry/string/alert_delta
diff --git a/code/modules/shuttle/battlecruiser_starfury.dm b/code/modules/shuttle/battlecruiser_starfury.dm
index ab1f6802d43ee..054de00504eb5 100644
--- a/code/modules/shuttle/battlecruiser_starfury.dm
+++ b/code/modules/shuttle/battlecruiser_starfury.dm
@@ -172,14 +172,14 @@
"The battlecruiser has an object of interest: [our_candidate]!",
source = our_candidate,
action = NOTIFY_ORBIT,
- header = "Something's Interesting!"
- )
+ header = "Something's Interesting!",
+ )
else
notify_ghosts(
"The battlecruiser has an object of interest: [spawner]!",
source = spawner,
action = NOTIFY_ORBIT,
- header="Something's Interesting!"
- )
+ header="Something's Interesting!",
+ )
priority_announce("Unidentified armed ship detected near the station.")
diff --git a/code/modules/shuttle/emergency.dm b/code/modules/shuttle/emergency.dm
index 4a9feda1fc882..0124db8cc22d6 100644
--- a/code/modules/shuttle/emergency.dm
+++ b/code/modules/shuttle/emergency.dm
@@ -358,7 +358,13 @@
else
SSshuttle.emergency_last_call_loc = null
- priority_announce("The emergency shuttle has been called. [red_alert ? "Red Alert state confirmed: Dispatching priority shuttle. " : "" ]It will arrive in [timeLeft(600)] minutes.[reason][SSshuttle.emergency_last_call_loc ? "\n\nCall signal traced. Results can be viewed on any communications console." : "" ][SSshuttle.admin_emergency_no_recall ? "\n\nWarning: Shuttle recall subroutines disabled; Recall not possible." : ""]", null, ANNOUNCER_SHUTTLECALLED, "Priority")
+ priority_announce(
+ text = "The emergency shuttle has been called. [red_alert ? "Red Alert state confirmed: Dispatching priority shuttle. " : "" ]It will arrive in [(timeLeft(60 SECONDS))] minutes.[reason][SSshuttle.emergency_last_call_loc ? "\n\nCall signal traced. Results can be viewed on any communications console." : "" ][SSshuttle.admin_emergency_no_recall ? "\n\nWarning: Shuttle recall subroutines disabled; Recall not possible." : ""]",
+ title = "Emergency Shuttle Dispatched",
+ sound = ANNOUNCER_SHUTTLECALLED,
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "orange",
+ )
/obj/docking_port/mobile/emergency/cancel(area/signalOrigin)
if(mode != SHUTTLE_CALL)
@@ -373,7 +379,13 @@
SSshuttle.emergency_last_call_loc = signalOrigin
else
SSshuttle.emergency_last_call_loc = null
- priority_announce("The emergency shuttle has been recalled.[SSshuttle.emergency_last_call_loc ? " Recall signal traced. Results can be viewed on any communications console." : "" ]", null, ANNOUNCER_SHUTTLERECALLED, "Priority")
+ priority_announce(
+ text = "The emergency shuttle has been recalled.[SSshuttle.emergency_last_call_loc ? " Recall signal traced. Results can be viewed on any communications console." : "" ]",
+ title = "Emergency Shuttle Recalled",
+ sound = ANNOUNCER_SHUTTLERECALLED,
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "orange",
+ )
SSticker.emergency_reason = null
@@ -462,7 +474,13 @@
mode = SHUTTLE_DOCKED
setTimer(SSshuttle.emergency_dock_time)
send2adminchat("Server", "The Emergency Shuttle has docked with the station.")
- priority_announce("[SSshuttle.emergency] has docked with the station. You have [timeLeft(600)] minutes to board the Emergency Shuttle.", null, ANNOUNCER_SHUTTLEDOCK, "Priority")
+ priority_announce(
+ text = "[SSshuttle.emergency] has docked with the station. You have [DisplayTimeText(SSshuttle.emergency_dock_time)] to board the emergency shuttle.",
+ title = "Emergency Shuttle Arrival",
+ sound = ANNOUNCER_SHUTTLEDOCK,
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "orange",
+ )
ShuttleDBStuff()
addtimer(CALLBACK(src, PROC_REF(announce_shuttle_events)), 20 SECONDS)
@@ -514,7 +532,12 @@
mode = SHUTTLE_ESCAPE
launch_status = ENDGAME_LAUNCHED
setTimer(SSshuttle.emergency_escape_time * engine_coeff)
- priority_announce("The Emergency Shuttle has left the station. Estimate [timeLeft(600)] minutes until the shuttle docks at Central Command.", null, null, "Priority")
+ priority_announce(
+ text = "The emergency shuttle has left the station. Estimate [timeLeft(60 SECONDS)] minutes until the shuttle docks at [command_name()].",
+ title = "Emergency Shuttle Departure",
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "orange",
+ )
INVOKE_ASYNC(SSticker, TYPE_PROC_REF(/datum/controller/subsystem/ticker, poll_hearts))
SSmapping.mapvote() //If no map vote has been run yet, start one.
@@ -579,7 +602,12 @@
mode = SHUTTLE_ESCAPE
launch_status = ENDGAME_LAUNCHED
setTimer(SSshuttle.emergency_escape_time)
- priority_announce("The Emergency Shuttle is preparing for direct jump. Estimate [timeLeft(600)] minutes until the shuttle docks at Central Command.", null, null, "Priority")
+ priority_announce(
+ text = "The emergency shuttle is preparing for direct jump. Estimate [timeLeft(60 SECONDS)] minutes until the shuttle docks at [command_name()].",
+ title = "Emergency Shuttle Transit Failure",
+ sender_override = "Emergency Shuttle Uplink Alert",
+ color_override = "orange",
+ )
///Generate a list of events to run during the departure
/obj/docking_port/mobile/emergency/proc/setup_shuttle_events()
diff --git a/code/modules/shuttle/shuttle.dm b/code/modules/shuttle/shuttle.dm
index 723514ae94397..6284461a337e3 100644
--- a/code/modules/shuttle/shuttle.dm
+++ b/code/modules/shuttle/shuttle.dm
@@ -149,7 +149,7 @@
#ifdef DOCKING_PORT_HIGHLIGHT
//Debug proc used to highlight bounding area
/obj/docking_port/proc/highlight(_color = "#f00")
- invisibility = 0
+ SetInvisibility(INVISIBILITY_NONE)
SET_PLANE_IMPLICIT(src, GHOST_PLANE)
var/list/L = return_coords()
var/turf/T0 = locate(L[1],L[2],z)
diff --git a/code/modules/shuttle/spaceship_navigation_beacon.dm b/code/modules/shuttle/spaceship_navigation_beacon.dm
index 4ae91af588a23..d46396a0e8ba9 100644
--- a/code/modules/shuttle/spaceship_navigation_beacon.dm
+++ b/code/modules/shuttle/spaceship_navigation_beacon.dm
@@ -101,7 +101,7 @@
/obj/item/folded_navigation_gigabeacon/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/deployable, 3 SECONDS, /obj/machinery/spaceship_navigation_beacon, delete_on_use = TRUE)
+ AddComponent(/datum/component/deployable, 3 SECONDS, /obj/machinery/spaceship_navigation_beacon)
/obj/item/folded_navigation_gigabeacon/examine()
.=..()
diff --git a/code/modules/shuttle/special.dm b/code/modules/shuttle/special.dm
index 5c1e21c56fb4e..0a18857efeda7 100644
--- a/code/modules/shuttle/special.dm
+++ b/code/modules/shuttle/special.dm
@@ -154,7 +154,7 @@
// Bar staff, GODMODE mobs(as long as they stay in the shuttle) that just want to make sure people have drinks
// and a good time.
-/mob/living/simple_animal/drone/snowflake/bardrone
+/mob/living/basic/drone/snowflake/bardrone
name = "Bardrone"
desc = "A barkeeping drone, a robot built to tend bars."
hacked = TRUE
@@ -166,9 +166,8 @@
initial_language_holder = /datum/language_holder/universal
default_storage = null
-/mob/living/simple_animal/drone/snowflake/bardrone/Initialize(mapload)
+/mob/living/basic/drone/snowflake/bardrone/Initialize(mapload)
. = ..()
- access_card.add_access(list(ACCESS_CENT_BAR))
AddComponentFrom(ROUNDSTART_TRAIT, /datum/component/area_based_godmode, area_type = /area/shuttle/escape, allow_area_subtypes = TRUE)
/mob/living/simple_animal/hostile/alien/maid/barmaid
@@ -230,6 +229,9 @@
if(is_bartender_job(human_user.mind?.assigned_role))
return TRUE
+ if(istype(user, /mob/living/basic/drone/snowflake/bardrone))
+ return TRUE
+
var/obj/item/card/id/ID = user.get_idcard(FALSE)
if(ID && (ACCESS_CENT_BAR in ID.access))
return TRUE
diff --git a/code/modules/shuttle/supply.dm b/code/modules/shuttle/supply.dm
index 0999ef3994d41..a3948b0e26c5f 100644
--- a/code/modules/shuttle/supply.dm
+++ b/code/modules/shuttle/supply.dm
@@ -32,7 +32,7 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list(
/obj/machinery/camera,
/obj/item/gps,
/obj/structure/checkoutmachine,
- /obj/machinery/fax
+ /obj/machinery/fax,
)))
/// How many goody orders we can fit in a lockbox before we upgrade to a crate
@@ -157,37 +157,39 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list(
var/value = 0
var/purchases = 0
+ var/price
+ var/pack_cost
var/list/goodies_by_buyer = list() // if someone orders more than GOODY_FREE_SHIPPING_MAX goodies, we upcharge to a normal crate so they can't carry around 20 combat shotties
+ var/list/rejected_orders = list() //list of all orders that exceeded the available budget and are uncancelable
for(var/datum/supply_order/spawning_order in SSshuttle.shopping_list)
if(!empty_turfs.len)
break
- var/price = spawning_order.pack.get_cost()
- if(spawning_order.applied_coupon)
- price *= (1 - spawning_order.applied_coupon.discount_pct_off)
-
- var/datum/bank_account/paying_for_this
+ price = spawning_order.get_final_cost()
//department orders EARN money for cargo, not the other way around
+ var/datum/bank_account/paying_for_this
if(!spawning_order.department_destination && spawning_order.charge_on_purchase)
if(spawning_order.paying_account) //Someone paid out of pocket
paying_for_this = spawning_order.paying_account
- var/list/current_buyer_orders = goodies_by_buyer[spawning_order.paying_account] // so we can access the length a few lines down
- if(!spawning_order.pack.goody)
- price *= 1.1 //TODO make this customizable by the quartermaster
-
// note this is before we increment, so this is the GOODY_FREE_SHIPPING_MAX + 1th goody to ship. also note we only increment off this step if they successfully pay the fee, so there's no way around it
- else if(LAZYLEN(current_buyer_orders) == GOODY_FREE_SHIPPING_MAX)
- price += CRATE_TAX
- paying_for_this.bank_card_talk("Goody order size exceeds free shipping limit: Assessing [CRATE_TAX] credit S&H fee.")
+ if(spawning_order.pack.goody)
+ var/list/current_buyer_orders = goodies_by_buyer[spawning_order.paying_account]
+ if(LAZYLEN(current_buyer_orders) == GOODY_FREE_SHIPPING_MAX)
+ price = round(price + CRATE_TAX)
+ paying_for_this.bank_card_talk("Goody order size exceeds free shipping limit: Assessing [CRATE_TAX] credit S&H fee.")
else
paying_for_this = SSeconomy.get_dep_account(ACCOUNT_CAR)
+
if(paying_for_this)
if(!paying_for_this.adjust_money(-price, "Cargo: [spawning_order.pack.name]"))
if(spawning_order.paying_account)
paying_for_this.bank_card_talk("Cargo order #[spawning_order.id] rejected due to lack of funds. Credits required: [price]")
+ if(!spawning_order.can_be_cancelled) //only if it absolutely cannot be canceled by the player do we cancel it for them
+ rejected_orders += spawning_order
continue
+ pack_cost = spawning_order.pack.get_cost()
if(spawning_order.paying_account)
paying_for_this = spawning_order.paying_account
if(spawning_order.pack.goody)
@@ -198,8 +200,8 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list(
paying_for_this.bank_card_talk(reciever_message)
SSeconomy.track_purchase(paying_for_this, price, spawning_order.pack.name)
var/datum/bank_account/department/cargo = SSeconomy.get_dep_account(ACCOUNT_CAR)
- cargo.adjust_money(price - spawning_order.pack.get_cost()) //Cargo gets the handling fee
- value += spawning_order.pack.get_cost()
+ cargo.adjust_money(price - pack_cost) //Cargo gets the handling fee
+ value += pack_cost
SSshuttle.shopping_list -= spawning_order
SSshuttle.order_history += spawning_order
QDEL_NULL(spawning_order.applied_coupon)
@@ -217,6 +219,11 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list(
message_admins("\A [spawning_order.pack.name] ordered by [ADMIN_LOOKUPFLW(spawning_order.orderer_ckey)], paid by [from_whom] has shipped.")
purchases++
+ //clear out all rejected uncancellable orders
+ for(var/datum/supply_order/rejected_order in rejected_orders)
+ SSshuttle.shopping_list -= rejected_order
+ qdel(rejected_order)
+
// we handle packing all the goodies last, since the type of crate we use depends on how many goodies they ordered. If it's more than GOODY_FREE_SHIPPING_MAX
// then we send it in a crate (including the CRATE_TAX cost), otherwise send it in a free shipping case
for(var/buyer_key in goodies_by_buyer)
diff --git a/code/modules/spells/spell_types/conjure/_conjure.dm b/code/modules/spells/spell_types/conjure/_conjure.dm
index 3afe7c5255754..5fcff10ee18ab 100644
--- a/code/modules/spells/spell_types/conjure/_conjure.dm
+++ b/code/modules/spells/spell_types/conjure/_conjure.dm
@@ -61,3 +61,28 @@
/// Called on atoms summoned after they are created, allows extra variable editing and such of created objects
/datum/action/cooldown/spell/conjure/proc/post_summon(atom/summoned_object, atom/cast_on)
return
+
+///limits the amount of summons
+/datum/action/cooldown/spell/conjure/limit_summons
+ ///max number of after images
+ var/max_summons
+ ///How many clones do we have summoned
+ var/number_of_summons = 0
+
+/datum/action/cooldown/spell/conjure/limit_summons/can_cast_spell(feedback = TRUE)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(number_of_summons >= max_summons)
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/spell/conjure/limit_summons/post_summon(atom/summoned_object, atom/cast_on)
+ RegisterSignals(summoned_object, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(delete_copy))
+ number_of_summons++
+
+/datum/action/cooldown/spell/conjure/limit_summons/proc/delete_copy(datum/source)
+ SIGNAL_HANDLER
+
+ UnregisterSignal(source, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH))
+ number_of_summons--
diff --git a/code/modules/spells/spell_types/conjure_item/lighting_packet.dm b/code/modules/spells/spell_types/conjure_item/lighting_packet.dm
index cffddf7e0ce94..2df0c85f470e1 100644
--- a/code/modules/spells/spell_types/conjure_item/lighting_packet.dm
+++ b/code/modules/spells/spell_types/conjure_item/lighting_packet.dm
@@ -33,7 +33,7 @@
hit_living.electrocute_act(80, src, flags = SHOCK_ILLUSION | SHOCK_NOGLOVES)
qdel(src)
-/obj/item/spellpacket/lightningbolt/throw_at(atom/target, range, speed, mob/thrower, spin = TRUE, diagonals_first = FALSE, datum/callback/callback, force = INFINITY, quickstart = TRUE)
+/obj/item/spellpacket/lightningbolt/throw_at(atom/target, range, speed, mob/thrower, spin = TRUE, diagonals_first = FALSE, datum/callback/callback, force = INFINITY, gentle, quickstart = TRUE)
. = ..()
if(ishuman(thrower))
var/mob/living/carbon/human/human_thrower = thrower
diff --git a/code/modules/spells/spell_types/pointed/mind_transfer.dm b/code/modules/spells/spell_types/pointed/mind_transfer.dm
index 08c638ffe0707..72441362b652e 100644
--- a/code/modules/spells/spell_types/pointed/mind_transfer.dm
+++ b/code/modules/spells/spell_types/pointed/mind_transfer.dm
@@ -55,9 +55,19 @@
if(!isliving(cast_on))
to_chat(owner, span_warning("You can only swap minds with living beings!"))
return FALSE
+
+ if(HAS_TRAIT(cast_on, TRAIT_MIND_TEMPORARILY_GONE))
+ to_chat(owner, span_warning("This creature's mind is somewhere else entirely!"))
+ return FALSE
+
+ if(HAS_TRAIT(cast_on, TRAIT_NO_MINDSWAP))
+ to_chat(owner, span_warning("This type of magic can't operate on [cast_on.p_their()] mind!"))
+ return FALSE
+
if(is_type_in_typecache(cast_on, blacklisted_mobs))
to_chat(owner, span_warning("This creature is too [pick("powerful", "strange", "arcane", "obscene")] to control!"))
return FALSE
+
if(isguardian(cast_on))
var/mob/living/simple_animal/hostile/guardian/stand = cast_on
if(stand.summoner && stand.summoner == owner)
diff --git a/code/modules/spells/spell_types/shapeshift/_shape_status.dm b/code/modules/spells/spell_types/shapeshift/_shape_status.dm
index 94e1d549af268..faa84835255a8 100644
--- a/code/modules/spells/spell_types/shapeshift/_shape_status.dm
+++ b/code/modules/spells/spell_types/shapeshift/_shape_status.dm
@@ -35,7 +35,8 @@
ADD_TRAIT(caster_mob, TRAIT_NO_TRANSFORM, REF(src))
caster_mob.apply_status_effect(/datum/status_effect/grouped/stasis, STASIS_SHAPECHANGE_EFFECT)
- RegisterSignal(owner, COMSIG_LIVING_PRE_WABBAJACKED, PROC_REF(on_wabbajacked))
+ RegisterSignal(owner, COMSIG_LIVING_PRE_WABBAJACKED, PROC_REF(on_pre_wabbajack))
+ RegisterSignal(owner, COMSIG_PRE_MOB_CHANGED_TYPE, PROC_REF(on_pre_type_change))
RegisterSignal(owner, COMSIG_LIVING_DEATH, PROC_REF(on_shape_death))
RegisterSignal(caster_mob, COMSIG_LIVING_DEATH, PROC_REF(on_caster_death))
RegisterSignal(caster_mob, COMSIG_QDELETING, PROC_REF(on_caster_deleted))
@@ -53,15 +54,24 @@
// but juuust in case make sure nothing sticks around.
caster_mob = null
-/// Signal proc for [COMSIG_LIVING_PRE_WABBAJACKED] to prevent us from being Wabbajacked and messed up.
-/datum/status_effect/shapechange_mob/proc/on_wabbajacked(mob/living/source, randomized)
+/// Called when we're shot by the Wabbajack but before we change into a different mob
+/datum/status_effect/shapechange_mob/proc/on_pre_wabbajack(mob/living/source)
SIGNAL_HANDLER
+ on_mob_transformed(source)
+ return STOP_WABBAJACK
+
+/// Called when we're turned into a different mob via the change_mob_type proc
+/datum/status_effect/shapechange_mob/proc/on_pre_type_change(mob/living/source)
+ SIGNAL_HANDLER
+ on_mob_transformed(source)
+ return COMPONENT_BLOCK_MOB_CHANGE
+/// Called when the transformed mob tries to change into a different kind of mob, we wouldn't handle this well so we'll just turn back
+/datum/status_effect/shapechange_mob/proc/on_mob_transformed(mob/living/source)
var/mob/living/revealed_mob = caster_mob
source.visible_message(span_warning("[revealed_mob] gets pulled back to their normal form!"))
restore_caster()
revealed_mob.Paralyze(10 SECONDS, ignore_canstun = TRUE)
- return STOP_WABBAJACK
/// Restores the caster back to their human form.
/// if kill_caster_after is TRUE, the caster will have death() called on them after restoring.
@@ -76,20 +86,27 @@
UnregisterSignal(owner, list(COMSIG_LIVING_PRE_WABBAJACKED, COMSIG_LIVING_DEATH))
UnregisterSignal(caster_mob, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH))
- caster_mob.forceMove(owner.loc)
REMOVE_TRAIT(caster_mob, TRAIT_NO_TRANSFORM, REF(src))
caster_mob.remove_status_effect(/datum/status_effect/grouped/stasis, STASIS_SHAPECHANGE_EFFECT)
- owner.mind?.transfer_to(caster_mob)
+
+ var/atom/former_loc = owner.loc
+ owner.moveToNullspace()
+ caster_mob.forceMove(former_loc) // This is to avoid crushing our former cockroach body
if(kill_caster_after)
caster_mob.death()
after_unchange()
- caster_mob = null
+
+ // We're about to remove the status effect and clear owner so we need to cache this
+ var/mob/living/former_body = owner
+
+ // Do this late as it will destroy the status effect we are in and null a bunch of values we are trying to use
+ owner.mind?.transfer_to(caster_mob)
// Destroy the owner after all's said and done, this will also destroy our status effect (src)
// retore_caster() should never reach this point while either the owner or the effect is being qdeleted
- qdel(owner)
+ qdel(former_body)
/// Effects done after the casting mob has reverted to their human form.
/datum/status_effect/shapechange_mob/proc/after_unchange()
@@ -154,9 +171,9 @@
source_spell.Grant(owner)
if(source_spell.convert_damage)
- var/damage_to_apply = owner.maxHealth * ((caster_mob.maxHealth - caster_mob.health) / caster_mob.maxHealth)
+ var/damage_to_apply = owner.maxHealth * (caster_mob.get_total_damage() / caster_mob.maxHealth)
- owner.apply_damage(damage_to_apply, source_spell.convert_damage_type, forced = TRUE, wound_bonus = CANT_WOUND)
+ owner.apply_damage(damage_to_apply, source_spell.convert_damage_type, forced = TRUE, spread_damage = TRUE, wound_bonus = CANT_WOUND)
owner.blood_volume = caster_mob.blood_volume
for(var/datum/action/bodybound_action as anything in caster_mob.actions)
@@ -186,11 +203,9 @@
if(QDELETED(source_spell) || !source_spell.convert_damage)
return
- if(caster_mob.stat != DEAD)
- caster_mob.revive(HEAL_DAMAGE)
-
- var/damage_to_apply = caster_mob.maxHealth * ((owner.maxHealth - owner.health) / owner.maxHealth)
- caster_mob.apply_damage(damage_to_apply, source_spell.convert_damage_type, forced = TRUE, wound_bonus = CANT_WOUND)
+ caster_mob.fully_heal(HEAL_DAMAGE) // Remove all of our damage before setting our health to a proportion of the former transformed mob's health
+ var/damage_to_apply = caster_mob.maxHealth * (owner.get_total_damage() / owner.maxHealth)
+ caster_mob.apply_damage(damage_to_apply, source_spell.convert_damage_type, forced = TRUE, spread_damage = TRUE, wound_bonus = CANT_WOUND)
caster_mob.blood_volume = owner.blood_volume
diff --git a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
index 5aecd863bce43..59c9ffdde3b0b 100644
--- a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
+++ b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
@@ -128,7 +128,7 @@
new gib_type(get_turf(possible_vent))
playsound(possible_vent, 'sound/effects/reee.ogg', 75, TRUE)
- priority_announce("We detected a pipe blockage around [get_area(get_turf(cast_on))], please dispatch someone to investigate.", "Central Command")
+ priority_announce("We detected a pipe blockage around [get_area(get_turf(cast_on))], please dispatch someone to investigate.", "[command_name()]")
// Gib our caster, and make sure to leave nothing behind
// (If we leave something behind, it'll drop on the turf of the pipe, which is kinda wrong.)
cast_on.investigate_log("has been gibbed by shapeshifting while ventcrawling.", INVESTIGATE_DEATHS)
diff --git a/code/modules/spells/spell_types/shapeshift/shapechange.dm b/code/modules/spells/spell_types/shapeshift/shapechange.dm
index 2e890eed6325c..dd2597d00970c 100644
--- a/code/modules/spells/spell_types/shapeshift/shapechange.dm
+++ b/code/modules/spells/spell_types/shapeshift/shapechange.dm
@@ -12,10 +12,10 @@
spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
possible_shapes = list(
+ /mob/living/basic/carp/magic/chaos,
+ /mob/living/basic/construct/juggernaut/mystic,
/mob/living/basic/mouse,
/mob/living/basic/pet/dog/corgi,
- /mob/living/basic/carp/magic/chaos,
- /mob/living/simple_animal/bot/secbot/ed209,
/mob/living/basic/spider/giant/viper/wizard,
- /mob/living/simple_animal/hostile/construct/juggernaut/mystic,
+ /mob/living/simple_animal/bot/secbot/ed209,
)
diff --git a/code/modules/station_goals/bsa.dm b/code/modules/station_goals/bsa.dm
index 9ffa24f3c4357..d1ee4fc7a3f77 100644
--- a/code/modules/station_goals/bsa.dm
+++ b/code/modules/station_goals/bsa.dm
@@ -241,7 +241,12 @@ GLOBAL_VAR_INIT(bsa_unlock, FALSE)
point.Beam(target, icon_state = "bsa_beam", time = 5 SECONDS, maxdistance = world.maxx) //ZZZAP
new /obj/effect/temp_visual/bsa_splash(point, dir)
- notify_ghosts("The Bluespace Artillery has been fired!", source = bullseye, header = "KABOOM!")
+ notify_ghosts(
+ "The Bluespace Artillery has been fired!",
+ source = bullseye,
+ header = "KABOOM!",
+ )
+
if(!blocker)
message_admins("[ADMIN_LOOKUPFLW(user)] has launched an artillery strike targeting [ADMIN_VERBOSEJMP(bullseye)].")
user.log_message("has launched an artillery strike targeting [AREACOORD(bullseye)].", LOG_GAME)
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index 2e140bdbf26e7..574e3bbc13c7c 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -453,7 +453,7 @@
/obj/item/bodypart/proc/receive_damage(brute = 0, burn = 0, blocked = 0, updating_health = TRUE, forced = FALSE, required_bodytype = null, wound_bonus = 0, bare_wound_bonus = 0, sharpness = NONE, attack_direction = null, damage_source)
SHOULD_CALL_PARENT(TRUE)
- var/hit_percent = (100-blocked)/100
+ var/hit_percent = forced ? 1 : (100-blocked)/100
if((!brute && !burn) || hit_percent <= 0)
return FALSE
if (!forced)
@@ -878,7 +878,7 @@
SHOULD_CALL_PARENT(TRUE)
if(IS_ORGANIC_LIMB(src))
- if(owner && HAS_TRAIT(owner, TRAIT_HUSK))
+ if(!(bodypart_flags & BODYPART_UNHUSKABLE) && owner && HAS_TRAIT(owner, TRAIT_HUSK))
dmg_overlay_type = "" //no damage overlay shown when husked
is_husked = TRUE
else if(owner && HAS_TRAIT(owner, TRAIT_INVISIBLE_MAN))
@@ -934,8 +934,8 @@
icon_state = initial(icon_state)//no overlays found, we default back to initial icon.
return
for(var/image/img as anything in standing)
- img.pixel_x = px_x
- img.pixel_y = px_y
+ img.pixel_x += px_x
+ img.pixel_y += px_y
add_overlay(standing)
///Generates an /image for the limb to be used as an overlay
diff --git a/code/modules/surgery/bodyparts/dismemberment.dm b/code/modules/surgery/bodyparts/dismemberment.dm
index 5a7343f8a0b3a..e863341eb4344 100644
--- a/code/modules/surgery/bodyparts/dismemberment.dm
+++ b/code/modules/surgery/bodyparts/dismemberment.dm
@@ -86,6 +86,8 @@
. += cavity_item
cavity_item = null
+ return .
+
///limb removal. The "special" argument is used for swapping a limb with a new one without the effects of losing a limb kicking in.
/obj/item/bodypart/proc/drop_limb(special, dismembered)
if(!owner)
diff --git a/code/modules/surgery/bodyparts/head_hair_and_lips.dm b/code/modules/surgery/bodyparts/head_hair_and_lips.dm
index 010ed9d111615..01ddea49df15f 100644
--- a/code/modules/surgery/bodyparts/head_hair_and_lips.dm
+++ b/code/modules/surgery/bodyparts/head_hair_and_lips.dm
@@ -114,16 +114,17 @@
var/facial_hair_gradient_style = LAZYACCESS(gradient_styles, GRADIENT_FACIAL_HAIR_KEY)
if(facial_hair_gradient_style)
var/facial_hair_gradient_color = LAZYACCESS(gradient_colors, GRADIENT_FACIAL_HAIR_KEY)
- var/image/facial_hair_gradient_overlay = get_gradient_overlay(sprite_accessory.icon, sprite_accessory.icon_state, -HAIR_LAYER, GLOB.facial_hair_gradients_list[facial_hair_gradient_style], facial_hair_gradient_color)
+ var/image/facial_hair_gradient_overlay = get_gradient_overlay(sprite_accessory.icon, sprite_accessory.icon_state, -HAIR_LAYER, GLOB.facial_hair_gradients_list[facial_hair_gradient_style], facial_hair_gradient_color, image_dir)
. += facial_hair_gradient_overlay
var/image/hair_overlay
if(!(show_debrained && (head_flags & HEAD_DEBRAIN)) && !hair_hidden && hairstyle && (head_flags & HEAD_HAIR))
- sprite_accessory = GLOB.hairstyles_list[hairstyle]
- if(sprite_accessory)
+ var/datum/sprite_accessory/hair/hair_sprite_accessory = GLOB.hairstyles_list[hairstyle]
+ if(hair_sprite_accessory)
//Overlay
- hair_overlay = image(sprite_accessory.icon, sprite_accessory.icon_state, -HAIR_LAYER, image_dir)
+ hair_overlay = image(hair_sprite_accessory.icon, hair_sprite_accessory.icon_state, -HAIR_LAYER, image_dir)
hair_overlay.alpha = hair_alpha
+ hair_overlay.pixel_y = hair_sprite_accessory.y_offset
//Emissive blocker
if(blocks_emissive != EMISSIVE_BLOCK_NONE)
hair_overlay.overlays += emissive_blocker(hair_overlay.icon, hair_overlay.icon_state, location, alpha = hair_alpha)
@@ -134,7 +135,8 @@
var/hair_gradient_style = LAZYACCESS(gradient_styles, GRADIENT_HAIR_KEY)
if(hair_gradient_style)
var/hair_gradient_color = LAZYACCESS(gradient_colors, GRADIENT_HAIR_KEY)
- var/image/hair_gradient_overlay = get_gradient_overlay(sprite_accessory.icon, sprite_accessory.icon_state, -HAIR_LAYER, GLOB.hair_gradients_list[hair_gradient_style], hair_gradient_color)
+ var/image/hair_gradient_overlay = get_gradient_overlay(hair_sprite_accessory.icon, hair_sprite_accessory.icon_state, -HAIR_LAYER, GLOB.hair_gradients_list[hair_gradient_style], hair_gradient_color, image_dir)
+ hair_gradient_overlay.pixel_y = hair_sprite_accessory.y_offset
. += hair_gradient_overlay
if(show_debrained && (head_flags & HEAD_DEBRAIN))
@@ -197,12 +199,12 @@
return eyeless_overlay
/// Returns an appropriate hair/facial hair gradient overlay
-/obj/item/bodypart/head/proc/get_gradient_overlay(file, icon, layer, datum/sprite_accessory/gradient, grad_color)
+/obj/item/bodypart/head/proc/get_gradient_overlay(file, icon, layer, datum/sprite_accessory/gradient, grad_color, image_dir)
RETURN_TYPE(/mutable_appearance)
var/mutable_appearance/gradient_overlay = mutable_appearance(layer = layer)
- var/icon/temp = icon(gradient.icon, gradient.icon_state)
- var/icon/temp_hair = icon(file, icon)
+ var/icon/temp = icon(gradient.icon, gradient.icon_state, image_dir)
+ var/icon/temp_hair = icon(file, icon, image_dir)
temp.Blend(temp_hair, ICON_ADD)
gradient_overlay.icon = temp
gradient_overlay.color = grad_color
diff --git a/code/modules/surgery/bodyparts/parts.dm b/code/modules/surgery/bodyparts/parts.dm
index c3924163784e5..0ebc781085212 100644
--- a/code/modules/surgery/bodyparts/parts.dm
+++ b/code/modules/surgery/bodyparts/parts.dm
@@ -32,6 +32,8 @@
var/datum/worn_feature_offset/worn_suit_offset
/// Offset to apply to equipment worn on the neck
var/datum/worn_feature_offset/worn_neck_offset
+ /// Which functional (i.e. flightpotion) wing types (if any) does this bodypart support? If count is >1 a radial menu is used to choose between all icons in list
+ var/list/wing_types = list(/obj/item/organ/external/wings/functional/angel)
/obj/item/bodypart/chest/can_dismember(obj/item/item)
if(owner.stat < HARD_CRIT || !get_organs())
@@ -81,6 +83,7 @@
bodypart_flags = BODYPART_UNREMOVABLE
max_damage = 500
acceptable_bodytype = BODYTYPE_HUMANOID
+ wing_types = NONE
/obj/item/bodypart/chest/larva
icon = 'icons/mob/human/species/alien/bodyparts.dmi'
@@ -93,6 +96,7 @@
max_damage = 50
bodytype = BODYTYPE_LARVA_PLACEHOLDER | BODYTYPE_ORGANIC
acceptable_bodytype = BODYTYPE_LARVA_PLACEHOLDER
+ wing_types = NONE
/// Parent Type for arms, should not appear in game.
/obj/item/bodypart/arm
@@ -113,6 +117,8 @@
var/datum/worn_feature_offset/worn_glove_offset
/// Datum describing how to offset things held in the hands of this arm, the x offset IS functional here
var/datum/worn_feature_offset/held_hand_offset
+ /// The noun to use when referring to this arm's appendage, e.g. "hand" or "paw"
+ var/appendage_noun = "hand"
biological_state = BIO_STANDARD_JOINTED
@@ -211,6 +217,7 @@
unarmed_damage_low = 1 /// monkey punches must be really weak, considering they bite people instead and their bites are weak as hell.
unarmed_damage_high = 2
unarmed_stun_threshold = 3
+ appendage_noun = "paw"
/obj/item/bodypart/arm/left/alien
icon = 'icons/mob/human/species/alien/bodyparts.dmi'
@@ -224,6 +231,7 @@
can_be_disabled = FALSE
max_damage = 100
should_draw_greyscale = FALSE
+ appendage_noun = "scythe-like hand"
/obj/item/bodypart/arm/right
@@ -314,6 +322,7 @@
unarmed_damage_low = 1
unarmed_damage_high = 2
unarmed_stun_threshold = 3
+ appendage_noun = "paw"
/obj/item/bodypart/arm/right/alien
icon = 'icons/mob/human/species/alien/bodyparts.dmi'
@@ -327,6 +336,7 @@
can_be_disabled = FALSE
max_damage = 100
should_draw_greyscale = FALSE
+ appendage_noun = "scythe-like hand"
/// Parent Type for legs, should not appear in game.
/obj/item/bodypart/leg
diff --git a/code/modules/surgery/bodyparts/robot_bodyparts.dm b/code/modules/surgery/bodyparts/robot_bodyparts.dm
index 99591daaa4b2b..b980e304e439b 100644
--- a/code/modules/surgery/bodyparts/robot_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/robot_bodyparts.dm
@@ -42,6 +42,7 @@
damage_examines = list(BRUTE = ROBOTIC_BRUTE_EXAMINE_TEXT, BURN = ROBOTIC_BURN_EXAMINE_TEXT, CLONE = DEFAULT_CLONE_EXAMINE_TEXT)
disabling_threshold_percentage = 1
+ bodypart_flags = BODYPART_UNHUSKABLE
/obj/item/bodypart/arm/right/robot
name = "cyborg right arm"
@@ -75,6 +76,7 @@
biological_state = (BIO_ROBOTIC|BIO_JOINTED)
damage_examines = list(BRUTE = ROBOTIC_BRUTE_EXAMINE_TEXT, BURN = ROBOTIC_BURN_EXAMINE_TEXT, CLONE = DEFAULT_CLONE_EXAMINE_TEXT)
+ bodypart_flags = BODYPART_UNHUSKABLE
/obj/item/bodypart/leg/left/robot
name = "cyborg left leg"
@@ -108,6 +110,7 @@
biological_state = (BIO_ROBOTIC|BIO_JOINTED)
damage_examines = list(BRUTE = ROBOTIC_BRUTE_EXAMINE_TEXT, BURN = ROBOTIC_BURN_EXAMINE_TEXT, CLONE = DEFAULT_CLONE_EXAMINE_TEXT)
+ bodypart_flags = BODYPART_UNHUSKABLE
/obj/item/bodypart/leg/left/robot/emp_act(severity)
. = ..()
@@ -154,6 +157,7 @@
biological_state = (BIO_ROBOTIC|BIO_JOINTED)
damage_examines = list(BRUTE = ROBOTIC_BRUTE_EXAMINE_TEXT, BURN = ROBOTIC_BURN_EXAMINE_TEXT, CLONE = DEFAULT_CLONE_EXAMINE_TEXT)
+ bodypart_flags = BODYPART_UNHUSKABLE
/obj/item/bodypart/leg/right/robot/emp_act(severity)
. = ..()
@@ -197,12 +201,15 @@
biological_state = (BIO_ROBOTIC)
damage_examines = list(BRUTE = ROBOTIC_BRUTE_EXAMINE_TEXT, BURN = ROBOTIC_BURN_EXAMINE_TEXT, CLONE = DEFAULT_CLONE_EXAMINE_TEXT)
+ bodypart_flags = BODYPART_UNHUSKABLE
+
+ robotic_emp_paralyze_damage_percent_threshold = 0.6
+
+ wing_types = list(/obj/item/organ/external/wings/functional/robotic)
var/wired = FALSE
var/obj/item/stock_parts/cell/cell = null
- robotic_emp_paralyze_damage_percent_threshold = 0.6
-
/obj/item/bodypart/chest/robot/emp_act(severity)
. = ..()
if(!. || isnull(owner))
@@ -235,8 +242,46 @@
/obj/item/bodypart/chest/robot/Destroy()
QDEL_NULL(cell)
+ UnregisterSignal(src, COMSIG_BODYPART_ATTACHED)
return ..()
+/obj/item/bodypart/chest/robot/Initialize(mapload)
+ . = ..()
+ RegisterSignal(src, COMSIG_BODYPART_ATTACHED, PROC_REF(on_attached))
+ RegisterSignal(src, COMSIG_BODYPART_REMOVED, PROC_REF(on_detached))
+
+/obj/item/bodypart/chest/robot/proc/on_attached(obj/item/bodypart/chest/robot/this_bodypart, mob/living/carbon/human/new_owner)
+ SIGNAL_HANDLER
+
+ RegisterSignals(new_owner, list(COMSIG_CARBON_POST_ATTACH_LIMB, COMSIG_CARBON_POST_REMOVE_LIMB), PROC_REF(check_limbs))
+
+/obj/item/bodypart/chest/robot/proc/on_detached(obj/item/bodypart/chest/robot/this_bodypart, mob/living/carbon/human/old_owner)
+ SIGNAL_HANDLER
+
+ UnregisterSignal(old_owner, list(COMSIG_CARBON_POST_ATTACH_LIMB, COMSIG_CARBON_POST_REMOVE_LIMB))
+
+/obj/item/bodypart/chest/robot/proc/check_limbs()
+ SIGNAL_HANDLER
+
+ var/all_robotic = TRUE
+ for(var/obj/item/bodypart/part in owner.bodyparts)
+ all_robotic = all_robotic && IS_ROBOTIC_LIMB(part)
+
+ if(all_robotic)
+ owner.add_traits(list(
+ TRAIT_RESISTCOLD,
+ TRAIT_RESISTHEAT,
+ TRAIT_RESISTLOWPRESSURE,
+ TRAIT_RESISTHIGHPRESSURE,
+ ), AUGMENTATION_TRAIT)
+ else
+ owner.remove_traits(list(
+ TRAIT_RESISTCOLD,
+ TRAIT_RESISTHEAT,
+ TRAIT_RESISTLOWPRESSURE,
+ TRAIT_RESISTHIGHPRESSURE,
+ ), AUGMENTATION_TRAIT)
+
/obj/item/bodypart/chest/robot/attackby(obj/item/weapon, mob/user, params)
if(istype(weapon, /obj/item/stock_parts/cell))
if(cell)
@@ -332,6 +377,7 @@
damage_examines = list(BRUTE = ROBOTIC_BRUTE_EXAMINE_TEXT, BURN = ROBOTIC_BURN_EXAMINE_TEXT, CLONE = DEFAULT_CLONE_EXAMINE_TEXT)
head_flags = HEAD_EYESPRITES
+ bodypart_flags = BODYPART_UNHUSKABLE
var/obj/item/assembly/flash/handheld/flash1 = null
var/obj/item/assembly/flash/handheld/flash2 = null
diff --git a/code/modules/surgery/bodyparts/species_parts/ethereal_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/ethereal_bodyparts.dm
index 2215b388320e8..feda164b6f259 100644
--- a/code/modules/surgery/bodyparts/species_parts/ethereal_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/species_parts/ethereal_bodyparts.dm
@@ -22,6 +22,7 @@
is_dimorphic = FALSE
dmg_overlay_type = null
brute_modifier = 1.25 //ethereal are weak to brute damages
+ wing_types = NONE
/obj/item/bodypart/chest/ethereal/update_limb(dropping_limb, is_creating)
. = ..()
diff --git a/code/modules/surgery/bodyparts/species_parts/lizard_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/lizard_bodyparts.dm
index c5f7ff87346d3..8c971174d6d93 100644
--- a/code/modules/surgery/bodyparts/species_parts/lizard_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/species_parts/lizard_bodyparts.dm
@@ -8,6 +8,7 @@
icon_greyscale = 'icons/mob/human/species/lizard/bodyparts.dmi'
limb_id = SPECIES_LIZARD
is_dimorphic = TRUE
+ wing_types = list(/obj/item/organ/external/wings/functional/dragon)
/obj/item/bodypart/arm/left/lizard
icon_greyscale = 'icons/mob/human/species/lizard/bodyparts.dmi'
diff --git a/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm
index b8b7d427b67b2..062f5fe4b9d7e 100644
--- a/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm
@@ -9,6 +9,7 @@
limb_id = SPECIES_SNAIL
is_dimorphic = FALSE
burn_modifier = 2
+ wing_types = NONE
/obj/item/bodypart/arm/left/snail
limb_id = SPECIES_SNAIL
@@ -47,6 +48,7 @@
limb_id = SPECIES_ABDUCTOR
is_dimorphic = FALSE
should_draw_greyscale = FALSE
+ wing_types = NONE
/obj/item/bodypart/arm/left/abductor
limb_id = SPECIES_ABDUCTOR
@@ -81,6 +83,7 @@
is_dimorphic = TRUE
dmg_overlay_type = null
burn_modifier = 0.5 // = 1/2x generic burn damage
+ wing_types = NONE
/obj/item/bodypart/arm/left/jelly
biological_state = (BIO_FLESH|BIO_BLOODED|BIO_JOINTED)
@@ -117,6 +120,7 @@
biological_state = (BIO_FLESH|BIO_BLOODED)
limb_id = SPECIES_SLIMEPERSON
is_dimorphic = TRUE
+ wing_types = NONE
/obj/item/bodypart/arm/left/slime
biological_state = (BIO_FLESH|BIO_BLOODED|BIO_JOINTED)
@@ -145,6 +149,7 @@
biological_state = (BIO_FLESH|BIO_BLOODED)
limb_id = SPECIES_LUMINESCENT
is_dimorphic = TRUE
+ wing_types = NONE
/obj/item/bodypart/arm/left/luminescent
biological_state = (BIO_FLESH|BIO_BLOODED|BIO_JOINTED)
@@ -173,6 +178,7 @@
limb_id = SPECIES_ZOMBIE
is_dimorphic = FALSE
should_draw_greyscale = FALSE
+ wing_types = NONE
/obj/item/bodypart/arm/left/zombie
limb_id = SPECIES_ZOMBIE
@@ -211,6 +217,7 @@
limb_id = SPECIES_PODPERSON
is_dimorphic = TRUE
burn_modifier = 1.25
+ wing_types = NONE
/obj/item/bodypart/arm/left/pod
limb_id = SPECIES_PODPERSON
@@ -247,6 +254,7 @@
limb_id = SPECIES_FLYPERSON
is_dimorphic = TRUE
should_draw_greyscale = FALSE
+ wing_types = list(/obj/item/organ/external/wings/functional/fly)
/obj/item/bodypart/arm/left/fly
limb_id = SPECIES_FLYPERSON
@@ -277,6 +285,7 @@
is_dimorphic = FALSE
should_draw_greyscale = FALSE
burn_modifier = 1.5
+ wing_types = NONE
/obj/item/bodypart/arm/left/shadow
limb_id = SPECIES_SHADOW
@@ -312,6 +321,7 @@
should_draw_greyscale = FALSE
dmg_overlay_type = null
head_flags = NONE
+ bodypart_flags = BODYPART_UNHUSKABLE
/obj/item/bodypart/chest/skeleton
biological_state = BIO_BONE
@@ -319,30 +329,36 @@
is_dimorphic = FALSE
should_draw_greyscale = FALSE
dmg_overlay_type = null
+ bodypart_flags = BODYPART_UNHUSKABLE
+ wing_types = list(/obj/item/organ/external/wings/functional/skeleton)
/obj/item/bodypart/arm/left/skeleton
biological_state = (BIO_BONE|BIO_JOINTED)
limb_id = SPECIES_SKELETON
should_draw_greyscale = FALSE
dmg_overlay_type = null
+ bodypart_flags = BODYPART_UNHUSKABLE
/obj/item/bodypart/arm/right/skeleton
biological_state = (BIO_BONE|BIO_JOINTED)
limb_id = SPECIES_SKELETON
should_draw_greyscale = FALSE
dmg_overlay_type = null
+ bodypart_flags = BODYPART_UNHUSKABLE
/obj/item/bodypart/leg/left/skeleton
biological_state = (BIO_BONE|BIO_JOINTED)
limb_id = SPECIES_SKELETON
should_draw_greyscale = FALSE
dmg_overlay_type = null
+ bodypart_flags = BODYPART_UNHUSKABLE
/obj/item/bodypart/leg/right/skeleton
biological_state = (BIO_BONE|BIO_JOINTED)
limb_id = SPECIES_SKELETON
should_draw_greyscale = FALSE
dmg_overlay_type = null
+ bodypart_flags = BODYPART_UNHUSKABLE
///MUSHROOM
/obj/item/bodypart/head/mushroom
@@ -356,6 +372,7 @@
is_dimorphic = TRUE
bodypart_traits = list(TRAIT_NO_JUMPSUIT)
burn_modifier = 1.25
+ wing_types = NONE
/obj/item/bodypart/arm/left/mushroom
limb_id = SPECIES_MUSHROOM
@@ -456,6 +473,7 @@
should_draw_greyscale = FALSE
dmg_overlay_type = null
bodypart_traits = list(TRAIT_NO_JUMPSUIT)
+ wing_types = NONE
/obj/item/bodypart/chest/golem/Initialize(mapload)
worn_belt_offset = new(
@@ -558,3 +576,45 @@
unarmed_damage_low = 7
unarmed_damage_high = 21
unarmed_stun_threshold = 11
+
+///flesh
+
+/obj/item/bodypart/arm/left/flesh
+ limb_id = BODYPART_ID_MEAT
+ should_draw_greyscale = FALSE
+
+/obj/item/bodypart/arm/left/flesh/Initialize(mapload, dont_spawn_flesh = FALSE)
+ . = ..()
+ if(!dont_spawn_flesh)
+ new /mob/living/basic/living_limb_flesh(src, src)
+ ADD_TRAIT(src, TRAIT_IGNORED_BY_LIVING_FLESH, BODYPART_TRAIT)
+
+/obj/item/bodypart/arm/right/flesh
+ limb_id = BODYPART_ID_MEAT
+ should_draw_greyscale = FALSE
+
+/obj/item/bodypart/arm/right/flesh/Initialize(mapload, dont_spawn_flesh = FALSE)
+ . = ..()
+ if(!dont_spawn_flesh)
+ new /mob/living/basic/living_limb_flesh(src, src)
+ ADD_TRAIT(src, TRAIT_IGNORED_BY_LIVING_FLESH, BODYPART_TRAIT)
+
+/obj/item/bodypart/leg/left/flesh
+ limb_id = BODYPART_ID_MEAT
+ should_draw_greyscale = FALSE
+
+/obj/item/bodypart/leg/left/flesh/Initialize(mapload, dont_spawn_flesh = FALSE)
+ . = ..()
+ if(!dont_spawn_flesh)
+ new /mob/living/basic/living_limb_flesh(src, src)
+ ADD_TRAIT(src, TRAIT_IGNORED_BY_LIVING_FLESH, BODYPART_TRAIT)
+
+/obj/item/bodypart/leg/right/flesh
+ limb_id = BODYPART_ID_MEAT
+ should_draw_greyscale = FALSE
+
+/obj/item/bodypart/leg/right/flesh/Initialize(mapload, dont_spawn_flesh = FALSE)
+ . = ..()
+ if(!dont_spawn_flesh)
+ new /mob/living/basic/living_limb_flesh(src, src)
+ ADD_TRAIT(src, TRAIT_IGNORED_BY_LIVING_FLESH, BODYPART_TRAIT)
diff --git a/code/modules/surgery/bodyparts/species_parts/moth_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/moth_bodyparts.dm
index ed6c91ed0fc10..4dabec067fd0c 100644
--- a/code/modules/surgery/bodyparts/species_parts/moth_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/species_parts/moth_bodyparts.dm
@@ -14,6 +14,7 @@
limb_id = SPECIES_MOTH
is_dimorphic = TRUE
should_draw_greyscale = FALSE
+ wing_types = list(/obj/item/organ/external/wings/functional/moth/megamoth, /obj/item/organ/external/wings/functional/moth/mothra)
/obj/item/bodypart/arm/left/moth
icon = 'icons/mob/human/species/moth/bodyparts.dmi'
diff --git a/code/modules/surgery/bodyparts/species_parts/plasmaman_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/plasmaman_bodyparts.dm
index f478d522d5690..8ba27c2cdf9d0 100644
--- a/code/modules/surgery/bodyparts/species_parts/plasmaman_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/species_parts/plasmaman_bodyparts.dm
@@ -10,6 +10,7 @@
brute_modifier = 1.5 //Plasmemes are weak
burn_modifier = 1.5 //Plasmemes are weak
head_flags = HEAD_EYESPRITES
+ bodypart_flags = BODYPART_UNHUSKABLE
/obj/item/bodypart/chest/plasmaman
icon = 'icons/mob/human/species/plasmaman/bodyparts.dmi'
@@ -22,6 +23,8 @@
dmg_overlay_type = null
brute_modifier = 1.5 //Plasmemes are weak
burn_modifier = 1.5 //Plasmemes are weak
+ bodypart_flags = BODYPART_UNHUSKABLE
+ wing_types = NONE
/obj/item/bodypart/arm/left/plasmaman
icon = 'icons/mob/human/species/plasmaman/bodyparts.dmi'
@@ -33,6 +36,7 @@
dmg_overlay_type = null
brute_modifier = 1.5 //Plasmemes are weak
burn_modifier = 1.5 //Plasmemes are weak
+ bodypart_flags = BODYPART_UNHUSKABLE
/obj/item/bodypart/arm/right/plasmaman
icon = 'icons/mob/human/species/plasmaman/bodyparts.dmi'
@@ -44,6 +48,7 @@
dmg_overlay_type = null
brute_modifier = 1.5 //Plasmemes are weak
burn_modifier = 1.5 //Plasmemes are weak
+ bodypart_flags = BODYPART_UNHUSKABLE
/obj/item/bodypart/leg/left/plasmaman
icon = 'icons/mob/human/species/plasmaman/bodyparts.dmi'
@@ -55,6 +60,7 @@
dmg_overlay_type = null
brute_modifier = 1.5 //Plasmemes are weak
burn_modifier = 1.5 //Plasmemes are weak
+ bodypart_flags = BODYPART_UNHUSKABLE
/obj/item/bodypart/leg/right/plasmaman
icon = 'icons/mob/human/species/plasmaman/bodyparts.dmi'
@@ -66,3 +72,4 @@
dmg_overlay_type = null
brute_modifier = 1.5 //Plasmemes are weak
burn_modifier = 1.5 //Plasmemes are weak
+ bodypart_flags = BODYPART_UNHUSKABLE
diff --git a/code/modules/surgery/organ_manipulation.dm b/code/modules/surgery/organ_manipulation.dm
index c194c150d6da5..aaa0bc765e1cb 100644
--- a/code/modules/surgery/organ_manipulation.dm
+++ b/code/modules/surgery/organ_manipulation.dm
@@ -87,7 +87,7 @@
var/obj/item/tool = user.get_active_held_item()
if(step.try_op(user, target, user.zone_selected, tool, src, try_to_fail))
return TRUE
- if(tool && tool.item_flags) //Mechanic organ manipulation isn't done with just surgery tools
+ if(tool && tool.tool_behaviour) //Mechanic organ manipulation isn't done with just surgery tools
to_chat(user, span_warning("This step requires a different tool!"))
return TRUE
diff --git a/code/modules/surgery/organs/_organ.dm b/code/modules/surgery/organs/_organ.dm
index 5b78cb30796e7..a3feba76fece3 100644
--- a/code/modules/surgery/organs/_organ.dm
+++ b/code/modules/surgery/organs/_organ.dm
@@ -181,7 +181,11 @@ INITIALIZE_IMMEDIATE(/obj/item/organ)
SEND_SIGNAL(src, COMSIG_ORGAN_REMOVED, organ_owner)
SEND_SIGNAL(organ_owner, COMSIG_CARBON_LOSE_ORGAN, src, special)
- if(!IS_ROBOTIC_ORGAN(src) && !(item_flags & NO_BLOOD_ON_ITEM) && !QDELING(src))
+ // We don't need to readd things to the organ if it's getting deleted
+ if(QDELING(src))
+ return
+
+ if(!IS_ROBOTIC_ORGAN(src) && !(item_flags & NO_BLOOD_ON_ITEM))
AddElement(/datum/element/decal/blood)
var/list/diseases = organ_owner.get_static_viruses()
diff --git a/code/modules/surgery/organs/autosurgeon.dm b/code/modules/surgery/organs/autosurgeon.dm
index 0987df92bd94e..921acf808ed90 100644
--- a/code/modules/surgery/organs/autosurgeon.dm
+++ b/code/modules/surgery/organs/autosurgeon.dm
@@ -74,15 +74,21 @@
return
if(implant_time)
- user.visible_message( "[user] prepares to use [src] on [target].", "You begin to prepare to use [src] on [target].")
- if(!do_after(user, (8 SECONDS * surgery_speed), target))
+ user.visible_message(
+ span_notice("[user] prepares to use [src] on [target]."),
+ span_notice("You begin to prepare to use [src] on [target]."),
+ )
+ if(!do_after(user, (implant_time * surgery_speed), target))
return
if(target != user)
log_combat(user, target, "autosurgeon implanted [stored_organ] into", "[src]", "in [AREACOORD(target)]")
user.visible_message(span_notice("[user] presses a button on [src] as it plunges into [target]'s body."), span_notice("You press a button on [src] as it plunges into [target]'s body."))
else
- user.visible_message(span_notice("[user] pressses a button on [src] as it plunges into [user.p_their()] body."), "You press a button on [src] as it plunges into your body.")
+ user.visible_message(
+ span_notice("[user] pressses a button on [src] as it plunges into [user.p_their()] body."),
+ span_notice("You press a button on [src] as it plunges into your body."),
+ )
stored_organ.Insert(target)//insert stored organ into the user
stored_organ = null
diff --git a/code/modules/surgery/organs/external/tails.dm b/code/modules/surgery/organs/external/tails.dm
index e20f4f700f777..f509f8cd6c04c 100644
--- a/code/modules/surgery/organs/external/tails.dm
+++ b/code/modules/surgery/organs/external/tails.dm
@@ -34,7 +34,7 @@
/obj/item/organ/external/tail/Remove(mob/living/carbon/organ_owner, special, moving)
if(wag_flags & WAG_WAGGING)
- wag(FALSE)
+ wag(organ_owner, start = FALSE)
return ..()
@@ -47,30 +47,42 @@
organ_owner.add_mood_event("tail_lost", /datum/mood_event/tail_lost)
organ_owner.add_mood_event("tail_balance_lost", /datum/mood_event/tail_balance_lost)
-
-/obj/item/organ/external/tail/proc/wag(mob/user, start = TRUE, stop_after = 0)
+/obj/item/organ/external/tail/proc/wag(mob/living/carbon/organ_owner, start = TRUE, stop_after = 0)
if(!(wag_flags & WAG_ABLE))
return
if(start)
- start_wag()
- if(stop_after)
- addtimer(CALLBACK(src, PROC_REF(wag), FALSE), stop_after, TIMER_STOPPABLE|TIMER_DELETE_ME)
+ if(start_wag(organ_owner) && stop_after)
+ addtimer(CALLBACK(src, PROC_REF(wag), organ_owner, FALSE), stop_after, TIMER_STOPPABLE|TIMER_DELETE_ME)
else
- stop_wag()
- owner.update_body_parts()
+ stop_wag(organ_owner)
///We need some special behaviour for accessories, wrapped here so we can easily add more interactions later
-/obj/item/organ/external/tail/proc/start_wag()
+/obj/item/organ/external/tail/proc/start_wag(mob/living/carbon/organ_owner)
+ if(wag_flags & WAG_WAGGING) // we are already wagging
+ return FALSE
+ if(organ_owner.stat == DEAD || organ_owner != owner) // no wagging when owner is dead or tail has been disembodied
+ return FALSE
+
var/datum/bodypart_overlay/mutant/tail/accessory = bodypart_overlay
wag_flags |= WAG_WAGGING
accessory.wagging = TRUE
+ organ_owner.update_body_parts()
+ RegisterSignal(organ_owner, COMSIG_LIVING_DEATH, PROC_REF(stop_wag))
+ return TRUE
///We need some special behaviour for accessories, wrapped here so we can easily add more interactions later
-/obj/item/organ/external/tail/proc/stop_wag()
+/obj/item/organ/external/tail/proc/stop_wag(mob/living/carbon/organ_owner)
+ SIGNAL_HANDLER
+
var/datum/bodypart_overlay/mutant/tail/accessory = bodypart_overlay
wag_flags &= ~WAG_WAGGING
accessory.wagging = FALSE
+ if(isnull(organ_owner))
+ return
+
+ organ_owner.update_body_parts()
+ UnregisterSignal(organ_owner, COMSIG_LIVING_DEATH)
///Tail parent type, with wagging functionality
/datum/bodypart_overlay/mutant/tail
diff --git a/code/modules/surgery/organs/external/wings/functional_wings.dm b/code/modules/surgery/organs/external/wings/functional_wings.dm
index 1313da8826811..45dee7691c865 100644
--- a/code/modules/surgery/organs/external/wings/functional_wings.dm
+++ b/code/modules/surgery/organs/external/wings/functional_wings.dm
@@ -26,6 +26,9 @@
///Are our wings open or closed?
var/wings_open = FALSE
+ // grind_results = list(/datum/reagent/flightpotion = 5)
+ food_reagents = list(/datum/reagent/flightpotion = 5)
+
/obj/item/organ/external/wings/functional/Insert(mob/living/carbon/receiver, special, drop_if_replaced)
. = ..()
if(. && isnull(fly))
diff --git a/code/modules/surgery/organs/internal/appendix/_appendix.dm b/code/modules/surgery/organs/internal/appendix/_appendix.dm
index bb02c8b9ef9e9..43a6bfb7a41a8 100644
--- a/code/modules/surgery/organs/internal/appendix/_appendix.dm
+++ b/code/modules/surgery/organs/internal/appendix/_appendix.dm
@@ -46,7 +46,12 @@
if(owner)
ADD_TRAIT(owner, TRAIT_DISEASELIKE_SEVERITY_MEDIUM, type)
owner.med_hud_set_status()
- notify_ghosts("[owner] has developed spontaneous appendicitis!", source = owner, action = NOTIFY_ORBIT, header = "Whoa, Sick!")
+ notify_ghosts(
+ "[owner] has developed spontaneous appendicitis!",
+ source = owner,
+ action = NOTIFY_ORBIT,
+ header = "Whoa, Sick!",
+ )
/obj/item/organ/internal/appendix/proc/inflamation(seconds_per_tick)
var/mob/living/carbon/organ_owner = owner
diff --git a/code/modules/surgery/organs/internal/cyberimp/augments_arms.dm b/code/modules/surgery/organs/internal/cyberimp/augments_arms.dm
index 86cdf85513446..486284b8b6d2c 100644
--- a/code/modules/surgery/organs/internal/cyberimp/augments_arms.dm
+++ b/code/modules/surgery/organs/internal/cyberimp/augments_arms.dm
@@ -381,11 +381,11 @@
/obj/item/organ/internal/cyberimp/arm/muscle/Insert(mob/living/carbon/reciever, special = FALSE, drop_if_replaced = TRUE)
. = ..()
if(ishuman(reciever)) //Sorry, only humans
- RegisterSignal(reciever, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, PROC_REF(on_attack_hand))
+ RegisterSignal(reciever, COMSIG_LIVING_EARLY_UNARMED_ATTACK, PROC_REF(on_attack_hand))
/obj/item/organ/internal/cyberimp/arm/muscle/Remove(mob/living/carbon/implant_owner, special = 0)
. = ..()
- UnregisterSignal(implant_owner, COMSIG_HUMAN_EARLY_UNARMED_ATTACK)
+ UnregisterSignal(implant_owner, COMSIG_LIVING_EARLY_UNARMED_ATTACK)
/obj/item/organ/internal/cyberimp/arm/muscle/emp_act(severity)
. = ..()
@@ -402,15 +402,17 @@
/obj/item/organ/internal/cyberimp/arm/muscle/proc/on_attack_hand(mob/living/carbon/human/source, atom/target, proximity, modifiers)
SIGNAL_HANDLER
- if(source.get_active_hand() != source.get_bodypart(check_zone(zone)) || !proximity)
- return
+ if(source.get_active_hand() != hand || !proximity)
+ return NONE
if(!source.combat_mode || LAZYACCESS(modifiers, RIGHT_CLICK))
- return
+ return NONE
if(!isliving(target))
- return
+ return NONE
var/datum/dna/dna = source.has_dna()
if(dna?.check_mutation(/datum/mutation/human/hulk)) //NO HULK
- return
+ return NONE
+ if(!source.can_unarmed_attack())
+ return COMPONENT_SKIP_ATTACK
var/mob/living/living_target = target
source.changeNext_move(CLICK_CD_MELEE)
diff --git a/code/modules/surgery/organs/internal/eyes/_eyes.dm b/code/modules/surgery/organs/internal/eyes/_eyes.dm
index 6d9c941e830a2..6933374140dc3 100644
--- a/code/modules/surgery/organs/internal/eyes/_eyes.dm
+++ b/code/modules/surgery/organs/internal/eyes/_eyes.dm
@@ -363,14 +363,14 @@
. = ..()
if(!eye)
eye = new /obj/item/flashlight/eyelight()
- eye.on = TRUE
+ eye.set_light_on(TRUE)
eye.forceMove(victim)
eye.update_brightness(victim)
victim.become_blind(FLASHLIGHT_EYES)
/obj/item/organ/internal/eyes/robotic/flashlight/on_remove(mob/living/carbon/victim)
. = ..()
- eye.on = FALSE
+ eye.set_light_on(FALSE)
eye.update_brightness(victim)
eye.forceMove(src)
victim.cure_blind(FLASHLIGHT_EYES)
@@ -420,7 +420,7 @@
/obj/item/organ/internal/eyes/robotic/glow/emp_act()
. = ..()
- if(!eye.on || . & EMP_PROTECT_SELF)
+ if(!eye.light_on || . & EMP_PROTECT_SELF)
return
deactivate(close_ui = TRUE)
@@ -521,7 +521,7 @@
* Turns on the attached flashlight object, updates the mob overlay to be added.
*/
/obj/item/organ/internal/eyes/robotic/glow/proc/activate()
- eye.on = TRUE
+ eye.light_on = TRUE
if(eye.light_range) // at range 0 we are just going to make the eyes glow emissively, no light overlay
eye.set_light_on(TRUE)
update_mob_eye_color()
@@ -537,7 +537,6 @@
/obj/item/organ/internal/eyes/robotic/glow/proc/deactivate(mob/living/carbon/eye_owner = owner, close_ui = FALSE)
if(close_ui)
SStgui.close_uis(src)
- eye.on = FALSE
eye.set_light_on(FALSE)
update_mob_eye_color(eye_owner)
@@ -564,7 +563,7 @@
*/
/obj/item/organ/internal/eyes/robotic/glow/proc/set_beam_range(new_range)
var/old_light_range = eye.light_range
- if(old_light_range == 0 && new_range > 0 && eye.on) // turn bring back the light overlay if we were previously at 0 (aka emissive eyes only)
+ if(old_light_range == 0 && new_range > 0 && eye.light_on) // turn bring back the light overlay if we were previously at 0 (aka emissive eyes only)
eye.light_on = FALSE // this is stupid, but this has to be FALSE for set_light_on() to work.
eye.set_light_on(TRUE)
eye.set_light_range(clamp(new_range, 0, max_light_beam_distance))
@@ -599,7 +598,7 @@
* Toggle the attached flashlight object on or off
*/
/obj/item/organ/internal/eyes/robotic/glow/proc/toggle_active()
- if(eye.on)
+ if(eye.light_on)
deactivate()
else
activate()
@@ -632,7 +631,7 @@
if(QDELETED(eye_owner) || !ishuman(eye_owner)) //Other carbon mobs don't have eye color.
return
- if(!eye.on)
+ if(!eye.light_on)
eye_icon_state = initial(eye_icon_state)
overlay_ignore_lighting = FALSE
else
@@ -727,7 +726,7 @@
//add lighting
if(!adapt_light)
adapt_light = new /obj/item/flashlight/eyelight/adapted()
- adapt_light.on = TRUE
+ adapt_light.set_light_on(TRUE)
adapt_light.forceMove(eye_owner)
adapt_light.update_brightness(eye_owner)
ADD_TRAIT(eye_owner, TRAIT_UNNATURAL_RED_GLOWY_EYES, ORGAN_TRAIT)
@@ -743,7 +742,7 @@
/obj/item/organ/internal/eyes/night_vision/maintenance_adapted/Remove(mob/living/carbon/unadapted, special = FALSE)
//remove lighting
- adapt_light.on = FALSE
+ adapt_light.set_light_on(FALSE)
adapt_light.update_brightness(unadapted)
adapt_light.forceMove(src)
REMOVE_TRAIT(unadapted, TRAIT_UNNATURAL_RED_GLOWY_EYES, ORGAN_TRAIT)
diff --git a/code/modules/surgery/organs/internal/heart/heart_ethereal.dm b/code/modules/surgery/organs/internal/heart/heart_ethereal.dm
index bb0b30ddd6286..8ad9301fe7412 100644
--- a/code/modules/surgery/organs/internal/heart/heart_ethereal.dm
+++ b/code/modules/surgery/organs/internal/heart/heart_ethereal.dm
@@ -139,7 +139,7 @@
return
///Lets you stop the process with enough brute damage
-/obj/item/organ/internal/heart/ethereal/proc/on_take_damage(datum/source, damage, damagetype, def_zone)
+/obj/item/organ/internal/heart/ethereal/proc/on_take_damage(datum/source, damage, damagetype, def_zone, ...)
SIGNAL_HANDLER
if(damagetype != BRUTE)
return
diff --git a/code/modules/surgery/organs/internal/lungs/_lungs.dm b/code/modules/surgery/organs/internal/lungs/_lungs.dm
index faf67a4596f2c..2e99f70d98916 100644
--- a/code/modules/surgery/organs/internal/lungs/_lungs.dm
+++ b/code/modules/surgery/organs/internal/lungs/_lungs.dm
@@ -264,7 +264,7 @@
return
var/ratio = (breath.gases[/datum/gas/oxygen][MOLES] / safe_oxygen_max) * 10
- breather.apply_damage_type(clamp(ratio, oxy_breath_dam_min, oxy_breath_dam_max), oxy_damage_type)
+ breather.apply_damage(clamp(ratio, oxy_breath_dam_min, oxy_breath_dam_max), oxy_damage_type, spread_damage = TRUE)
breather.throw_alert(ALERT_TOO_MUCH_OXYGEN, /atom/movable/screen/alert/too_much_oxy)
/// Handles NOT having too much o2. only relevant if safe_oxygen_max has a value
@@ -320,10 +320,10 @@
breather.throw_alert(ALERT_TOO_MUCH_CO2, /atom/movable/screen/alert/too_much_co2)
breather.Unconscious(6 SECONDS)
// Lets hurt em a little, let them know we mean business.
- breather.apply_damage_type(3, co2_damage_type)
+ breather.apply_damage(3, co2_damage_type, spread_damage = TRUE)
// They've been in here 30s now, start to kill them for their own good!
if((world.time - breather.co2overloadtime) > 30 SECONDS)
- breather.apply_damage_type(8, co2_damage_type)
+ breather.apply_damage(8, co2_damage_type, spread_damage = TRUE)
/// Handles NOT having too much co2. only relevant if safe_co2_max has a value
/obj/item/organ/internal/lungs/proc/safe_co2(mob/living/carbon/breather, datum/gas_mixture/breath, old_co2_pp)
@@ -364,7 +364,7 @@
breather.throw_alert(ALERT_TOO_MUCH_PLASMA, /atom/movable/screen/alert/too_much_plas)
var/ratio = (breath.gases[/datum/gas/plasma][MOLES] / safe_plasma_max) * 10
- breather.apply_damage_type(clamp(ratio, plas_breath_dam_min, plas_breath_dam_max), plas_damage_type)
+ breather.apply_damage(clamp(ratio, plas_breath_dam_min, plas_breath_dam_max), plas_damage_type, spread_damage = TRUE)
/// Resets plasma side effects
/obj/item/organ/internal/lungs/proc/safe_plasma(mob/living/carbon/breather, datum/gas_mixture/breath, old_plasma_pp)
@@ -754,11 +754,11 @@
if(!HAS_TRAIT(breather, TRAIT_RESISTCOLD)) // COLD DAMAGE
var/cold_modifier = breather.dna.species.coldmod
if(breath_temperature < cold_level_3_threshold)
- breather.apply_damage_type(cold_level_3_damage*cold_modifier, cold_damage_type)
+ breather.apply_damage(cold_level_3_damage*cold_modifier, cold_damage_type, spread_damage = TRUE)
if(breath_temperature > cold_level_3_threshold && breath_temperature < cold_level_2_threshold)
- breather.apply_damage_type(cold_level_2_damage*cold_modifier, cold_damage_type)
+ breather.apply_damage(cold_level_2_damage*cold_modifier, cold_damage_type, spread_damage = TRUE)
if(breath_temperature > cold_level_2_threshold && breath_temperature < cold_level_1_threshold)
- breather.apply_damage_type(cold_level_1_damage*cold_modifier, cold_damage_type)
+ breather.apply_damage(cold_level_1_damage*cold_modifier, cold_damage_type, spread_damage = TRUE)
if(breath_temperature < cold_level_1_threshold)
if(prob(20))
to_chat(breather, span_warning("You feel [cold_message] in your [name]!"))
@@ -766,11 +766,11 @@
if(!HAS_TRAIT(breather, TRAIT_RESISTHEAT)) // HEAT DAMAGE
var/heat_modifier = breather.dna.species.heatmod
if(breath_temperature > heat_level_1_threshold && breath_temperature < heat_level_2_threshold)
- breather.apply_damage_type(heat_level_1_damage*heat_modifier, heat_damage_type)
+ breather.apply_damage(heat_level_1_damage*heat_modifier, heat_damage_type, spread_damage = TRUE)
if(breath_temperature > heat_level_2_threshold && breath_temperature < heat_level_3_threshold)
- breather.apply_damage_type(heat_level_2_damage*heat_modifier, heat_damage_type)
+ breather.apply_damage(heat_level_2_damage*heat_modifier, heat_damage_type, spread_damage = TRUE)
if(breath_temperature > heat_level_3_threshold)
- breather.apply_damage_type(heat_level_3_damage*heat_modifier, heat_damage_type)
+ breather.apply_damage(heat_level_3_damage*heat_modifier, heat_damage_type, spread_damage = TRUE)
if(breath_temperature > heat_level_1_threshold)
if(prob(20))
to_chat(breather, span_warning("You feel [hot_message] in your [name]!"))
diff --git a/code/modules/surgery/organs/internal/stomach/stomach_ethereal.dm b/code/modules/surgery/organs/internal/stomach/stomach_ethereal.dm
index 4cc8ee404c14d..c1632f33329d4 100644
--- a/code/modules/surgery/organs/internal/stomach/stomach_ethereal.dm
+++ b/code/modules/surgery/organs/internal/stomach/stomach_ethereal.dm
@@ -92,7 +92,7 @@
playsound(carbon, 'sound/magic/lightningshock.ogg', 100, TRUE, extrarange = 5)
carbon.cut_overlay(overcharge)
- tesla_zap(carbon, 2, crystal_charge * 1e3, ZAP_OBJ_DAMAGE | ZAP_LOW_POWER_GEN | ZAP_ALLOW_DUPLICATES)
+ tesla_zap(source = carbon, zap_range = 2, power = crystal_charge * 2.5, cutoff = 1e3, zap_flags = ZAP_OBJ_DAMAGE | ZAP_LOW_POWER_GEN | ZAP_ALLOW_DUPLICATES)
adjust_charge(ETHEREAL_CHARGE_FULL - crystal_charge)
carbon.visible_message(span_danger("[carbon] violently discharges energy!"), span_warning("You violently discharge energy!"))
diff --git a/code/modules/tgs/core/core.dm b/code/modules/tgs/core/core.dm
index 41a0473394525..b9a9f27a28ae8 100644
--- a/code/modules/tgs/core/core.dm
+++ b/code/modules/tgs/core/core.dm
@@ -153,4 +153,9 @@
/world/TgsSecurityLevel()
var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
if(api)
- api.SecurityLevel()
+ return api.SecurityLevel()
+
+/world/TgsVisibility()
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ return api.Visibility()
diff --git a/code/modules/tgs/core/datum.dm b/code/modules/tgs/core/datum.dm
index 68b0330fe8606..07ce3b684584e 100644
--- a/code/modules/tgs/core/datum.dm
+++ b/code/modules/tgs/core/datum.dm
@@ -11,6 +11,15 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
src.event_handler = event_handler
src.version = version
+/datum/tgs_api/proc/TerminateWorld()
+ while(TRUE)
+ TGS_DEBUG_LOG("About to terminate world. Tick: [world.time], sleep_offline: [world.sleep_offline]")
+ world.sleep_offline = FALSE // https://www.byond.com/forum/post/2894866
+ del(world)
+ world.sleep_offline = FALSE // just in case, this is BYOND after all...
+ sleep(1)
+ TGS_DEBUG_LOG("BYOND DIDN'T TERMINATE THE WORLD!!! TICK IS: [world.time], sleep_offline: [world.sleep_offline]")
+
/datum/tgs_api/latest
parent_type = /datum/tgs_api/v5
@@ -57,3 +66,6 @@ TGS_PROTECT_DATUM(/datum/tgs_api)
/datum/tgs_api/proc/SecurityLevel()
return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/Visibility()
+ return TGS_UNIMPLEMENTED
diff --git a/code/modules/tgs/v4/api.dm b/code/modules/tgs/v4/api.dm
index b9a75c4abb489..945e2e4117671 100644
--- a/code/modules/tgs/v4/api.dm
+++ b/code/modules/tgs/v4/api.dm
@@ -73,7 +73,7 @@
if(cached_json["apiValidateOnly"])
TGS_INFO_LOG("Validating API and exiting...")
Export(TGS4_COMM_VALIDATE, list(TGS4_PARAMETER_DATA = "[minimum_required_security_level]"))
- del(world)
+ TerminateWorld()
security_level = cached_json["securityLevel"]
chat_channels_json_path = cached_json["chatChannelsJson"]
@@ -188,7 +188,7 @@
requesting_new_port = TRUE
if(!world.OpenPort(0)) //open any port
TGS_ERROR_LOG("Unable to open random port to retrieve new port![TGS4_PORT_CRITFAIL_MESSAGE]")
- del(world)
+ TerminateWorld()
//request a new port
export_lock = FALSE
@@ -196,16 +196,16 @@
if(!new_port_json)
TGS_ERROR_LOG("No new port response from server![TGS4_PORT_CRITFAIL_MESSAGE]")
- del(world)
+ TerminateWorld()
var/new_port = new_port_json[TGS4_PARAMETER_DATA]
if(!isnum(new_port) || new_port <= 0)
TGS_ERROR_LOG("Malformed new port json ([json_encode(new_port_json)])![TGS4_PORT_CRITFAIL_MESSAGE]")
- del(world)
+ TerminateWorld()
if(new_port != world.port && !world.OpenPort(new_port))
TGS_ERROR_LOG("Unable to open port [new_port]![TGS4_PORT_CRITFAIL_MESSAGE]")
- del(world)
+ TerminateWorld()
requesting_new_port = FALSE
while(export_lock)
diff --git a/code/modules/tgs/v5/__interop_version.dm b/code/modules/tgs/v5/__interop_version.dm
index 5d3d491a7362b..1b52b31d6a73e 100644
--- a/code/modules/tgs/v5/__interop_version.dm
+++ b/code/modules/tgs/v5/__interop_version.dm
@@ -1 +1 @@
-"5.6.1"
+"5.6.2"
diff --git a/code/modules/tgs/v5/_defines.dm b/code/modules/tgs/v5/_defines.dm
index f973338daa032..bdcd4e4dd58e6 100644
--- a/code/modules/tgs/v5/_defines.dm
+++ b/code/modules/tgs/v5/_defines.dm
@@ -48,6 +48,7 @@
#define DMAPI5_RUNTIME_INFORMATION_REVISION "revision"
#define DMAPI5_RUNTIME_INFORMATION_TEST_MERGES "testMerges"
#define DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL "securityLevel"
+#define DMAPI5_RUNTIME_INFORMATION_VISIBILITY "visibility"
#define DMAPI5_CHAT_UPDATE_CHANNELS "channels"
diff --git a/code/modules/tgs/v5/api.dm b/code/modules/tgs/v5/api.dm
index 34cc43f8762f7..7226f29bba603 100644
--- a/code/modules/tgs/v5/api.dm
+++ b/code/modules/tgs/v5/api.dm
@@ -4,6 +4,7 @@
var/instance_name
var/security_level
+ var/visibility
var/reboot_mode = TGS_REBOOT_MODE_NORMAL
@@ -50,10 +51,11 @@
if(runtime_information[DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY])
TGS_INFO_LOG("DMAPI validation, exiting...")
- del(world)
+ TerminateWorld()
version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION])
security_level = runtime_information[DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL]
+ visibility = runtime_information[DMAPI5_RUNTIME_INFORMATION_VISIBILITY]
instance_name = runtime_information[DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME]
var/list/revisionData = runtime_information[DMAPI5_RUNTIME_INFORMATION_REVISION]
@@ -252,3 +254,7 @@
/datum/tgs_api/v5/SecurityLevel()
RequireInitialBridgeResponse()
return security_level
+
+/datum/tgs_api/v5/Visibility()
+ RequireInitialBridgeResponse()
+ return visibility
diff --git a/code/modules/tgs/v5/undefs.dm b/code/modules/tgs/v5/undefs.dm
index c679737dfc496..f163adaaafe3b 100644
--- a/code/modules/tgs/v5/undefs.dm
+++ b/code/modules/tgs/v5/undefs.dm
@@ -48,6 +48,7 @@
#undef DMAPI5_RUNTIME_INFORMATION_REVISION
#undef DMAPI5_RUNTIME_INFORMATION_TEST_MERGES
#undef DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL
+#undef DMAPI5_RUNTIME_INFORMATION_VISIBILITY
#undef DMAPI5_CHAT_UPDATE_CHANNELS
diff --git a/code/modules/tgui/states/notcontained.dm b/code/modules/tgui/states/notcontained.dm
index 018e0fa0304c3..b144a375757d0 100644
--- a/code/modules/tgui/states/notcontained.dm
+++ b/code/modules/tgui/states/notcontained.dm
@@ -28,5 +28,5 @@ GLOBAL_DATUM_INIT(notcontained_state, /datum/ui_state/notcontained_state, new)
/mob/living/silicon/notcontained_can_use_topic(src_object)
return default_can_use_topic(src_object) // Silicons use default bevhavior.
-/mob/living/simple_animal/drone/notcontained_can_use_topic(src_object)
+/mob/living/basic/drone/notcontained_can_use_topic(src_object)
return default_can_use_topic(src_object) // Drones use default bevhavior.
diff --git a/code/modules/tgui_input/alert.dm b/code/modules/tgui_input/alert.dm
index 4dc197a3c8cdd..0ea9c45d310b8 100644
--- a/code/modules/tgui_input/alert.dm
+++ b/code/modules/tgui_input/alert.dm
@@ -18,7 +18,11 @@
var/client/client = user
user = client.mob
else
- return
+ return null
+
+ if(isnull(user.client))
+ return null
+
// A gentle nudge - you should not be using TGUI alert for anything other than a simple message.
if(length(buttons) > 3)
log_tgui(user, "Error: TGUI Alert initiated with too many buttons. Use a list.", "TguiAlert")
diff --git a/code/modules/tgui_input/checkboxes.dm b/code/modules/tgui_input/checkboxes.dm
index ec43bd8914d5a..9204e67ba3607 100644
--- a/code/modules/tgui_input/checkboxes.dm
+++ b/code/modules/tgui_input/checkboxes.dm
@@ -14,13 +14,17 @@
if (!user)
user = usr
if(!length(items))
- return
+ return null
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
- return
+ return null
+
+ if(isnull(user.client))
+ return null
+
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
return input(user, message, title) as null|anything in items
var/datum/tgui_checkbox_input/input = new(user, message, title, items, min_checked, max_checked, timeout, ui_state)
diff --git a/code/modules/tgui_input/list.dm b/code/modules/tgui_input/list.dm
index 95daaadb32649..18525e6b10a1d 100644
--- a/code/modules/tgui_input/list.dm
+++ b/code/modules/tgui_input/list.dm
@@ -14,17 +14,24 @@
if (!user)
user = usr
if(!length(items))
- return
+ return null
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
- return
+ return null
+
+ if(isnull(user.client))
+ return null
+
/// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
return input(user, message, title, default) as null|anything in items
var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout, ui_state)
+ if(input.invalid)
+ qdel(input)
+ return
input.ui_interact(user)
input.wait()
if (input)
@@ -58,6 +65,8 @@
var/closed
/// The TGUI UI state that will be returned in ui_state(). Default: always_state
var/datum/ui_state/state
+ /// Whether the tgui list input is invalid or not (i.e. due to all list entries being null)
+ var/invalid = FALSE
/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout, ui_state)
src.title = title
@@ -77,6 +86,9 @@
string_key = avoid_assoc_duplicate_keys(string_key, repeat_items)
src.items += string_key
src.items_map[string_key] = i
+
+ if(length(src.items) == 0)
+ invalid = TRUE
if (timeout)
src.timeout = timeout
start_time = world.time
diff --git a/code/modules/tgui_input/number.dm b/code/modules/tgui_input/number.dm
index bcdf495fd82e8..e0a3f1951e5a7 100644
--- a/code/modules/tgui_input/number.dm
+++ b/code/modules/tgui_input/number.dm
@@ -23,7 +23,11 @@
var/client/client = user
user = client.mob
else
- return
+ return null
+
+ if (isnull(user.client))
+ return null
+
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
var/input_number = input(user, message, title, default) as null|num
diff --git a/code/modules/tgui_input/say_modal/modal.dm b/code/modules/tgui_input/say_modal/modal.dm
index 8afba448762aa..bc3f8f314e021 100644
--- a/code/modules/tgui_input/say_modal/modal.dm
+++ b/code/modules/tgui_input/say_modal/modal.dm
@@ -36,6 +36,7 @@
/datum/tgui_say/New(client/client, id)
src.client = client
window = new(client, id)
+ winset(client, "tgui_say", "size=1,1;is-visible=0;")
window.subscribe(src, PROC_REF(on_message))
window.is_browser = TRUE
@@ -62,11 +63,14 @@
*/
/datum/tgui_say/proc/load()
window_open = FALSE
- winshow(client, "tgui_say", FALSE)
+
+ winset(client, "tgui_say", "pos=848,500;size=231,30;is-visible=0;")
+
window.send_message("props", list(
lightMode = client.prefs?.read_preference(/datum/preference/toggle/tgui_say_light_mode),
maxLength = max_length,
))
+
stop_thinking()
return TRUE
@@ -84,9 +88,7 @@
window_open = TRUE
if(payload["channel"] != OOC_CHANNEL && payload["channel"] != ADMIN_CHANNEL)
start_thinking()
- if(client.typing_indicators)
- log_speech_indicators("[key_name(client)] started typing at [loc_name(client.mob)], indicators enabled.")
- else
+ if(!client.typing_indicators)
log_speech_indicators("[key_name(client)] started typing at [loc_name(client.mob)], indicators DISABLED.")
return TRUE
@@ -97,9 +99,7 @@
/datum/tgui_say/proc/close()
window_open = FALSE
stop_thinking()
- if(client.typing_indicators)
- log_speech_indicators("[key_name(client)] stopped typing at [loc_name(client.mob)], indicators enabled.")
- else
+ if(!client.typing_indicators)
log_speech_indicators("[key_name(client)] stopped typing at [loc_name(client.mob)], indicators DISABLED.")
/**
diff --git a/code/modules/tgui_input/text.dm b/code/modules/tgui_input/text.dm
index 811673a4c03aa..f78ededab5d96 100644
--- a/code/modules/tgui_input/text.dm
+++ b/code/modules/tgui_input/text.dm
@@ -23,7 +23,11 @@
var/client/client = user
user = client.mob
else
- return
+ return null
+
+ if(isnull(user.client))
+ return null
+
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
if(encode)
diff --git a/code/modules/transport/_transport_machinery.dm b/code/modules/transport/_transport_machinery.dm
new file mode 100644
index 0000000000000..2d10b4ada5d2a
--- /dev/null
+++ b/code/modules/transport/_transport_machinery.dm
@@ -0,0 +1,199 @@
+/obj/machinery/transport
+ armor_type = /datum/armor/transport_machinery
+ max_integrity = 400
+ integrity_failure = 0.1
+ /// ID of the transport we're associated with for filtering commands
+ var/configured_transport_id = TRAMSTATION_LINE_1
+ /// weakref of the transport we're associated with
+ var/datum/weakref/transport_ref
+ var/list/methods_to_fix = list()
+ var/list/repair_signals
+ var/static/list/how_do_we_fix_it = list(
+ "try turning it off and on again with a multitool" = TOOL_MULTITOOL,
+ "try forcing an unexpected reboot with a multitool" = TOOL_MULTITOOL,
+ "patch the system's call table with a multitool" = TOOL_MULTITOOL,
+ "gently reset the invalid memory with a crowbar" = TOOL_CROWBAR,
+ "secure its ground connection with a wrench" = TOOL_WRENCH,
+ "tighten some screws with a screwdriver" = TOOL_SCREWDRIVER,
+ "check its wire voltages with a multitool" = TOOL_MULTITOOL,
+ "cut some excess wires with wirecutters" = TOOL_WIRECUTTER,
+ )
+ var/malfunctioning = FALSE
+
+/datum/armor/transport_machinery
+ melee = 40
+ bullet = 10
+ laser = 10
+ bomb = 45
+ fire = 90
+ acid = 100
+
+/obj/machinery/transport/Initialize(mapload)
+ . = ..()
+ if(!id_tag)
+ id_tag = assign_random_name()
+
+/obj/machinery/transport/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ if(held_item?.tool_behaviour == TOOL_SCREWDRIVER)
+ context[SCREENTIP_CONTEXT_RMB] = panel_open ? "close panel" : "open panel"
+
+ if(panel_open)
+ if(malfunctioning || methods_to_fix.len)
+ context[SCREENTIP_CONTEXT_LMB] = "repair electronics"
+ if(held_item?.tool_behaviour == TOOL_CROWBAR)
+ context[SCREENTIP_CONTEXT_RMB] = "deconstruct"
+
+ if(held_item?.tool_behaviour == TOOL_WELDER)
+ context[SCREENTIP_CONTEXT_LMB] = "repair frame"
+
+ return CONTEXTUAL_SCREENTIP_SET
+
+/**
+ * Finds the tram
+ *
+ * Locates tram parts in the lift global list after everything is done.
+ */
+/obj/machinery/transport/proc/link_tram()
+ for(var/datum/transport_controller/linear/tram/tram as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
+ if(tram.specific_transport_id != configured_transport_id)
+ continue
+ transport_ref = WEAKREF(tram)
+ log_transport("[id_tag]: Successfuly linked to transport ID [tram.specific_transport_id] [transport_ref]")
+ break
+
+ if(isnull(transport_ref))
+ log_transport("[id_tag]: Tried to find a transport with ID [configured_transport_id], but failed!")
+
+/obj/machinery/transport/proc/local_fault()
+ if(malfunctioning || !isnull(repair_signals))
+ return
+
+ generate_repair_signals()
+ malfunctioning = TRUE
+ set_is_operational(FALSE)
+ update_appearance()
+
+/**
+ * All subtypes have the same method of repair for consistency and predictability
+ * The key of this assoc list is the "method" of how they're fixing the thing (just flavor for examine),
+ * and the value is what tool they actually need to use on the thing to fix it
+ */
+/obj/machinery/transport/proc/generate_repair_signals()
+
+ // Select a few methods of how to fix it
+ var/list/fix_it_keys = assoc_to_keys(how_do_we_fix_it)
+ methods_to_fix += pick_n_take(fix_it_keys)
+
+ // Construct the signals
+ LAZYINITLIST(repair_signals)
+ for(var/tool_method as anything in methods_to_fix)
+ repair_signals += COMSIG_ATOM_TOOL_ACT(how_do_we_fix_it[tool_method])
+
+ // Register signals to make it fixable
+ if(length(repair_signals))
+ RegisterSignals(src, repair_signals, PROC_REF(on_machine_tooled))
+
+/obj/machinery/transport/proc/clear_repair_signals()
+ UnregisterSignal(src, repair_signals)
+ QDEL_LAZYLIST(repair_signals)
+
+/obj/machinery/transport/examine(mob/user)
+ . = ..()
+ if(methods_to_fix)
+ for(var/tool_method as anything in methods_to_fix)
+ . += span_warning("It needs someone to [EXAMINE_HINT(tool_method)].")
+ if(panel_open)
+ . += span_notice("It can be deconstructed with a [EXAMINE_HINT("crowbar.")]")
+
+/**
+ * Signal proc for [COMSIG_ATOM_TOOL_ACT], from a variety of signals, registered on the machinery.
+ *
+ * We allow for someone to stop the event early by using the proper tools, hinted at in examine, on the machine
+ */
+/obj/machinery/transport/proc/on_machine_tooled(obj/machinery/source, mob/living/user, obj/item/tool)
+ SIGNAL_HANDLER
+
+ INVOKE_ASYNC(src, PROC_REF(try_fix_machine), source, user, tool)
+ return COMPONENT_BLOCK_TOOL_ATTACK
+
+/// Attempts a do_after, and if successful, stops the event
+/obj/machinery/transport/proc/try_fix_machine(obj/machinery/transport/machine, mob/living/user, obj/item/tool)
+ SHOULD_CALL_PARENT(TRUE)
+
+ machine.balloon_alert(user, "percussive maintenance...")
+ if(!tool.use_tool(machine, user, 7 SECONDS, volume = 50))
+ machine.balloon_alert(user, "interrupted!")
+ return FALSE
+
+ playsound(src, 'sound/machines/synth_yes.ogg', 75, use_reverb = TRUE)
+ machine.balloon_alert(user, "success!")
+ UnregisterSignal(src, repair_signals)
+ QDEL_LAZYLIST(repair_signals)
+ QDEL_LAZYLIST(methods_to_fix)
+ malfunctioning = FALSE
+ set_machine_stat(machine_stat & ~EMAGGED)
+ set_is_operational(TRUE)
+ update_appearance()
+ return TRUE
+
+/obj/machinery/transport/welder_act(mob/living/user, obj/item/tool)
+ if(user.combat_mode)
+ return
+ if(atom_integrity >= max_integrity)
+ balloon_alert(user, "it doesn't need repairs!")
+ return TRUE
+ balloon_alert(user, "repairing...")
+ if(!tool.use_tool(src, user, 4 SECONDS, amount = 0, volume=50))
+ return TRUE
+ balloon_alert(user, "repaired")
+ atom_integrity = max_integrity
+ set_machine_stat(machine_stat & ~BROKEN)
+ update_appearance()
+ return TRUE
+
+/obj/item/wallframe/tram/try_build(obj/structure/tram/on_tram, mob/user)
+ if(get_dist(on_tram,user) > 1)
+ balloon_alert(user, "you are too far!")
+ return
+
+ var/floor_to_tram = get_dir(user, on_tram)
+ if(!(floor_to_tram in GLOB.cardinals))
+ balloon_alert(user, "stand in line with tram wall!")
+ return
+
+ var/turf/tram_turf = get_turf(user)
+ var/obj/structure/thermoplastic/tram_floor = locate() in tram_turf
+ if(!istype(tram_floor))
+ balloon_alert(user, "needs tram!")
+ return
+
+ if(check_wall_item(tram_turf, floor_to_tram, wall_external))
+ balloon_alert(user, "already something here!")
+ return
+
+ return TRUE
+
+/obj/item/wallframe/tram/attach(obj/structure/tram/on_tram, mob/user)
+ if(result_path)
+ playsound(src.loc, 'sound/machines/click.ogg', 75, TRUE)
+ user.visible_message(span_notice("[user.name] installs [src] on the tram."),
+ span_notice("You install [src] on the tram."),
+ span_hear("You hear clicking."))
+ var/floor_to_tram = get_dir(user, on_tram)
+
+ var/obj/cabinet = new result_path(get_turf(user), floor_to_tram, TRUE)
+ cabinet.setDir(floor_to_tram)
+
+ if(pixel_shift)
+ switch(floor_to_tram)
+ if(NORTH)
+ cabinet.pixel_y = pixel_shift
+ if(SOUTH)
+ cabinet.pixel_y = -pixel_shift
+ if(EAST)
+ cabinet.pixel_x = pixel_shift
+ if(WEST)
+ cabinet.pixel_x = -pixel_shift
+ after_attach(cabinet)
+
+ qdel(src)
diff --git a/code/modules/transport/admin.dm b/code/modules/transport/admin.dm
new file mode 100644
index 0000000000000..9d68d967e60ae
--- /dev/null
+++ b/code/modules/transport/admin.dm
@@ -0,0 +1,85 @@
+/**
+ * Helper tool to try and resolve tram controller errors, or reset the contents if someone put a million chickens on the tram
+ * and now it's slow as hell and lagging things.
+ */
+/datum/admins/proc/reset_tram()
+ set name = "Reset Tram"
+ set category = "Debug"
+ var/static/list/debug_tram_list = list(
+ TRAMSTATION_LINE_1,
+ BIRDSHOT_LINE_1,
+ BIRDSHOT_LINE_2,
+ HILBERT_LINE_1,
+ )
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ var/datum/transport_controller/linear/tram/broken_controller
+ var/selected_transport_id = tgui_input_list(usr, "Which tram?", "Off the rails", debug_tram_list)
+ var/reset_type = tgui_input_list(usr, "How hard of a reset?", "How bad is it screwed up", list("Clear Tram Contents", "Controller", "Controller and Contents", "Delete Datum", "Cancel"))
+
+ if(isnull(reset_type) || reset_type == "Cancel")
+ return
+
+ for(var/datum/transport_controller/linear/tram/transport as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
+ if(transport.specific_transport_id == selected_transport_id)
+ broken_controller = transport
+ break
+
+ if(isnull(broken_controller))
+ to_chat(usr, span_warning("Couldn't find a transport controller datum with ID [selected_transport_id]!"))
+ return
+
+ switch(reset_type)
+ if("Clear Tram Contents")
+ var/selection = tgui_alert(usr, "Include player mobs in the clearing?", "Contents reset [selected_transport_id]", list("Contents", "Contents and Players", "Cancel"))
+ switch(selection)
+ if("Contents")
+ broken_controller.reset_lift_contents(foreign_objects = TRUE, foreign_non_player_mobs = TRUE, consider_player_mobs = FALSE)
+ message_admins("[key_name_admin(usr)] performed a contents reset of tram ID [selected_transport_id].")
+ log_transport("TC: [selected_transport_id]: [key_name_admin(usr)] performed a contents reset.")
+ if("Contents and Players")
+ broken_controller.reset_lift_contents(foreign_objects = TRUE, foreign_non_player_mobs = TRUE, consider_player_mobs = TRUE)
+ message_admins("[key_name_admin(usr)] performed a contents and player mob reset of tram ID [selected_transport_id].")
+ log_transport("TC: [selected_transport_id]: [key_name_admin(usr)] performed a contents and player mob reset.")
+ else
+ return
+
+ if("Controller")
+ log_transport("TC: [selected_transport_id]: [key_name_admin(usr)] performed a controller reset, force operational.")
+ message_admins("[key_name_admin(usr)] performed a controller reset of tram ID [selected_transport_id].")
+ broken_controller.set_operational(TRUE)
+ broken_controller.reset_position()
+
+ if("Controller and Contents")
+ var/selection = tgui_alert(usr, "Include player mobs in the clearing?", "Contents reset [selected_transport_id]", list("Contents", "Contents and Players", "Cancel"))
+ switch(selection)
+ if("Contents")
+ message_admins("[key_name_admin(usr)] performed a contents and controller reset of tram ID [selected_transport_id].")
+ log_transport("TC: [selected_transport_id]: [key_name_admin(usr)] performed a contents reset. Controller reset, force operational.")
+ broken_controller.reset_lift_contents(foreign_objects = TRUE, foreign_non_player_mobs = TRUE, consider_player_mobs = FALSE)
+ if("Contents and Players")
+ message_admins("[key_name_admin(usr)] performed a contents/player/controller reset of tram ID [selected_transport_id].")
+ log_transport("TC: [selected_transport_id]: [key_name_admin(usr)] performed a contents and player mob reset. Controller reset, force operational.")
+ broken_controller.reset_lift_contents(foreign_objects = TRUE, foreign_non_player_mobs = TRUE, consider_player_mobs = TRUE)
+ else
+ return
+
+ broken_controller.set_operational(TRUE)
+ broken_controller.reset_position()
+
+ if("Delete Datum")
+ var/confirm = tgui_alert(usr, "Deleting [selected_transport_id] will make it unrecoverable this round. Are you sure?", "Delete tram ID [selected_transport_id]", list("Yes", "Cancel"))
+ if(confirm != "Yes")
+ return
+
+ var/obj/machinery/transport/tram_controller/tram_cabinet = broken_controller.paired_cabinet
+ if(!isnull(tram_cabinet))
+ tram_cabinet.controller_datum = null
+ tram_cabinet.update_appearance()
+
+ broken_controller.cycle_doors(CYCLE_OPEN, BYPASS_DOOR_CHECKS)
+ broken_controller.estop()
+ qdel(broken_controller)
+ message_admins("[key_name_admin(usr)] performed a datum delete of tram ID [selected_transport_id].")
diff --git a/code/modules/industrial_lift/elevator/elevator_controller.dm b/code/modules/transport/elevator/elev_controller.dm
similarity index 81%
rename from code/modules/industrial_lift/elevator/elevator_controller.dm
rename to code/modules/transport/elevator/elev_controller.dm
index 9d8a18ed798bc..aae79cfe0f14b 100644
--- a/code/modules/industrial_lift/elevator/elevator_controller.dm
+++ b/code/modules/transport/elevator/elev_controller.dm
@@ -4,7 +4,7 @@
base_icon_state = "tram"
icon_state = "tram"
can_alter_skin = FALSE
- light_color = LIGHT_COLOR_DARK_BLUE
+ light_color = COLOR_DISPLAY_BLUE
device_type = /obj/item/assembly/control/elevator
req_access = list()
id = 1
@@ -19,7 +19,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/button/elevator, 32)
/obj/item/assembly/control/elevator
name = "elevator controller"
desc = "A small device used to call elevators to the current floor."
- /// A weakref to the lift_master datum we control
+ /// A weakref to the transport_controller datum we control
var/datum/weakref/lift_weakref
COOLDOWN_DECLARE(elevator_cooldown)
@@ -29,16 +29,16 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/button/elevator, 32)
if(mapload)
return INITIALIZE_HINT_LATELOAD
- var/datum/lift_master/lift = get_lift()
+ var/datum/transport_controller/linear/lift = get_lift()
if(!lift)
return
lift_weakref = WEAKREF(lift)
/obj/item/assembly/control/elevator/LateInitialize()
- var/datum/lift_master/lift = get_lift()
+ var/datum/transport_controller/linear/lift = get_lift()
if(!lift)
- log_mapping("Elevator call button at [AREACOORD(src)] found no associated lift to link with, this may be a mapping error.")
+ log_mapping("Elevator call button at [AREACOORD(src)] found no associated elevator to link with, this may be a mapping error.")
return
lift_weakref = WEAKREF(lift)
@@ -49,17 +49,17 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/button/elevator, 32)
return FALSE
obj_flags |= EMAGGED
- var/datum/lift_master/lift = lift_weakref?.resolve()
+ var/datum/transport_controller/linear/lift = lift_weakref?.resolve()
if(!lift)
return FALSE
- for(var/obj/structure/industrial_lift/lift_platform as anything in lift.lift_platforms)
+ for(var/obj/structure/transport/linear/lift_platform as anything in lift.transport_modules)
lift_platform.violent_landing = TRUE
lift_platform.warns_on_down_movement = FALSE
lift_platform.elevator_vertical_speed = initial(lift_platform.elevator_vertical_speed) * 0.5
for(var/obj/machinery/door/elevator_door as anything in GLOB.elevator_doors)
- if(elevator_door.elevator_linked_id != lift.lift_id)
+ if(elevator_door.transport_linked_id != lift.specific_transport_id)
continue
if(elevator_door.obj_flags & EMAGGED)
continue
@@ -78,17 +78,17 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/button/elevator, 32)
if(!(obj_flags & EMAGGED))
return ..()
- var/datum/lift_master/lift = lift_weakref?.resolve()
+ var/datum/transport_controller/linear/lift = lift_weakref?.resolve()
if(isnull(lift))
return ..()
- for(var/obj/structure/industrial_lift/lift_platform as anything in lift.lift_platforms)
+ for(var/obj/structure/transport/linear/lift_platform as anything in lift.transport_modules)
lift_platform.violent_landing = initial(lift_platform.violent_landing)
lift_platform.warns_on_down_movement = initial(lift_platform.warns_on_down_movement)
lift_platform.elevator_vertical_speed = initial(lift_platform.elevator_vertical_speed)
for(var/obj/machinery/door/elevator_door as anything in GLOB.elevator_doors)
- if(elevator_door.elevator_linked_id != lift.lift_id)
+ if(elevator_door.transport_linked_id != lift.specific_transport_id)
continue
if(!(elevator_door.obj_flags & EMAGGED))
continue
@@ -116,7 +116,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/button/elevator, 32)
/// Returns TRUE if the move setup was a success, EVEN IF the move itself fails afterwards
/obj/item/assembly/control/elevator/proc/call_elevator(mob/activator)
// We can't call an elevator that doesn't exist
- var/datum/lift_master/lift = lift_weakref?.resolve()
+ var/datum/transport_controller/linear/lift = lift_weakref?.resolve()
if(!lift)
loc.balloon_alert(activator, "no elevator connected!")
return FALSE
@@ -127,9 +127,9 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/button/elevator, 32)
return FALSE
// If the elevator is already here, open the doors.
- var/obj/structure/industrial_lift/prime_lift = lift.return_closest_platform_to_z(loc.z)
+ var/obj/structure/transport/linear/prime_lift = lift.return_closest_platform_to_z(loc.z)
if(prime_lift.z == loc.z)
- INVOKE_ASYNC(lift, TYPE_PROC_REF(/datum/lift_master, open_lift_doors_callback))
+ INVOKE_ASYNC(lift, TYPE_PROC_REF(/datum/transport_controller/linear, open_lift_doors_callback))
loc.balloon_alert(activator, "elevator is here!")
return TRUE
@@ -169,10 +169,10 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/button/elevator, 32)
return FALSE
return TRUE
-/// Gets the lift associated with our assembly / button
+/// Gets the elevator associated with our assembly / button
/obj/item/assembly/control/elevator/proc/get_lift()
- for(var/datum/lift_master/possible_match as anything in GLOB.active_lifts_by_type[BASIC_LIFT_ID])
- if(possible_match.specific_lift_id != id)
+ for(var/datum/transport_controller/linear/possible_match as anything in SStransport.transports_by_type[TRANSPORT_TYPE_ELEVATOR])
+ if(possible_match.specific_transport_id != id)
continue
return possible_match
diff --git a/code/modules/industrial_lift/elevator/elevator_doors.dm b/code/modules/transport/elevator/elev_doors.dm
similarity index 100%
rename from code/modules/industrial_lift/elevator/elevator_doors.dm
rename to code/modules/transport/elevator/elev_doors.dm
diff --git a/code/modules/industrial_lift/elevator/elevator_indicator.dm b/code/modules/transport/elevator/elev_indicator.dm
similarity index 78%
rename from code/modules/industrial_lift/elevator/elevator_indicator.dm
rename to code/modules/transport/elevator/elev_indicator.dm
index 77518a4bdc843..cf9fa46e96328 100644
--- a/code/modules/industrial_lift/elevator/elevator_indicator.dm
+++ b/code/modules/transport/elevator/elev_indicator.dm
@@ -1,5 +1,5 @@
/**
- * A lift indicator aka an elevator hall lantern w/ floor number
+ * An indicator display aka an elevator hall lantern w/ floor number
*/
/obj/machinery/lift_indicator
name = "elevator indicator"
@@ -16,7 +16,7 @@
light_range = 1
light_power = 1
- light_color = LIGHT_COLOR_DARK_BLUE
+ light_color = COLOR_DISPLAY_BLUE
luminosity = 1
maptext_x = 18
@@ -24,17 +24,17 @@
maptext_width = 8
maptext_height = 16
- /// What specific_lift_id do we link with?
+ /// What specific_transport_id do we link with?
var/linked_elevator_id
/// 'Floors' for display purposes are by default offset by 1 from their actual z-levels
var/lowest_floor_offset = 1
- /// Weakref to the lift.
+ /// Weakref to the transport.
var/datum/weakref/lift_ref
- /// The lowest floor number. Determined by lift init.
+ /// The lowest floor number. Determined by transport module init.
var/lowest_floor_num = 1
/// Positive for going up, negative going down, 0 for stopped
var/current_lift_direction = 0
- /// The lift's current floor relative to its lowest floor being 1
+ /// The elevator's current floor relative to its lowest floor being 1
var/current_lift_floor = 1
/obj/machinery/lift_indicator/Initialize(mapload)
@@ -44,8 +44,8 @@
/obj/machinery/lift_indicator/LateInitialize()
. = ..()
- for(var/datum/lift_master/possible_match as anything in GLOB.active_lifts_by_type[BASIC_LIFT_ID])
- if(possible_match.specific_lift_id != linked_elevator_id)
+ for(var/datum/transport_controller/linear/possible_match as anything in SStransport.transports_by_type[TRANSPORT_TYPE_ELEVATOR])
+ if(possible_match.specific_transport_id != linked_elevator_id)
continue
lift_ref = WEAKREF(possible_match)
@@ -70,12 +70,12 @@
. += span_notice("The elevator is at floor [current_lift_floor], [dirtext].")
/**
- * Update state, and only process if lift is moving.
+ * Update state, and only process if elevator is moving.
*/
/obj/machinery/lift_indicator/proc/on_lift_direction(datum/source, direction)
SIGNAL_HANDLER
- var/datum/lift_master/lift = lift_ref?.resolve()
+ var/datum/transport_controller/linear/lift = lift_ref?.resolve()
if(!lift)
return
@@ -102,26 +102,26 @@
return FALSE
/obj/machinery/lift_indicator/process()
- var/datum/lift_master/lift = lift_ref?.resolve()
+ var/datum/transport_controller/linear/lift = lift_ref?.resolve()
// Check for stopped states.
if(!lift || !is_operational)
- // Lift missing, or we lost power.
+ // elevator missing, or we lost power.
set_lift_state(0, 0, force = !is_operational)
return PROCESS_KILL
use_power(active_power_usage)
- var/obj/structure/industrial_lift/lift_part = lift.lift_platforms[1]
+ var/obj/structure/transport/linear/lift_part = lift.transport_modules[1]
if(QDELETED(lift_part))
set_lift_state(0, 0, force = !is_operational)
return PROCESS_KILL
// Update
- set_lift_state(current_lift_direction, lift.lift_platforms[1].z - lowest_floor_offset)
+ set_lift_state(current_lift_direction, lift.transport_modules[1].z - lowest_floor_offset)
- // Lift's not moving, we're done; we just had to update the floor number one last time.
+ // elevator's not moving, we're done; we just had to update the floor number one last time.
if(!current_lift_direction)
return PROCESS_KILL
@@ -150,7 +150,7 @@
return
set_light(l_on = TRUE)
- maptext = "
[current_lift_floor]
"
+ maptext = "
[current_lift_floor]
"
/obj/machinery/lift_indicator/update_overlays()
. = ..()
diff --git a/code/modules/industrial_lift/elevator/elevator_music_zone.dm b/code/modules/transport/elevator/elev_music_zone.dm
similarity index 98%
rename from code/modules/industrial_lift/elevator/elevator_music_zone.dm
rename to code/modules/transport/elevator/elev_music_zone.dm
index 00d55751a5c6f..1f09a00a68be6 100644
--- a/code/modules/industrial_lift/elevator/elevator_music_zone.dm
+++ b/code/modules/transport/elevator/elev_music_zone.dm
@@ -7,7 +7,7 @@ GLOBAL_LIST_EMPTY(elevator_music)
invisibility = INVISIBILITY_MAXIMUM // Setting this to ABSTRACT means it isn't moved by the lift
icon = 'icons/obj/art/musician.dmi'
icon_state = "piano"
- /// What specific_lift_id do we link with?
+ /// What specific_transport_id do we link with?
var/linked_elevator_id = ""
/// Radius around this map helper in which to play the sound
var/range = 1
diff --git a/code/modules/industrial_lift/elevator/elevator_panel.dm b/code/modules/transport/elevator/elev_panel.dm
similarity index 83%
rename from code/modules/industrial_lift/elevator/elevator_panel.dm
rename to code/modules/transport/elevator/elev_panel.dm
index 8791c885a50ba..3e9e0e073c19f 100644
--- a/code/modules/industrial_lift/elevator/elevator_panel.dm
+++ b/code/modules/transport/elevator/elev_panel.dm
@@ -5,9 +5,9 @@
* allowing users to enter a UI to move it up or down
*
* These can be placed in two methods:
- * - You can place the control panel on the same turf as a lift. It will move up and down with the lift
- * - You can place the control panel to the side of a lift, NOT attached to the lift. It will remain in position
- * I don't recommend using both methods on the same elevator, as it might result in some jank, but it's functional.
+ * - You can place the control panel on the same turf as an elevator. It will move up and down with the elevator
+ * - You can place the control panel to the side of an elevator, NOT attached to the elevator. It will remain in position
+ * - I don't recommend using both methods on the same elevator, as it might result in some jank, but it's functional.
*/
/obj/machinery/elevator_control_panel
name = "elevator panel"
@@ -26,9 +26,9 @@
/// Were we instantiated at mapload? Used to determine when we should link / throw errors
var/maploaded = FALSE
- /// A weakref to the lift_master datum we control
+ /// A weakref to the transport_controller datum we control
var/datum/weakref/lift_weakref
- /// What specific_lift_id do we link with?
+ /// What specific_transport_id do we link with?
var/linked_elevator_id
/// A list of all possible destinations this elevator can travel.
@@ -82,9 +82,9 @@
// and also so we can throw mapping errors to let people know if they messed up setup.
link_with_lift(log_error = TRUE)
-/// Link with associated lift objects, only log failure to find a lift in LateInit because those are mapped in
+/// Link with associated transport controllers, only log failure to find a lift in LateInit because those are mapped in
/obj/machinery/elevator_control_panel/proc/link_with_lift(log_error = FALSE)
- var/datum/lift_master/lift = get_associated_lift()
+ var/datum/transport_controller/linear/lift = get_associated_lift()
if(!lift)
if (log_error)
log_mapping("Elevator control panel at [AREACOORD(src)] found no associated lift to link with, this may be a mapping error.")
@@ -102,17 +102,17 @@
obj_flags |= EMAGGED
- var/datum/lift_master/lift = lift_weakref?.resolve()
+ var/datum/transport_controller/linear/lift = lift_weakref?.resolve()
if(!lift)
return FALSE
- for(var/obj/structure/industrial_lift/lift_platform as anything in lift.lift_platforms)
+ for(var/obj/structure/transport/linear/lift_platform as anything in lift.transport_modules)
lift_platform.violent_landing = TRUE
lift_platform.warns_on_down_movement = FALSE
lift_platform.elevator_vertical_speed = initial(lift_platform.elevator_vertical_speed) * 0.5
for(var/obj/machinery/door/elevator_door as anything in GLOB.elevator_doors)
- if(elevator_door.elevator_linked_id != linked_elevator_id)
+ if(elevator_door.transport_linked_id != linked_elevator_id)
continue
if(elevator_door.obj_flags & EMAGGED)
continue
@@ -125,7 +125,7 @@
return TRUE
/obj/machinery/elevator_control_panel/multitool_act(mob/living/user)
- var/datum/lift_master/lift = lift_weakref?.resolve()
+ var/datum/transport_controller/linear/lift = lift_weakref?.resolve()
if(!lift)
return
@@ -135,18 +135,18 @@
balloon_alert(user, "interrupted!")
return TRUE
- if(QDELETED(lift) || !length(lift.lift_platforms))
+ if(QDELETED(lift) || !length(lift.transport_modules))
return
// If we were emagged, reset us
if(obj_flags & EMAGGED)
- for(var/obj/structure/industrial_lift/lift_platform as anything in lift.lift_platforms)
+ for(var/obj/structure/transport/linear/lift_platform as anything in lift.transport_modules)
lift_platform.violent_landing = initial(lift_platform.violent_landing)
lift_platform.warns_on_down_movement = initial(lift_platform.warns_on_down_movement)
lift_platform.elevator_vertical_speed = initial(lift_platform.elevator_vertical_speed)
for(var/obj/machinery/door/elevator_door as anything in GLOB.elevator_doors)
- if(elevator_door.elevator_linked_id != linked_elevator_id)
+ if(elevator_door.transport_linked_id != linked_elevator_id)
continue
if(!(elevator_door.obj_flags & EMAGGED))
continue
@@ -168,8 +168,8 @@
/// Find the elevator associated with our lift button.
/obj/machinery/elevator_control_panel/proc/get_associated_lift()
- for(var/datum/lift_master/possible_match as anything in GLOB.active_lifts_by_type[BASIC_LIFT_ID])
- if(possible_match.specific_lift_id != linked_elevator_id)
+ for(var/datum/transport_controller/linear/possible_match as anything in SStransport.transports_by_type[TRANSPORT_TYPE_ELEVATOR])
+ if(possible_match.specific_transport_id != linked_elevator_id)
continue
return possible_match
@@ -177,13 +177,13 @@
return null
/// Goes through and populates the linked_elevator_destination list with all possible destinations the lift can go.
-/obj/machinery/elevator_control_panel/proc/populate_destinations_list(datum/lift_master/linked_lift)
+/obj/machinery/elevator_control_panel/proc/populate_destinations_list(datum/transport_controller/linear/linked_lift)
// This list will track all the raw z-levels which we found that we can travel to
var/list/raw_destinations = list()
// Get a list of all the starting locs our elevator starts at
var/list/starting_locs = list()
- for(var/obj/structure/industrial_lift/lift_piece as anything in linked_lift.lift_platforms)
+ for(var/obj/structure/transport/linear/lift_piece as anything in linked_lift.transport_modules)
starting_locs |= lift_piece.locs
// The raw destination list will start with all the z's we start at
raw_destinations |= lift_piece.z
@@ -242,7 +242,7 @@
// Add the Zs of all the found turfs as possible destinations
for(var/turf/found as anything in checked_turfs)
- // We check all turfs we found in case of multi-z lift memes.
+ // We check all turfs we found in case of multi-z memes.
destinations |= found.z
// And recursively call the proc with all the turfs we found on the next level
@@ -255,7 +255,7 @@
ui.open()
/obj/machinery/elevator_control_panel/ui_status(mob/user)
- // We moved up a z-level, probably via the lift itself, so don't preserve the UI.
+ // We moved up a z-level, probably via the elevator itself, so don't preserve the UI.
if(user.z != z)
return UI_CLOSE
@@ -277,12 +277,12 @@
data["is_emergency"] = SSsecurity_level.get_current_level_as_number() >= SEC_LEVEL_RED
data["doors_open"] = !!door_reset_timerid
- var/datum/lift_master/lift = lift_weakref?.resolve()
+ var/datum/transport_controller/linear/lift = lift_weakref?.resolve()
if(lift)
data["lift_exists"] = TRUE
data["currently_moving"] = lift.controls_locked == LIFT_PLATFORM_LOCKED
data["currently_moving_to_floor"] = last_move_target
- data["current_floor"] = lift.lift_platforms[1].z
+ data["current_floor"] = lift.transport_modules[1].z
else
data["lift_exists"] = FALSE
@@ -322,16 +322,16 @@
if(!(num2text(desired_z) in linked_elevator_destination))
return TRUE // Something is inaccurate, update UI
- var/datum/lift_master/lift = lift_weakref?.resolve()
+ var/datum/transport_controller/linear/lift = lift_weakref?.resolve()
if(!lift || lift.controls_locked == LIFT_PLATFORM_LOCKED)
return TRUE // We shouldn't be moving anything, update UI
- INVOKE_ASYNC(lift, TYPE_PROC_REF(/datum/lift_master, move_to_zlevel), desired_z, CALLBACK(src, PROC_REF(check_panel)), usr)
+ INVOKE_ASYNC(lift, TYPE_PROC_REF(/datum/transport_controller/linear, move_to_zlevel), desired_z, CALLBACK(src, PROC_REF(check_panel)), usr)
last_move_target = desired_z
return TRUE // Succcessfully initiated a move. Regardless of whether it actually works, update the UI
if("emergency_door")
- var/datum/lift_master/lift = lift_weakref?.resolve()
+ var/datum/transport_controller/linear/lift = lift_weakref?.resolve()
if(!lift)
return TRUE // Something is wrong, update UI
@@ -340,8 +340,8 @@
if(SSsecurity_level.get_current_level_as_number() < SEC_LEVEL_RED)
return TRUE // The security level might have been lowered since last update, so update UI
- // Open all lift doors, it's an emergency dang it!
- lift.update_lift_doors(action = OPEN_DOORS)
+ // Open all elevator doors, it's an emergency dang it!
+ lift.update_lift_doors(action = CYCLE_OPEN)
door_reset_timerid = addtimer(CALLBACK(src, PROC_REF(reset_doors)), 3 MINUTES, TIMER_UNIQUE|TIMER_STOPPABLE)
return TRUE // We opened up all the doors, update the UI so the emergency button is replaced correctly
@@ -353,7 +353,7 @@
reset_doors()
return TRUE // We closed all the doors, update the UI so the door button is replaced correctly
-/// Callback for move_to_zlevel to ensure the lift can continue to move.
+/// Callback for move_to_zlevel to ensure the elevator can continue to move.
/obj/machinery/elevator_control_panel/proc/check_panel()
if(QDELETED(src))
return FALSE
@@ -363,9 +363,9 @@
return TRUE
/// Helper proc to go through all of our desetinations and reset all elevator doors,
-/// closing doors on z-levels the lift is away from, and opening doors on the z the lift is
+/// closing doors on z-levels the elevator is away from, and opening doors on the z the elevator is
/obj/machinery/elevator_control_panel/proc/reset_doors()
- var/datum/lift_master/lift = lift_weakref?.resolve()
+ var/datum/transport_controller/linear/lift = lift_weakref?.resolve()
if(!lift)
return
@@ -381,8 +381,8 @@
// Open all the doors on the zs we should be open on,
// and close all doors we aren't on. Simple enough.
- lift.update_lift_doors(zs_we_are_present_on, action = OPEN_DOORS)
- lift.update_lift_doors(zs_we_are_absent, action = CLOSE_DOORS)
+ lift.update_lift_doors(zs_we_are_present_on, action = CYCLE_OPEN)
+ lift.update_lift_doors(zs_we_are_absent, action = CYCLE_CLOSED)
door_reset_timerid = null
diff --git a/code/modules/transport/linear_controller.dm b/code/modules/transport/linear_controller.dm
new file mode 100644
index 0000000000000..dd90562deb643
--- /dev/null
+++ b/code/modules/transport/linear_controller.dm
@@ -0,0 +1,635 @@
+///coordinate and control movement across linked transport_controllers. allows moving large single multitile platforms and many 1 tile platforms.
+///also is capable of linking platforms across linked z levels
+/datum/transport_controller/linear
+ ///the lift platforms we consider as part of this transport. ordered in order of lowest z level to highest z level after init.
+ ///(the sorting algorithm sucks btw)
+ var/list/obj/structure/transport/linear/transport_modules
+
+ /// Typepath list of what to ignore smashing through, controls all lifts
+ var/static/list/ignored_smashthroughs = list(
+ /obj/machinery/power/supermatter_crystal,
+ /obj/structure/holosign,
+ /obj/machinery/field,
+ )
+
+ ///whether the lift handled by this transport_controller datum is multitile as opposed to nxm platforms per z level
+ var/modular_set = FALSE
+
+ ///taken from our lift platforms. if true we go through each z level of platforms and attempt to make the lowest left corner platform
+ ///into one giant multitile object the size of all other platforms on that z level.
+ var/create_modular_set = FALSE
+
+ ///lift platforms have already been sorted in order of z level.
+ var/z_sorted = FALSE
+
+ ///transport_id taken from our base lift platform, used to put us into SStransport.transports_by_type
+ var/transport_id = TRANSPORT_TYPE_ELEVATOR
+
+ ///overridable ID string to link control units to this specific transport_controller datum. created by placing a transport id landmark object
+ ///somewhere on the tram, if its anywhere on the tram we'll find it in init and set this to whatever it specifies
+ var/specific_transport_id
+
+ ///bitfield of various transport states
+ var/controller_status = NONE
+
+ ///if true, the platform cannot be manually moved.
+ var/controls_locked = FALSE
+
+/datum/transport_controller/linear/New(obj/structure/transport/linear/transport_module)
+ transport_id = transport_module.transport_id
+ create_modular_set = transport_module.create_modular_set
+
+ link_transport_modules(transport_module)
+ ignored_smashthroughs = typecacheof(ignored_smashthroughs)
+
+ LAZYADDASSOCLIST(SStransport.transports_by_type, transport_id, src)
+
+ for(var/obj/structure/transport/linear/lift as anything in transport_modules)
+ lift.add_initial_contents()
+
+/datum/transport_controller/linear/Destroy()
+ for(var/obj/structure/transport/linear/transport_module as anything in transport_modules)
+ transport_module.transport_controller_datum = null
+ transport_modules = null
+
+ LAZYREMOVEASSOC(SStransport.transports_by_type, transport_id, src)
+ if(isnull(SStransport.transports_by_type))
+ SStransport.transports_by_type = list()
+
+ return ..()
+
+/datum/transport_controller/linear/proc/add_transport_modules(obj/structure/transport/linear/new_transport_module)
+ if(new_transport_module in transport_modules)
+ return
+ for(var/obj/structure/transport/linear/other_module in new_transport_module.loc)
+ if(other_module != new_transport_module)
+ stack_trace("there is more than one transport module on a tile when a controller adds it. this causes problems")
+ qdel(other_module)
+
+ new_transport_module.transport_controller_datum = src
+ LAZYADD(transport_modules, new_transport_module)
+ RegisterSignal(new_transport_module, COMSIG_QDELETING, PROC_REF(remove_transport_modules))
+
+ check_for_landmarks(new_transport_module)
+
+ if(z_sorted)//make sure we dont lose z ordering if we get additional platforms after init
+ order_platforms_by_z_level()
+
+/datum/transport_controller/linear/proc/remove_transport_modules(obj/structure/transport/linear/old_transport_module)
+ SIGNAL_HANDLER
+
+ if(!(old_transport_module in transport_modules))
+ return
+
+ old_transport_module.transport_controller_datum = null
+ LAZYREMOVE(transport_modules, old_transport_module)
+ UnregisterSignal(old_transport_module, COMSIG_QDELETING)
+ if(!length(transport_modules))
+ qdel(src)
+
+///Collect all bordered platforms via a simple floodfill algorithm. allows multiz trams because its funny
+/datum/transport_controller/linear/proc/link_transport_modules(obj/structure/transport/linear/base_transport_module)
+ add_transport_modules(base_transport_module)
+ var/list/possible_expansions = list(base_transport_module)
+
+ while(possible_expansions.len)
+ for(var/obj/structure/transport/linear/borderline as anything in possible_expansions)
+ var/list/result = borderline.module_adjacency(src)
+ if(length(result))
+ for(var/obj/structure/transport/linear/transport_module as anything in result)
+ if(transport_modules.Find(transport_module))
+ continue
+
+ add_transport_modules(transport_module)
+ possible_expansions |= transport_module
+
+ possible_expansions -= borderline
+
+///check for any landmarks placed inside the locs of the given transport_module
+/datum/transport_controller/linear/proc/check_for_landmarks(obj/structure/transport/linear/new_transport_module)
+ SHOULD_CALL_PARENT(TRUE)
+
+ for(var/turf/platform_loc as anything in new_transport_module.locs)
+ var/obj/effect/landmark/transport/transport_id/id_giver = locate() in platform_loc
+
+ if(id_giver)
+ set_info_from_id_landmark(id_giver)
+
+///set vars and such given an overriding transport_id landmark
+/datum/transport_controller/linear/proc/set_info_from_id_landmark(obj/effect/landmark/transport/transport_id/landmark)
+ SHOULD_CALL_PARENT(TRUE)
+
+ if(!istype(landmark, /obj/effect/landmark/transport/transport_id))//transport_controller subtypes can want differnet id's than the base type wants
+ return
+
+ if(landmark.specific_transport_id)
+ specific_transport_id = landmark.specific_transport_id
+
+ qdel(landmark)
+
+///orders the lift platforms in order of lowest z level to highest z level.
+/datum/transport_controller/linear/proc/order_platforms_by_z_level()
+ //contains nested lists for every z level in the world. why? because its really easy to sort
+ var/list/platforms_by_z = list()
+ platforms_by_z.len = world.maxz
+
+ for(var/z in 1 to world.maxz)
+ platforms_by_z[z] = list()
+
+ for(var/obj/structure/transport/linear/transport_module as anything in transport_modules)
+ if(QDELETED(transport_module) || !transport_module.z)
+ transport_modules -= transport_module
+ continue
+
+ platforms_by_z[transport_module.z] += transport_module
+
+ if(create_modular_set)
+ for(var/list/z_list as anything in platforms_by_z)
+ if(!length(z_list))
+ continue
+
+ create_modular_set_for_z_level(z_list)//this will subtract all but one platform from the list
+
+ var/list/output = list()
+
+ for(var/list/z_list as anything in platforms_by_z)
+ output += z_list
+
+ transport_modules = output
+
+ z_sorted = TRUE
+
+///goes through all platforms in the given list and finds the one in the lower left corner
+/datum/transport_controller/linear/proc/create_modular_set_for_z_level(list/obj/structure/transport/linear/platforms_in_z)
+ var/min_x = INFINITY
+ var/max_x = 0
+
+ var/min_y = INFINITY
+ var/max_y = 0
+
+ var/z = 0
+
+ for(var/obj/structure/transport/linear/module_to_sort as anything in platforms_in_z)
+ if(!z)
+ if(!module_to_sort.z)
+ stack_trace("create_modular_set_for_z_level() was given a platform in nullspace or not on a turf!")
+ platforms_in_z -= module_to_sort
+ continue
+
+ z = module_to_sort.z
+
+ if(z != module_to_sort.z)
+ stack_trace("create_modular_set_for_z_level() was given lifts on different z levels!")
+ platforms_in_z -= module_to_sort
+ continue
+
+ min_x = min(min_x, module_to_sort.x)
+ max_x = max(max_x, module_to_sort.x)
+
+ min_y = min(min_y, module_to_sort.y)
+ max_y = max(max_y, module_to_sort.y)
+
+ var/turf/lower_left_corner_loc = locate(min_x, min_y, z)
+ if(!lower_left_corner_loc)
+ CRASH("was unable to find a turf at the lower left corner of this z")
+
+ var/obj/structure/transport/linear/lower_left_corner_transport = locate() in lower_left_corner_loc
+
+ if(!lower_left_corner_transport)
+ CRASH("there was no transport in the lower left corner of the given transport")
+
+ platforms_in_z.Cut()
+ platforms_in_z += lower_left_corner_transport//we want to change the list given to us not create a new one. so we do this
+
+ lower_left_corner_transport.create_modular_set(min_x, min_y, max_x, max_y, z)
+
+///returns the closest transport to the specified atom, prioritizing transports on the same z level. used for comparing distance
+/datum/transport_controller/linear/proc/return_closest_platform_to(atom/comparison, allow_multiple_answers = FALSE)
+ if(!istype(comparison) || !comparison.z)
+ return FALSE
+
+ var/list/obj/structure/transport/linear/candidate_platforms = list()
+
+ for(var/obj/structure/transport/linear/platform as anything in transport_modules)
+ if(platform.z == comparison.z)
+ candidate_platforms += platform
+
+ var/obj/structure/transport/linear/winner = candidate_platforms[1]
+ var/winner_distance = get_dist(comparison, winner)
+
+ var/list/tied_winners = list(winner)
+
+ for(var/obj/structure/transport/linear/platform_to_sort as anything in candidate_platforms)
+ var/platform_distance = get_dist(comparison, platform_to_sort)
+
+ if(platform_distance < winner_distance)
+ winner = platform_to_sort
+ winner_distance = platform_distance
+
+ if(allow_multiple_answers)
+ tied_winners = list(winner)
+
+ else if(platform_distance == winner_distance && allow_multiple_answers)
+ tied_winners += platform_to_sort
+
+ if(allow_multiple_answers)
+ return tied_winners
+
+ return winner
+
+/// Returns a platform on the z-level which is vertically closest to the passed target_z
+/datum/transport_controller/linear/proc/return_closest_platform_to_z(target_z)
+ var/obj/structure/transport/linear/found_platform
+ for(var/obj/structure/transport/linear/lift as anything in transport_modules)
+ // Already at the same Z-level, we can stop
+ if(lift.z == target_z)
+ found_platform = lift
+ break
+
+ // Set up an initial lift to compare to
+ if(!found_platform)
+ found_platform = lift
+ continue
+
+ // Same level, we can go with the one we currently have
+ if(lift.z == found_platform.z)
+ continue
+
+ // If the difference between the current found platform and the target
+ // if less than the distance between the next lift and the target,
+ // our current platform is closer to the target than the next one, so we can skip it
+ if(abs(found_platform.z - target_z) < abs(lift.z - target_z))
+ continue
+
+ // The difference is smaller for this lift, so it's closer
+ found_platform = lift
+
+ return found_platform
+
+/// Returns a list of all the z-levels our transport is currently on.
+/datum/transport_controller/linear/proc/get_zs_we_are_on()
+ var/list/zs_we_are_present_on = list()
+ for(var/obj/structure/transport/linear/lift as anything in transport_modules)
+ zs_we_are_present_on |= lift.z
+ return zs_we_are_present_on
+
+///returns all transport modules associated with this transport on the given z level or given atoms z level
+/datum/transport_controller/linear/proc/get_platforms_on_level(atom/atom_reference_OR_z_level_number)
+ var/z = atom_reference_OR_z_level_number
+ if(isatom(atom_reference_OR_z_level_number))
+ z = atom_reference_OR_z_level_number.z
+
+ if(!isnum(z) || z < 0 || z > world.maxz)
+ return null
+
+ var/list/platforms_in_z = list()
+
+ for(var/obj/structure/transport/linear/lift_to_check as anything in transport_modules)
+ if(lift_to_check.z)
+ platforms_in_z += lift_to_check
+
+ return platforms_in_z
+
+/**
+ * Moves the platform UP or DOWN, this is what users invoke with their hand.
+ * This is a SAFE proc, ensuring every part of it moves SANELY.
+ *
+ * Arguments:
+ * going - UP or DOWN directions, where the platform should go. Keep in mind by this point checks of whether it should go up or down have already been done.
+ * user - Whomever made the movement.
+ */
+/datum/transport_controller/linear/proc/move_lift_vertically(going, mob/user)
+ //transport_modules are sorted in order of lowest z to highest z, so going upwards we need to move them in reverse order to not collide
+ if(going == UP)
+ var/obj/structure/transport/linear/platform_to_move
+ var/current_index = length(transport_modules)
+
+ while(current_index > 0)
+ platform_to_move = transport_modules[current_index]
+ current_index--
+
+ platform_to_move.travel(going)
+
+ else if(going == DOWN)
+ for(var/obj/structure/transport/linear/transport_module as anything in transport_modules)
+ transport_module.travel(going)
+
+/**
+ * Moves the platform after a passed delay.
+ *
+ * This is a more "user friendly" or "realistic" move.
+ * It includes things like:
+ * - Allowing platform "travel time"
+ * - Shutting elevator safety doors
+ * - Sound effects while moving
+ * - Safety warnings for anyone below the platform (while it's moving downwards)
+ *
+ * Arguments:
+ * duration - required, how long do we wait to move the platform?
+ * door_duration - optional, how long should we wait to open the doors after arriving? If null, we won't open or close doors
+ * direction - which direction are we moving the lift?
+ * user - optional, who is moving the lift?
+ */
+/datum/transport_controller/linear/proc/move_after_delay(lift_move_duration, door_duration, direction, mob/user)
+ if(!isnum(lift_move_duration))
+ CRASH("[type] move_after_delay called with invalid duration ([lift_move_duration]).")
+ if(lift_move_duration <= 0 SECONDS)
+ move_lift_vertically(direction, user)
+ return
+
+ // Get the lowest or highest platform according to which direction we're moving
+ var/obj/structure/transport/linear/prime_lift = return_closest_platform_to_z(direction == UP ? world.maxz : 0)
+
+ // If anyone changes the hydraulic sound effect I sure hope they update this variable...
+ var/hydraulic_sfx_duration = 2 SECONDS
+ // ...because we use the duration of the sound effect to make it last for roughly the duration of the lift travel
+ playsound(prime_lift, 'sound/mecha/hydraulic.ogg', 25, vary = TRUE, frequency = clamp(hydraulic_sfx_duration / lift_move_duration, 0.33, 3))
+
+ // Move the platform after a timer
+ addtimer(CALLBACK(src, PROC_REF(move_lift_vertically), direction, user), lift_move_duration, TIMER_UNIQUE)
+ // Open doors after the set duration if supplied
+ if(isnum(door_duration))
+ addtimer(CALLBACK(src, PROC_REF(open_lift_doors_callback)), door_duration, TIMER_UNIQUE)
+
+ // Here on we only care about platforms going DOWN
+ if(direction != DOWN)
+ return
+
+ // Okay we're going down, let's try to display some warnings to people below
+ var/list/turf/lift_locs = list()
+ for(var/obj/structure/transport/linear/going_to_move as anything in transport_modules)
+ // This platform has no warnings so we don't even need to worry about it
+ if(!going_to_move.warns_on_down_movement)
+ continue
+ // Collect all the turfs our platform is found at
+ lift_locs |= going_to_move.locs
+
+ for(var/turf/moving in lift_locs)
+ // Find what's below the turf that's moving
+ var/turf/below_us = get_step_multiz(moving, DOWN)
+ // Hold up the turf below us is also in our locs list. Multi-z? Don't show a warning
+ if(below_us in lift_locs)
+ continue
+ // Display the warning for until we land
+ new /obj/effect/temp_visual/telegraphing/lift_travel(below_us, lift_move_duration)
+
+/**
+ * Simple wrapper for checking if we can move 1 zlevel, and if we can, do said move.
+ * Locks controls, closes all doors, then moves the platform and re-opens the doors afterwards.
+ *
+ * Arguments:
+ * direction - which direction are we moving?
+ * lift_move_duration - how long does the move take? can be 0 or null for instant move.
+ * door_duration - how long does it take for the doors to open after a move?
+ * user - optional, who moved it?
+ */
+/datum/transport_controller/linear/proc/simple_move_wrapper(direction, lift_move_duration, mob/user)
+ if(!Check_lift_move(direction))
+ return FALSE
+
+ // Lock controls, to prevent moving-while-moving memes
+ controls_lock(TRUE)
+ // Send out a signal that we're going
+ SEND_SIGNAL(src, COMSIG_LIFT_SET_DIRECTION, direction)
+ // Close all lift doors
+ update_lift_doors(action = CYCLE_CLOSED)
+
+ if(isnull(lift_move_duration) || lift_move_duration <= 0 SECONDS)
+ // Do an instant move
+ move_lift_vertically(direction, user)
+ // Open doors on the zs we arrive at
+ update_lift_doors(get_zs_we_are_on(), action = CYCLE_OPEN)
+ // And unlock the controls after
+ controls_lock(FALSE)
+ return TRUE
+
+ // Do a delayed move
+ move_after_delay(
+ lift_move_duration = lift_move_duration,
+ door_duration = lift_move_duration * 1.5,
+ direction = direction,
+ user = user,
+ )
+
+ addtimer(CALLBACK(src, PROC_REF(finish_simple_move_wrapper)), lift_move_duration * 1.5)
+ return TRUE
+
+/**
+ * Wrap everything up from simple_move_wrapper finishing its movement
+ */
+/datum/transport_controller/linear/proc/finish_simple_move_wrapper()
+ SEND_SIGNAL(src, COMSIG_LIFT_SET_DIRECTION, 0)
+ controls_lock(FALSE)
+
+/**
+ * Moves the platform to the passed z-level.
+ *
+ * Checks for validity of the move: Are we moving to the same z-level, can we actually move to that z-level?
+ * Does NOT check if the controls are currently locked.
+ *
+ * Moves to the passed z-level by calling move_after_delay repeatedly until the passed z-level is reached.
+ * This proc sleeps as it moves.
+ *
+ * Arguments:
+ * target_z - required, the Z we want to move to
+ * loop_callback - optional, an additional callback invoked during the loop that allows the move to cancel.
+ * user - optional, who started the move
+ */
+/datum/transport_controller/linear/proc/move_to_zlevel(target_z, datum/callback/loop_callback, mob/user)
+ if(!isnum(target_z) || target_z <= 0)
+ CRASH("[type] move_to_zlevel was passed an invalid target_z ([target_z]).")
+
+ var/obj/structure/transport/linear/prime_lift = return_closest_platform_to_z(target_z)
+ var/lift_z = prime_lift.z
+ // We're already at the desired z-level!
+ if(target_z == lift_z)
+ return FALSE
+
+ // The amount of z levels between the our and target_z
+ var/z_difference = abs(target_z - lift_z)
+ // Direction (up/down) needed to go to reach target_z
+ var/direction = lift_z < target_z ? UP : DOWN
+
+ // We can't go that way anymore, or possibly ever
+ if(!Check_lift_move(direction))
+ return FALSE
+
+ // Okay we're ready to start moving now.
+ controls_lock(TRUE)
+ // Send out a signal that we're going
+ SEND_SIGNAL(src, COMSIG_LIFT_SET_DIRECTION, direction)
+ var/travel_speed = prime_lift.elevator_vertical_speed
+
+ // Close all lift doors
+ update_lift_doors(action = CYCLE_CLOSED)
+ // Approach the desired z-level one step at a time
+ for(var/i in 1 to z_difference)
+ if(!Check_lift_move(direction))
+ break
+ if(loop_callback && !loop_callback.Invoke())
+ break
+ // move_after_delay will set up a timer and cause us to move after a time
+ move_after_delay(
+ lift_move_duration = travel_speed,
+ direction = direction,
+ user = user,
+ )
+ // and we don't want to send another request until the timer's done
+ stoplag(travel_speed + 0.1 SECONDS)
+ if(QDELETED(src) || QDELETED(prime_lift))
+ return
+
+ addtimer(CALLBACK(src, PROC_REF(open_lift_doors_callback)), 2 SECONDS)
+ SEND_SIGNAL(src, COMSIG_LIFT_SET_DIRECTION, 0)
+ controls_lock(FALSE)
+ return TRUE
+
+/**
+ * Updates all blast doors and shutters that share an ID with our lift.
+ *
+ * Arguments:
+ * on_z_level - optional, only open doors on this z-level or list of z-levels
+ * action - how do we update the doors? CYCLE_OPEN to make them open, CYCLE_CLOSED to make them shut
+ */
+/datum/transport_controller/linear/proc/update_lift_doors(on_z_level, action)
+
+ if(!isnull(on_z_level) && !islist(on_z_level))
+ on_z_level = list(on_z_level)
+
+ var/played_ding = FALSE
+ for(var/obj/machinery/door/elevator_door as anything in GLOB.elevator_doors)
+ if(elevator_door.transport_linked_id != specific_transport_id)
+ continue
+ if(on_z_level && !(elevator_door.z in on_z_level))
+ continue
+ switch(action)
+ if(CYCLE_OPEN)
+ elevator_door.elevator_status = LIFT_PLATFORM_UNLOCKED
+ if(!played_ding)
+ playsound(elevator_door, 'sound/machines/ping.ogg', 50, TRUE)
+ played_ding = TRUE
+ addtimer(CALLBACK(elevator_door, TYPE_PROC_REF(/obj/machinery/door, open)), 0.7 SECONDS)
+ if(CYCLE_CLOSED)
+ elevator_door.elevator_status = LIFT_PLATFORM_LOCKED
+ INVOKE_ASYNC(elevator_door, TYPE_PROC_REF(/obj/machinery/door, close))
+ else
+ stack_trace("Elevator lift update_lift_doors called with an improper action ([action]).")
+
+/// Helper used in callbacks to open all the doors our platform is on
+/datum/transport_controller/linear/proc/open_lift_doors_callback()
+ update_lift_doors(get_zs_we_are_on(), action = CYCLE_OPEN)
+
+/**
+ * Moves the platform, this is what users invoke with their hand.
+ * This is a SAFE proc, ensuring every part of the lift moves SANELY.
+ * It also locks controls for the (miniscule) duration of the movement, so the elevator cannot be broken by spamming.
+ */
+/datum/transport_controller/linear/proc/move_transport_horizontally(going)
+ if(modular_set)
+ controls_lock(TRUE)
+ for(var/obj/structure/transport/linear/module_to_move as anything in transport_modules)
+ module_to_move.travel(going)
+
+ controls_lock(FALSE)
+ return
+
+ var/max_x = 0
+ var/max_y = 0
+ var/max_z = 0
+ var/min_x = world.maxx
+ var/min_y = world.maxy
+ var/min_z = world.maxz
+
+ for(var/obj/structure/transport/linear/transport_module as anything in transport_modules)
+ max_z = max(max_z, transport_module.z)
+ min_z = min(min_z, transport_module.z)
+
+ min_x = min(min_x, transport_module.x)
+ max_x = max(max_x, transport_module.x)
+ //this assumes that all z levels have identical horizontal bounding boxes
+ //but if youre still using a non multitile platform at this point
+ //then its your own problem. it wont runtime it will just be slower than it needs to be if this assumption isnt
+ //the case
+
+ min_y = min(min_y, transport_module.y)
+ max_y = max(max_y, transport_module.y)
+
+ for(var/z in min_z to max_z)
+ //This must be safe way to border tile to tile move of bordered platforms, that excludes platform overlapping.
+ if(going & WEST)
+ //Go along the X axis from min to max, from left to right
+ for(var/x in min_x to max_x)
+ if(going & NORTH)
+ //Go along the Y axis from max to min, from up to down
+ for(var/y in max_y to min_y step -1)
+ var/obj/structure/transport/linear/transport_module = locate(/obj/structure/transport/linear, locate(x, y, z))
+ transport_module?.travel(going)
+
+ else if(going & SOUTH)
+ //Go along the Y axis from min to max, from down to up
+ for(var/y in min_y to max_y)
+ var/obj/structure/transport/linear/transport_module = locate(/obj/structure/transport/linear, locate(x, y, z))
+ transport_module?.travel(going)
+
+ else
+ for(var/y in min_y to max_y)
+ var/obj/structure/transport/linear/transport_module = locate(/obj/structure/transport/linear, locate(x, y, z))
+ transport_module?.travel(going)
+ else
+ //Go along the X axis from max to min, from right to left
+ for(var/x in max_x to min_x step -1)
+ if(going & NORTH)
+ //Go along the Y axis from max to min, from up to down
+ for(var/y in max_y to min_y step -1)
+ var/obj/structure/transport/linear/transport_module = locate(/obj/structure/transport/linear, locate(x, y, z))
+ transport_module?.travel(going)
+
+ else if (going & SOUTH)
+ for(var/y in min_y to max_y)
+ var/obj/structure/transport/linear/transport_module = locate(/obj/structure/transport/linear, locate(x, y, z))
+ transport_module?.travel(going)
+
+ else
+ //Go along the Y axis from min to max, from down to up
+ for(var/y in min_y to max_y)
+ var/obj/structure/transport/linear/transport_module = locate(/obj/structure/transport/linear, locate(x, y, z))
+ transport_module?.travel(going)
+
+///Check destination turfs
+/datum/transport_controller/linear/proc/Check_lift_move(check_dir)
+ for(var/obj/structure/transport/linear/transport_module as anything in transport_modules)
+ for(var/turf/bound_turf in transport_module.locs)
+ var/turf/T = get_step_multiz(transport_module, check_dir)
+ if(!T)//the edges of multi-z maps
+ return FALSE
+ if(check_dir == UP && !istype(T, /turf/open/openspace)) // We don't want to go through the ceiling!
+ return FALSE
+ if(check_dir == DOWN && !istype(get_turf(transport_module), /turf/open/openspace)) // No going through the floor!
+ return FALSE
+ return TRUE
+
+/**
+ * Sets transport controls_locked state. Used to prevent moving mid movement, or cooldowns.
+ */
+/datum/transport_controller/linear/proc/controls_lock(state)
+ switch(state)
+ if(FALSE)
+ controller_status &= ~CONTROLS_LOCKED
+ else
+ controller_status |= CONTROLS_LOCKED
+
+/**
+ * resets the contents of all platforms to their original state in case someone put a bunch of shit onto the platform.
+ * intended to be called by admins. passes all arguments to reset_contents() for each of our platforms.
+ *
+ * Arguments:
+ * * consider_anything_past - number. if > 0 our platforms will only handle foreign contents that exceed this number in each of their locs
+ * * foreign_objects - bool. if true our platforms will consider /atom/movable's that arent mobs as part of foreign contents
+ * * foreign_non_player_mobs - bool. if true our platforms consider mobs that dont have a mind to be foreign
+ * * consider_player_mobs - bool. if true our platforms consider player mobs to be foreign. only works if foreign_non_player_mobs is true as well
+ */
+/datum/transport_controller/linear/proc/reset_lift_contents(consider_anything_past = 0, foreign_objects = TRUE, foreign_non_player_mobs = TRUE, consider_player_mobs = FALSE)
+ for(var/obj/structure/transport/linear/lift_to_reset in transport_modules)
+ lift_to_reset.reset_contents(consider_anything_past, foreign_objects, foreign_non_player_mobs, consider_player_mobs)
+
+ return TRUE
diff --git a/code/modules/transport/tram/tram_controller.dm b/code/modules/transport/tram/tram_controller.dm
new file mode 100644
index 0000000000000..c20fb1bfef4ac
--- /dev/null
+++ b/code/modules/transport/tram/tram_controller.dm
@@ -0,0 +1,1076 @@
+/**
+ * Tram specific variant of the generic linear transport controller.
+ *
+ * Hierarchy
+ * The sstransport subsystem manages a list of controllers,
+ * A controller manages a list of transport modules (individual tiles) which together make up a transport unit (in this case a tram)
+ */
+/datum/transport_controller/linear/tram
+ ///whether this controller is active (any state we don't accept new orders, not nessecarily moving)
+ var/controller_active = FALSE
+ ///whether all required parts of the tram are considered operational
+ var/controller_operational = TRUE
+ var/obj/machinery/transport/tram_controller/paired_cabinet
+ ///if we're travelling, what direction are we going
+ var/travel_direction = NONE
+ ///if we're travelling, how far do we have to go
+ var/travel_remaining = 0
+ ///how far in total we'll be travelling
+ var/travel_trip_length = 0
+
+ ///multiplier on how much damage/force the tram imparts on things it hits
+ var/collision_lethality = 1
+ var/obj/effect/landmark/transport/nav_beacon/tram/nav/nav_beacon
+ /// reference to the destination landmarks we consider ourselves "at" or travelling towards. since we potentially span multiple z levels we dont actually
+ /// know where on us this platform is. as long as we know THAT its on us we can just move the distance and direction between this
+ /// and the destination landmark.
+ var/obj/effect/landmark/transport/nav_beacon/tram/platform/idle_platform
+ /// reference to the destination landmarks we consider ourselves travelling towards. since we potentially span multiple z levels we dont actually
+ /// know where on us this platform is. as long as we know THAT its on us we can just move the distance and direction between this
+ /// and the destination landmark.
+ var/obj/effect/landmark/transport/nav_beacon/tram/platform/destination_platform
+
+ var/current_speed = 0
+ var/current_load = 0
+
+ ///decisecond delay between horizontal movement. cannot make the tram move faster than 1 movement per world.tick_lag.
+ var/speed_limiter = 0.5
+
+ ///version of speed_limiter that gets set in init and is considered our base speed if our lift gets slowed down
+ var/base_speed_limiter = 0.5
+
+ ///the world.time we should next move at. in case our speed is set to less than 1 movement per tick
+ var/scheduled_move = INFINITY
+
+ ///whether we have been slowed down automatically
+ var/recovery_mode = FALSE
+
+ ///how many times we moved while costing more than SStransport.max_time milliseconds per movement.
+ ///if this exceeds SStransport.max_exceeding_moves
+ var/recovery_activate_count = 0
+
+ ///how many times we moved while costing less than 0.5 * SStransport.max_time milliseconds per movement
+ var/recovery_clear_count = 0
+
+ var/datum/tram_mfg_info/tram_registration
+
+ var/list/tram_history
+
+/datum/tram_mfg_info
+ var/serial_number
+ var/active = TRUE
+ var/mfg_date
+ var/install_location
+ var/distance_travelled = 0
+ var/collisions = 0
+
+/**
+ * Assign registration details to a new tram.
+ *
+ * When a new tram is created, we give it a builder's plate with the date it was created.
+ * We track a few stats about it, and keep a small historical record on the
+ * information plate inside the tram.
+ */
+/datum/tram_mfg_info/New(specific_transport_id)
+ if(GLOB.round_id)
+ serial_number = "LT306TG[add_leading(GLOB.round_id, 6, "0")]"
+ else
+ serial_number = "LT306TG[rand(000000, 999999)]"
+
+ mfg_date = "[CURRENT_STATION_YEAR]-[time2text(world.timeofday, "MM-DD")]"
+ install_location = specific_transport_id
+
+/datum/tram_mfg_info/proc/load_from_json(list/json_data)
+ serial_number = json_data["serial_number"]
+ active = json_data["active"]
+ mfg_date = json_data["mfg_date"]
+ install_location = json_data["install_location"]
+ distance_travelled = json_data["distance_travelled"]
+ collisions = json_data["collisions"]
+
+/datum/tram_mfg_info/proc/export_to_json()
+ var/list/new_data = list()
+ new_data["serial_number"] = serial_number
+ new_data["active"] = active
+ new_data["mfg_date"] = mfg_date
+ new_data["install_location"] = install_location
+ new_data["distance_travelled"] = distance_travelled
+ new_data["collisions"] = collisions
+ return new_data
+
+/**
+ * Make sure all modules have matching speed limiter vars, pull save data from persistence
+ *
+ * We track a few stats about it, and keep a small historical record on the
+ * information plate inside the tram.
+ */
+/datum/transport_controller/linear/tram/New(obj/structure/transport/linear/tram/transport_module)
+ . = ..()
+ speed_limiter = transport_module.speed_limiter
+ base_speed_limiter = transport_module.speed_limiter
+ tram_history = SSpersistence.load_tram_history(specific_transport_id)
+ var/datum/tram_mfg_info/previous_tram = peek(tram_history)
+ if(!isnull(previous_tram) && previous_tram.active)
+ tram_registration = pop(tram_history)
+ else
+ tram_registration = new /datum/tram_mfg_info(specific_transport_id)
+
+ check_starting_landmark()
+
+/**
+ * If someone VVs the base speed limiter of the tram, copy it to the current active speed limiter.
+ */
+/datum/transport_controller/linear/tram/vv_edit_var(var_name, var_value)
+ . = ..()
+ if(var_name == "base_speed_limiter")
+ speed_limiter = max(speed_limiter, base_speed_limiter)
+
+/datum/transport_controller/linear/tram/Destroy()
+ paired_cabinet = null
+ set_status_code(SYSTEM_FAULT, TRUE)
+ tram_registration.active = FALSE
+ SSblackbox.record_feedback("amount", "tram_destroyed", 1)
+ SSpersistence.save_tram_history(specific_transport_id)
+ ..()
+
+/**
+ * Register transport modules to the controller
+ *
+ * Spreads out searching neighbouring tiles for additional transport modules, to combine into one full tram.
+ * We register to every module's signal that it's collided with something, be it mob, structure, etc.
+ */
+/datum/transport_controller/linear/tram/add_transport_modules(obj/structure/transport/linear/new_transport_module)
+ . = ..()
+ RegisterSignal(new_transport_module, COMSIG_MOVABLE_BUMP, PROC_REF(gracefully_break))
+
+/**
+ * The mapper should have placed the tram at one of the stations, the controller will search for a landmark within
+ * its control area and set it as its idle position.
+ */
+/datum/transport_controller/linear/tram/check_for_landmarks(obj/structure/transport/linear/tram/new_transport_module)
+ . = ..()
+ for(var/turf/platform_loc as anything in new_transport_module.locs)
+ var/obj/effect/landmark/transport/nav_beacon/tram/platform/initial_destination = locate() in platform_loc
+ var/obj/effect/landmark/transport/nav_beacon/tram/nav/beacon = locate() in platform_loc
+
+ if(initial_destination)
+ idle_platform = initial_destination
+ destination_platform = initial_destination
+
+ if(beacon)
+ nav_beacon = beacon
+
+/**
+ * Verify tram is in a valid starting location, start the subsystem.
+ *
+ * Throw an error if someone mapped a tram with no landmarks available for it to register.
+ * The processing subsystem starts off because not all maps have elevators/transports.
+ * Now that the tram is aware of its surroundings, we start the subsystem.
+ */
+/datum/transport_controller/linear/tram/proc/check_starting_landmark()
+ if(!idle_platform || !nav_beacon)
+ CRASH("a tram lift_master was initialized without the required landmarks to give it direction!")
+
+ SStransport.can_fire = TRUE
+
+ return TRUE
+
+/**
+ * The tram explodes if it hits a few types of objects.
+ *
+ * Signal for when the tram runs into a field of which it cannot go through.
+ * Stops the train's travel fully, sends a message, and destroys the train.
+ * Arguments:
+ * * bumped_atom - The atom this tram bumped into
+ */
+/datum/transport_controller/linear/tram/proc/gracefully_break(atom/bumped_atom)
+ SIGNAL_HANDLER
+
+ travel_remaining = 0
+ bumped_atom.visible_message(span_userdanger("The [bumped_atom.name] crashes into the field violently!"))
+ for(var/obj/structure/transport/linear/tram/transport_module as anything in transport_modules)
+ transport_module.set_travelling(FALSE)
+ for(var/explosive_target in transport_module.transport_contents)
+ if(iseffect(explosive_target))
+ continue
+
+ if(isliving(explosive_target))
+ explosion(explosive_target, devastation_range = rand(0, 1), heavy_impact_range = 2, light_impact_range = 3) //50% chance of gib
+
+ else if(prob(9))
+ explosion(explosive_target, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 3)
+
+ explosion(transport_module, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 3)
+ qdel(transport_module)
+
+ send_transport_active_signal()
+
+/**
+ * Calculate the journey details to the requested platform
+ *
+ * These will eventually be passed to the transport modules as args telling them where to move.
+ * We do some sanity checking in case of discrepencany between where the subsystem thinks the
+ * tram is and where the tram actually is. (For example, moving the landmarks after round start.)
+
+ */
+/datum/transport_controller/linear/tram/proc/calculate_route(obj/effect/landmark/transport/nav_beacon/tram/destination)
+ if(destination == idle_platform)
+ return FALSE
+
+ destination_platform = destination
+ travel_direction = get_dir(nav_beacon, destination_platform)
+ travel_remaining = get_dist(nav_beacon, destination_platform)
+ travel_trip_length = travel_remaining
+ log_transport("TC: [specific_transport_id] trip calculation: src: [nav_beacon.x], [nav_beacon.y], [nav_beacon.z] dst: [destination_platform] [destination_platform.x], [destination_platform.y], [destination_platform.z] = Dir [travel_direction] Dist [travel_remaining].")
+ return TRUE
+
+/**
+ * Handles moving the tram
+ *
+ * Called by the subsystem, the controller tells the individual tram parts where to actually go and has extra safety checks
+ * incase multiple inputs get through, preventing conflicting directions and the tram literally ripping itself apart.
+ * All of the actual movement is handled by SStransport.
+ *
+ * If we're this far all the PRE_DEPARTURE checks should have passed, so we leave the PRE_DEPARTURE status and actually move.
+ * We send a signal to anything registered that cares about the physical movement of the tram.
+ *
+ * Arguments:
+ * * destination_platform - where the subsystem wants it to go
+ */
+
+/datum/transport_controller/linear/tram/proc/dispatch_transport(obj/effect/landmark/transport/nav_beacon/tram/destination_platform)
+ log_transport("TC: [specific_transport_id] starting departure.")
+ set_status_code(PRE_DEPARTURE, FALSE)
+ if(controller_status & EMERGENCY_STOP)
+ set_status_code(EMERGENCY_STOP, FALSE)
+ playsound(paired_cabinet, 'sound/machines/synth_yes.ogg', 40, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ paired_cabinet.say("Controller reset.")
+
+ SEND_SIGNAL(src, COMSIG_TRAM_TRAVEL, idle_platform, destination_platform)
+
+ for(var/obj/structure/transport/linear/tram/transport_module as anything in transport_modules) //only thing everyone needs to know is the new location.
+ if(transport_module.travelling) //wee woo wee woo there was a double action queued. damn multi tile structs
+ return //we don't care to undo cover_locked controls, though, as that will resolve itself
+ transport_module.glide_size_override = DELAY_TO_GLIDE_SIZE(speed_limiter)
+ transport_module.set_travelling(TRUE)
+
+ scheduled_move = world.time + speed_limiter
+
+ START_PROCESSING(SStransport, src)
+
+/**
+ * Tram processing loop
+ *
+ * Moves the tram to its set destination.
+ * When it arrives at its destination perform callback to the post-arrival procs like controls and lights.
+ * We update the odometer and kill the process until we need to move again.
+ *
+ * If the status is EMERGENCY_STOP the tram should immediately come to a stop regardless of the travel_remaining.
+ * Some extra things happen in an emergency stop (throwing the passengers) and when reset will run a
+ * recovery procedure to head to the nearest platform and sync logical and physical location data
+ * (idle_platform and nav_beacon) once the issue is resolved.
+ */
+/datum/transport_controller/linear/tram/process(seconds_per_tick)
+ if(isnull(paired_cabinet))
+ set_status_code(SYSTEM_FAULT, TRUE)
+
+ if(controller_status & SYSTEM_FAULT || controller_status & EMERGENCY_STOP)
+ halt_and_catch_fire()
+ return PROCESS_KILL
+
+ if(!travel_remaining)
+ if(!controller_operational)
+ degraded_stop()
+ return PROCESS_KILL
+
+ normal_stop()
+ return PROCESS_KILL
+
+ else if(world.time >= scheduled_move)
+ var/start_time = TICK_USAGE
+ travel_remaining--
+
+ move_transport_horizontally(travel_direction)
+
+ var/duration = TICK_USAGE_TO_MS(start_time)
+ current_load = duration
+ current_speed = transport_modules[1].glide_size
+ if(recovery_mode)
+ if(duration <= (SStransport.max_time / 2))
+ recovery_clear_count++
+ else
+ recovery_clear_count = 0
+
+ if(recovery_clear_count >= SStransport.max_cheap_moves)
+ speed_limiter = base_speed_limiter
+ recovery_mode = FALSE
+ recovery_clear_count = 0
+ log_transport("TC: [specific_transport_id] removing speed limiter, performance issue resolved. Last tick was [duration]ms.")
+
+ else if(duration > SStransport.max_time)
+ recovery_activate_count++
+ if(recovery_activate_count >= SStransport.max_exceeding_moves)
+ message_admins("The tram at [ADMIN_JMP(transport_modules[1])] is taking [duration] ms which is more than [SStransport.max_time] ms per movement for [recovery_activate_count] ticks. Reducing its movement speed until it recovers. If this continues to be a problem you can reset the tram contents to its original state, and clear added objects on the Debug tab.")
+ log_transport("TC: [specific_transport_id] activating speed limiter due to poor performance. Last tick was [duration]ms.")
+ speed_limiter = base_speed_limiter * 2 //halves its speed
+ recovery_mode = TRUE
+ recovery_activate_count = 0
+ else
+ recovery_activate_count = max(recovery_activate_count - 1, 0)
+
+ scheduled_move = world.time + speed_limiter
+
+/datum/transport_controller/linear/tram/proc/normal_stop()
+ cycle_doors(CYCLE_OPEN)
+ log_transport("TC: [specific_transport_id] trip completed. Info: nav_pos ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z]) idle_pos ([destination_platform.x], [destination_platform.y], [destination_platform.z]).")
+ addtimer(CALLBACK(src, PROC_REF(unlock_controls)), 2 SECONDS)
+ if((controller_status & SYSTEM_FAULT) && (nav_beacon.loc == destination_platform.loc)) //position matches between controller and tram, we're back on track
+ set_status_code(SYSTEM_FAULT, FALSE)
+ playsound(paired_cabinet, 'sound/machines/synth_yes.ogg', 40, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ paired_cabinet.say("Controller reset.")
+ log_transport("TC: [specific_transport_id] position data successfully reset.")
+ speed_limiter = initial(speed_limiter)
+ idle_platform = destination_platform
+ tram_registration.distance_travelled += (travel_trip_length - travel_remaining)
+ travel_trip_length = 0
+ current_speed = 0
+ current_load = 0
+ speed_limiter = initial(speed_limiter)
+
+/datum/transport_controller/linear/tram/proc/degraded_stop()
+ log_transport("TC: [specific_transport_id] trip completed with a degraded status. Info: [TC_TS_STATUS] nav_pos ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z]) idle_pos ([destination_platform.x], [destination_platform.y], [destination_platform.z]).")
+ addtimer(CALLBACK(src, PROC_REF(unlock_controls)), 4 SECONDS)
+ if(controller_status & SYSTEM_FAULT)
+ set_status_code(SYSTEM_FAULT, FALSE)
+ playsound(paired_cabinet, 'sound/machines/synth_yes.ogg', 40, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ paired_cabinet.say("Controller reset.")
+ log_transport("TC: [specific_transport_id] position data successfully reset. ")
+ speed_limiter = initial(speed_limiter)
+ idle_platform = destination_platform
+ tram_registration.distance_travelled += (travel_trip_length - travel_remaining)
+ travel_trip_length = 0
+ current_speed = 0
+ current_load = 0
+ speed_limiter = initial(speed_limiter)
+ var/throw_direction = travel_direction
+ for(var/obj/structure/transport/linear/tram/module in transport_modules)
+ module.estop_throw(throw_direction)
+
+/datum/transport_controller/linear/tram/proc/halt_and_catch_fire()
+ if(controller_status & SYSTEM_FAULT)
+ if(!isnull(paired_cabinet))
+ playsound(paired_cabinet, 'sound/machines/buzz-sigh.ogg', 60, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ paired_cabinet.say("Controller error. Please contact your engineering department.")
+ log_transport("TC: [specific_transport_id] Transport Controller failed!")
+
+ if(travel_remaining)
+ travel_remaining = 0
+ var/throw_direction = travel_direction
+ for(var/obj/structure/transport/linear/tram/module in transport_modules)
+ module.estop_throw(throw_direction)
+
+ addtimer(CALLBACK(src, PROC_REF(unlock_controls)), 4 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(cycle_doors), CYCLE_OPEN), 2 SECONDS)
+ idle_platform = null
+ log_transport("TC: [specific_transport_id] Transport Controller needs new position data from the tram.")
+ tram_registration.distance_travelled += (travel_trip_length - travel_remaining)
+ travel_trip_length = 0
+ current_speed = 0
+ current_load = 0
+
+/datum/transport_controller/linear/tram/proc/reset_position()
+ if(idle_platform)
+ if(get_turf(idle_platform) == get_turf(nav_beacon))
+ set_status_code(SYSTEM_FAULT, FALSE)
+ set_status_code(EMERGENCY_STOP, FALSE)
+ playsound(paired_cabinet, 'sound/machines/synth_yes.ogg', 40, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ paired_cabinet.say("Controller reset.")
+ log_transport("TC: [specific_transport_id] Transport Controller reset was requested, but the tram nav data seems correct. Info: nav_pos ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z]) idle_pos ([idle_platform.x], [idle_platform.y], [idle_platform.z]).")
+ return
+
+ log_transport("TC: [specific_transport_id] performing Transport Controller reset. Locating closest reset beacon to ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z])")
+ var/tram_velocity_sign
+ if(travel_direction & (NORTH|SOUTH))
+ tram_velocity_sign = travel_direction & NORTH ? OUTBOUND : INBOUND
+ else
+ tram_velocity_sign = travel_direction & EAST ? OUTBOUND : INBOUND
+
+ var/reset_beacon = closest_nav_in_travel_dir(nav_beacon, tram_velocity_sign, specific_transport_id)
+
+ if(!reset_beacon)
+ playsound(paired_cabinet, 'sound/machines/buzz-sigh.ogg', 60, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ paired_cabinet.say("Controller reset failed. Contact manufacturer.") // If you screwed up the tram this bad, I don't even
+ log_transport("TC: [specific_transport_id] non-recoverable error! Tram is at ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z] [tram_velocity_sign ? "OUTBOUND" : "INBOUND"]) and can't find a reset beacon.")
+ message_admins("Tram ID [specific_transport_id] is in a non-recoverable error state at [ADMIN_JMP(nav_beacon)]. If it's causing problems, delete the controller datum from the 'Reset Tram' proc in the Debug tab.")
+ return
+
+ travel_direction = get_dir(nav_beacon, reset_beacon)
+ travel_remaining = get_dist(nav_beacon, reset_beacon)
+ travel_trip_length = travel_remaining
+ destination_platform = reset_beacon
+ speed_limiter = 1.5
+ playsound(paired_cabinet, 'sound/machines/ping.ogg', 40, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ paired_cabinet.say("Peforming controller reset... Navigating to reset point.")
+ log_transport("TC: [specific_transport_id] trip calculation: src: [nav_beacon.x], [nav_beacon.y], [nav_beacon.z] dst: [destination_platform] [destination_platform.x], [destination_platform.y], [destination_platform.z] = Dir [travel_direction] Dist [travel_remaining].")
+ cycle_doors(CYCLE_CLOSED)
+ set_active(TRUE)
+ set_status_code(CONTROLS_LOCKED, TRUE)
+ addtimer(CALLBACK(src, PROC_REF(dispatch_transport), reset_beacon), 3 SECONDS)
+ log_transport("TC: [specific_transport_id] trying to reset at [destination_platform].")
+
+/datum/transport_controller/linear/tram/proc/estop()
+ playsound(paired_cabinet, 'sound/machines/buzz-sigh.ogg', 60, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ paired_cabinet.say("Emergency stop activated!")
+ set_status_code(EMERGENCY_STOP, TRUE)
+ log_transport("TC: [specific_transport_id] requested emergency stop.")
+
+/**
+ * Handles unlocking the tram controls for use after moving
+ *
+ * More safety checks to make sure the tram has actually docked properly
+ * at a location before users are allowed to interact with the tram console again.
+ * Tram finds its location at this point before fully unlocking controls to the user.
+ */
+/datum/transport_controller/linear/tram/proc/unlock_controls()
+ controls_lock(FALSE)
+ for(var/obj/structure/transport/linear/tram/transport_module as anything in transport_modules) //only thing everyone needs to know is the new location.
+ transport_module.set_travelling(FALSE)
+ set_active(FALSE)
+
+/**
+ * Sets the active status for the controller and sends a signal to listeners.
+ *
+ * The main signal used by most components, it has the active status, the bitfield of the controller's status, its direction, and set destination.
+ *
+ * Arguments:
+ * new_status - The active status of the controller (whether it's busy doing something and not taking commands right now)
+ */
+/datum/transport_controller/linear/tram/proc/set_active(new_status)
+ if(controller_active == new_status)
+ return
+
+ controller_active = new_status
+ send_transport_active_signal()
+ log_transport("TC: [specific_transport_id] controller state [controller_active ? "READY > PROCESSING" : "PROCESSING > READY"].")
+
+/**
+ * Sets the controller status bitfield
+ *
+ * This status var is used by various components like lights, crossing signals, signs
+ * Sent via signal the listening components will perform required actions based on
+ * the status codes.
+ *
+ * Arguments:
+ * * code - The status bitflag we're changing
+ * * value - boolean TRUE/FALSE to set the code
+ */
+/datum/transport_controller/linear/tram/proc/set_status_code(code, value)
+ if(code != DOORS_READY)
+ log_transport("TC: [specific_transport_id] status change [value ? "+" : "-"][english_list(bitfield_to_list(code, TRANSPORT_FLAGS))].")
+ switch(value)
+ if(TRUE)
+ controller_status |= code
+ if(FALSE)
+ controller_status &= ~code
+ else
+ stack_trace("Transport controller received invalid status code request [code]/[value]")
+ return
+
+ send_transport_active_signal()
+
+/datum/transport_controller/linear/tram/proc/send_transport_active_signal()
+ SEND_SIGNAL(SStransport, COMSIG_TRANSPORT_ACTIVE, src, controller_active, controller_status, travel_direction, destination_platform)
+
+/**
+ * Part of the pre-departure list, checks the status of the doors on the tram
+ *
+ * Checks if all doors are closed, and updates the status code accordingly.
+ *
+ * TODO: this is probably better renamed check_door_status()
+ */
+/datum/transport_controller/linear/tram/proc/update_status()
+ for(var/obj/machinery/door/airlock/tram/door as anything in SStransport.doors)
+ if(door.transport_linked_id != specific_transport_id)
+ continue
+ if(door.crushing_in_progress)
+ log_transport("TC: [specific_transport_id] door [door.id_tag] failed crush status check.")
+ set_status_code(DOORS_READY, FALSE)
+ return
+
+ set_status_code(DOORS_READY, TRUE)
+
+/**
+ * Cycle all the doors on the tram.
+ */
+/datum/transport_controller/linear/tram/proc/cycle_doors(door_status, rapid)
+ switch(door_status)
+ if(CYCLE_OPEN)
+ for(var/obj/machinery/door/airlock/tram/door as anything in SStransport.doors)
+ if(door.transport_linked_id == specific_transport_id)
+ INVOKE_ASYNC(door, TYPE_PROC_REF(/obj/machinery/door/airlock/tram, open), rapid)
+
+ if(CYCLE_CLOSED)
+ for(var/obj/machinery/door/airlock/tram/door as anything in SStransport.doors)
+ if(door.transport_linked_id == specific_transport_id)
+ INVOKE_ASYNC(door, TYPE_PROC_REF(/obj/machinery/door/airlock/tram, close), rapid)
+
+/datum/transport_controller/linear/tram/proc/notify_controller(obj/machinery/transport/tram_controller/new_cabinet)
+ paired_cabinet = new_cabinet
+ RegisterSignal(new_cabinet, COMSIG_MACHINERY_POWER_LOST, PROC_REF(power_lost))
+ RegisterSignal(new_cabinet, COMSIG_MACHINERY_POWER_RESTORED, PROC_REF(power_restored))
+ RegisterSignal(new_cabinet, COMSIG_QDELETING, PROC_REF(on_cabinet_qdel))
+ log_transport("TC: [specific_transport_id] is now paired with [new_cabinet].")
+ if(controller_status & SYSTEM_FAULT)
+ set_status_code(SYSTEM_FAULT, FALSE)
+ reset_position()
+
+/datum/transport_controller/linear/tram/proc/on_cabinet_qdel()
+ paired_cabinet = null
+ log_transport("TC: [specific_transport_id] received QDEL from controller cabinet.")
+ set_status_code(SYSTEM_FAULT, TRUE)
+
+/**
+ * Tram malfunction random event. Set comm error, increase tram lethality.
+ */
+/datum/transport_controller/linear/tram/proc/start_malf_event()
+ set_status_code(COMM_ERROR, TRUE)
+ SEND_TRANSPORT_SIGNAL(COMSIG_COMMS_STATUS, src, FALSE)
+ paired_cabinet.generate_repair_signals()
+ collision_lethality = 1.25
+ log_transport("TC: [specific_transport_id] starting Tram Malfunction event.")
+
+/**
+ * Remove effects of tram malfunction event.
+ *
+ * If engineers didn't already repair the tram by the end of the event,
+ * automagically reset it remotely.
+ */
+/datum/transport_controller/linear/tram/proc/end_malf_event()
+ if(!(controller_status & COMM_ERROR))
+ return
+ set_status_code(COMM_ERROR, FALSE)
+ paired_cabinet.clear_repair_signals()
+ collision_lethality = initial(collision_lethality)
+ SEND_TRANSPORT_SIGNAL(COMSIG_COMMS_STATUS, src, TRUE)
+ log_transport("TC: [specific_transport_id] ending Tram Malfunction event.")
+
+/datum/transport_controller/linear/tram/proc/register_collision()
+ tram_registration.collisions += 1
+ SEND_TRANSPORT_SIGNAL(COMSIG_TRAM_COLLISION, SSpersistence.tram_hits_this_round)
+
+/datum/transport_controller/linear/tram/proc/power_lost()
+ set_operational(FALSE)
+ log_transport("TC: [specific_transport_id] power lost.")
+ send_transport_active_signal()
+
+/datum/transport_controller/linear/tram/proc/power_restored()
+ set_operational(TRUE)
+ log_transport("TC: [specific_transport_id] power restored.")
+ cycle_doors(CYCLE_OPEN)
+ send_transport_active_signal()
+
+/datum/transport_controller/linear/tram/proc/set_operational(new_value)
+ if(controller_operational != new_value)
+ controller_operational = new_value
+
+/**
+ * Returns the closest tram nav beacon to an atom
+ *
+ * Creates a list of nav beacons in the requested direction
+ * and returns the closest to be passed to the industrial_lift
+ *
+ * Arguments: source: the starting point to find a beacon
+ * travel_dir: travel direction in tram form, INBOUND or OUTBOUND
+ * beacon_type: what list of beacons we pull from
+ */
+/datum/transport_controller/linear/tram/proc/closest_nav_in_travel_dir(atom/origin, travel_dir, beacon_type)
+ if(!istype(origin) || !origin.z)
+ return FALSE
+
+ var/list/obj/effect/landmark/transport/nav_beacon/tram/inbound_candidates = list()
+ var/list/obj/effect/landmark/transport/nav_beacon/tram/outbound_candidates = list()
+
+ for(var/obj/effect/landmark/transport/nav_beacon/tram/candidate_beacon in SStransport.nav_beacons[beacon_type])
+ if(candidate_beacon.z != origin.z || candidate_beacon.z != nav_beacon.z)
+ continue
+
+ switch(nav_beacon.dir)
+ if(EAST, WEST)
+ if(candidate_beacon.y != nav_beacon.y)
+ continue
+ else if(candidate_beacon.x < nav_beacon.x)
+ inbound_candidates += candidate_beacon
+ else
+ outbound_candidates += candidate_beacon
+ if(NORTH, SOUTH)
+ if(candidate_beacon.x != nav_beacon.x)
+ continue
+ else if(candidate_beacon.y < nav_beacon.y)
+ inbound_candidates += candidate_beacon
+ else
+ outbound_candidates += candidate_beacon
+
+ switch(travel_dir)
+ if(INBOUND)
+ var/obj/effect/landmark/transport/nav_beacon/tram/nav/selected = get_closest_atom(/obj/effect/landmark/transport/nav_beacon/tram, inbound_candidates, origin)
+ if(selected)
+ return selected
+ stack_trace("No inbound beacon candidate found for [origin]. Cancelling dispatch.")
+ return FALSE
+
+ if(OUTBOUND)
+ var/obj/effect/landmark/transport/nav_beacon/tram/nav/selected = get_closest_atom(/obj/effect/landmark/transport/nav_beacon/tram, outbound_candidates, origin)
+ if(selected)
+ return selected
+ stack_trace("No outbound beacon candidate found for [origin]. Cancelling dispatch.")
+ return FALSE
+
+ else
+ stack_trace("Tram receieved invalid travel direction [travel_dir]. Cancelling dispatch.")
+
+ return FALSE
+
+/**
+ * Moves the tram when hit by an immovable rod
+ *
+ * Tells the individual tram parts where to actually go and has an extra safety checks
+ * incase multiple inputs get through, preventing conflicting directions and the tram
+ * literally ripping itself apart. all of the actual movement is handled by SStramprocess
+ *
+ * Arguments: collided_rod (the immovable rod that hit the tram)
+ * Return: push_destination (the landmark /obj/effect/landmark/tram/nav that the tram is being pushed to due to the rod's trajectory)
+ */
+/datum/transport_controller/linear/tram/proc/rod_collision(obj/effect/immovablerod/collided_rod)
+ log_transport("TC: [specific_transport_id] hit an immovable rod.")
+ if(!controller_operational)
+ return
+ var/rod_velocity_sign
+ // Determine inbound or outbound
+ if(collided_rod.dir & (NORTH|SOUTH))
+ rod_velocity_sign = collided_rod.dir & NORTH ? OUTBOUND : INBOUND
+ else
+ rod_velocity_sign = collided_rod.dir & EAST ? OUTBOUND : INBOUND
+
+ var/obj/effect/landmark/transport/nav_beacon/tram/nav/push_destination = closest_nav_in_travel_dir(origin = nav_beacon, travel_dir = rod_velocity_sign, beacon_type = IMMOVABLE_ROD_DESTINATIONS)
+ if(!push_destination)
+ return
+ travel_direction = get_dir(nav_beacon, push_destination)
+ travel_remaining = get_dist(nav_beacon, push_destination)
+ travel_trip_length = travel_remaining
+ destination_platform = push_destination
+ log_transport("TC: [specific_transport_id] collided at ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z]) towards [push_destination] ([push_destination.x], [push_destination.y], [push_destination.z]) Dir [travel_direction] Dist [travel_remaining].")
+ // Don't bother processing crossing signals, where this tram's going there are no signals
+ //for(var/obj/machinery/transport/crossing_signal/xing as anything in SStransport.crossing_signals)
+ // xing.temp_malfunction()
+ priority_announce("In a turn of rather peculiar events, it appears that [GLOB.station_name] has struck an immovable rod. (Don't ask us where it came from.) This has led to a station brakes failure on one of the tram platforms.\n\n\
+ Our diligent team of engineers have been informed and they're rushing over - although not quite at the speed of our recently flying tram.\n\n\
+ So while we all look in awe at the universe's mysterious sense of humour, please stand clear of the tracks and remember to stand behind the yellow line.", "Braking News")
+ set_active(TRUE)
+ set_status_code(CONTROLS_LOCKED, TRUE)
+ dispatch_transport(destination_platform = push_destination)
+ return push_destination
+
+/**
+ * The physical cabinet on the tram. Acts as the interface between players and the controller datum.
+ */
+/obj/machinery/transport/tram_controller
+ name = "tram controller"
+ desc = "Makes the tram go, or something. Holds the tram's electronics, controls, and maintenance panel. A sticker above the card reader says 'Engineering access only.'"
+ icon = 'icons/obj/tram/tram_controllers.dmi'
+ icon_state = "controller-panel"
+ anchored = TRUE
+ density = FALSE
+ armor_type = /datum/armor/transport_module
+ resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
+ max_integrity = 750
+ integrity_failure = 0.25
+ layer = SIGN_LAYER
+ req_access = list(ACCESS_TCOMMS)
+ idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 4.8
+ active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 4.8
+ var/datum/transport_controller/linear/tram/controller_datum
+ /// If the cover is open
+ var/cover_open = FALSE
+ /// If the cover is locked
+ var/cover_locked = TRUE
+ COOLDOWN_DECLARE(manual_command_cooldown)
+
+/obj/machinery/transport/tram_controller/hilbert
+ configured_transport_id = HILBERT_LINE_1
+ flags_1 = NODECONSTRUCT_1
+
+/obj/machinery/transport/tram_controller/Initialize(mapload)
+ . = ..()
+ register_context()
+ if(!id_tag)
+ id_tag = assign_random_name()
+ return INITIALIZE_HINT_LATELOAD
+
+/**
+ * Mapped or built tram cabinet isn't located on a transport module.
+ */
+/obj/machinery/transport/tram_controller/LateInitialize(mapload)
+ . = ..()
+ SStransport.hello(src, name, id_tag)
+ find_controller()
+ update_appearance()
+
+/obj/machinery/transport/tram_controller/atom_break()
+ set_machine_stat(machine_stat | BROKEN)
+ ..()
+
+/obj/machinery/transport/tram_controller/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ if(held_item?.tool_behaviour == TOOL_SCREWDRIVER)
+ context[SCREENTIP_CONTEXT_RMB] = panel_open ? "close panel" : "open panel"
+
+ if(!held_item)
+ context[SCREENTIP_CONTEXT_LMB] = cover_open ? "access controls" : "open cabinet"
+ context[SCREENTIP_CONTEXT_RMB] = cover_open ? "close cabinet" : "toggle lock"
+
+
+ if(panel_open)
+ if(held_item?.tool_behaviour == TOOL_WRENCH)
+ context[SCREENTIP_CONTEXT_RMB] = "unscrew cabinet"
+ if(malfunctioning || methods_to_fix.len)
+ context[SCREENTIP_CONTEXT_LMB] = "repair electronics"
+
+ if(held_item?.tool_behaviour == TOOL_WELDER)
+ context[SCREENTIP_CONTEXT_LMB] = "repair frame"
+
+ if(istype(held_item, /obj/item/card/emag) && !(obj_flags & EMAGGED))
+ context[SCREENTIP_CONTEXT_LMB] = "emag controller"
+
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/machinery/transport/tram_controller/examine(mob/user)
+ . = ..()
+ . += span_notice("The door appears to be [cover_locked ? "locked. Swipe an ID card to unlock" : "unlocked. Swipe an ID card to lock"].")
+ if(panel_open)
+ . += span_notice("It is secured to the tram wall with [EXAMINE_HINT("bolts.")]")
+ . += span_notice("The maintenance panel can be closed with a [EXAMINE_HINT("screwdriver.")]")
+ else
+ . += span_notice("The maintenance panel can be opened with a [EXAMINE_HINT("screwdriver.")]")
+
+ if(cover_open)
+ . += span_notice("The [EXAMINE_HINT("yellow reset button")] resets the tram controller if a problem occurs or needs to be restarted.")
+ . += span_notice("The [EXAMINE_HINT("red stop button")] immediately stops the tram, requiring a reset afterwards.")
+ . += span_notice("The cabinet can be closed with a [EXAMINE_HINT("Right-click.")]")
+ else
+ . += span_notice("The cabinet can be opened with a [EXAMINE_HINT("Left-click.")]")
+
+
+/obj/machinery/transport/tram_controller/attackby(obj/item/weapon, mob/living/user, params)
+ if(user.combat_mode || cover_open)
+ return ..()
+
+ var/obj/item/card/id/id_card = user.get_id_in_hand()
+ if(!isnull(id_card))
+ try_toggle_lock(user, id_card)
+ return
+
+ return ..()
+
+/obj/machinery/transport/tram_controller/attack_hand(mob/living/user, params)
+ . = ..()
+ if(cover_open)
+ return
+
+ if(cover_locked)
+ var/obj/item/card/id/id_card = user.get_idcard(TRUE)
+ if(isnull(id_card))
+ balloon_alert(user, "access denied!")
+ return
+
+ try_toggle_lock(user, id_card)
+ return
+
+ toggle_door()
+
+/obj/machinery/transport/tram_controller/attack_hand_secondary(mob/living/user, params)
+ . = ..()
+
+ if(!cover_open)
+ var/obj/item/card/id/id_card = user.get_idcard(TRUE)
+ if(isnull(id_card))
+ balloon_alert(user, "access denied!")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ try_toggle_lock(user, id_card)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ toggle_door()
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/obj/machinery/transport/tram_controller/proc/toggle_door()
+ if(!cover_open)
+ playsound(loc, 'sound/machines/closet_open.ogg', 35, TRUE, -3)
+ else
+ playsound(loc, 'sound/machines/closet_close.ogg', 50, TRUE, -3)
+ cover_open = !cover_open
+ update_appearance()
+
+/obj/machinery/transport/tram_controller/proc/try_toggle_lock(mob/living/user, obj/item/card/id_card, params)
+ if(isnull(id_card))
+ id_card = user.get_idcard(TRUE)
+ if(obj_flags & EMAGGED)
+ balloon_alert(user, "access controller damaged!")
+ return FALSE
+
+ if(check_access(id_card))
+ cover_locked = !cover_locked
+ balloon_alert(user, "controls [cover_locked ? "locked" : "unlocked"]")
+ update_appearance()
+ return TRUE
+
+ balloon_alert(user, "access denied!")
+ return FALSE
+
+/obj/machinery/transport/tram_controller/wrench_act_secondary(mob/living/user, obj/item/tool)
+ . = ..()
+ if(panel_open && cover_open)
+ balloon_alert(user, "unsecuring...")
+ tool.play_tool_sound(src)
+ if(!tool.use_tool(src, user, 6 SECONDS))
+ return
+ playsound(loc, 'sound/items/deconstruct.ogg', 50, vary = TRUE)
+ balloon_alert(user, "unsecured")
+ deconstruct()
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/obj/machinery/transport/tram_controller/screwdriver_act_secondary(mob/living/user, obj/item/tool)
+ . = ..()
+ if(!cover_open)
+ return
+
+ tool.play_tool_sound(src)
+ panel_open = !panel_open
+ balloon_alert(user, "[panel_open ? "mounting bolts exposed" : "mounting bolts hidden"]")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/obj/machinery/transport/tram_controller/deconstruct(disassembled = TRUE)
+ if(flags_1 & NODECONSTRUCT_1)
+ return
+
+ var/turf/drop_location = find_obstruction_free_location(1, src)
+
+ if(disassembled)
+ new /obj/item/wallframe/tram/controller(drop_location)
+ else
+ new /obj/item/stack/sheet/mineral/titanium(drop_location, 2)
+ new /obj/item/stack/sheet/iron(drop_location, 1)
+ qdel(src)
+
+/**
+ * Update the blinky lights based on the controller status, allowing to quickly check without opening up the cabinet.
+ */
+/obj/machinery/transport/tram_controller/update_overlays()
+ . = ..()
+
+ if(!cover_open)
+ . += mutable_appearance(icon, "controller-closed")
+ if(cover_locked)
+ . += mutable_appearance(icon, "controller-locked")
+
+ else
+ var/mutable_appearance/controller_door = mutable_appearance(icon, "controller-open")
+ controller_door.pixel_w = -3
+ . += controller_door
+
+ if(machine_stat & NOPOWER)
+ . += mutable_appearance(icon, "estop")
+ . += emissive_appearance(icon, "estop", src, alpha = src.alpha)
+ return
+
+ . += mutable_appearance(icon, "power")
+ . += emissive_appearance(icon, "power", src, alpha = src.alpha)
+
+ if(!controller_datum)
+ . += mutable_appearance(icon, "fatal")
+ . += emissive_appearance(icon, "fatal", src, alpha = src.alpha)
+ return
+
+ if(controller_datum.controller_status & EMERGENCY_STOP)
+ . += mutable_appearance(icon, "estop")
+ . += emissive_appearance(icon, "estop", src, alpha = src.alpha)
+ return
+
+ if(!(controller_datum.controller_status & DOORS_READY))
+ . += mutable_appearance(icon, "doors")
+ . += emissive_appearance(icon, "doors", src, alpha = src.alpha)
+
+ if(controller_datum.controller_active)
+ . += mutable_appearance(icon, "active")
+ . += emissive_appearance(icon, "active", src, alpha = src.alpha)
+
+ if(controller_datum.controller_status & SYSTEM_FAULT)
+ . += mutable_appearance(icon, "fault")
+ . += emissive_appearance(icon, "fault", src, alpha = src.alpha)
+
+ else if(controller_datum.controller_status & COMM_ERROR)
+ . += mutable_appearance(icon, "comms")
+ . += emissive_appearance(icon, "comms", src, alpha = src.alpha)
+
+ else
+ . += mutable_appearance(icon, "normal")
+ . += emissive_appearance(icon, "normal", src, alpha = src.alpha)
+
+/**
+ * Find the controller associated with the transport module the cabinet is sitting on.
+ */
+/obj/machinery/transport/tram_controller/proc/find_controller()
+ var/obj/structure/transport/linear/tram/tram_structure = locate() in src.loc
+ if(!tram_structure)
+ return
+
+ controller_datum = tram_structure.transport_controller_datum
+ if(!controller_datum)
+ return
+
+ controller_datum.notify_controller(src)
+ RegisterSignal(SStransport, COMSIG_TRANSPORT_ACTIVE, PROC_REF(sync_controller))
+
+/obj/machinery/transport/tram_controller/hilbert/find_controller()
+ for(var/datum/transport_controller/linear/tram/tram as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
+ if(tram.specific_transport_id == configured_transport_id)
+ controller_datum = tram
+ break
+
+ if(!controller_datum)
+ return
+
+ controller_datum.notify_controller(src)
+ RegisterSignal(SStransport, COMSIG_TRANSPORT_ACTIVE, PROC_REF(sync_controller))
+
+/**
+ * Since the machinery obj is a dumb terminal for the controller datum, sync the display with the status bitfield of the tram
+ */
+/obj/machinery/transport/tram_controller/proc/sync_controller(source, controller, controller_status, travel_direction, destination_platform)
+ use_power(active_power_usage)
+ if(controller != controller_datum)
+ return
+ update_appearance()
+
+/obj/machinery/transport/tram_controller/emag_act(mob/user, obj/item/card/emag/emag_card)
+ if(obj_flags & EMAGGED)
+ balloon_alert(user, "already fried!")
+ return FALSE
+ obj_flags |= EMAGGED
+ cover_locked = FALSE
+ playsound(src, SFX_SPARKS, 100, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ balloon_alert(user, "access controller shorted")
+ return TRUE
+
+/**
+ * Check if the tram was malfunctioning due to the random event, and if so end the event on repair.
+ */
+/obj/machinery/transport/tram_controller/try_fix_machine(obj/machinery/transport/machine, mob/living/user, obj/item/tool)
+ . = ..()
+
+ if(. == FALSE)
+ return
+
+ if(!controller_datum)
+ return
+
+ var/datum/round_event/tram_malfunction/malfunction_event = locate(/datum/round_event/tram_malfunction) in SSevents.running
+ if(malfunction_event)
+ malfunction_event.end()
+
+ if(controller_datum.controller_status & COMM_ERROR)
+ controller_datum.set_status_code(COMM_ERROR, FALSE)
+
+/obj/machinery/transport/tram_controller/ui_interact(mob/user, datum/tgui/ui)
+ . = ..()
+
+ if(!cover_open && !issiliconoradminghost(user) && !isobserver(user))
+ return
+
+ if(!is_operational)
+ return
+
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "TramController")
+ ui.open()
+
+/obj/machinery/transport/tram_controller/ui_data(mob/user)
+ var/list/data = list()
+
+ data = list(
+ "transportId" = controller_datum.specific_transport_id,
+ "controllerActive" = controller_datum.controller_active,
+ "controllerOperational" = controller_datum.controller_operational,
+ "travelDirection" = controller_datum.travel_direction,
+ "destinationPlatform" = controller_datum.destination_platform,
+ "idlePlatform" = controller_datum.idle_platform,
+ "recoveryMode" = controller_datum.recovery_mode,
+ "currentSpeed" = controller_datum.current_speed,
+ "currentLoad" = controller_datum.current_load,
+ "statusSF" = controller_datum.controller_status & SYSTEM_FAULT,
+ "statusCE" = controller_datum.controller_status & COMM_ERROR,
+ "statusES" = controller_datum.controller_status & EMERGENCY_STOP,
+ "statusPD" = controller_datum.controller_status & PRE_DEPARTURE,
+ "statusDR" = controller_datum.controller_status & DOORS_READY,
+ "statusCL" = controller_datum.controller_status & CONTROLS_LOCKED,
+ "statusBS" = controller_datum.controller_status & BYPASS_SENSORS,
+ )
+
+ return data
+
+/obj/machinery/transport/tram_controller/ui_static_data(mob/user)
+ var/list/data = list()
+ data["destinations"] = SStransport.detailed_destination_list(controller_datum.specific_transport_id)
+
+ return data
+
+/obj/machinery/transport/tram_controller/ui_act(action, params)
+ . = ..()
+ if (.)
+ return
+
+ if(!COOLDOWN_FINISHED(src, manual_command_cooldown))
+ return
+
+ switch(action)
+
+ if("dispatch")
+ var/obj/effect/landmark/transport/nav_beacon/tram/platform/destination_platform
+ for (var/obj/effect/landmark/transport/nav_beacon/tram/platform/destination as anything in SStransport.nav_beacons[controller_datum.specific_transport_id])
+ if(destination.name == params["tripDestination"])
+ destination_platform = destination
+ break
+
+ if(!destination_platform)
+ return FALSE
+
+ SEND_SIGNAL(src, COMSIG_TRANSPORT_REQUEST, controller_datum.specific_transport_id, destination_platform.platform_code)
+ update_appearance()
+
+ if("estop")
+ controller_datum.estop()
+
+ if("reset")
+ controller_datum.reset_position()
+
+ if("dclose")
+ controller_datum.cycle_doors(CYCLE_CLOSED)
+
+ if("dopen")
+ controller_datum.cycle_doors(CYCLE_OPEN)
+
+ if("togglesensors")
+ if(controller_datum.controller_status & BYPASS_SENSORS)
+ controller_datum.set_status_code(BYPASS_SENSORS, FALSE)
+ else
+ controller_datum.set_status_code(BYPASS_SENSORS, TRUE)
+
+ COOLDOWN_START(src, manual_command_cooldown, 2 SECONDS)
+
+/obj/item/wallframe/tram/controller
+ name = "tram controller cabinet"
+ desc = "A box that contains the equipment to control a tram. Just secure to the tram wall."
+ icon = 'icons/obj/tram/tram_controllers.dmi'
+ icon_state = "controller-panel"
+ custom_materials = list(/datum/material/titanium = SHEET_MATERIAL_AMOUNT * 4, /datum/material/iron = SHEET_MATERIAL_AMOUNT * 2, /datum/material/glass = SHEET_MATERIAL_AMOUNT * 2)
+ result_path = /obj/machinery/transport/tram_controller
+ pixel_shift = 32
diff --git a/code/modules/transport/tram/tram_controls.dm b/code/modules/transport/tram/tram_controls.dm
new file mode 100644
index 0000000000000..2ecdad304bbc6
--- /dev/null
+++ b/code/modules/transport/tram/tram_controls.dm
@@ -0,0 +1,248 @@
+/obj/machinery/computer/tram_controls
+ name = "tram controls"
+ desc = "An interface for the tram that lets you tell the tram where to go and hopefully it makes it there. I'm here to describe the controls to you, not to inspire confidence."
+ icon_state = "tram_controls"
+ base_icon_state = "tram"
+ icon_screen = TRAMSTATION_LINE_1
+ icon_keyboard = null
+ layer = SIGN_LAYER
+ density = FALSE
+ max_integrity = 400
+ integrity_failure = 0.1
+ armor_type = /datum/armor/transport_machinery
+ circuit = /obj/item/circuitboard/computer/tram_controls
+ light_color = COLOR_BLUE_LIGHT
+ light_range = 0 //we dont want to spam SSlighting with source updates every movement
+ brightness_on = 0
+ /// What sign face prefixes we have icons for
+ var/static/list/available_faces = list()
+ /// The sign face we're displaying
+ var/sign_face
+ /// Weakref to the tram piece we control
+ var/datum/weakref/transport_ref
+ /// The ID of the tram we're controlling
+ var/specific_transport_id = TRAMSTATION_LINE_1
+ /// If the sign is adjusted for split type tram windows
+ var/split_mode = FALSE
+
+/obj/machinery/computer/tram_controls/split
+ circuit = /obj/item/circuitboard/computer/tram_controls/split
+ split_mode = TRUE
+
+/obj/machinery/computer/tram_controls/split/directional/north
+ dir = SOUTH
+ pixel_x = -8
+ pixel_y = 32
+
+/obj/machinery/computer/tram_controls/split/directional/south
+ dir = NORTH
+ pixel_x = 8
+ pixel_y = -32
+
+/obj/machinery/computer/tram_controls/split/directional/east
+ dir = WEST
+ pixel_x = 32
+
+/obj/machinery/computer/tram_controls/split/directional/west
+ dir = EAST
+ pixel_x = -32
+
+/obj/machinery/computer/tram_controls/Initialize(mapload)
+ . = ..()
+ var/obj/item/circuitboard/computer/tram_controls/my_circuit = circuit
+ split_mode = my_circuit.split_mode
+
+/obj/machinery/computer/tram_controls/LateInitialize()
+ . = ..()
+ if(!id_tag)
+ id_tag = assign_random_name()
+ SStransport.hello(src, name, id_tag)
+ RegisterSignal(SStransport, COMSIG_TRANSPORT_RESPONSE, PROC_REF(call_response))
+ find_tram()
+
+ var/datum/transport_controller/linear/tram/tram = transport_ref?.resolve()
+ if(tram)
+ RegisterSignal(SStransport, COMSIG_TRANSPORT_ACTIVE, PROC_REF(update_display))
+
+/**
+ * Finds the tram from the console
+ *
+ * Locates tram parts in the lift global list after everything is done.
+ */
+/obj/machinery/computer/tram_controls/proc/find_tram()
+ for(var/datum/transport_controller/linear/transport as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
+ if(transport.specific_transport_id == specific_transport_id)
+ transport_ref = WEAKREF(transport)
+ return
+
+/obj/machinery/computer/tram_controls/ui_state(mob/user)
+ return GLOB.not_incapacitated_state
+
+/obj/machinery/computer/tram_controls/ui_status(mob/user,/datum/tgui/ui)
+ var/datum/transport_controller/linear/tram/tram = transport_ref?.resolve()
+
+ if(tram?.controller_active)
+ return UI_CLOSE
+ if(!in_range(user, src) && !isobserver(user))
+ return UI_CLOSE
+ return ..()
+
+/obj/machinery/computer/tram_controls/ui_interact(mob/user, datum/tgui/ui)
+ . = ..()
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "TramControl", name)
+ ui.open()
+
+/obj/machinery/computer/tram_controls/ui_data(mob/user)
+ var/datum/transport_controller/linear/tram/tram_controller = transport_ref?.resolve()
+ var/list/data = list()
+ data["moving"] = tram_controller?.controller_active
+ data["broken"] = (tram_controller ? FALSE : TRUE) || (tram_controller?.paired_cabinet ? FALSE : TRUE)
+ var/obj/effect/landmark/transport/nav_beacon/tram/platform/current_loc = tram_controller?.idle_platform
+ if(current_loc)
+ data["tram_location"] = current_loc.name
+ return data
+
+/obj/machinery/computer/tram_controls/ui_static_data(mob/user)
+ var/list/data = list()
+ data["destinations"] = get_destinations()
+ return data
+
+/**
+ * Finds the destinations for the tram console gui
+ *
+ * Pulls tram landmarks from the landmark gobal list
+ * and uses those to show the proper icons and destination
+ * names for the tram console gui.
+ */
+/obj/machinery/computer/tram_controls/proc/get_destinations()
+ . = list()
+ for(var/obj/effect/landmark/transport/nav_beacon/tram/platform/destination as anything in SStransport.nav_beacons[specific_transport_id])
+ var/list/this_destination = list()
+ this_destination["name"] = destination.name
+ this_destination["dest_icons"] = destination.tgui_icons
+ this_destination["id"] = destination.platform_code
+ . += list(this_destination)
+
+/obj/machinery/computer/tram_controls/ui_act(action, params)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("send")
+ var/obj/effect/landmark/transport/nav_beacon/tram/platform/destination_platform
+ for(var/obj/effect/landmark/transport/nav_beacon/tram/platform/destination as anything in SStransport.nav_beacons[specific_transport_id])
+ if(destination.platform_code == params["destination"])
+ destination_platform = destination
+ break
+
+ if(isnull(destination_platform))
+ return FALSE
+
+ SStransport.incoming_request(src, specific_transport_id, destination_platform.platform_code)
+ update_appearance()
+
+/obj/machinery/computer/tram_controls/proc/update_display(datum/source, datum/transport_controller/linear/tram/controller, controller_active, controller_status, travel_direction, obj/effect/landmark/transport/nav_beacon/tram/platform/destination_platform)
+ SIGNAL_HANDLER
+
+ if(machine_stat & (NOPOWER|BROKEN))
+ icon_screen = null
+ update_appearance()
+ return
+
+ if(isnull(controller) || !controller.controller_operational)
+ icon_screen = "[base_icon_state]_broken"
+ update_appearance()
+ return
+
+ if(isnull(destination_platform))
+ icon_screen = "[specific_transport_id]"
+ update_appearance()
+ return
+
+ if(controller.controller_status & EMERGENCY_STOP || controller.controller_status & SYSTEM_FAULT)
+ icon_screen = "[base_icon_state]_NIS"
+ update_appearance()
+ return
+
+ if(controller_active)
+ icon_screen = "[base_icon_state]_0[travel_direction]"
+ update_appearance()
+ return
+
+ icon_screen = ""
+ icon_screen += "[controller.specific_transport_id]"
+ icon_screen += "[destination_platform.platform_code]"
+
+ update_appearance()
+
+/obj/machinery/computer/tram_controls/on_construction(mob/user)
+ . = ..()
+ var/obj/item/circuitboard/computer/tram_controls/my_circuit = circuit
+ split_mode = my_circuit.split_mode
+ if(split_mode)
+ switch(dir)
+ if(NORTH)
+ pixel_x = 8
+ pixel_y = -32
+ if(SOUTH)
+ pixel_x = -8
+ pixel_y = 32
+ if(EAST)
+ pixel_x = -32
+ pixel_y = -8
+ if(WEST)
+ pixel_x = 32
+ pixel_y = 8
+ else
+ switch(dir)
+ if(NORTH)
+ pixel_y = -32
+ if(SOUTH)
+ pixel_y = 32
+ if(EAST)
+ pixel_x = -32
+ if(WEST)
+ pixel_x = 32
+
+/obj/machinery/computer/tram_controls/update_overlays()
+ . = ..()
+
+ if(isnull(icon_screen))
+ return
+
+ . += emissive_appearance(icon, icon_screen, src, alpha = src.alpha)
+
+/obj/machinery/computer/tram_controls/power_change()
+ ..()
+ var/datum/transport_controller/linear/tram/tram = transport_ref?.resolve()
+ if(isnull(tram))
+ icon_screen = "[base_icon_state]_broken"
+ update_appearance()
+ return
+
+ update_display(src, tram, tram.controller_active, tram.controller_status, tram.travel_direction, tram.destination_platform)
+
+/obj/machinery/computer/tram_controls/proc/call_response(controller, list/relevant, response_code, response_info)
+ SIGNAL_HANDLER
+ switch(response_code)
+ if(REQUEST_SUCCESS)
+ say("The next station is: [response_info]")
+
+ if(REQUEST_FAIL)
+ if(!LAZYFIND(relevant, src))
+ return
+
+ switch(response_info)
+ if(NOT_IN_SERVICE)
+ say("The tram is not in service. Please contact the nearest engineer.")
+ if(INVALID_PLATFORM)
+ say("Configuration error. Please contact the nearest engineer.")
+ if(INTERNAL_ERROR)
+ say("Tram controller error. Please contact the nearest engineer or crew member with telecommunications access to reset the controller.")
+ else
+ return
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/computer/tram_controls, 32)
diff --git a/code/modules/transport/tram/tram_displays.dm b/code/modules/transport/tram/tram_displays.dm
new file mode 100644
index 0000000000000..908651f3b1122
--- /dev/null
+++ b/code/modules/transport/tram/tram_displays.dm
@@ -0,0 +1,166 @@
+/obj/machinery/transport/destination_sign
+ name = "destination sign"
+ desc = "A display to show you what direction the tram is travelling."
+ icon = 'icons/obj/tram/tram_display.dmi'
+ icon_state = "desto_blank"
+ base_icon_state = "desto"
+ use_power = NO_POWER_USE
+ idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 1.2
+ active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 0.47
+ anchored = TRUE
+ density = FALSE
+ layer = SIGN_LAYER
+ light_range = 0
+ /// What sign face prefixes we have icons for
+ var/static/list/available_faces = list()
+ /// The sign face we're displaying
+ var/sign_face
+ var/sign_color = COLOR_DISPLAY_BLUE
+
+/obj/machinery/transport/destination_sign/split/north
+ pixel_x = -8
+
+/obj/machinery/transport/destination_sign/split/south
+ pixel_x = 8
+
+/obj/machinery/transport/destination_sign/indicator
+ icon = 'icons/obj/tram/tram_indicator.dmi'
+ icon_state = "indi_blank"
+ base_icon_state = "indi"
+ use_power = IDLE_POWER_USE
+ max_integrity = 50
+ light_range = 2
+ light_power = 0.7
+ light_angle = 115
+ flags_1 = NONE
+
+/obj/item/wallframe/indicator_display
+ name = "indicator display frame"
+ desc = "Used to build tram indicator displays, just secure to the wall."
+ icon_state = "indi_blank"
+ icon = 'icons/obj/tram/tram_indicator.dmi'
+ custom_materials = list(/datum/material/titanium = SHEET_MATERIAL_AMOUNT * 4, /datum/material/iron = SHEET_MATERIAL_AMOUNT * 2, /datum/material/glass = SHEET_MATERIAL_AMOUNT * 2)
+ result_path = /obj/machinery/transport/destination_sign/indicator
+ pixel_shift = 32
+
+/obj/machinery/transport/destination_sign/Initialize(mapload)
+ . = ..()
+ RegisterSignal(SStransport, COMSIG_TRANSPORT_ACTIVE, PROC_REF(update_sign))
+ SStransport.displays += src
+ available_faces = list(
+ TRAMSTATION_LINE_1,
+ )
+ set_light(l_dir = REVERSE_DIR(dir))
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/machinery/transport/destination_sign/Destroy()
+ SStransport.displays -= src
+ . = ..()
+
+/obj/machinery/transport/destination_sign/indicator/setDir(newdir)
+ . = ..()
+ set_light(l_dir = REVERSE_DIR(dir))
+
+/obj/machinery/transport/destination_sign/indicator/LateInitialize(mapload)
+ . = ..()
+ link_tram()
+
+/obj/machinery/transport/destination_sign/indicator/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ if(held_item?.tool_behaviour == TOOL_WRENCH)
+ context[SCREENTIP_CONTEXT_RMB] = "unanchor"
+ if(held_item?.tool_behaviour == TOOL_WELDER)
+ context[SCREENTIP_CONTEXT_LMB] = "repair"
+
+ return CONTEXTUAL_SCREENTIP_SET
+
+
+/obj/machinery/transport/destination_sign/indicator/examine(mob/user)
+ . = ..()
+
+ if(panel_open)
+ . += span_notice("It is secured to the tram wall with [EXAMINE_HINT("bolts.")]")
+
+/obj/machinery/transport/destination_sign/deconstruct(disassembled = TRUE)
+ if(flags_1 & NODECONSTRUCT_1)
+ return
+ if(disassembled)
+ new /obj/item/wallframe/indicator_display(drop_location())
+ else
+ new /obj/item/stack/sheet/mineral/titanium(drop_location(), 2)
+ new /obj/item/stack/sheet/iron(drop_location(), 1)
+ new /obj/item/shard(drop_location())
+ new /obj/item/shard(drop_location())
+ qdel(src)
+
+/obj/machinery/transport/destination_sign/indicator/wrench_act_secondary(mob/living/user, obj/item/tool)
+ . = ..()
+ balloon_alert(user, "[anchored ? "un" : ""]securing...")
+ tool.play_tool_sound(src)
+ if(tool.use_tool(src, user, 6 SECONDS))
+ playsound(loc, 'sound/items/deconstruct.ogg', 50, vary = TRUE)
+ balloon_alert(user, "[anchored ? "un" : ""]secured")
+ deconstruct()
+ return TRUE
+
+/obj/machinery/transport/destination_sign/proc/update_sign(datum/source, datum/transport_controller/linear/tram/controller, controller_active, controller_status, travel_direction, obj/effect/landmark/transport/nav_beacon/tram/platform/destination_platform)
+ SIGNAL_HANDLER
+
+ if(machine_stat & (NOPOWER|BROKEN))
+ sign_face = null
+ update_appearance()
+ return
+
+ if(!controller || !controller.controller_operational || isnull(destination_platform))
+ sign_face = "[base_icon_state]_NIS"
+ sign_color = COLOR_DISPLAY_RED
+ update_appearance()
+ return
+
+ if(controller.controller_status & EMERGENCY_STOP || controller.controller_status & SYSTEM_FAULT)
+ sign_face = "[base_icon_state]_NIS"
+ sign_color = COLOR_DISPLAY_RED
+ update_appearance()
+ return
+
+ sign_face = ""
+ sign_face += "[base_icon_state]_"
+ if(!LAZYFIND(available_faces, controller.specific_transport_id))
+ sign_face += "[TRAMSTATION_LINE_1]"
+ else
+ sign_face += "[controller.specific_transport_id]"
+
+ sign_face += "[controller_active]"
+ sign_face += "[destination_platform.platform_code]"
+ sign_face += "[travel_direction]"
+ sign_color = COLOR_DISPLAY_BLUE
+
+ update_appearance()
+
+/obj/machinery/transport/destination_sign/update_icon_state()
+ . = ..()
+ if(isnull(sign_face))
+ icon_state = "[base_icon_state]_blank"
+ return
+ else
+ icon_state = sign_face
+
+/obj/machinery/transport/destination_sign/update_overlays()
+ . = ..()
+
+ if(isnull(sign_face))
+ set_light(l_on = FALSE)
+ return
+
+ set_light(l_on = TRUE, l_color = sign_color)
+ . += emissive_appearance(icon, "[sign_face]_e", src, alpha = src.alpha)
+
+/obj/machinery/transport/destination_sign/indicator/power_change()
+ ..()
+ var/datum/transport_controller/linear/tram/tram = transport_ref?.resolve()
+ if(!tram)
+ return
+
+ update_sign(src, tram, tram.controller_active, tram.controller_status, tram.travel_direction, tram.destination_platform)
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/transport/destination_sign/indicator, 32)
diff --git a/code/modules/transport/tram/tram_doors.dm b/code/modules/transport/tram/tram_doors.dm
new file mode 100644
index 0000000000000..d836685acf11f
--- /dev/null
+++ b/code/modules/transport/tram/tram_doors.dm
@@ -0,0 +1,230 @@
+#define TRAM_DOOR_WARNING_TIME (1.4 SECONDS)
+#define TRAM_DOOR_CYCLE_TIME (0.4 SECONDS)
+#define TRAM_DOOR_CRUSH_TIME (0.7 SECONDS)
+#define TRAM_DOOR_RECYCLE_TIME (3 SECONDS)
+
+/obj/machinery/door/airlock/tram
+ name = "tram door"
+ icon = 'icons/obj/doors/airlocks/tram/tram.dmi'
+ overlays_file = 'icons/obj/doors/airlocks/tram/tram-overlays.dmi'
+ multi_tile = TRUE
+ opacity = FALSE
+ assemblytype = null
+ airlock_material = "glass"
+ air_tight = TRUE
+ req_access = list(ACCESS_TCOMMS)
+ transport_linked_id = TRAMSTATION_LINE_1
+ doorOpen = 'sound/machines/tramopen.ogg'
+ doorClose = 'sound/machines/tramclose.ogg'
+ autoclose = FALSE
+ /// Weakref to the tram we're attached
+ var/datum/weakref/transport_ref
+ var/retry_counter
+ var/crushing_in_progress = FALSE
+ bound_width = 64
+
+/obj/machinery/door/airlock/tram/Initialize(mapload)
+ . = ..()
+ if(!id_tag)
+ id_tag = assign_random_name()
+
+/obj/machinery/door/airlock/tram/open(forced = DEFAULT_DOOR_CHECKS)
+ if(operating || welded || locked || seal)
+ return FALSE
+
+ if(!density)
+ return TRUE
+
+ if(forced == DEFAULT_DOOR_CHECKS && (!hasPower() || wires.is_cut(WIRE_OPEN)))
+ return FALSE
+
+ SEND_SIGNAL(src, COMSIG_AIRLOCK_OPEN, FALSE)
+ operating = TRUE
+ update_icon(ALL, AIRLOCK_OPENING, TRUE)
+
+ if(forced >= BYPASS_DOOR_CHECKS)
+ playsound(src, 'sound/machines/airlockforced.ogg', vol = 40, vary = FALSE)
+ sleep(TRAM_DOOR_CYCLE_TIME)
+ else
+ playsound(src, doorOpen, vol = 40, vary = FALSE)
+ sleep(TRAM_DOOR_WARNING_TIME)
+
+ set_density(FALSE)
+ if(!isnull(filler))
+ filler.set_density(FALSE)
+ update_freelook_sight()
+ flags_1 &= ~PREVENT_CLICK_UNDER_1
+ air_update_turf(TRUE, FALSE)
+ sleep(TRAM_DOOR_CYCLE_TIME)
+ layer = OPEN_DOOR_LAYER
+ update_icon(ALL, AIRLOCK_OPEN, TRUE)
+ operating = FALSE
+
+ return TRUE
+
+/obj/machinery/door/airlock/tram/close(forced = DEFAULT_DOOR_CHECKS, force_crush = FALSE)
+ retry_counter++
+ if(retry_counter >= 4 || force_crush || forced == BYPASS_DOOR_CHECKS)
+ try_to_close(forced = BYPASS_DOOR_CHECKS)
+ return
+
+ if(retry_counter == 1)
+ playsound(src, 'sound/machines/chime.ogg', 40, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+
+ addtimer(CALLBACK(src, PROC_REF(verify_status)), TRAM_DOOR_RECYCLE_TIME)
+ try_to_close()
+
+/**
+ * Perform a close attempt and report TRUE/FALSE if it worked
+ *
+ * Arguments:
+ * * rapid - boolean: if TRUE will skip safety checks and crush whatever is in the way
+ */
+/obj/machinery/door/airlock/tram/proc/try_to_close(forced = DEFAULT_DOOR_CHECKS)
+ if(operating || welded || locked || seal)
+ return FALSE
+ if(density)
+ return TRUE
+ crushing_in_progress = TRUE
+ var/hungry_door = (forced == BYPASS_DOOR_CHECKS || !safe)
+ if((obj_flags & EMAGGED) || !safe)
+ do_sparks(3, TRUE, src)
+ playsound(src, SFX_SPARKS, vol = 75, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ use_power(50)
+ playsound(src, doorClose, vol = 40, vary = FALSE)
+ operating = TRUE
+ layer = CLOSED_DOOR_LAYER
+ update_icon(ALL, AIRLOCK_CLOSING, 1)
+ sleep(TRAM_DOOR_WARNING_TIME)
+ if(!hungry_door)
+ for(var/turf/checked_turf in locs)
+ for(var/atom/movable/blocker in checked_turf)
+ if(blocker.density && blocker != src) //something is blocking the door
+ say("Please stand clear of the doors!")
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 60, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ layer = OPEN_DOOR_LAYER
+ update_icon(ALL, AIRLOCK_OPEN, 1)
+ operating = FALSE
+ return FALSE
+ SEND_SIGNAL(src, COMSIG_AIRLOCK_CLOSE)
+ sleep(TRAM_DOOR_CRUSH_TIME)
+ set_density(TRUE)
+ if(!isnull(filler))
+ filler.set_density(TRUE)
+ update_freelook_sight()
+ flags_1 |= PREVENT_CLICK_UNDER_1
+ air_update_turf(TRUE, TRUE)
+ crush()
+ crushing_in_progress = FALSE
+ sleep(TRAM_DOOR_CYCLE_TIME)
+ update_icon(ALL, AIRLOCK_CLOSED, 1)
+ operating = FALSE
+ retry_counter = 0
+ return TRUE
+
+/**
+ * Crush the jerk holding up the tram from moving
+ *
+ * Tram doors need their own crush proc because the normal one
+ * leaves you stunned far too long, leading to the doors crushing
+ * you over and over, no escape!
+ *
+ * While funny to watch, not ideal for the player.
+ */
+/obj/machinery/door/airlock/tram/crush()
+ for(var/turf/checked_turf in locs)
+ for(var/mob/living/future_pancake in checked_turf)
+ future_pancake.visible_message(span_warning("[src] beeps angrily and closes on [future_pancake]!"), span_userdanger("[src] beeps angrily and closes on you!"))
+ SEND_SIGNAL(future_pancake, COMSIG_LIVING_DOORCRUSHED, src)
+ if(ishuman(future_pancake) || ismonkey(future_pancake))
+ future_pancake.emote("scream")
+ future_pancake.adjustBruteLoss(DOOR_CRUSH_DAMAGE * 2)
+ future_pancake.Paralyze(2 SECONDS)
+
+ else //for simple_animals & borgs
+ future_pancake.adjustBruteLoss(DOOR_CRUSH_DAMAGE * 2)
+ var/turf/location = get_turf(src)
+ //add_blood doesn't work for borgs/xenos, but add_blood_floor does.
+ future_pancake.add_splatter_floor(location)
+
+ log_combat(src, future_pancake, "crushed")
+
+ for(var/obj/vehicle/sealed/mecha/mech in checked_turf) // Your fancy metal won't save you here!
+ mech.take_damage(DOOR_CRUSH_DAMAGE)
+ log_combat(src, mech, "crushed")
+
+/**
+ * Checks if the door close action was successful. Retries if it failed
+ *
+ * If some jerk is blocking the doors, they've had enough warning by attempt 3,
+ * take a chunk of skin, people have places to be!
+ */
+/obj/machinery/door/airlock/tram/proc/verify_status()
+ if(airlock_state == AIRLOCK_CLOSED)
+ return
+
+ if(retry_counter < 3)
+ close()
+ return
+
+ playsound(src, 'sound/machines/buzz-two.ogg', 60, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ say("YOU'RE HOLDING UP THE TRAM, ASSHOLE!")
+ close(forced = BYPASS_DOOR_CHECKS)
+
+/**
+ * Set the weakref for the tram we're attached to
+ */
+/obj/machinery/door/airlock/tram/proc/find_tram()
+ for(var/datum/transport_controller/linear/tram/tram as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
+ if(tram.specific_transport_id == transport_linked_id)
+ transport_ref = WEAKREF(tram)
+
+/obj/machinery/door/airlock/tram/Initialize(mapload, set_dir, unres_sides)
+ . = ..()
+ RemoveElement(/datum/element/atmos_sensitive, mapload)
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/machinery/door/airlock/tram/LateInitialize(mapload)
+ . = ..()
+ INVOKE_ASYNC(src, PROC_REF(open))
+ SStransport.doors += src
+ find_tram()
+
+/obj/machinery/door/airlock/tram/Destroy()
+ SStransport.doors -= src
+ return ..()
+
+/**
+ * Tram doors can be opened with hands when unpowered
+ */
+/obj/machinery/door/airlock/tram/examine(mob/user)
+ . = ..()
+ . += span_notice("It has an emergency mechanism to open using [EXAMINE_HINT("just your hands")] in the event of an emergency.")
+
+/**
+ * Tram doors can be opened with hands when unpowered
+ */
+/obj/machinery/door/airlock/tram/try_safety_unlock(mob/user)
+ if(!hasPower() && density)
+ balloon_alert(user, "pulling emergency exit...")
+ if(do_after(user, 4 SECONDS, target = src))
+ try_to_crowbar(null, user, TRUE)
+ return TRUE
+
+/**
+ * If you pry (bump) the doors open midtravel, open quickly so you can jump out and make a daring escape.
+ */
+/obj/machinery/door/airlock/tram/bumpopen(mob/user, forced = BYPASS_DOOR_CHECKS)
+ if(operating || !density)
+ return
+ var/datum/transport_controller/linear/tram/tram_part = transport_ref?.resolve()
+ add_fingerprint(user)
+ if((tram_part.travel_remaining < DEFAULT_TRAM_LENGTH || tram_part.travel_remaining > tram_part.travel_trip_length - DEFAULT_TRAM_LENGTH) && tram_part.controller_active)
+ return // we're already animating, don't reset that
+ open(forced = BYPASS_DOOR_CHECKS)
+ return
+
+#undef TRAM_DOOR_WARNING_TIME
+#undef TRAM_DOOR_CYCLE_TIME
+#undef TRAM_DOOR_CRUSH_TIME
+#undef TRAM_DOOR_RECYCLE_TIME
diff --git a/code/modules/transport/tram/tram_floors.dm b/code/modules/transport/tram/tram_floors.dm
new file mode 100644
index 0000000000000..2afb59f9b4a6e
--- /dev/null
+++ b/code/modules/transport/tram/tram_floors.dm
@@ -0,0 +1,322 @@
+/turf/open/floor/noslip/tram
+ name = "high-traction tram platform"
+ icon = 'icons/turf/tram.dmi'
+ icon_state = "noslip_tram"
+ base_icon_state = "noslip_tram"
+ floor_tile = /obj/item/stack/tile/noslip/tram
+
+/turf/open/floor/tram
+ name = "tram guideway"
+ icon = 'icons/turf/tram.dmi'
+ icon_state = "tram_platform"
+ base_icon_state = "tram_platform"
+ floor_tile = /obj/item/stack/tile/tram
+ footstep = FOOTSTEP_CATWALK
+ barefootstep = FOOTSTEP_HARD_BAREFOOT
+ clawfootstep = FOOTSTEP_HARD_CLAW
+ heavyfootstep = FOOTSTEP_GENERIC_HEAVY
+ tiled_dirt = FALSE
+ rcd_proof = TRUE
+
+/turf/open/floor/tram/examine(mob/user)
+ . += ..()
+ . += span_notice("The reinforcement bolts are [EXAMINE_HINT("wrenched")] firmly in place. Use a [EXAMINE_HINT("wrench")] to remove the plate.")
+
+/turf/open/floor/tram/attackby(obj/item/object, mob/living/user, params)
+ . = ..()
+ if(istype(object, /obj/item/stack/thermoplastic))
+ build_with_transport_tiles(object, user)
+ else if(istype(object, /obj/item/stack/sheet/mineral/titanium))
+ build_with_titanium(object, user)
+
+/turf/open/floor/tram/make_plating(force = FALSE)
+ if(force)
+ return ..()
+ return //unplateable
+
+/turf/open/floor/tram/try_replace_tile(obj/item/stack/tile/replacement_tile, mob/user, params)
+ return
+
+/turf/open/floor/tram/crowbar_act(mob/living/user, obj/item/item)
+ return
+
+/turf/open/floor/tram/wrench_act(mob/living/user, obj/item/item)
+ ..()
+ to_chat(user, span_notice("You begin removing the plate..."))
+ if(item.use_tool(src, user, 30, volume=80))
+ if(!istype(src, /turf/open/floor/tram))
+ return TRUE
+ if(floor_tile)
+ new floor_tile(src, 2)
+ ScrapeAway(flags = CHANGETURF_INHERIT_AIR)
+ return TRUE
+
+/turf/open/floor/tram/ex_act(severity, target)
+ if(target == src)
+ ScrapeAway(flags = CHANGETURF_INHERIT_AIR)
+ return TRUE
+ if(severity < EXPLODE_DEVASTATE && is_shielded())
+ return FALSE
+
+ switch(severity)
+ if(EXPLODE_DEVASTATE)
+ if(prob(80))
+ if(!ispath(baseturf_at_depth(2), /turf/open/floor))
+ attempt_lattice_replacement()
+ else
+ ScrapeAway(2, flags = CHANGETURF_INHERIT_AIR)
+ else
+ break_tile()
+ if(EXPLODE_HEAVY)
+ if(prob(30))
+ if(!ispath(baseturf_at_depth(2), /turf/open/floor))
+ attempt_lattice_replacement()
+ else
+ ScrapeAway(2, flags = CHANGETURF_INHERIT_AIR)
+ else
+ break_tile()
+ if(EXPLODE_LIGHT)
+ if(prob(50))
+ break_tile()
+
+ return TRUE
+
+/turf/open/floor/tram/broken_states()
+ return list("tram_platform-damaged1","tram_platform-damaged2")
+
+/turf/open/floor/tram/tram_platform/burnt_states()
+ return list("tram_platform-scorched1","tram_platform-scorched2")
+
+/turf/open/floor/tram/plate
+ name = "linear induction plate"
+ desc = "The linear induction plate that powers the tram."
+ icon = 'icons/turf/tram.dmi'
+ icon_state = "tram_plate"
+ base_icon_state = "tram_plate"
+ flags_1 = NONE
+
+/turf/open/floor/tram/plate/broken_states()
+ return list("tram_plate-damaged1","tram_plate-damaged2")
+
+/turf/open/floor/tram/plate/burnt_states()
+ return list("tram_plate-scorched1","tram_plate-scorched2")
+
+/turf/open/floor/tram/plate/energized
+ desc = "The linear induction plate that powers the tram. It is currently energized."
+ /// Inbound station
+ var/inbound
+ /// Outbound station
+ var/outbound
+ /// Transport ID of the tram
+ var/specific_transport_id = TRAMSTATION_LINE_1
+
+/turf/open/floor/tram/plate/energized/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/energized, inbound, outbound, specific_transport_id)
+
+/turf/open/floor/tram/plate/energized/examine(mob/user)
+ . = ..()
+ if(broken || burnt)
+ . += span_danger("It looks damaged and the electrical components exposed!")
+ . += span_notice("The plate can be repaired using a [EXAMINE_HINT("titanium sheet")].")
+
+/turf/open/floor/tram/plate/energized/broken_states()
+ return list("energized_plate_damaged")
+
+/turf/open/floor/tram/plate/energized/burnt_states()
+ return list("energized_plate_damaged")
+
+/turf/open/floor/tram/plate/energized/attackby(obj/item/attacking_item, mob/living/user, params)
+ if((broken || burnt) && istype(attacking_item, /obj/item/stack/sheet/mineral/titanium))
+ if(attacking_item.use(1))
+ broken = FALSE
+ update_appearance()
+ balloon_alert(user, "plate replaced")
+ return
+ return ..()
+
+// Resetting the tram contents to its original state needs the turf to be there
+/turf/open/indestructible/tram
+ name = "tram guideway"
+ icon = 'icons/turf/tram.dmi'
+ icon_state = "tram_platform"
+ base_icon_state = "tram_platform"
+ footstep = FOOTSTEP_CATWALK
+ barefootstep = FOOTSTEP_HARD_BAREFOOT
+ clawfootstep = FOOTSTEP_HARD_CLAW
+ heavyfootstep = FOOTSTEP_GENERIC_HEAVY
+
+/turf/open/indestructible/tram/attackby(obj/item/object, mob/living/user, params)
+ . = ..()
+ if(istype(object, /obj/item/stack/thermoplastic))
+ build_with_transport_tiles(object, user)
+ else if(istype(object, /obj/item/stack/sheet/mineral/titanium))
+ build_with_titanium(object, user)
+
+/turf/open/indestructible/tram/plate
+ name = "linear induction plate"
+ desc = "The linear induction plate that powers the tram."
+ icon_state = "tram_plate"
+ base_icon_state = "tram_plate"
+ flags_1 = NONE
+
+/turf/open/floor/glass/reinforced/tram/Initialize(mapload)
+ . = ..()
+ RemoveElement(/datum/element/atmos_sensitive, mapload)
+
+/turf/open/floor/glass/reinforced/tram
+ name = "tram bridge"
+ desc = "It shakes a bit when you step, but lets you cross between sides quickly!"
+
+/obj/structure/thermoplastic
+ name = "tram floor"
+ desc = "A lightweight thermoplastic flooring."
+ icon = 'icons/turf/tram.dmi'
+ icon_state = "tram_dark"
+ base_icon_state = "tram_dark"
+ density = FALSE
+ anchored = TRUE
+ max_integrity = 150
+ integrity_failure = 0.75
+ armor_type = /datum/armor/tram_floor
+ layer = TRAM_FLOOR_LAYER
+ plane = FLOOR_PLANE
+ obj_flags = BLOCK_Z_OUT_DOWN | BLOCK_Z_OUT_UP
+ appearance_flags = PIXEL_SCALE|KEEP_TOGETHER
+ var/secured = TRUE
+ var/floor_tile = /obj/item/stack/thermoplastic
+ var/mutable_appearance/damage_overlay
+
+/datum/armor/tram_floor
+ melee = 40
+ bullet = 10
+ laser = 10
+ bomb = 45
+ fire = 90
+ acid = 100
+
+/obj/structure/thermoplastic/light
+ icon_state = "tram_light"
+ base_icon_state = "tram_light"
+ floor_tile = /obj/item/stack/thermoplastic/light
+
+/obj/structure/thermoplastic/examine(mob/user)
+ . = ..()
+
+ if(secured)
+ . += span_notice("It is secured with a set of [EXAMINE_HINT("screws.")] To remove tile use a [EXAMINE_HINT("screwdriver.")]")
+ else
+ . += span_notice("You can [EXAMINE_HINT("crowbar")] to remove the tile.")
+ . += span_notice("It can be re-secured using a [EXAMINE_HINT("screwdriver.")]")
+
+/obj/structure/thermoplastic/take_damage(damage_amount, damage_type = BRUTE, damage_flag = "", sound_effect = TRUE, attack_dir, armour_penetration = 0)
+ . = ..()
+ if(.) //received damage
+ update_appearance()
+
+/obj/structure/thermoplastic/update_icon_state()
+ . = ..()
+ var/ratio = atom_integrity / max_integrity
+ ratio = CEILING(ratio * 4, 1) * 25
+ if(ratio > 75)
+ icon_state = base_icon_state
+ return
+
+ icon_state = "[base_icon_state]_damage[ratio]"
+
+/obj/structure/thermoplastic/screwdriver_act_secondary(mob/living/user, obj/item/tool)
+ . = ..()
+ if(secured)
+ user.visible_message(span_notice("[user] begins to unscrew the tile..."),
+ span_notice("You begin to unscrew the tile..."))
+ if(tool.use_tool(src, user, 1 SECONDS, volume = 50))
+ secured = FALSE
+ to_chat(user, span_notice("The screws come out, and a gap forms around the edge of the tile."))
+ else
+ user.visible_message(span_notice("[user] begins to fasten the tile..."),
+ span_notice("You begin to fasten the tile..."))
+ if(tool.use_tool(src, user, 1 SECONDS, volume = 50))
+ secured = TRUE
+ to_chat(user, span_notice("The tile is securely screwed in place."))
+
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/structure/thermoplastic/crowbar_act_secondary(mob/living/user, obj/item/tool)
+ . = ..()
+ if(secured)
+ to_chat(user, span_warning("The security screws need to be removed first!"))
+ return FALSE
+
+ else
+ user.visible_message(span_notice("[user] wedges \the [tool] into the tile's gap in the edge and starts prying..."),
+ span_notice("You wedge \the [tool] into the tram panel's gap in the frame and start prying..."))
+ if(tool.use_tool(src, user, 1 SECONDS, volume = 50))
+ to_chat(user, span_notice("The panel pops out of the frame."))
+ var/obj/item/stack/thermoplastic/pulled_tile = new()
+ pulled_tile.update_integrity(atom_integrity)
+ user.put_in_hands(pulled_tile)
+ qdel(src)
+
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/structure/thermoplastic/welder_act(mob/living/user, obj/item/tool)
+ if(atom_integrity >= max_integrity)
+ to_chat(user, span_warning("[src] is already in good condition!"))
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+ if(!tool.tool_start_check(user, amount = 0))
+ return FALSE
+ to_chat(user, span_notice("You begin repairing [src]..."))
+ var/integrity_to_repair = max_integrity - atom_integrity
+ if(tool.use_tool(src, user, integrity_to_repair * 0.5, volume = 50))
+ atom_integrity = max_integrity
+ to_chat(user, span_notice("You repair [src]."))
+ update_appearance()
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/item/stack/thermoplastic
+ name = "thermoplastic tram tile"
+ singular_name = "thermoplastic tram tile"
+ desc = "A high-traction floor tile. It sparkles in the light."
+ icon = 'icons/obj/tiles.dmi'
+ lefthand_file = 'icons/mob/inhands/items/tiles_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/items/tiles_righthand.dmi'
+ icon_state = "tile_textured_white_large"
+ inhand_icon_state = "tile-neon-glow"
+ color = COLOR_TRAM_BLUE
+ w_class = WEIGHT_CLASS_NORMAL
+ force = 1
+ throwforce = 1
+ throw_speed = 3
+ throw_range = 7
+ max_amount = 60
+ novariants = TRUE
+ merge_type = /obj/item/stack/thermoplastic
+ var/tile_type = /obj/structure/thermoplastic
+
+/obj/item/stack/thermoplastic/light
+ color = COLOR_TRAM_LIGHT_BLUE
+ tile_type = /obj/structure/thermoplastic/light
+
+/obj/item/stack/thermoplastic/Initialize(mapload, new_amount, merge = TRUE, list/mat_override=null, mat_amt=1)
+ . = ..()
+ pixel_x = rand(-3, 3)
+ pixel_y = rand(-3, 3) //randomize a little
+
+/obj/item/stack/thermoplastic/examine(mob/user)
+ . = ..()
+ if(throwforce && !is_cyborg) //do not want to divide by zero or show the message to borgs who can't throw
+ var/damage_value
+ switch(CEILING(MAX_LIVING_HEALTH / throwforce, 1)) //throws to crit a human
+ if(1 to 3)
+ damage_value = "superb"
+ if(4 to 6)
+ damage_value = "great"
+ if(7 to 9)
+ damage_value = "good"
+ if(10 to 12)
+ damage_value = "fairly decent"
+ if(13 to 15)
+ damage_value = "mediocre"
+ if(!damage_value)
+ return
+ . += span_notice("Those could work as a [damage_value] throwing weapon.")
diff --git a/code/modules/transport/tram/tram_machinery.dm b/code/modules/transport/tram/tram_machinery.dm
new file mode 100644
index 0000000000000..7371447d08244
--- /dev/null
+++ b/code/modules/transport/tram/tram_machinery.dm
@@ -0,0 +1,106 @@
+/obj/item/assembly/control/transport
+ /// The ID of the tram we're linked to
+ var/specific_transport_id = TRAMSTATION_LINE_1
+ /// Options to be passed with the requests to the transport subsystem
+ var/options = NONE
+
+/obj/item/assembly/control/transport/multitool_act(mob/living/user)
+ var/list/available_platforms = list()
+ for(var/obj/effect/landmark/transport/nav_beacon/tram/platform/platform as anything in SStransport.nav_beacons[specific_transport_id])
+ LAZYADD(available_platforms, platform.name)
+
+ var/selected_platform = tgui_input_list(user, "Set the platform ID", "Platform", available_platforms)
+ var/obj/effect/landmark/transport/nav_beacon/tram/platform/change_platform
+ for(var/obj/effect/landmark/transport/nav_beacon/tram/platform/destination as anything in SStransport.nav_beacons[specific_transport_id])
+ if(destination.name == selected_platform)
+ change_platform = destination
+ break
+
+ if(!change_platform || QDELETED(user) || QDELETED(src) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
+ return
+
+ if(get_dist(change_platform, src) > 15)
+ balloon_alert(user, "out of range!")
+ return
+
+ id = change_platform.platform_code
+ balloon_alert(user, "platform changed")
+ to_chat(user, span_notice("You change the platform ID to [change_platform.name]."))
+
+/obj/item/assembly/control/transport/call_button
+ name = "tram call button"
+ desc = "A small device used to bring trams to you."
+ ///ID to link to allow us to link to one specific tram in the world
+ id = 0
+
+/obj/item/assembly/control/transport/call_button/Initialize(mapload)
+ . = ..()
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/item/assembly/control/transport/call_button/LateInitialize(mapload)
+ . = ..()
+ if(!id_tag)
+ id_tag = assign_random_name()
+ SStransport.hello(src, name, id_tag)
+ RegisterSignal(SStransport, COMSIG_TRANSPORT_RESPONSE, PROC_REF(call_response))
+
+/obj/item/assembly/control/transport/proc/call_response(controller, list/relevant, response_code, response_info)
+ SIGNAL_HANDLER
+ if(!LAZYFIND(relevant, src))
+ return
+
+ switch(response_code)
+ if(REQUEST_SUCCESS)
+ say("The tram has been called to the platform.")
+
+ if(REQUEST_FAIL)
+ switch(response_info)
+ if(BROKEN_BEYOND_REPAIR)
+ say("The tram has suffered a catastrophic failure. Please seek alternate modes of travel.")
+ if(NOT_IN_SERVICE) //tram has no power or other fault, but it's not broken forever
+ say("The tram is not in service due to loss of power or system problems. Please contact the nearest engineer to check power and controller.")
+ if(INVALID_PLATFORM) //engineer needs to fix button
+ say("Button configuration error. Please contact the nearest engineer.")
+ if(TRANSPORT_IN_USE)
+ say("The tram is tramversing the station, please wait.")
+ if(INTERNAL_ERROR)
+ say("Tram controller error. Please contact the nearest engineer or crew member with telecommunications access to reset the controller.")
+ if(NO_CALL_REQUIRED) //already here
+ say("The tram is already here. Please board the tram and select a destination.")
+ else
+ say("Tram controller error. Please contact the nearest engineer or crew member with telecommunications access to reset the controller.")
+
+/obj/item/assembly/control/transport/call_button/activate()
+ if(cooldown)
+ return
+ cooldown = TRUE
+ addtimer(VARSET_CALLBACK(src, cooldown, FALSE), 2 SECONDS)
+
+ // INVOKE_ASYNC(SStransport, TYPE_PROC_REF(/datum/controller/subsystem/processing/transport, call_request), src, specific_transport_id, id)
+ SEND_SIGNAL(src, COMSIG_TRANSPORT_REQUEST, specific_transport_id, id)
+
+/obj/machinery/button/transport/tram
+ name = "tram request"
+ desc = "A button for calling the tram. It has a speakerbox in it with some internals."
+ base_icon_state = "tram"
+ icon_state = "tram"
+ light_color = COLOR_DISPLAY_BLUE
+ can_alter_skin = FALSE
+ device_type = /obj/item/assembly/control/transport/call_button
+ req_access = list()
+ id = 0
+ /// The ID of the tram we're linked to
+ var/specific_transport_id = TRAMSTATION_LINE_1
+
+/obj/machinery/button/transport/tram/setup_device()
+ var/obj/item/assembly/control/transport/call_button/tram_device = device
+ tram_device.id = id
+ tram_device.specific_transport_id = specific_transport_id
+ return ..()
+
+/obj/machinery/button/transport/tram/examine(mob/user)
+ . = ..()
+ . += span_notice("There's a small inscription on the button...")
+ . += span_notice("THIS CALLS THE TRAM! IT DOES NOT OPERATE IT! The console on the tram tells it where to go!")
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/button/transport/tram, 32)
diff --git a/code/modules/transport/tram/tram_remote.dm b/code/modules/transport/tram/tram_remote.dm
new file mode 100644
index 0000000000000..08e127bc6b976
--- /dev/null
+++ b/code/modules/transport/tram/tram_remote.dm
@@ -0,0 +1,126 @@
+/obj/item/assembly/control/transport/remote
+ icon_state = "tramremote_nis"
+ inhand_icon_state = "electronic"
+ lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
+ icon = 'icons/obj/device.dmi'
+ name = "tram remote"
+ desc = "A remote control that can be linked to a tram. This can only go well."
+ w_class = WEIGHT_CLASS_TINY
+ options = RAPID_MODE
+ ///desired tram destination
+ var/destination
+ COOLDOWN_DECLARE(tram_remote)
+
+/obj/item/assembly/control/transport/remote/Initialize(mapload)
+ . = ..()
+ if(!id_tag)
+ id_tag = assign_random_name()
+ SStransport.hello(src, name, id_tag)
+ register_context()
+
+/obj/item/assembly/control/transport/remote/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ if(!specific_transport_id)
+ context[SCREENTIP_CONTEXT_LMB] = "Link tram"
+ return CONTEXTUAL_SCREENTIP_SET
+ context[SCREENTIP_CONTEXT_LMB] = "Dispatch tram"
+ context[SCREENTIP_CONTEXT_RMB] = "Select destination"
+ context[SCREENTIP_CONTEXT_CTRL_LMB] = "Toggle door safeties"
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "Change tram"
+ return CONTEXTUAL_SCREENTIP_SET
+
+//set tram destination
+/obj/item/assembly/control/transport/remote/attack_self_secondary(mob/user)
+ var/list/available_platforms = list()
+ for(var/obj/effect/landmark/transport/nav_beacon/tram/platform/platform as anything in SStransport.nav_beacons[specific_transport_id])
+ LAZYADD(available_platforms, platform.name)
+
+ var/selected_platform = tgui_input_list(user, "Available destinations", "Where to?", available_platforms)
+ for(var/obj/effect/landmark/transport/nav_beacon/tram/platform/potential_platform as anything in SStransport.nav_beacons[specific_transport_id])
+ if(potential_platform.name == selected_platform)
+ destination = potential_platform.platform_code
+ break
+
+ balloon_alert(user, "set [selected_platform]")
+ to_chat(user, span_notice("You change the platform ID on [src] to [selected_platform]."))
+
+///set safety bypass
+/obj/item/assembly/control/transport/remote/CtrlClick(mob/user)
+ switch(options)
+ if(!RAPID_MODE)
+ options |= RAPID_MODE
+ if(RAPID_MODE)
+ options &= ~RAPID_MODE
+ update_appearance()
+ balloon_alert(user, "mode: [options ? "fast" : "safe"]")
+
+/obj/item/assembly/control/transport/remote/examine(mob/user)
+ . = ..()
+ if(!specific_transport_id)
+ . += "There is an X showing on the display."
+ . += "Left-click to link to a tram."
+ return
+ . += "The rapid mode light is [options ? "on" : "off"]."
+ if(cooldown)
+ . += "The number on the display shows [DisplayTimeText(cooldown, 1)]."
+ else
+ . += "The display indicates ready."
+ . += "Left-click to dispatch tram."
+ . += "Right-click to set destination."
+ . += "Ctrl-click to toggle safety bypass."
+ . += "Alt-click to change configured tram."
+
+/obj/item/assembly/control/transport/remote/update_icon_state()
+ . = ..()
+
+ if(!specific_transport_id)
+ icon_state = "tramremote_nis"
+ return
+
+ icon_state = "tramremote_ob"
+
+/obj/item/assembly/control/transport/remote/update_overlays()
+ . = ..()
+ if(options & RAPID_MODE)
+ . += mutable_appearance(icon, "tramremote_emag")
+
+/obj/item/assembly/control/transport/remote/attack_self(mob/user)
+ if(!specific_transport_id)
+ link_tram(user)
+ return
+
+ if(cooldown)
+ balloon_alert(user, "cooldown: [DisplayTimeText(cooldown, 1)]")
+ return
+
+ activate(user)
+ COOLDOWN_START(src, tram_remote, 2 MINUTES)
+
+///send our selected commands to the tram
+/obj/item/assembly/control/transport/remote/activate(mob/user)
+ if(!specific_transport_id)
+ balloon_alert(user, "no tram linked!")
+ return
+ if(!destination)
+ balloon_alert(user, "no destination!")
+ return
+
+ SEND_SIGNAL(src, COMSIG_TRANSPORT_REQUEST, specific_transport_id, destination, options)
+
+/obj/item/assembly/control/transport/remote/AltClick(mob/user)
+ link_tram(user)
+
+/obj/item/assembly/control/transport/remote/proc/link_tram(mob/user)
+ specific_transport_id = null
+ var/list/transports_available
+ for(var/datum/transport_controller/linear/tram/tram as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
+ LAZYADD(transports_available, tram.specific_transport_id)
+
+ specific_transport_id = tgui_input_list(user, "Available transports", "Select a transport", transports_available)
+
+ if(specific_transport_id)
+ balloon_alert(user, "tram linked")
+ else
+ balloon_alert(user, "link failed!")
+
+ update_appearance()
diff --git a/code/modules/transport/tram/tram_signals.dm b/code/modules/transport/tram/tram_signals.dm
new file mode 100644
index 0000000000000..9983b32fe3312
--- /dev/null
+++ b/code/modules/transport/tram/tram_signals.dm
@@ -0,0 +1,739 @@
+/// Pedestrian crossing signal for tram
+/obj/machinery/transport/crossing_signal
+ name = "crossing signal"
+ desc = "Indicates to pedestrians if it's safe to cross the tracks. Connects to sensors down the track."
+ icon = 'icons/obj/tram/crossing_signal.dmi'
+ icon_state = "crossing-inbound"
+ base_icon_state = "crossing-inbound"
+ plane = GAME_PLANE_UPPER
+ layer = TRAM_SIGNAL_LAYER
+ max_integrity = 250
+ integrity_failure = 0.25
+ light_range = 2
+ light_power = 0.7
+ idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 2.4
+ active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 0.48
+ anchored = TRUE
+ density = FALSE
+ circuit = /obj/item/circuitboard/machine/crossing_signal
+ // pointless if it only takes 2 seconds to cross but updates every 2 seconds
+ subsystem_type = /datum/controller/subsystem/processing/transport
+ light_color = LIGHT_COLOR_BABY_BLUE
+ luminosity = 1
+ /// green, amber, or red for tram, blue if it's emag, tram missing, etc.
+ var/signal_state = XING_STATE_MALF
+ /// the sensor we use
+ var/datum/weakref/sensor_ref
+ /// Inbound station
+ var/inbound
+ /// Outbound station
+ var/outbound
+ /// If us or anything else in the operation chain is broken
+ var/operating_status = TRANSPORT_SYSTEM_NORMAL
+ var/sign_dir = INBOUND
+ /** Proximity thresholds for crossing signal states
+ *
+ * The proc that checks the distance between the tram and crossing signal uses these vars to determine the distance between tram and signal to change
+ * colors. The numbers are specifically set for Tramstation. If we get another map with crossing signals we'll have to probably subtype it or something.
+ * If the value is set too high, it will cause the lights to turn red when the tram arrives at another station. You want to optimize the amount of
+ * warning without turning it red unnessecarily.
+ *
+ * Red: decent chance of getting hit, but if you're quick it's a decent gamble.
+ * Amber: slow people may be in danger.
+ */
+ var/amber_distance_threshold = AMBER_THRESHOLD_NORMAL
+ var/red_distance_threshold = RED_THRESHOLD_NORMAL
+
+/** Crossing signal subtypes
+ *
+ * Each map will have a different amount of tiles between stations, so adjust the signals here based on the map.
+ * The distance is calculated from the bottom left corner of the tram,
+ * so signals on the east side have their distance reduced by the tram length, in this case 10 for Tramstation.
+*/
+/obj/machinery/transport/crossing_signal/northwest
+ dir = NORTH
+ sign_dir = INBOUND
+
+/obj/machinery/transport/crossing_signal/northeast
+ dir = NORTH
+ sign_dir = OUTBOUND
+
+/obj/machinery/transport/crossing_signal/southwest
+ dir = SOUTH
+ sign_dir = INBOUND
+ pixel_y = 20
+
+/obj/machinery/transport/crossing_signal/southeast
+ dir = SOUTH
+ sign_dir = OUTBOUND
+ pixel_y = 20
+
+/obj/machinery/static_signal
+ name = "crossing signal"
+ desc = "Indicates to pedestrians if it's safe to cross the tracks."
+ icon = 'icons/obj/tram/crossing_signal.dmi'
+ icon_state = "crossing-inbound"
+ plane = GAME_PLANE_UPPER
+ max_integrity = 250
+ integrity_failure = 0.25
+ idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 2.4
+ active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 0.74
+ anchored = TRUE
+ density = FALSE
+ light_range = 1.5
+ light_power = 3
+ light_color = COLOR_VIBRANT_LIME
+ luminosity = 1
+ var/sign_dir = INBOUND
+
+/obj/machinery/static_signal/northwest
+ dir = NORTH
+ sign_dir = INBOUND
+
+/obj/machinery/static_signal/northeast
+ dir = NORTH
+ sign_dir = OUTBOUND
+
+/obj/machinery/static_signal/southwest
+ dir = SOUTH
+ sign_dir = INBOUND
+ pixel_y = 20
+
+/obj/machinery/static_signal/southeast
+ dir = SOUTH
+ sign_dir = OUTBOUND
+ pixel_y = 20
+
+/obj/machinery/transport/crossing_signal/Initialize(mapload)
+ . = ..()
+ RegisterSignal(SStransport, COMSIG_TRANSPORT_ACTIVE, PROC_REF(wake_up))
+ RegisterSignal(SStransport, COMSIG_COMMS_STATUS, PROC_REF(comms_change))
+ SStransport.crossing_signals += src
+ register_context()
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/machinery/transport/crossing_signal/LateInitialize(mapload)
+ . = ..()
+ link_tram()
+ link_sensor()
+ find_uplink()
+
+/obj/machinery/transport/crossing_signal/Destroy()
+ SStransport.crossing_signals -= src
+ . = ..()
+
+/obj/machinery/transport/crossing_signal/attackby(obj/item/weapon, mob/living/user, params)
+ if(!user.combat_mode)
+ if(default_deconstruction_screwdriver(user, icon_state, icon_state, weapon))
+ return
+
+ if(default_deconstruction_crowbar(weapon))
+ return
+
+ return ..()
+
+/obj/machinery/transport/crossing_signal/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ if(panel_open)
+ if(held_item?.tool_behaviour == TOOL_WRENCH)
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "rotate signal"
+ context[SCREENTIP_CONTEXT_RMB] = "flip signal"
+
+ if(istype(held_item, /obj/item/card/emag) && !(obj_flags & EMAGGED))
+ context[SCREENTIP_CONTEXT_LMB] = "disable sensors"
+
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/machinery/transport/crossing_signal/examine(mob/user)
+ . = ..()
+ . += span_notice("The maintenance panel is [panel_open ? "open" : "closed"].")
+ if(panel_open)
+ . += span_notice("It can be flipped or rotated with a [EXAMINE_HINT("wrench.")]")
+ switch(operating_status)
+ if(TRANSPORT_REMOTE_WARNING)
+ . += span_notice("The orange [EXAMINE_HINT("remote warning")] light is on.")
+ . += span_notice("The status display reads: Check track sensor.")
+ if(TRANSPORT_REMOTE_FAULT)
+ . += span_notice("The blue [EXAMINE_HINT("remote fault")] light is on.")
+ . += span_notice("The status display reads: Check tram controller.")
+ if(TRANSPORT_LOCAL_FAULT)
+ . += span_notice("The red [EXAMINE_HINT("local fault")] light is on.")
+ . += span_notice("The status display reads: Repair required.")
+ switch(dir)
+ if(NORTH, SOUTH)
+ . += span_notice("The tram configuration display shows EAST/WEST.")
+ if(EAST, WEST)
+ . += span_notice("The tram configuration display shows NORTH/SOUTH.")
+
+/obj/machinery/transport/crossing_signal/emag_act(mob/living/user)
+ if(obj_flags & EMAGGED)
+ return FALSE
+ balloon_alert(user, "disabled motion sensors")
+ operating_status = TRANSPORT_LOCAL_FAULT
+ obj_flags |= EMAGGED
+ return TRUE
+
+/obj/machinery/transport/crossing_signal/AltClick(mob/living/user)
+ . = ..()
+
+ var/obj/item/tool = user.get_active_held_item()
+ if(!panel_open || tool?.tool_behaviour != TOOL_WRENCH)
+ return FALSE
+
+ tool.play_tool_sound(src, 50)
+ setDir(turn(dir,-90))
+ to_chat(user, span_notice("You rotate [src]."))
+ find_uplink()
+ return TRUE
+
+/obj/machinery/transport/crossing_signal/attackby_secondary(obj/item/weapon, mob/user, params)
+ . = ..()
+
+ if(weapon.tool_behaviour == TOOL_WRENCH && panel_open)
+ switch(sign_dir)
+ if(INBOUND)
+ sign_dir = OUTBOUND
+ if(OUTBOUND)
+ sign_dir = INBOUND
+
+ to_chat(user, span_notice("You flip directions on [src]."))
+ update_appearance()
+
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/obj/machinery/transport/crossing_signal/proc/link_sensor()
+ sensor_ref = WEAKREF(find_closest_valid_sensor())
+ update_appearance()
+
+/obj/machinery/transport/crossing_signal/proc/unlink_sensor()
+ sensor_ref = null
+ if(operating_status < TRANSPORT_REMOTE_WARNING)
+ operating_status = TRANSPORT_REMOTE_WARNING
+ degraded_response()
+ update_appearance()
+
+/obj/machinery/transport/crossing_signal/proc/wake_sensor()
+ if(operating_status > TRANSPORT_REMOTE_WARNING)
+ degraded_response()
+ return
+
+ var/obj/machinery/transport/guideway_sensor/linked_sensor = sensor_ref?.resolve()
+ if(isnull(linked_sensor))
+ operating_status = TRANSPORT_REMOTE_WARNING
+ degraded_response()
+
+ else if(linked_sensor.trigger_sensor())
+ operating_status = TRANSPORT_SYSTEM_NORMAL
+ normal_response()
+
+ else
+ operating_status = TRANSPORT_REMOTE_WARNING
+ degraded_response()
+
+/obj/machinery/transport/crossing_signal/proc/normal_response()
+ amber_distance_threshold = AMBER_THRESHOLD_NORMAL
+ red_distance_threshold = RED_THRESHOLD_NORMAL
+
+/obj/machinery/transport/crossing_signal/proc/degraded_response()
+ amber_distance_threshold = AMBER_THRESHOLD_DEGRADED
+ red_distance_threshold = RED_THRESHOLD_DEGRADED
+
+/obj/machinery/transport/crossing_signal/proc/clear_uplink()
+ inbound = null
+ outbound = null
+ update_appearance()
+
+/**
+ * Only process if the tram is actually moving
+ */
+/obj/machinery/transport/crossing_signal/proc/wake_up(datum/source, transport_controller, controller_active)
+ SIGNAL_HANDLER
+
+ if(machine_stat & BROKEN || machine_stat & NOPOWER)
+ operating_status = TRANSPORT_LOCAL_FAULT
+ update_appearance()
+ return
+
+ if(prob(TRANSPORT_BREAKDOWN_RATE))
+ operating_status = TRANSPORT_LOCAL_FAULT
+ local_fault()
+ return
+
+ var/datum/transport_controller/linear/tram/tram = transport_ref?.resolve()
+ var/obj/machinery/transport/guideway_sensor/linked_sensor = sensor_ref?.resolve()
+
+ if(malfunctioning)
+ operating_status = TRANSPORT_LOCAL_FAULT
+ else if(isnull(tram) || tram.controller_status & COMM_ERROR)
+ operating_status = TRANSPORT_REMOTE_FAULT
+ else
+ operating_status = TRANSPORT_SYSTEM_NORMAL
+
+ if(isnull(linked_sensor))
+ link_sensor()
+ wake_sensor()
+ update_operating()
+
+/obj/machinery/transport/crossing_signal/on_set_machine_stat()
+ . = ..()
+ if(machine_stat & BROKEN || machine_stat & NOPOWER)
+ operating_status = TRANSPORT_LOCAL_FAULT
+ else
+ operating_status = TRANSPORT_SYSTEM_NORMAL
+
+/obj/machinery/transport/crossing_signal/on_set_is_operational()
+ . = ..()
+ if(!is_operational)
+ operating_status = TRANSPORT_LOCAL_FAULT
+ else
+ operating_status = TRANSPORT_SYSTEM_NORMAL
+ update_operating()
+
+/obj/machinery/transport/crossing_signal/proc/comms_change(source, controller, new_status)
+ SIGNAL_HANDLER
+
+ var/datum/transport_controller/linear/tram/updated_controller = controller
+
+ if(updated_controller.specific_transport_id != configured_transport_id)
+ return
+
+ switch(new_status)
+ if(TRUE)
+ if(operating_status == TRANSPORT_REMOTE_FAULT)
+ operating_status = TRANSPORT_SYSTEM_NORMAL
+ if(FALSE)
+ if(operating_status == TRANSPORT_SYSTEM_NORMAL)
+ operating_status = TRANSPORT_REMOTE_FAULT
+
+/**
+ * Update processing state.
+ *
+ * Returns whether we are still processing.
+ */
+/obj/machinery/transport/crossing_signal/proc/update_operating()
+ use_power(idle_power_usage)
+ update_appearance()
+ // Immediately process for snappy feedback
+ var/should_process = process() != PROCESS_KILL
+ if(should_process)
+ begin_processing()
+ return
+ end_processing()
+
+/obj/machinery/transport/crossing_signal/process()
+
+ var/datum/transport_controller/linear/tram/tram = transport_ref?.resolve()
+
+ // Check for stopped states.
+ if(!tram || !tram.controller_operational || !is_operational || !inbound || !outbound)
+ // Tram missing, we lost power, or something isn't right
+ // Throw the error message (blue)
+ set_signal_state(XING_STATE_MALF, force = !is_operational)
+ return PROCESS_KILL
+
+ use_power(active_power_usage)
+
+ var/obj/structure/transport/linear/tram_part = tram.return_closest_platform_to(src)
+
+ if(QDELETED(tram_part))
+ set_signal_state(XING_STATE_MALF, force = !is_operational)
+ return PROCESS_KILL
+
+ // Everything will be based on position and travel direction
+ var/signal_pos
+ var/tram_pos
+ var/tram_velocity_sign // 1 for positive axis movement, -1 for negative
+ // Try to be agnostic about N-S vs E-W movement
+ if(tram.travel_direction & (NORTH|SOUTH))
+ signal_pos = y
+ tram_pos = tram_part.y
+ tram_velocity_sign = tram.travel_direction & NORTH ? 1 : -1
+ else
+ signal_pos = x
+ tram_pos = tram_part.x
+ tram_velocity_sign = tram.travel_direction & EAST ? 1 : -1
+
+ // How far away are we? negative if already passed.
+ var/approach_distance = tram_velocity_sign * (signal_pos - (tram_pos + (DEFAULT_TRAM_LENGTH * 0.5)))
+
+ // Check for stopped state.
+ // Will kill the process since tram starting up will restart process.
+ if(!tram.controller_active)
+ set_signal_state(XING_STATE_GREEN)
+ return PROCESS_KILL
+
+ // Check if tram is driving away from us.
+ if(approach_distance < 0)
+ // driving away. Green. In fact, in order to reverse, it'll have to stop, so let's go ahead and kill.
+ set_signal_state(XING_STATE_GREEN)
+ return PROCESS_KILL
+
+ // Check the tram's terminus station.
+ // INBOUND 1 < 2 < 3
+ // OUTBOUND 1 > 2 > 3
+ if(tram.travel_direction & WEST && inbound < tram.destination_platform.platform_code)
+ set_signal_state(XING_STATE_GREEN)
+ return PROCESS_KILL
+ if(tram.travel_direction & EAST && outbound > tram.destination_platform.platform_code)
+ set_signal_state(XING_STATE_GREEN)
+ return PROCESS_KILL
+
+ // Finally the interesting part where it's ACTUALLY approaching
+ if(approach_distance <= red_distance_threshold)
+ if(operating_status != TRANSPORT_SYSTEM_NORMAL)
+ set_signal_state(XING_STATE_MALF)
+ else
+ set_signal_state(XING_STATE_RED)
+ return
+ if(approach_distance <= amber_distance_threshold)
+ set_signal_state(XING_STATE_AMBER)
+ return
+ set_signal_state(XING_STATE_GREEN)
+
+/**
+ * Set the signal state and update appearance.
+ *
+ * Arguments:
+ * new_state - the new state (XING_STATE_RED, etc)
+ * force_update - force appearance to update even if state didn't change.
+ */
+/obj/machinery/transport/crossing_signal/proc/set_signal_state(new_state, force = FALSE)
+ if(new_state == signal_state && !force)
+ return
+
+ signal_state = new_state
+ flick_overlay()
+ update_appearance()
+
+/obj/machinery/transport/crossing_signal/update_icon_state()
+ switch(dir)
+ if(SOUTH, EAST)
+ pixel_y = 20
+ if(NORTH, WEST)
+ pixel_y = 0
+
+ switch(sign_dir)
+ if(INBOUND)
+ icon_state = "crossing-inbound"
+ base_icon_state = "crossing-inbound"
+ if(OUTBOUND)
+ icon_state = "crossing-outbound"
+ base_icon_state = "crossing-outbound"
+
+ return ..()
+
+/obj/machinery/static_signal/update_icon_state()
+ switch(dir)
+ if(SOUTH, EAST)
+ pixel_y = 20
+ if(NORTH, WEST)
+ pixel_y = 0
+
+ switch(sign_dir)
+ if(INBOUND)
+ icon_state = "crossing-inbound"
+ base_icon_state = "crossing-inbound"
+ if(OUTBOUND)
+ icon_state = "crossing-outbound"
+ base_icon_state = "crossing-outbound"
+
+ return ..()
+
+/obj/machinery/transport/crossing_signal/update_appearance(updates)
+ . = ..()
+
+ if(machine_stat & NOPOWER)
+ set_light(l_on = FALSE)
+ return
+
+ var/new_color
+ switch(signal_state)
+ if(XING_STATE_MALF)
+ new_color = LIGHT_COLOR_BABY_BLUE
+ if(XING_STATE_GREEN)
+ new_color = LIGHT_COLOR_VIVID_GREEN
+ if(XING_STATE_AMBER)
+ new_color = LIGHT_COLOR_BRIGHT_YELLOW
+ else
+ new_color = LIGHT_COLOR_FLARE
+
+ set_light(l_on = TRUE, l_color = new_color)
+
+/obj/machinery/transport/crossing_signal/update_overlays()
+ . = ..()
+
+ if(machine_stat & NOPOWER)
+ return
+
+ if(machine_stat & BROKEN)
+ operating_status = TRANSPORT_LOCAL_FAULT
+
+ var/lights_overlay = "[base_icon_state]-l[signal_state]"
+ var/status_overlay = "[base_icon_state]-s[operating_status]"
+
+ . += mutable_appearance(icon, lights_overlay)
+ . += mutable_appearance(icon, status_overlay)
+ . += emissive_appearance(icon, lights_overlay, offset_spokesman = src, alpha = src.alpha)
+ . += emissive_appearance(icon, status_overlay, offset_spokesman = src, alpha = src.alpha)
+
+/obj/machinery/static_signal/power_change()
+ ..()
+
+ if(!is_operational)
+ set_light(l_on = FALSE)
+ return
+
+ set_light(l_on = TRUE)
+
+/obj/machinery/static_signal/update_overlays()
+ . = ..()
+
+ if(!is_operational)
+ return
+
+ . += mutable_appearance(icon, "[base_icon_state]-l0")
+ . += mutable_appearance(icon, "[base_icon_state]-s0")
+ . += emissive_appearance(icon, "[base_icon_state]-l0", offset_spokesman = src, alpha = src.alpha)
+ . += emissive_appearance(icon, "[base_icon_state]-s0", offset_spokesman = src, alpha = src.alpha)
+
+/obj/machinery/transport/guideway_sensor
+ name = "guideway sensor"
+ icon = 'icons/obj/tram/tram_sensor.dmi'
+ icon_state = "sensor-base"
+ desc = "Uses an infrared beam to detect passing trams. Works when paired with a sensor on the other side of the track."
+ layer = TRAM_RAIL_LAYER
+ use_power = 0
+ circuit = /obj/item/circuitboard/machine/guideway_sensor
+ /// Sensors work in a married pair
+ var/datum/weakref/paired_sensor
+ /// If us or anything else in the operation chain is broken
+ var/operating_status = TRANSPORT_SYSTEM_NORMAL
+
+/obj/machinery/transport/guideway_sensor/Initialize(mapload)
+ . = ..()
+ SStransport.sensors += src
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/machinery/transport/guideway_sensor/LateInitialize(mapload)
+ . = ..()
+ pair_sensor()
+ RegisterSignal(SStransport, COMSIG_TRANSPORT_ACTIVE, PROC_REF(wake_up))
+
+/obj/machinery/transport/guideway_sensor/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ if(panel_open)
+ if(held_item?.tool_behaviour == TOOL_WRENCH)
+ context[SCREENTIP_CONTEXT_RMB] = "rotate sensor"
+
+ if(istype(held_item, /obj/item/card/emag) && !(obj_flags & EMAGGED))
+ context[SCREENTIP_CONTEXT_LMB] = "disable sensor"
+
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/machinery/transport/guideway_sensor/examine(mob/user)
+ . = ..()
+ . += span_notice("The maintenance panel is [panel_open ? "open" : "closed"].")
+ if(panel_open)
+ . += span_notice("It can be rotated with a [EXAMINE_HINT("wrench.")]")
+ switch(operating_status)
+ if(TRANSPORT_REMOTE_WARNING)
+ . += span_notice("The orange [EXAMINE_HINT("remote warning")] light is on.")
+ . += span_notice("The status display reads: Check paired sensor.")
+ if(TRANSPORT_REMOTE_FAULT)
+ . += span_notice("The blue [EXAMINE_HINT("remote fault")] light is on.")
+ . += span_notice("The status display reads: Paired sensor not found.")
+ if(TRANSPORT_LOCAL_FAULT)
+ . += span_notice("The red [EXAMINE_HINT("local fault")] light is on.")
+ . += span_notice("The status display reads: Repair required.")
+
+/obj/machinery/transport/guideway_sensor/attackby(obj/item/weapon, mob/living/user, params)
+ if (!user.combat_mode)
+ if(default_deconstruction_screwdriver(user, icon_state, icon_state, weapon))
+ return
+
+ if(default_deconstruction_crowbar(weapon))
+ return
+
+ return ..()
+
+/obj/machinery/transport/guideway_sensor/proc/pair_sensor()
+ set_machine_stat(machine_stat | MAINT)
+ if(paired_sensor)
+ var/obj/machinery/transport/guideway_sensor/divorcee = paired_sensor?.resolve()
+ divorcee.set_machine_stat(machine_stat | MAINT)
+ divorcee.paired_sensor = null
+ divorcee.update_appearance()
+ paired_sensor = null
+
+ for(var/obj/machinery/transport/guideway_sensor/potential_sensor in SStransport.sensors)
+ if(potential_sensor == src)
+ continue
+ switch(potential_sensor.dir)
+ if(NORTH, SOUTH)
+ if(potential_sensor.x == src.x)
+ paired_sensor = WEAKREF(potential_sensor)
+ set_machine_stat(machine_stat & ~MAINT)
+ break
+ if(EAST, WEST)
+ if(potential_sensor.y == src.y)
+ paired_sensor = WEAKREF(potential_sensor)
+ set_machine_stat(machine_stat & ~MAINT)
+ break
+
+ update_appearance()
+
+ var/obj/machinery/transport/guideway_sensor/new_partner = paired_sensor?.resolve()
+ if(isnull(new_partner))
+ return
+
+ new_partner.paired_sensor = WEAKREF(src)
+ new_partner.set_machine_stat(machine_stat & ~MAINT)
+ new_partner.update_appearance()
+ playsound(src, 'sound/machines/synth_yes.ogg', 75, vary = FALSE, use_reverb = TRUE)
+
+/obj/machinery/transport/guideway_sensor/Destroy()
+ SStransport.sensors -= src
+ if(paired_sensor)
+ var/obj/machinery/transport/guideway_sensor/divorcee = paired_sensor?.resolve()
+ divorcee.set_machine_stat(machine_stat & ~MAINT)
+ divorcee.paired_sensor = null
+ divorcee.update_appearance()
+ playsound(src, 'sound/machines/synth_no.ogg', 75, vary = FALSE, use_reverb = TRUE)
+ paired_sensor = null
+ . = ..()
+
+/obj/machinery/transport/guideway_sensor/wrench_act(mob/living/user, obj/item/tool)
+ . = ..()
+
+ if(default_change_direction_wrench(user, tool))
+ pair_sensor()
+ return TRUE
+
+/obj/machinery/transport/guideway_sensor/update_overlays()
+ . = ..()
+
+ if(machine_stat & BROKEN || machine_stat & NOPOWER || malfunctioning)
+ operating_status = TRANSPORT_LOCAL_FAULT
+ . += mutable_appearance(icon, "sensor-[TRANSPORT_LOCAL_FAULT]")
+ . += emissive_appearance(icon, "sensor-[TRANSPORT_LOCAL_FAULT]", src, alpha = src.alpha)
+ return
+
+ if(machine_stat & MAINT)
+ operating_status = TRANSPORT_REMOTE_FAULT
+ . += mutable_appearance(icon, "sensor-[TRANSPORT_REMOTE_FAULT]")
+ . += emissive_appearance(icon, "sensor-[TRANSPORT_REMOTE_FAULT]", src, alpha = src.alpha)
+ return
+
+ var/obj/machinery/transport/guideway_sensor/buddy = paired_sensor?.resolve()
+ if(buddy)
+ if(!buddy.is_operational)
+ operating_status = TRANSPORT_REMOTE_WARNING
+ . += mutable_appearance(icon, "sensor-[TRANSPORT_REMOTE_WARNING]")
+ . += emissive_appearance(icon, "sensor-[TRANSPORT_REMOTE_WARNING]", src, alpha = src.alpha)
+ else
+ operating_status = TRANSPORT_SYSTEM_NORMAL
+ . += mutable_appearance(icon, "sensor-[TRANSPORT_SYSTEM_NORMAL]")
+ . += emissive_appearance(icon, "sensor-[TRANSPORT_SYSTEM_NORMAL]", src, alpha = src.alpha)
+ return
+
+ else
+ operating_status = TRANSPORT_REMOTE_FAULT
+ . += mutable_appearance(icon, "sensor-[TRANSPORT_REMOTE_FAULT]")
+ . += emissive_appearance(icon, "sensor-[TRANSPORT_REMOTE_FAULT]", src, alpha = src.alpha)
+
+/obj/machinery/transport/guideway_sensor/proc/trigger_sensor()
+ var/obj/machinery/transport/guideway_sensor/buddy = paired_sensor?.resolve()
+ if(!buddy)
+ return FALSE
+
+ if(!is_operational || !buddy.is_operational)
+ return FALSE
+
+ return TRUE
+
+/obj/machinery/transport/guideway_sensor/proc/wake_up()
+ SIGNAL_HANDLER
+
+ if(machine_stat & BROKEN)
+ update_appearance()
+ return
+
+ if(prob(TRANSPORT_BREAKDOWN_RATE))
+ operating_status = TRANSPORT_LOCAL_FAULT
+ local_fault()
+
+ var/obj/machinery/transport/guideway_sensor/buddy = paired_sensor?.resolve()
+
+ if(buddy)
+ set_machine_stat(machine_stat & ~MAINT)
+
+ update_appearance()
+
+/obj/machinery/transport/guideway_sensor/on_set_is_operational()
+ . = ..()
+
+ var/obj/machinery/transport/guideway_sensor/buddy = paired_sensor?.resolve()
+ if(buddy)
+ buddy.update_appearance()
+
+ update_appearance()
+
+/obj/machinery/transport/crossing_signal/proc/find_closest_valid_sensor()
+ if(!istype(src) || !src.z)
+ return FALSE
+
+ var/list/obj/machinery/transport/guideway_sensor/sensor_candidates = list()
+
+ for(var/obj/machinery/transport/guideway_sensor/sensor in SStransport.sensors)
+ if(sensor.z == src.z)
+ if((sensor.x == src.x && sensor.dir & NORTH|SOUTH) || (sensor.y == src.y && sensor.dir & EAST|WEST))
+ sensor_candidates += sensor
+
+ var/obj/machinery/transport/guideway_sensor/selected_sensor = get_closest_atom(/obj/machinery/transport/guideway_sensor, sensor_candidates, src)
+ var/sensor_distance = get_dist(src, selected_sensor)
+ if(sensor_distance <= DEFAULT_TRAM_LENGTH)
+ return selected_sensor
+
+ return FALSE
+
+/obj/machinery/transport/crossing_signal/proc/find_uplink()
+ if(!istype(src) || !src.z)
+ return FALSE
+
+ var/list/obj/effect/landmark/transport/nav_beacon/tram/platform/inbound_candidates = list()
+ var/list/obj/effect/landmark/transport/nav_beacon/tram/platform/outbound_candidates = list()
+
+ inbound = null
+ outbound = null
+
+ for(var/obj/effect/landmark/transport/nav_beacon/tram/platform/beacon in SStransport.nav_beacons[configured_transport_id])
+ if(beacon.z != src.z)
+ continue
+
+ switch(src.dir)
+ if(NORTH, SOUTH)
+ if(abs((beacon.y - src.y)) <= DEFAULT_TRAM_LENGTH)
+ if(beacon.x < src.x)
+ inbound_candidates += beacon
+ else
+ outbound_candidates += beacon
+ if(EAST, WEST)
+ if(abs((beacon.x - src.x)) <= DEFAULT_TRAM_LENGTH)
+ if(beacon.y < src.y)
+ inbound_candidates += beacon
+ else
+ outbound_candidates += beacon
+
+ var/obj/effect/landmark/transport/nav_beacon/tram/platform/selected_inbound = get_closest_atom(/obj/effect/landmark/transport/nav_beacon/tram/platform, inbound_candidates, src)
+ if(isnull(selected_inbound))
+ return FALSE
+
+ inbound = selected_inbound.platform_code
+
+ var/obj/effect/landmark/transport/nav_beacon/tram/platform/selected_outbound = get_closest_atom(/obj/effect/landmark/transport/nav_beacon/tram/platform, outbound_candidates, src)
+ if(isnull(selected_outbound))
+ return FALSE
+
+ outbound = selected_outbound.platform_code
+
+ update_appearance()
diff --git a/code/modules/transport/tram/tram_structures.dm b/code/modules/transport/tram/tram_structures.dm
new file mode 100644
index 0000000000000..c6291f775b3f7
--- /dev/null
+++ b/code/modules/transport/tram/tram_structures.dm
@@ -0,0 +1,635 @@
+/**
+ * the tram has a few objects mapped onto it at roundstart, by default many of those objects have unwanted properties
+ * for example grilles and windows have the atmos_sensitive element applied to them, which makes them register to
+ * themselves moving to re register signals onto the turf via connect_loc. this is bad and dumb since it makes the tram
+ * more expensive to move.
+ *
+ * if you map something on to the tram, make SURE if possible that it doesnt have anything reacting to its own movement
+ * it will make the tram more expensive to move and we dont want that because we dont want to return to the days where
+ * the tram took a third of the tick per movement when its just carrying its default mapped in objects
+ */
+
+/obj/structure/grille/tram/Initialize(mapload)
+ . = ..()
+ RemoveElement(/datum/element/atmos_sensitive, mapload)
+ //atmos_sensitive applies connect_loc which 1. reacts to movement in order to 2. unregister and register signals to
+ //the old and new locs. we dont want that, pretend these grilles and windows are plastic or something idk
+
+/obj/structure/tram/Initialize(mapload, direct)
+ . = ..()
+ RemoveElement(/datum/element/atmos_sensitive, mapload)
+
+/obj/structure/tram
+ name = "tram wall"
+ desc = "A lightweight titanium composite structure with titanium silicate panels."
+ icon = 'icons/obj/tram/tram_structure.dmi'
+ icon_state = "tram-part-0"
+ base_icon_state = "tram-part"
+ max_integrity = 150
+ layer = TRAM_WALL_LAYER
+ density = TRUE
+ opacity = FALSE
+ anchored = TRUE
+ flags_1 = PREVENT_CLICK_UNDER_1
+ armor_type = /datum/armor/tram_structure
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_TRAM_STRUCTURE
+ canSmoothWith = SMOOTH_GROUP_TRAM_STRUCTURE
+ can_be_unanchored = FALSE
+ can_atmos_pass = ATMOS_PASS_DENSITY
+ explosion_block = 3
+ receive_ricochet_chance_mod = 1.2
+ rad_insulation = RAD_MEDIUM_INSULATION
+ /// What state of de/construction it's in
+ var/state = TRAM_SCREWED_TO_FRAME
+ /// Mineral to return when deconstructed
+ var/mineral = /obj/item/stack/sheet/titaniumglass
+ /// Amount of mineral to return when deconstructed
+ var/mineral_amount = 2
+ /// Type of structure made out of girder
+ var/tram_wall_type = /obj/structure/tram
+ /// Type of girder made when deconstructed
+ var/girder_type = /obj/structure/girder/tram
+ var/mutable_appearance/damage_overlay
+ /// Sound when it breaks
+ var/break_sound = SFX_SHATTER
+ /// Sound when hit without combat mode
+ var/knock_sound = 'sound/effects/glassknock.ogg'
+ /// Sound when hit with combat mode
+ var/bash_sound = 'sound/effects/glassbash.ogg'
+
+/obj/structure/tram/split
+ base_icon_state = "tram-split"
+
+/datum/armor/tram_structure
+ melee = 40
+ bullet = 10
+ laser = 10
+ bomb = 45
+ fire = 90
+ acid = 100
+
+/obj/structure/tram/Initialize(mapload)
+ AddElement(/datum/element/blocks_explosives)
+ . = ..()
+ var/obj/item/stack/initialized_mineral = new mineral
+ set_custom_materials(initialized_mineral.mats_per_unit, mineral_amount)
+ qdel(initialized_mineral)
+ air_update_turf(TRUE, TRUE)
+ register_context()
+
+/obj/structure/tram/examine(mob/user)
+ . = ..()
+ switch(state)
+ if(TRAM_SCREWED_TO_FRAME)
+ . += span_notice("The panel is [EXAMINE_HINT("screwed")] to the frame. To dismantle use a [EXAMINE_HINT("screwdriver.")]")
+ if(TRAM_IN_FRAME)
+ . += span_notice("The panel is [EXAMINE_HINT("unscrewed,")] but [EXAMINE_HINT("pried")] into the frame. To dismantle use a [EXAMINE_HINT("crowbar.")]")
+ if(TRAM_OUT_OF_FRAME)
+ . += span_notice("The panel is [EXAMINE_HINT("pried")] out of the frame, but still[EXAMINE_HINT("wired.")] To dismantle use [EXAMINE_HINT("wirecutters.")]")
+
+/obj/structure/tram/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ if(held_item?.tool_behaviour == TOOL_WELDER && atom_integrity < max_integrity)
+ context[SCREENTIP_CONTEXT_LMB] = "repair"
+ if(held_item?.tool_behaviour == TOOL_SCREWDRIVER && state == TRAM_SCREWED_TO_FRAME)
+ context[SCREENTIP_CONTEXT_RMB] = "unscrew panel"
+ if(held_item?.tool_behaviour == TOOL_CROWBAR && state == TRAM_IN_FRAME)
+ context[SCREENTIP_CONTEXT_RMB] = "remove panel"
+ if(held_item?.tool_behaviour == TOOL_WIRECUTTER && state == TRAM_OUT_OF_FRAME)
+ context[SCREENTIP_CONTEXT_RMB] = "disconnect panel"
+
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/structure/tram/update_overlays(updates = ALL)
+ . = ..()
+ var/ratio = atom_integrity / max_integrity
+ ratio = CEILING(ratio * 4, 1) * 25
+ cut_overlay(damage_overlay)
+ if(ratio > 75)
+ return
+
+ damage_overlay = mutable_appearance('icons/obj/structures.dmi', "damage[ratio]", -(layer + 0.1))
+ . += damage_overlay
+
+/obj/structure/tram/attack_hand(mob/living/user, list/modifiers)
+ . = ..()
+
+ if(!user.combat_mode)
+ user.visible_message(span_notice("[user] knocks on [src]."), \
+ span_notice("You knock on [src]."))
+ playsound(src, knock_sound, 50, TRUE)
+ else
+ user.visible_message(span_warning("[user] bashes [src]!"), \
+ span_warning("You bash [src]!"))
+ playsound(src, bash_sound, 100, TRUE)
+
+/obj/structure/tram/rcd_vals(mob/user, obj/item/construction/rcd/the_rcd)
+ switch(the_rcd.mode)
+ if(RCD_DECONSTRUCT)
+ return list("mode" = RCD_DECONSTRUCT, "delay" = 3 SECONDS, "cost" = 10)
+ return FALSE
+
+/obj/structure/tram/rcd_act(mob/user, obj/item/construction/rcd/the_rcd)
+ switch(the_rcd.mode)
+ if(RCD_DECONSTRUCT)
+ qdel(src)
+ return TRUE
+ return FALSE
+
+/obj/structure/tram/take_damage(damage_amount, damage_type = BRUTE, damage_flag = "", sound_effect = TRUE, attack_dir, armour_penetration = 0)
+ . = ..()
+ if(.) //received damage
+ update_appearance()
+
+/obj/structure/tram/narsie_act()
+ add_atom_colour(NARSIE_WINDOW_COLOUR, FIXED_COLOUR_PRIORITY)
+
+/obj/structure/tram/singularity_pull(singulo, current_size)
+ ..()
+
+ if(current_size >= STAGE_FIVE)
+ deconstruct(disassembled = FALSE)
+
+/obj/structure/tram/welder_act(mob/living/user, obj/item/tool)
+ if(atom_integrity >= max_integrity)
+ to_chat(user, span_warning("[src] is already in good condition!"))
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+ if(!tool.tool_start_check(user, amount = 0))
+ return FALSE
+ to_chat(user, span_notice("You begin repairing [src]..."))
+ if(tool.use_tool(src, user, 4 SECONDS, volume = 50))
+ atom_integrity = max_integrity
+ to_chat(user, span_notice("You repair [src]."))
+ update_appearance()
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/structure/tram/attackby_secondary(obj/item/tool, mob/user, params)
+ switch(state)
+ if(TRAM_SCREWED_TO_FRAME)
+ if(tool.tool_behaviour == TOOL_SCREWDRIVER)
+ user.visible_message(span_notice("[user] begins to unscrew the tram panel from the frame..."),
+ span_notice("You begin to unscrew the tram panel from the frame..."))
+ if(tool.use_tool(src, user, 1 SECONDS, volume = 50))
+ state = TRAM_IN_FRAME
+ to_chat(user, span_notice("The screws come out, and a gap forms around the edge of the pane."))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ if(tool.tool_behaviour)
+ to_chat(user, span_warning("The security screws need to be removed first!"))
+
+ if(TRAM_IN_FRAME)
+ if(tool.tool_behaviour == TOOL_CROWBAR)
+ user.visible_message(span_notice("[user] wedges \the [tool] into the tram panel's gap in the frame and starts prying..."),
+ span_notice("You wedge \the [tool] into the tram panel's gap in the frame and start prying..."))
+ if(tool.use_tool(src, user, 1 SECONDS, volume = 50))
+ state = TRAM_OUT_OF_FRAME
+ to_chat(user, span_notice("The panel pops out of the frame, exposing some cabling that look like they can be cut."))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ if(tool.tool_behaviour == TOOL_SCREWDRIVER)
+ user.visible_message(span_notice("[user] resecures the tram panel to the frame..."),
+ span_notice("You resecure the tram panel to the frame..."))
+ state = TRAM_SCREWED_TO_FRAME
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ if(TRAM_OUT_OF_FRAME)
+ if(tool.tool_behaviour == TOOL_WIRECUTTER)
+ user.visible_message(span_notice("[user] starts cutting the connective cabling on \the [src]..."),
+ span_notice("You start cutting the connective cabling on \the [src]"))
+ if(tool.use_tool(src, user, 1 SECONDS, volume = 50))
+ to_chat(user, span_notice("The panels falls out of the way exposing the frame backing."))
+ deconstruct(disassembled = TRUE)
+
+ if(tool.tool_behaviour == TOOL_CROWBAR)
+ user.visible_message(span_notice("[user] snaps the tram panel into place."),
+ span_notice("You snap the tram panel into place..."))
+ state = TRAM_IN_FRAME
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ if(tool.tool_behaviour)
+ to_chat(user, span_warning("The cabling need to be cut first!"))
+
+ return ..()
+
+/obj/structure/tram/deconstruct(disassembled = TRUE)
+ if(!(flags_1 & NODECONSTRUCT_1))
+ if(disassembled)
+ new girder_type(loc)
+ if(mineral_amount)
+ for(var/i in 1 to mineral_amount)
+ new mineral(loc)
+ qdel(src)
+
+/obj/structure/tram/attackby(obj/item/item, mob/user, params)
+ . = ..()
+
+ if(istype(item, /obj/item/wallframe/tram))
+ try_wallmount(item, user)
+
+/obj/structure/tram/proc/try_wallmount(obj/item/wallmount, mob/user)
+ if(!istype(wallmount, /obj/item/wallframe/tram))
+ return
+
+ var/obj/item/wallframe/frame = wallmount
+ if(frame.try_build(src, user))
+ frame.attach(src, user)
+
+ return
+
+/*
+ * Other misc tramwall types
+ */
+
+/obj/structure/tram/alt
+
+
+/obj/structure/tram/alt/titanium
+ name = "solid tram"
+ desc = "A lightweight titanium composite structure. There is further solid plating where the panels usually attach to the frame."
+ icon = 'icons/turf/walls/shuttle_wall.dmi'
+ icon_state = "shuttle_wall-0"
+ base_icon_state = "shuttle_wall"
+ mineral = /obj/item/stack/sheet/mineral/titanium
+ tram_wall_type = /obj/structure/tram/alt/titanium
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_TITANIUM_WALLS + SMOOTH_GROUP_WALLS
+ canSmoothWith = SMOOTH_GROUP_SHUTTLE_PARTS + SMOOTH_GROUP_AIRLOCK + SMOOTH_GROUP_TITANIUM_WALLS
+
+/obj/structure/tram/alt/plastitanium
+ name = "reinforced tram"
+ desc = "An evil tram of plasma and titanium."
+ icon = 'icons/turf/walls/plastitanium_wall.dmi'
+ icon_state = "plastitanium_wall-0"
+ base_icon_state = "plastitanium_wall"
+ mineral = /obj/item/stack/sheet/mineral/plastitanium
+ tram_wall_type = /obj/structure/tram/alt/plastitanium
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_PLASTITANIUM_WALLS + SMOOTH_GROUP_WALLS
+ canSmoothWith = SMOOTH_GROUP_SHUTTLE_PARTS + SMOOTH_GROUP_AIRLOCK + SMOOTH_GROUP_PLASTITANIUM_WALLS
+
+/obj/structure/tram/alt/gold
+ name = "gold tram"
+ desc = "A solid gold tram. Swag!"
+ icon = 'icons/turf/walls/gold_wall.dmi'
+ icon_state = "gold_wall-0"
+ base_icon_state = "gold_wall"
+ mineral = /obj/item/stack/sheet/mineral/gold
+ tram_wall_type = /obj/structure/tram/alt/gold
+ explosion_block = 0 //gold is a soft metal you dingus.
+ smoothing_groups = SMOOTH_GROUP_GOLD_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_GOLD_WALLS
+ custom_materials = list(/datum/material/gold = SHEET_MATERIAL_AMOUNT * 2)
+
+/obj/structure/tram/alt/silver
+ name = "silver tram"
+ desc = "A solid silver tram. Shiny!"
+ icon = 'icons/turf/walls/silver_wall.dmi'
+ icon_state = "silver_wall-0"
+ base_icon_state = "silver_wall"
+ mineral = /obj/item/stack/sheet/mineral/silver
+ tram_wall_type = /obj/structure/tram/alt/silver
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_SILVER_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_SILVER_WALLS
+ custom_materials = list(/datum/material/silver = SHEET_MATERIAL_AMOUNT * 2)
+
+/obj/structure/tram/alt/diamond
+ name = "diamond tram"
+ desc = "A composite structure with diamond-plated panels. Looks awfully sharp..."
+ icon = 'icons/turf/walls/diamond_wall.dmi'
+ icon_state = "diamond_wall-0"
+ base_icon_state = "diamond_wall"
+ mineral = /obj/item/stack/sheet/mineral/diamond
+ tram_wall_type = /obj/structure/tram/alt/diamond //diamond wall takes twice as much time to slice
+ max_integrity = 800
+ explosion_block = 3
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_DIAMOND_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_DIAMOND_WALLS
+ custom_materials = list(/datum/material/diamond = SHEET_MATERIAL_AMOUNT * 2)
+
+/obj/structure/tram/alt/bananium
+ name = "bananium tram"
+ desc = "A composite structure with bananium plating. Honk!"
+ icon = 'icons/turf/walls/bananium_wall.dmi'
+ icon_state = "bananium_wall-0"
+ base_icon_state = "bananium_wall"
+ mineral = /obj/item/stack/sheet/mineral/bananium
+ tram_wall_type = /obj/structure/tram/alt/bananium
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_BANANIUM_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_BANANIUM_WALLS
+ custom_materials = list(/datum/material/bananium = SHEET_MATERIAL_AMOUNT*2)
+
+/obj/structure/tram/alt/sandstone
+ name = "sandstone tram"
+ desc = "A composite structure with sandstone plating. Rough."
+ icon = 'icons/turf/walls/sandstone_wall.dmi'
+ icon_state = "sandstone_wall-0"
+ base_icon_state = "sandstone_wall"
+ mineral = /obj/item/stack/sheet/mineral/sandstone
+ tram_wall_type = /obj/structure/tram/alt/sandstone
+ explosion_block = 0
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_SANDSTONE_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_SANDSTONE_WALLS
+ custom_materials = list(/datum/material/sandstone = SHEET_MATERIAL_AMOUNT*2)
+
+/obj/structure/tram/alt/uranium
+ article = "a"
+ name = "uranium tram"
+ desc = "A composite structure with uranium plating. This is probably a bad idea."
+ icon = 'icons/turf/walls/uranium_wall.dmi'
+ icon_state = "uranium_wall-0"
+ base_icon_state = "uranium_wall"
+ mineral = /obj/item/stack/sheet/mineral/uranium
+ tram_wall_type = /obj/structure/tram/alt/uranium
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_URANIUM_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_URANIUM_WALLS
+ custom_materials = list(/datum/material/uranium = SHEET_MATERIAL_AMOUNT*2)
+
+ /// Mutex to prevent infinite recursion when propagating radiation pulses
+ var/active = null
+
+ /// The last time a radiation pulse was performed
+ var/last_event = 0
+
+/obj/structure/tram/alt/uranium/attackby(obj/item/W, mob/user, params)
+ radiate()
+ return ..()
+
+/obj/structure/tram/alt/uranium/attack_hand(mob/user, list/modifiers)
+ radiate()
+ return ..()
+
+/obj/structure/tram/alt/uranium/proc/radiate()
+ SIGNAL_HANDLER
+ if(active)
+ return
+ if(world.time <= last_event + 1.5 SECONDS)
+ return
+ active = TRUE
+ radiation_pulse(
+ src,
+ max_range = 3,
+ threshold = RAD_LIGHT_INSULATION,
+ chance = URANIUM_IRRADIATION_CHANCE,
+ minimum_exposure_time = URANIUM_RADIATION_MINIMUM_EXPOSURE_TIME,
+ )
+ propagate_radiation_pulse()
+ last_event = world.time
+ active = FALSE
+
+/obj/structure/tram/alt/plasma
+ name = "plasma tram"
+ desc = "A composite structure with plasma plating. This is definitely a bad idea."
+ icon = 'icons/turf/walls/plasma_wall.dmi'
+ icon_state = "plasma_wall-0"
+ base_icon_state = "plasma_wall"
+ mineral = /obj/item/stack/sheet/mineral/plasma
+ tram_wall_type = /obj/structure/tram/alt/plasma
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_PLASMA_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_PLASMA_WALLS
+ custom_materials = list(/datum/material/plasma = SHEET_MATERIAL_AMOUNT*2)
+
+/obj/structure/tram/alt/wood
+ name = "wooden tram"
+ desc = "A tram with wooden framing. Flammable. There's a reason we use metal now."
+ icon = 'icons/turf/walls/wood_wall.dmi'
+ icon_state = "wood_wall-0"
+ base_icon_state = "wood_wall"
+ mineral = /obj/item/stack/sheet/mineral/wood
+ tram_wall_type = /obj/structure/tram/alt/wood
+ explosion_block = 0
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_WOOD_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_WOOD_WALLS
+ custom_materials = list(/datum/material/wood = SHEET_MATERIAL_AMOUNT*2)
+
+/obj/structure/tram/alt/wood/attackby(obj/item/W, mob/user)
+ if(W.get_sharpness() && W.force)
+ var/duration = ((4.8 SECONDS) / W.force) * 2 //In seconds, for now.
+ if(istype(W, /obj/item/hatchet) || istype(W, /obj/item/fireaxe))
+ duration /= 4 //Much better with hatchets and axes.
+ if(do_after(user, duration * (1 SECONDS), target=src)) //Into deciseconds.
+ deconstruct(disassembled = FALSE)
+ return
+ return ..()
+
+/obj/structure/tram/alt/bamboo
+ name = "bamboo tram"
+ desc = "A tram with a bamboo framing."
+ icon = 'icons/turf/walls/bamboo_wall.dmi'
+ icon_state = "wall-0"
+ base_icon_state = "wall"
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_WALLS + SMOOTH_GROUP_BAMBOO_WALLS + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_BAMBOO_WALLS
+ mineral = /obj/item/stack/sheet/mineral/bamboo
+ tram_wall_type = /obj/structure/tram/alt/bamboo
+
+/obj/structure/tram/alt/iron
+ name = "rough iron tram"
+ desc = "A composite structure with rough iron plating."
+ icon = 'icons/turf/walls/iron_wall.dmi'
+ icon_state = "iron_wall-0"
+ base_icon_state = "iron_wall"
+ mineral = /obj/item/stack/rods
+ mineral_amount = 5
+ tram_wall_type = /obj/structure/tram/alt/iron
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_IRON_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_IRON_WALLS
+ custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2.5)
+
+/obj/structure/tram/alt/abductor
+ name = "alien tram"
+ desc = "A composite structure made of some kind of alien alloy."
+ icon = 'icons/turf/walls/abductor_wall.dmi'
+ icon_state = "abductor_wall-0"
+ base_icon_state = "abductor_wall"
+ mineral = /obj/item/stack/sheet/mineral/abductor
+ tram_wall_type = /obj/structure/tram/alt/abductor
+ explosion_block = 3
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = SMOOTH_GROUP_ABDUCTOR_WALLS + SMOOTH_GROUP_WALLS + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_ABDUCTOR_WALLS
+ custom_materials = list(/datum/material/alloy/alien = SHEET_MATERIAL_AMOUNT*2)
+
+/obj/structure/tram/get_dumping_location()
+ return null
+
+/obj/structure/tram/spoiler
+ name = "tram spoiler"
+ icon = 'icons/obj/tram/tram_structure.dmi'
+ desc = "Nanotrasen bought the luxury package under the impression titanium spoilers make the tram go faster. They're just for looks, or potentially stabbing anybody who gets in the way."
+ icon_state = "tram-spoiler-retracted"
+ max_integrity = 400
+ obj_flags = CAN_BE_HIT
+ mineral = /obj/item/stack/sheet/mineral/titanium
+ girder_type = /obj/structure/girder/tram/corner
+ smoothing_flags = NONE
+ smoothing_groups = null
+ canSmoothWith = null
+ /// Position of the spoiler
+ var/deployed = FALSE
+ /// Malfunctioning due to tampering or emag
+ var/malfunctioning = FALSE
+ /// Weakref to the tram piece we control
+ var/datum/weakref/tram_ref
+ /// The tram we're attached to
+ var/tram_id = TRAMSTATION_LINE_1
+
+/obj/structure/tram/spoiler/Initialize(mapload)
+ . = ..()
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/structure/tram/spoiler/LateInitialize()
+ . = ..()
+ RegisterSignal(SStransport, COMSIG_TRANSPORT_ACTIVE, PROC_REF(set_spoiler))
+
+/obj/structure/tram/spoiler/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ if(held_item?.tool_behaviour == TOOL_MULTITOOL && (obj_flags & EMAGGED))
+ context[SCREENTIP_CONTEXT_LMB] = "repair"
+
+ if(held_item?.tool_behaviour == TOOL_WELDER && atom_integrity >= max_integrity)
+ context[SCREENTIP_CONTEXT_LMB] = "[malfunctioning ? "repair" : "lock"]"
+
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/structure/tram/spoiler/examine(mob/user)
+ . = ..()
+ if(obj_flags & EMAGGED)
+ . += span_warning("The electronics panel is sparking occasionally. It can be reset with a [EXAMINE_HINT("multitool.")]")
+
+ if(malfunctioning)
+ . += span_warning("The spoiler is [EXAMINE_HINT("welded")] in place!")
+ else
+ . += span_notice("The spoiler can be locked in to place with a [EXAMINE_HINT("welder.")]")
+
+/obj/structure/tram/spoiler/proc/set_spoiler(source, controller, controller_active, controller_status, travel_direction)
+ SIGNAL_HANDLER
+
+ var/spoiler_direction = travel_direction
+ if(obj_flags & EMAGGED && !malfunctioning)
+ malfunctioning = TRUE
+
+ if(malfunctioning || controller_status & COMM_ERROR)
+ if(!deployed)
+ // Bring out the blades
+ if(malfunctioning)
+ visible_message(span_danger("\the [src] locks up due to its servo overheating!"))
+ do_sparks(3, cardinal_only = FALSE, source = src)
+ deploy_spoiler()
+ return
+
+ if(!controller_active)
+ return
+
+ switch(spoiler_direction)
+ if(SOUTH, EAST)
+ switch(dir)
+ if(NORTH, EAST)
+ retract_spoiler()
+ if(SOUTH, WEST)
+ deploy_spoiler()
+
+ if(NORTH, WEST)
+ switch(dir)
+ if(NORTH, EAST)
+ deploy_spoiler()
+ if(SOUTH, WEST)
+ retract_spoiler()
+ return
+
+/obj/structure/tram/spoiler/proc/deploy_spoiler()
+ if(deployed)
+ return
+ flick("tram-spoiler-deploying", src)
+ icon_state = "tram-spoiler-deployed"
+ deployed = TRUE
+ update_appearance()
+
+/obj/structure/tram/spoiler/proc/retract_spoiler()
+ if(!deployed)
+ return
+ flick("tram-spoiler-retracting", src)
+ icon_state = "tram-spoiler-retracted"
+ deployed = FALSE
+ update_appearance()
+
+/obj/structure/tram/spoiler/emag_act(mob/user)
+ if(obj_flags & EMAGGED)
+ return
+ to_chat(user, span_warning("You short-circuit the [src]'s servo to overheat!"), type = MESSAGE_TYPE_INFO)
+ playsound(src, SFX_SPARKS, 100, vary = TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ do_sparks(5, cardinal_only = FALSE, source = src)
+ obj_flags |= EMAGGED
+
+/obj/structure/tram/spoiler/multitool_act(mob/living/user, obj/item/tool)
+ if(user.combat_mode)
+ return FALSE
+
+ if(obj_flags & EMAGGED)
+ balloon_alert(user, "electronics reset!")
+ obj_flags &= ~EMAGGED
+ return TRUE
+
+ return FALSE
+
+/obj/structure/tram/spoiler/welder_act(mob/living/user, obj/item/tool)
+ if(!tool.tool_start_check(user, amount = 1))
+ return FALSE
+
+ if(atom_integrity >= max_integrity)
+ to_chat(user, span_warning("You begin to weld \the [src], [malfunctioning ? "repairing damage" : "preventing retraction"]."))
+ if(!tool.use_tool(src, user, 4 SECONDS, volume = 50))
+ return
+ malfunctioning = !malfunctioning
+ user.visible_message(span_warning("[user] [malfunctioning ? "welds \the [src] in place" : "repairs \the [src]"] with [tool]."), \
+ span_warning("You finish welding \the [src], [malfunctioning ? "locking it in place." : "it can move freely again!"]"), null, COMBAT_MESSAGE_RANGE)
+
+ if(malfunctioning)
+ deploy_spoiler()
+
+ update_appearance()
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+ to_chat(user, span_notice("You begin repairing [src]..."))
+ if(!tool.use_tool(src, user, 4 SECONDS, volume = 50))
+ return
+ atom_integrity = max_integrity
+ to_chat(user, span_notice("You repair [src]."))
+ update_appearance()
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/structure/tram/spoiler/update_overlays()
+ . = ..()
+ if(deployed && malfunctioning)
+ . += mutable_appearance(icon, "tram-spoiler-welded")
+
+/obj/structure/chair/sofa/bench/tram
+ name = "bench"
+ desc = "Perfectly designed to be comfortable to sit on, and hellish to sleep on."
+ icon_state = "bench_middle"
+ greyscale_config = /datum/greyscale_config/bench_middle
+ greyscale_colors = COLOR_TRAM_BLUE
+
+/obj/structure/chair/sofa/bench/tram/left
+ icon_state = "bench_left"
+ greyscale_config = /datum/greyscale_config/bench_left
+
+/obj/structure/chair/sofa/bench/tram/right
+ icon_state = "bench_right"
+ greyscale_config = /datum/greyscale_config/bench_right
+
+/obj/structure/chair/sofa/bench/tram/corner
+ icon_state = "bench_corner"
+ greyscale_config = /datum/greyscale_config/bench_corner
+
+/obj/structure/chair/sofa/bench/tram/solo
+ icon_state = "bench_solo"
+ greyscale_config = /datum/greyscale_config/bench_solo
diff --git a/code/modules/industrial_lift/industrial_lift.dm b/code/modules/transport/transport_module.dm
similarity index 58%
rename from code/modules/industrial_lift/industrial_lift.dm
rename to code/modules/transport/transport_module.dm
index 0bcc2a49bc84a..56aa52beebc90 100644
--- a/code/modules/industrial_lift/industrial_lift.dm
+++ b/code/modules/transport/transport_module.dm
@@ -1,59 +1,55 @@
-GLOBAL_LIST_EMPTY(lifts)
-GLOBAL_LIST_INIT(all_radial_directions, list(
- "NORTH" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTH),
- "NORTHEAST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTHEAST),
- "EAST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = EAST),
- "SOUTHEAST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTHEAST),
- "SOUTH" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTH),
- "SOUTHWEST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTHWEST),
- "WEST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = WEST),
- "NORTHWEST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTHWEST)
-))
-
-/obj/structure/industrial_lift
- name = "lift platform"
- desc = "A lightweight lift platform. It moves up and down."
+/**
+ * Base transport structure. A single tile that can form a modular set with neighbouring tiles
+ * This type holds elevators and trams
+ */
+/obj/structure/transport/linear
+ name = "linear transport module"
+ desc = "A lightweight lift platform. It moves."
icon = 'icons/obj/smooth_structures/catwalk.dmi'
icon_state = "catwalk-0"
base_icon_state = "catwalk"
density = FALSE
anchored = TRUE
- armor_type = /datum/armor/structure_industrial_lift
+ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
+ armor_type = /datum/armor/transport_module
max_integrity = 50
- layer = LATTICE_LAYER //under pipes
+ layer = TRAM_FLOOR_LAYER
plane = FLOOR_PLANE
smoothing_flags = SMOOTH_BITMASK
smoothing_groups = SMOOTH_GROUP_INDUSTRIAL_LIFT
canSmoothWith = SMOOTH_GROUP_INDUSTRIAL_LIFT
- obj_flags = CAN_BE_HIT | BLOCK_Z_OUT_DOWN
+ obj_flags = BLOCK_Z_OUT_DOWN
appearance_flags = PIXEL_SCALE|KEEP_TOGETHER //no TILE_BOUND since we're potentially multitile
// If we don't do this, we'll build our overlays early, and fuck up how we're rendered
blocks_emissive = EMISSIVE_BLOCK_NONE
- ///ID used to determine what lift types we can merge with
- var/lift_id = BASIC_LIFT_ID
+ ///ID used to determine what transport types we can merge with
+ var/transport_id = TRANSPORT_TYPE_ELEVATOR
///if true, the elevator works through floors
var/pass_through_floors = FALSE
///what movables on our platform that we are moving
- var/list/atom/movable/lift_load = list()
+ var/list/atom/movable/transport_contents = list()
///weakrefs to the contents we have when we're first created. stored so that admins can clear the tram to its initial state
///if someone put a bunch of stuff onto it.
var/list/datum/weakref/initial_contents = list()
///what glide_size we set our moving contents to.
var/glide_size_override = 8
- ///movables inside lift_load who had their glide_size changed since our last movement.
+ ///movables inside transport_contents who had their glide_size changed since our last movement.
///used so that we dont have to change the glide_size of every object every movement, which scales to cost more than you'd think
var/list/atom/movable/changed_gliders = list()
- ///master datum that controls our movement. in general /industrial_lift subtypes control moving themselves, and
- /// /datum/lift_master instances control moving the entire tram and any behavior associated with that.
- var/datum/lift_master/lift_master_datum
- ///what subtype of /datum/lift_master to create for itself if no other platform on this tram has created one yet.
+ ///decisecond delay between horizontal movements. cannot make the tram move faster than 1 movement per world.tick_lag. only used to give to the transport_controller
+ var/speed_limiter = 0.5
+
+ ///master datum that controls our movement. in general /transport/linear subtypes control moving themselves, and
+ /// /datum/transport_controller instances control moving the entire tram and any behavior associated with that.
+ var/datum/transport_controller/linear/transport_controller_datum
+ ///what subtype of /datum/transport_controller to create for itself if no other platform on this tram has created one yet.
///very important for some behaviors since
- var/lift_master_type = /datum/lift_master
+ var/transport_controller_type = /datum/transport_controller/linear
///how many tiles this platform extends on the x axis
var/width = 1
@@ -61,7 +57,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
var/height = 1
///if TRUE, this platform will late initialize and then expand to become a multitile object across all other linked platforms on this z level
- var/create_multitile_platform = FALSE
+ var/create_modular_set = FALSE
/// Does our elevator warn people (with visual effects) when moving down?
var/warns_on_down_movement = FALSE
@@ -77,91 +73,91 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
/// A lazylist of REFs to all mobs which have a radial open currently
var/list/current_operators
-/datum/armor/structure_industrial_lift
- melee = 50
- fire = 80
- acid = 50
+/datum/armor/transport_module
+ melee = 80
+ bullet = 90
+ bomb = 70
+ fire = 100
+ acid = 100
-/obj/structure/industrial_lift/Initialize(mapload)
+/obj/structure/transport/linear/Initialize(mapload)
. = ..()
- GLOB.lifts.Add(src)
-
// Yes if it's VV'd it won't be accurate but it probably shouldn't ever be
if(radial_travel)
- AddElement(/datum/element/contextual_screentip_bare_hands, lmb_text = "Send Elevator")
+ AddElement(/datum/element/contextual_screentip_bare_hands, lmb_text = "Send Transport")
set_movement_registrations()
- //since lift_master datums find all connected platforms when an industrial lift first creates it and then
- //sets those platforms' lift_master_datum to itself, this check will only evaluate to true once per tram platform
- if(!lift_master_datum && lift_master_type)
- lift_master_datum = new lift_master_type(src)
+ //since transport_controller datums find all connected platforms when a transport structure first creates it and then
+ //sets those platforms' transport_controller_datum to itself, this check will only evaluate to true once per tram platform
+ if(!transport_controller_datum && transport_controller_type)
+ transport_controller_datum = new transport_controller_type(src)
return INITIALIZE_HINT_LATELOAD
-/obj/structure/industrial_lift/LateInitialize()
- //after everything is initialized the lift master can order everything
- lift_master_datum.order_platforms_by_z_level()
+/obj/structure/transport/linear/LateInitialize()
+ . = ..()
+ //after everything is initialized the transport controller can order everything
+ transport_controller_datum.order_platforms_by_z_level()
-/obj/structure/industrial_lift/Destroy()
- GLOB.lifts.Remove(src)
- lift_master_datum = null
+/obj/structure/transport/linear/Destroy()
+ transport_controller_datum = null
return ..()
///set the movement registrations to our current turf(s) so contents moving out of our tile(s) are removed from our movement lists
-/obj/structure/industrial_lift/proc/set_movement_registrations(list/turfs_to_set)
+/obj/structure/transport/linear/proc/set_movement_registrations(list/turfs_to_set)
for(var/turf/turf_loc as anything in turfs_to_set || locs)
- RegisterSignal(turf_loc, COMSIG_ATOM_EXITED, PROC_REF(UncrossedRemoveItemFromLift))
- RegisterSignals(turf_loc, list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON), PROC_REF(AddItemOnLift))
+ RegisterSignal(turf_loc, COMSIG_ATOM_EXITED, PROC_REF(uncrossed_remove_item_from_transport))
+ RegisterSignals(turf_loc, list(COMSIG_ATOM_ENTERED,COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON), PROC_REF(add_item_on_transport))
///unset our movement registrations from turfs that no longer contain us (or every loc if turfs_to_unset is unspecified)
-/obj/structure/industrial_lift/proc/unset_movement_registrations(list/turfs_to_unset)
+/obj/structure/transport/linear/proc/unset_movement_registrations(list/turfs_to_unset)
var/static/list/registrations = list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_EXITED, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON)
for(var/turf/turf_loc as anything in turfs_to_unset || locs)
UnregisterSignal(turf_loc, registrations)
-/obj/structure/industrial_lift/proc/UncrossedRemoveItemFromLift(datum/source, atom/movable/gone, direction)
+/obj/structure/transport/linear/proc/uncrossed_remove_item_from_transport(datum/source, atom/movable/gone, direction)
SIGNAL_HANDLER
if(!(gone.loc in locs))
- RemoveItemFromLift(gone)
+ remove_item_from_transport(gone)
-/obj/structure/industrial_lift/proc/RemoveItemFromLift(atom/movable/potential_rider)
+/obj/structure/transport/linear/proc/remove_item_from_transport(atom/movable/potential_rider)
SIGNAL_HANDLER
- if(!(potential_rider in lift_load))
+ if(!(potential_rider in transport_contents))
return
if(isliving(potential_rider) && HAS_TRAIT(potential_rider, TRAIT_CANNOT_BE_UNBUCKLED))
REMOVE_TRAIT(potential_rider, TRAIT_CANNOT_BE_UNBUCKLED, BUCKLED_TRAIT)
- lift_load -= potential_rider
+ transport_contents -= potential_rider
changed_gliders -= potential_rider
UnregisterSignal(potential_rider, list(COMSIG_QDELETING, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE))
-/obj/structure/industrial_lift/proc/AddItemOnLift(datum/source, atom/movable/new_lift_contents)
+/obj/structure/transport/linear/proc/add_item_on_transport(datum/source, atom/movable/new_transport_contents)
SIGNAL_HANDLER
- var/static/list/blacklisted_types = typecacheof(list(/obj/structure/fluff/tram_rail, /obj/effect/decal/cleanable, /obj/structure/industrial_lift, /mob/camera))
- if(is_type_in_typecache(new_lift_contents, blacklisted_types) || new_lift_contents.invisibility == INVISIBILITY_ABSTRACT) //prevents the tram from stealing things like landmarks
+ var/static/list/blacklisted_types = typecacheof(list(/obj/structure/fluff/tram_rail, /obj/effect/decal/cleanable, /obj/structure/transport/linear, /mob/camera))
+ if(is_type_in_typecache(new_transport_contents, blacklisted_types) || new_transport_contents.invisibility == INVISIBILITY_ABSTRACT) //prevents the tram from stealing things like landmarks
return FALSE
- if(new_lift_contents in lift_load)
+ if(new_transport_contents in transport_contents)
return FALSE
- if(isliving(new_lift_contents) && !HAS_TRAIT(new_lift_contents, TRAIT_CANNOT_BE_UNBUCKLED))
- ADD_TRAIT(new_lift_contents, TRAIT_CANNOT_BE_UNBUCKLED, BUCKLED_TRAIT)
+ if(isliving(new_transport_contents) && !HAS_TRAIT(new_transport_contents, TRAIT_CANNOT_BE_UNBUCKLED))
+ ADD_TRAIT(new_transport_contents, TRAIT_CANNOT_BE_UNBUCKLED, BUCKLED_TRAIT)
- lift_load += new_lift_contents
- RegisterSignal(new_lift_contents, COMSIG_QDELETING, PROC_REF(RemoveItemFromLift))
+ transport_contents += new_transport_contents
+ RegisterSignal(new_transport_contents, COMSIG_QDELETING, PROC_REF(remove_item_from_transport))
return TRUE
-///adds everything on our tile that can be added to our lift_load and initial_contents lists when we're created
-/obj/structure/industrial_lift/proc/add_initial_contents()
+///adds everything on our tile that can be added to our transport_contents and initial_contents lists when we're created
+/obj/structure/transport/linear/proc/add_initial_contents()
for(var/turf/turf_loc in locs)
for(var/atom/movable/movable_contents as anything in turf_loc)
if(movable_contents == src)
continue
- if(AddItemOnLift(src, movable_contents))
+ if(add_item_on_transport(src, movable_contents))
var/datum/weakref/new_initial_contents = WEAKREF(movable_contents)
if(!new_initial_contents)
@@ -169,39 +165,39 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
initial_contents += new_initial_contents
-///signal handler for COMSIG_MOVABLE_UPDATE_GLIDE_SIZE: when a movable in lift_load changes its glide_size independently.
+///signal handler for COMSIG_MOVABLE_UPDATE_GLIDE_SIZE: when a movable in transport_contents changes its glide_size independently.
///adds that movable to a lazy list, movables in that list have their glide_size updated when the tram next moves
-/obj/structure/industrial_lift/proc/on_changed_glide_size(atom/movable/moving_contents, new_glide_size)
+/obj/structure/transport/linear/proc/on_changed_glide_size(atom/movable/moving_contents, new_glide_size)
SIGNAL_HANDLER
if(new_glide_size != glide_size_override)
changed_gliders += moving_contents
///make this tram platform multitile, expanding to cover all the tram platforms adjacent to us and deleting them. makes movement more efficient.
-///the platform becoming multitile should be in the bottom left corner since thats assumed to be the loc of multitile objects
-/obj/structure/industrial_lift/proc/create_multitile_platform(min_x, min_y, max_x, max_y, z)
+///the platform becoming multitile should be in the lower left corner since thats assumed to be the loc of multitile objects
+/obj/structure/transport/linear/proc/create_modular_set(min_x, min_y, max_x, max_y, z)
if(!(min_x && min_y && max_x && max_y && z))
- for(var/obj/structure/industrial_lift/other_lift as anything in lift_master_datum.lift_platforms)
- if(other_lift.z != z)
+ for(var/obj/structure/transport/linear/other_transport as anything in transport_controller_datum.transport_modules)
+ if(other_transport.z != z)
continue
- min_x = min(min_x, other_lift.x)
- max_x = max(max_x, other_lift.x)
+ min_x = min(min_x, other_transport.x)
+ max_x = max(max_x, other_transport.x)
- min_y = min(min_y, other_lift.y)
- max_y = max(max_y, other_lift.y)
+ min_y = min(min_y, other_transport.y)
+ max_y = max(max_y, other_transport.y)
- var/turf/bottom_left_loc = locate(min_x, min_y, z)
- var/obj/structure/industrial_lift/loc_corner_lift = locate() in bottom_left_loc
+ var/turf/lower_left_corner = locate(min_x, min_y, z)
+ var/obj/structure/transport/linear/primary_module = locate() in lower_left_corner
- if(!loc_corner_lift)
- stack_trace("no lift in the bottom left corner of a lift level!")
+ if(!primary_module)
+ stack_trace("no lift in the lower left corner of a lift level!")
return FALSE
- if(loc_corner_lift != src)
+ if(primary_module != src)
//the loc of a multitile object must always be the lower left corner
- return loc_corner_lift.create_multitile_platform()
+ return primary_module.create_modular_set()
width = (max_x - min_x) + 1
height = (max_y - min_y) + 1
@@ -229,40 +225,40 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
var/x_pixel_offset = world.icon_size * x
- var/turf/lift_turf = locate(x + min_x, y + min_y, z)
+ var/turf/set_turf = locate(x + min_x, y + min_y, z)
- if(!lift_turf)
+ if(!set_turf)
continue
- if(lift_turf in locs_to_skip)
+ if(set_turf in locs_to_skip)
continue
- var/obj/structure/industrial_lift/other_lift = locate() in lift_turf
+ var/obj/structure/transport/linear/other_transport = locate() in set_turf
- if(!other_lift)
+ if(!other_transport)
continue
- locs_to_skip += other_lift.locs.Copy()//make sure we never go over multitile platforms multiple times
+ locs_to_skip += other_transport.locs.Copy()//make sure we never go over multitile platforms multiple times
- other_lift.pixel_x = x_pixel_offset
- other_lift.pixel_y = y_pixel_offset
+ other_transport.pixel_x = x_pixel_offset
+ other_transport.pixel_y = y_pixel_offset
- overlays += other_lift
+ overlays += other_transport
//now we vore all the other lifts connected to us on our z level
- for(var/obj/structure/industrial_lift/other_lift in lift_master_datum.lift_platforms)
- if(other_lift == src || other_lift.z != z)
+ for(var/obj/structure/transport/linear/other_transport in transport_controller_datum.transport_modules)
+ if(other_transport == src || other_transport.z != z)
continue
- lift_master_datum.lift_platforms -= other_lift
- if(other_lift.lift_load)
- lift_load |= other_lift.lift_load
- if(other_lift.initial_contents)
- initial_contents |= other_lift.initial_contents
+ transport_controller_datum.transport_modules -= other_transport
+ if(other_transport.transport_contents)
+ transport_contents |= other_transport.transport_contents
+ if(other_transport.initial_contents)
+ initial_contents |= other_transport.initial_contents
- qdel(other_lift)
+ qdel(other_transport)
- lift_master_datum.multitile_platform = TRUE
+ transport_controller_datum.create_modular_set = TRUE
var/turf/old_loc = loc
@@ -272,44 +268,44 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
update_appearance()
return TRUE
-///returns an unordered list of all lift platforms adjacent to us. used so our lift_master_datum can control all connected platforms.
-///includes platforms directly above or below us as well. only includes platforms with an identical lift_id to our own.
-/obj/structure/industrial_lift/proc/lift_platform_expansion(datum/lift_master/lift_master_datum)
+///returns an unordered list of all lift platforms adjacent to us. used so our transport_controller_datum can control all connected platforms.
+///includes platforms directly above or below us as well. only includes platforms with an identical transport_id to our own.
+/obj/structure/transport/linear/proc/module_adjacency(datum/transport_controller/transport_controller_datum)
. = list()
for(var/direction in GLOB.cardinals_multiz)
- var/obj/structure/industrial_lift/neighbor = locate() in get_step_multiz(src, direction)
- if(!neighbor || neighbor.lift_id != lift_id)
+ var/obj/structure/transport/linear/neighbor = locate() in get_step_multiz(src, direction)
+ if(!neighbor || neighbor.transport_id != transport_id)
continue
. += neighbor
-///main proc for moving the lift in the direction [going]. handles horizontal and/or vertical movement for multi platformed lifts and multitile lifts.
-/obj/structure/industrial_lift/proc/travel(going)
- var/list/things_to_move = lift_load
+///main proc for moving the lift in the direction [travel_direction]. handles horizontal and/or vertical movement for multi platformed lifts and multitile lifts.
+/obj/structure/transport/linear/proc/travel(travel_direction)
+ var/list/things_to_move = transport_contents
var/turf/destination
- if(!isturf(going))
- destination = get_step_multiz(src, going)
+ if(!isturf(travel_direction))
+ destination = get_step_multiz(src, travel_direction)
else
- destination = going
- going = get_dir_multiz(loc, going)
+ destination = travel_direction
+ travel_direction = get_dir_multiz(loc, travel_direction)
var/x_offset = ROUND_UP(bound_width / 32) - 1 //how many tiles our horizontally farthest edge is from us
var/y_offset = ROUND_UP(bound_height / 32) - 1 //how many tiles our vertically farthest edge is from us
//the x coordinate of the edge furthest from our future destination, which would be our right hand side
var/back_edge_x = destination.x + x_offset//if we arent multitile this should just be destination.x
- var/top_edge_y = destination.y + y_offset
+ var/upper_edge_y = destination.y + y_offset
- var/turf/top_right_corner = locate(min(world.maxx, back_edge_x), min(world.maxy, top_edge_y), destination.z)
+ var/turf/upper_right_corner = locate(min(world.maxx, back_edge_x), min(world.maxy, upper_edge_y), destination.z)
var/list/dest_locs = block(
destination,
- top_right_corner
+ upper_right_corner
)
var/list/entering_locs = dest_locs - locs
var/list/exited_locs = locs - dest_locs
- if(going == DOWN)
+ if(travel_direction == DOWN)
for(var/turf/dest_turf as anything in entering_locs)
SEND_SIGNAL(dest_turf, COMSIG_TURF_INDUSTRIAL_LIFT_ENTER, things_to_move)
@@ -337,7 +333,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
crushed.apply_damage(15, BRUTE, BODY_ZONE_L_ARM, wound_bonus = 15)
crushed.apply_damage(15, BRUTE, BODY_ZONE_R_ARM, wound_bonus = 15)
- else if(going == UP)
+ else if(travel_direction == UP)
for(var/turf/dest_turf as anything in entering_locs)
///handles any special interactions objects could have with the lift/tram, handled on the item itself
SEND_SIGNAL(dest_turf, COMSIG_TURF_INDUSTRIAL_LIFT_ENTER, things_to_move)
@@ -376,7 +372,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
for(var/obj/structure/victim_structure in dest_turf.contents)
if(QDELING(victim_structure))
continue
- if(!is_type_in_typecache(victim_structure, lift_master_datum.ignored_smashthroughs) && victim_structure.layer >= LOW_OBJ_LAYER)
+ if(!is_type_in_typecache(victim_structure, transport_controller_datum.ignored_smashthroughs) && victim_structure.layer >= LOW_OBJ_LAYER)
if(victim_structure.anchored && initial(victim_structure.anchored) == TRUE)
visible_message(span_danger("[src] smashes through [victim_structure]!"))
@@ -384,7 +380,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
else
if(!throw_target)
- throw_target = get_edge_target_turf(src, turn(going, pick(45, -45)))
+ throw_target = get_edge_target_turf(src, turn(travel_direction, pick(45, -45)))
visible_message(span_danger("[src] violently rams [victim_structure] out of the way!"))
victim_structure.anchored = FALSE
victim_structure.take_damage(rand(20, 25) * collision_lethality)
@@ -393,7 +389,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
for(var/obj/machinery/victim_machine in dest_turf.contents)
if(QDELING(victim_machine))
continue
- if(is_type_in_typecache(victim_machine, lift_master_datum.ignored_smashthroughs))
+ if(is_type_in_typecache(victim_machine, transport_controller_datum.ignored_smashthroughs))
continue
if(istype(victim_machine, /obj/machinery/field)) //graceful break handles this scenario
continue
@@ -402,45 +398,69 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
visible_message(span_danger("[src] smashes through [victim_machine]!"))
qdel(victim_machine)
- for(var/mob/living/collided in dest_turf.contents)
- var/damage_multiplier = collided.maxHealth * 0.01
- if(lift_master_datum.ignored_smashthroughs[collided.type])
+ for(var/mob/living/victim_living in dest_turf.contents)
+ var/damage_multiplier = victim_living.maxHealth * 0.01
+ var/extra_ouch = FALSE // if emagged you're gonna have a really bad time
+ if(speed_limiter == 0.5) // slow trams don't cause extra damage
+ for(var/obj/structure/tram/spoiler/my_spoiler in transport_contents)
+ if(get_dist(my_spoiler, victim_living) != 1)
+ continue
+
+ if(my_spoiler.deployed)
+ extra_ouch = TRUE
+ break
+
+ if(transport_controller_datum.ignored_smashthroughs[victim_living.type])
continue
- to_chat(collided, span_userdanger("[src] collides into you!"))
+ to_chat(victim_living, span_userdanger("[src] collides into you!"))
playsound(src, 'sound/effects/splat.ogg', 50, TRUE)
var/damage = 0
- if(prob(15)) //sorry buddy, luck wasn't on your side
- damage = 29 * collision_lethality * damage_multiplier
- else
- damage = rand(7, 21) * collision_lethality * damage_multiplier
- collided.apply_damage(2 * damage, BRUTE, BODY_ZONE_HEAD, wound_bonus = 7)
- collided.apply_damage(3 * damage, BRUTE, BODY_ZONE_CHEST, wound_bonus = 21)
- collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_LEG, wound_bonus = 14)
- collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_LEG, wound_bonus = 14)
- collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_ARM, wound_bonus = 14)
- collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_ARM, wound_bonus = 14)
- log_combat(src, collided, "collided with")
-
- if(QDELETED(collided)) //in case it was a mob that dels on death
+ switch(extra_ouch)
+ if(TRUE)
+ playsound(src, 'sound/effects/grillehit.ogg', 50, TRUE)
+ var/obj/item/bodypart/head/head = victim_living.get_bodypart("head")
+ if(head)
+ log_combat(src, victim_living, "beheaded")
+ head.dismember()
+ victim_living.regenerate_icons()
+ add_overlay(mutable_appearance(icon, "blood_overlay"))
+
+ if(FALSE)
+ log_combat(src, victim_living, "collided with")
+ if(prob(15)) //sorry buddy, luck wasn't on your side
+ damage = 29 * collision_lethality * damage_multiplier
+ else
+ damage = rand(7, 21) * collision_lethality * damage_multiplier
+ victim_living.apply_damage(2 * damage, BRUTE, BODY_ZONE_HEAD, wound_bonus = 7)
+ victim_living.apply_damage(3 * damage, BRUTE, BODY_ZONE_CHEST, wound_bonus = 21)
+ victim_living.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_LEG, wound_bonus = 14)
+ victim_living.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_LEG, wound_bonus = 14)
+ victim_living.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_ARM, wound_bonus = 14)
+ victim_living.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_ARM, wound_bonus = 14)
+
+ if(QDELETED(victim_living)) //in case it was a mob that dels on death
continue
if(!throw_target)
- throw_target = get_edge_target_turf(src, turn(going, pick(45, -45)))
+ throw_target = get_edge_target_turf(src, turn(travel_direction, pick(45, -45)))
- var/turf/T = get_turf(collided)
- T.add_mob_blood(collided)
+ var/turf/turf_to_bloody = get_turf(victim_living)
+ turf_to_bloody.add_mob_blood(victim_living)
- collided.throw_at()
- //if going EAST, will turn to the NORTHEAST or SOUTHEAST and throw the ran over guy away
- var/datum/callback/land_slam = new(collided, TYPE_PROC_REF(/mob/living/, tram_slam_land))
- collided.throw_at(throw_target, 200 * collision_lethality, 4 * collision_lethality, callback = land_slam)
+ victim_living.throw_at()
+ //if travel_direction EAST, will turn to the NORTHEAST or SOUTHEAST and throw the ran over guy away
+ var/datum/callback/land_slam = new(victim_living, TYPE_PROC_REF(/mob/living/, tram_slam_land))
+ victim_living.throw_at(throw_target, 200 * collision_lethality, 4 * collision_lethality, callback = land_slam)
- //increment the hit counter signs
- if(ismob(collided) && collided.client)
- SSpersistence.tram_hits_this_round++
- SEND_SIGNAL(src, COMSIG_TRAM_COLLISION, SSpersistence.tram_hits_this_round)
+ //increment the hit counters
+ if(ismob(victim_living) && victim_living.client)
+ if(istype(transport_controller_datum, /datum/transport_controller/linear/tram))
+ SSpersistence.tram_hits_this_round++
+ SSblackbox.record_feedback("amount", "tram_collision", 1)
+ var/datum/transport_controller/linear/tram/tram_controller = transport_controller_datum
+ tram_controller.register_collision()
unset_movement_registrations(exited_locs)
- group_move(things_to_move, going)
+ group_move(things_to_move, travel_direction)
set_movement_registrations(entering_locs)
///move the movers list of movables on our tile to destination if we successfully move there first.
@@ -449,9 +469,9 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
///none of the movers are able to react to the movement of any other mover, saving a lot of needless processing cost
///and is more sensible. without this, if you and a banana are on the same platform, when that platform moves you will slip
///on the banana even if youre not moving relative to it.
-/obj/structure/industrial_lift/proc/group_move(list/atom/movable/movers, movement_direction)
+/obj/structure/transport/linear/proc/group_move(list/atom/movable/movers, movement_direction)
if(movement_direction == NONE)
- stack_trace("an industrial lift was told to move to somewhere it already is!")
+ stack_trace("a transport was told to move to somewhere it already is!")
return FALSE
var/turf/our_dest = get_step(src, movement_direction)
@@ -509,7 +529,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
* reset the contents of this lift platform to its original state in case someone put too much shit on it.
* everything that is considered foreign is deleted, you can configure what is considered foreign.
*
- * used by an admin via calling reset_lift_contents() on our lift_master_datum.
+ * used by an admin via calling reset_lift_contents() on our transport_controller_datum.
*
* Arguments:
* * consider_anything_past - number. if > 0 this platform will only handle foreign contents that exceed this number on each of our locs
@@ -517,17 +537,17 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
* * foreign_non_player_mobs - bool. if true we consider mobs that dont have a mind to be foreign
* * consider_player_mobs - bool. if true we consider player mobs to be foreign. only works if foreign_non_player_mobs is true as well
*/
-/obj/structure/industrial_lift/proc/reset_contents(consider_anything_past = 0, foreign_objects = TRUE, foreign_non_player_mobs = TRUE, consider_player_mobs = FALSE)
+/obj/structure/transport/linear/proc/reset_contents(consider_anything_past = 0, foreign_objects = TRUE, foreign_non_player_mobs = TRUE, consider_player_mobs = FALSE)
if(!foreign_objects && !foreign_non_player_mobs && !consider_player_mobs)
return FALSE
consider_anything_past = isnum(consider_anything_past) ? max(consider_anything_past, 0) : 0
//just in case someone fucks up the arguments
- if(consider_anything_past && length(lift_load) <= consider_anything_past)
+ if(consider_anything_past && length(transport_contents) <= consider_anything_past)
return FALSE
- ///list of resolve()'d initial_contents that are still in lift_load
+ ///list of resolve()'d initial_contents that are still in transport_contents
var/list/atom/movable/original_contents = list(src)
///list of objects we consider foreign according to the given arguments
@@ -543,7 +563,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
if(!resolved_contents)
continue
- if(!(resolved_contents in lift_load))
+ if(!(resolved_contents in transport_contents))
continue
original_contents += resolved_contents
@@ -552,7 +572,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
var/list/atom/movable/foreign_contents_in_loc = list()
for(var/atom/movable/foreign_movable as anything in (turf_loc.contents - original_contents))
- if(foreign_objects && ismovable(foreign_movable) && !ismob(foreign_movable) && !istype(foreign_movable, /obj/effect/landmark/tram))
+ if(foreign_objects && ismovable(foreign_movable) && !ismob(foreign_movable) && !istype(foreign_movable, /obj/effect/landmark/transport/nav_beacon))
foreign_contents_in_loc += foreign_movable
continue
@@ -577,7 +597,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
return TRUE
/// Callback / general proc to check if the lift is usable by the passed mob.
-/obj/structure/industrial_lift/proc/can_open_lift_radial(mob/living/user, starting_position)
+/obj/structure/transport/linear/proc/can_open_lift_radial(mob/living/user, starting_position)
// Gotta be a living mob
if(!isliving(user))
return FALSE
@@ -597,25 +617,25 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
return TRUE
/// Opens the radial for the lift, allowing the user to move it around.
-/obj/structure/industrial_lift/proc/open_lift_radial(mob/living/user)
+/obj/structure/transport/linear/proc/open_lift_radial(mob/living/user)
var/starting_position = loc
if(!can_open_lift_radial(user, starting_position))
return
// One radial per person
- for(var/obj/structure/industrial_lift/other_platform as anything in lift_master_datum.lift_platforms)
+ for(var/obj/structure/transport/linear/other_platform as anything in transport_controller_datum.transport_modules)
if(REF(user) in other_platform.current_operators)
return
var/list/possible_directions = list()
- if(lift_master_datum.Check_lift_move(UP))
+ if(transport_controller_datum.Check_lift_move(UP))
var/static/image/up_arrow
if(!up_arrow)
up_arrow = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTH)
possible_directions["Up"] = up_arrow
- if(lift_master_datum.Check_lift_move(DOWN))
+ if(transport_controller_datum.Check_lift_move(DOWN))
var/static/image/down_arrow
if(!down_arrow)
down_arrow = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTH)
@@ -640,7 +660,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
LAZYREMOVE(current_operators, REF(user))
if(!can_open_lift_radial(user, starting_position))
return //nice try
- if(!isnull(result) && result != "Cancel" && lift_master_datum.controls_locked)
+ if(!isnull(result) && result != "Cancel" && transport_controller_datum.controller_status & CONTROLS_LOCKED)
// Only show this message if they actually wanted to move
balloon_alert(user, "elevator controls locked!")
return
@@ -648,14 +668,14 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
if("Up")
// We have to make sure that they don't do illegal actions
// by not having their radial menu refresh from someone else moving the lift.
- if(!lift_master_datum.simple_move_wrapper(UP, elevator_vertical_speed, user))
+ if(!transport_controller_datum.simple_move_wrapper(UP, elevator_vertical_speed, user))
return
show_fluff_message(UP, user)
open_lift_radial(user)
if("Down")
- if(!lift_master_datum.simple_move_wrapper(DOWN, elevator_vertical_speed, user))
+ if(!transport_controller_datum.simple_move_wrapper(DOWN, elevator_vertical_speed, user))
return
show_fluff_message(DOWN, user)
@@ -673,12 +693,12 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
* Returns:
* * boolean, FALSE if the menu should be closed, TRUE if the menu is clear to stay opened.
*/
-/obj/structure/industrial_lift/proc/check_menu(mob/user, starting_loc)
+/obj/structure/transport/linear/proc/check_menu(mob/user, starting_loc)
if(user.incapacitated() || !user.Adjacent(src) || starting_loc != src.loc)
return FALSE
return TRUE
-/obj/structure/industrial_lift/attack_hand(mob/user, list/modifiers)
+/obj/structure/transport/linear/attack_hand(mob/user, list/modifiers)
. = ..()
if(.)
return
@@ -688,7 +708,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
return open_lift_radial(user)
//ai probably shouldn't get to use lifts but they sure are great for admins to crush people with
-/obj/structure/industrial_lift/attack_ghost(mob/user)
+/obj/structure/transport/linear/attack_ghost(mob/user)
. = ..()
if(.)
return
@@ -699,19 +719,19 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
return open_lift_radial(user)
-/obj/structure/industrial_lift/attack_paw(mob/user, list/modifiers)
+/obj/structure/transport/linear/attack_paw(mob/user, list/modifiers)
if(!radial_travel)
return ..()
return open_lift_radial(user)
-/obj/structure/industrial_lift/attackby(obj/item/attacking_item, mob/user, params)
+/obj/structure/transport/linear/attackby(obj/item/attacking_item, mob/user, params)
if(!radial_travel)
return ..()
return open_lift_radial(user)
-/obj/structure/industrial_lift/attack_robot(mob/living/user)
+/obj/structure/transport/linear/attack_robot(mob/living/user)
if(!radial_travel)
return ..()
@@ -723,7 +743,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
* * direction - What direction are we going
* * user - The mob that caused the lift to move, for the visible message.
*/
-/obj/structure/industrial_lift/proc/show_fluff_message(direction, mob/user)
+/obj/structure/transport/linear/proc/show_fluff_message(direction, mob/user)
if(direction == UP)
user.visible_message(span_notice("[user] moves the lift upwards."), span_notice("You move the lift upwards."))
@@ -731,123 +751,123 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
user.visible_message(span_notice("[user] moves the lift downwards."), span_notice("You move the lift downwards."))
// A subtype intended for "public use"
-/obj/structure/industrial_lift/public
+/obj/structure/transport/linear/public
icon = 'icons/turf/floors.dmi'
icon_state = "rockvault"
base_icon_state = null
smoothing_flags = NONE
smoothing_groups = null
canSmoothWith = null
- resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
warns_on_down_movement = TRUE
violent_landing = FALSE
elevator_vertical_speed = 3 SECONDS
radial_travel = FALSE
-/obj/structure/industrial_lift/debug
+/obj/structure/transport/linear/debug
name = "transport platform"
desc = "A lightweight platform. It moves in any direction, except up and down."
color = "#5286b9ff"
- lift_id = DEBUG_LIFT_ID
+ transport_id = TRANSPORT_TYPE_DEBUG
radial_travel = TRUE
-/obj/structure/industrial_lift/debug/open_lift_radial(mob/living/user)
+/obj/structure/transport/linear/debug/open_lift_radial(mob/living/user)
var/starting_position = loc
if (!can_open_lift_radial(user,starting_position))
return
-
- var/result = show_radial_menu(user, src, GLOB.all_radial_directions, custom_check = CALLBACK(src, PROC_REF(can_open_lift_radial), user, starting_position), require_near = TRUE, tooltips = FALSE)
+//NORTH, SOUTH, EAST, WEST, NORTHEAST, NORTHWEST, SOUTHEAST, SOUTHWEST
+ var/static/list/tool_list = list(
+ "NORTH" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTH),
+ "NORTHEAST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTH),
+ "EAST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = EAST),
+ "SOUTHEAST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = EAST),
+ "SOUTH" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTH),
+ "SOUTHWEST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTH),
+ "WEST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = WEST),
+ "NORTHWEST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = WEST)
+ )
+
+ var/result = show_radial_menu(user, src, tool_list, custom_check = CALLBACK(src, PROC_REF(can_open_lift_radial), user, starting_position), require_near = TRUE, tooltips = FALSE)
if (!can_open_lift_radial(user,starting_position))
return // nice try
- if(!isnull(result) && result != "Cancel" && lift_master_datum.controls_locked)
+ if(!isnull(result) && result != "Cancel" && transport_controller_datum.controller_status & CONTROLS_LOCKED)
// Only show this message if they actually wanted to move
balloon_alert(user, "elevator controls locked!")
return
switch(result)
if("NORTH")
- lift_master_datum.move_lift_horizontally(NORTH)
+ transport_controller_datum.move_transport_horizontally(NORTH)
open_lift_radial(user)
if("NORTHEAST")
- lift_master_datum.move_lift_horizontally(NORTHEAST)
+ transport_controller_datum.move_transport_horizontally(NORTHEAST)
open_lift_radial(user)
if("EAST")
- lift_master_datum.move_lift_horizontally(EAST)
+ transport_controller_datum.move_transport_horizontally(EAST)
open_lift_radial(user)
if("SOUTHEAST")
- lift_master_datum.move_lift_horizontally(SOUTHEAST)
+ transport_controller_datum.move_transport_horizontally(SOUTHEAST)
open_lift_radial(user)
if("SOUTH")
- lift_master_datum.move_lift_horizontally(SOUTH)
+ transport_controller_datum.move_transport_horizontally(SOUTH)
open_lift_radial(user)
if("SOUTHWEST")
- lift_master_datum.move_lift_horizontally(SOUTHWEST)
+ transport_controller_datum.move_transport_horizontally(SOUTHWEST)
open_lift_radial(user)
if("WEST")
- lift_master_datum.move_lift_horizontally(WEST)
+ transport_controller_datum.move_transport_horizontally(WEST)
open_lift_radial(user)
if("NORTHWEST")
- lift_master_datum.move_lift_horizontally(NORTHWEST)
+ transport_controller_datum.move_transport_horizontally(NORTHWEST)
open_lift_radial(user)
if("Cancel")
return
add_fingerprint(user)
-/obj/structure/industrial_lift/tram
- name = "tram"
- desc = "A tram for tramversing the station."
- icon = 'icons/turf/floors.dmi'
- icon_state = "textured_large"
- layer = TRAM_FLOOR_LAYER
+/obj/structure/transport/linear/tram
+ name = "tram subfloor"
+ desc = "The subfloor lattice of the tram. You can build a tram wall frame by using titanium sheets, or place down thermoplastic tram floor tiles."
+ icon = 'icons/obj/tram/tram_structure.dmi'
+ icon_state = "subfloor"
base_icon_state = null
+ density = FALSE
+ layer = TRAM_STRUCTURE_LAYER
smoothing_flags = NONE
smoothing_groups = null
canSmoothWith = null
- //kind of a centerpiece of the station, so pretty tough to destroy
- resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
-
- lift_id = TRAM_LIFT_ID
- lift_master_type = /datum/lift_master/tram
+ //the modular structure is pain to work with, damage is done to the floor on top
+ transport_id = TRANSPORT_TYPE_TRAM
+ transport_controller_type = /datum/transport_controller/linear/tram
radial_travel = FALSE
-
+ obj_flags = NONE
/// Set by the tram control console in late initialize
var/travelling = FALSE
- //the following are only used to give to the lift_master datum when it's first created
-
- ///decisecond delay between horizontal movements. cannot make the tram move faster than 1 movement per world.tick_lag. only used to give to the lift_master
- var/horizontal_speed = 0.5
-
- create_multitile_platform = TRUE
+ /// Do we want this transport to link with nearby modules to make a multi-tile platform
+ create_modular_set = TRUE
-/obj/structure/industrial_lift/tram/white
- icon_state = "textured_white_large"
+/obj/structure/transport/linear/tram/corner/northwest
+ icon_state = "subfloor-corner-nw"
-/obj/structure/industrial_lift/tram/purple
- icon_state = "titanium_purple"
+/obj/structure/transport/linear/tram/corner/southwest
+ icon_state = "subfloor-corner-sw"
-/obj/structure/industrial_lift/tram/subfloor
- icon_state = "tram_subfloor"
+/obj/structure/transport/linear/tram/corner/northeast
+ icon_state = "subfloor-corner-ne"
-/obj/structure/industrial_lift/tram/subfloor/window
- icon_state = "tram_subfloor_window"
+/obj/structure/transport/linear/tram/corner/southeast
+ icon_state = "subfloor-corner-se"
-/datum/armor/structure_industrial_lift
- melee = 50
- fire = 80
- acid = 50
-
-/obj/structure/industrial_lift/tram/AddItemOnLift(datum/source, atom/movable/AM)
+/obj/structure/transport/linear/tram/add_item_on_transport(datum/source, atom/movable/item)
. = ..()
if(travelling)
- on_changed_glide_size(AM, AM.glide_size)
+ on_changed_glide_size(item, item.glide_size)
-/obj/structure/industrial_lift/tram/proc/set_travelling(travelling)
+/obj/structure/transport/linear/tram/proc/set_travelling(travelling)
if (src.travelling == travelling)
return
- for(var/atom/movable/glider as anything in lift_load)
+ for(var/atom/movable/glider as anything in transport_contents)
if(travelling)
glider.set_glide_size(glide_size_override)
RegisterSignal(glider, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, PROC_REF(on_changed_glide_size))
@@ -856,9 +876,9 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
UnregisterSignal(glider, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE)
src.travelling = travelling
- SEND_SIGNAL(src, COMSIG_TRAM_SET_TRAVELLING, travelling)
+ SEND_SIGNAL(src, COMSIG_TRANSPORT_ACTIVE, travelling)
-/obj/structure/industrial_lift/tram/set_currently_z_moving()
+/obj/structure/transport/linear/tram/set_currently_z_moving()
return FALSE //trams can never z fall and shouldnt waste any processing time trying to do so
/**
@@ -868,13 +888,13 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
* at a location before users are allowed to interact with the tram console again.
* Tram finds its location at this point before fully unlocking controls to the user.
*/
-/obj/structure/industrial_lift/tram/proc/unlock_controls()
- for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_master_datum.lift_platforms) //only thing everyone needs to know is the new location.
+/obj/structure/transport/linear/tram/proc/unlock_controls()
+ for(var/obj/structure/transport/linear/tram/tram_part as anything in transport_controller_datum.transport_modules) //only thing everyone needs to know is the new location.
tram_part.set_travelling(FALSE)
- lift_master_datum.set_controls(LIFT_PLATFORM_UNLOCKED)
+ transport_controller_datum.controls_lock(FALSE)
///debug proc to highlight the locs of the tram platform
-/obj/structure/industrial_lift/tram/proc/find_dimensions(iterations = 100)
+/obj/structure/transport/linear/tram/proc/find_dimensions(iterations = 100)
message_admins("num turfs: [length(locs)]")
var/overlay = /obj/effect/overlay/ai_detect_hud
@@ -886,7 +906,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
addtimer(CALLBACK(src, PROC_REF(clear_turfs), turfs, iterations), 1)
-/obj/structure/industrial_lift/tram/proc/clear_turfs(list/turfs_to_clear, iterations)
+/obj/structure/transport/linear/tram/proc/clear_turfs(list/turfs_to_clear, iterations)
for(var/turf/our_old_turf as anything in turfs_to_clear)
var/obj/effect/overlay/ai_detect_hud/hud = locate() in our_old_turf
if(hud)
@@ -905,3 +925,12 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
if(iterations)
addtimer(CALLBACK(src, PROC_REF(clear_turfs), turfs, iterations), 1)
+
+/obj/structure/transport/linear/tram/proc/estop_throw(throw_direction)
+ if(prob(50))
+ do_sparks(2, FALSE, src)
+ for(var/mob/living/passenger in transport_contents)
+ to_chat(passenger, span_userdanger("The tram comes to a sudden, grinding stop!"))
+ var/throw_target = get_edge_target_turf(src, throw_direction)
+ var/datum/callback/land_slam = new(passenger, TYPE_PROC_REF(/mob/living/, tram_slam_land))
+ passenger.throw_at(throw_target, 400, 4, force = MOVE_FORCE_OVERPOWERING, callback = land_slam)
diff --git a/code/modules/transport/transport_navigation.dm b/code/modules/transport/transport_navigation.dm
new file mode 100644
index 0000000000000..3b5c73b5de1a1
--- /dev/null
+++ b/code/modules/transport/transport_navigation.dm
@@ -0,0 +1,108 @@
+/**
+ * transport_controller landmarks. used to map specific destinations on the map.
+ */
+/obj/effect/landmark/transport/nav_beacon/tram
+ name = "tram destination" //the tram buttons will mention this.
+ icon_state = "tram"
+
+ /// The ID of the tram we're linked to
+ var/specific_transport_id = TRAMSTATION_LINE_1
+ /// The ID of that particular destination
+ var/platform_code = null
+ /// Icons for the tgui console to list out for what is at this location
+ var/list/tgui_icons = list()
+
+/obj/effect/landmark/transport/nav_beacon/tram/Initialize(mapload)
+ . = ..()
+ LAZYADDASSOCLIST(SStransport.nav_beacons, specific_transport_id, src)
+
+/obj/effect/landmark/transport/nav_beacon/tram/Destroy()
+ LAZYREMOVEASSOC(SStransport.nav_beacons, specific_transport_id, src)
+ return ..()
+
+/obj/effect/landmark/transport/nav_beacon/tram/nav
+ name = "tram nav beacon"
+ invisibility = INVISIBILITY_MAXIMUM // nav aids can't be abstract since they stay with the tram
+
+/**
+ * transport_controller landmarks. used to map in specific_transport_id to trams and elevators. when the transport_controller encounters one on a tile
+ * it sets its specific_transport_id to that landmark. allows you to have multiple trams and multiple objects linking to their specific tram
+ */
+/obj/effect/landmark/transport/transport_id
+ name = "transport init landmark"
+ icon_state = "lift_id"
+ ///what specific id we give to the tram we're placed on, should explicitely set this if its a subtype, or weird things might happen
+ var/specific_transport_id
+
+//tramstation
+
+/obj/effect/landmark/transport/transport_id/tramstation/line_1
+ specific_transport_id = TRAMSTATION_LINE_1
+
+/obj/effect/landmark/transport/nav_beacon/tram/nav/tramstation/main
+ name = TRAMSTATION_LINE_1
+ specific_transport_id = TRAM_NAV_BEACONS
+ dir = WEST
+
+/obj/effect/landmark/transport/nav_beacon/tram/platform/tramstation/west
+ name = "West Wing"
+ platform_code = TRAMSTATION_WEST
+ tgui_icons = list("Arrivals" = "plane-arrival", "Command" = "bullhorn", "Security" = "gavel")
+
+/obj/effect/landmark/transport/nav_beacon/tram/platform/tramstation/central
+ name = "Central Wing"
+ platform_code = TRAMSTATION_CENTRAL
+ tgui_icons = list("Service" = "cocktail", "Medical" = "plus", "Engineering" = "wrench")
+
+/obj/effect/landmark/transport/nav_beacon/tram/platform/tramstation/east
+ name = "East Wing"
+ platform_code = TRAMSTATION_EAST
+ tgui_icons = list("Departures" = "plane-departure", "Cargo" = "box", "Science" = "flask")
+
+//birdshot
+
+/obj/effect/landmark/transport/transport_id/birdshot/line_1
+ specific_transport_id = BIRDSHOT_LINE_1
+
+/obj/effect/landmark/transport/transport_id/birdshot/line_2
+ specific_transport_id = BIRDSHOT_LINE_2
+
+/obj/effect/landmark/transport/nav_beacon/tram/nav/birdshot/prison
+ name = BIRDSHOT_LINE_1
+ specific_transport_id = TRAM_NAV_BEACONS
+ dir = NORTH
+
+/obj/effect/landmark/transport/nav_beacon/tram/nav/birdshot/maint
+ name = BIRDSHOT_LINE_2
+ specific_transport_id = TRAM_NAV_BEACONS
+ dir = WEST
+
+/obj/effect/landmark/transport/nav_beacon/tram/platform/birdshot/sec_wing
+ name = "Security Wing"
+ specific_transport_id = BIRDSHOT_LINE_1
+ platform_code = BIRDSHOT_SECURITY_WING
+ tgui_icons = list("Security" = "gavel")
+
+/obj/effect/landmark/transport/nav_beacon/tram/platform/birdshot/prison_wing
+ name = "Prison Wing"
+ specific_transport_id = BIRDSHOT_LINE_1
+ platform_code = BIRDSHOT_PRISON_WING
+ tgui_icons = list("Prison" = "box")
+
+/obj/effect/landmark/transport/nav_beacon/tram/platform/birdshot/maint_left
+ name = "Port Platform"
+ specific_transport_id = BIRDSHOT_LINE_2
+ platform_code = BIRDSHOT_MAINTENANCE_LEFT
+ tgui_icons = list("Port Platform" = "plane-departure")
+
+/obj/effect/landmark/transport/nav_beacon/tram/platform/birdshot/maint_right
+ name = "Starboard Platform"
+ specific_transport_id = BIRDSHOT_LINE_2
+ platform_code = BRIDSHOT_MAINTENANCE_RIGHT
+ tgui_icons = list("Starboard Platform" = "plane-arrival")
+
+//map-agnostic landmarks
+
+/obj/effect/landmark/transport/nav_beacon/tram/nav/immovable_rod
+ name = "DESTINATION/NOT/FOUND"
+ specific_transport_id = IMMOVABLE_ROD_DESTINATIONS
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index c6fdecbd29bc0..4c91a6151dc4c 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -149,6 +149,7 @@
#include "heretic_rituals.dm"
#include "high_five.dm"
#include "holidays.dm"
+#include "hulk.dm"
#include "human_through_recycler.dm"
#include "hunger_curse.dm"
#include "hydroponics_extractor_storage.dm"
@@ -168,6 +169,7 @@
#include "load_map_security.dm"
#include "lungs.dm"
#include "machine_disassembly.dm"
+#include "mafia.dm"
#include "map_landmarks.dm"
#include "mapload_space_verification.dm"
#include "mapping.dm"
@@ -196,6 +198,7 @@
#include "organ_set_bonus.dm"
#include "organs.dm"
#include "outfit_sanity.dm"
+#include "oxyloss_suffocation.dm"
#include "paintings.dm"
#include "pills.dm"
#include "plane_double_transform.dm"
@@ -253,6 +256,7 @@
#include "subsystem_init.dm"
#include "suit_storage_icons.dm"
#include "surgeries.dm"
+#include "tail_wag.dm"
#include "teleporters.dm"
#include "tgui_create_message.dm"
#include "timer_sanity.dm"
diff --git a/code/modules/unit_tests/barsigns.dm b/code/modules/unit_tests/barsigns.dm
index 7058dd5346dc9..f5bb27f10643e 100644
--- a/code/modules/unit_tests/barsigns.dm
+++ b/code/modules/unit_tests/barsigns.dm
@@ -12,7 +12,7 @@
for(var/sign_type in (subtypesof(/datum/barsign) - /datum/barsign/hiddensigns))
var/datum/barsign/sign = new sign_type()
- if(!(sign.icon in barsign_icon_states))
+ if(!(sign.icon_state in barsign_icon_states))
TEST_FAIL("Icon state for [sign_type] does not exist in [barsign_icon].")
/**
diff --git a/code/modules/unit_tests/combat.dm b/code/modules/unit_tests/combat.dm
index 3302eca9c74e1..d645df98cbfd9 100644
--- a/code/modules/unit_tests/combat.dm
+++ b/code/modules/unit_tests/combat.dm
@@ -99,3 +99,42 @@
TEST_ASSERT_EQUAL(victim.loc.x, run_loc_floor_bottom_left.x + 2, "Victim was moved after being pushed against a wall")
TEST_ASSERT(victim.has_status_effect(/datum/status_effect/incapacitating/knockdown), "Victim was not knocked down after being pushed against a wall")
TEST_ASSERT_EQUAL(victim.get_active_held_item(), null, "Victim didn't drop toolbox after being pushed against a wall")
+
+/// Tests you can punch yourself
+/datum/unit_test/self_punch
+
+/datum/unit_test/self_punch/Run()
+ var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent)
+ ADD_TRAIT(dummy, TRAIT_PERFECT_ATTACKER, TRAIT_SOURCE_UNIT_TESTS)
+ dummy.set_combat_mode(TRUE)
+ dummy.ClickOn(dummy)
+ TEST_ASSERT_NOTEQUAL(dummy.getBruteLoss(), 0, "Dummy took no brute damage after self-punching")
+
+/// Tests handcuffed (HANDS_BLOCKED) mobs cannot punch
+/datum/unit_test/handcuff_punch
+
+/datum/unit_test/handcuff_punch/Run()
+ var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human/consistent)
+ var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human/consistent)
+ ADD_TRAIT(attacker, TRAIT_PERFECT_ATTACKER, TRAIT_SOURCE_UNIT_TESTS)
+ ADD_TRAIT(attacker, TRAIT_HANDS_BLOCKED, TRAIT_SOURCE_UNIT_TESTS)
+ attacker.set_combat_mode(TRUE)
+ attacker.ClickOn(victim)
+ TEST_ASSERT_EQUAL(victim.getBruteLoss(), 0, "Victim took brute damage from being punched by a handcuffed attacker")
+ attacker.next_move = -1
+ attacker.next_click = -1
+ attacker.ClickOn(attacker)
+ TEST_ASSERT_EQUAL(attacker.getBruteLoss(), 0, "Attacker took brute damage from self-punching while handcuffed")
+
+/// Tests handcuffed (HANDS_BLOCKED) monkeys can still bite despite being cuffed
+/datum/unit_test/handcuff_bite
+
+/datum/unit_test/handcuff_bite/Run()
+ var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human/consistent)
+ var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human/consistent)
+ ADD_TRAIT(attacker, TRAIT_PERFECT_ATTACKER, TRAIT_SOURCE_UNIT_TESTS)
+ ADD_TRAIT(attacker, TRAIT_HANDS_BLOCKED, TRAIT_SOURCE_UNIT_TESTS)
+ attacker.set_combat_mode(TRUE)
+ attacker.set_species(/datum/species/monkey)
+ attacker.ClickOn(victim)
+ TEST_ASSERT_NOTEQUAL(victim.getBruteLoss(), 0, "Victim took no brute damage from being bit by a handcuffed monkey, which is incorrect, as it's a bite attack")
diff --git a/code/modules/unit_tests/dcs_check_list_arguments.dm b/code/modules/unit_tests/dcs_check_list_arguments.dm
index 8089896ba3d46..67d7417062b27 100644
--- a/code/modules/unit_tests/dcs_check_list_arguments.dm
+++ b/code/modules/unit_tests/dcs_check_list_arguments.dm
@@ -1,18 +1,36 @@
/**
- * list arguments for bespoke elements are treated just like any other datum: as a text ref in the ID.
- * Using un-cached lists in AddElement() and RemoveElement() calls will just create new elements over
- * and over. That's what this unit test is for. It's not a catch-all, but it does a decent job at it.
+ * list arguments for bespoke elements are treated as a text ref in the ID, like any other datum.
+ * Which means that, unless cached, using lists as arguments will lead to multiple instance of the same element
+ * being created over and over.
+ *
+ * Because of how it works, this unit test checks that these list datum args
+ * do not share similar contents (when rearranged in descending alpha-numerical order), to ensure that
+ * the least necessary amount of elements is created. So, using static lists may not be enough,
+ * for example, in the case of two different critters using the death_drops element to drop ectoplasm on death, since,
+ * despite being static lists, the two are different instances assigned to different mob types.
+ *
+ * Most of the time, you won't encounter two different static lists with similar contents used as element args,
+ * meaning using static lists is accepted. However, should that happen, it's advised to replace the instances
+ * with various string_x procs: lists, assoc_lists, assoc_nested_lists or numbers_list, depending on the type.
+ *
+ * In the case of an element where the position of the contents of each datum list argument is important,
+ * ELEMENT_DONT_SORT_LIST_ARGS should be added to its flags, to prevent such issues where the contents are similar
+ * when sorted, but the element instances are not.
+ *
+ * In the off-chance the element is not compatible with this unit test (such as for connect_loc et simila),
+ * you can also use ELEMENT_NO_LIST_UNIT_TEST so that they won't be processed by this unit test at all.
*/
/datum/unit_test/dcs_check_list_arguments
/**
- * This unit test requires every (tangible) atom to have been created at least once
- * so its search is more accurate. That's why it's run after create_and_destroy.
+ * This unit test requires every (unless ignored) atom to have been created at least once
+ * for a more accurate search, which is why it's run after create_and_destroy is done running.
*/
priority = TEST_AFTER_CREATE_AND_DESTROY
/datum/unit_test/dcs_check_list_arguments/Run()
+ var/we_failed = FALSE
for(var/element_type in SSdcs.arguments_that_are_lists_by_element)
- // Keeps tracks of the lists that shouldn't be compared with again.
+ // Keeps track of the lists that shouldn't be compared with again.
var/list/to_ignore = list()
var/list/superlist = SSdcs.arguments_that_are_lists_by_element[element_type]
for(var/list/current as anything in superlist)
@@ -27,7 +45,11 @@
bad_lists += list(compare)
to_ignore[compare] = TRUE
if(bad_lists)
+ we_failed = TRUE
//Include the original, unsorted list in the report. It should be easier to find by the contributor.
var/list/unsorted_list = superlist[current]
- TEST_FAIL("found [length(bad_lists)] identical lists used as argument for element [element_type]. List: [json_encode(unsorted_list)].\n\
- Make sure it's a cached list, or use one of the string_list proc. Also, use the ELEMENT_DONT_SORT_LIST_ARGS flag if the key position of your lists matters.")
+ TEST_FAIL("Found [length(bad_lists)] datum list arguments with similar contents for [element_type]. Contents: [json_encode(unsorted_list)].")
+ ///Let's avoid sending the same instructions over and over, as it's just going to clutter the CI and confuse someone.
+ if(we_failed)
+ TEST_FAIL("Ensure that each list is static or cached. string_lists() (as well as similar procs) is your friend here.\n\
+ Check the documentation from dcs_check_list_arguments.dm for more information!")
diff --git a/code/modules/unit_tests/designs.dm b/code/modules/unit_tests/designs.dm
index a9cda144649cb..0495ebdc7d9ae 100644
--- a/code/modules/unit_tests/designs.dm
+++ b/code/modules/unit_tests/designs.dm
@@ -18,6 +18,8 @@
TEST_FAIL("Design [current_design.type] requires materials but does not have have any build_path or make_reagent set")
else if (!isnull(current_design.build_path) || !isnull(current_design.build_path)) // //Design requires no materials but creates stuff
TEST_FAIL("Design [current_design.type] requires NO materials but has build_path or make_reagent set")
+ if (length(current_design.reagents_list) && !(current_design.build_type & LIMBGROWER))
+ TEST_FAIL("Design [current_design.type] requires reagents but isn't a limb grower design. Reagent costs are only supported by limb grower designs")
for(var/path in subtypesof(/datum/design/surgery))
var/datum/design/surgery/current_design = new path //Create an instance of each design
diff --git a/code/modules/unit_tests/dragon_expiration.dm b/code/modules/unit_tests/dragon_expiration.dm
index 7b36b5762911c..45262dc9d6090 100644
--- a/code/modules/unit_tests/dragon_expiration.dm
+++ b/code/modules/unit_tests/dragon_expiration.dm
@@ -2,9 +2,12 @@
/datum/unit_test/contents_barfer
/datum/unit_test/contents_barfer/Run()
- var/mob/living/simple_animal/hostile/space_dragon/dragon_time = allocate(/mob/living/simple_animal/hostile/space_dragon)
+ var/mob/living/basic/space_dragon/dragon_time = allocate(/mob/living/basic/space_dragon)
var/mob/living/carbon/human/to_be_consumed = allocate(/mob/living/carbon/human/consistent)
+ to_be_consumed.adjust_fire_stacks(5)
+ to_be_consumed.ignite_mob()
TEST_ASSERT(dragon_time.eat(to_be_consumed), "The space dragon failed to consume the dummy!")
+ TEST_ASSERT(!to_be_consumed.has_status_effect(/datum/status_effect/fire_handler/fire_stacks), "The space dragon failed to extinguish the dummy!")
TEST_ASSERT_EQUAL(to_be_consumed.loc, dragon_time, "The dummy's location, after being successfuly consumed, was not within the space dragon's contents!")
dragon_time.death()
TEST_ASSERT(isturf(to_be_consumed.loc), "After dying, the space dragon did not eject the consumed dummy content barfer element.")
@@ -13,7 +16,7 @@
/datum/unit_test/space_dragon_expiration
/datum/unit_test/space_dragon_expiration/Run()
- var/mob/living/simple_animal/hostile/space_dragon/dragon_time = allocate(/mob/living/simple_animal/hostile/space_dragon)
+ var/mob/living/basic/space_dragon/dragon_time = allocate(/mob/living/basic/space_dragon)
var/mob/living/carbon/human/to_be_consumed = allocate(/mob/living/carbon/human/consistent)
dragon_time.mind_initialize()
diff --git a/code/modules/unit_tests/hulk.dm b/code/modules/unit_tests/hulk.dm
new file mode 100644
index 0000000000000..52706e9ac73fd
--- /dev/null
+++ b/code/modules/unit_tests/hulk.dm
@@ -0,0 +1,44 @@
+/// Tests hulk attacking over normal attacking
+/datum/unit_test/hulk_attack
+ var/hulk_hits = 0
+ var/hand_hits = 0
+
+/datum/unit_test/hulk_attack/Run()
+ var/mob/living/carbon/human/hulk = allocate(/mob/living/carbon/human/consistent)
+ var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent)
+
+
+ RegisterSignal(dummy, COMSIG_ATOM_HULK_ATTACK, PROC_REF(hulk_sig_fire))
+ RegisterSignal(dummy, COMSIG_ATOM_ATTACK_HAND, PROC_REF(hand_sig_fire))
+
+ hulk.dna.add_mutation(/datum/mutation/human/hulk)
+ hulk.set_combat_mode(TRUE)
+ hulk.ClickOn(dummy)
+
+ TEST_ASSERT_EQUAL(hulk_hits, 1, "Hulk should have hit the dummy once.")
+ TEST_ASSERT_EQUAL(hand_hits, 0, "Hulk should not have hit the dummy with attack_hand.")
+ TEST_ASSERT(dummy.getBruteLoss(), "Dummy should have taken brute damage from being hulk punched.")
+
+/datum/unit_test/hulk_attack/proc/hulk_sig_fire()
+ SIGNAL_HANDLER
+ hulk_hits += 1
+
+/datum/unit_test/hulk_attack/proc/hand_sig_fire()
+ SIGNAL_HANDLER
+ hand_hits += 1
+
+/// Tests that hulks aren't given rapid attacks from rapid attack gloves
+/datum/unit_test/hulk_north_star
+
+/datum/unit_test/hulk_north_star/Run()
+ var/mob/living/carbon/human/hulk = allocate(/mob/living/carbon/human/consistent)
+ var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent)
+ var/obj/item/clothing/gloves/rapid/fotns = allocate(/obj/item/clothing/gloves/rapid)
+
+ hulk.equip_to_appropriate_slot(fotns)
+ hulk.dna.add_mutation(/datum/mutation/human/hulk)
+ hulk.set_combat_mode(TRUE)
+ hulk.ClickOn(dummy)
+
+ TEST_ASSERT_NOTEQUAL(hulk.next_move, world.time + CLICK_CD_RAPID, "Hulk should not gain the effects of the Fists of the North Star.")
+ TEST_ASSERT_EQUAL(hulk.next_move, world.time + CLICK_CD_MELEE, "Hulk click cooldown was a value not expected.")
diff --git a/code/modules/unit_tests/mafia.dm b/code/modules/unit_tests/mafia.dm
new file mode 100644
index 0000000000000..85fa50842932b
--- /dev/null
+++ b/code/modules/unit_tests/mafia.dm
@@ -0,0 +1,48 @@
+///Checks if a Mafia game with a Modular Computer and a Ghost will run with 'basic_setup', which is the default
+///way the game is ran, without admin-intervention.
+///The game should immediately end in a Town Victory due to lack of evils, but we can verify that both the PDA and the ghost
+///successfully managed to get into the round.
+/datum/unit_test/mafia
+ ///Boolean on whether the Mafia game started or not. Will Fail if it hasn't.
+ var/mafia_game_started = FALSE
+
+/datum/unit_test/mafia/Run()
+ RegisterSignal(SSdcs, COMSIG_MAFIA_GAME_START, PROC_REF(on_mafia_start))
+ var/datum/mafia_controller/controller = GLOB.mafia_game || new()
+
+ TEST_ASSERT(controller, "No Mafia game was found, nor was it able to be created properly.")
+
+ //spawn human and give them a laptop.
+ var/mob/living/carbon/human/consistent/living_player = allocate(/mob/living/carbon/human/consistent)
+ var/obj/item/modular_computer/laptop/preset/mafia/modpc_player = allocate(/obj/item/modular_computer/laptop/preset/mafia)
+ living_player.put_in_active_hand(modpc_player, TRUE)
+
+ //make the laptop run Mafia app.
+ var/datum/computer_file/program/mafia/mafia_program = locate() in modpc_player.stored_files
+ TEST_ASSERT(mafia_program, "Mafia program was unable to be found on [modpc_player].")
+ modpc_player.active_program = mafia_program
+
+ //Spawn a ghost and make them eligible to use the Mafia UI (just to be safe).
+ var/mob/dead/observer/ghost_player = allocate(/mob/dead/observer)
+ var/datum/client_interface/mock_client = new()
+ ghost_player.mock_client = mock_client
+ mock_client.mob = ghost_player
+ ADD_TRAIT(ghost_player, TRAIT_PRESERVE_UI_WITHOUT_CLIENT, TRAIT_SOURCE_UNIT_TESTS)
+
+ //First make the human sign up for Mafia, then the ghost, then we'll auto-start it.
+ controller.signup_mafia(living_player, modpc = modpc_player)
+ controller.signup_mafia(ghost_player, ghost_client = mock_client)
+
+ controller.basic_setup()
+
+ TEST_ASSERT(mafia_game_started, "Mafia game did not start despite basic_setup being called.")
+ TEST_ASSERT_NOTNULL(controller.player_role_lookup[modpc_player], "The Modular Computer was unable to join a game of Mafia.")
+ TEST_ASSERT_NOTNULL(controller.player_role_lookup[mock_client.ckey], "The Mock client wasn't put into a game of Mafia.")
+
+ mock_client.mob = null
+
+ qdel(controller)
+
+/datum/unit_test/mafia/proc/on_mafia_start(datum/controller/subsystem/processing/dcs/source, datum/mafia_controller/game)
+ SIGNAL_HANDLER
+ mafia_game_started = TRUE
diff --git a/code/modules/unit_tests/monkey_business.dm b/code/modules/unit_tests/monkey_business.dm
index 20bfffe6a4821..80044a0486d98 100644
--- a/code/modules/unit_tests/monkey_business.dm
+++ b/code/modules/unit_tests/monkey_business.dm
@@ -14,7 +14,8 @@
/datum/unit_test/monkey_business/Run()
for(var/monkey_id in 1 to length(GLOB.the_station_areas))
- var/mob/living/carbon/human/monkey = allocate(/mob/living/carbon/human/consistent, get_first_open_turf_in_area(GLOB.the_station_areas[monkey_id]))
+ var/area/monkey_zone = GLOB.areas_by_type[GLOB.the_station_areas[monkey_id]]
+ var/mob/living/carbon/human/monkey = allocate(/mob/living/carbon/human/consistent, get_first_open_turf_in_area(monkey_zone))
monkey.set_species(/datum/species/monkey)
monkey.set_name("Monkey [monkey_id]")
if(monkey_id % monkey_angry_nth == 0) // BLOOD FOR THE BLOOD GODS
diff --git a/code/modules/unit_tests/oxyloss_suffocation.dm b/code/modules/unit_tests/oxyloss_suffocation.dm
new file mode 100644
index 0000000000000..a44911ebbba76
--- /dev/null
+++ b/code/modules/unit_tests/oxyloss_suffocation.dm
@@ -0,0 +1,10 @@
+/// Test getting over a certain threshold of oxy damage results in KO
+/datum/unit_test/oxyloss_suffocation
+
+/datum/unit_test/oxyloss_suffocation/Run()
+ var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent)
+
+ dummy.setOxyLoss(75)
+ TEST_ASSERT(HAS_TRAIT_FROM(dummy, TRAIT_KNOCKEDOUT, OXYLOSS_TRAIT), "Dummy should have been knocked out from taking oxy damage.")
+ dummy.setOxyLoss(0)
+ TEST_ASSERT(!HAS_TRAIT_FROM(dummy, TRAIT_KNOCKEDOUT, OXYLOSS_TRAIT), "Dummy should have woken up from KO when healing to 0 oxy damage.")
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodbrother.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodbrother.png
index e17d8c14159d1..f34414bdc13d3 100644
Binary files a/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodbrother.png and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodbrother.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_monkey_holodeck.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_monkey_holodeck.png
new file mode 100644
index 0000000000000..e0d02f4302f43
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_monkey_holodeck.png differ
diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm
index a4ef1a1a28255..d5e1e773c9256 100644
--- a/code/modules/unit_tests/simple_animal_freeze.dm
+++ b/code/modules/unit_tests/simple_animal_freeze.dm
@@ -33,15 +33,6 @@
/mob/living/simple_animal/bot/secbot/honkbot,
/mob/living/simple_animal/bot/secbot/pingsky,
/mob/living/simple_animal/bot/vibebot,
- /mob/living/simple_animal/drone,
- /mob/living/simple_animal/drone/classic,
- /mob/living/simple_animal/drone/derelict,
- /mob/living/simple_animal/drone/polymorphed,
- /mob/living/simple_animal/drone/snowflake,
- /mob/living/simple_animal/drone/snowflake/bardrone,
- /mob/living/simple_animal/drone/syndrone,
- /mob/living/simple_animal/drone/syndrone/badass,
- /mob/living/simple_animal/holodeck_monkey,
/mob/living/simple_animal/hostile,
/mob/living/simple_animal/hostile/alien,
/mob/living/simple_animal/hostile/alien/drone,
@@ -67,24 +58,6 @@
/mob/living/simple_animal/hostile/asteroid/polarbear,
/mob/living/simple_animal/hostile/asteroid/polarbear/lesser,
/mob/living/simple_animal/hostile/asteroid/wolf,
- /mob/living/simple_animal/hostile/construct,
- /mob/living/simple_animal/hostile/construct/artificer,
- /mob/living/simple_animal/hostile/construct/artificer/angelic,
- /mob/living/simple_animal/hostile/construct/artificer/hostile,
- /mob/living/simple_animal/hostile/construct/artificer/mystic,
- /mob/living/simple_animal/hostile/construct/artificer/noncult,
- /mob/living/simple_animal/hostile/construct/juggernaut,
- /mob/living/simple_animal/hostile/construct/juggernaut/angelic,
- /mob/living/simple_animal/hostile/construct/juggernaut/hostile,
- /mob/living/simple_animal/hostile/construct/juggernaut/mystic,
- /mob/living/simple_animal/hostile/construct/juggernaut/noncult,
- /mob/living/simple_animal/hostile/construct/proteon,
- /mob/living/simple_animal/hostile/construct/proteon/hostile,
- /mob/living/simple_animal/hostile/construct/wraith,
- /mob/living/simple_animal/hostile/construct/wraith/angelic,
- /mob/living/simple_animal/hostile/construct/wraith/hostile,
- /mob/living/simple_animal/hostile/construct/wraith/mystic,
- /mob/living/simple_animal/hostile/construct/wraith/noncult,
/mob/living/simple_animal/hostile/dark_wizard,
/mob/living/simple_animal/hostile/guardian,
/mob/living/simple_animal/hostile/guardian/assassin,
@@ -101,72 +74,39 @@
/mob/living/simple_animal/hostile/illusion,
/mob/living/simple_animal/hostile/illusion/escape,
/mob/living/simple_animal/hostile/illusion/mirage,
- /mob/living/simple_animal/hostile/jungle,
- /mob/living/simple_animal/hostile/jungle/leaper,
/mob/living/simple_animal/hostile/megafauna,
/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner,
/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/doom,
/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/guidance,
/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/hunter,
- /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/virtual_domain,
/mob/living/simple_animal/hostile/megafauna/bubblegum,
/mob/living/simple_animal/hostile/megafauna/bubblegum/hallucination,
- /mob/living/simple_animal/hostile/megafauna/bubblegum/virtual_domain,
/mob/living/simple_animal/hostile/megafauna/clockwork_defender,
/mob/living/simple_animal/hostile/megafauna/colossus,
- /mob/living/simple_animal/hostile/megafauna/colossus/virtual_domain,
/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner,
/mob/living/simple_animal/hostile/megafauna/dragon,
/mob/living/simple_animal/hostile/megafauna/dragon/lesser,
- /mob/living/simple_animal/hostile/megafauna/dragon/virtual_domain,
/mob/living/simple_animal/hostile/megafauna/hierophant,
- /mob/living/simple_animal/hostile/megafauna/hierophant/virtual_domain,
/mob/living/simple_animal/hostile/megafauna/legion,
- /mob/living/simple_animal/hostile/megafauna/legion/virtual_domain,
/mob/living/simple_animal/hostile/megafauna/legion/medium,
/mob/living/simple_animal/hostile/megafauna/legion/medium/eye,
/mob/living/simple_animal/hostile/megafauna/legion/medium/left,
/mob/living/simple_animal/hostile/megafauna/legion/medium/right,
/mob/living/simple_animal/hostile/megafauna/legion/small,
/mob/living/simple_animal/hostile/megafauna/wendigo,
- /mob/living/simple_animal/hostile/megafauna/wendigo/virtual_domain,
/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/nanotrasen,
- /mob/living/simple_animal/hostile/nanotrasen/elite,
- /mob/living/simple_animal/hostile/nanotrasen/ranged,
- /mob/living/simple_animal/hostile/nanotrasen/ranged/assault,
- /mob/living/simple_animal/hostile/nanotrasen/ranged/smg,
- /mob/living/simple_animal/hostile/nanotrasen/screaming,
/mob/living/simple_animal/hostile/ooze,
/mob/living/simple_animal/hostile/ooze/gelatinous,
/mob/living/simple_animal/hostile/ooze/grapes,
- /mob/living/simple_animal/hostile/pirate,
- /mob/living/simple_animal/hostile/pirate/melee,
- /mob/living/simple_animal/hostile/pirate/melee/space,
- /mob/living/simple_animal/hostile/pirate/ranged,
- /mob/living/simple_animal/hostile/pirate/ranged/space,
/mob/living/simple_animal/hostile/retaliate,
/mob/living/simple_animal/hostile/retaliate/goose,
/mob/living/simple_animal/hostile/retaliate/goose/vomit,
- /mob/living/simple_animal/hostile/retaliate/nanotrasenpeace,
- /mob/living/simple_animal/hostile/retaliate/nanotrasenpeace/ranged,
- /mob/living/simple_animal/hostile/retaliate/trader,
- /mob/living/simple_animal/hostile/retaliate/trader/mrbones,
- /mob/living/simple_animal/hostile/skeleton,
- /mob/living/simple_animal/hostile/skeleton/eskimo,
- /mob/living/simple_animal/hostile/skeleton/ice,
- /mob/living/simple_animal/hostile/skeleton/plasmaminer,
- /mob/living/simple_animal/hostile/skeleton/plasmaminer/jackhammer,
- /mob/living/simple_animal/hostile/skeleton/templar,
- /mob/living/simple_animal/hostile/space_dragon,
- /mob/living/simple_animal/hostile/space_dragon/spawn_with_antag,
/mob/living/simple_animal/hostile/vatbeast,
- /mob/living/simple_animal/hostile/wizard,
/mob/living/simple_animal/hostile/zombie,
/mob/living/simple_animal/parrot,
/mob/living/simple_animal/parrot/natural,
@@ -185,7 +125,6 @@
/mob/living/simple_animal/pet/gondola,
/mob/living/simple_animal/pet/gondola/gondolapod,
/mob/living/simple_animal/pet/gondola/virtual_domain,
- /mob/living/simple_animal/shade,
/mob/living/simple_animal/slime,
/mob/living/simple_animal/slime/pet,
/mob/living/simple_animal/slime/random,
diff --git a/code/modules/unit_tests/spell_shapeshift.dm b/code/modules/unit_tests/spell_shapeshift.dm
index 4e2ce6590037e..9b5804e352062 100644
--- a/code/modules/unit_tests/spell_shapeshift.dm
+++ b/code/modules/unit_tests/spell_shapeshift.dm
@@ -18,6 +18,8 @@
qdel(shift)
+#define TRIGGER_RESET_COOLDOWN(spell) spell.next_use_time = 0; spell.Trigger();
+
/**
* Validates that shapeshift spells put the mob in another mob, as they should.
*/
@@ -25,7 +27,7 @@
/datum/unit_test/shapeshift_spell/Run()
- var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent)
+ var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent, run_loc_floor_bottom_left)
dummy.mind_initialize()
for(var/spell_type in subtypesof(/datum/action/cooldown/spell/shapeshift))
@@ -57,8 +59,7 @@
if(forced_shape)
shift.shapeshift_type = forced_shape
- shift.next_use_time = 0
- shift.Trigger()
+ TRIGGER_RESET_COOLDOWN(shift)
var/mob/expected_shape = shift.shapeshift_type
if(!istype(dummy.loc, expected_shape))
return TEST_FAIL("Shapeshift spell: [shift.name] failed to transform the dummy into the shape [initial(expected_shape.name)]. \
@@ -68,8 +69,7 @@
if(!(shift in shape.actions))
return TEST_FAIL("Shapeshift spell: [shift.name] failed to grant the spell to the dummy's shape.")
- shift.next_use_time = 0
- shift.Trigger()
+ TRIGGER_RESET_COOLDOWN(shift)
if(istype(dummy.loc, shift.shapeshift_type))
return TEST_FAIL("Shapeshift spell: [shift.name] failed to transform the dummy back into a human.")
@@ -81,7 +81,7 @@
/datum/unit_test/shapeshift_holoparasites/Run()
- var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent)
+ var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent, run_loc_floor_bottom_left)
var/datum/action/cooldown/spell/shapeshift/wizard/shift = new(dummy)
shift.shapeshift_type = shift.possible_shapes[1]
@@ -99,9 +99,53 @@
TEST_ASSERT_EQUAL(test_stand.summoner, dummy.loc, "Shapeshift spell failed to transfer the holoparasite to the dummy's shape.")
// Dummy casts shapeshfit back, the stand's summoner should become the dummy again.
- shift.next_use_time = 0
- shift.Trigger()
+ TRIGGER_RESET_COOLDOWN(shift)
TEST_ASSERT(!istype(dummy.loc, shift.shapeshift_type), "Shapeshift spell failed to transform the dummy back into human form.")
TEST_ASSERT_EQUAL(test_stand.summoner, dummy, "Shapeshift spell failed to transfer the holoparasite back to the dummy's human form.")
qdel(shift)
+
+#define EXPECTED_HEALTH_RATIO 0.5
+
+/// Validates that shapeshifting carries health or death between forms properly, if it is supposed to
+/datum/unit_test/shapeshift_health
+
+/datum/unit_test/shapeshift_health/Run()
+ for(var/spell_type in subtypesof(/datum/action/cooldown/spell/shapeshift))
+ var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent, run_loc_floor_bottom_left)
+ var/datum/action/cooldown/spell/shapeshift/shift_spell = new spell_type(dummy)
+ shift_spell.Grant(dummy)
+ shift_spell.shapeshift_type = shift_spell.possible_shapes[1]
+
+ if (istype(shift_spell, /datum/action/cooldown/spell/shapeshift/polymorph_belt))
+ var/datum/action/cooldown/spell/shapeshift/polymorph_belt/belt_spell = shift_spell
+ belt_spell.channel_time = 0 SECONDS // No do-afters
+
+ if (shift_spell.convert_damage)
+ shift_spell.Trigger()
+ TEST_ASSERT(istype(dummy.loc, shift_spell.shapeshift_type), "Failed to transform into [shift_spell.shapeshift_type]using [shift_spell.name].")
+ var/mob/living/shifted_mob = dummy.loc
+ shifted_mob.apply_damage(shifted_mob.maxHealth * EXPECTED_HEALTH_RATIO, BRUTE, forced = TRUE)
+ TRIGGER_RESET_COOLDOWN(shift_spell)
+ TEST_ASSERT(!istype(dummy.loc, shift_spell.shapeshift_type), "Failed to unfransform from [shift_spell.shapeshift_type] using [shift_spell.name].")
+ TEST_ASSERT_EQUAL(dummy.get_total_damage(), dummy.maxHealth * EXPECTED_HEALTH_RATIO, "Failed to transfer damage from [shift_spell.shapeshift_type] to original form using [shift_spell.name].")
+ TRIGGER_RESET_COOLDOWN(shift_spell)
+ TEST_ASSERT(istype(dummy.loc, shift_spell.shapeshift_type), "Failed to transform into [shift_spell.shapeshift_type] after taking damage using [shift_spell.name].")
+ shifted_mob = dummy.loc
+ TEST_ASSERT_EQUAL(shifted_mob.get_total_damage(), shifted_mob.maxHealth * EXPECTED_HEALTH_RATIO, "Failed to transfer damage from original form to [shift_spell.shapeshift_type] using [shift_spell.name].")
+ TRIGGER_RESET_COOLDOWN(shift_spell)
+
+ if (shift_spell.die_with_shapeshifted_form)
+ TRIGGER_RESET_COOLDOWN(shift_spell)
+ TEST_ASSERT(istype(dummy.loc, shift_spell.shapeshift_type), "Failed to transform into [shift_spell.shapeshift_type]")
+ var/mob/living/shifted_mob = dummy.loc
+ shifted_mob.health = 0 // Fucking megafauna
+ shifted_mob.death()
+ if (shift_spell.revert_on_death)
+ TEST_ASSERT(!istype(dummy.loc, shift_spell.shapeshift_type), "Failed to untransform after death using [shift_spell.name].")
+ TEST_ASSERT_EQUAL(dummy.stat, DEAD, "Failed to kill original mob when transformed mob died using [shift_spell.name].")
+
+ qdel(shift_spell)
+
+#undef EXPECTED_HEALTH_RATIO
+#undef TRIGGER_RESET_COOLDOWN
diff --git a/code/modules/unit_tests/tail_wag.dm b/code/modules/unit_tests/tail_wag.dm
new file mode 100644
index 0000000000000..0d828557953d6
--- /dev/null
+++ b/code/modules/unit_tests/tail_wag.dm
@@ -0,0 +1,90 @@
+/// Tests to make sure tail wagging behaves as expected
+/datum/unit_test/tail_wag
+ // used by the stop_after test
+ var/timer_finished = FALSE
+
+/datum/unit_test/tail_wag/Run()
+ var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent)
+ var/obj/item/organ/external/tail/cat/dummy_tail = allocate(/obj/item/organ/external/tail/cat)
+ dummy_tail.Insert(dummy, special = TRUE, drop_if_replaced = FALSE)
+
+ // SANITY TEST
+
+ // start wagging
+ SEND_SIGNAL(dummy, COMSIG_ORGAN_WAG_TAIL, TRUE)
+ if(!(dummy_tail.wag_flags & WAG_WAGGING))
+ TEST_FAIL("Tail did not start wagging when it should have!")
+
+ // stop wagging
+ SEND_SIGNAL(dummy, COMSIG_ORGAN_WAG_TAIL, FALSE)
+ if(dummy_tail.wag_flags & WAG_WAGGING)
+ TEST_FAIL("Tail did not stop wagging when it should have!")
+
+ // TESTING WAG_ABLE FLAG
+
+ // flip the wag flag to unwaggable
+ dummy_tail.wag_flags &= ~WAG_ABLE
+
+ // try to wag it again
+ SEND_SIGNAL(dummy, COMSIG_ORGAN_WAG_TAIL, TRUE)
+ if(dummy_tail.wag_flags & WAG_WAGGING)
+ TEST_FAIL("Tail should not have the ability to wag, yet it did!")
+
+ // flip the wag flag to waggable again
+ dummy_tail.wag_flags |= WAG_ABLE
+
+ // start wagging again
+ SEND_SIGNAL(dummy, COMSIG_ORGAN_WAG_TAIL, TRUE)
+ if(!(dummy_tail.wag_flags & WAG_WAGGING))
+ TEST_FAIL("Tail did not start wagging when it should have!")
+
+ // TESTING STOP_AFTER
+
+ // stop wagging
+ SEND_SIGNAL(dummy, COMSIG_ORGAN_WAG_TAIL, FALSE)
+ if(dummy_tail.wag_flags & WAG_WAGGING)
+ TEST_FAIL("Tail did not stop wagging when it should have!")
+
+ // start wagging, stop after 0.1 seconds
+ SEND_SIGNAL(dummy, COMSIG_ORGAN_WAG_TAIL, TRUE, 0.1 SECONDS)
+ // because timers are a pain
+ addtimer(VARSET_CALLBACK(src, timer_finished, TRUE), 0.2 SECONDS)
+ if(!(dummy_tail.wag_flags & WAG_WAGGING))
+ TEST_FAIL("Tail did not start wagging when it should have!")
+
+ UNTIL(timer_finished) // wait a little bit
+
+ if(dummy_tail.wag_flags & WAG_WAGGING)
+ TEST_FAIL("Tail was supposed to stop wagging on its own after 0.1 seconds but it did not!")
+
+ // TESTING TAIL REMOVAL
+
+ // remove the tail
+ dummy_tail.Remove(dummy, special = TRUE)
+
+ // check if tail is still wagging after being removed
+ if(dummy_tail.wag_flags & WAG_WAGGING)
+ TEST_FAIL("Tail was still wagging after being removed!")
+
+ // try to wag the removed tail
+ SEND_SIGNAL(dummy, COMSIG_ORGAN_WAG_TAIL, TRUE)
+ if(dummy_tail.wag_flags & WAG_WAGGING)
+ TEST_FAIL("A disembodied tail was able to start wagging!")
+
+ // TESTING MOB DEATH
+
+ // put it back and start wagging again
+ dummy_tail.Insert(dummy, special = TRUE, drop_if_replaced = FALSE)
+ SEND_SIGNAL(dummy, COMSIG_ORGAN_WAG_TAIL, TRUE)
+ if(!(dummy_tail.wag_flags & WAG_WAGGING))
+ TEST_FAIL("Tail did not start wagging when it should have!")
+
+ // kill the mob, see if it stops wagging
+ dummy.adjustBruteLoss(9001)
+ if(dummy_tail.wag_flags & WAG_WAGGING)
+ TEST_FAIL("A mob's tail was still wagging after being killed!")
+
+ // check if we are still able to wag the tail after death
+ SEND_SIGNAL(dummy, COMSIG_ORGAN_WAG_TAIL, TRUE)
+ if(dummy_tail.wag_flags & WAG_WAGGING)
+ TEST_FAIL("A dead mob was able to wag their tail!")
diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm
index 583a74dacc768..414c4189bb4a1 100644
--- a/code/modules/unit_tests/unit_test.dm
+++ b/code/modules/unit_tests/unit_test.dm
@@ -311,7 +311,7 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests())
//Needs a holodeck area linked to it which is not guarenteed to exist and technically is supposed to have a 1:1 relationship with computer anyway.
returnable_list += typesof(/obj/machinery/computer/holodeck)
//runtimes if not paired with a landmark
- returnable_list += typesof(/obj/structure/industrial_lift)
+ returnable_list += typesof(/obj/structure/transport/linear)
// Runtimes if the associated machinery does not exist, but not the base type
returnable_list += subtypesof(/obj/machinery/airlock_controller)
// Always ought to have an associated escape menu. Any references it could possibly hold would need one regardless.
diff --git a/code/modules/uplink/uplink_items/badass.dm b/code/modules/uplink/uplink_items/badass.dm
index 67676edcd7da9..458747a2a7b3f 100644
--- a/code/modules/uplink/uplink_items/badass.dm
+++ b/code/modules/uplink/uplink_items/badass.dm
@@ -22,7 +22,12 @@
if(!.)
return
- notify_ghosts("[user] has purchased a BADASS Syndicate Balloon!", source = src, action = NOTIFY_ORBIT, header = "What are they THINKING?")
+ notify_ghosts(
+ "[user] has purchased a BADASS Syndicate Balloon!",
+ source = src,
+ action = NOTIFY_ORBIT,
+ header = "What are they THINKING?",
+ )
/datum/uplink_item/badass/syndiecards
name = "Syndicate Playing Cards"
diff --git a/code/modules/uplink/uplink_items/clownops.dm b/code/modules/uplink/uplink_items/clownops.dm
index b973869cf121b..88cd9b6464f63 100644
--- a/code/modules/uplink/uplink_items/clownops.dm
+++ b/code/modules/uplink/uplink_items/clownops.dm
@@ -114,6 +114,26 @@
restricted = TRUE
refundable = TRUE
+/datum/uplink_item/reinforcement/monkey_agent
+ name = "Simian Agent Reinforcements"
+ desc = "Call in an extremely well trained monkey secret agent from our Syndicate Banana Department. \
+ They've been trained to operate machinery and can read, but they can't speak Common."
+ item = /obj/item/antag_spawner/loadout/monkey_man
+ cost = 7
+ purchasable_from = UPLINK_CLOWN_OPS
+ restricted = TRUE
+ refundable = TRUE
+
+/datum/uplink_item/reinforcement/monkey_supplies
+ name = "Simian Agent Supplies"
+ desc = "Sometimes you need a bit more firepower than a rabid monkey. Such as a rabid, armed monkey! \
+ Monkeys can unpack this kit to recieve a bag with a bargain-bin gun, ammunition, and some miscellaneous supplies."
+ item = /obj/item/storage/toolbox/guncase/monkeycase
+ cost = 4
+ purchasable_from = UPLINK_CLOWN_OPS
+ restricted = TRUE
+ refundable = TRUE
+
/datum/uplink_item/mech/honker
name = "Dark H.O.N.K."
desc = "A clown combat mech equipped with bombanana peel and tearstache grenade launchers, as well as the ubiquitous HoNkER BlAsT 5000."
diff --git a/code/modules/uplink/uplink_items/device_tools.dm b/code/modules/uplink/uplink_items/device_tools.dm
index 0fd5aa35b781b..66cb58c7b2899 100644
--- a/code/modules/uplink/uplink_items/device_tools.dm
+++ b/code/modules/uplink/uplink_items/device_tools.dm
@@ -46,7 +46,7 @@
desc = "When linked to a tram's on board computer systems, this device allows the user to manipulate the controls remotely. \
Includes direction toggle and a rapid mode to bypass door safety checks and crossing signals. \
Perfect for running someone over in the name of a tram malfunction!"
- item = /obj/item/tram_remote
+ item = /obj/item/assembly/control/transport/remote
cost = 2
/datum/uplink_item/device_tools/thermal
diff --git a/code/modules/uplink/uplink_items/job.dm b/code/modules/uplink/uplink_items/job.dm
index 2dc7fdf153cc5..01bef40289eb4 100644
--- a/code/modules/uplink/uplink_items/job.dm
+++ b/code/modules/uplink/uplink_items/job.dm
@@ -333,3 +333,32 @@
item = /obj/item/seeds/seedling/evil
cost = 8
restricted_roles = list(JOB_BOTANIST)
+
+/datum/uplink_item/role_restricted/bee_smoker
+ name = "Bee Smoker"
+ desc = "A device that runs on cannabis, turning it into a gas that can hypnotize bees to follow our commands."
+ item = /obj/item/bee_smoker
+ cost = 4
+ restricted_roles = list(JOB_BOTANIST)
+
+/datum/uplink_item/role_restricted/monkey_agent
+ name = "Simian Agent Reinforcements"
+ desc = "Call in an extremely well trained monkey secret agent from our Syndicate Banana Department. \
+ They've been trained to operate machinery and can read, but they can't speak Common. \
+ Please note that these are free-range monkeys that don't react with Mutadone."
+ item = /obj/item/antag_spawner/loadout/monkey_man
+ cost = 6
+ restricted_roles = list(JOB_RESEARCH_DIRECTOR, JOB_SCIENTIST, JOB_GENETICIST, JOB_ASSISTANT, JOB_MIME, JOB_CLOWN)
+ restricted = TRUE
+ refundable = TRUE
+
+/datum/uplink_item/role_restricted/monkey_supplies
+ name = "Simian Agent Supplies"
+ desc = "Sometimes you need a bit more firepower than a rabid monkey. Such as a rabid, armed monkey! \
+ Monkeys can unpack this kit to recieve a bag with a bargain-bin gun, ammunition, and some miscellaneous supplies."
+ item = /obj/item/storage/toolbox/guncase/monkeycase
+ cost = 4
+ limited_stock = 3
+ restricted_roles = list(JOB_ASSISTANT, JOB_MIME, JOB_CLOWN)
+ restricted = TRUE
+ refundable = FALSE
diff --git a/code/modules/uplink/uplink_items/nukeops.dm b/code/modules/uplink/uplink_items/nukeops.dm
index 8a1c301830a09..b872fc61c3a85 100644
--- a/code/modules/uplink/uplink_items/nukeops.dm
+++ b/code/modules/uplink/uplink_items/nukeops.dm
@@ -42,6 +42,21 @@
// ~~ Weapon Categories ~~
+// Core Gear Box: This contains all the 'fundamental' equipment that most nuclear operatives probably should be buying. It isn't cheaper, but it is a quick and convenient method of acquiring all the gear necessary immediately.
+// Only allows one purchase, and doesn't prevent the purchase of the contained items. Focused on newer players to help them understand what items they need to succeed, and to help older players quickly purchase the baseline gear they need.
+
+/datum/uplink_item/weapon_kits/core
+ name = "Core Equipment Box (Essential)"
+ desc = "This box contains an airlock authentification override card, a C-4 explosive charge, a freedom implant and a stimpack injector. \
+ The most important support items for most operatives to succeed in their mission, bundled together. It is highly recommend you buy this kit. \
+ Note: This bundle is not at a discount. You can purchase all of these items separately. You do not NEED these items, but most operatives fail WITHOUT at \
+ least SOME of these items. More experienced operatives can do without."
+ item = /obj/item/storage/box/syndie_kit/core_gear
+ cost = 14 //freedom 5, doormag 3, c-4 1, stimpack 5
+ limited_stock = 1
+ cant_discount = TRUE
+ purchasable_from = UPLINK_NUKE_OPS
+
//Low-cost firearms: Around 8 TC each. Meant for easy squad weapon purchases
/datum/uplink_item/weapon_kits/low_cost
@@ -153,7 +168,7 @@
cost = 4
purchasable_from = UPLINK_NUKE_OPS
-// ~~ Energy Sword and Shield ~~
+// ~~ Energy Sword and Shield & CQC ~~
/datum/uplink_item/weapon_kits/medium_cost/sword_and_board
name = "Energy Shield and Sword Case (Very Hard)"
@@ -161,6 +176,13 @@
energy and laser projectiles, and the sword most forms of attack. Perfect for the enterprising nuclear knight. "
item = /obj/item/storage/toolbox/guncase/sword_and_board
+/datum/uplink_item/weapon_kits/medium_cost/cqc
+ name = "CQC Equipment Case (Very Hard)"
+ desc = "Contains a manual that instructs you in the ways of CQC, or Close Quarters Combat. Comes with a stealth implant, a pack of smokes and a snazzy bandana (use it with the hat stabilizers in your MODsuit)."
+ item = /obj/item/storage/toolbox/guncase/cqc
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS
+ surplus = 0
+
// ~~ Syndicate Revolver ~~
// Nuclear operatives get a special deal on their revolver purchase compared to traitors.
@@ -331,13 +353,6 @@
Blast your enemies with instant shots! Just watch out for the rebound..."
item = /obj/item/ammo_box/magazine/sniper_rounds/marksman
-/datum/uplink_item/weapon_kits/high_cost/cqc
- name = "CQC Equipment Case (Very Hard)"
- desc = "Contains a manual that instructs you in the ways of CQC, or Close Quarters Combat. Comes with a stealth implant and a snazzy bandana (and a hat stabilizer to go with it)."
- item = /obj/item/storage/toolbox/guncase/cqc
- purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS
- surplus = 0
-
/datum/uplink_item/weapon_kits/high_cost/doublesword
name = "Double-Energy Sword Case (Very Hard)"
desc = "A case containing a double-energy sword, anti-slip module, meth autoinjector, and a bar of soap. \
@@ -803,7 +818,7 @@
/datum/uplink_item/badass/hats
name = "Hat Crate"
- desc = "Hat crate! Contains hats, along with hat stabilizers to wear your hats while you're in your suit! HATS!!!"
+ desc = "Hat crate! Contains hats! HATS!!!"
item = /obj/structure/closet/crate/large/hats
cost = 5
purchasable_from = UPLINK_CLOWN_OPS | UPLINK_NUKE_OPS
diff --git a/code/modules/uplink/uplink_items/stealthy.dm b/code/modules/uplink/uplink_items/stealthy.dm
index 2f205a9d0bd69..36cb47cc28f46 100644
--- a/code/modules/uplink/uplink_items/stealthy.dm
+++ b/code/modules/uplink/uplink_items/stealthy.dm
@@ -76,7 +76,7 @@
and gain the ability to swat bullets from the air, but you will also refuse to use dishonorable ranged weaponry."
item = /obj/item/book/granter/martial/carp
progression_minimum = 30 MINUTES
- cost = 13
+ cost = 17
surplus = 0
purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
diff --git a/code/modules/vehicles/atv.dm b/code/modules/vehicles/atv.dm
index c4fd2bcaceb7e..b72449de091c3 100644
--- a/code/modules/vehicles/atv.dm
+++ b/code/modules/vehicles/atv.dm
@@ -117,12 +117,11 @@
smoke.start()
/obj/vehicle/ridden/atv/bullet_act(obj/projectile/P)
- if(prob(50) || !buckled_mobs)
+ if(prob(50) || !LAZYLEN(buckled_mobs))
return ..()
- for(var/m in buckled_mobs)
- var/mob/buckled_mob = m
+ for(var/mob/buckled_mob as anything in buckled_mobs)
buckled_mob.bullet_act(P)
- return TRUE
+ return BULLET_ACT_HIT
/obj/vehicle/ridden/atv/atom_destruction()
explosion(src, devastation_range = -1, light_impact_range = 2, flame_range = 3, flash_range = 4)
diff --git a/code/modules/vehicles/cars/clowncar.dm b/code/modules/vehicles/cars/clowncar.dm
index 1fd230bb47a82..6b019cac27059 100644
--- a/code/modules/vehicles/cars/clowncar.dm
+++ b/code/modules/vehicles/cars/clowncar.dm
@@ -198,7 +198,7 @@
* * Fart and make everyone nearby laugh
*/
/obj/vehicle/sealed/car/clowncar/proc/roll_the_dice(mob/user)
- if(TIMER_COOLDOWN_CHECK(src, COOLDOWN_CLOWNCAR_RANDOMNESS))
+ if(TIMER_COOLDOWN_RUNNING(src, COOLDOWN_CLOWNCAR_RANDOMNESS))
to_chat(user, span_notice("The button panel is currently recharging."))
return
TIMER_COOLDOWN_START(src, COOLDOWN_CLOWNCAR_RANDOMNESS, dice_cooldown_time)
diff --git a/code/modules/vehicles/mecha/_mecha.dm b/code/modules/vehicles/mecha/_mecha.dm
index 0652c554b8a4a..f586a9656e82b 100644
--- a/code/modules/vehicles/mecha/_mecha.dm
+++ b/code/modules/vehicles/mecha/_mecha.dm
@@ -173,6 +173,10 @@
var/overclock_temp = 0
///Temperature threshold at which actuators may start causing internal damage
var/overclock_temp_danger = 15
+ ///Whether the mech has an option to enable safe overclocking
+ var/overclock_safety_available = FALSE
+ ///Whether the overclocking turns off automatically when overheated
+ var/overclock_safety = FALSE
//Bool for zoom on/off
var/zoom_mode = FALSE
@@ -520,6 +524,9 @@
overclock_temp = min(overclock_temp + seconds_per_tick, overclock_temp_danger * 2)
if(overclock_temp < overclock_temp_danger)
return
+ if(overclock_temp >= overclock_temp_danger && overclock_safety)
+ toggle_overclock(FALSE)
+ return
var/damage_chance = 100 * ((overclock_temp - overclock_temp_danger) / (overclock_temp_danger * 2))
if(SPT_PROB(damage_chance, seconds_per_tick))
do_sparks(5, TRUE, src)
@@ -675,7 +682,7 @@
if(!(livinguser in return_controllers_with_flag(VEHICLE_CONTROL_MELEE)))
to_chat(livinguser, span_warning("You're in the wrong seat to interact with your hands."))
return
- var/on_cooldown = TIMER_COOLDOWN_CHECK(src, COOLDOWN_MECHA_MELEE_ATTACK)
+ var/on_cooldown = TIMER_COOLDOWN_RUNNING(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
@@ -742,7 +749,7 @@
balloon_alert(user, "cabin can't be sealed!")
log_message("Tried to seal cabin. This mech can't be airtight.", LOG_MECHA)
return
- if(TIMER_COOLDOWN_CHECK(src, COOLDOWN_MECHA_CABIN_SEAL))
+ if(TIMER_COOLDOWN_RUNNING(src, COOLDOWN_MECHA_CABIN_SEAL))
balloon_alert(user, "on cooldown!")
return
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_CABIN_SEAL, 1 SECONDS)
@@ -811,6 +818,15 @@
else
overclock_mode = !overclock_mode
log_message("Toggled overclocking.", LOG_MECHA)
+
+ for(var/mob/occupant as anything in occupants)
+ var/datum/action/act = locate(/datum/action/vehicle/sealed/mecha/mech_overclock) in occupant.actions
+ if(!act)
+ continue
+ act.button_icon_state = "mech_overload_[overclock_mode ? "on" : "off"]"
+ balloon_alert(occupant, "overclock [overclock_mode ? "on":"off"]")
+ act.build_all_button_icons()
+
if(overclock_mode)
movedelay = movedelay / overclock_coeff
visible_message(span_notice("[src] starts heating up, making humming sounds."))
@@ -857,5 +873,5 @@
act.button_icon_state = "mech_lights_on"
else
act.button_icon_state = "mech_lights_off"
- balloon_alert(occupant, "toggled lights [mecha_flags & LIGHTS_ON ? "on":"off"]")
+ balloon_alert(occupant, "lights [mecha_flags & LIGHTS_ON ? "on":"off"]")
act.build_all_button_icons()
diff --git a/code/modules/vehicles/mecha/combat/durand.dm b/code/modules/vehicles/mecha/combat/durand.dm
index 4ac13b8dd64b7..f1c7bac4f81cd 100644
--- a/code/modules/vehicles/mecha/combat/durand.dm
+++ b/code/modules/vehicles/mecha/combat/durand.dm
@@ -233,7 +233,7 @@ own integrity back to max. Shield is automatically dropped if we run out of powe
set_light_on(chassis.defense_mode)
if(chassis.defense_mode)
- invisibility = 0
+ SetInvisibility(INVISIBILITY_NONE, id=type)
flick("shield_raise", src)
playsound(src, 'sound/mecha/mech_shield_raise.ogg', 50, FALSE)
icon_state = "shield"
@@ -256,7 +256,7 @@ own integrity back to max. Shield is automatically dropped if we run out of powe
*/
/obj/durand_shield/proc/make_invisible()
if(!chassis.defense_mode)
- invisibility = INVISIBILITY_MAXIMUM
+ RemoveInvisibility(type)
/obj/durand_shield/proc/resetdir(datum/source, olddir, newdir)
SIGNAL_HANDLER
diff --git a/code/modules/vehicles/mecha/combat/gygax.dm b/code/modules/vehicles/mecha/combat/gygax.dm
index 204739cd7fc0f..223ab66ca31d8 100644
--- a/code/modules/vehicles/mecha/combat/gygax.dm
+++ b/code/modules/vehicles/mecha/combat/gygax.dm
@@ -22,6 +22,8 @@
)
step_energy_drain = 4
can_use_overclock = TRUE
+ overclock_safety_available = TRUE
+ overclock_safety = TRUE
/datum/armor/mecha_gygax
melee = 25
diff --git a/code/modules/vehicles/mecha/combat/marauder.dm b/code/modules/vehicles/mecha/combat/marauder.dm
index 79b0956899430..2fe8da4bdc73c 100644
--- a/code/modules/vehicles/mecha/combat/marauder.dm
+++ b/code/modules/vehicles/mecha/combat/marauder.dm
@@ -61,7 +61,7 @@
/datum/action/vehicle/sealed/mecha/mech_smoke/Trigger(trigger_flags)
if(!owner || !chassis || !(owner in chassis.occupants))
return
- if(!TIMER_COOLDOWN_CHECK(src, COOLDOWN_MECHA_SMOKE) && chassis.smoke_charges>0)
+ if(TIMER_COOLDOWN_FINISHED(src, COOLDOWN_MECHA_SMOKE) && chassis.smoke_charges>0)
chassis.smoke_system.start()
chassis.smoke_charges--
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_SMOKE, chassis.smoke_cooldown)
diff --git a/code/modules/vehicles/mecha/combat/savannah_ivanov.dm b/code/modules/vehicles/mecha/combat/savannah_ivanov.dm
index e3926aa7d3502..1851e6b39b19a 100644
--- a/code/modules/vehicles/mecha/combat/savannah_ivanov.dm
+++ b/code/modules/vehicles/mecha/combat/savannah_ivanov.dm
@@ -86,7 +86,7 @@
if(chassis.phasing)
to_chat(owner, span_warning("You're already airborne!"))
return
- if(TIMER_COOLDOWN_CHECK(chassis, COOLDOWN_MECHA_SKYFALL))
+ if(TIMER_COOLDOWN_RUNNING(chassis, COOLDOWN_MECHA_SKYFALL))
var/timeleft = S_TIMER_COOLDOWN_TIMELEFT(chassis, COOLDOWN_MECHA_SKYFALL)
to_chat(owner, span_warning("You need to wait [DisplayTimeText(timeleft, 1)] before attempting to Skyfall."))
return
@@ -254,7 +254,7 @@
/datum/action/vehicle/sealed/mecha/ivanov_strike/Trigger(trigger_flags)
if(!owner || !chassis || !(owner in chassis.occupants))
return
- if(TIMER_COOLDOWN_CHECK(chassis, COOLDOWN_MECHA_MISSILE_STRIKE))
+ if(TIMER_COOLDOWN_RUNNING(chassis, COOLDOWN_MECHA_MISSILE_STRIKE))
var/timeleft = S_TIMER_COOLDOWN_TIMELEFT(chassis, COOLDOWN_MECHA_MISSILE_STRIKE)
to_chat(owner, span_warning("You need to wait [DisplayTimeText(timeleft, 1)] before firing another Ivanov Strike."))
return
diff --git a/code/modules/vehicles/mecha/equipment/mecha_equipment.dm b/code/modules/vehicles/mecha/equipment/mecha_equipment.dm
index f9ee84ba4b893..aea1a21c0829e 100644
--- a/code/modules/vehicles/mecha/equipment/mecha_equipment.dm
+++ b/code/modules/vehicles/mecha/equipment/mecha_equipment.dm
@@ -107,7 +107,7 @@
if(get_integrity() <= 1)
to_chat(chassis.occupants, span_warning("Error -- Equipment critically damaged."))
return FALSE
- if(TIMER_COOLDOWN_CHECK(chassis, COOLDOWN_MECHA_EQUIPMENT(type)))
+ if(TIMER_COOLDOWN_RUNNING(chassis, COOLDOWN_MECHA_EQUIPMENT(type)))
return FALSE
return TRUE
diff --git a/code/modules/vehicles/mecha/equipment/weapons/weapons.dm b/code/modules/vehicles/mecha/equipment/weapons/weapons.dm
index fe97c5f2b89ae..a879341629650 100644
--- a/code/modules/vehicles/mecha/equipment/weapons/weapons.dm
+++ b/code/modules/vehicles/mecha/equipment/weapons/weapons.dm
@@ -78,7 +78,7 @@
//Base energy weapon type
/obj/item/mecha_parts/mecha_equipment/weapon/energy
name = "general energy weapon"
- firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/energy
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/red
/obj/item/mecha_parts/mecha_equipment/weapon/energy/laser
equip_cooldown = 8
@@ -100,6 +100,7 @@
variance = 25
projectiles_per_shot = 5
fire_sound = 'sound/weapons/taser2.ogg'
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/blue
/obj/item/mecha_parts/mecha_equipment/weapon/energy/laser/heavy
equip_cooldown = 15
@@ -173,6 +174,7 @@
equip_cooldown = 8
projectile = /obj/projectile/energy/electrode
fire_sound = 'sound/weapons/taser.ogg'
+ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect
/obj/item/mecha_parts/mecha_equipment/weapon/honker
diff --git a/code/modules/vehicles/mecha/mecha_actions.dm b/code/modules/vehicles/mecha/mecha_actions.dm
index f42e89aa1d55f..43301c19605e3 100644
--- a/code/modules/vehicles/mecha/mecha_actions.dm
+++ b/code/modules/vehicles/mecha/mecha_actions.dm
@@ -152,6 +152,5 @@
if(!owner || !chassis || !(owner in chassis.occupants))
return
chassis.toggle_overclock(forced_state)
- chassis.balloon_alert(owner, chassis.overclock_mode ? "started overclocking" : "stopped overclocking")
button_icon_state = "mech_overload_[chassis.overclock_mode ? "on" : "off"]"
build_all_button_icons()
diff --git a/code/modules/vehicles/mecha/mecha_defense.dm b/code/modules/vehicles/mecha/mecha_defense.dm
index 37fe73f4a2d11..3df6ba94992d8 100644
--- a/code/modules/vehicles/mecha/mecha_defense.dm
+++ b/code/modules/vehicles/mecha/mecha_defense.dm
@@ -41,6 +41,7 @@
if(damage_taken <= 0 || atom_integrity < 0)
return damage_taken
+ diag_hud_set_mechhealth()
spark_system?.start()
try_deal_internal_damage(damage_taken)
if(damage_taken >= 5 || prob(33))
@@ -114,10 +115,19 @@
return ..()
/obj/vehicle/sealed/mecha/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit) //wrapper
- if(!enclosed && LAZYLEN(occupants) && !(mecha_flags & SILICON_PILOT) && (hitting_projectile.def_zone == BODY_ZONE_HEAD || hitting_projectile.def_zone == BODY_ZONE_CHEST)) //allows bullets to hit the pilot of open-canopy mechs
+ . = ..()
+ if(. != BULLET_ACT_HIT)
+ return .
+
+ //allows bullets to hit the pilot of open-canopy mechs
+ if(!enclosed \
+ && LAZYLEN(occupants) \
+ && !(mecha_flags & SILICON_PILOT) \
+ && (def_zone == BODY_ZONE_HEAD || def_zone == BODY_ZONE_CHEST))
for(var/mob/living/hitmob as anything in occupants)
hitmob.bullet_act(hitting_projectile, def_zone, piercing_hit) //If the sides are open, the occupant can be hit
return BULLET_ACT_HIT
+
log_message("Hit by projectile. Type: [hitting_projectile]([hitting_projectile.damage_type]).", LOG_MECHA, color="red")
// yes we *have* to run the armor calc proc here I love tg projectile code too
try_damage_component(run_atom_armor(
@@ -126,8 +136,7 @@
damage_flag = hitting_projectile.armor_flag,
attack_dir = REVERSE_DIR(hitting_projectile.dir),
armour_penetration = hitting_projectile.armour_penetration,
- ), hitting_projectile.def_zone)
- return ..()
+ ), def_zone)
/obj/vehicle/sealed/mecha/ex_act(severity, target)
log_message("Affected by explosion of severity: [severity].", LOG_MECHA, color="red")
@@ -324,6 +333,7 @@
. = ..()
if(.)
try_damage_component(., user.zone_selected)
+ diag_hud_set_mechhealth()
/obj/vehicle/sealed/mecha/examine(mob/user)
. = ..()
@@ -420,6 +430,7 @@
break
if(did_the_thing)
user.balloon_alert_to_viewers("[(atom_integrity >= max_integrity) ? "fully" : "partially"] repaired [src]")
+ diag_hud_set_mechhealth()
else
user.balloon_alert_to_viewers("stopped welding [src]", "interrupted the repair!")
@@ -428,6 +439,7 @@
atom_integrity = max_integrity
if(cell && charge_cell)
cell.charge = cell.maxcharge
+ diag_hud_set_mechcell()
if(internal_damage & MECHA_INT_FIRE)
clear_internal_damage(MECHA_INT_FIRE)
if(internal_damage & MECHA_INT_TEMP_CONTROL)
@@ -438,6 +450,7 @@
clear_internal_damage(MECHA_CABIN_AIR_BREACH)
if(internal_damage & MECHA_INT_CONTROL_LOST)
clear_internal_damage(MECHA_INT_CONTROL_LOST)
+ diag_hud_set_mechhealth()
/obj/vehicle/sealed/mecha/narsie_act()
emp_act(EMP_HEAVY)
diff --git a/code/modules/vehicles/mecha/mecha_helpers.dm b/code/modules/vehicles/mecha/mecha_helpers.dm
index ad012386df392..c9de84747e915 100644
--- a/code/modules/vehicles/mecha/mecha_helpers.dm
+++ b/code/modules/vehicles/mecha/mecha_helpers.dm
@@ -8,7 +8,10 @@
return cell?.charge
/obj/vehicle/sealed/mecha/proc/use_power(amount)
- return (get_charge() && cell.use(amount))
+ var/output = get_charge() && cell.use(amount)
+ if (output)
+ diag_hud_set_mechcell()
+ return output
/obj/vehicle/sealed/mecha/proc/give_power(amount)
if(!isnull(get_charge()))
diff --git a/code/modules/vehicles/mecha/mecha_movement.dm b/code/modules/vehicles/mecha/mecha_movement.dm
index d6c8feed4b3b9..e8c43ae48f94a 100644
--- a/code/modules/vehicles/mecha/mecha_movement.dm
+++ b/code/modules/vehicles/mecha/mecha_movement.dm
@@ -70,14 +70,14 @@
return loc_atom.relaymove(src, direction)
var/obj/machinery/portable_atmospherics/canister/internal_tank = get_internal_tank()
if(internal_tank?.connected_port)
- if(!TIMER_COOLDOWN_CHECK(src, COOLDOWN_MECHA_MESSAGE))
+ if(TIMER_COOLDOWN_FINISHED(src, COOLDOWN_MECHA_MESSAGE))
to_chat(occupants, "[icon2html(src, occupants)][span_warning("Unable to move while connected to the air system port!")]")
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MESSAGE, 2 SECONDS)
return FALSE
if(!Process_Spacemove(direction))
return FALSE
if(zoom_mode)
- if(!TIMER_COOLDOWN_CHECK(src, COOLDOWN_MECHA_MESSAGE))
+ if(TIMER_COOLDOWN_FINISHED(src, COOLDOWN_MECHA_MESSAGE))
to_chat(occupants, "[icon2html(src, occupants)][span_warning("Unable to move while in zoom mode!")]")
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MESSAGE, 2 SECONDS)
return FALSE
@@ -89,17 +89,17 @@
if(isnull(servo))
missing_parts += "micro-servo"
if(length(missing_parts))
- if(!TIMER_COOLDOWN_CHECK(src, COOLDOWN_MECHA_MESSAGE))
+ if(TIMER_COOLDOWN_FINISHED(src, COOLDOWN_MECHA_MESSAGE))
to_chat(occupants, "[icon2html(src, occupants)][span_warning("Missing [english_list(missing_parts)].")]")
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MESSAGE, 2 SECONDS)
return FALSE
if(!use_power(step_energy_drain))
- if(!TIMER_COOLDOWN_CHECK(src, COOLDOWN_MECHA_MESSAGE))
+ if(TIMER_COOLDOWN_FINISHED(src, COOLDOWN_MECHA_MESSAGE))
to_chat(occupants, "[icon2html(src, occupants)][span_warning("Insufficient power to move!")]")
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MESSAGE, 2 SECONDS)
return FALSE
if(lavaland_only && is_mining_level(z))
- if(!TIMER_COOLDOWN_CHECK(src, COOLDOWN_MECHA_MESSAGE))
+ if(TIMER_COOLDOWN_FINISHED(src, COOLDOWN_MECHA_MESSAGE))
to_chat(occupants, "[icon2html(src, occupants)][span_warning("Invalid Environment.")]")
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MESSAGE, 2 SECONDS)
return FALSE
diff --git a/code/modules/vehicles/mecha/mecha_ui.dm b/code/modules/vehicles/mecha/mecha_ui.dm
index 815770875ea05..76d8b4613fac8 100644
--- a/code/modules/vehicles/mecha/mecha_ui.dm
+++ b/code/modules/vehicles/mecha/mecha_ui.dm
@@ -82,6 +82,8 @@
data["internal_damage"] = internal_damage
data["can_use_overclock"] = can_use_overclock
+ data["overclock_safety_available"] = overclock_safety_available
+ data["overclock_safety"] = overclock_safety
data["overclock_mode"] = overclock_mode
data["overclock_temp_percentage"] = overclock_temp / overclock_temp_danger
@@ -210,9 +212,8 @@
toggle_lights(user = usr)
if("toggle_overclock")
toggle_overclock()
- var/datum/action/act = locate(/datum/action/vehicle/sealed/mecha/mech_overclock) in usr.actions
- act.button_icon_state = "mech_overload_[overclock_mode ? "on" : "off"]"
- act.build_all_button_icons()
+ if("toggle_overclock_safety")
+ overclock_safety = !overclock_safety
if("repair_int_damage")
try_repair_int_damage(usr, params["flag"])
return FALSE
diff --git a/code/modules/vehicles/mecha/working/ripley.dm b/code/modules/vehicles/mecha/working/ripley.dm
index 9db12624125aa..d2b13d48d26ab 100644
--- a/code/modules/vehicles/mecha/working/ripley.dm
+++ b/code/modules/vehicles/mecha/working/ripley.dm
@@ -266,11 +266,11 @@ GLOBAL_DATUM(cargo_ripley, /obj/vehicle/sealed/mecha/ripley/cargo)
var/turf/T = get_turf(loc)
if(lavaland_equipment_pressure_check(T))
- movedelay = fast_pressure_step_in
+ movedelay = !overclock_mode ? fast_pressure_step_in : fast_pressure_step_in / overclock_coeff
for(var/obj/item/mecha_parts/mecha_equipment/drill/drill in flat_equipment)
drill.equip_cooldown = initial(drill.equip_cooldown) * 0.5
else
- movedelay = slow_pressure_step_in
+ movedelay = !overclock_mode ? slow_pressure_step_in : slow_pressure_step_in / overclock_coeff
for(var/obj/item/mecha_parts/mecha_equipment/drill/drill in flat_equipment)
drill.equip_cooldown = initial(drill.equip_cooldown)
diff --git a/code/modules/vehicles/vehicle_actions.dm b/code/modules/vehicles/vehicle_actions.dm
index 1fbf394d2700a..5afcd58c0c0ca 100644
--- a/code/modules/vehicles/vehicle_actions.dm
+++ b/code/modules/vehicles/vehicle_actions.dm
@@ -232,7 +232,7 @@
var/hornsound = 'sound/items/carhorn.ogg'
/datum/action/vehicle/sealed/horn/Trigger(trigger_flags)
- if(TIMER_COOLDOWN_CHECK(src, COOLDOWN_CAR_HONK))
+ if(TIMER_COOLDOWN_RUNNING(src, COOLDOWN_CAR_HONK))
return
TIMER_COOLDOWN_START(src, COOLDOWN_CAR_HONK, 2 SECONDS)
vehicle_entered_target.visible_message(span_danger("[vehicle_entered_target] loudly honks!"))
@@ -316,7 +316,7 @@
var/bell_cooldown
/datum/action/vehicle/ridden/wheelchair/bell/Trigger(trigger_flags)
- if(TIMER_COOLDOWN_CHECK(src, bell_cooldown))
+ if(TIMER_COOLDOWN_RUNNING(src, bell_cooldown))
return
TIMER_COOLDOWN_START(src, bell_cooldown, 0.5 SECONDS)
playsound(vehicle_ridden_target, 'sound/machines/microwave/microwave-end.ogg', 70)
diff --git a/code/modules/vending/megaseed.dm b/code/modules/vending/megaseed.dm
index 8bdc7e637e25a..130a0921a43a4 100644
--- a/code/modules/vending/megaseed.dm
+++ b/code/modules/vending/megaseed.dm
@@ -20,6 +20,7 @@
/obj/item/seeds/cocoapod = 3,
/obj/item/seeds/eggplant = 3,
/obj/item/seeds/grape = 3,
+ /obj/item/seeds/lanternfruit = 3,
/obj/item/seeds/lemon = 3,
/obj/item/seeds/lime = 3,
/obj/item/seeds/olive = 3,
diff --git a/code/modules/vending/security.dm b/code/modules/vending/security.dm
index 9b5af87ab44c6..b54edffe4c044 100644
--- a/code/modules/vending/security.dm
+++ b/code/modules/vending/security.dm
@@ -42,7 +42,7 @@
G.arm_grenade()
else if(istype(I, /obj/item/flashlight))
var/obj/item/flashlight/F = I
- F.on = TRUE
+ F.set_light_on(TRUE)
F.update_brightness()
/obj/item/vending_refill/security
diff --git a/code/modules/wiremod/components/action/pathfind.dm b/code/modules/wiremod/components/action/pathfind.dm
index e7dcb16020745..0de6d346db17f 100644
--- a/code/modules/wiremod/components/action/pathfind.dm
+++ b/code/modules/wiremod/components/action/pathfind.dm
@@ -56,9 +56,11 @@
if(isnull(target_Y))
return
- var/atom/path_id = id_card.value
- if(path_id && !isidcard(path_id))
- path_id = null
+ var/list/access = list()
+ if(isidcard(id_card.value))
+ var/obj/item/card/id/id = id_card.value
+ access = id.GetAccess()
+ else if (id_card.value)
failed.set_output(COMPONENT_SIGNAL)
reason_failed.set_output("Object marked is not an ID! Using no ID instead.")
@@ -75,7 +77,7 @@
return
// If we're going to the same place and the cooldown hasn't subsided, we're probably on the same path as before
- if (destination == old_dest && TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME))
+ if (destination == old_dest && TIMER_COOLDOWN_RUNNING(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME))
// Check if the current turf is the same as the current turf we're supposed to be in. If so, then we set the next step as the next turf on the list
if(current_turf == next_turf)
@@ -90,7 +92,7 @@
else // Either we're not going to the same place or the cooldown is over. Either way, we need a new path
- if(destination != old_dest && TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_PATHFIND_DIF))
+ if(destination != old_dest && TIMER_COOLDOWN_RUNNING(parent, COOLDOWN_CIRCUIT_PATHFIND_DIF))
failed.set_output(COMPONENT_SIGNAL)
reason_failed.set_output("Cooldown still active!")
return
@@ -98,7 +100,7 @@
TIMER_COOLDOWN_END(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME)
old_dest = destination
- path = get_path_to(src, destination, max_range, id=path_id)
+ path = get_path_to(src, destination, max_range, access=access)
if(length(path) == 0 || !path)// Check if we can even path there
next_turf = null
failed.set_output(COMPONENT_SIGNAL)
diff --git a/code/modules/wiremod/components/action/radio.dm b/code/modules/wiremod/components/action/radio.dm
index cd13e0e1e4515..3940059453ead 100644
--- a/code/modules/wiremod/components/action/radio.dm
+++ b/code/modules/wiremod/components/action/radio.dm
@@ -28,8 +28,17 @@
/// The ckey of the user who used the shell we were placed in, important for signalling logs.
var/owner_ckey = null
+ /// The radio connection we are using to receive signals.
var/datum/radio_frequency/radio_connection
+ /// How long of a cooldown we have before we can send another signal.
+ var/signal_cooldown_time = 1 SECONDS
+
+/obj/item/circuit_component/radio/Initialize(mapload)
+ . = ..()
+ if(signal_cooldown_time > 0)
+ desc = "[desc] It has a [signal_cooldown_time * 0.1] second cooldown between sending signals."
+
/obj/item/circuit_component/radio/register_shell(atom/movable/shell)
parent_shell = shell
var/potential_fingerprints = shell.fingerprintslast
@@ -65,8 +74,10 @@
INVOKE_ASYNC(src, PROC_REF(handle_radio_input), port)
/obj/item/circuit_component/radio/proc/handle_radio_input(datum/port/input/port)
- var/frequency = freq.value
+ if(TIMER_COOLDOWN_RUNNING(parent, COOLDOWN_SIGNALLER_SEND))
+ return
+ var/frequency = freq.value
if(frequency != current_freq)
SSradio.remove_object(src, current_freq)
radio_connection = SSradio.add_object(src, frequency, RADIO_SIGNALER)
@@ -84,7 +95,8 @@
loggable_strings += ": The last fingerprints on the containing shell was [parent_shell.fingerprintslast]."
var/loggable_string = loggable_strings.Join(" ")
- GLOB.lastsignalers.Add(loggable_string)
+ add_to_signaler_investigate_log(loggable_string)
+ TIMER_COOLDOWN_START(parent, COOLDOWN_SIGNALLER_SEND, signal_cooldown_time)
var/datum/signal/signal = new(list("code" = signal_code, "key" = parent?.owner_id), logging_data = loggable_string)
radio_connection.post_signal(src, signal)
diff --git a/code/modules/wiremod/components/action/soundemitter.dm b/code/modules/wiremod/components/action/soundemitter.dm
index 6ee9b273fae39..44b9cbae8ab05 100644
--- a/code/modules/wiremod/components/action/soundemitter.dm
+++ b/code/modules/wiremod/components/action/soundemitter.dm
@@ -87,7 +87,7 @@
if(!parent.shell)
return
- if(TIMER_COOLDOWN_CHECK(parent.shell, COOLDOWN_CIRCUIT_SOUNDEMITTER))
+ if(TIMER_COOLDOWN_RUNNING(parent.shell, COOLDOWN_CIRCUIT_SOUNDEMITTER))
return
var/sound_to_play = options_map[sound_file.value]
diff --git a/code/modules/wiremod/components/action/speech.dm b/code/modules/wiremod/components/action/speech.dm
index 809c473c14a50..4bf735cf82bf0 100644
--- a/code/modules/wiremod/components/action/speech.dm
+++ b/code/modules/wiremod/components/action/speech.dm
@@ -26,7 +26,7 @@
if(!parent.shell)
return
- if(TIMER_COOLDOWN_CHECK(parent.shell, COOLDOWN_CIRCUIT_SPEECH))
+ if(TIMER_COOLDOWN_RUNNING(parent.shell, COOLDOWN_CIRCUIT_SPEECH))
return
if(message.value)
diff --git a/code/modules/wiremod/components/bci/hud/target_intercept.dm b/code/modules/wiremod/components/bci/hud/target_intercept.dm
index 8352b0aead991..bfdaec13122a0 100644
--- a/code/modules/wiremod/components/bci/hud/target_intercept.dm
+++ b/code/modules/wiremod/components/bci/hud/target_intercept.dm
@@ -42,7 +42,7 @@
if(!owner || !istype(owner) || !owner.client)
return
- if(TIMER_COOLDOWN_CHECK(parent.shell, COOLDOWN_CIRCUIT_TARGET_INTERCEPT))
+ if(TIMER_COOLDOWN_RUNNING(parent.shell, COOLDOWN_CIRCUIT_TARGET_INTERCEPT))
return
to_chat(owner, "Left-click to trigger target interceptor!")
diff --git a/code/modules/wiremod/components/list/assoc_list_pick.dm b/code/modules/wiremod/components/list/assoc_list_pick.dm
index 9c8e94c074a63..64dbe6a0a0b65 100644
--- a/code/modules/wiremod/components/list/assoc_list_pick.dm
+++ b/code/modules/wiremod/components/list/assoc_list_pick.dm
@@ -15,7 +15,6 @@
/obj/item/circuit_component/list_pick/assoc/make_list_port()
input_list = add_input_port("List", PORT_TYPE_ASSOC_LIST(PORT_TYPE_STRING, PORT_TYPE_ANY))
-
/obj/item/circuit_component/list_pick/assoc/pre_input_received(datum/port/input/port)
if(port == list_options)
var/new_type = list_options.value
diff --git a/code/modules/wiremod/components/list/assoc_literal.dm b/code/modules/wiremod/components/list/assoc_literal.dm
index a3f97cae74550..f829ad08f25ec 100644
--- a/code/modules/wiremod/components/list/assoc_literal.dm
+++ b/code/modules/wiremod/components/list/assoc_literal.dm
@@ -39,20 +39,20 @@
/obj/item/circuit_component/assoc_literal/populate_ports()
AddComponent(/datum/component/circuit_component_add_port, \
- port_list = entry_ports, \
+ port_list = key_ports, \
add_action = "add", \
remove_action = "remove", \
- port_type = PORT_TYPE_ANY, \
- prefix = "Index", \
+ port_type = PORT_TYPE_STRING, \
+ prefix = "Key", \
minimum_amount = 1, \
maximum_amount = 20 \
)
AddComponent(/datum/component/circuit_component_add_port, \
- port_list = key_ports, \
+ port_list = entry_ports, \
add_action = "add", \
remove_action = "remove", \
- port_type = PORT_TYPE_STRING, \
- prefix = "Key", \
+ port_type = PORT_TYPE_ANY, \
+ prefix = "Value", \
minimum_amount = 1, \
maximum_amount = 20 \
)
diff --git a/code/modules/wiremod/components/list/list_pick.dm b/code/modules/wiremod/components/list/list_pick.dm
index bce3e82688f88..a1e8141f3c34a 100644
--- a/code/modules/wiremod/components/list/list_pick.dm
+++ b/code/modules/wiremod/components/list/list_pick.dm
@@ -27,39 +27,24 @@
circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL
- var/index_type = PORT_TYPE_NUMBER
-
-/obj/item/circuit_component/list_pick/populate_options()
- list_options = add_option_port("List Type", GLOB.wiremod_basic_types)
-
/obj/item/circuit_component/list_pick/proc/make_list_port()
- input_list = add_input_port("List", PORT_TYPE_LIST(PORT_TYPE_ANY))
+ input_list = add_input_port("List", PORT_TYPE_LIST(PORT_TYPE_STRING))
/obj/item/circuit_component/list_pick/populate_ports()
input_name = add_input_port("Input Name", PORT_TYPE_STRING)
- user = add_input_port("User", PORT_TYPE_ATOM)
+ user = add_input_port("User", PORT_TYPE_USER)
make_list_port()
- output = add_output_port("Picked Item", PORT_TYPE_NUMBER)
- trigger_output = add_output_port("Triggered", PORT_TYPE_SIGNAL)
+ output = add_output_port("Picked Item", PORT_TYPE_STRING)
failure = add_output_port("On Failure", PORT_TYPE_SIGNAL)
- success = add_output_port("On Succes", PORT_TYPE_SIGNAL)
-
-
-/obj/item/circuit_component/list_pick/pre_input_received(datum/port/input/port)
- if(port == list_options)
- var/new_type = list_options.value
- input_list.set_datatype(PORT_TYPE_LIST(new_type))
- output.set_datatype(new_type)
-
+ success = add_output_port("On Success", PORT_TYPE_SIGNAL)
/obj/item/circuit_component/list_pick/input_received(datum/port/input/port)
- if(parent.Adjacent(user.value))
+ var/mob/mob_user = user.value
+ if(!ismob(mob_user) || HAS_TRAIT_FROM(parent, TRAIT_CIRCUIT_UI_OPEN, REF(mob_user)))
+ failure.set_output(COMPONENT_SIGNAL)
return
-
- if(ismob(user.value))
- trigger_output.set_output(COMPONENT_SIGNAL)
- INVOKE_ASYNC(src, PROC_REF(show_list), user.value, input_name.value, input_list.value)
+ INVOKE_ASYNC(src, PROC_REF(show_list), mob_user, input_name.value, input_list.value)
/// Show a list of options to the user using standed TGUI input list
/obj/item/circuit_component/list_pick/proc/show_list(mob/user, message, list/showed_list)
@@ -68,10 +53,17 @@
return
if(!message)
message = "circuit input"
- if(!(user.can_perform_action(src, FORBID_TELEKINESIS_REACH)))
+ if(!(user.can_perform_action(parent.shell, FORBID_TELEKINESIS_REACH|ALLOW_SILICON_REACH|ALLOW_RESTING)))
+ failure.set_output(COMPONENT_SIGNAL)
return
+ var/user_ref = REF(user)
+ ADD_TRAIT(parent, TRAIT_CIRCUIT_UI_OPEN, user_ref)
var/picked = tgui_input_list(user, message = message, items = showed_list)
- if(!(user.can_perform_action(src, FORBID_TELEKINESIS_REACH)))
+ REMOVE_TRAIT(parent, TRAIT_CIRCUIT_UI_OPEN, user_ref)
+ if(QDELETED(src))
+ return
+ if(!(user.can_perform_action(parent.shell, FORBID_TELEKINESIS_REACH|ALLOW_SILICON_REACH|ALLOW_RESTING)))
+ failure.set_output(COMPONENT_SIGNAL)
return
choose_item(picked, showed_list)
diff --git a/code/modules/wiremod/components/sensors/view_sensor.dm b/code/modules/wiremod/components/sensors/view_sensor.dm
index f65853986e315..1725044c03597 100644
--- a/code/modules/wiremod/components/sensors/view_sensor.dm
+++ b/code/modules/wiremod/components/sensors/view_sensor.dm
@@ -37,7 +37,7 @@
if(!parent.shell)
return
- if(TIMER_COOLDOWN_CHECK(parent.shell, COOLDOWN_CIRCUIT_VIEW_SENSOR))
+ if(TIMER_COOLDOWN_RUNNING(parent.shell, COOLDOWN_CIRCUIT_VIEW_SENSOR))
result.set_output(null)
cooldown.set_output(COMPONENT_SIGNAL)
return
diff --git a/code/modules/wiremod/datatypes/datum.dm b/code/modules/wiremod/datatypes/datum.dm
index f3faa8f5d8e1c..4045ee3793e66 100644
--- a/code/modules/wiremod/datatypes/datum.dm
+++ b/code/modules/wiremod/datatypes/datum.dm
@@ -4,6 +4,7 @@
datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT|DATATYPE_FLAG_ALLOW_ATOM_INPUT
can_receive_from = list(
PORT_TYPE_ATOM,
+ PORT_TYPE_USER
)
/datum/circuit_datatype/datum/convert_value(datum/port/port, value_to_convert)
diff --git a/code/modules/wiremod/datatypes/entity.dm b/code/modules/wiremod/datatypes/entity.dm
index e3a76892f97d9..437b3d7adb49c 100644
--- a/code/modules/wiremod/datatypes/entity.dm
+++ b/code/modules/wiremod/datatypes/entity.dm
@@ -2,6 +2,9 @@
datatype = PORT_TYPE_ATOM
color = "purple"
datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT|DATATYPE_FLAG_ALLOW_ATOM_INPUT
+ can_receive_from = list(
+ PORT_TYPE_USER,
+ )
/datum/circuit_datatype/entity/convert_value(datum/port/port, value_to_convert)
var/atom/object = value_to_convert
diff --git a/code/modules/wiremod/datatypes/user.dm b/code/modules/wiremod/datatypes/user.dm
new file mode 100644
index 0000000000000..693941dcd7409
--- /dev/null
+++ b/code/modules/wiremod/datatypes/user.dm
@@ -0,0 +1,9 @@
+/datum/circuit_datatype/user
+ datatype = PORT_TYPE_USER
+ color = "label"
+
+/datum/circuit_datatype/user/convert_value(datum/port/port, value_to_convert)
+ var/datum/object = value_to_convert
+ if(QDELETED(object))
+ return null
+ return object
diff --git a/code/modules/wiremod/shell/bot.dm b/code/modules/wiremod/shell/bot.dm
index 36fd6c5b36993..dfd9845bc05b1 100644
--- a/code/modules/wiremod/shell/bot.dm
+++ b/code/modules/wiremod/shell/bot.dm
@@ -31,7 +31,7 @@
var/datum/port/output/entity
/obj/item/circuit_component/bot/populate_ports()
- entity = add_output_port("User", PORT_TYPE_ATOM)
+ entity = add_output_port("User", PORT_TYPE_USER)
signal = add_output_port("Signal", PORT_TYPE_SIGNAL)
/obj/item/circuit_component/bot/register_shell(atom/movable/shell)
@@ -46,4 +46,3 @@
playsound(source, get_sfx(SFX_TERMINAL_TYPE), 25, FALSE)
entity.set_output(user)
signal.set_output(COMPONENT_SIGNAL)
-
diff --git a/code/modules/wiremod/shell/brain_computer_interface.dm b/code/modules/wiremod/shell/brain_computer_interface.dm
index 89aac209c83e4..c3ac3154d2d13 100644
--- a/code/modules/wiremod/shell/brain_computer_interface.dm
+++ b/code/modules/wiremod/shell/brain_computer_interface.dm
@@ -114,7 +114,7 @@
send_message_signal = add_input_port("Send Message", PORT_TYPE_SIGNAL)
show_charge_meter = add_input_port("Show Charge Meter", PORT_TYPE_NUMBER, trigger = PROC_REF(update_charge_action))
- user_port = add_output_port("User", PORT_TYPE_ATOM)
+ user_port = add_output_port("User", PORT_TYPE_USER)
/obj/item/circuit_component/bci_core/Destroy()
QDEL_NULL(charge_action)
diff --git a/code/modules/wiremod/shell/controller.dm b/code/modules/wiremod/shell/controller.dm
index 7040e554a0a4a..3092d3315f165 100644
--- a/code/modules/wiremod/shell/controller.dm
+++ b/code/modules/wiremod/shell/controller.dm
@@ -34,7 +34,7 @@
var/datum/port/output/entity
/obj/item/circuit_component/controller/populate_ports()
- entity = add_output_port("User", PORT_TYPE_ATOM)
+ entity = add_output_port("User", PORT_TYPE_USER)
signal = add_output_port("Signal", PORT_TYPE_SIGNAL)
alt = add_output_port("Alternate Signal", PORT_TYPE_SIGNAL)
right = add_output_port("Extra Signal", PORT_TYPE_SIGNAL)
@@ -51,6 +51,12 @@
COMSIG_CLICK_ALT,
))
+/obj/item/circuit_component/controller/proc/handle_trigger(atom/source, user, port_name, datum/port/output/port_signal)
+ source.balloon_alert(user, "clicked [port_name] button")
+ playsound(source, get_sfx(SFX_TERMINAL_TYPE), 25, FALSE)
+ entity.set_output(user)
+ port_signal.set_output(COMPONENT_SIGNAL)
+
/**
* Called when the shell item is used in hand
*/
@@ -58,10 +64,7 @@
SIGNAL_HANDLER
if(!user.Adjacent(source))
return
- source.balloon_alert(user, "clicked primary button")
- playsound(source, get_sfx(SFX_TERMINAL_TYPE), 25, FALSE)
- entity.set_output(user)
- signal.set_output(COMPONENT_SIGNAL)
+ handle_trigger(source, user, "primary", signal)
/**
* Called when the shell item is alt-clicked
@@ -70,10 +73,8 @@
SIGNAL_HANDLER
if(!user.Adjacent(source))
return
- source.balloon_alert(user, "clicked alternate button")
- playsound(source, get_sfx(SFX_TERMINAL_TYPE), 25, FALSE)
- entity.set_output(user)
- alt.set_output(COMPONENT_SIGNAL)
+ handle_trigger(source, user, "alternate", alt)
+
/**
* Called when the shell item is right-clicked in active hand
@@ -82,7 +83,4 @@
SIGNAL_HANDLER
if(!user.Adjacent(source))
return
- source.balloon_alert(user, "clicked extra button")
- playsound(source, get_sfx(SFX_TERMINAL_TYPE), 25, FALSE)
- entity.set_output(user)
- right.set_output(COMPONENT_SIGNAL)
+ handle_trigger(source, user, "extra", right)
diff --git a/code/modules/wiremod/shell/keyboard.dm b/code/modules/wiremod/shell/keyboard.dm
index 4231a6dc814d4..05b9ded074baa 100644
--- a/code/modules/wiremod/shell/keyboard.dm
+++ b/code/modules/wiremod/shell/keyboard.dm
@@ -27,7 +27,7 @@
var/datum/port/output/output
/obj/item/circuit_component/keyboard_shell/populate_ports()
- entity = add_output_port("User", PORT_TYPE_ATOM)
+ entity = add_output_port("User", PORT_TYPE_USER)
output = add_output_port("Message", PORT_TYPE_STRING)
signal = add_output_port("Signal", PORT_TYPE_SIGNAL)
@@ -53,4 +53,3 @@
output.set_output(message)
signal.set_output(COMPONENT_SIGNAL)
-
diff --git a/code/modules/wiremod/shell/module.dm b/code/modules/wiremod/shell/module.dm
index bffff02e5fbca..4201c6afb179a 100644
--- a/code/modules/wiremod/shell/module.dm
+++ b/code/modules/wiremod/shell/module.dm
@@ -123,7 +123,7 @@
toggle_suit = add_input_port("Toggle Suit", PORT_TYPE_SIGNAL)
select_module = add_input_port("Select Module", PORT_TYPE_SIGNAL)
// States
- wearer = add_output_port("Wearer", PORT_TYPE_ATOM)
+ wearer = add_output_port("Wearer", PORT_TYPE_USER)
deployed = add_output_port("Deployed", PORT_TYPE_NUMBER)
activated = add_output_port("Activated", PORT_TYPE_NUMBER)
selected_module = add_output_port("Selected Module", PORT_TYPE_STRING)
diff --git a/code/modules/wiremod/shell/moneybot.dm b/code/modules/wiremod/shell/moneybot.dm
index 46a834e2d6054..20eb596eb7267 100644
--- a/code/modules/wiremod/shell/moneybot.dm
+++ b/code/modules/wiremod/shell/moneybot.dm
@@ -95,7 +95,7 @@
/obj/item/circuit_component/money_bot/populate_ports()
total_money = add_output_port("Total Money", PORT_TYPE_NUMBER)
money_input = add_output_port("Last Input Money", PORT_TYPE_NUMBER)
- entity = add_output_port("User", PORT_TYPE_ATOM)
+ entity = add_output_port("User", PORT_TYPE_USER)
money_trigger = add_output_port("Money Input", PORT_TYPE_SIGNAL)
/obj/item/circuit_component/money_bot/register_shell(atom/movable/shell)
diff --git a/code/modules/zombie/organs.dm b/code/modules/zombie/organs.dm
index 3530146f1a2aa..43a6130c77336 100644
--- a/code/modules/zombie/organs.dm
+++ b/code/modules/zombie/organs.dm
@@ -37,10 +37,22 @@
if(timer_id)
deltimer(timer_id)
+/obj/item/organ/internal/zombie_infection/on_insert(mob/living/carbon/organ_owner, special)
+ . = ..()
+ RegisterSignal(organ_owner, COMSIG_LIVING_DEATH, PROC_REF(organ_owner_died))
+
+/obj/item/organ/internal/zombie_infection/on_remove(mob/living/carbon/organ_owner, special)
+ . = ..()
+ UnregisterSignal(organ_owner, COMSIG_LIVING_DEATH)
+
+/obj/item/organ/internal/zombie_infection/proc/organ_owner_died(mob/living/carbon/source, gibbed)
+ SIGNAL_HANDLER
+ qdel(src) // Congrats you somehow died so hard you stopped being a zombie
+
/obj/item/organ/internal/zombie_infection/on_find(mob/living/finder)
- to_chat(finder, "Inside the head is a disgusting black \
+ to_chat(finder, span_warning("Inside the head is a disgusting black \
web of pus and viscera, bound tightly around the brain like some \
- biological harness.")
+ biological harness."))
/obj/item/organ/internal/zombie_infection/process(seconds_per_tick, times_fired)
if(!owner)
diff --git a/config/iceruinblacklist.txt b/config/iceruinblacklist.txt
index 6ce22cdba3616..420a9d79a28fc 100644
--- a/config/iceruinblacklist.txt
+++ b/config/iceruinblacklist.txt
@@ -13,6 +13,7 @@
#_maps/RandomRuins/IceRuins/icemoon_underground_lavaland.dmm
##MISC
+#_maps/RandomRuins/IceRuins/icemoon_surface_gas.dmm
#_maps/RandomRuins/IceRuins/icemoon_surface_lust.dmm
#_maps/RandomRuins/IceRuins/icemoon_surface_asteroid.dmm
#_maps/RandomRuins/IceRuins/icemoon_surface_engioutpost.dmm
diff --git a/config/jobconfig.toml b/config/jobconfig.toml
index f1a9b65d594b9..c8a5c5c3dfa5d 100644
--- a/config/jobconfig.toml
+++ b/config/jobconfig.toml
@@ -46,6 +46,13 @@
"# Spawn Positions" = 1
"# Total Positions" = 1
+[BITRUNNER]
+"# Playtime Requirements" = 0
+"# Required Account Age" = 0
+"# Required Character Age" = 0
+"# Spawn Positions" = 3
+"# Total Positions" = 3
+
[BOTANIST]
"# Playtime Requirements" = 0
"# Required Account Age" = 0
diff --git a/html/changelogs/AutoChangeLog-pr-78583.yml b/html/changelogs/AutoChangeLog-pr-78583.yml
deleted file mode 100644
index d5723ae152162..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78583.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-author: "ZephyrTFA"
-delete-after: True
-changes:
- - rscadd: "Vent Pumps can now be overclocked, do some light reading in the air alarm to figure out what this means."
- - balance: "Vent Pumps now have fan integrity, the damaging of which reduces their ability to move air."
- - sound: "Overclocking sounds, including spool, stop, and loop"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78809.yml b/html/changelogs/AutoChangeLog-pr-78809.yml
deleted file mode 100644
index e2c06429f4d0f..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78809.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Iamgoofball"
-delete-after: True
-changes:
- - balance: "Allows spacemen to use age-appropriate drugs by making it so you can now huff N2O to get high."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78914.yml b/html/changelogs/AutoChangeLog-pr-78914.yml
deleted file mode 100644
index 0d9e5a3217f21..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78914.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "starrm4nn"
-delete-after: True
-changes:
- - bugfix: "makes the riot helmet hide hair like other sec helmets"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78972.yml b/html/changelogs/AutoChangeLog-pr-78972.yml
deleted file mode 100644
index ee28a7c42d525..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78972.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "carlarctg"
-delete-after: True
-changes:
- - bugfix: "Fixes Monkey's Delight recipe"
\ No newline at end of file
diff --git a/html/changelogs/archive/2023-10.yml b/html/changelogs/archive/2023-10.yml
index ff449e2e69689..1a0eb987d0c24 100644
--- a/html/changelogs/archive/2023-10.yml
+++ b/html/changelogs/archive/2023-10.yml
@@ -563,3 +563,617 @@
san7890:
- bugfix: Space Dragons can now, once again, tear down walls and eat corpses. They
also have regained their special damage modifier when attacking mechs.
+2023-10-15:
+ GPeckman:
+ - bugfix: Having all augmented limbs will make you properly spaceproof once again.
+ - bugfix: Androids are immune to crit damage again.
+ - bugfix: Surgery on robotic limbs can be canceled.
+ Iamgoofball:
+ - balance: Allows spacemen to use age-appropriate drugs by making it so you can
+ now huff N2O to get high.
+ Jacquerel:
+ - refactor: Space Dragons are now basic mobs, please report any unexpected behaviour.
+ - balance: You can now see that a space dragon is destroying a wall with a visual
+ indicator of the wall being damaged.
+ - balance: Space Dragons can pry open airlocks.
+ Melbert:
+ - qol: Leaning now has a small animation associated.
+ - qol: Cyborgs can now lean against walls.
+ - bugfix: Fixed some runtimes associated with leaning.
+ - bugfix: Fixed being able to lean while dead or in otherwise invalid states.
+ ZephyrTFA:
+ - rscadd: Vent Pumps can now be overclocked, do some light reading in the air alarm
+ to figure out what this means.
+ - balance: Vent Pumps now have fan integrity, the damaging of which reduces their
+ ability to move air.
+ - sound: Overclocking sounds, including spool, stop, and loop
+ carlarctg:
+ - qol: Bladists can now use silver *or* titanium while creating their blades
+ - bugfix: Fixes Monkey's Delight recipe
+ starrm4nn:
+ - bugfix: makes the riot helmet hide hair like other sec helmets
+2023-10-16:
+ BlueMemesauce:
+ - spellcheck: Broken canisters now have a description
+ Cruix:
+ - rscadd: Added Afro (Huge) hairstyle
+ Melbert:
+ - code_imp: Removed species death and species hitby, replaced any uses with signals.
+ SyncIt21:
+ - bugfix: plumbing setups should(hopefully) no longer grind to a halt nor will overflow
+ with excess volume of reagents.
+ - code_imp: created defines for min & max ph. Improved some reaction_reagent code.
+ Made synthesizer dispensable reagent list values static to save memory.
+ - refactor: ph balancing mechanism for reaction chamber is significantly improved.
+ Optimized it's code overall
+ - refactor: examining each individual reagent will display their results back to
+ 2 decimal places again and not 4 for easy readability.
+ Wallem:
+ - bugfix: The active sonar module won't attempt to create 6000000 new pings per
+ process cycle anymore
+2023-10-17:
+ DaydreamIQ:
+ - qol: Icebox Visitation now has a door connected to brig
+ Fazzie:
+ - rscadd: The free golem ship has been swapped for a much better one, with fishing
+ equipment.
+ - rscadd: The wizard's den now has a book with the guide to wizard. Hope that helps
+ their winrate!
+ - qol: The wizard's den no longer looks like a flying cucumber and has received
+ a major overhaul. Hooray!
+ - bugfix: Fixed the doors on the Beach away mission
+ - bugfix: Fixed the railings on the Beach away mission
+ GoldenAlpharex:
+ - code_imp: Got rid of a few more single-letter variables. Only over six thousand
+ left to go, woo!
+ - code_imp: Documented a huge part of telecommunications machinery and signal code,
+ and did some minor code improvements to said code.
+ - bugfix: Hands of cards will now properly display the last card added to the hand
+ all the time, even when there's more than five cards in that hand.
+ Higgin:
+ - bugfix: Fixes respiration-transmission advanced viruses to no longer have an always-guaranteed
+ infection chance per tick.
+ Jacquerel:
+ - bugfix: Megafauna, lavaland elites, and abstract mobs now correctly cannot be
+ spawned by a toolbox of ash drake summoning
+ LT3:
+ - bugfix: The remaining survival pod bed on Icebox is now a medical bed
+ - bugfix: Fixed font scaling for announcements
+ - bugfix: Microwave will no longer get stuck turned on if a PDA has no cell
+ - bugfix: Silicons can no longer silently change the microwave between cook and
+ charge
+ Melbert:
+ - rscdel: Deleted a mapped in wrestling belt
+ - refactor: Refactored unarmed attacking mechanisms, this means dis-coordinated
+ humans will now bite people like monkeys (like how coordinated monkeys punch
+ people like humans?)
+ - refactor: Dis-coordinated humans smashing up machines now use their hands, rather
+ than their paws
+ NamelessFairy:
+ - bugfix: TGC Mana and Health bars are correctly offset on the holodeck.
+ - bugfix: Players without bodies to return to can play CTF again.
+ RedBaronFlyer:
+ - sound: added sounds for scanning valued items with an export scanner
+ Rhials:
+ - balance: Random event frequency has been adjusted to fire events more often.
+ - code_imp: The event subsystem has been prettied up with comments and longer variable
+ names.
+ SyncIt21:
+ - code_imp: removed unnecessary calls to `update_total()`
+ Thunder12345:
+ - bugfix: Delta's cargo bay has been connected to the atmos pipe networks
+ - bugfix: Fuel tanks are explosive again
+ jlsnow301:
+ - bugfix: TGUI Say should no longer flash during initialization
+ mc-oofert:
+ - rscadd: added a new hallucination, your mother
+ vinylspiders:
+ - bugfix: ghost sheets will now have the correct flags for digi sprites
+ - bugfix: basic mobs will no longer runtime when trying to check the faction of
+ a porta turret
+ - refactor: faction checking is now done at the atom/movable level
+ - bugfix: Fixes a bug with the plasma flower core MODsuit that would cause a butterfly
+ murder scene wherever there were turrets
+2023-10-18:
+ Ghommie:
+ - bugfix: Fish analyzers can now be actually used for experiments.
+ LT3:
+ - refactor: Tram process/industrial lift refactored into transport subsystem
+ - refactor: Nanotrasen has traded in last year's tram for a new 2563 model!
+ - rscadd: Tram spoilers can be emagged to cause extra damage on collision
+ - rscadd: Tram can now be manually driven from the controller cabinet using TGUI
+ - rscadd: Tram floor tiles can be created using plastic sheets
+ - rscadd: Tram walls/floor tiles can be removed, repaired, and replaced
+ - rscadd: Tram frames (girder) can be created/placed using titanium sheets
+ - rscadd: Broken tram machinery causes crossing signals to malfunction
+ - rscadd: Ninja drain on tram controls will cause minor malfunction on other tram
+ components
+ - rscadd: Engineers can stop/start and disable the tram
+ - qol: Tram machinery now has context hints and examine text to indicate status
+ - qol: Tram remote no longer requires physically touching the tram before using
+ - code_imp: You can now construct furniture and other decorations on the tram
+ - code_imp: Tram electrocution is now a component, not a turf
+ - code_imp: Tram stats are now persistent between rounds and logged to feedback
+ - code_imp: Engineers can now end Tram Malfunction event early by repairing the
+ tram controller
+ - code_imp: Replacement tram machinery is researchable and buildable
+ - code_imp: Most tram machinery now performs auto configuration instead of hardcoded
+ map values
+ - code_imp: Tram remote can now set specific destinations
+ - code_imp: Tram actions/errors now create log entries
+ - balance: Tram base floor is no longer indestructible
+ - balance: Damaged energized tram plates have a chance to electrocute
+ - bugfix: Tram destination signs should no longer display incorrect emissive overlays
+ - bugfix: Tram crossing signals no longer turn amber before the tram starts moving
+ - image: Improved sprites for most tram components
+ - image: Removed all unused/duplicate tram icons from .dmis
+ - image: Colors are now consistent between status, tram, incident displays
+ - admin: Reset tram commands available to admins in the debug panel
+ MTandi:
+ - bugfix: Distilled drink quality is fixed - can't give a mood debuff anymore
+ Melbert:
+ - rscadd: Revenants can now use Ouija Boards
+ - rscadd: Ouija Boards now have more options to select from by default (and admins
+ can VV it to even more options)
+ - bugfix: Blind people are now worse at using Ouija Boards
+ - bugfix: You can punch yourself again
+ ninjanomnom:
+ - admin: Invisimin can now be used on mobs that are already invisible, whether through
+ temporary or permanent effects.
+ - bugfix: Monkeyize/Humanize mob transformations no longer permanently reveal invisible
+ mobs if they had effects making them invisible otherwise.
+ - bugfix: Objects with the undertile element that have been made invisible through
+ other means are no longer revealed by being uncovered.
+ vinylspiders:
+ - bugfix: fixed runtime caused by simple mobs AttackingTarget() missing an arg
+ - bugfix: being killed or ghosting while being scoped will no longer cause the cursor
+ offset to persist in a bugged state
+2023-10-19:
+ LT3:
+ - spellcheck: More announcement CSS fixes, now including light mode
+ Rhials:
+ - qol: As an observer, clicking on a bitrunning pod will let you orbit it's bitrunning
+ avatar. Cool!
+ carlarctg:
+ - qol: 'Added slapcraft recipes for: Pillow suits, pillow helmets, bone and sinew
+ tailoring/weaponry, pipeguns, ghetto jetpacks, and pneumatic cannons.'
+ - code_imp: The base type of cowboy hats no longer looks and is named like a bounty
+ hunter hat, clarifying the recipe for the heroic laser musket. They need cowboy
+ hats not bounty hats.
+ - bugfix: Fixed an issue where if a slapcraft recipe required more than one instance
+ of its 'primary' slapcrafting item, it wouldn't show the additional instance
+ when examining its recipes.
+ lizardqueenlexi:
+ - qol: Birdshot ordnance is now equipped with a second RPD and two holofan projectors.
+ - qol: Ordnance mishaps on Birdshot are significantly less likely to slam you into
+ an electrified window until you die.
+ vinylspiders:
+ - bugfix: fixes a tgui bluescreen bug with the bank account console that can occur
+ when there is bad bank account data
+ - bugfix: '"line snapped" and "rod dropped" balloon alerts will now display when
+ they are supposed to while fishing'
+2023-10-20:
+ GPeckman:
+ - bugfix: B.O.R.I.S. modules can once again be properly applied to the unformatted
+ borg created when you reset an AI shell.
+ Higgin:
+ - bugfix: automatic breathers rejoice. oxyloss now knocks people out again.
+ LT3:
+ - bugfix: Maploaded medical beds now have correct brake lights
+ LemonInTheDark:
+ - rscadd: Screen is now more grungy for halloween
+ Melbert:
+ - bugfix: Silicons don't spark when shot by disablers
+ - bugfix: Changelings who fail to catch something with a tencacle will have throw
+ mode disabled automatically
+ - bugfix: Fixes occasions where you can reflect with Sleeping Carp when you shouldn't
+ be able to
+ - bugfix: Fixes some projectiles causing like 20x less eye blur than they should
+ be
+ - refactor: Refactored bullet-mob interactions
+ - refactor: Nightmare "shadow dodge" projectile ability is now sourced from their
+ brain
+ - bugfix: Magic Mirrors can change your race again (?)
+ - bugfix: Magic Mirrors properly prevent you from being soft locked
+ - bugfix: Robo customers are as robust as before
+ - bugfix: Spasms won't trigger in stasis, incapacitated, if your hands are blocked,
+ or you are immobilized.
+ Rhials:
+ - qol: Ghosts will now be prompted to orbit when someone loses control due to being
+ blackout drunk.
+ - qol: Ghosts will now be prompted to orbit when a cultist begins inscribing a Nar'Sie
+ rune.
+ Yttriums:
+ - balance: reduces cellulose fibers required for advanced regenerative mesh creation
+ from 20u to 10u
+ carlarctg:
+ - code_imp: Adds 'Bloody Spreader' component that bloodies everything it touches!
+ - code_imp: For example inserting an item into it if it is a storage item. Or entering
+ it if it's a turf, or bumping onto it, or... you get the point, hopefully.
+ - rscadd: Added this component to the MEAT backpack, meat slabs, bouncy castles,
+ meateor fluff, meateor heart, and the heretic sanguine blade.
+ - rscadd: Gave most of these the blood walk component as well, which spreads blood
+ if it's dragged around.
+ - rscadd: Meat slabs contain a limited amount of both components, eventually they
+ will 'dry out'.
+ - code_imp: Added a signal for when an item is entered into storage.
+ lizardqueenlexi:
+ - bugfix: Heretic summons should now display the correct name when polling ghosts
+ to play as them.
+ san7890:
+ - bugfix: People should be crawling into welded vents a lot less now.
+ timothymtorres:
+ - bugfix: Airtank suicides will now drop items and organs again.
+ - bugfix: Fix husks fire decay rate to be slower. Pyre chaplains can now use husked
+ bodies (only caused by burns) to complete their burning sacrifice rite.
+ vinylspiders:
+ - bugfix: Fixes a bug where your mother would delete your species after calling
+ you a disappointment, rendering you a broken husk of a mob
+2023-10-21:
+ Ben10Omintrix:
+ - rscadd: added a new syndicate item - the bee smoker
+ GPeckman:
+ - qol: Nonhuman autopsy, Tier Two Lasers, and several other experiments can now
+ be completed earlier.
+ - balance: Advanced robotics techweb node no longer requires neural programming
+ node.
+ - rscdel: Protolathe/circuit imprinter/techfab designs costing reagents is now totally
+ deprecated.
+ Ical92:
+ - bugfix: connected Meta's Cytology equipment properly
+ Jacquerel:
+ - bugfix: Blobbernauts will once again take damage when not on blob tiles.
+ LT3, san7890:
+ - rscadd: Announcements have gotten a fresh coat of paint! They should be popping
+ with splendid new colors and should have a lot less ugly linebreaks, while still
+ managing to keep your attention at the screen.
+ OrionTheFox:
+ - bugfix: '[Tramstation] fixed an unlinked Disposals Bin in the Pod Bay'
+ Paxilmaniac:
+ - code_imp: Bitrunner domains can now have spells or items from disks disabled if
+ the domain maker wants such a thing
+ Pickle-Coding:
+ - bugfix: Fixed tesla coil zaps cutting off too early.
+ SyncIt21:
+ - bugfix: plumbing pill press & bottler won't stop when processing 50 unit bottles
+ - code_imp: 'made a lot of variables defines and lists static to save memory for
+ plumbing pill press. Moved global lists to it''s rightful
+
+ place'
+ - code_imp: copied over chem master pill & patch designs over to plumbing pill press
+ and removed the old designs. resized UI
+ - bugfix: RCD & RTD ui updates when switching between root categories
+ Watermelon914:
+ - admin: Added SS13.get_runner_ckey() and SS13.get_runner_client() which stores
+ the ckey and returns the client of the user who ran the lua script. Can be unreliable
+ if accessed after sleeping.
+ - admin: Added timer loop helpers to the SS13.lua module, check the docs
+ - admin: The SS13.lua module can now be made local without causing any errors.
+ lizardqueenlexi:
+ - refactor: Artificer constructs have been converted to the basic mob framework.
+ This should change very little about them, but please report any bugs. NPC artificers
+ are now smarter, and will focus on healing nearby wounded constructs - if you
+ see them, take them out first!
+ mc-oofert:
+ - bugfix: making assembly activated bombs works again
+ nikothedude:
+ - bugfix: Scream for me, the spell, now works
+ - bugfix: Non-random puncture wounds can now be applied
+ ninjanomnom:
+ - balance: It damages your eyes to look at the supermatter singularity
+ san7890:
+ - refactor: Holodeck monkeys have been moved to the same system as old monkeys,
+ and should retain the similar "ephermeal" behavior, while being a whole lot
+ smarter by leveraging new AI. Please report anything that is completely wack
+ about this.
+ - balance: Slimes can't eat holodeck monkeys anymore, because apparently they could
+ and that is wack.
+ timothymtorres:
+ - rscadd: Add new fitness skill and mechanics for weight machines and punching bags. Working
+ out with a proper diet and good sleep will result in massive fitness gains. As
+ your fitness increases, so does your mass.
+2023-10-22:
+ Ben10Omintrix:
+ - bugfix: fixes not being able to walk over or pull mook corpses
+ BlueMemesauce:
+ - balance: Export scanner no longer shows value of shipping manifests, now you actually
+ have to read them.
+ - balance: Shipping manifest penalty is now only half crate cost as well as capped
+ to 500 credits.
+ - balance: Shipping manifests for private orders or locked crates can no longer
+ have the incorrect contents error. Shipping manifests for departmental orders
+ can n longer have any error.
+ FlufflesTheDog:
+ - bugfix: Kisses and emitters no longer make the SM crystal scream so much.
+ Ghommie:
+ - bugfix: Cooked meat no longer spreads blood around as if it weren't cooked.
+ Ical92:
+ - rscadd: Demonic-Frost Miner's ruin gets an aesthetic refresh
+ Jacquerel:
+ - bugfix: Meatwheat Clumps, Bungo Pits, and Eggplant Eggs should once again inherit
+ reagent purity from the grown item which produces them.
+ - bugfix: You should be revived properly when entering the mansus realm following
+ a heretic sacrifice
+ - bugfix: The buff which is supposed to heal you in the mansus realm will now do
+ that instead of unavoidably damaging you
+ - balance: The mansus realm's healing buff heals for 25% as much as it did before
+ it was broken
+ JohnFulpWillard, sprites by CoiledLamb:
+ - rscadd: You can now play Mafia on your PDA.
+ - balance: Mafia changelings can now only talk to eachother during the night.
+ - bugfix: Mafia abilities can't be repeatedly used on people.
+ Jolly:
+ - image: Colored labcoats have been GAGSed! Please report any weird oddities on
+ Github.
+ - bugfix: The coroners lab coat is no longer offset by one pixel.
+ LT3:
+ - bugfix: Fixed tram cabinet LMB/RMB actions being reversed
+ - bugfix: Tram cabinet can now read IDs inside PDAs and wallets
+ - bugfix: Crossing signals now correctly indicate broken/no power
+ - bugfix: Trying to repair tram (weld) without welding fuel fails
+ - bugfix: You can actually unbolt the tram controller from the wall
+ - qol: Tram spoilers now have visual and examine hints about being malfunctioning/emagged
+ - qol: Improved some tram error messages
+ - image: Added Halloween themed floor/tram tiles
+ LemonInTheDark:
+ - rscadd: Starlight should be a bit more intense, and flow better onto non space
+ tiles
+ MTandi:
+ - qol: changed wording of a popup in the admin dressing menu
+ - rscadd: Gygax type mechs now have an option to disable overclock when overheated.
+ - bugfix: Fixed overclocking having no effect on Ripley.
+ Melbert:
+ - rscadd: Adds Food Allergies as a -2 quirk. You can select which food you're allergic
+ to or rock a random option.
+ SyncIt21:
+ - bugfix: cryo and stuff that transfers reagents with a multiplier should transfer
+ correct volumes as expected.
+ Treach:
+ - bugfix: Skeleton Keys now fit in the Explorer's Webbing.
+ ZephyrTFA:
+ - qol: signalers now tell you their cooldown and also use balloon alerts
+ jlsnow301:
+ - rscadd: Adds a subtle ghost poll. This pings in dead chat and gives a screen alert,
+ but no TGUI popup. Orbit the point of interest to be selected for the role.
+ - refactor: A number of ghost spawns now feature this alert. Write an issue report
+ if anything breaks.
+ lizardqueenlexi:
+ - refactor: Maintenance Drones now use the basic mob framework. This shouldn't come
+ with any noticeable gameplay changes, but please report any bugs.
+ - bugfix: Drones can now interact normally with electrified doors.
+ - bugfix: Drones' built-in tools can no longer be placed in storage objects and/or
+ thrown on the floor.
+ - bugfix: Drones can now perform right-click interactions correctly, such as deconstructing
+ reinforced windows.
+ - bugfix: Drones can now reboot or cannibalize other drones without being in combat
+ mode.
+ ninjanomnom:
+ - rscadd: New dream that plays sound at you
+ timothymtorres:
+ - bugfix: Fix holodeck items from being eaten, crafted, recycled, juiced, or grinded.
+ - bugfix: Fix cqc kicks to only cause staminaloss when target is on the floor
+ vinylspiders:
+ - bugfix: admin triggering the Revenant event now works again
+ xXPawnStarrXx:
+ - rscadd: Most pies can now be sliced rather than being consumed whole.
+ - rscadd: Rootdough can be crafted using soy milk in place of eggs.
+ - rscadd: Two new lizard-safe rootbread sandwiches can be crafted.
+2023-10-23:
+ SyncIt21:
+ - bugfix: items that require reagent containers & the reagents inside it for crafting(e.g.
+ molotov) now crafts properly.
+ - bugfix: most crafting recipes should work now
+ jlsnow301:
+ - rscadd: Just in time for Halloween- ghost notifications have been upgraded to
+ their own announcements. Boo!
+ san7890:
+ - qol: Adminwho messages are now in an examine block for heightened clarity.
+2023-10-24:
+ DrDiasyl:
+ - qol: The Command intercom now has a High-Volume setting like command headsets
+ - qol: A memo telling frequencies of station radio channels are now present near
+ the Command intercom and T-Comms room
+ - image: Station, Command, and Prison intercoms have received new sprites
+ - spellcheck: Station and Command intercom descriptions have been changed to tell
+ about their functionality
+ Fikou:
+ - bugfix: surgical trays no longer animate when opened
+ Ghommie:
+ - bugfix: The polymorphic belt shouldn't work on animated objects. Logically wouldn't
+ have DNA.
+ Jacquerel:
+ - bugfix: Food created by mixing chemicals once again has a reagent purity based
+ on the component chemicals.
+ SyncIt21:
+ - bugfix: plastic sheet produces 4 tiles via the tiles option without using the
+ crafting menu
+ cnleth:
+ - bugfix: Replaced error spaghetti in thunderdome kitchen with regular cooked spaghetti
+ exdal:
+ - bugfix: hallucination announcements use new announcement style
+ jlsnow301:
+ - bugfix: Entering a virtual domain should no longer give you a message that it
+ doesn't forbid items
+2023-10-25:
+ CoiledLamb:
+ - image: resprites air alarms
+ DrDiasyl:
+ - rscadd: New automatic weapon for the crew - Disabler SMG. Capable of rapidly firing
+ weak disabler beams.
+ - image: Muzzle flashes got a new sprite, each direction included!
+ - image: Temperature Gun "BAKE" beams are now lava colored
+ Hatterhat:
+ - qol: Universal scanners are now capable of recognizing the account owners and
+ assigned profit splits on barcodes. Cargo technicians are asked to do their
+ due diligence when matters call for it.
+ - rscadd: Anomaly-locked MODsuit modules can now be varedited to have unremovable
+ cores, or can be spawned with this functionality by using the /prebuilt/locked
+ subtype.
+ Jacquerel:
+ - bugfix: You will no longer be asked to construct meteor shields on stations which
+ cannot be hit by meteors.
+ Melbert:
+ - refactor: Refactored zombies to use the regenerator component. Now they'll have
+ a slight glow/animation when the regeneration actually kicks in.
+ - qol: Monkey cubes have a slight animation associated now.
+ - bugfix: Cooking Deserts 101 grants all intended recipes
+ OrionTheFox:
+ - qol: Railings now have Examine hints for how to deconstruct them
+ - bugfix: '[Tramstation] fixed a missing Scrubber in the Civilian Radiation Shelter'
+ Rhials:
+ - bugfix: The Infected Domain should no longer fill up with smelly, poisonous gas
+ over time.
+ TheBoondock:
+ - qol: improves blackout drunk character gameplay
+ - bugfix: fixed improper prob() placement that caused blackout character to be forced
+ sleep
+ - sound: added hiccup sound
+ jlsnow301:
+ - bugfix: Ghost alerts have been tuned down a bit.
+ lizardqueenlexi:
+ - refactor: Hostile skeleton NPCs now use the basic mob framework. They're a little
+ smarter, and they also have a slightly improved set of attack effects and sounds.
+ They love to drink milk, but will be harmed greatly if any heartless spaceman
+ tricks them into drinking soymilk instead. Please report any bugs.
+ - refactor: Hostile Nanotrasen mobs now use the basic mob framework. This should
+ make them a little smarter and more dangerous. Please report any bugs.
+ - bugfix: Russian mobs will now actually use those knives they're holding.
+ - refactor: Juggernaut constructs now use the basic mob framework. Please report
+ any bugs.
+ mc-oofert:
+ - bugfix: you may not enter knock path caretakers last refuge with the nuke disk
+ - bugfix: you can no longer cuff knock heretics in refuge
+ necromanceranne:
+ - bugfix: The nanites inside of thermal pistols are once again angry, and aggressively
+ want to burn/puncture people.
+2023-10-26:
+ Cruix:
+ - bugfix: fixed hair gradients not applying correctly to huge afros
+ - bugfix: fixed hair gradients not applying properly on dismembered heads.
+ Xackii:
+ - bugfix: Fixed gorilla attack cooldown. Now attacking speed to mobs is the same
+ as attacking speed to objects.
+ carlarctg:
+ - refactor: Adds charges to omens and omen smiting rather than only being permanent
+ or one-use. Mirrors now grant seven bad luckers.
+ - qol: Reduces omen bad luck if nobody's nearby to witness the funny. (Ghosts are
+ included in the check!)
+ - bugfix: Fixed an issue where a monkey check in doorcrushing was never actually
+ able to pass. Also they screech now.
+ timothymtorres:
+ - qol: Add smoke particles to burning fireplace
+2023-10-27:
+ FlufflesTheDog:
+ - bugfix: Wigs now properly follow your head when you're any non-standard height
+ Hatterhat:
+ - qol: Examining an ammo box (incl. magazines) now tells you the top loaded round,
+ so if you have different ammo types in different magazines, you can at least
+ try to figure out which one is which.
+ - spellcheck: Ammo boxes (incl. magazines) can now be set to use different phrasing
+ for their ammunition (e.g. cartridges, shells, etc. instead of just mixing "rounds"
+ and "shells").
+ Jacquerel:
+ - bugfix: Dying when using (most) shapeshift spells will now kill you rather than
+ having you pop out of the corpse of your previous form.
+ - bugfix: Damage will now be accurately carried between forms rather than being
+ slightly reduced upon each transformation.
+ Watermelon914:
+ - rscadd: Reworked the colour schemes for the minor and major announcements as well
+ as their layout
+ - rscdel: Rolled back changes to deadchat notifications
+ - admin: Admins can now select the colour of their announcements as well as the
+ subheader when creating a command report.
+ mc-oofert:
+ - bugfix: venus human traps no longer die when on weeds
+2023-10-28:
+ GPeckman:
+ - bugfix: The health bar on the mech diagnostic hud display should update consistently
+ now.
+ Jacquerel:
+ - bugfix: If a mob you are shapeshifted into attempts to grow up into a different
+ kind of mob then you will stop being shapeshifted
+ Melbert:
+ - bugfix: Disablers and Lasers now show their on-impact effects on hit mobs again.
+ - bugfix: People held at gunpoint can now flinch when being hit.
+ - bugfix: Regenerating mobs no longer stop regenerating no matter hit with what.
+ - bugfix: Pressure damage is now properly modified by a mob's brute damage modifier.
+ - bugfix: Fixes some occasions which some effects (glass jaw, explodable worn items)
+ won't respond to hits.
+ - refactor: Refactored core code related to mobs sustaining damage.
+ Xackii:
+ - balance: Sutures now heal a percentage of basic/animal max health instead of a
+ flat amount.
+ lizardqueenlexi:
+ - bugfix: Mobs without the "advanced tool user" trait - such as monkeys - are no
+ longer able to interact with camera consoles.
+ - bugfix: Monkeys can now properly attack parrots.
+ - bugfix: The Demonic Frost-Miner will no longer run around destroying the corpses
+ in its arena the moment the round begins.
+ unit0016:
+ - bugfix: Every misaligned railing ending has been corrected by the Nanotrasen Hall
+ Monitor's Lunchclub.
+2023-10-29:
+ DrDiasyl:
+ - sound: Dying with a SecHailer on your face will make a unique death sound
+ Ghommie:
+ - rscadd: Added a few fish related bounties.
+ - rscadd: Fish cases to store and preserve life fish within can be now printed from
+ the service techfab and the autolathe.
+ GoldenAlpharex:
+ - code_imp: Added support to the wet_floor component to make it so the wet overlay
+ could not be applied to certain turfs if desired.
+ - bugfix: Ice turfs no longer look tiled, and instead look smooth when placed next
+ to one-another.
+ Melbert:
+ - bugfix: Fixes being unable to open airlocks with telekinesis
+ Paxilmaniac:
+ - code_imp: the deployable component has been tweaked and improved with some new
+ options to it
+ Profakos:
+ - bugfix: Gnomes no longer runtime if they explode while sinking into the ground
+ necromanceranne:
+ - bugfix: Every person on the station now no longer has the Tranquility Evades the
+ Shield Pinky Finger Shovegrab unarmed combat technique, an ancient and forbidden
+ strike that allows anyone (literally anyone) to bypass all forms of blocking
+ defense by simply not being in combat mode when they shove or grab their target.
+ As a direct result, the chakra energy of the Spinward Sector has become severely
+ misaligned. Oh well.
+2023-10-30:
+ GPeckman:
+ - bugfix: Light-Eaten objects can no longer emit light after being turned off and
+ then back on.
+ - code_imp: Flashlights now use light_on instead of defining their own variable.
+ Please report buggy behavior.
+ - bugfix: Removed rare hard delete involving telecomms machines.
+2023-10-31:
+ CRITAWAKETS:
+ - rscadd: Corn oil is now produced from corn instead of regular vegetable oil.
+ - balance: Glycerol now uses corn oil instead of vegetable oil in it's recipe.
+ - bugfix: Grinding corn now produces oil when it previously only made cornmeal despite
+ having oil in it's reagents.
+ Mothblocks:
+ - spellcheck: Changed candy description to read better
+ Toastgoats:
+ - rscadd: The Ethereal Vintner's Union has been "convinced" to trade their signature
+ Lanternfruit with Nanotrasen!
+ - image: Sprites for the aforementioned fruit.
+ jlsnow301:
+ - bugfix: Fixed an invisibility exploit on large mobs. Probably better this way
+ lizardqueenlexi:
+ - refactor: Wraith constructs have been converted to the basic mob framework. NPC
+ wraiths are now extra cruel and will attack the lowest-health mob they can see
+ at any given time. Make sure this isn't you! Please report any bugs.
+ - bugfix: Artificers and juggernauts no longer attack significantly more slowly
+ than intended.
+ - refactor: Pirate NPCs now use the basic mob framework. They'll be a little smarter
+ in combat, and if you're wearing your ID they'll siphon your bank account with
+ every melee attack! Beware! Please report any bugs.
+ mc-oofert:
+ - rscadd: living floor, living flesh, and other stuff for the bioresearch outpost
+ ruin
+ - rscadd: bioresearch outpost ruin
+ - bugfix: you may not defeat indestructible grilles and windows with mere tools
+ san7890:
+ - bugfix: Bitrunners can no longer get mass-mindswapped out of their avatar when
+ the wizard does the event. Something about machinery and magic not going well
+ together.
diff --git a/html/changelogs/archive/2023-11.yml b/html/changelogs/archive/2023-11.yml
new file mode 100644
index 0000000000000..32f6ef3c98b41
--- /dev/null
+++ b/html/changelogs/archive/2023-11.yml
@@ -0,0 +1,331 @@
+2023-11-01:
+ Data_:
+ - bugfix: The detective's curtains can be closed again in Birdshot.
+ EEASAS:
+ - rscadd: Added a new ruin to Ice Box Station, Lizard Gas Station
+ GPeckman:
+ - refactor: The wings you get from a flight potion (if any) are now determined by
+ your chest bodypart, not your species.
+ - qol: Functional wings can now be ground up to get the flight potion back, if you
+ want to get a different wing variant.
+ - bugfix: Practice laser carbines can no longer be used to rapidly fire regular
+ laser guns.
+ - bugfix: The fire visual on mobs should no longer persist after the fire has been
+ extinguished.
+ Iajret:
+ - code_imp: mod reskins now properly shows their icon when skins loaded from different
+ .dmi
+ Jackraxxus:
+ - bugfix: Obsessed's moodlets (Both positive and negative) go away when the trauma
+ is cured or the antag status is removed.
+ Jacquerel:
+ - bugfix: Non-human mobs can hallucinate their mothers without causing a runtime
+ error
+ LT3:
+ - code_imp: Changing security levels will only trigger the nightshift subsystem
+ if lighting changes are required
+ Melbert:
+ - qol: Clicking on an adjacent dense object (or mob) with Spray Cleaner will now
+ spritz it rather than doing nothing. This means you can use Spray Cleaner to
+ clean bloodied windows, as the janitor gods intended. It also means you can
+ fill a spray bottle with Napalm, I guess.
+ - refactor: Any cleaning object can now clean a microwave.
+ Pickle-Coding:
+ - bugfix: Fixes tesla zaps being weird.
+ - admin: Logs explosions from explosive zaps.
+ Profakos:
+ - refactor: Traders are basic mobs now. Please alert us of any strange behaviours!
+ - code_imp: If there is only one option, radial lists will autopick it. This behaviour
+ can be turned off via a new argument.
+ SethLafuente:
+ - rscadd: Added Abnormal Eggs
+ - rscadd: Added Two new spiders
+ - rscdel: Some Tarantula abilities
+ - balance: Spiders speed are now connected to health
+ - balance: Spiders now take more brute damage
+ - balance: All egg laying now has a cooldown
+ SyncIt21:
+ - bugfix: Plumbing IV Drip has full control over its injection/draining rate. No
+ longer displays the message controlled by plumbing system
+ - bugfix: Plumbing IV Drip can be plunged by plunger again
+ - bugfix: Cargo will remove/cancel orders from its cart if that order exceeds the
+ available budget (both private or cargo) and the player cannot cancel this order
+ manually. All order costs are rounded up to integer values
+ - bugfix: Galactic material market will deny appending stacks to your existing order
+ if it exceeds the available (private or cargo depending on the mode of ordering)
+ budget & if it exceeds the available materials on the market. Galactic material
+ market UI is overall improved.
+ Xackii:
+ - bugfix: chameleon projector now can copy icon for storage items(backpacks, box,
+ holsters etc) using right-click on it.
+ ZephyrTFA:
+ - bugfix: signals in circuits now actually function
+ deathrobotpunch1:
+ - rscdel: Removed the biometric scanning toggle from the PENLITE holobarrier
+ jlsnow301:
+ - bugfix: Paraplegics can now enter netpods.
+ - bugfix: Fixes an exploit caused by teleporting out of a netpod.
+ - bugfix: Outfit selection at netpods shouldn't give armor bonuses any longer.
+ - bugfix: Bluespace launchpads no longer work on shuttles
+ mc-oofert:
+ - rscadd: ctrlclicking the knock path eldritch id card will toggle whether it creates
+ inverted portals or not
+ moocowswag:
+ - bugfix: Modular shield generator modules no longer lose linkage when riding a
+ shuttle
+ - bugfix: Modular shield generators now gracefully turn off when being moved by
+ a shuttle rather than leaving their projections behind, the generator's description
+ was updated to advertise this behavior.
+ necromanceranne:
+ - bugfix: Lethal ballistic pellet-based shotgun shells no longer instantly delete.
+ - balance: Operatives can once again read about the basics of CQC at a reasonable
+ price of 14 TC.
+ - qol: All Syndicate MODsuits come with the potent ability to wear hats on their
+ helmets FOR FREE. No longer does any operative need be shamed by their bald
+ helmet's unhatted state when they spot the captain, in their MODsuit, wearing
+ a hat on their helmet. The embarrassment has resulted in more than a few operatives
+ prematurely detonating their implants! BUT NO LONGER! FASHION IS YOURS!
+ - qol: There is now a Core Gear Box, containing a few essential pieces of gear for
+ success as an operative. This is right at the top of the uplink, you can't miss
+ it! Great for those operatives just starting out, or operatives who need all
+ their baseline equipment NOW.
+ - qol: To avoid poor magazine discipline, most combat-ready personnel have instructed
+ _not_ to put magazines into the gun loops on their armor vests.
+ rageguy505:
+ - rscdel: Removed extra consoles from birdshot's bitrunners
+ starrm4nn:
+ - bugfix: The Syndicate Revolver now has a Syndicate Firing Pin on the Nuke Ops
+ uplink.
+ timothymtorres:
+ - rscdel: Remove duplicate pipe on pirate ship
+ - rscdel: Remove duplicate power computer on oldstation ruin
+ - rscdel: Remove duplicate light in battlecruiser starfury
+ - rscdel: Remove duplicate space heater from snowcabin ruin
+ zxaber:
+ - balance: Malf Ability "Robotics Factory" can now be purchased multiple times.
+2023-11-02:
+ ArcaneMusic:
+ - qol: Steam vents in maintenance now have tooltips.
+ DrDiasyl:
+ - qol: There is now a more convenient way to prompt surrender to emote - the Security
+ Pen. Each officer is equipped with one in their PDA. Simply click someone with
+ it to prompt a 30-second surrender. They are printable at Security Techfab as
+ well.
+ - qol: Penlights now are printable at Medical Techfab.
+ - image: Penlights got a new cleaner sprite to replace its ancient one
+ - code_imp: The code for Penlight's holographic sign has been improved.
+ OrionTheFox:
+ - qol: The RPD now accepts upgrade disks inserted by hand, as well as their original
+ method of hitting the disk with the RPD
+ SyncIt21:
+ - bugfix: plumbing reaction chamber will attempt to balance the ph of the solution
+ a maximum of 5 times before giving up and thus preventing infinite loops, high
+ tick usage
+ - bugfix: plumbing bottler now only deals with bottles, beakers similar stuff but
+ not pills, patches, syringes or anything than can hold reagents. That & slime
+ extracts, slime industrial extract & shotgun shell
+ kawoppi:
+ - spellcheck: basic mobs getting shoved by humans now display the mob's disarm response
+ lizardqueenlexi:
+ - refactor: Proteon constructs now use the basic mob framework. The ones encountered
+ in ruins are a bit flightier now, and will briefly flee combat if attacked -
+ only so that they can return and menace you again soon after. Please report
+ any bugs.
+ timothymtorres:
+ - rscdel: Removed duplicate northstar message monitor computer
+ - rscdel: Remove duplicate syndi turret on waystation ruin
+ - rscdel: Remove duplicate HoP shutter from birdshot
+ - rscdel: Remove duplicate atmos fire alarm, cargo air alarm, and chemistry shutter
+ from icebox.
+ - rscdel: Remove duplicate machinery from russian derelict
+2023-11-03:
+ Melbert:
+ - balance: Changelings gutted by Megafauna now take 8x as long to finalize revival
+ stasis (~5 minutes).
+ Mickyan:
+ - spellcheck: The Mothic Rations Chart poster description now mentions Sparkweed
+ Cigarettes rather than Windgrass
+ Profakos:
+ - code_imp: creatures in cowboy boots will retaliate properly
+ Xackii:
+ - bugfix: Gorilla and dexterity holoparasite can now take things out of the backpack
+ while holding it in his hand.
+ jlsnow301:
+ - bugfix: Bubblegum should no longer teleport out of the simulation when threatened
+ - rscdel: Chamber of Echoes map removed as it conflicts with the actual Legion
+ - rscadd: Added some clarity to the range of netpods (4 tiles) in their exam text.
+ - bugfix: Heretics won't lose their living heart while bitrunning anymore.
+ - bugfix: You can no longer eavesdrop on nearby borgs' radio comms if they're using
+ encryption keys
+ - bugfix: Possessed blades can attempt to channel spirits again
+ larentoun:
+ - rscadd: Emote Panel TGUI added in IC category.
+ lizardqueenlexi:
+ - bugfix: Basic mobs will no longer randomly walk into terrain that harms or kills
+ them.
+ mc-oofert:
+ - bugfix: venus human traps heal in kudzu again
+ moocowswag:
+ - bugfix: Space carp that arrive via rifts are no longer stricken with rift travel
+ related sickness that causes them to become weaker.
+ ninjanomnom:
+ - bugfix: Fixes turrets being invisible when they shouldn't be
+ timothymtorres:
+ - bugfix: Fix fireplace smoke particles to work properly with all directions
+2023-11-04:
+ Jacquerel:
+ - balance: Gorillas, Seedlings, Gold Grubs, Mooks, Constructs, Ascended Knock Heretics,
+ Fugu and mobs subject to a Fugu Gland now rip up walls in a slightly slower
+ but more cinematic way.
+ Melbert:
+ - admin: Admins without `R_POLL` no longer have access to "Server Poll Management",
+ not that they could have used it anyways.
+ Tattle:
+ - bugfix: you should now be able to scrub through the library without lagging the
+ server
+ Zergspower:
+ - bugfix: Bounties - Virus bounties work once more
+ jlsnow301:
+ - bugfix: Added feedback for both extractor and extractee while using fulton extraction
+ packs.
+ - qol: Extraction packs now have better exam text.
+ - bugfix: Fixes midround selection for observers
+ - rscadd: Added a new fishing map to bitrunning.
+ - rscadd: You are no longer limited to pina coladas on the beach bar domain. Cheers!
+ lizardqueenlexi:
+ - refactor: Shades now use the basic mob framework. Please report any bugs.
+ nikothedude:
+ - qol: Character preferences now have descriptions as tooltips - hover over their
+ names to see them
+ rageguy505:
+ - rscdel: Removed a extra coffee pot in birdshot's security breakroom
+ san7890:
+ - refactor: The way mobs get specialized actions (like revenants shocking lights
+ or regal rats summoning rats to their side when you slap them) have been modified,
+ please report any bugs.
+ timothymtorres:
+ - rscdel: 'Remove duplicate machinery from Metastation: morgue disposal bin, medical
+ break room air alarm, and IV drip in permabrig medroom'
+ vinylspiders:
+ - bugfix: Softspoken quirk will no longer be applied to sign language
+2023-11-05:
+ D4C-420:
+ - bugfix: lab coats no longer have directional sprites when thrown
+ Jacquerel:
+ - bugfix: Fugu can correctly destroy walls when they get big.
+ Profakos:
+ - bugfix: Basic mobs using JPS can move again
+ Tattle:
+ - bugfix: paintings can once again be filtered
+ lessthnthree:
+ - admin: Holobarrier creation now adds player fingerprint data
+ san7890:
+ - bugfix: The HFR will not print out a piece of paper every time you multitool it,
+ saving any desired energy to use for more useful processes.
+ timothymtorres:
+ - rscdel: Remove duplicate lighting from beach bar domain
+ timothymtorres, Rahlzel:
+ - sound: Port salute emote sound from Colonial Marines SS13 attributed to Rahlzel
+2023-11-06:
+ JohnFulpWillard:
+ - bugfix: Mafia games can now start properly.
+ LT3:
+ - qol: Holobarriers will match the spray paint colour of their projector
+ - code_imp: Emergency shuttle announcements no longer use hardcoded values
+ - code_imp: Central Command announcements now correctly use its new name when changed
+ - spellcheck: Consistency pass on event announcements
+ Rhials:
+ - code_imp: The notify_ghosts proc has been cleaned up. Please report any abnormal
+ changes in deadchat notification behavior.
+ - qol: The on-screen deadchat popups now contain the notification blurb when hovered
+ with your mouse again.
+ SyncIt21:
+ - bugfix: reagent volumes should be consistent & non breaking across plumbing &
+ chemistry as a whole
+ - bugfix: plumbing reaction chambers are more proactive. Will attempt to take in
+ reagents more frequently
+2023-11-07:
+ ArcaneMusic:
+ - qol: Light switches have tooltips, and may now be deconstructed with right click
+ using a screwdriver.
+ Deadgebert:
+ - bugfix: walls built next to firelocks no longer hold onto their alarms
+ FlufflesTheDog:
+ - balance: paraplegic is no longer exclusive with spacer or settler or spacer. Broken
+ legs don't discriminate!
+ Ghommie:
+ - image: Several holidays now have themed floor and tram tiling.
+ Higgin:
+ - bugfix: fixes synthflesh not dealing and in fact healing toxin damage.
+ Jacquerel:
+ - bugfix: The plasma river is about as deadly for animals as it is for humans.
+ - bugfix: Golems can now wade in the plasma river unscathed.
+ - bugfix: Undismemberable limbs will no longer be dismembered by the plasma river.
+ - balance: Golems and plasmamen cannot become husked.
+ - image: Robotic and Skeletal parts will remain distinct while the rest of the body
+ is husked.
+ LT3:
+ - bugfix: Maximum smoothing turf groups now includes cliffs
+ - bugfix: Tramstation floor tiles will correctly get custom station colors when
+ they exist
+ Melbert:
+ - rscadd: Fishers can now try their luck at fishing out of hydroponics basins.
+ MoffNyan:
+ - rscadd: Maltroach bar sign!
+ NeonNik2245:
+ - qol: Make notepad available for everyone, who has only laptop or console.
+ Watermelon914:
+ - rscadd: Added a user type to integrated circuits
+ cnleth:
+ - bugfix: Reagent lookup in chem dispensers now shows correct reagent metabolization
+ rates
+ lizardqueenlexi:
+ - bugfix: You will now only become blackout drunk if you've actually been drinking.
+ - bugfix: Observers should stop being notified that a nameless entity is blacking
+ out.
+ san7890:
+ - bugfix: Both magic mirrors and regular mirrors are far better at respecting the
+ choice of the beard you wish to wear (within reason, of course).
+ spockye:
+ - bugfix: fixed a couple missing and misplaced disposals pipes on metastation
+ timothymtorres:
+ - qol: Improve the emote help verb to be more user friendly
+ - bugfix: Fix light eater affecting lava, space, openspace, and transparent turfs
+ vinylspiders:
+ - bugfix: tails will no longer keep wagging even in death
+ - code_imp: added some trailing commas in lists that were missing them, fixed a
+ typo in comments
+ vvvv-vvvv:
+ - bugfix: Fix refresh button in log viewer
+2023-11-08:
+ Ben10Omintrix:
+ - balance: sets the leaper move and pull forces to strong
+ Bumtickley00:
+ - spellcheck: You no longer hear weaponelding when deconstructing a closet.
+ D4C-420:
+ - rscadd: Being sufficiently drunk now has a chance to cause working out to fail
+ and harm you
+ Isratosh:
+ - bugfix: Nuclear operative induction implants now work correctly on antagonists
+ and fail on non-antagonists
+ jlsnow301:
+ - bugfix: The screen alert should no longer break ghost UI when it's huge
+ lizardqueenlexi:
+ - config: The bitrunner job now has a default config for server owners.
+ necromanceranne:
+ - balance: Harnessing Shoreline Quay (bluespace energy, probably), a mystical energy
+ (total bullshit) that permeates the Astral Waterways (bluespace quantum dimensions,
+ probably), Sleeping Carp users can now once against deflect projectiles with
+ their bare hands when focused in on battle (in combat mode).
+ - balance: The Keelhaul technique is now nonlethal (a philosophical acknowledgement
+ of the familial bond of sleep and death), but causes the target to become temporarily
+ blind and dizzy along with its previous effects.
+ - balance: Sleeping carp users, while in combat mode, deal Stamina damage with their
+ grabs and shoves. If the target of their grab has enough Stamina damage (80),
+ they are knocked unconscious from a well placed nerve pinch.
+ - balance: Sleeping carp users find it very hard to wake up once they fall asleep....
+ san7890:
+ - bugfix: Slimes now need to be on an open turf to reproduce and split into more
+ slimy slimes, instead of getting away with using phasing powers in pipes.
+ - bugfix: All vehicles (such as VIMs operated by a mouse or a lizard) will no longer
+ be able to phase through containment fields.
diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi
index b01986a9522d0..0dd945ff37eb4 100644
Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ
diff --git a/icons/hud/screen_alert.dmi b/icons/hud/screen_alert.dmi
index 0fa8ec218500e..593a3878c2e6f 100644
Binary files a/icons/hud/screen_alert.dmi and b/icons/hud/screen_alert.dmi differ
diff --git a/icons/mob/actions/actions_animal.dmi b/icons/mob/actions/actions_animal.dmi
index f3f9a667f2204..64b1c700f414c 100644
Binary files a/icons/mob/actions/actions_animal.dmi and b/icons/mob/actions/actions_animal.dmi differ
diff --git a/icons/mob/actions/actions_trader.dmi b/icons/mob/actions/actions_trader.dmi
new file mode 100644
index 0000000000000..59330b4aac832
Binary files /dev/null and b/icons/mob/actions/actions_trader.dmi differ
diff --git a/icons/mob/clothing/suits/labcoat.dmi b/icons/mob/clothing/suits/labcoat.dmi
index f499203c95e95..37cb0e8696bdb 100644
Binary files a/icons/mob/clothing/suits/labcoat.dmi and b/icons/mob/clothing/suits/labcoat.dmi differ
diff --git a/icons/mob/human/bodyparts.dmi b/icons/mob/human/bodyparts.dmi
index 7e804e894d213..2150b3c2c810f 100644
Binary files a/icons/mob/human/bodyparts.dmi and b/icons/mob/human/bodyparts.dmi differ
diff --git a/icons/mob/human/human_face.dmi b/icons/mob/human/human_face.dmi
index 6530b300aa676..886f0bf793b6b 100644
Binary files a/icons/mob/human/human_face.dmi and b/icons/mob/human/human_face.dmi differ
diff --git a/icons/mob/human/species/monkey/uniform.dmi b/icons/mob/human/species/monkey/uniform.dmi
index 21d70e5694f2c..a835629528ae5 100644
Binary files a/icons/mob/human/species/monkey/uniform.dmi and b/icons/mob/human/species/monkey/uniform.dmi differ
diff --git a/icons/mob/inhands/equipment/hydroponics_lefthand.dmi b/icons/mob/inhands/equipment/hydroponics_lefthand.dmi
index 031909dbf8240..5f771404f2636 100644
Binary files a/icons/mob/inhands/equipment/hydroponics_lefthand.dmi and b/icons/mob/inhands/equipment/hydroponics_lefthand.dmi differ
diff --git a/icons/mob/inhands/equipment/hydroponics_righthand.dmi b/icons/mob/inhands/equipment/hydroponics_righthand.dmi
index a2cac9c47f307..11b9195203272 100644
Binary files a/icons/mob/inhands/equipment/hydroponics_righthand.dmi and b/icons/mob/inhands/equipment/hydroponics_righthand.dmi differ
diff --git a/icons/mob/inhands/weapons/guns_lefthand.dmi b/icons/mob/inhands/weapons/guns_lefthand.dmi
index d0caa04a33a03..3e29ac726f995 100644
Binary files a/icons/mob/inhands/weapons/guns_lefthand.dmi and b/icons/mob/inhands/weapons/guns_lefthand.dmi differ
diff --git a/icons/mob/inhands/weapons/guns_righthand.dmi b/icons/mob/inhands/weapons/guns_righthand.dmi
index e4842aca23aa6..1a640935e0c6a 100644
Binary files a/icons/mob/inhands/weapons/guns_righthand.dmi and b/icons/mob/inhands/weapons/guns_righthand.dmi differ
diff --git a/icons/mob/landmarks.dmi b/icons/mob/landmarks.dmi
index f3ba54fe9b045..4f2402389df9f 100644
Binary files a/icons/mob/landmarks.dmi and b/icons/mob/landmarks.dmi differ
diff --git a/icons/mob/simple/animal.dmi b/icons/mob/simple/animal.dmi
index 01670b07d9389..3bd3438bc62d5 100644
Binary files a/icons/mob/simple/animal.dmi and b/icons/mob/simple/animal.dmi differ
diff --git a/icons/mob/simple/arachnoid.dmi b/icons/mob/simple/arachnoid.dmi
index fca53195d4c06..d17297f2ccf57 100644
Binary files a/icons/mob/simple/arachnoid.dmi and b/icons/mob/simple/arachnoid.dmi differ
diff --git a/icons/mob/simple/jungle/leaper.dmi b/icons/mob/simple/jungle/leaper.dmi
index 39f807a191c2e..2150c7b1ac2cf 100644
Binary files a/icons/mob/simple/jungle/leaper.dmi and b/icons/mob/simple/jungle/leaper.dmi differ
diff --git a/icons/mob/simple/traders.dmi b/icons/mob/simple/traders.dmi
deleted file mode 100644
index 2f2b828bed911..0000000000000
Binary files a/icons/mob/simple/traders.dmi and /dev/null differ
diff --git a/icons/obj/clothing/head/cowboy.dmi b/icons/obj/clothing/head/cowboy.dmi
index 66c0767294101..1a330eba0f801 100644
Binary files a/icons/obj/clothing/head/cowboy.dmi and b/icons/obj/clothing/head/cowboy.dmi differ
diff --git a/icons/obj/clothing/suits/labcoat.dmi b/icons/obj/clothing/suits/labcoat.dmi
index fa404813e40dc..430d11d5f96ab 100644
Binary files a/icons/obj/clothing/suits/labcoat.dmi and b/icons/obj/clothing/suits/labcoat.dmi differ
diff --git a/icons/obj/doors/airlocks/tram/tram-overlays.dmi b/icons/obj/doors/airlocks/tram/tram-overlays.dmi
new file mode 100644
index 0000000000000..879c920686d5e
Binary files /dev/null and b/icons/obj/doors/airlocks/tram/tram-overlays.dmi differ
diff --git a/icons/obj/doors/airlocks/tram/tram.dmi b/icons/obj/doors/airlocks/tram/tram.dmi
new file mode 100644
index 0000000000000..43d30b58a6f45
Binary files /dev/null and b/icons/obj/doors/airlocks/tram/tram.dmi differ
diff --git a/icons/obj/doors/puzzledoor/danger.dmi b/icons/obj/doors/puzzledoor/danger.dmi
new file mode 100644
index 0000000000000..89d19131cc189
Binary files /dev/null and b/icons/obj/doors/puzzledoor/danger.dmi differ
diff --git a/icons/obj/doors/puzzledoor/default.dmi b/icons/obj/doors/puzzledoor/default.dmi
index ec4fbed514bfa..49a9206139580 100644
Binary files a/icons/obj/doors/puzzledoor/default.dmi and b/icons/obj/doors/puzzledoor/default.dmi differ
diff --git a/icons/obj/doors/tramdoor.dmi b/icons/obj/doors/tramdoor.dmi
deleted file mode 100644
index 8ca8c9aee499e..0000000000000
Binary files a/icons/obj/doors/tramdoor.dmi and /dev/null differ
diff --git a/icons/obj/fluff/tram_rails.dmi b/icons/obj/fluff/tram_rails.dmi
deleted file mode 100644
index 359fc5f783800..0000000000000
Binary files a/icons/obj/fluff/tram_rails.dmi and /dev/null differ
diff --git a/icons/obj/food/lizard.dmi b/icons/obj/food/lizard.dmi
index f404218aa7a60..7cc7bf0fcf9c5 100644
Binary files a/icons/obj/food/lizard.dmi and b/icons/obj/food/lizard.dmi differ
diff --git a/icons/obj/food/piecake.dmi b/icons/obj/food/piecake.dmi
index 9a122e00ef7df..8474ba29fe9f8 100644
Binary files a/icons/obj/food/piecake.dmi and b/icons/obj/food/piecake.dmi differ
diff --git a/icons/obj/lighting.dmi b/icons/obj/lighting.dmi
index 061099defd4cd..2bda7341e518f 100644
Binary files a/icons/obj/lighting.dmi and b/icons/obj/lighting.dmi differ
diff --git a/icons/obj/machines/barsigns.dmi b/icons/obj/machines/barsigns.dmi
index c9450a009a152..4101f256495dd 100644
Binary files a/icons/obj/machines/barsigns.dmi and b/icons/obj/machines/barsigns.dmi differ
diff --git a/icons/obj/machines/computer.dmi b/icons/obj/machines/computer.dmi
index 5ffa3445db692..9cb0dda4967fd 100644
Binary files a/icons/obj/machines/computer.dmi and b/icons/obj/machines/computer.dmi differ
diff --git a/icons/obj/machines/crossing_signal.dmi b/icons/obj/machines/crossing_signal.dmi
deleted file mode 100644
index 4e31966f7e43d..0000000000000
Binary files a/icons/obj/machines/crossing_signal.dmi and /dev/null differ
diff --git a/icons/obj/machines/lift_indicator.dmi b/icons/obj/machines/lift_indicator.dmi
index 878606ae48962..24983cf5dae9c 100644
Binary files a/icons/obj/machines/lift_indicator.dmi and b/icons/obj/machines/lift_indicator.dmi differ
diff --git a/icons/obj/machines/modular_console.dmi b/icons/obj/machines/modular_console.dmi
index 86e3d7139579d..2677dbb71220a 100644
Binary files a/icons/obj/machines/modular_console.dmi and b/icons/obj/machines/modular_console.dmi differ
diff --git a/icons/obj/machines/tram_sign.dmi b/icons/obj/machines/tram_sign.dmi
deleted file mode 100644
index 1043153c4e3b1..0000000000000
Binary files a/icons/obj/machines/tram_sign.dmi and /dev/null differ
diff --git a/icons/obj/machines/wallmounts.dmi b/icons/obj/machines/wallmounts.dmi
index 39e4cc2476b58..c5f2254c4bb15 100644
Binary files a/icons/obj/machines/wallmounts.dmi and b/icons/obj/machines/wallmounts.dmi differ
diff --git a/icons/obj/modular_laptop.dmi b/icons/obj/modular_laptop.dmi
index 22432826e9287..ee275066d1653 100644
Binary files a/icons/obj/modular_laptop.dmi and b/icons/obj/modular_laptop.dmi differ
diff --git a/icons/obj/modular_pda.dmi b/icons/obj/modular_pda.dmi
index 18df3249e62db..b43be78e717b0 100644
Binary files a/icons/obj/modular_pda.dmi and b/icons/obj/modular_pda.dmi differ
diff --git a/icons/obj/railings.dmi b/icons/obj/railings.dmi
index 3dbbd7c8318e7..6518908d544c2 100644
Binary files a/icons/obj/railings.dmi and b/icons/obj/railings.dmi differ
diff --git a/icons/obj/service/bureaucracy.dmi b/icons/obj/service/bureaucracy.dmi
index 8cccb7f591032..d8190ba2e723f 100644
Binary files a/icons/obj/service/bureaucracy.dmi and b/icons/obj/service/bureaucracy.dmi differ
diff --git a/icons/obj/service/hydroponics/equipment.dmi b/icons/obj/service/hydroponics/equipment.dmi
index afcc4de523512..ed339a8a4209d 100644
Binary files a/icons/obj/service/hydroponics/equipment.dmi and b/icons/obj/service/hydroponics/equipment.dmi differ
diff --git a/icons/obj/service/hydroponics/growing_fruits.dmi b/icons/obj/service/hydroponics/growing_fruits.dmi
index 92c52b55dbfce..c0f547322c7cc 100644
Binary files a/icons/obj/service/hydroponics/growing_fruits.dmi and b/icons/obj/service/hydroponics/growing_fruits.dmi differ
diff --git a/icons/obj/service/hydroponics/harvest.dmi b/icons/obj/service/hydroponics/harvest.dmi
index 57a127cd9975e..f4489b01fa053 100644
Binary files a/icons/obj/service/hydroponics/harvest.dmi and b/icons/obj/service/hydroponics/harvest.dmi differ
diff --git a/icons/obj/service/hydroponics/seeds.dmi b/icons/obj/service/hydroponics/seeds.dmi
index ffef219193a71..db6f93878d06f 100644
Binary files a/icons/obj/service/hydroponics/seeds.dmi and b/icons/obj/service/hydroponics/seeds.dmi differ
diff --git a/icons/obj/signs.dmi b/icons/obj/signs.dmi
index 20531b0f8ea27..9ece919c21332 100644
Binary files a/icons/obj/signs.dmi and b/icons/obj/signs.dmi differ
diff --git a/icons/obj/smooth_structures/tram_window.dmi b/icons/obj/smooth_structures/tram_window.dmi
deleted file mode 100644
index 938ca3a0c0bfa..0000000000000
Binary files a/icons/obj/smooth_structures/tram_window.dmi and /dev/null differ
diff --git a/icons/obj/structures.dmi b/icons/obj/structures.dmi
index 0289c9105be06..a41aa161d0f10 100644
Binary files a/icons/obj/structures.dmi and b/icons/obj/structures.dmi differ
diff --git a/icons/obj/trader_signs.dmi b/icons/obj/trader_signs.dmi
new file mode 100644
index 0000000000000..a6789e0bb4c5f
Binary files /dev/null and b/icons/obj/trader_signs.dmi differ
diff --git a/icons/obj/tram/crossing_signal.dmi b/icons/obj/tram/crossing_signal.dmi
new file mode 100644
index 0000000000000..bfcda58c7f3a1
Binary files /dev/null and b/icons/obj/tram/crossing_signal.dmi differ
diff --git a/icons/obj/tram/tram_controllers.dmi b/icons/obj/tram/tram_controllers.dmi
new file mode 100644
index 0000000000000..93462e0b41e0b
Binary files /dev/null and b/icons/obj/tram/tram_controllers.dmi differ
diff --git a/icons/obj/tram/tram_display.dmi b/icons/obj/tram/tram_display.dmi
new file mode 100644
index 0000000000000..e28beef468fef
Binary files /dev/null and b/icons/obj/tram/tram_display.dmi differ
diff --git a/icons/obj/tram/tram_indicator.dmi b/icons/obj/tram/tram_indicator.dmi
new file mode 100644
index 0000000000000..f1b0b4123908d
Binary files /dev/null and b/icons/obj/tram/tram_indicator.dmi differ
diff --git a/icons/obj/tram/tram_rails.dmi b/icons/obj/tram/tram_rails.dmi
new file mode 100644
index 0000000000000..8e0316223b3e0
Binary files /dev/null and b/icons/obj/tram/tram_rails.dmi differ
diff --git a/icons/obj/tram/tram_sensor.dmi b/icons/obj/tram/tram_sensor.dmi
new file mode 100644
index 0000000000000..5146e79c4ac3b
Binary files /dev/null and b/icons/obj/tram/tram_sensor.dmi differ
diff --git a/icons/obj/tram/tram_structure.dmi b/icons/obj/tram/tram_structure.dmi
new file mode 100644
index 0000000000000..9fd919163d1f2
Binary files /dev/null and b/icons/obj/tram/tram_structure.dmi differ
diff --git a/icons/turf/walls/tram_wall.dmi b/icons/obj/tram/tram_wall.dmi
similarity index 100%
rename from icons/turf/walls/tram_wall.dmi
rename to icons/obj/tram/tram_wall.dmi
diff --git a/icons/obj/weapons/guns/energy.dmi b/icons/obj/weapons/guns/energy.dmi
index 9a86e65329fb4..5d607026f0133 100644
Binary files a/icons/obj/weapons/guns/energy.dmi and b/icons/obj/weapons/guns/energy.dmi differ
diff --git a/icons/obj/weapons/guns/magic.dmi b/icons/obj/weapons/guns/magic.dmi
index 7cab0cdfc2592..53217ef461b37 100644
Binary files a/icons/obj/weapons/guns/magic.dmi and b/icons/obj/weapons/guns/magic.dmi differ
diff --git a/icons/program_icons/mafia.gif b/icons/program_icons/mafia.gif
new file mode 100644
index 0000000000000..5821f55e1e49c
Binary files /dev/null and b/icons/program_icons/mafia.gif differ
diff --git a/icons/turf/damaged.dmi b/icons/turf/damaged.dmi
index d3d06d53e4691..a81384d8be6a1 100644
Binary files a/icons/turf/damaged.dmi and b/icons/turf/damaged.dmi differ
diff --git a/icons/turf/floors.dmi b/icons/turf/floors.dmi
index 6ddc178b98cdb..9dca06d4153ca 100644
Binary files a/icons/turf/floors.dmi and b/icons/turf/floors.dmi differ
diff --git a/icons/turf/tram.dmi b/icons/turf/tram.dmi
new file mode 100644
index 0000000000000..d38802bd8de60
Binary files /dev/null and b/icons/turf/tram.dmi differ
diff --git a/icons/turf/walls/meat.dmi b/icons/turf/walls/meat.dmi
new file mode 100644
index 0000000000000..fbbeb8a9c3cac
Binary files /dev/null and b/icons/turf/walls/meat.dmi differ
diff --git a/icons/ui_icons/patches/bandaid_1.png b/icons/ui_icons/patches/bandaid_1.png
deleted file mode 100644
index f752da706a92c..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_1.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_2.png b/icons/ui_icons/patches/bandaid_2.png
deleted file mode 100644
index 50ad473ba5d1f..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_2.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_3.png b/icons/ui_icons/patches/bandaid_3.png
deleted file mode 100644
index cee1dd19b83c5..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_3.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_4.png b/icons/ui_icons/patches/bandaid_4.png
deleted file mode 100644
index 3061235434c98..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_4.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_blank.png b/icons/ui_icons/patches/bandaid_blank.png
deleted file mode 100644
index ddd3882922ed7..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_blank.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_both.png b/icons/ui_icons/patches/bandaid_both.png
deleted file mode 100644
index 94245e560fe09..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_both.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_brute.png b/icons/ui_icons/patches/bandaid_brute.png
deleted file mode 100644
index bf6350f6ced97..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_brute.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_brute_2.png b/icons/ui_icons/patches/bandaid_brute_2.png
deleted file mode 100644
index 66c2d8494d556..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_brute_2.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_burn.png b/icons/ui_icons/patches/bandaid_burn.png
deleted file mode 100644
index 67c6fb0dcbbb4..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_burn.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_burn_2.png b/icons/ui_icons/patches/bandaid_burn_2.png
deleted file mode 100644
index db5df4f1e60ce..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_burn_2.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_clown.png b/icons/ui_icons/patches/bandaid_clown.png
deleted file mode 100644
index 0912ee04eac49..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_clown.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_colonthree.png b/icons/ui_icons/patches/bandaid_colonthree.png
deleted file mode 100644
index ffc7146cca49e..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_colonthree.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_exclaimationpoint.png b/icons/ui_icons/patches/bandaid_exclaimationpoint.png
deleted file mode 100644
index a6a423aa20bda..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_exclaimationpoint.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_mix.png b/icons/ui_icons/patches/bandaid_mix.png
deleted file mode 100644
index 1d5c03bfef6e4..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_mix.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_monke.png b/icons/ui_icons/patches/bandaid_monke.png
deleted file mode 100644
index d1083aa932fd3..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_monke.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_msic.png b/icons/ui_icons/patches/bandaid_msic.png
deleted file mode 100644
index ab7860c080c64..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_msic.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_questionmark.png b/icons/ui_icons/patches/bandaid_questionmark.png
deleted file mode 100644
index 99382b208aec3..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_questionmark.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_suffocation.png b/icons/ui_icons/patches/bandaid_suffocation.png
deleted file mode 100644
index 802baa07a8de3..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_suffocation.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_suffocation_2.png b/icons/ui_icons/patches/bandaid_suffocation_2.png
deleted file mode 100644
index 20d4223ecd7f8..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_suffocation_2.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_toxin.png b/icons/ui_icons/patches/bandaid_toxin.png
deleted file mode 100644
index 3a2fce33f0ffb..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_toxin.png and /dev/null differ
diff --git a/icons/ui_icons/patches/bandaid_toxin_2.png b/icons/ui_icons/patches/bandaid_toxin_2.png
deleted file mode 100644
index c40331e789d2a..0000000000000
Binary files a/icons/ui_icons/patches/bandaid_toxin_2.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill1.png b/icons/ui_icons/pills/pill1.png
deleted file mode 100644
index e5545302761d9..0000000000000
Binary files a/icons/ui_icons/pills/pill1.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill10.png b/icons/ui_icons/pills/pill10.png
deleted file mode 100644
index bb0c5f1df7177..0000000000000
Binary files a/icons/ui_icons/pills/pill10.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill11.png b/icons/ui_icons/pills/pill11.png
deleted file mode 100644
index d07d604b34deb..0000000000000
Binary files a/icons/ui_icons/pills/pill11.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill12.png b/icons/ui_icons/pills/pill12.png
deleted file mode 100644
index 912ce1c19e0d0..0000000000000
Binary files a/icons/ui_icons/pills/pill12.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill13.png b/icons/ui_icons/pills/pill13.png
deleted file mode 100644
index 4920af9e248b7..0000000000000
Binary files a/icons/ui_icons/pills/pill13.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill14.png b/icons/ui_icons/pills/pill14.png
deleted file mode 100644
index aff03f20610c4..0000000000000
Binary files a/icons/ui_icons/pills/pill14.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill15.png b/icons/ui_icons/pills/pill15.png
deleted file mode 100644
index 041b5be4b6bfb..0000000000000
Binary files a/icons/ui_icons/pills/pill15.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill16.png b/icons/ui_icons/pills/pill16.png
deleted file mode 100644
index 745151051b2f9..0000000000000
Binary files a/icons/ui_icons/pills/pill16.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill17.png b/icons/ui_icons/pills/pill17.png
deleted file mode 100644
index fcd92288ad94f..0000000000000
Binary files a/icons/ui_icons/pills/pill17.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill18.png b/icons/ui_icons/pills/pill18.png
deleted file mode 100644
index 9d730172a8e18..0000000000000
Binary files a/icons/ui_icons/pills/pill18.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill19.png b/icons/ui_icons/pills/pill19.png
deleted file mode 100644
index 1e3773404d6a3..0000000000000
Binary files a/icons/ui_icons/pills/pill19.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill2.png b/icons/ui_icons/pills/pill2.png
deleted file mode 100644
index e8ed754a6d3f3..0000000000000
Binary files a/icons/ui_icons/pills/pill2.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill20.png b/icons/ui_icons/pills/pill20.png
deleted file mode 100644
index aa77c3e41bec6..0000000000000
Binary files a/icons/ui_icons/pills/pill20.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill21.png b/icons/ui_icons/pills/pill21.png
deleted file mode 100644
index 37a4512d15d0d..0000000000000
Binary files a/icons/ui_icons/pills/pill21.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill22.png b/icons/ui_icons/pills/pill22.png
deleted file mode 100644
index 374de4d58fc24..0000000000000
Binary files a/icons/ui_icons/pills/pill22.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill3.png b/icons/ui_icons/pills/pill3.png
deleted file mode 100644
index e0c812fd0a44b..0000000000000
Binary files a/icons/ui_icons/pills/pill3.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill4.png b/icons/ui_icons/pills/pill4.png
deleted file mode 100644
index 5e02dd0d3e003..0000000000000
Binary files a/icons/ui_icons/pills/pill4.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill5.png b/icons/ui_icons/pills/pill5.png
deleted file mode 100644
index f9025d0995fd0..0000000000000
Binary files a/icons/ui_icons/pills/pill5.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill6.png b/icons/ui_icons/pills/pill6.png
deleted file mode 100644
index 2dbf3720142b2..0000000000000
Binary files a/icons/ui_icons/pills/pill6.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill7.png b/icons/ui_icons/pills/pill7.png
deleted file mode 100644
index 7c1ff90c43f7e..0000000000000
Binary files a/icons/ui_icons/pills/pill7.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill8.png b/icons/ui_icons/pills/pill8.png
deleted file mode 100644
index 88280ecd045c8..0000000000000
Binary files a/icons/ui_icons/pills/pill8.png and /dev/null differ
diff --git a/icons/ui_icons/pills/pill9.png b/icons/ui_icons/pills/pill9.png
deleted file mode 100644
index 732cd4ef4c8e3..0000000000000
Binary files a/icons/ui_icons/pills/pill9.png and /dev/null differ
diff --git a/lua/SS13.lua b/lua/SS13.lua
index a17d5b50577bb..076dc9ee72ed7 100644
--- a/lua/SS13.lua
+++ b/lua/SS13.lua
@@ -1,5 +1,9 @@
local SS13 = {}
+__SS13_signal_handlers = __SS13_signal_handlers or {}
+__SS13_timeouts = __SS13_timeouts or {}
+__SS13_timeouts_id_mapping = __SS13_timeouts_id_mapping or {}
+
SS13.SSlua = dm.global_vars.vars.SSlua
SS13.global_proc = "some_magic_bullshit"
@@ -11,6 +15,14 @@ for _, state in SS13.SSlua.vars.states do
end
end
+function SS13.get_runner_ckey()
+ return SS13.state:get_var("ckey_last_runner")
+end
+
+function SS13.get_runner_client()
+ return dm.global_vars:get_var("GLOB"):get_var("directory"):get(SS13.get_runner_ckey())
+end
+
function SS13.istype(thing, type)
return dm.global_proc("_istype", thing, dm.global_proc("_text2path", type)) == 1
end
@@ -65,39 +77,36 @@ function SS13.await(thing_to_call, proc_to_call, ...)
return return_value, runtime_message
end
-function SS13.wait(time, timer)
+function SS13.wait(time)
local callback = SS13.new("/datum/callback", SS13.SSlua, "queue_resume", SS13.state, __next_yield_index)
- local timedevent = dm.global_proc("_addtimer", callback, time * 10, 8, timer, debug.info(1, "sl"))
+ local timedevent = dm.global_proc("_addtimer", callback, time * 10, 8, nil, debug.info(1, "sl"))
coroutine.yield()
- dm.global_proc("deltimer", timedevent, timer)
+ dm.global_proc("deltimer", timedevent)
SS13.stop_tracking(callback)
end
function SS13.register_signal(datum, signal, func, make_easy_clear_function)
- if not SS13.signal_handlers then
- SS13.signal_handlers = {}
- end
if not SS13.istype(datum, "/datum") then
return
end
- if not SS13.signal_handlers[datum] then
- SS13.signal_handlers[datum] = {}
+ if not __SS13_signal_handlers[datum] then
+ __SS13_signal_handlers[datum] = {}
end
if signal == "_cleanup" then
return
end
- if not SS13.signal_handlers[datum][signal] then
- SS13.signal_handlers[datum][signal] = {}
+ if not __SS13_signal_handlers[datum][signal] then
+ __SS13_signal_handlers[datum][signal] = {}
end
local callback = SS13.new("/datum/callback", SS13.state, "call_function_return_first")
callback:call_proc("RegisterSignal", datum, signal, "Invoke")
- local path = { "SS13", "signal_handlers", dm.global_proc("WEAKREF", datum), signal, dm.global_proc("WEAKREF", callback), "func" }
+ local path = { "__SS13_signal_handlers", dm.global_proc("WEAKREF", datum), signal, dm.global_proc("WEAKREF", callback), "func" }
callback.vars.arguments = { path }
- if not SS13.signal_handlers[datum]["_cleanup"] then
- local cleanup_path = { "SS13", "signal_handlers", dm.global_proc("WEAKREF", datum), "_cleanup", "func" }
+ if not __SS13_signal_handlers[datum]["_cleanup"] then
+ local cleanup_path = { "__SS13_signal_handlers", dm.global_proc("WEAKREF", datum), "_cleanup", "func" }
local cleanup_callback = SS13.new("/datum/callback", SS13.state, "call_function_return_first", cleanup_path)
cleanup_callback:call_proc("RegisterSignal", datum, "parent_qdeleting", "Invoke")
- SS13.signal_handlers[datum]["_cleanup"] = {
+ __SS13_signal_handlers[datum]["_cleanup"] = {
func = function(datum)
SS13.signal_handler_cleanup(datum)
SS13.stop_tracking(cleanup_callback)
@@ -111,14 +120,14 @@ function SS13.register_signal(datum, signal, func, make_easy_clear_function)
local lookup_for_signal = comp_lookup.entries.parent_qdeleting
if lookup_for_signal and not SS13.istype(lookup_for_signal, "/datum") then
local cleanup_callback_index =
- dm.global_proc("_list_find", lookup_for_signal, SS13.signal_handlers[datum]["_cleanup"].callback)
+ dm.global_proc("_list_find", lookup_for_signal, __SS13_signal_handlers[datum]["_cleanup"].callback)
if cleanup_callback_index ~= 0 and cleanup_callback_index ~= #comp_lookup then
dm.global_proc("_list_swap", lookup_for_signal, cleanup_callback_index, #lookup_for_signal)
end
end
end
end
- SS13.signal_handlers[datum][signal][callback] = { func = func, callback = callback }
+ __SS13_signal_handlers[datum][signal][callback] = { func = func, callback = callback }
if make_easy_clear_function then
local clear_function_name = "clear_signal_" .. tostring(datum) .. "_" .. signal .. "_" .. tostring(callback)
SS13[clear_function_name] = function()
@@ -148,70 +157,93 @@ function SS13.unregister_signal(datum, signal, callback)
SS13.stop_tracking(handler_callback)
end
- if not SS13.signal_handlers then
- return
- end
-
local function clear_easy_clear_function(callback_to_clear)
local clear_function_name = "clear_signal_" .. tostring(datum) .. "_" .. signal .. "_" .. tostring(callback_to_clear)
SS13[clear_function_name] = nil
end
- if not SS13.signal_handlers[datum] then
+ if not __SS13_signal_handlers[datum] then
return
end
if signal == "_cleanup" then
return
end
- if not SS13.signal_handlers[datum][signal] then
+ if not __SS13_signal_handlers[datum][signal] then
return
end
if not callback then
- for handler_key, handler_info in SS13.signal_handlers[datum][signal] do
+ for handler_key, handler_info in __SS13_signal_handlers[datum][signal] do
clear_easy_clear_function(handler_key)
clear_handler(handler_info)
end
- SS13.signal_handlers[datum][signal] = nil
+ __SS13_signal_handlers[datum][signal] = nil
else
if not SS13.istype(callback, "/datum/callback") then
return
end
clear_easy_clear_function(callback)
- clear_handler(SS13.signal_handlers[datum][signal][callback])
- SS13.signal_handlers[datum][signal][callback] = nil
+ clear_handler(__SS13_signal_handlers[datum][signal][callback])
+ __SS13_signal_handlers[datum][signal][callback] = nil
end
end
function SS13.signal_handler_cleanup(datum)
- if not SS13.signal_handlers then
- return
- end
- if not SS13.signal_handlers[datum] then
+ if not __SS13_signal_handlers[datum] then
return
end
- for signal, _ in SS13.signal_handlers[datum] do
+ for signal, _ in __SS13_signal_handlers[datum] do
SS13.unregister_signal(datum, signal)
end
- SS13.signal_handlers[datum] = nil
+ __SS13_signal_handlers[datum] = nil
end
-function SS13.set_timeout(time, func, timer)
- if not SS13.timeouts then
- SS13.timeouts = {}
+function SS13.set_timeout(time, func)
+ SS13.start_loop(time, 1, func)
+end
+
+function SS13.start_loop(time, amount, func)
+ if not amount or amount == 0 then
+ return
end
local callback = SS13.new("/datum/callback", SS13.state, "call_function")
- local timedevent = dm.global_proc("_addtimer", callback, time * 10, 8, timer, debug.info(1, "sl"))
- SS13.timeouts[callback] = function()
- SS13.timeouts[callback] = nil
- dm.global_proc("deltimer", timedevent, timer)
- SS13.stop_tracking(callback)
+ local timedevent = dm.global_proc("_addtimer", callback, time * 10, 40, nil, debug.info(1, "sl"))
+ local doneAmount = 0
+ __SS13_timeouts[callback] = function()
+ doneAmount += 1
+ if amount ~= -1 and doneAmount >= amount then
+ SS13.end_loop(timedevent)
+ end
func()
end
- local path = { "SS13", "timeouts", dm.global_proc("WEAKREF", callback) }
+ local loop_data = {
+ callback = callback,
+ loop_amount = amount,
+ }
+ __SS13_timeouts_id_mapping[timedevent] = loop_data
+ local path = { "__SS13_timeouts", dm.global_proc("WEAKREF", callback) }
callback.vars.arguments = { path }
+ return timedevent
+end
+
+function SS13.end_loop(id)
+ local data = __SS13_timeouts_id_mapping[id]
+ if data then
+ __SS13_timeouts_id_mapping[id] = nil
+ __SS13_timeouts[data.callback] = nil
+ SS13.stop_tracking(data.callback)
+ dm.global_proc("deltimer", id)
+ end
+end
+
+function SS13.stop_all_loops()
+ for id, data in __SS13_timeouts_id_mapping do
+ if data.amount ~= 1 then
+ SS13.end_loop(id)
+ end
+ end
end
return SS13
diff --git a/sound/attributions.txt b/sound/attributions.txt
index 4f000ad82f95c..e7ceb44f08b0e 100644
--- a/sound/attributions.txt
+++ b/sound/attributions.txt
@@ -106,6 +106,9 @@ https://freesound.org/people/reelworldstudio/sounds/161122/
arcade_jump.ogg is adapted from se2001's "8-Bit Jump 3", which is public domain (CC 0):
hhttps://freesound.org/people/se2001/sounds/528568/
+hiccup sfx is taken from SOUNDFISHING's hiccup sound effects, which is license under LESF and allowed ussage for video games under section 4 of aggreement:
+https://www.soundfishing.eu/sound/hiccup
+
laser2.ogg is adapted with 3 SFX made by junggle (CC 4), inferno (CC Sampling+), humanoide9000 (CC 0):
https://freesound.org/people/junggle/sounds/28917/
https://freesound.org/people/inferno/sounds/18397/
diff --git a/sound/effects/sf_hiccup_male_01.ogg b/sound/effects/sf_hiccup_male_01.ogg
new file mode 100644
index 0000000000000..bdff5eb24b83a
Binary files /dev/null and b/sound/effects/sf_hiccup_male_01.ogg differ
diff --git a/sound/items/frog_statue_release.ogg b/sound/items/frog_statue_release.ogg
new file mode 100644
index 0000000000000..de7d3547778a9
Binary files /dev/null and b/sound/items/frog_statue_release.ogg differ
diff --git a/sound/misc/salute.ogg b/sound/misc/salute.ogg
new file mode 100644
index 0000000000000..76521a63540ec
Binary files /dev/null and b/sound/misc/salute.ogg differ
diff --git a/sound/voice/sec_death.ogg b/sound/voice/sec_death.ogg
new file mode 100644
index 0000000000000..25f9b24c313f2
Binary files /dev/null and b/sound/voice/sec_death.ogg differ
diff --git a/sound/weapons/beesmoke.ogg b/sound/weapons/beesmoke.ogg
new file mode 100644
index 0000000000000..5e29f37a224e3
Binary files /dev/null and b/sound/weapons/beesmoke.ogg differ
diff --git a/sound/weapons/taser3.ogg b/sound/weapons/taser3.ogg
new file mode 100644
index 0000000000000..bfe904c902a2f
Binary files /dev/null and b/sound/weapons/taser3.ogg differ
diff --git a/strings/names/syndicate_monkey.txt b/strings/names/syndicate_monkey.txt
new file mode 100644
index 0000000000000..9b2c059a7dfc5
--- /dev/null
+++ b/strings/names/syndicate_monkey.txt
@@ -0,0 +1,19 @@
+Agent 9
+Agent Banana
+Agent Potassium
+Agent Ape
+Al Chimpone
+Aldo
+Banana Bond
+Bonobo Assassin
+Caesar
+Evil Monkey
+Solid Simian
+Tony Bananas
+Koba
+Murderous George
+Monkey Business
+Hit-Monkey
+Guenter
+Goku
+Kill Julien
diff --git a/strings/tips.txt b/strings/tips.txt
index 51dd6a635d160..a0f75a9fad467 100644
--- a/strings/tips.txt
+++ b/strings/tips.txt
@@ -94,6 +94,7 @@ As a Security Officer, mindshield implants can only prevent someone from being t
As a Security Officer, remember that correlation does not equal causation. Someone may have just been at the wrong place at the wrong time!
As a Security Officer, remember that you can attach a sec-lite to your disabler or your helmet!
As a Security Officer, your sechuds or HUDsunglasses can not only see crewmates' job assignments and criminal status, but also if they are mindshield implanted. You can tell by the flashing blue outline around their job icon. Use this to your advantage in a revolution to definitively tell who is on your side!
+As a Security Officer, you have a special pen in your PDA that you can use to prompt people to surrender. This will stun them without the need to use a baton!
As a Service Cyborg, your spray can knocks people down. However, it is blocked by masks and glasses.
As a Shaft Miner, always have a GPS on you, so a fellow miner or cyborg can come to save you if you die.
As a Shaft Miner, every monster on Lavaland has a pattern you can exploit to minimize damage from the encounters.
diff --git a/tgstation.dme b/tgstation.dme
index bb2da82fed3ee..e8b7d9ff89a5b 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -18,10 +18,11 @@
#include "code\_compile_options.dm"
#include "code\_experiments.dm"
#include "code\world.dm"
+#include "code\__DEFINES\__globals.dm"
#include "code\__DEFINES\_atoms.dm"
#include "code\__DEFINES\_bitfields.dm"
#include "code\__DEFINES\_click.dm"
-#include "code\__DEFINES\_globals.dm"
+#include "code\__DEFINES\_flags.dm"
#include "code\__DEFINES\_helpers.dm"
#include "code\__DEFINES\_protect.dm"
#include "code\__DEFINES\_tick.dm"
@@ -35,6 +36,7 @@
#include "code\__DEFINES\airlock.dm"
#include "code\__DEFINES\alarm.dm"
#include "code\__DEFINES\alerts.dm"
+#include "code\__DEFINES\announcements.dm"
#include "code\__DEFINES\anomaly.dm"
#include "code\__DEFINES\antagonists.dm"
#include "code\__DEFINES\apc_defines.dm"
@@ -92,7 +94,6 @@
#include "code\__DEFINES\fantasy_affixes.dm"
#include "code\__DEFINES\firealarm.dm"
#include "code\__DEFINES\fish.dm"
-#include "code\__DEFINES\flags.dm"
#include "code\__DEFINES\flora.dm"
#include "code\__DEFINES\font_awesome_icons.dm"
#include "code\__DEFINES\fonts.dm"
@@ -101,6 +102,7 @@
#include "code\__DEFINES\fov.dm"
#include "code\__DEFINES\generators.dm"
#include "code\__DEFINES\ghost.dm"
+#include "code\__DEFINES\gradient.dm"
#include "code\__DEFINES\gravity.dm"
#include "code\__DEFINES\guardian_defines.dm"
#include "code\__DEFINES\holiday.dm"
@@ -109,7 +111,6 @@
#include "code\__DEFINES\icon_smoothing.dm"
#include "code\__DEFINES\id_cards.dm"
#include "code\__DEFINES\important_recursive_contents.dm"
-#include "code\__DEFINES\industrial_lift.dm"
#include "code\__DEFINES\injection.dm"
#include "code\__DEFINES\input.dm"
#include "code\__DEFINES\instruments.dm"
@@ -138,6 +139,7 @@
#include "code\__DEFINES\MC.dm"
#include "code\__DEFINES\mecha.dm"
#include "code\__DEFINES\medical.dm"
+#include "code\__DEFINES\megafauna.dm"
#include "code\__DEFINES\melee.dm"
#include "code\__DEFINES\memory_defines.dm"
#include "code\__DEFINES\mergers.dm"
@@ -155,6 +157,7 @@
#include "code\__DEFINES\nitrile.dm"
#include "code\__DEFINES\nuclear_bomb.dm"
#include "code\__DEFINES\obj_flags.dm"
+#include "code\__DEFINES\observers.dm"
#include "code\__DEFINES\overlays.dm"
#include "code\__DEFINES\pai.dm"
#include "code\__DEFINES\paintings.dm"
@@ -208,6 +211,7 @@
#include "code\__DEFINES\species_clothing_paths.dm"
#include "code\__DEFINES\speech_channels.dm"
#include "code\__DEFINES\sprite_accessories.dm"
+#include "code\__DEFINES\stack.dm"
#include "code\__DEFINES\stack_trace.dm"
#include "code\__DEFINES\stat.dm"
#include "code\__DEFINES\stat_tracking.dm"
@@ -228,8 +232,9 @@
#include "code\__DEFINES\time.dm"
#include "code\__DEFINES\tools.dm"
#include "code\__DEFINES\toys.dm"
+#include "code\__DEFINES\trader.dm"
#include "code\__DEFINES\traits.dm"
-#include "code\__DEFINES\tram.dm"
+#include "code\__DEFINES\transport.dm"
#include "code\__DEFINES\tts.dm"
#include "code\__DEFINES\turbine_defines.dm"
#include "code\__DEFINES\turfs.dm"
@@ -256,6 +261,7 @@
#include "code\__DEFINES\ai\pets.dm"
#include "code\__DEFINES\ai\simplemob.dm"
#include "code\__DEFINES\ai\tourist.dm"
+#include "code\__DEFINES\ai\trader.dm"
#include "code\__DEFINES\ai\vending.dm"
#include "code\__DEFINES\ai\ventcrawling.dm"
#include "code\__DEFINES\atmospherics\atmos_core.dm"
@@ -338,8 +344,8 @@
#include "code\__DEFINES\dcs\signals\signals_techweb.dm"
#include "code\__DEFINES\dcs\signals\signals_tools.dm"
#include "code\__DEFINES\dcs\signals\signals_traitor.dm"
-#include "code\__DEFINES\dcs\signals\signals_tram.dm"
#include "code\__DEFINES\dcs\signals\signals_transform.dm"
+#include "code\__DEFINES\dcs\signals\signals_transport.dm"
#include "code\__DEFINES\dcs\signals\signals_turf.dm"
#include "code\__DEFINES\dcs\signals\signals_twohand.dm"
#include "code\__DEFINES\dcs\signals\signals_vehicle.dm"
@@ -417,7 +423,6 @@
#include "code\__HELPERS\mouse_control.dm"
#include "code\__HELPERS\nameof.dm"
#include "code\__HELPERS\names.dm"
-#include "code\__HELPERS\path.dm"
#include "code\__HELPERS\piping_colors_lists.dm"
#include "code\__HELPERS\priority_announce.dm"
#include "code\__HELPERS\pronouns.dm"
@@ -472,8 +477,12 @@
#include "code\__HELPERS\logging\shuttle.dm"
#include "code\__HELPERS\logging\talk.dm"
#include "code\__HELPERS\logging\tool.dm"
+#include "code\__HELPERS\logging\transport.dm"
#include "code\__HELPERS\logging\ui.dm"
#include "code\__HELPERS\logging\virus.dm"
+#include "code\__HELPERS\paths\jps.dm"
+#include "code\__HELPERS\paths\path.dm"
+#include "code\__HELPERS\paths\sssp.dm"
#include "code\__HELPERS\sorts\__main.dm"
#include "code\__HELPERS\sorts\InsertSort.dm"
#include "code\__HELPERS\sorts\MergeSort.dm"
@@ -506,6 +515,7 @@
#include "code\_globalvars\lists\mobs.dm"
#include "code\_globalvars\lists\names.dm"
#include "code\_globalvars\lists\objects.dm"
+#include "code\_globalvars\lists\plumbing.dm"
#include "code\_globalvars\lists\poll_ignore.dm"
#include "code\_globalvars\lists\quirks.dm"
#include "code\_globalvars\lists\rcd.dm"
@@ -630,11 +640,11 @@
#include "code\controllers\subsystem\pai.dm"
#include "code\controllers\subsystem\parallax.dm"
#include "code\controllers\subsystem\pathfinder.dm"
-#include "code\controllers\subsystem\persistence.dm"
#include "code\controllers\subsystem\persistent_paintings.dm"
#include "code\controllers\subsystem\ping.dm"
#include "code\controllers\subsystem\points_of_interest.dm"
#include "code\controllers\subsystem\profiler.dm"
+#include "code\controllers\subsystem\queuelinks.dm"
#include "code\controllers\subsystem\radiation.dm"
#include "code\controllers\subsystem\radio.dm"
#include "code\controllers\subsystem\radioactive_nebula.dm"
@@ -661,6 +671,7 @@
#include "code\controllers\subsystem\timer.dm"
#include "code\controllers\subsystem\title.dm"
#include "code\controllers\subsystem\traitor.dm"
+#include "code\controllers\subsystem\transport.dm"
#include "code\controllers\subsystem\tts.dm"
#include "code\controllers\subsystem\tutorials.dm"
#include "code\controllers\subsystem\verb_manager.dm"
@@ -676,6 +687,16 @@
#include "code\controllers\subsystem\movement\movement.dm"
#include "code\controllers\subsystem\movement\movement_types.dm"
#include "code\controllers\subsystem\movement\spacedrift.dm"
+#include "code\controllers\subsystem\persistence\_persistence.dm"
+#include "code\controllers\subsystem\persistence\counter_delamination.dm"
+#include "code\controllers\subsystem\persistence\counter_tram_hits.dm"
+#include "code\controllers\subsystem\persistence\custom_outfits.dm"
+#include "code\controllers\subsystem\persistence\engravings.dm"
+#include "code\controllers\subsystem\persistence\photo_albums.dm"
+#include "code\controllers\subsystem\persistence\recipes.dm"
+#include "code\controllers\subsystem\persistence\scars.dm"
+#include "code\controllers\subsystem\persistence\tattoos.dm"
+#include "code\controllers\subsystem\persistence\trophies.dm"
#include "code\controllers\subsystem\processing\acid.dm"
#include "code\controllers\subsystem\processing\ai_basic_avoidance.dm"
#include "code\controllers\subsystem\processing\ai_behaviors.dm"
@@ -699,7 +720,6 @@
#include "code\controllers\subsystem\processing\singulo.dm"
#include "code\controllers\subsystem\processing\station.dm"
#include "code\controllers\subsystem\processing\supermatter_cascade.dm"
-#include "code\controllers\subsystem\processing\tramprocess.dm"
#include "code\controllers\subsystem\processing\wet_floors.dm"
#include "code\datums\alarm.dm"
#include "code\datums\beam.dm"
@@ -813,6 +833,7 @@
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\nearest_targetting.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\pick_up_item.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\run_away_from_target.dm"
+#include "code\datums\ai\basic_mobs\basic_ai_behaviors\set_travel_destination.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\step_towards_turf.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\stop_and_stare.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\targeted_mob_ability.dm"
@@ -820,25 +841,30 @@
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\tipped_reaction.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\travel_towards.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\ventcrawling.dm"
+#include "code\datums\ai\basic_mobs\basic_ai_behaviors\wounded_targetting.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\write_on_paper.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\attack_adjacent_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\attack_obstacle_in_path.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\call_reinforcements.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\capricious_retaliate.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\climb_tree.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\find_food.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\find_paper_and_write.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\find_parent.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\flee_target.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\go_for_swim.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\maintain_distance.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\mine_walls.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\move_to_cardinal.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\opportunistic_ventcrawler.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\prepare_travel_to_destination.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\ranged_skirmish.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\run_emote.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\shapechange_ambush.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\simple_attack_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\simple_find_nearest_target_to_flee.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\simple_find_target.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\simple_find_wounded_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\sleep_with_no_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\speech_subtree.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\stare_at_thing.dm"
@@ -949,6 +975,7 @@
#include "code\datums\components\beetlejuice.dm"
#include "code\datums\components\blob_minion.dm"
#include "code\datums\components\blood_walk.dm"
+#include "code\datums\components\bloody_spreader.dm"
#include "code\datums\components\bloodysoles.dm"
#include "code\datums\components\boomerang.dm"
#include "code\datums\components\boss_music.dm"
@@ -992,9 +1019,11 @@
#include "code\datums\components\egg_layer.dm"
#include "code\datums\components\electrified_buckle.dm"
#include "code\datums\components\embedded.dm"
+#include "code\datums\components\energized.dm"
#include "code\datums\components\engraved.dm"
#include "code\datums\components\evolutionary_leap.dm"
#include "code\datums\components\explodable.dm"
+#include "code\datums\components\explode_on_attack.dm"
#include "code\datums\components\faction_granter.dm"
#include "code\datums\components\fertile_egg.dm"
#include "code\datums\components\fishing_spot.dm"
@@ -1047,6 +1076,7 @@
#include "code\datums\components\omen.dm"
#include "code\datums\components\on_hit_effect.dm"
#include "code\datums\components\onwear_mood.dm"
+#include "code\datums\components\orbit_poll.dm"
#include "code\datums\components\orbiter.dm"
#include "code\datums\components\overlay_lighting.dm"
#include "code\datums\components\palette.dm"
@@ -1055,8 +1085,8 @@
#include "code\datums\components\pellet_cloud.dm"
#include "code\datums\components\phylactery.dm"
#include "code\datums\components\pinata.dm"
+#include "code\datums\components\plundering_attacks.dm"
#include "code\datums\components\pricetag.dm"
-#include "code\datums\components\pry_open_door.dm"
#include "code\datums\components\punchcooldown.dm"
#include "code\datums\components\puzzgrid.dm"
#include "code\datums\components\radiation_countdown.dm"
@@ -1064,6 +1094,7 @@
#include "code\datums\components\radioactive_exposure.dm"
#include "code\datums\components\ranged_attacks.dm"
#include "code\datums\components\reagent_refiller.dm"
+#include "code\datums\components\recharging_attacks.dm"
#include "code\datums\components\redirect_attack_hand_from_turf.dm"
#include "code\datums\components\reflection.dm"
#include "code\datums\components\regenerator.dm"
@@ -1127,6 +1158,7 @@
#include "code\datums\components\tippable.dm"
#include "code\datums\components\toggle_attached_clothing.dm"
#include "code\datums\components\toggle_suit.dm"
+#include "code\datums\components\torn_wall.dm"
#include "code\datums\components\transforming.dm"
#include "code\datums\components\trapdoor.dm"
#include "code\datums\components\tree_climber.dm"
@@ -1138,6 +1170,7 @@
#include "code\datums\components\uplink.dm"
#include "code\datums\components\usb_port.dm"
#include "code\datums\components\vacuum.dm"
+#include "code\datums\components\vision_hurting.dm"
#include "code\datums\components\wall_mounted.dm"
#include "code\datums\components\wearertargeting.dm"
#include "code\datums\components\weatherannouncer.dm"
@@ -1183,7 +1216,6 @@
#include "code\datums\components\plumbing\_plumbing.dm"
#include "code\datums\components\plumbing\chemical_acclimator.dm"
#include "code\datums\components\plumbing\filter.dm"
-#include "code\datums\components\plumbing\IV_drip.dm"
#include "code\datums\components\plumbing\reaction_chamber.dm"
#include "code\datums\components\plumbing\splitter.dm"
#include "code\datums\components\riding\riding.dm"
@@ -1191,9 +1223,11 @@
#include "code\datums\components\riding\riding_vehicle.dm"
#include "code\datums\components\style\style.dm"
#include "code\datums\components\style\style_meter.dm"
+#include "code\datums\components\trader\trader.dm"
#include "code\datums\diseases\_disease.dm"
#include "code\datums\diseases\_MobProcs.dm"
#include "code\datums\diseases\adrenal_crisis.dm"
+#include "code\datums\diseases\anaphylaxis.dm"
#include "code\datums\diseases\anxiety.dm"
#include "code\datums\diseases\beesease.dm"
#include "code\datums\diseases\brainrot.dm"
@@ -1258,6 +1292,7 @@
#include "code\datums\elements\ai_flee_while_injured.dm"
#include "code\datums\elements\ai_held_item.dm"
#include "code\datums\elements\ai_retaliate.dm"
+#include "code\datums\elements\ai_swap_combat_mode.dm"
#include "code\datums\elements\ai_target_damagesource.dm"
#include "code\datums\elements\amputating_limbs.dm"
#include "code\datums\elements\animal_variety.dm"
@@ -1303,6 +1338,7 @@
#include "code\datums\elements\dextrous.dm"
#include "code\datums\elements\diggable.dm"
#include "code\datums\elements\digitalcamo.dm"
+#include "code\datums\elements\door_pryer.dm"
#include "code\datums\elements\drag_pickup.dm"
#include "code\datums\elements\dryable.dm"
#include "code\datums\elements\earhealing.dm"
@@ -1324,6 +1360,7 @@
#include "code\datums\elements\haunted.dm"
#include "code\datums\elements\high_fiver.dm"
#include "code\datums\elements\honkspam.dm"
+#include "code\datums\elements\human_biter.dm"
#include "code\datums\elements\immerse.dm"
#include "code\datums\elements\item_fov.dm"
#include "code\datums\elements\item_scaling.dm"
@@ -1339,6 +1376,7 @@
#include "code\datums\elements\mirage_border.dm"
#include "code\datums\elements\mob_grabber.dm"
#include "code\datums\elements\mob_killed_tally.dm"
+#include "code\datums\elements\move_force_on_death.dm"
#include "code\datums\elements\movement_turf_changer.dm"
#include "code\datums\elements\movetype_handler.dm"
#include "code\datums\elements\nerfed_pulling.dm"
@@ -1374,7 +1412,6 @@
#include "code\datums\elements\strippable.dm"
#include "code\datums\elements\structure_repair.dm"
#include "code\datums\elements\swabbable.dm"
-#include "code\datums\elements\tear_wall.dm"
#include "code\datums\elements\temporary_atom.dm"
#include "code\datums\elements\tenacious.dm"
#include "code\datums\elements\tiny_mob_hunter.dm"
@@ -1390,6 +1427,7 @@
#include "code\datums\elements\waddling.dm"
#include "code\datums\elements\wall_engraver.dm"
#include "code\datums\elements\wall_smasher.dm"
+#include "code\datums\elements\wall_tearer.dm"
#include "code\datums\elements\wall_walker.dm"
#include "code\datums\elements\weapon_description.dm"
#include "code\datums\elements\weather_listener.dm"
@@ -1461,7 +1499,6 @@
#include "code\datums\looping_sounds\music.dm"
#include "code\datums\looping_sounds\vents.dm"
#include "code\datums\looping_sounds\weather.dm"
-#include "code\datums\mapgen\_MapGenerator.dm"
#include "code\datums\mapgen\CaveGenerator.dm"
#include "code\datums\mapgen\JungleGenerator.dm"
#include "code\datums\mapgen\biomes\_biome.dm"
@@ -1546,6 +1583,7 @@
#include "code\datums\quirks\negative_quirks\deafness.dm"
#include "code\datums\quirks\negative_quirks\depression.dm"
#include "code\datums\quirks\negative_quirks\family_heirloom.dm"
+#include "code\datums\quirks\negative_quirks\food_allergy.dm"
#include "code\datums\quirks\negative_quirks\frail.dm"
#include "code\datums\quirks\negative_quirks\glass_jaw.dm"
#include "code\datums\quirks\negative_quirks\heavy_sleeper.dm"
@@ -1645,6 +1683,7 @@
#include "code\datums\skills\_skill.dm"
#include "code\datums\skills\cleaning.dm"
#include "code\datums\skills\fishing.dm"
+#include "code\datums\skills\fitness.dm"
#include "code\datums\skills\gaming.dm"
#include "code\datums\skills\mining.dm"
#include "code\datums\station_traits\_station_trait.dm"
@@ -2131,6 +2170,7 @@
#include "code\game\objects\items\extinguisher.dm"
#include "code\game\objects\items\fireaxe.dm"
#include "code\game\objects\items\flamethrower.dm"
+#include "code\game\objects\items\frog_statue.dm"
#include "code\game\objects\items\gift.dm"
#include "code\game\objects\items\gun_maintenance.dm"
#include "code\game\objects\items\hand_items.dm"
@@ -3019,6 +3059,7 @@
#include "code\modules\antagonists\space_ninja\space_ninja.dm"
#include "code\modules\antagonists\spiders\spiders.dm"
#include "code\modules\antagonists\survivalist\survivalist.dm"
+#include "code\modules\antagonists\syndicate_monkey\syndicate_monkey.dm"
#include "code\modules\antagonists\traitor\balance_helper.dm"
#include "code\modules\antagonists\traitor\datum_traitor.dm"
#include "code\modules\antagonists\traitor\objective_category.dm"
@@ -3130,10 +3171,8 @@
#include "code\modules\asset_cache\assets\orbit.dm"
#include "code\modules\asset_cache\assets\paper.dm"
#include "code\modules\asset_cache\assets\particle_editor.dm"
-#include "code\modules\asset_cache\assets\patches.dm"
#include "code\modules\asset_cache\assets\pda.dm"
#include "code\modules\asset_cache\assets\permissions.dm"
-#include "code\modules\asset_cache\assets\pills.dm"
#include "code\modules\asset_cache\assets\pipes.dm"
#include "code\modules\asset_cache\assets\plane_debug.dm"
#include "code\modules\asset_cache\assets\plumbing.dm"
@@ -3268,6 +3307,7 @@
#include "code\modules\bitrunning\components\avatar_connection.dm"
#include "code\modules\bitrunning\components\bitrunning_points.dm"
#include "code\modules\bitrunning\components\netpod_healing.dm"
+#include "code\modules\bitrunning\components\virtual_elite_mob.dm"
#include "code\modules\bitrunning\objects\byteforge.dm"
#include "code\modules\bitrunning\objects\clothing.dm"
#include "code\modules\bitrunning\objects\disks.dm"
@@ -3286,18 +3326,19 @@
#include "code\modules\bitrunning\server\obj_generation.dm"
#include "code\modules\bitrunning\server\quantum_server.dm"
#include "code\modules\bitrunning\server\signal_handlers.dm"
+#include "code\modules\bitrunning\server\threats.dm"
#include "code\modules\bitrunning\server\util.dm"
#include "code\modules\bitrunning\virtual_domain\safehouses.dm"
#include "code\modules\bitrunning\virtual_domain\virtual_domain.dm"
#include "code\modules\bitrunning\virtual_domain\domains\ash_drake.dm"
#include "code\modules\bitrunning\virtual_domain\domains\beach_bar.dm"
#include "code\modules\bitrunning\virtual_domain\domains\blood_drunk_miner.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\breeze_bay.dm"
#include "code\modules\bitrunning\virtual_domain\domains\bubblegum.dm"
#include "code\modules\bitrunning\virtual_domain\domains\clown_planet.dm"
#include "code\modules\bitrunning\virtual_domain\domains\colossus.dm"
#include "code\modules\bitrunning\virtual_domain\domains\gondola_asteroid.dm"
#include "code\modules\bitrunning\virtual_domain\domains\hierophant.dm"
-#include "code\modules\bitrunning\virtual_domain\domains\legion.dm"
#include "code\modules\bitrunning\virtual_domain\domains\pipedream.dm"
#include "code\modules\bitrunning\virtual_domain\domains\pirates.dm"
#include "code\modules\bitrunning\virtual_domain\domains\psyker_shuffle.dm"
@@ -3436,6 +3477,7 @@
#include "code\modules\client\preferences\broadcast_login_logout.dm"
#include "code\modules\client\preferences\clothing.dm"
#include "code\modules\client\preferences\darkened_flash.dm"
+#include "code\modules\client\preferences\food_allergy.dm"
#include "code\modules\client\preferences\fov_darkness.dm"
#include "code\modules\client\preferences\fps.dm"
#include "code\modules\client\preferences\gender.dm"
@@ -3897,6 +3939,7 @@
#include "code\modules\hallucination\hud_screw.dm"
#include "code\modules\hallucination\ice_cube.dm"
#include "code\modules\hallucination\inhand_fake_item.dm"
+#include "code\modules\hallucination\mother.dm"
#include "code\modules\hallucination\nearby_fake_item.dm"
#include "code\modules\hallucination\on_fire.dm"
#include "code\modules\hallucination\screwy_health_doll.dm"
@@ -3911,7 +3954,6 @@
#include "code\modules\holodeck\holo_effect.dm"
#include "code\modules\holodeck\holodeck_map_templates.dm"
#include "code\modules\holodeck\items.dm"
-#include "code\modules\holodeck\mobs.dm"
#include "code\modules\holodeck\turfs.dm"
#include "code\modules\hydroponics\biogenerator.dm"
#include "code\modules\hydroponics\bouquets.dm"
@@ -3927,6 +3969,7 @@
#include "code\modules\hydroponics\seed_extractor.dm"
#include "code\modules\hydroponics\seeds.dm"
#include "code\modules\hydroponics\unique_plant_genes.dm"
+#include "code\modules\hydroponics\beekeeping\bee_smoker.dm"
#include "code\modules\hydroponics\beekeeping\beebox.dm"
#include "code\modules\hydroponics\beekeeping\beekeeper_suit.dm"
#include "code\modules\hydroponics\beekeeping\honey_frame.dm"
@@ -3978,23 +4021,6 @@
#include "code\modules\hydroponics\grown\weeds\kudzu.dm"
#include "code\modules\hydroponics\grown\weeds\nettle.dm"
#include "code\modules\hydroponics\grown\weeds\starthistle.dm"
-#include "code\modules\industrial_lift\industrial_lift.dm"
-#include "code\modules\industrial_lift\lift_master.dm"
-#include "code\modules\industrial_lift\elevator\elevator_controller.dm"
-#include "code\modules\industrial_lift\elevator\elevator_doors.dm"
-#include "code\modules\industrial_lift\elevator\elevator_indicator.dm"
-#include "code\modules\industrial_lift\elevator\elevator_music_zone.dm"
-#include "code\modules\industrial_lift\elevator\elevator_panel.dm"
-#include "code\modules\industrial_lift\tram\tram_doors.dm"
-#include "code\modules\industrial_lift\tram\tram_floors.dm"
-#include "code\modules\industrial_lift\tram\tram_landmark.dm"
-#include "code\modules\industrial_lift\tram\tram_lift_master.dm"
-#include "code\modules\industrial_lift\tram\tram_machinery.dm"
-#include "code\modules\industrial_lift\tram\tram_override_objects.dm"
-#include "code\modules\industrial_lift\tram\tram_remote.dm"
-#include "code\modules\industrial_lift\tram\tram_structures.dm"
-#include "code\modules\industrial_lift\tram\tram_walls.dm"
-#include "code\modules\industrial_lift\tram\tram_windows.dm"
#include "code\modules\instruments\items.dm"
#include "code\modules\instruments\piano_synth.dm"
#include "code\modules\instruments\stationary.dm"
@@ -4180,6 +4206,7 @@
#include "code\modules\logging\categories\log_category_uplink.dm"
#include "code\modules\mafia\_defines.dm"
#include "code\modules\mafia\controller.dm"
+#include "code\modules\mafia\controller_ui.dm"
#include "code\modules\mafia\map_pieces.dm"
#include "code\modules\mafia\outfits.dm"
#include "code\modules\mafia\abilities\abilities.dm"
@@ -4210,6 +4237,7 @@
#include "code\modules\mapfluff\ruins\icemoonruin_code\hotsprings.dm"
#include "code\modules\mapfluff\ruins\icemoonruin_code\library.dm"
#include "code\modules\mapfluff\ruins\icemoonruin_code\mailroom.dm"
+#include "code\modules\mapfluff\ruins\icemoonruin_code\mining_site.dm"
#include "code\modules\mapfluff\ruins\icemoonruin_code\wrath.dm"
#include "code\modules\mapfluff\ruins\lavalandruin_code\biodome_clown_planet.dm"
#include "code\modules\mapfluff\ruins\lavalandruin_code\biodome_winter.dm"
@@ -4242,6 +4270,7 @@
#include "code\modules\mapfluff\ruins\spaceruin_code\hilbertshotel.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\interdyne.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\listeningstation.dm"
+#include "code\modules\mapfluff\ruins\spaceruin_code\meatderelict.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\meateor.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\originalcontent.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\spacehotel.dm"
@@ -4375,8 +4404,23 @@
#include "code\modules\mob\living\basic\blob_minions\blobbernaut.dm"
#include "code\modules\mob\living\basic\clown\clown.dm"
#include "code\modules\mob\living\basic\clown\clown_ai.dm"
-#include "code\modules\mob\living\basic\constructs\_construct.dm"
-#include "code\modules\mob\living\basic\constructs\harvester.dm"
+#include "code\modules\mob\living\basic\cult\shade.dm"
+#include "code\modules\mob\living\basic\cult\constructs\_construct.dm"
+#include "code\modules\mob\living\basic\cult\constructs\artificer.dm"
+#include "code\modules\mob\living\basic\cult\constructs\construct_ai.dm"
+#include "code\modules\mob\living\basic\cult\constructs\harvester.dm"
+#include "code\modules\mob\living\basic\cult\constructs\juggernaut.dm"
+#include "code\modules\mob\living\basic\cult\constructs\proteon.dm"
+#include "code\modules\mob\living\basic\cult\constructs\wraith.dm"
+#include "code\modules\mob\living\basic\drone\_drone.dm"
+#include "code\modules\mob\living\basic\drone\drone_say.dm"
+#include "code\modules\mob\living\basic\drone\drone_tools.dm"
+#include "code\modules\mob\living\basic\drone\drones_as_items.dm"
+#include "code\modules\mob\living\basic\drone\extra_drone_types.dm"
+#include "code\modules\mob\living\basic\drone\interaction.dm"
+#include "code\modules\mob\living\basic\drone\inventory.dm"
+#include "code\modules\mob\living\basic\drone\verbs.dm"
+#include "code\modules\mob\living\basic\drone\visuals_icons.dm"
#include "code\modules\mob\living\basic\farm_animals\deer.dm"
#include "code\modules\mob\living\basic\farm_animals\pig.dm"
#include "code\modules\mob\living\basic\farm_animals\pony.dm"
@@ -4398,11 +4442,11 @@
#include "code\modules\mob\living\basic\farm_animals\gorilla\gorilla_accessories.dm"
#include "code\modules\mob\living\basic\farm_animals\gorilla\gorilla_ai.dm"
#include "code\modules\mob\living\basic\farm_animals\gorilla\gorilla_emotes.dm"
+#include "code\modules\mob\living\basic\heretic\_heretic_summon.dm"
#include "code\modules\mob\living\basic\heretic\ash_spirit.dm"
#include "code\modules\mob\living\basic\heretic\fire_shark.dm"
#include "code\modules\mob\living\basic\heretic\flesh_stalker.dm"
#include "code\modules\mob\living\basic\heretic\flesh_worm.dm"
-#include "code\modules\mob\living\basic\heretic\heretic_summon.dm"
#include "code\modules\mob\living\basic\heretic\maid_in_the_mirror.dm"
#include "code\modules\mob\living\basic\heretic\raw_prophet.dm"
#include "code\modules\mob\living\basic\heretic\rust_walker.dm"
@@ -4414,6 +4458,9 @@
#include "code\modules\mob\living\basic\icemoon\ice_whelp\ice_whelp_abilities.dm"
#include "code\modules\mob\living\basic\icemoon\ice_whelp\ice_whelp_ai.dm"
#include "code\modules\mob\living\basic\jungle\venus_human_trap.dm"
+#include "code\modules\mob\living\basic\jungle\leaper\leaper.dm"
+#include "code\modules\mob\living\basic\jungle\leaper\leaper_abilities.dm"
+#include "code\modules\mob\living\basic\jungle\leaper\leaper_ai.dm"
#include "code\modules\mob\living\basic\jungle\mega_arachnid\mega_arachnid.dm"
#include "code\modules\mob\living\basic\jungle\mega_arachnid\mega_arachnid_abilities.dm"
#include "code\modules\mob\living\basic\jungle\mega_arachnid\mega_arachnid_ai.dm"
@@ -4473,7 +4520,13 @@
#include "code\modules\mob\living\basic\pets\dog\corgi.dm"
#include "code\modules\mob\living\basic\pets\dog\dog_subtypes.dm"
#include "code\modules\mob\living\basic\pets\dog\strippable_items.dm"
+#include "code\modules\mob\living\basic\ruin_defender\flesh.dm"
+#include "code\modules\mob\living\basic\ruin_defender\living_floor.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\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"
#include "code\modules\mob\living\basic\space_fauna\ant.dm"
#include "code\modules\mob\living\basic\space_fauna\cat_surgeon.dm"
#include "code\modules\mob\living\basic\space_fauna\faithless.dm"
@@ -4530,6 +4583,9 @@
#include "code\modules\mob\living\basic\space_fauna\revenant\revenant_objectives.dm"
#include "code\modules\mob\living\basic\space_fauna\snake\snake.dm"
#include "code\modules\mob\living\basic\space_fauna\snake\snake_ai.dm"
+#include "code\modules\mob\living\basic\space_fauna\space_dragon\dragon_breath.dm"
+#include "code\modules\mob\living\basic\space_fauna\space_dragon\dragon_gust.dm"
+#include "code\modules\mob\living\basic\space_fauna\space_dragon\space_dragon.dm"
#include "code\modules\mob\living\basic\space_fauna\spider\spider.dm"
#include "code\modules\mob\living\basic\space_fauna\spider\giant_spider\giant_spider_ai.dm"
#include "code\modules\mob\living\basic\space_fauna\spider\giant_spider\giant_spider_subtrees.dm"
@@ -4548,9 +4604,17 @@
#include "code\modules\mob\living\basic\space_fauna\wumborian_fugu\inflation.dm"
#include "code\modules\mob\living\basic\space_fauna\wumborian_fugu\wumborian_ai.dm"
#include "code\modules\mob\living\basic\space_fauna\wumborian_fugu\wumborian_fugu.dm"
-#include "code\modules\mob\living\basic\syndicate\russian.dm"
-#include "code\modules\mob\living\basic\syndicate\syndicate.dm"
-#include "code\modules\mob\living\basic\syndicate\syndicate_ai.dm"
+#include "code\modules\mob\living\basic\trader\trader.dm"
+#include "code\modules\mob\living\basic\trader\trader_actions.dm"
+#include "code\modules\mob\living\basic\trader\trader_ai.dm"
+#include "code\modules\mob\living\basic\trader\trader_data.dm"
+#include "code\modules\mob\living\basic\trader\trader_items.dm"
+#include "code\modules\mob\living\basic\trooper\nanotrasen.dm"
+#include "code\modules\mob\living\basic\trooper\pirate.dm"
+#include "code\modules\mob\living\basic\trooper\russian.dm"
+#include "code\modules\mob\living\basic\trooper\syndicate.dm"
+#include "code\modules\mob\living\basic\trooper\trooper.dm"
+#include "code\modules\mob\living\basic\trooper\trooper_ai.dm"
#include "code\modules\mob\living\basic\vermin\axolotl.dm"
#include "code\modules\mob\living\basic\vermin\butterfly.dm"
#include "code\modules\mob\living\basic\vermin\cockroach.dm"
@@ -4619,7 +4683,6 @@
#include "code\modules\mob\living\carbon\alien\special\alien_embryo.dm"
#include "code\modules\mob\living\carbon\alien\special\facehugger.dm"
#include "code\modules\mob\living\carbon\human\_species.dm"
-#include "code\modules\mob\living\carbon\human\damage_procs.dm"
#include "code\modules\mob\living\carbon\human\death.dm"
#include "code\modules\mob\living\carbon\human\dummy.dm"
#include "code\modules\mob\living\carbon\human\emote.dm"
@@ -4706,7 +4769,6 @@
#include "code\modules\mob\living\simple_animal\animal_defense.dm"
#include "code\modules\mob\living\simple_animal\damage_procs.dm"
#include "code\modules\mob\living\simple_animal\parrot.dm"
-#include "code\modules\mob\living\simple_animal\shade.dm"
#include "code\modules\mob\living\simple_animal\simple_animal.dm"
#include "code\modules\mob\living\simple_animal\bot\bot.dm"
#include "code\modules\mob\living\simple_animal\bot\bot_announcement.dm"
@@ -4725,15 +4787,6 @@
#include "code\modules\mob\living\simple_animal\friendly\cat.dm"
#include "code\modules\mob\living\simple_animal\friendly\gondola.dm"
#include "code\modules\mob\living\simple_animal\friendly\pet.dm"
-#include "code\modules\mob\living\simple_animal\friendly\drone\_drone.dm"
-#include "code\modules\mob\living\simple_animal\friendly\drone\drone_say.dm"
-#include "code\modules\mob\living\simple_animal\friendly\drone\drone_tools.dm"
-#include "code\modules\mob\living\simple_animal\friendly\drone\drones_as_items.dm"
-#include "code\modules\mob\living\simple_animal\friendly\drone\extra_drone_types.dm"
-#include "code\modules\mob\living\simple_animal\friendly\drone\interaction.dm"
-#include "code\modules\mob\living\simple_animal\friendly\drone\inventory.dm"
-#include "code\modules\mob\living\simple_animal\friendly\drone\verbs.dm"
-#include "code\modules\mob\living\simple_animal\friendly\drone\visuals_icons.dm"
#include "code\modules\mob\living\simple_animal\guardian\guardian.dm"
#include "code\modules\mob\living\simple_animal\guardian\guardian_creator.dm"
#include "code\modules\mob\living\simple_animal\guardian\types\assassin.dm"
@@ -4752,20 +4805,9 @@
#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\nanotrasen.dm"
#include "code\modules\mob\living\simple_animal\hostile\ooze.dm"
-#include "code\modules\mob\living\simple_animal\hostile\pirate.dm"
-#include "code\modules\mob\living\simple_animal\hostile\skeleton.dm"
-#include "code\modules\mob\living\simple_animal\hostile\space_dragon.dm"
#include "code\modules\mob\living\simple_animal\hostile\vatbeast.dm"
-#include "code\modules\mob\living\simple_animal\hostile\wizard.dm"
#include "code\modules\mob\living\simple_animal\hostile\zombie.dm"
-#include "code\modules\mob\living\simple_animal\hostile\constructs\artificer.dm"
-#include "code\modules\mob\living\simple_animal\hostile\constructs\constructs.dm"
-#include "code\modules\mob\living\simple_animal\hostile\constructs\juggernaut.dm"
-#include "code\modules\mob\living\simple_animal\hostile\constructs\wraith.dm"
-#include "code\modules\mob\living\simple_animal\hostile\jungle\_jungle_mobs.dm"
-#include "code\modules\mob\living\simple_animal\hostile\jungle\leaper.dm"
#include "code\modules\mob\living\simple_animal\hostile\megafauna\_megafauna.dm"
#include "code\modules\mob\living\simple_animal\hostile\megafauna\blood_drunk_miner.dm"
#include "code\modules\mob\living\simple_animal\hostile\megafauna\bubblegum.dm"
@@ -4788,7 +4830,6 @@
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\pandora.dm"
#include "code\modules\mob\living\simple_animal\hostile\retaliate\goose.dm"
#include "code\modules\mob\living\simple_animal\hostile\retaliate\retaliate.dm"
-#include "code\modules\mob\living\simple_animal\hostile\retaliate\trader.dm"
#include "code\modules\mob\living\simple_animal\slime\death.dm"
#include "code\modules\mob\living\simple_animal\slime\emote.dm"
#include "code\modules\mob\living\simple_animal\slime\life.dm"
@@ -4873,6 +4914,7 @@
#include "code\modules\modular_computers\file_system\programs\file_browser.dm"
#include "code\modules\modular_computers\file_system\programs\frontier.dm"
#include "code\modules\modular_computers\file_system\programs\jobmanagement.dm"
+#include "code\modules\modular_computers\file_system\programs\mafia_ntos.dm"
#include "code\modules\modular_computers\file_system\programs\newscasterapp.dm"
#include "code\modules\modular_computers\file_system\programs\notepad.dm"
#include "code\modules\modular_computers\file_system\programs\nt_pay.dm"
@@ -4964,6 +5006,7 @@
#include "code\modules\plumbing\plumbers\fermenter.dm"
#include "code\modules\plumbing\plumbers\filter.dm"
#include "code\modules\plumbing\plumbers\grinder_chemical.dm"
+#include "code\modules\plumbing\plumbers\iv_drip.dm"
#include "code\modules\plumbing\plumbers\pill_press.dm"
#include "code\modules\plumbing\plumbers\plumbing_buffer.dm"
#include "code\modules\plumbing\plumbers\pumps.dm"
@@ -4971,6 +5014,7 @@
#include "code\modules\plumbing\plumbers\splitters.dm"
#include "code\modules\plumbing\plumbers\synthesizer.dm"
#include "code\modules\plumbing\plumbers\teleporter.dm"
+#include "code\modules\plumbing\plumbers\vatgrower.dm"
#include "code\modules\point\point.dm"
#include "code\modules\power\cable.dm"
#include "code\modules\power\cell.dm"
@@ -5336,7 +5380,6 @@
#include "code\modules\research\xenobiology\vatgrowing\microscope.dm"
#include "code\modules\research\xenobiology\vatgrowing\petri_dish.dm"
#include "code\modules\research\xenobiology\vatgrowing\swab.dm"
-#include "code\modules\research\xenobiology\vatgrowing\vatgrower.dm"
#include "code\modules\research\xenobiology\vatgrowing\samples\_micro_organism.dm"
#include "code\modules\research\xenobiology\vatgrowing\samples\_sample.dm"
#include "code\modules\research\xenobiology\vatgrowing\samples\cell_lines\common.dm"
@@ -5597,6 +5640,25 @@
#include "code\modules\tgui_panel\telemetry.dm"
#include "code\modules\tgui_panel\tgui_panel.dm"
#include "code\modules\tooltip\tooltip.dm"
+#include "code\modules\transport\_transport_machinery.dm"
+#include "code\modules\transport\admin.dm"
+#include "code\modules\transport\linear_controller.dm"
+#include "code\modules\transport\transport_module.dm"
+#include "code\modules\transport\transport_navigation.dm"
+#include "code\modules\transport\elevator\elev_controller.dm"
+#include "code\modules\transport\elevator\elev_doors.dm"
+#include "code\modules\transport\elevator\elev_indicator.dm"
+#include "code\modules\transport\elevator\elev_music_zone.dm"
+#include "code\modules\transport\elevator\elev_panel.dm"
+#include "code\modules\transport\tram\tram_controller.dm"
+#include "code\modules\transport\tram\tram_controls.dm"
+#include "code\modules\transport\tram\tram_displays.dm"
+#include "code\modules\transport\tram\tram_doors.dm"
+#include "code\modules\transport\tram\tram_floors.dm"
+#include "code\modules\transport\tram\tram_machinery.dm"
+#include "code\modules\transport\tram\tram_remote.dm"
+#include "code\modules\transport\tram\tram_signals.dm"
+#include "code\modules\transport\tram\tram_structures.dm"
#include "code\modules\tutorials\_tutorial.dm"
#include "code\modules\tutorials\tutorial_instruction.dm"
#include "code\modules\tutorials\tutorials\drop.dm"
@@ -5817,6 +5879,7 @@
#include "code\modules\wiremod\datatypes\option.dm"
#include "code\modules\wiremod\datatypes\signal.dm"
#include "code\modules\wiremod\datatypes\string.dm"
+#include "code\modules\wiremod\datatypes\user.dm"
#include "code\modules\wiremod\datatypes\composite\assoc_list.dm"
#include "code\modules\wiremod\datatypes\composite\composite.dm"
#include "code\modules\wiremod\datatypes\composite\list.dm"
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
index 09d6c93f4974b..48d146451deea 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
@@ -3,6 +3,9 @@
* SPDX-License-Identifier: MIT
*/
+@use 'sass:map';
+@use 'sass:color';
+
em {
font-style: normal;
font-weight: bold;
@@ -436,10 +439,33 @@ em {
}
.minorannounce {
+ color: #c51e1e;
+ font-weight: bold;
+ font-size: 185%;
+}
+
+.minoralert {
+ color: #a4bad6;
+ font-size: 125%;
+}
+
+.priorityannounce {
+ color: #a4bad6;
+ font-weight: bold;
+ font-size: 225%;
+}
+
+.prioritytitle {
+ color: #6685f5;
font-weight: bold;
font-size: 185%;
}
+.priorityalert {
+ color: #c51e1e;
+ font-size: 140%;
+}
+
.greenannounce {
color: #059223;
font-weight: bold;
@@ -678,8 +704,14 @@ em {
font-size: 185%;
}
+.spiderbreacher {
+ color: #e8b670;
+ font-weight: bold;
+ font-size: 140%;
+}
+
.spiderscout {
- color: #231d99;
+ color: #231d98;
font-weight: bold;
font-size: 120%;
}
@@ -918,3 +950,157 @@ em {
font-style: italic;
border-bottom: 1px dashed #fff;
}
+
+$alert-stripe-colors: (
+ 'default': #00283a,
+ 'green': #003d00,
+ 'blue': #00283a,
+ 'pink': #30001b,
+ 'yellow': #574a00,
+ 'orange': #593400,
+ 'red': #420000,
+ 'purple': #2c0030,
+ 'grey': #252525,
+);
+
+$alert-stripe-alternate-colors: (
+ 'default': #003045,
+ 'green': #004700,
+ 'blue': #003045,
+ 'pink': #400025,
+ 'yellow': #4d4100,
+ 'orange': #6b4200,
+ 'red': #520000,
+ 'purple': #38003d,
+ 'grey': #292929,
+);
+
+$alert-major-header-colors: (
+ 'default': #33d5ff,
+ 'green': #00ff80,
+ 'blue': #33d5ff,
+ 'pink': #ff5297,
+ 'yellow': #fff4e0,
+ 'orange': #feefe7,
+ 'red': #ff5297,
+ 'purple': #c7a1f7,
+ 'grey': #ff5297,
+);
+
+$alert-subheader-header-colors: (
+ 'default': #ff5297,
+ 'green': #ff85b5,
+ 'blue': #ff5297,
+ 'pink': #33d5ff,
+ 'yellow': #33d5ff,
+ 'orange': #33d5ff,
+ 'red': #33d5ff,
+ 'purple': #33d5ff,
+ 'grey': #33d5ff,
+);
+
+$border-width: 4;
+
+$border-width-px: $border-width * 1px;
+
+.major_announcement_title {
+ font-size: 175%;
+ padding: 0rem 0.5rem;
+ line-height: 100%;
+ text-align: left;
+ text-decoration: none;
+ width: 100%;
+}
+
+.subheader_announcement_text {
+ font-weight: bold;
+ padding: 0 0.5rem;
+ padding-top: 0.25rem;
+ line-height: 100%;
+ width: 100%;
+ height: 100%;
+ text-align: left;
+ font-size: 125%;
+}
+
+.major_announcement_text {
+ color: #eaeaea;
+ background-color: #131313;
+ font-weight: bold;
+ font-size: 100%;
+ text-align: left;
+ padding: 0.5rem 0.5rem;
+ width: 100%;
+ height: 100%;
+}
+
+.minor_announcement_title {
+ font-weight: bold;
+ padding: 0 0.5rem;
+ padding-top: 0;
+ line-height: 100%;
+ width: 100%;
+ height: 100%;
+ text-align: left;
+ font-size: 150%;
+}
+
+.minor_announcement_text {
+ background-color: #202020;
+ color: #eaeaea;
+ padding: 0.5rem 0.5rem;
+ text-align: left;
+ font-size: 100%;
+}
+
+.announcement_header {
+ padding: 0.5rem 0;
+ display: flex;
+ flex-direction: column;
+}
+
+@each $color-name, $color-value in $alert-stripe-colors {
+ .chat_alert_#{$color-name} {
+ color: #ffffff;
+ padding: 0.5rem 0.5rem;
+ box-shadow: none;
+ font-weight: bold;
+ margin: 1rem 0 1rem 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ border-image: repeating-linear-gradient(
+ -45deg,
+ map.get($alert-stripe-alternate-colors, $color-name),
+ map.get($alert-stripe-alternate-colors, $color-name) 10px,
+ $color-value 10px,
+ $color-value 20px
+ );
+ border-image-slice: $border-width fill;
+ border-width: $border-width-px;
+ border-image-width: $border-width-px;
+ border-image-outset: 0 0 0 0;
+ border-image-repeat: repeat repeat;
+ border-style: solid;
+ }
+
+ .chat_alert_#{$color-name} .major_announcement_title {
+ color: map.get($alert-major-header-colors, $color-name);
+ }
+
+ .chat_alert_#{$color-name} .minor_announcement_title {
+ color: map.get($alert-major-header-colors, $color-name);
+ }
+
+ .chat_alert_#{$color-name} .subheader_announcement_text {
+ color: map.get($alert-subheader-header-colors, $color-name);
+ }
+
+ .chat_alert_#{$color-name} .minor_announcement_text {
+ background-color: darken(map.get($alert-stripe-colors, $color-name), 5);
+ }
+
+ .chat_alert_#{$color-name} .major_announcement_text {
+ background-color: darken(map.get($alert-stripe-colors, $color-name), 5);
+ }
+}
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
index 60654f76e967c..759b07cfbd85e 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
@@ -3,6 +3,9 @@
* SPDX-License-Identifier: MIT
*/
+@use 'sass:map';
+@use 'sass:color';
+
html,
body {
padding: 0;
@@ -468,10 +471,33 @@ h2.alert {
}
.minorannounce {
+ color: #ff0000;
+ font-weight: bold;
+ font-size: 185%;
+}
+
+.minoralert {
+ color: #000000;
+ font-size: 125%;
+}
+
+.priorityannounce {
+ color: #000000;
+ font-weight: bold;
+ font-size: 225%;
+}
+
+.prioritytitle {
+ color: #0000ff;
font-weight: bold;
font-size: 185%;
}
+.priorityalert {
+ color: #ff0000;
+ font-size: 140%;
+}
+
.greenannounce {
color: #00ff00;
font-weight: bold;
@@ -710,8 +736,14 @@ h2.alert {
font-size: 185%;
}
+.spiderbreacher {
+ color: #804b02;
+ font-weight: bold;
+ font-size: 140%;
+}
+
.spiderscout {
- color: #1b10cd;
+ color: #0c0674;
font-weight: bold;
font-size: 120%;
}
@@ -950,3 +982,163 @@ h2.alert {
font-style: italic;
border-bottom: 1px dashed #000;
}
+
+$alert-stripe-colors: (
+ 'default': #b3bfff,
+ 'green': #adffad,
+ 'blue': #b3bfff,
+ 'pink': #ffb3df,
+ 'yellow': #fff3b3,
+ 'orange': #ffe2b3,
+ 'red': #ffb3b3,
+ 'purple': #fac2ff,
+ 'grey': #e3e3e3,
+);
+
+$alert-stripe-alternate-colors: (
+ 'default': #bdc8ff,
+ 'green': #bdffbd,
+ 'blue': #bdc8ff,
+ 'pink': #ffc2e5,
+ 'yellow': #fff5c2,
+ 'orange': #ffe8c2,
+ 'red': #ffc2c2,
+ 'purple': #fbd1ff,
+ 'grey': #ebebeb,
+);
+
+$alert-major-header-colors: (
+ 'default': #003061,
+ 'green': #005229,
+ 'blue': #003061,
+ 'pink': #800033,
+ 'yellow': #754900,
+ 'orange': #823208,
+ 'red': #800029,
+ 'purple': #450d8c,
+ 'grey': #800033,
+);
+
+$alert-subheader-header-colors: (
+ 'default': #6b0020,
+ 'green': #6b0020,
+ 'blue': #6b0020,
+ 'pink': #002c85,
+ 'yellow': #002c85,
+ 'orange': #002c85,
+ 'red': #002c85,
+ 'purple': #002c85,
+ 'grey': #002c85,
+);
+
+$border-width: 4;
+
+$border-width-px: $border-width * 1px;
+
+.major_announcement_title {
+ font-size: 175%;
+ padding: 0rem 0.5rem;
+ line-height: 100%;
+ text-align: left;
+ text-decoration: none;
+ width: 100%;
+}
+
+.subheader_announcement_text {
+ font-weight: bold;
+ padding: 0 0.5rem;
+ padding-top: 0.25rem;
+ line-height: 100%;
+ width: 100%;
+ height: 100%;
+ text-align: left;
+ font-size: 125%;
+}
+
+.major_announcement_text {
+ color: #131313;
+ background-color: #eaeaea;
+ font-weight: bold;
+ font-size: 100%;
+ text-align: left;
+ padding: 0.5rem 0.5rem;
+ width: 100%;
+ height: 100%;
+}
+
+.minor_announcement_title {
+ font-weight: bold;
+ padding: 0 0.5rem;
+ padding-top: 0;
+ line-height: 100%;
+ width: 100%;
+ height: 100%;
+ text-align: left;
+ font-size: 150%;
+}
+
+.minor_announcement_text {
+ background-color: #eaeaea;
+ color: #202020;
+ padding: 0.5rem 0.5rem;
+ text-align: left;
+ font-size: 100%;
+}
+
+.announcement_header {
+ padding: 0.5rem 0;
+ display: flex;
+ flex-direction: column;
+}
+
+@each $color-name, $color-value in $alert-stripe-colors {
+ .chat_alert_#{$color-name} {
+ color: #ffffff;
+ padding: 0.5rem 0.5rem;
+ box-shadow: none;
+ font-weight: bold;
+ margin: 1rem 0 1rem 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ border-image: repeating-linear-gradient(
+ -45deg,
+ map.get($alert-stripe-alternate-colors, $color-name),
+ map.get($alert-stripe-alternate-colors, $color-name) 10px,
+ $color-value 10px,
+ $color-value 20px
+ );
+ border-image-slice: $border-width fill;
+ border-width: $border-width-px;
+ border-image-width: $border-width-px;
+ border-image-outset: 0 0 0 0;
+ border-image-repeat: repeat repeat;
+ border-style: solid;
+ }
+
+ .chat_alert_#{$color-name} .major_announcement_title {
+ color: map.get($alert-major-header-colors, $color-name);
+ }
+
+ .chat_alert_#{$color-name} .minor_announcement_title {
+ color: map.get($alert-major-header-colors, $color-name);
+ }
+
+ .chat_alert_#{$color-name} .subheader_announcement_text {
+ color: map.get($alert-subheader-header-colors, $color-name);
+ }
+
+ .chat_alert_#{$color-name} .minor_announcement_text {
+ background-color: lighten(
+ map.get($alert-stripe-alternate-colors, $color-name),
+ 5
+ );
+ }
+
+ .chat_alert_#{$color-name} .major_announcement_text {
+ background-color: lighten(
+ map.get($alert-stripe-alternate-colors, $color-name),
+ 5
+ );
+ }
+}
diff --git a/tgui/packages/tgui-say/TguiSay.tsx b/tgui/packages/tgui-say/TguiSay.tsx
index b2de8cea1a9f9..267febef650af 100644
--- a/tgui/packages/tgui-say/TguiSay.tsx
+++ b/tgui/packages/tgui-say/TguiSay.tsx
@@ -4,7 +4,7 @@ import { Component, createRef, InfernoKeyboardEvent, RefObject } from 'inferno';
import { LINE_LENGTHS, RADIO_PREFIXES, WINDOW_SIZES } from './constants';
import { byondMessages } from './timers';
import { dragStartHandler } from 'tgui/drag';
-import { windowOpen, windowLoad, windowClose, windowSet } from './helpers';
+import { windowOpen, windowClose, windowSet } from './helpers';
import { BooleanLike } from 'common/react';
import { KEY } from 'common/keys';
@@ -68,7 +68,6 @@ export class TguiSay extends Component<{}, State> {
Byond.subscribeTo('props', this.handleProps);
Byond.subscribeTo('force', this.handleForceSay);
Byond.subscribeTo('open', this.handleOpen);
- windowLoad();
}
handleArrowKeys(direction: KEY.Up | KEY.Down) {
diff --git a/tgui/packages/tgui-say/helpers.ts b/tgui/packages/tgui-say/helpers.ts
index 195d71a740ad4..100e3c036682f 100644
--- a/tgui/packages/tgui-say/helpers.ts
+++ b/tgui/packages/tgui-say/helpers.ts
@@ -22,19 +22,6 @@ export const windowClose = () => {
Byond.sendMessage('close');
};
-/** Some QoL to hide the window on load. Doesn't log this event */
-export const windowLoad = () => {
- Byond.winset('tgui_say', {
- pos: '848,500',
- size: `${WINDOW_SIZES.width}x${WINDOW_SIZES.small}`,
- visible: false,
- });
-
- Byond.winset('map', {
- focus: true,
- });
-};
-
/**
* Modifies the window size.
*/
diff --git a/tgui/packages/tgui/components/LabeledList.tsx b/tgui/packages/tgui/components/LabeledList.tsx
index 283c0a2b91203..d89423aded2bf 100644
--- a/tgui/packages/tgui/components/LabeledList.tsx
+++ b/tgui/packages/tgui/components/LabeledList.tsx
@@ -8,6 +8,7 @@ import { BooleanLike, classes, pureComponentHooks } from 'common/react';
import { InfernoNode } from 'inferno';
import { Box, unit } from './Box';
import { Divider } from './Divider';
+import { Tooltip } from './Tooltip';
type LabeledListProps = {
children?: any;
@@ -20,19 +21,20 @@ export const LabeledList = (props: LabeledListProps) => {
LabeledList.defaultHooks = pureComponentHooks;
-type LabeledListItemProps = {
- className?: string | BooleanLike;
- label?: string | InfernoNode | BooleanLike;
- labelColor?: string | BooleanLike;
- labelWrap?: boolean;
- color?: string | BooleanLike;
- textAlign?: string | BooleanLike;
- buttons?: InfernoNode;
+type LabeledListItemProps = Partial<{
+ className: string | BooleanLike;
+ label: string | InfernoNode | BooleanLike;
+ labelColor: string | BooleanLike;
+ labelWrap: boolean;
+ color: string | BooleanLike;
+ textAlign: string | BooleanLike;
+ buttons: InfernoNode;
/** @deprecated */
- content?: any;
- children?: InfernoNode;
- verticalAlign?: string;
-};
+ content: any;
+ children: InfernoNode;
+ verticalAlign: string;
+ tooltip: string;
+}>;
const LabeledListItem = (props: LabeledListItemProps) => {
const {
@@ -46,20 +48,46 @@ const LabeledListItem = (props: LabeledListItemProps) => {
content,
children,
verticalAlign = 'baseline',
+ tooltip,
} = props;
+
+ let innerLabel;
+ if (label) {
+ innerLabel = label;
+ if (typeof label === 'string') innerLabel += ':';
+ }
+
+ if (tooltip !== undefined) {
+ innerLabel = (
+
+
+ {innerLabel}
+
+
+ );
+ }
+
+ let labelChild = (
+
+ {innerLabel}
+
+ );
+
return (